text: /**
   * @license
   * Copyright 2016 Google Inc.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  describe('Player', function() {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const Util = shaka.test.Util;
  
    const originalLogError = shaka.log.error;
    const originalLogWarn = shaka.log.warning;
    const originalLogAlwaysWarn = shaka.log.alwaysWarn;
    const originalIsTypeSupported = window.MediaSource.isTypeSupported;
  
    const fakeManifestUri = 'fake-manifest-uri';
  
    /** @type {!jasmine.Spy} */
    let logErrorSpy;
    /** @type {!jasmine.Spy} */
    let logWarnSpy;
    /** @type {!jasmine.Spy} */
    let onError;
    /** @type {shaka.extern.Manifest} */
    let manifest;
    /** @type {number} */
    let periodIndex;
    /** @type {!shaka.Player} */
    let player;
    /** @type {!shaka.test.FakeAbrManager} */
    let abrManager;
    /** @type {function():shaka.extern.AbrManager} */
    let abrFactory;
  
    /** @type {!shaka.test.FakeNetworkingEngine} */
    let networkingEngine;
    /** @type {!shaka.test.FakeStreamingEngine} */
    let streamingEngine;
    /** @type {!shaka.test.FakeDrmEngine} */
    let drmEngine;
    /** @type {!shaka.test.FakePlayhead} */
    let playhead;
    /** @type {!shaka.test.FakePlayheadObserver} */
    let playheadObserver;
    /** @type {!shaka.test.FakeTextDisplayer} */
    let textDisplayer;
    /** @type {function():shaka.extern.TextDisplayer} */
    let textDisplayFactory;
  
    let mediaSourceEngine;
  
    /** @type {!shaka.test.FakeVideo} */
    let video;
  
    beforeAll(function() {
      logErrorSpy = jasmine.createSpy('shaka.log.error');
      shaka.log.error = shaka.test.Util.spyFunc(logErrorSpy);
      logWarnSpy = jasmine.createSpy('shaka.log.warning');
      shaka.log.warning = shaka.test.Util.spyFunc(logWarnSpy);
      shaka.log.alwaysWarn = shaka.test.Util.spyFunc(logWarnSpy);
    });
  
    beforeEach(function() {
      // By default, errors are a failure.
      logErrorSpy.calls.reset();
      logErrorSpy.and.callFake(fail);
  
      logWarnSpy.calls.reset();
  
      // Since this is not an integration test, we don't want MediaSourceEngine to
      // fail assertions based on browser support for types.  Pretend that all
      // video and audio types are supported.
      window.MediaSource.isTypeSupported = function(mimeType) {
        let type = mimeType.split('/')[0];
        return type == 'video' || type == 'audio';
      };
  
      // Many tests assume the existence of a manifest, so create a basic one.
      // Test suites can override this with more specific manifests.
      manifest = new shaka.test.ManifestGenerator()
        .addPeriod(0)
          .addVariant(0)
            .addAudio(1)
            .addVideo(2)
        .addPeriod(1)
          .addVariant(1)
            .addAudio(3)
            .addVideo(4)
        .build();
      periodIndex = 0;
  
      abrManager = new shaka.test.FakeAbrManager();
      abrFactory = function() { return abrManager; };
  
      textDisplayer = new shaka.test.FakeTextDisplayer();
      textDisplayFactory = function() { return textDisplayer; };
  
      function dependencyInjector(player) {
        // Create a networking engine that always returns an empty buffer.
        networkingEngine = new shaka.test.FakeNetworkingEngine();
        networkingEngine.setDefaultValue(new ArrayBuffer(0));
  
        drmEngine = new shaka.test.FakeDrmEngine();
        playhead = new shaka.test.FakePlayhead();
        playheadObserver = new shaka.test.FakePlayheadObserver();
        streamingEngine = new shaka.test.FakeStreamingEngine(
            onChooseStreams, onCanSwitch);
        mediaSourceEngine = {
          init: jasmine.createSpy('init').and.returnValue(Promise.resolve()),
          open: jasmine.createSpy('open').and.returnValue(Promise.resolve()),
          destroy: jasmine.createSpy('destroy').and.
              returnValue(Promise.resolve()),
          setUseEmbeddedText: jasmine.createSpy('setUseEmbeddedText'),
          getUseEmbeddedText: jasmine.createSpy('getUseEmbeddedText'),
          setTextDisplayer: jasmine.createSpy('setTextDisplayer'),
        };
  
        player.createDrmEngine = function() { return drmEngine; };
        player.createNetworkingEngine = function() { return networkingEngine; };
        player.createPlayhead = function() { return playhead; };
        player.createPlayheadObserver = function() { return playheadObserver; };
        player.createMediaSourceEngine = function() { return mediaSourceEngine; };
        player.createStreamingEngine = function() {
          // This captures the variable |manifest| so this should only be used
          // after the manifest has been set.
          // Subtle: because this captures let manifest above, there cannot be any
          // other location for manifests in these tests.
          // TODO: fix this to use the manifest currently loaded by the player.
          let period = manifest.periods[0];
          streamingEngine.getCurrentPeriod.and.returnValue(period);
          return streamingEngine;
        };
      }
  
      video = new shaka.test.FakeVideo(20);
      player = new shaka.Player(video, dependencyInjector);
      player.configure({
        // Ensures we don't get a warning about missing preference.
        preferredAudioLanguage: 'en',
        abrFactory: abrFactory,
        textDisplayFactory: textDisplayFactory,
      });
  
      onError = jasmine.createSpy('error event');
      onError.and.callFake(function(event) {
        fail(event.detail);
      });
      player.addEventListener('error', shaka.test.Util.spyFunc(onError));
    });
  
    afterEach(function(done) {
      player.destroy().catch(fail).then(done);
    });
  
    afterAll(function() {
      shaka.log.error = originalLogError;
      shaka.log.warning = originalLogWarn;
      shaka.log.alwaysWarn = originalLogAlwaysWarn;
      window.MediaSource.isTypeSupported = originalIsTypeSupported;
    });
  
    describe('destroy', function() {
      it('cleans up all dependencies', async () => {
        goog.asserts.assert(manifest, 'Manifest should be non-null');
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
  
        await player.load(fakeManifestUri, 0, factory);
        await player.destroy();
  
        expect(abrManager.stop).toHaveBeenCalled();
        expect(networkingEngine.destroy).toHaveBeenCalled();
        expect(drmEngine.destroy).toHaveBeenCalled();
        expect(playhead.destroy).toHaveBeenCalled();
        expect(playheadObserver.destroy).toHaveBeenCalled();
        expect(mediaSourceEngine.destroy).toHaveBeenCalled();
        expect(streamingEngine.destroy).toHaveBeenCalled();
        expect(textDisplayer.destroy).toHaveBeenCalled();
      });
  
      it('destroys mediaSourceEngine before drmEngine', async () => {
        goog.asserts.assert(manifest, 'Manifest should be non-null');
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
  
        mediaSourceEngine.destroy.and.callFake(() => {
          expect(drmEngine.destroy).not.toHaveBeenCalled();
          return Util.delay(0.01).then(() => {
            expect(drmEngine.destroy).not.toHaveBeenCalled();
          });
        });
  
        await player.load(fakeManifestUri, 0, factory);
        await player.destroy();
  
        expect(mediaSourceEngine.destroy).toHaveBeenCalled();
        expect(drmEngine.destroy).toHaveBeenCalled();
      });
  
      it('destroys parser first when interrupting load', function(done) {
        let p = shaka.test.Util.delay(0.3);
        let parser = new shaka.test.FakeManifestParser(manifest);
        parser.start.and.returnValue(p);
        parser.stop.and.callFake(function() {
          expect(abrManager.stop).not.toHaveBeenCalled();
          expect(networkingEngine.destroy).not.toHaveBeenCalled();
          expect(textDisplayer.destroy).not.toHaveBeenCalled();
        });
        let factory = function() { return parser; };
  
        player.load(fakeManifestUri, 0, factory).then(fail).catch(() => {});
        shaka.test.Util.delay(0.1).then(function() {
          player.destroy().catch(fail).then(function() {
            expect(abrManager.stop).toHaveBeenCalled();
            expect(networkingEngine.destroy).toHaveBeenCalled();
            expect(textDisplayer.destroy).toHaveBeenCalled();
            expect(parser.stop).toHaveBeenCalled();
          }).then(done);
        });
      });
    });
  
    describe('load/unload', function() {
      /** @type {!shaka.test.FakeManifestParser} */
      let parser1;
      /** @type {!shaka.test.FakeManifestParser} */
      let parser2;
      /** @type {!Function} */
      let factory1;
      /** @type {!Function} */
      let factory2;
      /** @type {!jasmine.Spy} */
      let checkError;
  
      beforeEach(function() {
        goog.asserts.assert(manifest, 'manifest must be non-null');
        parser1 = new shaka.test.FakeManifestParser(manifest);
        parser2 = new shaka.test.FakeManifestParser(manifest);
        factory1 = function() { return parser1; };
        factory2 = function() { return parser2; };
  
        checkError = jasmine.createSpy('checkError');
        checkError.and.callFake(function(error) {
          expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED);
        });
      });
  
      it('won\'t start loading until unloading is done', function(done) {
        // There was a bug when calling unload before calling load would cause
        // the load to continue before the (first) unload was complete.
        // https://github.com/google/shaka-player/issues/612
        player.load(fakeManifestUri, 0, factory1).then(function() {
          // Delay the promise to destroy the parser.
          let p = new shaka.util.PublicPromise();
          parser1.stop.and.returnValue(p);
  
          let unloadDone = false;
          spyOn(player, 'createMediaSourceEngine').and.callThrough();
  
          shaka.test.Util.delay(0.5).then(function() {
            // Should not start loading yet.
            expect(player.createMediaSourceEngine).not.toHaveBeenCalled();
  
            // Unblock the unload chain.
            unloadDone = true;
            p.resolve();
          });
  
          // Explicitly unload the player first.  When load calls unload, it
          // should wait until the parser is destroyed.
          player.unload();
          player.load(fakeManifestUri, 0, factory2).then(function() {
            expect(unloadDone).toBe(true);
            done();
          });
        });
      });
  
      it('destroys TextDisplayer on unload', async () => {
        // Regression test for https://github.com/google/shaka-player/issues/984
        await player.load(fakeManifestUri, 0, factory1);
        textDisplayer.destroy.calls.reset();
        await player.unload();
        expect(textDisplayer.destroy).toHaveBeenCalled();
      });
  
      it('sets TextDisplayer and passes it to MediaSource on load', async () => {
        await player.load(fakeManifestUri, 0, factory1);
        expect(mediaSourceEngine.setTextDisplayer)
               .toHaveBeenCalledWith(textDisplayer);
      });
  
      it('handles repeated load/unload', async () => {
        await player.load(fakeManifestUri, 0, factory1);
        shaka.log.debug('finished load 1');
  
        await player.unload();
        shaka.log.debug('finished unload 1');
        expect(parser1.stop).toHaveBeenCalled();
  
        await player.load(fakeManifestUri, 0, factory2);
        shaka.log.debug('finished load 2');
  
        await player.unload();
        shaka.log.debug('finished unload 2');
        expect(parser2.stop).toHaveBeenCalled();
      });
  
      it('handles repeated loads', async () => {
        await player.load(fakeManifestUri, 0, factory1);
        await player.load(fakeManifestUri, 0, factory1);
        await player.load(fakeManifestUri, 0, factory2);
        expect(parser1.stop.calls.count()).toBe(2);
      });
  
      it('handles load interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
  
        await player.load(fakeManifestUri, 0, factory2);
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(2);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles unload interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
           .then(fail).catch(Util.spyFunc(checkError));
        player.unload().catch(fail);
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.unload().catch(fail);
  
        await player.load(fakeManifestUri, 0, factory2);
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(2);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles destroy interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
  
        await player.destroy();
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(1);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles multiple unloads interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.unload().catch(fail);
        player.unload().catch(fail);
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.unload().catch(fail);
        player.unload().catch(fail);
        player.unload().catch(fail);
  
        await player.load(fakeManifestUri, 0, factory2);
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(2);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles multiple destroys interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.destroy().catch(fail);
        player.destroy().catch(fail);
  
        await player.destroy();
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(1);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles unload, then destroy interrupting load', async () => {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.unload().catch(fail);
        player.unload().catch(fail);
  
        await player.destroy();
  
        // Delay so the interrupted calls have time to reject themselves.
        await shaka.test.Util.delay(0.5);
  
        expect(checkError.calls.count()).toBe(1);
        expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
      });
  
      it('handles destroy, then unload interrupting load', function(done) {
        player.load(fakeManifestUri, 0, factory1)
            .then(fail).catch(Util.spyFunc(checkError));
        player.destroy().catch(fail).then(function() {
          // Delay so the interrupted calls have time to reject themselves.
          return shaka.test.Util.delay(0.5);
        }).then(function() {
          expect(checkError.calls.count()).toBe(1);
          expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count());
          done();
        });
        player.unload().catch(fail);
        player.unload().catch(fail);
      });
  
      describe('streaming event', function() {
        /** @type {jasmine.Spy} */
        let streamingListener;
  
        beforeEach(function() {
          streamingListener = jasmine.createSpy('listener');
          player.addEventListener('streaming', Util.spyFunc(streamingListener));
  
          // We must have two different sets of codecs for some of our tests.
          manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0)
                .addAudio(1).mime('audio/mp4', 'mp4a.40.2')
                .addVideo(2).mime('video/mp4', 'avc1.4d401f')
              .addVariant(1)
                .addAudio(3).mime('audio/webm', 'opus')
                .addVideo(4).mime('video/webm', 'vp9')
            .build();
  
          parser1 = new shaka.test.FakeManifestParser(manifest);
        });
  
        async function runTest() {
          expect(streamingListener).not.toHaveBeenCalled();
          await player.load(fakeManifestUri, 0, factory1);
          expect(streamingListener).toHaveBeenCalled();
        }
  
        it('fires after tracks exist', async () => {
          streamingListener.and.callFake(function() {
            const tracks = player.getVariantTracks();
            expect(tracks).toBeDefined();
            expect(tracks.length).toBeGreaterThan(0);
          });
          await runTest();
        });
  
        it('fires before any tracks are active', async () => {
          streamingListener.and.callFake(function() {
            const activeTracks =
              player.getVariantTracks().filter((t) => t.active);
            expect(activeTracks.length).toEqual(0);
          });
          await runTest();
        });
  
        // We used to fire the event /before/ filtering, which meant that for
        // multi-codec content, the application might select something which will
        // later be removed during filtering.
        // https://github.com/google/shaka-player/issues/1119
        it('fires after tracks have been filtered', async () => {
          streamingListener.and.callFake(function() {
            const tracks = player.getVariantTracks();
            // Either WebM, or MP4, but not both.
            expect(tracks.length).toEqual(1);
          });
          await runTest();
        });
      });
  
      describe('setTextTrackVisibility', function() {
        beforeEach(function() {
          manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0)
                .addAudio(1)
                .addVideo(2)
              .addTextStream(3)
                .language('es').label('Spanish')
                .bandwidth(100).mime('text/vtt')
                .kind('caption')
            .build();
        });
  
        it('load text stream if caption is visible', async () => {
          await player.load(fakeManifestUri, 0, returnManifest(manifest));
          player.setTextTrackVisibility(true);
          expect(streamingEngine.loadNewTextStream).toHaveBeenCalled();
          expect(streamingEngine.getActiveText()).not.toBe(null);
        });
  
        it('does not load text stream if caption is invisible', async () => {
          await player.load(fakeManifestUri, 0, returnManifest(manifest));
          player.setTextTrackVisibility(false);
          expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled();
          expect(streamingEngine.getActiveText()).toBe(null);
        });
  
        it('loads text stream if alwaysStreamText is set', async () => {
          player.setTextTrackVisibility(false);
          player.configure({streaming: {alwaysStreamText: true}});
  
          await player.load(fakeManifestUri, 0, returnManifest(manifest));
          expect(streamingEngine.getActiveText()).not.toBe(null);
  
          player.setTextTrackVisibility(true);
          expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled();
          expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled();
  
          player.setTextTrackVisibility(false);
          expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled();
          expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled();
        });
      });
  
      describe('interruption during', function() {
        beforeEach(function() {
          checkError.and.callFake(function(error) {
            expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED);
            expect(parser1.stop.calls.count())
                .toEqual(parser1.start.calls.count());
            expect(parser2.stop.calls.count())
                .toEqual(parser2.start.calls.count());
          });
        });
  
        it('manifest type check', function(done) {
          // Block the network request.
          let p = networkingEngine.delayNextRequest();
          // Give the stage a factory so that it can succeed and get canceled.
          shaka.media.ManifestParser.registerParserByMime('undefined', factory1);
  
          player.load(fakeManifestUri, 0)
              .then(fail)
              .catch(Util.spyFunc(checkError))
              .then(function() {
                // Unregister our parser factory.
                delete shaka.media.ManifestParser.parsersByMime['undefined'];
                done();
              });
  
          shaka.test.Util.delay(0.5).then(function() {
            // Make sure we're blocked.
            const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
            networkingEngine.expectRequest(fakeManifestUri, requestType);
            // Interrupt load().
            player.unload();
            p.resolve();
          });
        });
  
        it('parser startup', function(done) {
          // Block parser startup.
          let p = new shaka.util.PublicPromise();
          parser1.start.and.returnValue(p);
  
          player.load(fakeManifestUri, 0, factory1)
              .then(fail)
              .catch(Util.spyFunc(checkError))
              .then(done);
  
          shaka.test.Util.delay(0.5).then(function() {
            // Make sure we're blocked.
            expect(parser1.start).toHaveBeenCalled();
            // Interrupt load().
            player.unload();
            p.resolve();
          });
        });
  
        it('during drm engine creation', async function() {
          // Interrupt |load| by asking the player to unload when it goes to
          // create and initialize drm engine.
          player.createDrmEngine = () => {
            // Interrupt load(). It should detect that it was asked to unload
            // which should throw the |LOAD_INTERRUPTED| error.
            player.unload();
  
            /** @type {!shaka.media.DrmEngine} */
            const drmEngine = new shaka.test.FakeDrmEngine();
            return Promise.resolve(drmEngine);
          };
  
          try {
            await player.load(fakeManifestUri, 0, factory1);
            fail();
          } catch (error) {
            expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED);
          }
        });
  
        it('during drm engine attach', async function() {
          const drmEngine = new shaka.test.FakeDrmEngine();
          drmEngine.attach.and.callFake(() => {
            // Interrupt load().
            player.unload();
          });
  
          player.createDrmEngine = () => {
            const cast = /** @type {!shaka.media.DrmEngine} */ (drmEngine);
            return Promise.resolve(cast);
          };
  
          try {
            await player.load(fakeManifestUri, 0, factory1);
            fail();
          } catch (error) {
            expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED);
          }
        });
  
        it('StreamingEngine init', function(done) {
          // Block StreamingEngine init.
          let p = new shaka.util.PublicPromise();
          streamingEngine.init.and.returnValue(p);
  
          player.load(fakeManifestUri, 0, factory1)
              .then(fail)
              .catch(Util.spyFunc(checkError))
              .then(done);
  
          shaka.test.Util.delay(1.5).then(function() {
            // Make sure we're blocked.
            expect(streamingEngine.init).toHaveBeenCalled();
            // Interrupt load().
            player.unload();
            p.resolve();
          });
        });
  
        it('StreamingEngine init w/ exception', function(done) {
          // Block StreamingEngine init.
          let p = new shaka.util.PublicPromise();
          streamingEngine.init.and.returnValue(p);
  
          player.load(fakeManifestUri, 0, factory1)
              .then(fail)
              .catch(Util.spyFunc(checkError))
              .then(done);
  
          shaka.test.Util.delay(1.5).then(function() {
            // Make sure we're blocked.
            expect(streamingEngine.init).toHaveBeenCalled();
            // Interrupt load().
            player.unload();
            // Fail StreamingEngine init with an exception.
            p.reject(new shaka.util.Error(
                shaka.util.Error.Severity.CRITICAL,
                shaka.util.Error.Category.MANIFEST,
                shaka.util.Error.Code.OPERATION_ABORTED));
          });
        });
      });  // describe('interruption during')
    });  // describe('load/unload')
  
    describe('getConfiguration', function() {
      it('returns a copy of the configuration', function() {
        let config1 = player.getConfiguration();
        config1.streaming.bufferBehind = -99;
        let config2 = player.getConfiguration();
        expect(config1.streaming.bufferBehind).not.toEqual(
            config2.streaming.bufferBehind);
      });
    });
  
    describe('configure', function() {
      it('overwrites defaults', function() {
        let defaultConfig = player.getConfiguration();
        // Make sure the default differs from our test value:
        expect(defaultConfig.drm.retryParameters.backoffFactor).not.toBe(5);
        expect(defaultConfig.manifest.retryParameters.backoffFactor).not.toBe(5);
  
        player.configure({
          drm: {
            retryParameters: {backoffFactor: 5},
          },
        });
  
        let newConfig = player.getConfiguration();
        // Make sure we changed the backoff for DRM, but not for manifests:
        expect(newConfig.drm.retryParameters.backoffFactor).toBe(5);
        expect(newConfig.manifest.retryParameters.backoffFactor).not.toBe(5);
      });
  
      it('reverts to defaults when undefined is given', function() {
        player.configure({
          streaming: {
            retryParameters: {backoffFactor: 5},
            bufferBehind: 7,
          },
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig.streaming.retryParameters.backoffFactor).toBe(5);
        expect(newConfig.streaming.bufferBehind).toBe(7);
  
        player.configure({
          streaming: {
            retryParameters: undefined,
          },
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.streaming.retryParameters.backoffFactor).not.toBe(5);
        expect(newConfig.streaming.bufferBehind).toBe(7);
  
        player.configure({streaming: undefined});
        newConfig = player.getConfiguration();
        expect(newConfig.streaming.bufferBehind).not.toBe(7);
      });
  
      it('restricts the types of config values', function() {
        logErrorSpy.and.stub();
        let defaultConfig = player.getConfiguration();
  
        // Try a bogus bufferBehind (string instead of number)
        player.configure({
          streaming: {bufferBehind: '77'},
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig).toEqual(defaultConfig);
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.streaming.bufferBehind'));
  
        // Try a bogus streaming config (number instead of Object)
        logErrorSpy.calls.reset();
        player.configure({
          streaming: 5,
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig).toEqual(defaultConfig);
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.streaming'));
      });
  
      it('expands dictionaries that allow arbitrary keys', function() {
        player.configure({
          drm: {servers: {'com.widevine.alpha': 'http://foo/widevine'}},
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({
          'com.widevine.alpha': 'http://foo/widevine',
        });
  
        player.configure({
          drm: {servers: {'com.microsoft.playready': 'http://foo/playready'}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({
          'com.widevine.alpha': 'http://foo/widevine',
          'com.microsoft.playready': 'http://foo/playready',
        });
      });
  
      it('expands dictionaries but still restricts their values', function() {
        // Try a bogus server value (number instead of string)
        logErrorSpy.and.stub();
        player.configure({
          drm: {servers: {'com.widevine.alpha': 7}},
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({});
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.drm.servers.com.widevine.alpha'));
  
        // Try a valid advanced config.
        logErrorSpy.calls.reset();
        player.configure({
          drm: {advanced: {'ks1': {distinctiveIdentifierRequired: true}}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.drm.advanced).toEqual({
          'ks1': jasmine.objectContaining({distinctiveIdentifierRequired: true}),
        });
        expect(logErrorSpy).not.toHaveBeenCalled();
        let lastGoodConfig = newConfig;
  
        // Try an invalid advanced config key.
        player.configure({
          drm: {advanced: {'ks1': {bogus: true}}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig).toEqual(lastGoodConfig);
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.drm.advanced.ks1.bogus'));
      });
  
      it('removes dictionary entries when undefined is given', function() {
        player.configure({
          drm: {
            servers: {
              'com.widevine.alpha': 'http://foo/widevine',
              'com.microsoft.playready': 'http://foo/playready',
            },
          },
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({
          'com.widevine.alpha': 'http://foo/widevine',
          'com.microsoft.playready': 'http://foo/playready',
        });
  
        player.configure({
          drm: {servers: {'com.widevine.alpha': undefined}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({
          'com.microsoft.playready': 'http://foo/playready',
        });
  
        player.configure({
          drm: {servers: undefined},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.drm.servers).toEqual({});
      });
  
      it('checks the number of arguments to functions', function() {
        let goodCustomScheme = function(node) {};
        let badCustomScheme1 = function() {};  // too few args
        let badCustomScheme2 = function(x, y) {};  // too many args
  
        // Takes good callback.
        player.configure({
          manifest: {dash: {customScheme: goodCustomScheme}},
        });
  
        let newConfig = player.getConfiguration();
        expect(newConfig.manifest.dash.customScheme).toBe(goodCustomScheme);
        expect(logWarnSpy).not.toHaveBeenCalled();
  
        // Warns about bad callback #1, still takes it.
        logWarnSpy.calls.reset();
        player.configure({
          manifest: {dash: {customScheme: badCustomScheme1}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.manifest.dash.customScheme).toBe(badCustomScheme1);
        expect(logWarnSpy).toHaveBeenCalledWith(
            stringContaining('.manifest.dash.customScheme'));
  
        // Warns about bad callback #2, still takes it.
        logWarnSpy.calls.reset();
        player.configure({
          manifest: {dash: {customScheme: badCustomScheme2}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.manifest.dash.customScheme).toBe(badCustomScheme2);
        expect(logWarnSpy).toHaveBeenCalledWith(
            stringContaining('.manifest.dash.customScheme'));
  
        // Resets to default if undefined.
        logWarnSpy.calls.reset();
        player.configure({
          manifest: {dash: {customScheme: undefined}},
        });
  
        newConfig = player.getConfiguration();
        expect(newConfig.manifest.dash.customScheme).not.toBe(badCustomScheme2);
        expect(logWarnSpy).not.toHaveBeenCalled();
      });
  
      // Regression test for https://github.com/google/shaka-player/issues/784
      it('does not throw when overwriting serverCertificate', function() {
        player.configure({
          drm: {
            advanced: {
              'com.widevine.alpha': {
                serverCertificate: new Uint8Array(1),
              },
            },
          },
        });
  
        player.configure({
          drm: {
            advanced: {
              'com.widevine.alpha': {
                serverCertificate: new Uint8Array(2),
              },
            },
          },
        });
      });
  
      it('checks the type of serverCertificate', function() {
        logErrorSpy.and.stub();
  
        player.configure({
          drm: {
            advanced: {
              'com.widevine.alpha': {
                serverCertificate: null,
              },
            },
          },
        });
  
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.serverCertificate'));
  
        logErrorSpy.calls.reset();
        player.configure({
          drm: {
            advanced: {
              'com.widevine.alpha': {
                serverCertificate: 'foobar',
              },
            },
          },
        });
  
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.serverCertificate'));
      });
  
      it('does not throw when null appears instead of an object', function() {
        logErrorSpy.and.stub();
  
        player.configure({
          drm: {advanced: null},
        });
  
        expect(logErrorSpy).toHaveBeenCalledWith(
            stringContaining('.drm.advanced'));
      });
  
      it('configures play and seek range for VOD', async () => {
        player.configure({playRangeStart: 5, playRangeEnd: 10});
        let timeline = new shaka.media.PresentationTimeline(300, 0);
        timeline.setStatic(true);
        manifest = new shaka.test.ManifestGenerator()
            .setTimeline(timeline)
            .addPeriod(0)
              .addVariant(0)
                .addVideo(1)
            .build();
        goog.asserts.assert(manifest, 'manifest must be non-null');
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        await player.load(fakeManifestUri, 0, factory);
        let seekRange = player.seekRange();
        expect(seekRange.start).toBe(5);
        expect(seekRange.end).toBe(10);
      });
  
      it('does not switch for plain configuration changes', async () => {
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
  
        let switchVariantSpy = spyOn(player, 'switchVariant_');
  
        await player.load(fakeManifestUri, 0, factory);
  
        player.configure({abr: {enabled: false}});
        player.configure({streaming: {bufferingGoal: 9001}});
  
        // Delay to ensure that the switch would have been called.
        await shaka.test.Util.delay(0.1);
  
        expect(switchVariantSpy).not.toHaveBeenCalled();
      });
  
      it('accepts parameters in a (fieldName, value) format', function() {
        let oldConfig = player.getConfiguration();
        let oldDelayLicense = oldConfig.drm.delayLicenseRequestUntilPlayed;
        let oldSwitchInterval = oldConfig.abr.switchInterval;
        let oldPreferredLang = oldConfig.preferredAudioLanguage;
  
        expect(oldDelayLicense).toBe(false);
        expect(oldSwitchInterval).toBe(8);
        expect(oldPreferredLang).toBe('en');
  
        player.configure('drm.delayLicenseRequestUntilPlayed', true);
        player.configure('abr.switchInterval', 10);
        player.configure('preferredAudioLanguage', 'fr');
  
        let newConfig = player.getConfiguration();
        let newDelayLicense = newConfig.drm.delayLicenseRequestUntilPlayed;
        let newSwitchInterval = newConfig.abr.switchInterval;
        let newPreferredLang = newConfig.preferredAudioLanguage;
  
        expect(newDelayLicense).toBe(true);
        expect(newSwitchInterval).toBe(10);
        expect(newPreferredLang).toBe('fr');
      });
  
      it('accepts escaped "." in names',
         /** @suppress {accessControls} */ (function() {
           expect(player.convertToConfigObject_('foo', 1)).toEqual({foo: 1});
           expect(player.convertToConfigObject_('foo.bar', 1))
               .toEqual({foo: {bar: 1}});
           expect(player.convertToConfigObject_('foo..bar', 1))
               .toEqual({foo: {'': {bar: 1}}});
           expect(player.convertToConfigObject_('foo.bar.baz', 1))
               .toEqual({foo: {bar: {baz: 1}}});
           expect(player.convertToConfigObject_('foo.bar\\.baz', 1))
               .toEqual({foo: {'bar.baz': 1}});
           expect(player.convertToConfigObject_('foo.baz.', 1))
               .toEqual({foo: {baz: {'': 1}}});
           expect(player.convertToConfigObject_('foo.baz\\.', 1))
               .toEqual({foo: {'baz.': 1}});
           expect(player.convertToConfigObject_('foo\\.bar', 1))
               .toEqual({'foo.bar': 1});
           expect(player.convertToConfigObject_('.foo', 1))
               .toEqual({'': {foo: 1}});
           expect(player.convertToConfigObject_('\\.foo', 1))
               .toEqual({'.foo': 1});
         }));
  
      it('returns whether the config was valid', function() {
        logErrorSpy.and.stub();
        expect(player.configure({streaming: {bufferBehind: '77'}})).toBe(false);
        expect(player.configure({streaming: {bufferBehind: 77}})).toBe(true);
      });
  
      it('still sets other fields when there are errors', function() {
        logErrorSpy.and.stub();
  
        let changes = {
          manifest: {foobar: false},
          streaming: {bufferBehind: 77},
        };
        expect(player.configure(changes)).toBe(false);
  
        let newConfig = player.getConfiguration();
        expect(newConfig.streaming.bufferBehind).toEqual(77);
      });
  
      // https://github.com/google/shaka-player/issues/1524
      it('does not pollute other advanced DRM configs', () => {
        player.configure('drm.advanced.foo', {});
        player.configure('drm.advanced.bar', {});
        const fooConfig1 = player.getConfiguration().drm.advanced.foo;
        const barConfig1 = player.getConfiguration().drm.advanced.bar;
        expect(fooConfig1.distinctiveIdentifierRequired).toEqual(false);
        expect(barConfig1.distinctiveIdentifierRequired).toEqual(false);
  
        player.configure('drm.advanced.foo.distinctiveIdentifierRequired', true);
        const fooConfig2 = player.getConfiguration().drm.advanced.foo;
        const barConfig2 = player.getConfiguration().drm.advanced.bar;
        expect(fooConfig2.distinctiveIdentifierRequired).toEqual(true);
        expect(barConfig2.distinctiveIdentifierRequired).toEqual(false);
      });
    });
  
    describe('resetConfiguration', function() {
      it('resets configurations to default', () => {
        const default_ = player.getConfiguration().streaming.bufferingGoal;
        expect(default_).not.toBe(100);
        player.configure('streaming.bufferingGoal', 100);
        expect(player.getConfiguration().streaming.bufferingGoal).toBe(100);
        player.resetConfiguration();
        expect(player.getConfiguration().streaming.bufferingGoal).toBe(default_);
      });
  
      it('resets the arbitrary keys', () => {
        player.configure('drm.servers.org\\.w3\\.clearKey', 'http://foo.com');
        expect(player.getConfiguration().drm.servers).toEqual({
          'org.w3.clearKey': 'http://foo.com',
        });
        player.resetConfiguration();
        expect(player.getConfiguration().drm.servers).toEqual({});
      });
  
      it('keeps shared configuration the same', () => {
        const config = player.getSharedConfiguration();
        player.resetConfiguration();
        expect(player.getSharedConfiguration()).toBe(config);
      });
    });
  
    describe('AbrManager', function() {
      /** @type {!shaka.test.FakeManifestParser} */
      let parser;
      /** @type {!Function} */
      let parserFactory;
  
      beforeEach(function() {
        goog.asserts.assert(manifest, 'manifest must be non-null');
        parser = new shaka.test.FakeManifestParser(manifest);
        parserFactory = function() { return parser; };
      });
  
      it('sets through load', async () => {
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(abrManager.init).toHaveBeenCalled();
      });
  
      it('calls chooseVariant', async () => {
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(abrManager.chooseVariant).toHaveBeenCalled();
      });
  
      it('does not enable before stream startup', async () => {
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(abrManager.enable).not.toHaveBeenCalled();
        streamingEngine.onCanSwitch();
        expect(abrManager.enable).toHaveBeenCalled();
      });
  
      it('does not enable if adaptation is disabled', async () => {
        player.configure({abr: {enabled: false}});
        await player.load(fakeManifestUri, 0, parserFactory);
        streamingEngine.onCanSwitch();
        expect(abrManager.enable).not.toHaveBeenCalled();
      });
  
      it('enables/disables though configure', async () => {
        await player.load(fakeManifestUri, 0, parserFactory);
        streamingEngine.onCanSwitch();
        abrManager.enable.calls.reset();
        abrManager.disable.calls.reset();
  
        player.configure({abr: {enabled: false}});
        expect(abrManager.disable).toHaveBeenCalled();
  
        player.configure({abr: {enabled: true}});
        expect(abrManager.enable).toHaveBeenCalled();
      });
  
      it('waits to enable if in-between Periods', async () => {
        player.configure({abr: {enabled: false}});
        await player.load(fakeManifestUri, 0, parserFactory);
        player.configure({abr: {enabled: true}});
        expect(abrManager.enable).not.toHaveBeenCalled();
        // Until onCanSwitch is called, the first period hasn't been set up yet.
        streamingEngine.onCanSwitch();
        expect(abrManager.enable).toHaveBeenCalled();
      });
  
      it('reuses AbrManager instance', async () => {
        /** @type {!jasmine.Spy} */
        const spy =
            jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager);
        player.configure({abrFactory: spy});
  
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(spy).toHaveBeenCalled();
        spy.calls.reset();
  
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(spy).not.toHaveBeenCalled();
      });
  
      it('creates new AbrManager if factory changes', async () => {
        /** @type {!jasmine.Spy} */
        const spy1 =
            jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager);
        /** @type {!jasmine.Spy} */
        const spy2 =
            jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager);
        player.configure({abrFactory: spy1});
  
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(spy1).toHaveBeenCalled();
        expect(spy2).not.toHaveBeenCalled();
        spy1.calls.reset();
  
        player.configure({abrFactory: spy2});
        await player.load(fakeManifestUri, 0, parserFactory);
        expect(spy1).not.toHaveBeenCalled();
        expect(spy2).toHaveBeenCalled();
      });
    });
  
    describe('filterTracks', function() {
      it('retains only video+audio variants if they exist', function(done) {
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(10)
              .addAudio(1)
            .addVariant(11)
              .addAudio(2)
              .addVideo(3)
            .addVariant(12)
              .addVideo(4)
          .addPeriod(1)
            .addVariant(20)
              .addAudio(5)
            .addVariant(21)
              .addVideo(6)
            .addVariant(22)
              .addAudio(7)
              .addVideo(8)
          .build();
  
        let variantTracks1 = [
          jasmine.objectContaining({
            id: 11,
            active: true,
            type: 'variant',
          }),
        ];
        let variantTracks2 = [
          jasmine.objectContaining({
            id: 22,
            active: false,
            type: 'variant',
          }),
        ];
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
        player.load(fakeManifestUri, 0, parserFactory).catch(fail).then(() => {
          // Check the first period's variant tracks.
          let actualVariantTracks1 = player.getVariantTracks();
          expect(actualVariantTracks1).toEqual(variantTracks1);
  
          // Check the second period's variant tracks.
          playhead.getTime.and.callFake(function() {
            return 100;
          });
          let actualVariantTracks2 = player.getVariantTracks();
          expect(actualVariantTracks2).toEqual(variantTracks2);
        }).then(done);
      });
    });
  
    describe('tracks', function() {
      /** @type {!Array.<shaka.extern.Track>} */
      let variantTracks;
      /** @type {!Array.<shaka.extern.Track>} */
      let textTracks;
  
      beforeEach(async () => {
        // A manifest we can use to test track expectations.
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(100)  // main surround, low res
              .bandwidth(1300)
              .language('en')
              .addVideo(1).originalId('video-1kbps').bandwidth(1000)
                .size(100, 200).frameRate(1000000 / 42000)
              .addAudio(3).originalId('audio-en-6c').bandwidth(300)
                .channelsCount(6).roles(['main'])
  
            .addVariant(101)  // main surround, high res
              .bandwidth(2300)
              .language('en')
              .addVideo(2).originalId('video-2kbps').bandwidth(2000)
                .size(200, 400).frameRate(24)
              .addExistingStream(3)  // audio
  
            .addVariant(102)  // main stereo, low res
              .bandwidth(1100)
              .language('en')
              .addExistingStream(1)  // video
              .addAudio(4).originalId('audio-en-2c').bandwidth(100)
                .channelsCount(2).roles(['main'])
  
            .addVariant(103)  // main stereo, high res
              .bandwidth(2100)
              .language('en')
              .addExistingStream(2)  // video
              .addExistingStream(4)  // audio
  
            .addVariant(104)  // commentary stereo, low res
              .bandwidth(1100)
              .language('en')
              .addExistingStream(1)  // video
              .addAudio(5).originalId('audio-commentary').bandwidth(100)
                .channelsCount(2).roles(['commentary'])
  
            .addVariant(105)  // commentary stereo, low res
              .bandwidth(2100)
              .language('en')
              .addExistingStream(2)  // video
              .addExistingStream(5)  // audio
  
            .addVariant(106)  // spanish stereo, low res
              .language('es')
              .bandwidth(1100)
              .addExistingStream(1)  // video
              .addAudio(6).originalId('audio-es').bandwidth(100)
                .channelsCount(2)
  
            .addVariant(107)  // spanish stereo, high res
              .language('es')
              .bandwidth(2100)
              .addExistingStream(2)  // video
              .addExistingStream(6)  // audio
  
            // All text tracks should remain, even with different MIME types.
            .addTextStream(50).originalId('text-es')
              .language('es').label('Spanish')
              .bandwidth(10).mime('text/vtt')
              .kind('caption')
            .addTextStream(51).originalId('text-en')
              .language('en').label('English')
              .bandwidth(10).mime('application/ttml+xml')
              .kind('caption').roles(['main'])
             .addTextStream(52).originalId('text-commentary')
              .language('en').label('English')
              .bandwidth(10).mime('application/ttml+xml')
              .kind('caption').roles(['commentary'])
          .addPeriod(1)
            .addVariant(200)
              .bandwidth(1100)
              .language('en')
              .addVideo(10).bandwidth(1000).size(100, 200)
              .addAudio(11).bandwidth(100).channelsCount(2)
            .addVariant(201)
              .bandwidth(1300)
              .language('en')
              .addExistingStream(10)  // video
              .addAudio(12).bandwidth(300).channelsCount(6)
          .build();
  
        variantTracks = [
          {
            id: 100,
            active: true,
            type: 'variant',
            bandwidth: 1300,
            language: 'en',
            label: null,
            kind: null,
            width: 100,
            height: 200,
            frameRate: 1000000 / 42000,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['main'],
            videoId: 1,
            audioId: 3,
            channelsCount: 6,
            audioBandwidth: 300,
            videoBandwidth: 1000,
            originalAudioId: 'audio-en-6c',
            originalVideoId: 'video-1kbps',
            originalTextId: null,
          },
          {
            id: 101,
            active: false,
            type: 'variant',
            bandwidth: 2300,
            language: 'en',
            label: null,
            kind: null,
            width: 200,
            height: 400,
            frameRate: 24,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['main'],
            videoId: 2,
            audioId: 3,
            channelsCount: 6,
            audioBandwidth: 300,
            videoBandwidth: 2000,
            originalAudioId: 'audio-en-6c',
            originalVideoId: 'video-2kbps',
            originalTextId: null,
          },
          {
            id: 102,
            active: false,
            type: 'variant',
            bandwidth: 1100,
            language: 'en',
            label: null,
            kind: null,
            width: 100,
            height: 200,
            frameRate: 1000000 / 42000,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['main'],
            videoId: 1,
            audioId: 4,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 1000,
            originalAudioId: 'audio-en-2c',
            originalVideoId: 'video-1kbps',
            originalTextId: null,
          },
          {
            id: 103,
            active: false,
            type: 'variant',
            bandwidth: 2100,
            language: 'en',
            label: null,
            kind: null,
            width: 200,
            height: 400,
            frameRate: 24,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['main'],
            videoId: 2,
            audioId: 4,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 2000,
            originalAudioId: 'audio-en-2c',
            originalVideoId: 'video-2kbps',
            originalTextId: null,
          },
          {
            id: 104,
            active: false,
            type: 'variant',
            bandwidth: 1100,
            language: 'en',
            label: null,
            kind: null,
            width: 100,
            height: 200,
            frameRate: 1000000 / 42000,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['commentary'],
            videoId: 1,
            audioId: 5,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 1000,
            originalAudioId: 'audio-commentary',
            originalVideoId: 'video-1kbps',
            originalTextId: null,
          },
          {
            id: 105,
            active: false,
            type: 'variant',
            bandwidth: 2100,
            language: 'en',
            label: null,
            kind: null,
            width: 200,
            height: 400,
            frameRate: 24,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: ['commentary'],
            videoId: 2,
            audioId: 5,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 2000,
            originalAudioId: 'audio-commentary',
            originalVideoId: 'video-2kbps',
            originalTextId: null,
          },
          {
            id: 106,
            active: false,
            type: 'variant',
            bandwidth: 1100,
            language: 'es',
            label: null,
            kind: null,
            width: 100,
            height: 200,
            frameRate: 1000000 / 42000,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: [],
            videoId: 1,
            audioId: 6,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 1000,
            originalAudioId: 'audio-es',
            originalVideoId: 'video-1kbps',
            originalTextId: null,
          },
          {
            id: 107,
            active: false,
            type: 'variant',
            bandwidth: 2100,
            language: 'es',
            label: null,
            kind: null,
            width: 200,
            height: 400,
            frameRate: 24,
            mimeType: 'video/mp4',
            codecs: 'avc1.4d401f, mp4a.40.2',
            audioCodec: 'mp4a.40.2',
            videoCodec: 'avc1.4d401f',
            primary: false,
            roles: [],
            videoId: 2,
            audioId: 6,
            channelsCount: 2,
            audioBandwidth: 100,
            videoBandwidth: 2000,
            originalAudioId: 'audio-es',
            originalVideoId: 'video-2kbps',
            originalTextId: null,
          },
        ];
  
        textTracks = [
          {
            id: 50,
            active: true,
            type: ContentType.TEXT,
            language: 'es',
            label: 'Spanish',
            kind: 'caption',
            mimeType: 'text/vtt',
            codecs: null,
            audioCodec: null,
            videoCodec: null,
            primary: false,
            roles: [],
            channelsCount: null,
            audioBandwidth: null,
            videoBandwidth: null,
            bandwidth: 0,
            width: null,
            height: null,
            frameRate: null,
            videoId: null,
            audioId: null,
            originalAudioId: null,
            originalVideoId: null,
            originalTextId: 'text-es',
          },
          {
            id: 51,
            active: false,
            type: ContentType.TEXT,
            language: 'en',
            label: 'English',
            kind: 'caption',
            mimeType: 'application/ttml+xml',
            codecs: null,
            audioCodec: null,
            videoCodec: null,
            primary: false,
            roles: ['main'],
            channelsCount: null,
            audioBandwidth: null,
            videoBandwidth: null,
            bandwidth: 0,
            width: null,
            height: null,
            frameRate: null,
            videoId: null,
            audioId: null,
            originalAudioId: null,
            originalVideoId: null,
            originalTextId: 'text-en',
          },
          {
            id: 52,
            active: false,
            type: ContentType.TEXT,
            language: 'en',
            label: 'English',
            kind: 'caption',
            mimeType: 'application/ttml+xml',
            codecs: null,
            audioCodec: null,
            videoCodec: null,
            primary: false,
            roles: ['commentary'],
            channelsCount: null,
            audioBandwidth: null,
            videoBandwidth: null,
            bandwidth: 0,
            width: null,
            height: null,
            frameRate: null,
            videoId: null,
            audioId: null,
            originalAudioId: null,
            originalVideoId: null,
            originalTextId: 'text-commentary',
          },
        ];
  
        goog.asserts.assert(manifest, 'manifest must be non-null');
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
  
        // Language/channel prefs must be set before load.  Used in
        // select*Language() tests.
        player.configure({
          preferredAudioLanguage: 'en',
          preferredTextLanguage: 'es',
          preferredAudioChannelCount: 6,
        });
  
        await player.load(fakeManifestUri, 0, parserFactory);
      });
  
      it('returns the correct tracks', function() {
        streamingEngine.onCanSwitch();
  
        expect(player.getVariantTracks()).toEqual(variantTracks);
        expect(player.getTextTracks()).toEqual(textTracks);
      });
  
      it('returns empty arrays before tracks can be determined', async () => {
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
        parser.start.and.callFake(function(manifestUri, playerInterface) {
          // The player does not yet have a manifest.
          expect(player.getVariantTracks()).toEqual([]);
          expect(player.getTextTracks()).toEqual([]);
  
          parser.playerInterface = playerInterface;
          return Promise.resolve(manifest);
        });
        drmEngine.initForPlayback.and.callFake(() => {
          // The player does not yet have a playhead.
          expect(player.getVariantTracks()).toEqual([]);
          expect(player.getTextTracks()).toEqual([]);
  
          return Promise.resolve();
        });
  
        await player.load(fakeManifestUri, 0, parserFactory);
  
        // Make sure the interruptions didn't mess up the tracks.
        streamingEngine.onCanSwitch();
        expect(player.getVariantTracks()).toEqual(variantTracks);
        expect(player.getTextTracks()).toEqual(textTracks);
      });
  
      it('doesn\'t disable AbrManager if switching variants', function() {
        streamingEngine.onCanSwitch();
  
        let config = player.getConfiguration();
        expect(config.abr.enabled).toBe(true);
  
        const newTrack = player.getVariantTracks().filter((t) => !t.active)[0];
        player.selectVariantTrack(newTrack);
  
        config = player.getConfiguration();
        expect(config.abr.enabled).toBe(true);
      });
  
      it('doesn\'t disable AbrManager if switching text', function() {
        streamingEngine.onCanSwitch();
  
        let config = player.getConfiguration();
        expect(config.abr.enabled).toBe(true);
  
        const newTrack = player.getTextTracks().filter((t) => !t.active)[0];
        player.selectTextTrack(newTrack);
  
        config = player.getConfiguration();
        expect(config.abr.enabled).toBe(true);
      });
  
      it('switches streams', function() {
        streamingEngine.onCanSwitch();
  
        const newTrack = player.getVariantTracks().filter((t) => !t.active)[0];
        player.selectVariantTrack(newTrack);
  
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const variant = streamingEngine.switchVariant.calls.argsFor(0)[0];
        expect(variant.id).toEqual(newTrack.id);
      });
  
      it('still switches streams if called during startup', function() {
        // startup is not complete until onCanSwitch is called.
  
        // pick a track
        const newTrack = player.getVariantTracks().filter((t) => !t.active)[0];
        // ask the player to switch to it
        player.selectVariantTrack(newTrack);
        // nothing happens yet
        expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
  
        // after startup is complete, the manual selection takes effect.
        streamingEngine.onCanSwitch();
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const variant = streamingEngine.switchVariant.calls.argsFor(0)[0];
        expect(variant.id).toEqual(newTrack.id);
      });
  
      it('still switches streams if called while switching Periods', function() {
        // startup is complete after onCanSwitch.
        streamingEngine.onCanSwitch();
  
        // startup doesn't call switchVariant
        expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
  
        // pick a track
        const newTrack = player.getVariantTracks().filter((t) => !t.active)[0];
  
        // simulate the transition to period 1
        transitionPeriod(1);
  
        // select the new track (from period 0, which is fine)
        player.selectVariantTrack(newTrack);
        expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
  
        // after transition is completed by onCanSwitch, switchVariant is called
        streamingEngine.onCanSwitch();
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const variant = streamingEngine.switchVariant.calls.argsFor(0)[0];
        expect(variant.id).toEqual(newTrack.id);
      });
  
      it('switching audio doesn\'t change selected text track', function() {
        streamingEngine.onCanSwitch();
        player.configure({
          preferredTextLanguage: 'es',
        });
  
        // We will manually switch from Spanish to English.
        const englishTextTrack =
            player.getTextTracks().filter((t) => t.language == 'en')[0];
  
        streamingEngine.switchTextStream.calls.reset();
        player.selectTextTrack(englishTextTrack);
        expect(streamingEngine.switchTextStream).toHaveBeenCalled();
        // We have selected an English text track explicitly.
        expect(getActiveTextTrack().id).toBe(englishTextTrack.id);
  
        const newVariantTrack =
            player.getVariantTracks().filter((t) => !t.active)[0];
        player.selectVariantTrack(newVariantTrack);
  
        // The active text track has not changed, even though the text language
        // preference is Spanish.
        expect(getActiveTextTrack().id).toBe(englishTextTrack.id);
      });
  
      it('selectAudioLanguage() takes precedence over ' +
         'preferredAudioLanguage', function() {
        streamingEngine.onCanSwitch();
  
        // This preference is set in beforeEach, before load().
        expect(player.getConfiguration().preferredAudioLanguage).toBe('en');
        expect(getActiveVariantTrack().language).toBe('en');
  
        streamingEngine.switchVariant.calls.reset();
        player.selectAudioLanguage('es');
  
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const args = streamingEngine.switchVariant.calls.argsFor(0);
        expect(args[0].language).toBe('es');
        expect(args[1]).toBe(true);
        expect(getActiveVariantTrack().language).toBe('es');
      });
  
      it('selectAudioLanguage() respects selected role', function() {
        streamingEngine.onCanSwitch();
        expect(getActiveVariantTrack().roles).not.toContain('commentary');
  
        streamingEngine.switchVariant.calls.reset();
        player.selectAudioLanguage('en', 'commentary');
  
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const args = streamingEngine.switchVariant.calls.argsFor(0);
        expect(args[0].audio.roles).toContain('commentary');
        expect(args[1]).toBe(true);
        expect(getActiveVariantTrack().roles).toContain('commentary');
      });
  
      it('selectAudioLanguage() does not change selected text track', function() {
        // This came up in a custom application that allows to select
        // from all tracks regardless of selected language.
        // We imitate this behavior by calling selectTextLanguage()
        // with one language and then selecting a track in a different
        // language.
        player.selectTextLanguage('en');
        const spanishTextTrack = textTracks.filter((t) => t.language == 'es')[0];
        player.selectTextTrack(spanishTextTrack);
        player.selectAudioLanguage('es');
        expect(getActiveTextTrack().id).toBe(spanishTextTrack.id);
      });
  
      it('selectTextLanguage() does not change selected variant track', () => {
        // This came up in a custom application that allows to select
        // from all tracks regardless of selected language.
        // We imitate this behavior by calling selectAudioLanguage()
        // with one language and then selecting a track in a different
        // language.
        player.selectAudioLanguage('es');
        const englishVariantTrack =
            variantTracks.filter((t) => t.language == 'en')[0];
        player.selectVariantTrack(englishVariantTrack);
        player.selectTextLanguage('es');
        expect(getActiveVariantTrack().id).toBe(englishVariantTrack.id);
      });
  
      it('selectTextLanguage() takes precedence over ' +
         'preferredTextLanguage', function() {
        streamingEngine.onCanSwitch();
  
        // This preference is set in beforeEach, before load().
        expect(player.getConfiguration().preferredTextLanguage).toBe('es');
        expect(getActiveTextTrack().language).toBe('es');
  
        streamingEngine.switchTextStream.calls.reset();
        player.selectTextLanguage('en');
  
        expect(streamingEngine.switchTextStream).toHaveBeenCalled();
        const args = streamingEngine.switchTextStream.calls.argsFor(0);
        expect(args[0].language).toBe('en');
        expect(getActiveTextTrack().language).toBe('en');
      });
  
      it('selectTextLanguage() respects selected role', function() {
        streamingEngine.onCanSwitch();
        expect(getActiveTextTrack().roles).not.toContain('commentary');
  
        streamingEngine.switchTextStream.calls.reset();
        player.selectTextLanguage('en', 'commentary');
  
        expect(streamingEngine.switchTextStream).toHaveBeenCalled();
        const args = streamingEngine.switchTextStream.calls.argsFor(0);
        expect(args[0].roles).toContain('commentary');
        expect(getActiveTextTrack().roles).toContain('commentary');
      });
  
      it('changing current audio language changes active stream', function() {
        streamingEngine.onCanSwitch();
  
        expect(getActiveVariantTrack().language).not.toBe('es');
        expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
        player.selectAudioLanguage('es');
  
        expect(streamingEngine.switchVariant).toHaveBeenCalled();
        const args = streamingEngine.switchVariant.calls.argsFor(0);
        expect(args[0].language).toBe('es');
        expect(args[1]).toBe(true);
        expect(getActiveVariantTrack().language).toBe('es');
      });
  
      it('changing current text language changes active stream', function() {
        streamingEngine.onCanSwitch();
  
        expect(getActiveTextTrack().language).not.toBe('en');
        expect(streamingEngine.switchTextStream).not.toHaveBeenCalled();
        player.selectTextLanguage('en');
  
        expect(streamingEngine.switchTextStream).toHaveBeenCalled();
        const args = streamingEngine.switchTextStream.calls.argsFor(0);
        expect(args[0].language).toBe('en');
        expect(getActiveTextTrack().language).toBe('en');
      });
  
      it('remembers the channel count when ABR is reenabled', () => {
        streamingEngine.onCanSwitch();
  
        // We prefer 6 channels, and we are currently playing 6 channels.
        expect(player.getConfiguration().preferredAudioChannelCount).toBe(6);
        expect(getActiveVariantTrack().channelsCount).toBe(6);
  
        // Manually turn off ABR and select a 2-channel track.
        player.configure({abr: {enabled: false}});
        const newTrack =
            player.getVariantTracks().filter((t) => t.channelsCount == 2)[0];
        player.selectVariantTrack(newTrack);
  
        // See that we are playing a 2-channel track now.
        expect(getActiveVariantTrack().channelsCount).toBe(2);
  
        // See that AbrManager has a list of 2-channel tracks now.
        expect(abrManager.variants.length).toBeGreaterThan(0);
        abrManager.variants.forEach((v) => {
          expect(v.audio.channelsCount).toBe(2);
        });
  
        // Re-enable ABR.
        player.configure({abr: {enabled: true}});
  
        // See that AbrManager still has a list of 2-channel tracks.
        expect(abrManager.variants.length).toBeGreaterThan(0);
        abrManager.variants.forEach((v) => {
          expect(v.audio.channelsCount).toBe(2);
        });
        // See that we are still playing a 2-channel track.
        expect(getActiveVariantTrack().channelsCount).toBe(2);
      });
  
      it('remembers the channel count across key status changes', () => {
        // Simulate an encrypted stream.  Mark half of the audio streams with key
        // ID 'aaa', and the other half with 'bbb'.  Remove all roles, so that our
        // choices are limited only by channel count and key status.
        manifest.periods[0].variants.forEach((variant) => {
          const keyId = (variant.audio.id % 2) ? 'aaa' : 'bbb';
          variant.audio.keyId = keyId;
          variant.video.roles = [];
          variant.audio.roles = [];
        });
  
        streamingEngine.onCanSwitch();
  
        // We prefer 6 channels, and we are currently playing 6 channels.
        expect(player.getConfiguration().preferredAudioChannelCount).toBe(6);
        expect(getActiveVariantTrack().channelsCount).toBe(6);
  
        // Manually select a 2-channel track.
        const newTrack =
            player.getVariantTracks().filter((t) => t.channelsCount == 2)[0];
        player.selectVariantTrack(newTrack);
  
        // See that we are playing a 2-channel track now.
        expect(getActiveVariantTrack().channelsCount).toBe(2);
  
        // See that AbrManager has a list of 2-channel tracks now.
        expect(abrManager.variants.length).toBeGreaterThan(0);
        abrManager.variants.forEach((v) => {
          expect(v.audio.channelsCount).toBe(2);
        });
  
        // Simulate a key status event that would trigger the removal of some
        // tracks.
        onKeyStatus({
          'aaa': 'usable',
          'bbb': 'output-restricted',
        });
  
        // See that AbrManager still has a list of 2-channel tracks.
        expect(abrManager.variants.length).toBeGreaterThan(0);
        abrManager.variants.forEach((v) => {
          expect(v.audio.channelsCount).toBe(2);
        });
        // See that we are still playing a 2-channel track.
        expect(getActiveVariantTrack().channelsCount).toBe(2);
      });
    });  // describe('tracks')
  
    describe('languages', function() {
      it('chooses the first as default', async () => {
        await runTest(['en', 'es'], 'pt', 0);
      });
  
      it('chooses the primary track', async () => {
        await runTest(['en', 'es', '*fr'], 'pt', 2);
      });
  
      it('chooses exact match for main language', async () => {
        await runTest(['en', 'pt', 'pt-BR'], 'pt', 1);
      });
  
      it('chooses exact match for subtags', async () => {
        await runTest(['en', 'pt', 'pt-BR'], 'PT-BR', 2);
      });
  
      it('chooses base language if exact does not exist', async () => {
        await runTest(['en', 'es', 'pt'], 'pt-BR', 2);
      });
  
      it('chooses other subtags if base language does not exist', async () => {
        await runTest(['en', 'es', 'pt-BR'], 'pt-PT', 2);
      });
  
      it('enables text if its language differs from audio at start', async () => {
        // A manifest we can use to test text visibility.
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(0).language('pt').addAudio(0)
            .addVariant(1).language('en').addAudio(1)
            .addTextStream(2).language('pt')
            .addTextStream(3).language('fr')
         .build();
  
        player.configure({
          preferredAudioLanguage: 'en',
          preferredTextLanguage: 'fr',
        });
  
        expect(player.isTextTrackVisible()).toBe(false);
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        await player.load(fakeManifestUri, 0, factory);
  
        // Text was turned on during startup.
        expect(player.isTextTrackVisible()).toBe(true);
  
        // Turn text back off.
        player.setTextTrackVisibility(false);
        expect(player.isTextTrackVisible()).toBe(false);
  
        // Change text languages after startup.
        player.selectTextLanguage('pt');
  
        // This should not turn text back on.
        expect(player.isTextTrackVisible()).toBe(false);
      });
  
      it('chooses an arbitrary language when none given', async () => {
        // The Player shouldn't allow changing between languages, so it should
        // choose an arbitrary language when none is given.
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(0).language('pt').addAudio(0)
            .addVariant(1).language('en').addAudio(1)
         .build();
  
        player.configure({
          preferredAudioLanguage: undefined,
        });
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
        await player.load(fakeManifestUri, 0, parserFactory);
  
        expect(abrManager.setVariants).toHaveBeenCalled();
  
        // If we have chosen any arbitrary language, setVariants is provided
        // with exactly one variant.
        let variants = abrManager.setVariants.calls.argsFor(0)[0];
        expect(variants.length).toBe(1);
      });
  
      /**
       * @param {!Array.<string>} languages
       * @param {string} preference
       * @param {number} expectedIndex
       * @return {!Promise}
       */
      function runTest(languages, preference, expectedIndex) {
        let generator = new shaka.test.ManifestGenerator().addPeriod(0);
  
        for (let i = 0; i < languages.length; i++) {
          let lang = languages[i];
          if (lang.charAt(0) == '*') {
            generator
              .addVariant(i)
                .primary()
                .language(lang.substr(1))
                .addAudio(i);
          } else {
            generator.addVariant(i).language(lang).addAudio(i);
          }
        }
        // A manifest we can use to test language selection.
        manifest = generator.build();
  
        // Set the user preferences, which must happen before load().
        player.configure({
          preferredAudioLanguage: preference,
        });
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        return player.load(fakeManifestUri, 0, factory).then(function() {
          expect(getActiveVariantTrack().id).toBe(expectedIndex);
        });
      }
    });
  
    describe('getStats', function() {
      beforeAll(function() {
        jasmine.clock().install();
        jasmine.clock().mockDate();
      });
  
      beforeEach(async () => {
        // A manifest we can use to test stats.
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(0)
              .bandwidth(200)
              .addAudio(1).bandwidth(100)
              .addVideo(2).bandwidth(100).size(100, 200)
            .addVariant(1)
              .bandwidth(300)
              .addExistingStream(1)  // audio
              .addVideo(3).bandwidth(200).size(200, 400)
            .addVariant(2)
              .bandwidth(300)
              .addAudio(4).bandwidth(200)
              .addExistingStream(2)  // video
            .addVariant(3)
              .bandwidth(400)
              .addExistingStream(4)  // audio
              .addExistingStream(3)  // video
          .build();
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
        await player.load(fakeManifestUri, 0, parserFactory);
  
        // Initialize the fake streams.
        streamingEngine.onCanSwitch();
      });
  
      afterAll(function() {
        jasmine.clock().uninstall();
      });
  
      it('can be called before player.load()', async () => {
        // Regression test for https://github.com/google/shaka-player/issues/968
        // Create a fresh Player, since all other tests start after load()
        await player.destroy();
        player = new shaka.Player(video);
  
        // In #968, getStats() throws an exception:
        let stats = player.getStats();
        expect(stats).toBeTruthy();
      });
  
      it('tracks estimated bandwidth', function() {
        abrManager.getBandwidthEstimate.and.returnValue(25);
        let stats = player.getStats();
        expect(stats.estimatedBandwidth).toBe(25);
      });
  
      it('tracks info about current stream', function() {
        let stats = player.getStats();
        // Should have chosen the first of each type of stream.
        expect(stats.width).toBe(100);
        expect(stats.height).toBe(200);
        expect(stats.streamBandwidth).toBe(200);
      });
  
      it('tracks frame info', function() {
        // getVideoPlaybackQuality does not exist yet.
        let stats = player.getStats();
        expect(stats.decodedFrames).toBeNaN();
        expect(stats.droppedFrames).toBeNaN();
  
        video.getVideoPlaybackQuality = function() {
          return {
            corruptedVideoFrames: 0,
            creationTime: 0,
            totalFrameDelay: 0,
            totalVideoFrames: 75,
            droppedVideoFrames: 125,
          };
        };
  
        // Now that it exists, theses stats should exist.
        stats = player.getStats();
        expect(stats.decodedFrames).toBe(75);
        expect(stats.droppedFrames).toBe(125);
      });
  
      describe('buffer/play times', function() {
        it('tracks play time', function() {
          let stats = player.getStats();
          expect(stats.playTime).toBeCloseTo(0);
          expect(stats.bufferingTime).toBeCloseTo(0);
  
          jasmine.clock().tick(5000);
  
          stats = player.getStats();
          expect(stats.playTime).toBeCloseTo(5);
          expect(stats.bufferingTime).toBeCloseTo(0);
        });
  
        it('tracks buffering time', function() {
          let stats = player.getStats();
          expect(stats.playTime).toBeCloseTo(0);
          expect(stats.bufferingTime).toBeCloseTo(0);
  
          buffering(true);
          jasmine.clock().tick(5000);
  
          stats = player.getStats();
          expect(stats.playTime).toBeCloseTo(0);
          expect(stats.bufferingTime).toBeCloseTo(5);
        });
  
        it('tracks correct time when switching states', function() {
          // buffering(false)
          jasmine.clock().tick(3000);
          buffering(true);
          jasmine.clock().tick(5000);
          buffering(true);
          jasmine.clock().tick(9000);
          buffering(false);
          jasmine.clock().tick(1000);
  
          let stats = player.getStats();
          expect(stats.playTime).toBeCloseTo(4);
          expect(stats.bufferingTime).toBeCloseTo(14);
        });
  
        /**
         * @param {boolean} buffering
         * @suppress {accessControls}
         */
        function buffering(buffering) {
          player.onBuffering_(buffering);
        }
      });
  
      describe('.switchHistory', function() {
        it('includes original choices', function() {
          // checkHistory prepends the initial stream selections.
          checkHistory([]);
        });
  
        it('includes selectVariantTrack choices', function() {
          let track = player.getVariantTracks()[3];
          player.selectVariantTrack(track);
  
          let period = manifest.periods[0];
          let variant =
              shaka.util.StreamUtils.findVariantForTrack(period, track);
  
          checkHistory([{
            // We are using a mock date, so this is not a race.
            timestamp: Date.now() / 1000,
            id: variant.id,
            type: 'variant',
            fromAdaptation: false,
            bandwidth: variant.bandwidth,
          }]);
        });
  
        it('includes adaptation choices', function() {
          let variant = manifest.periods[0].variants[3];
  
          switch_(variant);
          checkHistory(jasmine.arrayContaining([
            {
              timestamp: Date.now() / 1000,
              id: variant.id,
              type: 'variant',
              fromAdaptation: true,
              bandwidth: variant.bandwidth,
            },
          ]));
        });
  
        /**
         * Checks that the switch history is correct.
         * @param {!Array.<shaka.extern.TrackChoice>} additional
         */
        function checkHistory(additional) {
          let prefix = {
            timestamp: jasmine.any(Number),
            id: 0,
            type: 'variant',
            fromAdaptation: true,
            bandwidth: 200,
          };
  
          let switchHistory = player.getStats().switchHistory;
  
          expect(switchHistory[0]).toEqual(prefix);
          expect(switchHistory.slice(1)).toEqual(additional);
        }
  
        /**
         * @param {shaka.extern.Variant} variant
         * @suppress {accessControls}
         */
        function switch_(variant) {
          player.switch_(variant);
        }
      });
  
      describe('.stateHistory', function() {
        it('begins with buffering state', function() {
          setBuffering(true);
          expect(player.getStats().stateHistory).toEqual([
            {
              // We are using a mock date, so this is not a race.
              timestamp: Date.now() / 1000,
              duration: 0,
              state: 'buffering',
            },
          ]);
        });
  
        it('transitions to paused if the video is paused', function() {
          setBuffering(true);
          video.paused = true;
          setBuffering(false);
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'buffering',
            },
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'paused',
            },
          ]);
        });
  
        it('transitions to playing if the video is playing', function() {
          setBuffering(true);
          video.paused = false;
          setBuffering(false);
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'buffering',
            },
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'playing',
            },
          ]);
        });
  
        it('transitions to ended when the video ends', function() {
          setBuffering(false);
  
          video.ended = true;
          // Fire an 'ended' event on the mock video.
          video.on['ended']();
  
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'playing',
            },
            {
              timestamp: jasmine.any(Number),
              duration: jasmine.any(Number),
              state: 'ended',
            },
          ]);
        });
  
        it('accumulates duration as time passes', function() {
          // We are using a mock date, so this is not a race.
          let bufferingStarts = Date.now() / 1000;
          setBuffering(true);
  
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: bufferingStarts,
              duration: 0,
              state: 'buffering',
            },
          ]);
  
          jasmine.clock().tick(1500);
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: bufferingStarts,
              duration: 1.5,
              state: 'buffering',
            },
          ]);
  
          let playbackStarts = Date.now() / 1000;
          setBuffering(false);
          jasmine.clock().tick(9000);
          expect(player.getStats().stateHistory).toEqual([
            {
              timestamp: bufferingStarts,
              duration: 1.5,
              state: 'buffering',
            },
            {
              timestamp: playbackStarts,
              duration: 9,
              state: 'playing',
            },
          ]);
        });
  
        /**
         * @param {boolean} buffering
         * @suppress {accessControls}
         */
        function setBuffering(buffering) {
          player.onBuffering_(buffering);
        }
      });
    });
  
    describe('unplayable periods', function() {
      beforeEach(function() {
        // overriding for good / bad codecs.
        window.MediaSource.isTypeSupported = function(mimeType) {
          return mimeType.includes('good');
        };
      });
  
      it('success when one period is playable', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(0).mime('video/mp4', 'good')
                .build();
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        await player.load(fakeManifestUri, 0, factory);
      });
  
      it('success when all periods are playable', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(0).mime('video/mp4', 'good')
                .addPeriod(1)
                  .addVariant(1)
                    .addVideo(1).mime('video/mp4', 'good')
                .build();
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        await player.load(fakeManifestUri, 0, factory);
      });
  
      it('throw UNPLAYABLE_PERIOD when some periods are unplayable', async () => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0).bandwidth(500)
                .addVideo(0).mime('video/mp4', 'good')
            .addPeriod(1)
              .addVariant(1).bandwidth(500)
                .addVideo(1).mime('video/mp4', 'bad')
            .build();
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        try {
          await player.load(fakeManifestUri, 0, factory);
          fail();
        } catch (error) {
          shaka.test.Util.expectToEqualError(
              error,
              new shaka.util.Error(
                  shaka.util.Error.Severity.CRITICAL,
                  shaka.util.Error.Category.MANIFEST,
                  shaka.util.Error.Code.UNPLAYABLE_PERIOD));
        }
      });
  
      it('throw CONTENT_UNSUPPORTED_BY_BROWSER when the only period is ' +
          'unplayable', async () => {
            manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(500)
                    .addVideo(0).mime('video/mp4', 'bad')
                .build();
            let parser = new shaka.test.FakeManifestParser(manifest);
            let factory = function() { return parser; };
            try {
              await player.load(fakeManifestUri, 0, factory);
              fail();
            } catch (error) {
              shaka.test.Util.expectToEqualError(
                  error,
                  new shaka.util.Error(
                      shaka.util.Error.Severity.CRITICAL,
                      shaka.util.Error.Category.MANIFEST,
                      shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER));
            }
          });
  
      it('throw CONTENT_UNSUPPORTED_BY_BROWSER when all periods are unplayable',
          async () => {
            manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(500)
                    .addVideo(0).mime('video/mp4', 'bad')
                  .addVariant(1).bandwidth(500)
                    .addVideo(1).mime('video/mp4', 'bad')
                .addPeriod(1)
                  .addVariant(2)
                    .addVideo(2).mime('video/mp4', 'bad')
                .build();
  
            let parser = new shaka.test.FakeManifestParser(manifest);
            let factory = function() { return parser; };
            try {
              await player.load(fakeManifestUri, 0, factory);
              fail();
            } catch (error) {
              shaka.test.Util.expectToEqualError(
                  error,
                  new shaka.util.Error(
                      shaka.util.Error.Severity.CRITICAL,
                      shaka.util.Error.Category.MANIFEST,
                      shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER));
            }
          });
  
      it('throw UNPLAYABLE_PERIOD when the new period is unplayable', (done) => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0).bandwidth(500)
                .addVideo(0).mime('video/mp4', 'good')
              .addVariant(1).bandwidth(500)
                .addVideo(1).mime('video/mp4', 'good')
            .build();
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        player.load(fakeManifestUri, 0, factory).catch(fail).then(function() {
          let manifest2 = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0).bandwidth(500)
                .addVideo(0).mime('video/mp4', 'bad')
            .build();
          manifest.periods.push(manifest2.periods[0]);
          try {
            parser.playerInterface.filterNewPeriod(manifest2.periods[0]);
            fail('filter period wrong');
          } catch (error) {
            shaka.test.Util.expectToEqualError(
                error,
                new shaka.util.Error(
                    shaka.util.Error.Severity.CRITICAL,
                    shaka.util.Error.Category.MANIFEST,
                    shaka.util.Error.Code.UNPLAYABLE_PERIOD));
          }
        }).catch(fail).then(done);
      });
    });
  
    describe('restrictions', function() {
      it('switches if active is restricted by application', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(500)
                    .addVideo(1)
                  .addVariant(1).bandwidth(100)
                    .addVideo(2)
                .build();
  
        await setupPlayer();
        let activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(0);
  
        // Ask AbrManager to choose the 0th variant from those it is given.
        abrManager.chooseIndex = 0;
        abrManager.chooseVariant.calls.reset();
  
        // This restriction should make it so that the first variant (bandwidth
        // 500, id 0) cannot be selected.
        player.configure({
          restrictions: {maxBandwidth: 200},
        });
  
        // The restriction change should trigger a call to AbrManager.
        expect(abrManager.chooseVariant).toHaveBeenCalled();
  
        // The first variant is disallowed.
        expect(manifest.periods[0].variants[0].id).toBe(0);
        expect(manifest.periods[0].variants[0].allowedByApplication)
            .toBe(false);
  
        // AbrManager chose the second variant (id 1).
        activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(1);
      });
  
      it('updates AbrManager for restriction changes', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(1).bandwidth(500)
                    .addVideo(10)
                  .addVariant(2).bandwidth(100)
                    .addVideo(20)
                .build();
  
        await setupPlayer();
        abrManager.setVariants.calls.reset();
  
        player.configure({restrictions: {maxBandwidth: 200}});
  
        // AbrManager should have been updated with the restricted tracks.
        // The first variant is disallowed.
        expect(abrManager.setVariants).toHaveBeenCalledTimes(1);
        const variants = abrManager.setVariants.calls.argsFor(0)[0];
        expect(variants.length).toBe(1);
        expect(variants[0].id).toBe(2);
  
        // Now increase the restriction, AbrManager should still be updated.
        // https://github.com/google/shaka-player/issues/1533
        abrManager.setVariants.calls.reset();
        player.configure({restrictions: {maxBandwidth: Infinity}});
        expect(abrManager.setVariants).toHaveBeenCalledTimes(1);
        const newVariants = abrManager.setVariants.calls.argsFor(0)[0];
        expect(newVariants.length).toBe(2);
      });
  
      it('switches if active key status is "output-restricted"', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(1)
                    .addVideo(2)
                .build();
  
        await setupPlayer();
        let activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(0);
  
        abrManager.chooseIndex = 0;
        abrManager.chooseVariant.calls.reset();
  
        // This restricts the first variant, which triggers chooseVariant.
        onKeyStatus({'abc': 'output-restricted'});
        expect(abrManager.chooseVariant).toHaveBeenCalled();
  
        // The first variant is disallowed.
        expect(manifest.periods[0].variants[0].id).toBe(0);
        expect(manifest.periods[0].variants[0].allowedByKeySystem)
            .toBe(false);
  
        // The second variant was chosen.
        activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(1);
      });
  
      it('switches if active key status is "internal-error"', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(1)
                    .addVideo(2)
                .build();
  
        await setupPlayer();
        let activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(0);
  
        // AbrManager should choose the second track since the first is
        // restricted.
        abrManager.chooseIndex = 0;
        abrManager.chooseVariant.calls.reset();
        onKeyStatus({'abc': 'internal-error'});
        expect(abrManager.chooseVariant).toHaveBeenCalled();
        expect(manifest.periods[0].variants[0].id).toBe(0);
        expect(manifest.periods[0].variants[0].allowedByKeySystem)
            .toBe(false);
  
        activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(1);
      });
  
      it('doesn\'t switch if the active stream isn\'t restricted', async () => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0)
                .addVideo(1).keyId('abc')
              .addVariant(1)
                .addVideo(2)
            .build();
  
        await setupPlayer();
        abrManager.chooseVariant.calls.reset();
  
        let activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(0);
  
        onKeyStatus({'abc': 'usable'});
        expect(abrManager.chooseVariant).not.toHaveBeenCalled();
  
        activeVariant = getActiveVariantTrack();
        expect(activeVariant.id).toBe(0);
      });
  
      it('removes if key status is "output-restricted"', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(1)
                    .addVideo(2)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        onKeyStatus({'abc': 'output-restricted'});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(1);
      });
  
      it('removes if key status is "internal-error"', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(1)
                    .addVideo(2)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        onKeyStatus({'abc': 'internal-error'});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(1);
      });
  
      it('removes if we don\'t have the required key', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(2)
                    .addVideo(3)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        // We have some key statuses, but not for the key IDs we know.
        onKeyStatus({'foo': 'usable'});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(2);
      });
  
      it('does not restrict if no key statuses are available', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(2)
                    .addVideo(3)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        // This simulates, for example, the lack of key status on Chromecast
        // when using PlayReady.  See #1070.
        onKeyStatus({});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(2);
      });
  
      it('doesn\'t remove when using synthetic key status', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(2)
                    .addVideo(3)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        // A synthetic key status contains a single key status with key '00'.
        onKeyStatus({'00': 'usable'});
  
        expect(player.getVariantTracks().length).toBe(2);
      });
  
      it('removes all encrypted tracks for errors with synthetic key status',
          async () => {
            manifest = new shaka.test.ManifestGenerator()
                    .addPeriod(0)
                      .addVariant(0)
                        .addVideo(1).keyId('abc')
                      .addVariant(2)
                        .addVideo(3).keyId('xyz')
                      .addVariant(4)
                        .addVideo(5)
                    .build();
  
            await setupPlayer();
            expect(player.getVariantTracks().length).toBe(3);
  
            // A synthetic key status contains a single key status with key '00'.
            onKeyStatus({'00': 'internal-error'});
  
            let tracks = player.getVariantTracks();
            expect(tracks.length).toBe(1);
            expect(tracks[0].id).toBe(4);
          });
  
      it('removes if key system does not support codec', async () => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0).addDrmInfo('foo.bar')
                .addVideo(1).encrypted(true).mime('video/unsupported')
              .addVariant(1).addDrmInfo('foo.bar')
                .addVideo(2).encrypted(true)
            .build();
  
        // We must be careful that our video/unsupported was not filtered out
        // because of MSE support.  We are specifically testing EME-based
        // filtering of codecs.
        expect(MediaSource.isTypeSupported('video/unsupported')).toBe(true);
  
        // Make sure that drm engine will reject the variant with an unsupported
        // video mime type.
        drmEngine.supportsVariant.and.callFake((variant) => {
          return variant.video.mimeType != 'video/unsupported';
        });
  
        await setupPlayer();
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(1);
      });
  
      it('removes based on bandwidth', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(10)
                    .addVideo(1)
                  .addVariant(1).bandwidth(1500)
                    .addVideo(2)
                  .addVariant(2).bandwidth(500)
                    .addVideo(3)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(3);
  
        player.configure({restrictions: {minBandwidth: 100, maxBandwidth: 1000}});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(2);
      });
  
      it('removes based on pixels', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).size(900, 900)
                  .addVariant(1)
                    .addVideo(2).size(5, 5)
                  .addVariant(2)
                    .addVideo(3).size(190, 190)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(3);
  
        player.configure({restrictions: {minPixels: 100, maxPixels: 800 * 800}});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(2);
      });
  
      it('removes based on width', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).size(5, 5)
                  .addVariant(1)
                    .addVideo(2).size(1500, 200)
                  .addVariant(2)
                    .addVideo(3).size(190, 190)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(3);
  
        player.configure({restrictions: {minWidth: 100, maxWidth: 1000}});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(2);
      });
  
      it('removes based on height', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).size(5, 5)
                  .addVariant(1)
                    .addVideo(2).size(200, 1500)
                  .addVariant(2)
                    .addVideo(3).size(190, 190)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(3);
  
        player.configure({restrictions: {minHeight: 100, maxHeight: 1000}});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(2);
      });
  
      it('removes the whole variant if one stream is restricted', async () => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              .addVariant(0)
                .addVideo(1).size(5, 5)
                .addAudio(2)
              .addVariant(1)
                .addVideo(3).size(190, 190)
                .addAudio(4)
            .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(2);
  
        player.configure({restrictions: {minHeight: 100, maxHeight: 1000}});
  
        let tracks = player.getVariantTracks();
        expect(tracks.length).toBe(1);
        expect(tracks[0].id).toBe(1);
      });
  
      it('issues error if no streams are playable', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).size(5, 5)
                  .addVariant(1)
                    .addVideo(2).size(200, 300)
                  .addVariant(2)
                    .addVideo(3).size(190, 190)
                .build();
  
        await setupPlayer();
        expect(player.getVariantTracks().length).toBe(3);
  
        onError.and.callFake(function(e) {
          let error = e.detail;
          shaka.test.Util.expectToEqualError(
              error,
              new shaka.util.Error(
                  shaka.util.Error.Severity.CRITICAL,
                  shaka.util.Error.Category.MANIFEST,
                  shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET, {
                    hasAppRestrictions: true,
                    missingKeys: [],
                    restrictedKeyStatuses: [],
                  }));
        });
  
        player.configure({restrictions: {minHeight: 1000, maxHeight: 2000}});
        expect(onError).toHaveBeenCalled();
      });
  
      it('chooses efficient codecs and removes the rest', async () => {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                // More efficient codecs
                  .addVariant(0).bandwidth(100)
                    .addVideo(0).mime('video/mp4', 'good')
                  .addVariant(1).bandwidth(200)
                    .addVideo(1).mime('video/mp4', 'good')
                  .addVariant(2).bandwidth(300)
                    .addVideo(2).mime('video/mp4', 'good')
                // Less efficient codecs
                  .addVariant(3).bandwidth(10000)
                    .addVideo(3).mime('video/mp4', 'bad')
                  .addVariant(4).bandwidth(20000)
                    .addVideo(4).mime('video/mp4', 'bad')
                  .addVariant(5).bandwidth(30000)
                    .addVideo(5).mime('video/mp4', 'bad')
                .build();
  
        await setupPlayer();
        expect(abrManager.setVariants).toHaveBeenCalled();
        let variants = abrManager.setVariants.calls.argsFor(0)[0];
        // We've already chosen codecs, so only 3 tracks should remain.
        expect(variants.length).toBe(3);
        // They should be the low-bandwidth ones.
        expect(variants[0].video.codecs).toEqual('good');
        expect(variants[1].video.codecs).toEqual('good');
        expect(variants[2].video.codecs).toEqual('good');
      });
  
      it('updates AbrManager about restricted variants', function(done) {
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0)
                    .addVideo(1).keyId('abc')
                  .addVariant(2)
                    .addVideo(3)
                .build();
  
        let abrManager = new shaka.test.FakeAbrManager();
        player.configure({abrFactory: function() { return abrManager; }});
        setupPlayer().then(function() {
          expect(player.getVariantTracks().length).toBe(2);
  
          // We have some key statuses, but not for the key IDs we know.
          abrManager.setVariants.calls.reset();
          onKeyStatus({'foo': 'usable'});
  
          expect(abrManager.setVariants).toHaveBeenCalled();
          let variants = abrManager.setVariants.calls.argsFor(0)[0];
          expect(variants.length).toBe(1);
          expect(variants[0].id).toBe(2);
        }).catch(fail).then(done);
      });
  
      it('chooses codecs after considering 6-channel preference', async () => {
        manifest = new shaka.test.ManifestGenerator()
            .addPeriod(0)
              // Surround sound AC-3, preferred by config
              .addVariant(0).bandwidth(300)
                .addAudio(0).channelsCount(6).mime('audio/mp4', 'ac-3')
              // Stereo AAC, would win out based on bandwidth alone
              .addVariant(1).bandwidth(100)
                .addAudio(1).channelsCount(2).mime('audio/mp4', 'mp4a.40.2')
            .build();
  
        // Configure for 6 channels.
        player.configure({
          preferredAudioChannelCount: 6,
        });
        await setupPlayer();
        expect(abrManager.setVariants).toHaveBeenCalled();
        // We've chosen codecs, so only 1 track should remain.
        expect(abrManager.variants.length).toBe(1);
        // It should be the 6-channel variant, based on our preference.
        expect(abrManager.variants[0].audio.channelsCount).toEqual(6);
        expect(abrManager.variants[0].audio.codecs).toEqual('ac-3');
      });
  
      /**
       * @return {!Promise}
       */
      function setupPlayer() {
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
        return player.load(fakeManifestUri, 0, parserFactory).then(function() {
          // Initialize the fake streams.
          streamingEngine.onCanSwitch();
        });
      }
    });
  
    describe('getPlayheadTimeAsDate()', function() {
      beforeEach(async () => {
        let timeline = new shaka.media.PresentationTimeline(300, 0);
        timeline.setStatic(false);
        manifest = new shaka.test.ManifestGenerator()
            .setTimeline(timeline)
            .addPeriod(0)
              .addVariant(0)
              .addVideo(1)
            .build();
        goog.asserts.assert(manifest, 'manifest must be non-null');
        let parser = new shaka.test.FakeManifestParser(manifest);
        let factory = function() { return parser; };
        await player.load(fakeManifestUri, 0, factory);
      });
  
      it('gets current wall clock time in UTC', function() {
        let liveTimeUtc = player.getPlayheadTimeAsDate();
        expect(liveTimeUtc).toEqual(new Date(320000));
      });
    });
  
    it('rejects empty manifests', function(done) {
      manifest = new shaka.test.ManifestGenerator().build();
      let emptyParser = new shaka.test.FakeManifestParser(manifest);
      let emptyFactory = function() { return emptyParser; };
  
      player.load(fakeManifestUri, 0, emptyFactory).then(fail).catch((error) => {
        shaka.test.Util.expectToEqualError(
            error,
            new shaka.util.Error(
                shaka.util.Error.Severity.CRITICAL,
                shaka.util.Error.Category.MANIFEST,
                shaka.util.Error.Code.NO_PERIODS));
      }).then(done);
    });
  
    it('does not assert when adapting', async () => {
      // Most of our Player unit tests never adapt.  This allowed some assertions
      // to creep in that went uncaught until they happened during manual testing.
      // Repro only happens with audio+video variants in which we only adapt one
      // type.  This test covers https://github.com/google/shaka-player/issues/954
  
      manifest = new shaka.test.ManifestGenerator()
              .addPeriod(0)
                .addVariant(0).bandwidth(100)
                  .addVideo(0).mime('video/mp4', 'good')
                  .addAudio(9).mime('audio/mp4', 'good')
                .addVariant(1).bandwidth(200)
                  .addVideo(1).mime('video/mp4', 'good')
                  .addExistingStream(9)  // audio
                .addVariant(2).bandwidth(300)
                  .addVideo(2).mime('video/mp4', 'good')
                  .addExistingStream(9)  // audio
              .build();
  
      let parser = new shaka.test.FakeManifestParser(manifest);
      let parserFactory = function() { return parser; };
  
      await player.load(fakeManifestUri, 0, parserFactory);
      streamingEngine.onCanSwitch();
  
      // We've already loaded variants[0].  Switch to [1] and [2].
      abrManager.switchCallback(manifest.periods[0].variants[1]);
      abrManager.switchCallback(manifest.periods[0].variants[2]);
    });
  
    describe('isTextTrackVisible', function() {
      it('does not throw before load', function() {
        player.isTextTrackVisible();
      });
    });
  
    describe('setTextTrackVisibility', function() {
      it('does not throw before load', function() {
        player.setTextTrackVisibility(true);
      });
    });
  
    describe('isAudioOnly', function() {
      it('detects audio-only content', async () => {
        // This factory recreates the parser each time, so updates to |manifest|
        // affect the next load() call.
        let parserFactory = function() {
          return new shaka.test.FakeManifestParser(manifest);
        };
  
        // False before we've loaded anything.
        expect(player.isAudioOnly()).toEqual(false);
  
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(100)
                    .addVideo(0).mime('video/mp4', 'good')
                    .addAudio(1).mime('audio/mp4', 'good')
                .build();
        await player.load(fakeManifestUri, 0, parserFactory);
        // We have audio & video tracks, so this is not audio-only.
        expect(player.isAudioOnly()).toEqual(false);
  
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(100)
                    .addVideo(0).mime('video/mp4', 'good')
                .build();
        await player.load(fakeManifestUri, 0, parserFactory);
        // We have video-only tracks, so this is not audio-only.
        expect(player.isAudioOnly()).toEqual(false);
  
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(100)
                    .addAudio(1).mime('audio/mp4', 'good')
                .build();
        await player.load(fakeManifestUri, 0, parserFactory);
        // We have audio-only tracks, so this is audio-only.
        expect(player.isAudioOnly()).toEqual(true);
  
        await player.unload();
        // When we have nothing loaded, we go back to not audio-only status.
        expect(player.isAudioOnly()).toEqual(false);
      });
    });
  
    describe('load', function() {
      it('tolerates bandwidth of NaN, undefined, or 0', async () => {
        // Regression test for https://github.com/google/shaka-player/issues/938
        manifest = new shaka.test.ManifestGenerator()
                .addPeriod(0)
                  .addVariant(0).bandwidth(/** @type {?} */(undefined))
                    .addVideo(0).mime('video/mp4', 'good')
                  .addVariant(1).bandwidth(NaN)
                    .addVideo(1).mime('video/mp4', 'good')
                  .addVariant(2).bandwidth(0)
                    .addVideo(2).mime('video/mp4', 'good')
                .build();
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
  
        // Before the fix, load() would fail assertions and throw errors.
        await player.load(fakeManifestUri, 0, parserFactory);
      });
  
      it('respects startTime of 0', async () => {
        // What we shouldn't do is treat start time of 0 as the same as startTime
        // of null/undefined.  0 means timestamp 0, whereas null/undefined means
        // "default".  For VOD, the default is 0, but for live streams, the
        // default is the live edge.
  
        // Create a live timeline and manifest.
        let timeline = new shaka.media.PresentationTimeline(300, 0);
        timeline.setStatic(false);
  
        manifest = new shaka.test.ManifestGenerator()
            .setTimeline(timeline)
            .addPeriod(0)
              .addVariant(0)
              .addVideo(1)
            .build();
  
        let parser = new shaka.test.FakeManifestParser(manifest);
        let parserFactory = function() { return parser; };
  
        // To ensure that Playhead is correctly created, we must use the original
        // playhead injector.  To inspect the real Playhead instance, though, we
        // must shim this method and keep a copy of the real Playhead.  Otherwise,
        // we would be merely inspecting the mock Playhead.
        /** @type {shaka.media.Playhead} */
        let realPlayhead = null;
  
        /**
         * @this {shaka.Player}
         * @return {!shaka.media.Playhead}
         */
        player.createPlayhead = function() {
          realPlayhead =
              shaka.Player.prototype.createPlayhead.apply(this, arguments);
          return realPlayhead;
        };
  
        await player.load(fakeManifestUri, /* startTime */ 0, parserFactory);
  
        // Ensure this is seen as a live stream, or else the test is invalid.
        expect(player.isLive()).toBe(true);
  
        // If startTime of 0 was treated as null, then getTime() would point to
        // the live edge instead of 0.
        expect(realPlayhead.getTime()).toBe(0);
      });
    });
  
    describe('language methods', function() {
      let videoOnlyManifest;
      let parserFactory = function() {
        return new shaka.test.FakeManifestParser(manifest);
      };
  
      beforeEach(function() {
        manifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(1).language('fr')
              .addVideo(0).size(300, 400)
              .addAudio(1).language('fr')
  
            .addVariant(2).language('en')
              .addExistingStream(0)  // video
              .addAudio(2).language('en').roles(['main'])
  
            .addVariant(3).language('en')
              .addExistingStream(0)  // video
              .addAudio(3).language('en').roles(['commentary'])
  
            .addVariant(4).language('de')
              .addExistingStream(0)  // video
              .addAudio(4).language('de').roles(['foo', 'bar'])
  
            .addTextStream(5)
              .language('es').roles(['baz', 'qwerty'])
            .addTextStream(6)
              .language('en').kind('caption').roles(['main', 'caption'])
            .addTextStream(7)
              .language('en').kind('subtitle').roles(['main', 'subtitle'])
          .build();
  
        videoOnlyManifest = new shaka.test.ManifestGenerator()
          .addPeriod(0)
            .addVariant(1)
              .bandwidth(400)
              .addVideo(1).size(300, 400)
            .addVariant(2)
              .bandwidth(800)
              .addVideo(2).size(500, 600)
          .build();
      });
  
      describe('get*Languages', function() {
        it('returns a list of languages', async () => {
          await player.load(fakeManifestUri, 0, parserFactory);
          expect(player.getAudioLanguages()).toEqual(['fr', 'en', 'de']);
          expect(player.getTextLanguages()).toEqual(['es', 'en']);
        });
  
        it('returns "und" for video-only tracks', async () => {
          manifest = videoOnlyManifest;
  
          await player.load(fakeManifestUri, 0, parserFactory);
          expect(player.getAudioLanguages()).toEqual(['und']);
          expect(player.getTextLanguages()).toEqual([]);
        });
      });
  
      describe('getAudioLanguagesAndRoles', () => {
        it('ignores video roles', async () => {
          manifest = new shaka.test.ManifestGenerator()
              .addPeriod(0)
              .addVariant(0)
              .addVideo(1).roles(['video-only-role'])
              .addAudio(2).roles(['audio-only-role']).language('en')
              .build();
  
          await player.load(fakeManifestUri, 0, parserFactory);
  
          expect(player.getAudioLanguagesAndRoles()).toEqual([
            {language: 'en', role: 'audio-only-role'},
          ]);
        });
  
        it('lists all language-role combinations', async () => {
          await player.load(fakeManifestUri, 0, parserFactory);
          expect(player.getAudioLanguagesAndRoles()).toEqual([
            {language: 'fr', role: ''},
            {language: 'en', role: 'main'},
            {language: 'en', role: 'commentary'},
            {language: 'de', role: 'foo'},
            {language: 'de', role: 'bar'},
          ]);
        });
  
        it('uses "und" for video-only tracks', async () => {
          manifest = new shaka.test.ManifestGenerator()
              .addPeriod(0)
              .addVariant(0)
              .addVideo(1).roles(['video-only-role'])
              .build();
  
          await player.load(fakeManifestUri, 0, parserFactory);
          expect(player.getAudioLanguagesAndRoles()).toEqual([
            {language: 'und', role: ''},
          ]);
        });
      });
  
      describe('getTextLanguageAndRoles', function() {
        it('lists all language-role combinations', async () => {
          await player.load(fakeManifestUri, 0, parserFactory);
          expect(player.getTextLanguagesAndRoles()).toEqual([
            {language: 'es', role: 'baz'},
            {language: 'es', role: 'qwerty'},
            {language: 'en', role: 'main'},
            {language: 'en', role: 'caption'},
            {language: 'en', role: 'subtitle'},
          ]);
        });
      });
    });
  
    /**
     * Gets the currently active variant track.
     * @return {shaka.extern.Track}
     */
    function getActiveVariantTrack() {
      let activeTracks = player.getVariantTracks().filter(function(track) {
        return track.active;
      });
  
      expect(activeTracks.length).toBe(1);
      return activeTracks[0];
    }
  
    /**
     * Gets the currently active text track.
     * @return {shaka.extern.Track}
     */
    function getActiveTextTrack() {
      let activeTracks = player.getTextTracks().filter(function(track) {
        return track.active;
      });
  
      expect(activeTracks.length).toBe(1);
      return activeTracks[0];
    }
  
    /**
     * Simulate the transition to a new period using the fake StreamingEngine.
     * @param {number} index
     */
    function transitionPeriod(index) {
      periodIndex = index;
      streamingEngine.onChooseStreams();
    }
  
    /**
     * Choose streams for the given period.
     *
     * @suppress {accessControls}
     * @return {shaka.media.StreamingEngine.ChosenStreams}
     */
    function onChooseStreams() {
      return player.onChooseStreams_(manifest.periods[periodIndex]);
    }
  
    /** @suppress {accessControls} */
    function onCanSwitch() { player.canSwitch_(); }
  
    /**
     * A Jasmine asymmetric matcher for substring matches.
     * @param {string} substring
     * @return {!Object}
     */
    function stringContaining(substring) {
      return {
        asymmetricMatch: function(actual) {
          return actual.includes(substring);
        },
      };
    }
  
    /**
     * @param {!Object.<string, string>} keyStatusMap
     * @suppress {accessControls}
     */
    function onKeyStatus(keyStatusMap) {
      player.onKeyStatus_(keyStatusMap);
    }
  
    /**
     * @param {shaka.extern.Manifest} manifest
     * @return {function():shaka.extern.ManifestParser}
     */
    function returnManifest(manifest) {
      return () => {
        return new shaka.test.FakeManifestParser(manifest);
      };
    }
  });
  