UNPKG

shaka-player

Version:
737 lines (628 loc) 26.6 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('CastProxy', () => { const CastProxy = shaka.cast.CastProxy; const FakeEvent = shaka.util.FakeEvent; const Util = shaka.test.Util; const originalCastSender = shaka.cast.CastSender; const fakeAppId = 'fake app ID'; const fakeAndroidReceiverCompatible = false; let mockPlayer; let mockSender; /** @type {!shaka.test.FakeVideo} */ let mockVideo; /** @type {!jasmine.Spy} */ let mockCastSenderConstructor; /** @type {shaka.cast.CastProxy} */ let proxy; beforeEach(() => { mockCastSenderConstructor = jasmine.createSpy('CastSender constructor'); mockCastSenderConstructor.and.callFake(createMockCastSender); shaka.cast.CastSender = Util.spyFunc(mockCastSenderConstructor); mockVideo = new shaka.test.FakeVideo(); mockPlayer = createMockPlayer(); mockSender = null; proxy = new CastProxy(mockVideo, mockPlayer, fakeAppId, fakeAndroidReceiverCompatible); }); afterEach(async () => { try { await proxy.destroy(); } finally { shaka.cast.CastSender = originalCastSender; } }); describe('constructor', () => { it('creates and initializes a CastSender', () => { expect(mockCastSenderConstructor).toHaveBeenCalled(); expect(mockSender).toBeTruthy(); expect(mockSender.init).toHaveBeenCalled(); }); it('listens for video and player events', () => { expect(Object.keys(mockVideo.on).length).toBeGreaterThan(0); expect(Object.keys(mockPlayer.listeners).length).toBeGreaterThan(0); }); it('creates proxies for video and player', () => { expect(proxy.getVideo()).toBeTruthy(); expect(proxy.getVideo()).not.toBe(mockVideo); expect(proxy.getPlayer()).toBeTruthy(); expect(proxy.getPlayer()).not.toBe(mockPlayer); }); }); describe('canCast', () => { it('is true if the API is ready and we have receivers', () => { mockSender.apiReady.and.returnValue(false); mockSender.hasReceivers.and.returnValue(false); expect(proxy.canCast()).toBe(false); mockSender.apiReady.and.returnValue(true); expect(proxy.canCast()).toBe(false); mockSender.hasReceivers.and.returnValue(true); expect(proxy.canCast()).toBe(true); }); }); describe('isCasting', () => { it('delegates directly to the sender', () => { mockSender.isCasting.and.returnValue(false); expect(proxy.isCasting()).toBe(false); mockSender.isCasting.and.returnValue(true); expect(proxy.isCasting()).toBe(true); }); }); describe('receiverName', () => { it('delegates directly to the sender', () => { mockSender.receiverName.and.returnValue('abc'); expect(proxy.receiverName()).toBe('abc'); mockSender.receiverName.and.returnValue('xyz'); expect(proxy.receiverName()).toBe('xyz'); }); }); describe('setAppData', () => { it('delegates directly to the sender', () => { const fakeAppData = {key: 'value'}; expect(mockSender.setAppData).not.toHaveBeenCalled(); proxy.setAppData(fakeAppData); expect(mockSender.setAppData).toHaveBeenCalledWith(fakeAppData); }); }); describe('disconnect', () => { it('delegates directly to the sender', () => { expect(mockSender.showDisconnectDialog).not.toHaveBeenCalled(); proxy.suggestDisconnect(); expect(mockSender.showDisconnectDialog).toHaveBeenCalled(); }); }); describe('cast', () => { it('pauses the local video', () => { proxy.cast(); expect(mockVideo.pause).toHaveBeenCalled(); }); it('passes initial state to sender', () => { mockVideo.loop = true; mockVideo.playbackRate = 3; mockVideo.currentTime = 12; const fakeConfig = {key: 'value'}; mockPlayer.getConfiguration.and.returnValue(fakeConfig); mockPlayer.isTextTrackVisible.and.returnValue(false); const fakeManifestUri = 'foo://bar'; mockPlayer.getAssetUri.and.returnValue(fakeManifestUri); proxy.cast(); const calls = mockSender.cast.calls; expect(calls.count()).toBe(1); if (calls.count()) { const state = calls.argsFor(0)[0]; // Video state goes directly: expect(state.video.loop).toBe(mockVideo.loop); expect(state.video.playbackRate).toBe(mockVideo.playbackRate); // Player state uses corresponding setter names: expect(state.player.configure).toEqual(fakeConfig); expect(state['playerAfterLoad'].setTextTrackVisibility).toBe(false); // Manifest URI: expect(state.manifest).toBe(fakeManifestUri); // Start time: expect(state.startTime).toBe(mockVideo.currentTime); } }); it('does not provide a start time if the video has ended', () => { mockVideo.ended = true; mockVideo.currentTime = 12; proxy.cast(); const calls = mockSender.cast.calls; expect(calls.count()).toBe(1); if (calls.count()) { const state = calls.argsFor(0)[0]; expect(state.startTime).toBe(null); } }); it('unloads the local player after casting is complete', async () => { /** @type {!shaka.util.PublicPromise} */ const p = new shaka.util.PublicPromise(); mockSender.cast.and.returnValue(p); proxy.cast(); await shaka.test.Util.shortDelay(); // unload() has not been called yet. expect(mockPlayer.unload).not.toHaveBeenCalled(); // Resolve the cast() promise. p.resolve(); await shaka.test.Util.shortDelay(); // unload() has now been called. expect(mockPlayer.unload).toHaveBeenCalled(); }); }); describe('video proxy', () => { describe('get', () => { it('returns local values when we are playing back locally', () => { mockVideo.currentTime = 12; mockVideo.paused = true; expect(proxy.getVideo().currentTime).toBe(mockVideo.currentTime); expect(proxy.getVideo().paused).toBe(mockVideo.paused); expect(mockVideo.play).not.toHaveBeenCalled(); proxy.getVideo().play(); expect(mockVideo.play).toHaveBeenCalled(); // The local method call was properly bound: expect(mockVideo.play.calls.mostRecent().object).toBe(mockVideo); }); it('returns cached remote values when we are casting', () => { // Local values that will be ignored: mockVideo.currentTime = 12; mockVideo.paused = true; // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); // Simulate remote values: const cache = {video: { currentTime: 24, paused: false, play: jasmine.createSpy('play'), }}; mockSender.get.and.callFake((targetName, property) => { expect(targetName).toBe('video'); return cache.video[property]; }); expect(proxy.getVideo().currentTime).not.toBe(mockVideo.currentTime); expect(proxy.getVideo().currentTime).toBe(cache.video.currentTime); expect(proxy.getVideo().paused).not.toBe(mockVideo.paused); expect(proxy.getVideo().paused).toBe(cache.video.paused); // Call a method: expect(mockVideo.play).not.toHaveBeenCalled(); proxy.getVideo().play(); // The call was routed to the remote video. expect(mockVideo.play).not.toHaveBeenCalled(); expect(cache.video.play).toHaveBeenCalled(); }); it('returns local values when we have no remote values yet', () => { mockVideo.currentTime = 12; mockVideo.paused = true; // Set up the sender in casting mode, but without any remote values: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(false); // Simulate remote method: const playSpy = jasmine.createSpy('play'); mockSender.get.and.callFake((targetName, property) => { expect(targetName).toBe('video'); expect(property).toBe('play'); return playSpy; }); // Without remote values, we should still return the local ones. expect(proxy.getVideo().currentTime).toBe(mockVideo.currentTime); expect(proxy.getVideo().paused).toBe(mockVideo.paused); // Call a method: expect(mockVideo.play).not.toHaveBeenCalled(); proxy.getVideo().play(); // The call was still routed to the remote video. expect(mockVideo.play).not.toHaveBeenCalled(); expect(playSpy).toHaveBeenCalled(); }); }); describe('set', () => { it('writes local values when we are playing back locally', () => { mockVideo.currentTime = 12; expect(proxy.getVideo().currentTime).toBe(12); // Writes to the proxy are reflected immediately in both the proxy and // the local video. proxy.getVideo().currentTime = 24; expect(proxy.getVideo().currentTime).toBe(24); expect(mockVideo.currentTime).toBe(24); }); it('writes values remotely when we are casting', () => { mockVideo.currentTime = 12; // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); // Set the value of currentTime: expect(mockSender.set).not.toHaveBeenCalled(); proxy.getVideo().currentTime = 24; expect(mockSender.set).toHaveBeenCalledWith('video', 'currentTime', 24); // The local value was unaffected. expect(mockVideo.currentTime).toBe(12); }); }); describe('local events', () => { it('forward to the proxy when we are playing back locally', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getVideo().addEventListener( 'timeupdate', Util.spyFunc(proxyListener)); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'timeupdate', (new Map()).set('detail', 8675309)); mockVideo.on['timeupdate'](fakeEvent); expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'timeupdate', detail: 8675309, })); }); it('are ignored when we are casting', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getVideo().addEventListener( 'timeupdate', Util.spyFunc(proxyListener)); // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'timeupdate', (new Map()).set('detail', 8675309)); mockVideo.on['timeupdate'](fakeEvent); expect(proxyListener).not.toHaveBeenCalled(); }); }); describe('remote events', () => { it('forward to the proxy when we are casting', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getVideo().addEventListener( 'timeupdate', Util.spyFunc(proxyListener)); // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'timeupdate', (new Map()).set('detail', 8675309)); mockSender.onRemoteEvent('video', fakeEvent); expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'timeupdate', detail: 8675309, })); }); }); }); describe('player proxy', () => { describe('get', () => { it('returns local values when we are playing back locally', () => { const fakeConfig = {key: 'value'}; mockPlayer.getConfiguration.and.returnValue(fakeConfig); expect(proxy.getPlayer().getConfiguration()).toEqual(fakeConfig); expect(mockPlayer.trickPlay).not.toHaveBeenCalled(); proxy.getPlayer().trickPlay(5); expect(mockPlayer.trickPlay).toHaveBeenCalledWith(5); // The local method call was properly bound: expect(mockPlayer.trickPlay.calls.mostRecent().object).toBe(mockPlayer); }); it('returns cached remote values when we are casting', () => { // Local values that will be ignored: const fakeConfig = {key: 'value'}; mockPlayer.getConfiguration.and.returnValue(fakeConfig); mockPlayer.isTextTrackVisible.and.returnValue(false); // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); // Simulate remote values: const fakeConfig2 = {key2: 'value2'}; const cache = {player: { getConfiguration: fakeConfig2, isTextTrackVisible: true, trickPlay: jasmine.createSpy('trickPlay'), }}; mockSender.get.and.callFake((targetName, property) => { expect(targetName).toBe('player'); const value = cache.player[property]; if (typeof value == 'function') { // methods: return value; } else { // getters: return () => value; } }); expect(proxy.getPlayer().getConfiguration()).toEqual(fakeConfig2); expect(proxy.getPlayer().isTextTrackVisible()).toBe(true); // Call a method: expect(mockPlayer.trickPlay).not.toHaveBeenCalled(); proxy.getPlayer().trickPlay(5); // The call was routed to the remote player. expect(mockPlayer.trickPlay).not.toHaveBeenCalled(); expect(cache.player.trickPlay).toHaveBeenCalledWith(5); }); it('returns local values when we have no remote values yet', () => { const fakeConfig = {key: 'value'}; mockPlayer.getConfiguration.and.returnValue(fakeConfig); mockPlayer.isTextTrackVisible.and.returnValue(true); // Set up the sender in casting mode, but without any remote values: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(false); // Simulate remote method: const trickPlaySpy = jasmine.createSpy('trickPlay'); mockSender.get.and.callFake((targetName, property) => { expect(targetName).toBe('player'); expect(property).toBe('trickPlay'); return trickPlaySpy; }); // Without remote values, we should still return the local ones. expect(proxy.getPlayer().getConfiguration()).toEqual(fakeConfig); expect(proxy.getPlayer().isTextTrackVisible()).toBe(true); // Call a method: expect(mockPlayer.trickPlay).not.toHaveBeenCalled(); proxy.getPlayer().trickPlay(5); // The call was still routed to the remote player. expect(mockPlayer.trickPlay).not.toHaveBeenCalled(); expect(trickPlaySpy).toHaveBeenCalledWith(5); }); it('always returns a local NetworkingEngine', () => { // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); expect(mockPlayer.getNetworkingEngine).not.toHaveBeenCalled(); proxy.getPlayer().getNetworkingEngine(); expect(mockPlayer.getNetworkingEngine).toHaveBeenCalled(); // The local method call was properly bound: expect(mockPlayer.getNetworkingEngine.calls.mostRecent().object).toBe( mockPlayer); }); }); describe('local events', () => { it('forward to the proxy when we are playing back locally', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getPlayer().addEventListener( 'buffering', Util.spyFunc(proxyListener)); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'buffering', (new Map()).set('detail', 8675309)); mockPlayer.listeners['buffering'](fakeEvent); expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'buffering', detail: 8675309, })); }); it('are ignored when we are casting', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getPlayer().addEventListener( 'buffering', Util.spyFunc(proxyListener)); // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'buffering', (new Map()).set('detail', 8675309)); mockPlayer.listeners['buffering'](fakeEvent); expect(proxyListener).not.toHaveBeenCalled(); }); }); describe('remote events', () => { it('forward to the proxy when we are casting', () => { const proxyListener = jasmine.createSpy('listener'); proxy.getPlayer().addEventListener( 'buffering', Util.spyFunc(proxyListener)); // Set up the sender in casting mode: mockSender.isCasting.and.returnValue(true); mockSender.hasRemoteProperties.and.returnValue(true); expect(proxyListener).not.toHaveBeenCalled(); const fakeEvent = new FakeEvent( 'buffering', (new Map()).set('detail', 8675309)); mockSender.onRemoteEvent('player', fakeEvent); expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'buffering', detail: 8675309, })); }); }); }); describe('"caststatuschanged" event', () => { it('is triggered by the sender', () => { const listener = jasmine.createSpy('listener'); proxy.addEventListener('caststatuschanged', Util.spyFunc(listener)); expect(listener).not.toHaveBeenCalled(); mockSender.onCastStatusChanged(); expect(listener).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'caststatuschanged', })); }); }); describe('synthetic video events from onFirstCastStateUpdate', () => { it('sends a pause event if the video is paused', () => { mockVideo.paused = true; const proxyListener = jasmine.createSpy('listener'); proxy.getVideo().addEventListener('pause', Util.spyFunc(proxyListener)); expect(proxyListener).not.toHaveBeenCalled(); mockSender.onFirstCastStateUpdate(); expect(proxyListener).toHaveBeenCalled(); }); it('sends a play event if the video is playing', () => { mockVideo.paused = false; const proxyListener = jasmine.createSpy('listener'); proxy.getVideo().addEventListener('play', Util.spyFunc(proxyListener)); expect(proxyListener).not.toHaveBeenCalled(); mockSender.onFirstCastStateUpdate(); expect(proxyListener).toHaveBeenCalled(); }); }); describe('resume local playback', () => { let cache; beforeEach(() => { // Simulate cached remote state: cache = { video: { loop: true, playbackRate: 5, }, player: { getConfiguration: {key: 'value'}, isTextTrackVisisble: true, }, }; mockSender.get.and.callFake((targetName, property) => { if (targetName == 'player') { return () => cache[targetName][property]; } else { return cache[targetName][property]; } }); }); it('transfers remote state back to local objects', async () => { // Nothing has been set yet: expect(mockPlayer.configure).not.toHaveBeenCalled(); expect(mockPlayer.setTextTrackVisibility).not.toHaveBeenCalled(); expect(mockVideo.loop).toBe(false); expect(mockVideo.playbackRate).toBe(1); // Resume local playback. mockSender.onResumeLocal(); // Initial Player state first: expect(mockPlayer.configure).toHaveBeenCalledWith( cache.player.getConfiguration); // Nothing else yet: expect(mockPlayer.setTextTrackVisibility).not.toHaveBeenCalled(); expect(mockVideo.loop).toBe(false); expect(mockVideo.playbackRate).toBe(1); // The rest is done async: await shaka.test.Util.shortDelay(); expect(mockPlayer.setTextTrackVisibility).toHaveBeenCalledWith( cache.player.isTextTrackVisible); expect(mockVideo.loop).toBe(cache.video.loop); expect(mockVideo.playbackRate).toBe(cache.video.playbackRate); }); it('loads the manifest', () => { cache.video.currentTime = 12; cache.player.getAssetUri = 'foo://bar'; expect(mockPlayer.load).not.toHaveBeenCalled(); mockSender.onResumeLocal(); expect(mockPlayer.load).toHaveBeenCalledWith('foo://bar', 12); }); it('does not provide a start time if the video has ended', () => { cache.video.currentTime = 12; cache.video.ended = true; cache.player.getAssetUri = 'foo://bar'; expect(mockPlayer.load).not.toHaveBeenCalled(); mockSender.onResumeLocal(); expect(mockPlayer.load).toHaveBeenCalledWith('foo://bar', null); }); it('plays the video after loading', async () => { cache.player.getAssetUri = 'foo://bar'; // Should play even if the video was paused remotely. cache.video.paused = true; mockVideo.autoplay = true; mockSender.onResumeLocal(); // Video autoplay inhibited: expect(mockVideo.autoplay).toBe(false); await shaka.test.Util.shortDelay(); expect(mockVideo.play).toHaveBeenCalled(); // Video autoplay restored: expect(mockVideo.autoplay).toBe(true); }); it('does not load or play without a manifest URI', async () => { cache.player.getAssetUri = null; mockSender.onResumeLocal(); await shaka.test.Util.shortDelay(); // Nothing loaded or played: expect(mockPlayer.load).not.toHaveBeenCalled(); expect(mockVideo.play).not.toHaveBeenCalled(); // State was still transferred, though: expect(mockPlayer.setTextTrackVisibility).toHaveBeenCalledWith( cache.player.isTextTrackVisible); expect(mockVideo.loop).toBe(cache.video.loop); expect(mockVideo.playbackRate).toBe(cache.video.playbackRate); }); it('triggers an "error" event if load fails', async () => { cache.player.getAssetUri = 'foo://bar'; const fakeError = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE); mockPlayer.load.and.returnValue(Promise.reject(fakeError)); mockSender.onResumeLocal(); await shaka.test.Util.shortDelay(); expect(mockPlayer.load).toHaveBeenCalled(); expect(mockPlayer.dispatchEvent).toHaveBeenCalledWith( jasmine.objectContaining({type: 'error', detail: fakeError})); }); }); describe('destroy', () => { it('destroys the local player and the sender', async () => { expect(mockPlayer.destroy).not.toHaveBeenCalled(); expect(mockSender.destroy).not.toHaveBeenCalled(); expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); const p = proxy.destroy(); expect(mockPlayer.destroy).toHaveBeenCalled(); expect(mockSender.destroy).toHaveBeenCalled(); expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); await p; }); it('optionally forces the sender to disconnect', async () => { expect(mockSender.destroy).not.toHaveBeenCalled(); expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); const p = proxy.destroy(true); expect(mockSender.destroy).toHaveBeenCalled(); expect(mockSender.forceDisconnect).toHaveBeenCalled(); await p; }); }); /** * @param {string} appId * @param {Function} onCastStatusChanged * @param {Function} onFirstCastStateUpdate * @param {Function} onRemoteEvent * @param {Function} onResumeLocal * @return {!Object} */ function createMockCastSender( appId, onCastStatusChanged, onFirstCastStateUpdate, onRemoteEvent, onResumeLocal) { expect(appId).toBe(fakeAppId); mockSender = { init: jasmine.createSpy('init'), destroy: jasmine.createSpy('destroy'), apiReady: jasmine.createSpy('apiReady'), hasReceivers: jasmine.createSpy('hasReceivers'), isCasting: jasmine.createSpy('isCasting'), receiverName: jasmine.createSpy('receiverName'), hasRemoteProperties: jasmine.createSpy('hasRemoteProperties'), setAppData: jasmine.createSpy('setAppData'), forceDisconnect: jasmine.createSpy('forceDisconnect'), showDisconnectDialog: jasmine.createSpy('showDisconnectDialog'), cast: jasmine.createSpy('cast'), get: jasmine.createSpy('get'), set: jasmine.createSpy('set'), // For convenience: onCastStatusChanged: onCastStatusChanged, onFirstCastStateUpdate: onFirstCastStateUpdate, onRemoteEvent: onRemoteEvent, onResumeLocal: onResumeLocal, }; mockSender.cast.and.returnValue(Promise.resolve()); return mockSender; } function createMockPlayer() { const player = { load: jasmine.createSpy('load'), unload: jasmine.createSpy('unload'), getNetworkingEngine: jasmine.createSpy('getNetworkingEngine'), getAssetUri: jasmine.createSpy('getAssetUri'), getConfiguration: jasmine.createSpy('getConfiguration'), configure: jasmine.createSpy('configure'), isTextTrackVisible: jasmine.createSpy('isTextTrackVisible'), setTextTrackVisibility: jasmine.createSpy('setTextTrackVisibility'), trickPlay: jasmine.createSpy('trickPlay'), destroy: jasmine.createSpy('destroy'), addEventListener: (eventName, listener) => { player.listeners[eventName] = listener; }, removeEventListener: (eventName, listener) => { delete player.listeners[eventName]; }, dispatchEvent: jasmine.createSpy('dispatchEvent'), // For convenience: listeners: {}, }; player.load.and.returnValue(Promise.resolve()); player.unload.and.returnValue(Promise.resolve()); return player; } });