shaka-player
Version:
DASH/EME video player library
1,184 lines (1,015 loc) • 36.9 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// The receiver is only meant to run on the Chromecast, so we have the
// ability to use modern APIs there that may not be available on all of the
// browsers our library supports. Because of this, CastReceiver tests will
// only be run on Chrome and Chromecast.
/** @return {boolean} */
const castReceiverSupport =
() => shaka.util.Platform.isChrome() || shaka.util.Platform.isChromecast();
filterDescribe('CastReceiver', castReceiverSupport, () => {
const CastReceiver = shaka.cast.CastReceiver;
const CastUtils = shaka.cast.CastUtils;
const Util = shaka.test.Util;
const originalCast = window['cast'];
const originalUserAgent = navigator.userAgent;
const originalPollInterval = CastReceiver.POLL_INTERVAL;
const originalIdleInterval = CastReceiver.IDLE_INTERVAL;
/** @type {!shaka.test.FakeVideo} */
let mockVideo;
/** @type {!jasmine.Spy} */
let mockAppDataCallback;
let mockPlayer;
let mockReceiverManager;
let mockReceiverApi;
let mockShakaMessageBus;
let mockGenericMessageBus;
/** @type {!jasmine.Spy} */
let mockCanDisplayType;
/** @type {shaka.cast.CastReceiver} */
let receiver;
beforeAll(() => {
// In uncompiled mode, there is a UA check for Chromecast in order to make
// manual testing easier. For these automated tests, we want to act as if
// we are running on the Chromecast, even in Chrome.
// Since we can't write to window.navigator or navigator.userAgent, we use
// Object.defineProperty.
Object.defineProperty(window['navigator'],
'userAgent', {value: 'CrKey', configurable: true});
CastReceiver.POLL_INTERVAL = 0.001;
CastReceiver.IDLE_INTERVAL = 0.001;
});
beforeEach(() => {
mockReceiverApi = createMockReceiverApi();
mockCanDisplayType = jasmine.createSpy('canDisplayType');
mockCanDisplayType.and.returnValue(false);
// We're using quotes to access window.cast because the compiler
// knows about lots of Cast-specific APIs we aren't mocking. We
// don't need this mock strictly type-checked.
window['cast'] = {
receiver: mockReceiverApi,
__platform__: {canDisplayType: mockCanDisplayType},
};
mockReceiverManager = createMockReceiverManager();
mockShakaMessageBus = createMockMessageBus();
mockGenericMessageBus = createMockMessageBus();
mockVideo = new shaka.test.FakeVideo();
mockPlayer = createMockPlayer();
mockAppDataCallback = jasmine.createSpy('appDataCallback');
});
afterEach(async () => {
if (receiver) {
await receiver.destroy();
}
});
afterAll(() => {
if (originalUserAgent) {
window['cast'] = originalCast;
Object.defineProperty(window['navigator'],
'userAgent', {value: originalUserAgent});
}
CastReceiver.POLL_INTERVAL = originalPollInterval;
CastReceiver.IDLE_INTERVAL = originalIdleInterval;
});
describe('constructor', () => {
it('starts the receiver manager', () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
expect(mockReceiverManager.start).toHaveBeenCalled();
});
it('listens for video and player events', () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
expect(Object.keys(mockVideo.on).length).toBeGreaterThan(0);
expect(Object.keys(mockPlayer.listeners).length).toBeGreaterThan(0);
});
it('limits streams to 1080p on Chromecast v1 and v2', () => {
// Simulate the canDisplayType reponse of Chromecast v1 or v2
mockCanDisplayType.and.callFake((type) => {
const matches = /height=(\d+)/.exec(type);
const height = parseInt(matches[1], 10);
if (height && height > 1080) {
return false;
}
return true;
});
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
expect(mockCanDisplayType).toHaveBeenCalled();
expect(mockPlayer.setMaxHardwareResolution)
.toHaveBeenCalledWith(1920, 1080);
});
it('limits streams to 4k on Chromecast Ultra', () => {
// Simulate the canDisplayType reponse of Chromecast Ultra
mockCanDisplayType.and.callFake((type) => {
const matches = /height=(\d+)/.exec(type);
const height = parseInt(matches[1], 10);
if (height && height > 2160) {
return false;
}
return true;
});
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
expect(mockCanDisplayType).toHaveBeenCalled();
expect(mockPlayer.setMaxHardwareResolution)
.toHaveBeenCalledWith(3840, 2160);
});
it('does not start polling', () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
expect(mockPlayer.getConfiguration).not.toHaveBeenCalled();
expect(mockShakaMessageBus.messages.length).toBe(0);
});
});
describe('isConnected', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('is true when there are senders', () => {
expect(receiver.isConnected()).toBe(false);
fakeConnectedSenders(1);
expect(receiver.isConnected()).toBe(true);
fakeConnectedSenders(2);
expect(receiver.isConnected()).toBe(true);
fakeConnectedSenders(99);
expect(receiver.isConnected()).toBe(true);
fakeConnectedSenders(0);
expect(receiver.isConnected()).toBe(false);
});
});
describe('"caststatuschanged" event', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('triggers when senders connect or disconnect', async () => {
/** @type {!jasmine.Spy} */
const listener = jasmine.createSpy('listener');
receiver.addEventListener('caststatuschanged', Util.spyFunc(listener));
await shaka.test.Util.shortDelay();
expect(listener).not.toHaveBeenCalled();
fakeConnectedSenders(1);
await shaka.test.Util.shortDelay();
expect(listener).toHaveBeenCalled();
listener.calls.reset();
mockReceiverManager.onSenderDisconnected();
await shaka.test.Util.shortDelay();
expect(listener).toHaveBeenCalled();
});
it('triggers when idle state changes', async () => {
/** @type {!jasmine.Spy} */
const listener = jasmine.createSpy('listener');
receiver.addEventListener('caststatuschanged', Util.spyFunc(listener));
const fakeLoadingEvent = {type: 'loading'};
const fakeUnloadingEvent = {type: 'unloading'};
const fakeEndedEvent = {type: 'ended'};
const fakePlayingEvent = {type: 'playing'};
await shaka.test.Util.shortDelay();
expect(listener).not.toHaveBeenCalled();
expect(receiver.isIdle()).toBe(true);
mockPlayer.listeners['loading'](fakeLoadingEvent);
await shaka.test.Util.shortDelay();
expect(listener).toHaveBeenCalled();
expect(receiver.isIdle()).toBe(false);
listener.calls.reset();
mockPlayer.listeners['unloading'](fakeUnloadingEvent);
await shaka.test.Util.shortDelay();
expect(listener).toHaveBeenCalled();
expect(receiver.isIdle()).toBe(true);
listener.calls.reset();
mockVideo.ended = true;
mockVideo.on['ended'](fakeEndedEvent);
await shaka.test.Util.shortDelay();
expect(listener).toHaveBeenCalled();
listener.calls.reset();
expect(receiver.isIdle()).toBe(true);
mockVideo.ended = false;
mockVideo.on['playing'](fakePlayingEvent);
await Promise.resolve();
expect(listener).toHaveBeenCalled();
expect(receiver.isIdle()).toBe(false);
});
});
describe('local events', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('trigger "update" and "event" messages', () => {
fakeConnectedSenders(1);
// No messages yet.
expect(mockShakaMessageBus.messages).toEqual([]);
const fakeEvent = {type: 'timeupdate'};
mockVideo.on['timeupdate'](fakeEvent);
// There are now some number of "update" and "event" messages, in that
// order.
expect(mockShakaMessageBus.messages).toContain({
type: 'update',
update: jasmine.any(Object),
});
expect(mockShakaMessageBus.messages).toContain({
type: 'event',
targetName: 'video',
event: jasmine.objectContaining(fakeEvent),
});
const eventIndex = mockShakaMessageBus.messages.findIndex(
(message) => message.type == 'event');
expect(eventIndex).toBe(mockShakaMessageBus.messages.length - 1);
});
});
describe('"init" message', () => {
/** @const */
const fakeConfig = {key: 'value'};
/** @const */
const fakeAppData = {myFakeAppData: 1234};
let fakeInitState;
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeInitState = {
manifest: null,
startTime: null,
player: {
configure: fakeConfig,
},
playerAfterLoad: {
setTextTrackVisibility: true,
},
video: {
loop: true,
playbackRate: 5,
},
};
});
it('sets initial state', async () => {
expect(mockVideo.loop).toBe(false);
expect(mockVideo.playbackRate).toBe(1);
expect(mockPlayer.configure).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
// Initial Player state first:
expect(mockPlayer.configure).toHaveBeenCalledWith(fakeConfig);
// App data next:
expect(mockAppDataCallback).toHaveBeenCalledWith(fakeAppData);
// 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(
fakeInitState.playerAfterLoad.setTextTrackVisibility);
expect(mockVideo.loop).toBe(fakeInitState.video.loop);
expect(mockVideo.playbackRate).toBe(fakeInitState.video.playbackRate);
});
it('starts polling', () => {
const fakeConfig = {key: 'value'};
mockPlayer.getConfiguration.and.returnValue(fakeConfig);
fakeConnectedSenders(1);
mockPlayer.getConfiguration.calls.reset();
expect(mockShakaMessageBus.messages.length).toBe(0);
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
expect(mockPlayer.getConfiguration).toHaveBeenCalled();
expect(mockShakaMessageBus.messages).toContain(jasmine.objectContaining({
type: 'update',
update: jasmine.objectContaining({
player: jasmine.objectContaining({
getConfiguration: fakeConfig,
}),
}),
}));
});
it('doesn\'t poll live methods while loading a VOD', () => {
mockPlayer.getConfiguration.and.returnValue({key: 'value'});
mockPlayer.isLive.and.returnValue(false);
fakeConnectedSenders(1);
expect(mockShakaMessageBus.messages.length).toBe(0);
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
expect(mockPlayer.getPlayheadTimeAsDate).not.toHaveBeenCalled();
});
it('does poll live methods while loading a livestream', () => {
mockPlayer.getConfiguration.and.returnValue({key: 'value'});
mockPlayer.isLive.and.returnValue(true);
fakeConnectedSenders(1);
expect(mockShakaMessageBus.messages.length).toBe(0);
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
expect(mockPlayer.getPlayheadTimeAsDate).toHaveBeenCalled();
});
it('loads the manifest', () => {
fakeInitState.startTime = 12;
fakeInitState.manifest = 'foo://bar';
expect(mockPlayer.load).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
expect(mockPlayer.load).toHaveBeenCalledWith('foo://bar', 12);
});
it('plays the video after loading', async () => {
fakeInitState.manifest = 'foo://bar';
mockVideo.autoplay = true;
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
// 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 () => {
fakeInitState.manifest = null;
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
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(
fakeInitState.playerAfterLoad.setTextTrackVisibility);
expect(mockVideo.loop).toBe(fakeInitState.video.loop);
expect(mockVideo.playbackRate).toBe(fakeInitState.video.playbackRate);
});
it('triggers an "error" event if load fails', async () => {
fakeInitState.manifest = '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));
const listener = jasmine.createSpy('listener');
mockPlayer.addEventListener('error', listener);
expect(listener).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'init',
initState: fakeInitState,
appData: fakeAppData,
}, mockShakaMessageBus);
await shaka.test.Util.shortDelay();
expect(mockPlayer.load).toHaveBeenCalled();
expect(mockPlayer.dispatchEvent).toHaveBeenCalledWith(
jasmine.objectContaining({type: 'error', detail: fakeError}));
});
});
describe('"appData" message', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('triggers the app data callback', () => {
expect(mockAppDataCallback).not.toHaveBeenCalled();
const fakeAppData = {myFakeAppData: 1234};
fakeIncomingMessage({
type: 'appData',
appData: fakeAppData,
}, mockShakaMessageBus);
expect(mockAppDataCallback).toHaveBeenCalledWith(fakeAppData);
});
});
describe('"set" message', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('sets local properties', () => {
expect(mockVideo.currentTime).toBe(0);
fakeIncomingMessage({
type: 'set',
targetName: 'video',
property: 'currentTime',
value: 12,
}, mockShakaMessageBus);
expect(mockVideo.currentTime).toBe(12);
expect(mockPlayer['arbitraryName']).toBe(undefined);
fakeIncomingMessage({
type: 'set',
targetName: 'player',
property: 'arbitraryName',
value: 'arbitraryValue',
}, mockShakaMessageBus);
expect(mockPlayer['arbitraryName']).toBe('arbitraryValue');
});
it('routes volume properties to the receiver manager', () => {
expect(mockVideo.volume).toBe(1);
expect(mockVideo.muted).toBe(false);
expect(mockReceiverManager.setSystemVolumeLevel).not.toHaveBeenCalled();
expect(mockReceiverManager.setSystemVolumeMuted).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'set',
targetName: 'video',
property: 'volume',
value: 0.5,
}, mockShakaMessageBus);
fakeIncomingMessage({
type: 'set',
targetName: 'video',
property: 'muted',
value: true,
}, mockShakaMessageBus);
expect(mockVideo.volume).toBe(1);
expect(mockVideo.muted).toBe(false);
expect(mockReceiverManager.setSystemVolumeLevel)
.toHaveBeenCalledWith(0.5);
expect(mockReceiverManager.setSystemVolumeMuted)
.toHaveBeenCalledWith(true);
});
});
describe('"call" message', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('calls local methods', () => {
expect(mockVideo.play).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'call',
targetName: 'video',
methodName: 'play',
args: [1, 2, 3],
}, mockShakaMessageBus);
expect(mockVideo.play).toHaveBeenCalledWith(1, 2, 3);
expect(mockPlayer.configure).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'call',
targetName: 'player',
methodName: 'configure',
args: [42],
}, mockShakaMessageBus);
expect(mockPlayer.configure).toHaveBeenCalledWith(42);
});
});
describe('"asyncCall" message', () => {
/** @const */
const fakeSenderId = 'senderId';
/** @const */
const fakeCallId = '5';
/** @type {!shaka.util.PublicPromise} */
let p;
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeConnectedSenders(1);
p = new shaka.util.PublicPromise();
mockPlayer.load.and.returnValue(p);
expect(mockPlayer.load).not.toHaveBeenCalled();
fakeIncomingMessage({
type: 'asyncCall',
id: fakeCallId,
targetName: 'player',
methodName: 'load',
args: ['foo://bar', 12],
}, mockShakaMessageBus, fakeSenderId);
});
it('calls local async methods', () => {
expect(mockPlayer.load).toHaveBeenCalledWith('foo://bar', 12);
p.resolve();
});
it('sends "asyncComplete" replies when resolved', async () => {
// No messages have been sent, either broadcast or privately.
expect(mockShakaMessageBus.broadcast).not.toHaveBeenCalled();
expect(mockShakaMessageBus.getCastChannel).not.toHaveBeenCalled();
p.resolve();
await shaka.test.Util.shortDelay();
// No broadcast messages have been sent, but a private message has
// been sent to the sender who started the async call.
expect(mockShakaMessageBus.broadcast).not.toHaveBeenCalled();
expect(mockShakaMessageBus.getCastChannel).toHaveBeenCalledWith(
fakeSenderId);
const senderChannel = mockShakaMessageBus.getCastChannel();
expect(senderChannel.messages).toEqual([{
type: 'asyncComplete',
id: fakeCallId,
error: null,
}]);
});
it('sends "asyncComplete" replies when rejected', async () => {
// No messages have been sent, either broadcast or privately.
expect(mockShakaMessageBus.broadcast).not.toHaveBeenCalled();
expect(mockShakaMessageBus.getCastChannel).not.toHaveBeenCalled();
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);
p.reject(fakeError);
await shaka.test.Util.shortDelay();
// No broadcast messages have been sent, but a private message has
// been sent to the sender who started the async call.
expect(mockShakaMessageBus.broadcast).not.toHaveBeenCalled();
expect(mockShakaMessageBus.getCastChannel).toHaveBeenCalledWith(
fakeSenderId);
const senderChannel = mockShakaMessageBus.getCastChannel();
expect(senderChannel.messages).toEqual([{
type: 'asyncComplete',
id: fakeCallId,
error: jasmine.any(Object),
}]);
if (senderChannel.messages.length) {
const error = senderChannel.messages[0].error;
shaka.test.Util.expectToEqualError(fakeError, error);
}
});
});
describe('sends duration', () => {
beforeEach(async () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeConnectedSenders(1);
mockPlayer.load.and.callFake(() => {
mockVideo.duration = 1;
mockPlayer.getAssetUri = () => 'URI A';
return Promise.resolve();
});
fakeIncomingMessage({
type: 'init',
initState: {manifest: 'URI A'},
appData: {},
}, mockShakaMessageBus);
// The messages will show up asychronously:
await Util.shortDelay();
expectMediaInfo('URI A', 1);
mockGenericMessageBus.messages = [];
});
it('only once, if nothing else changes', async () => {
await Util.shortDelay();
expect(mockGenericMessageBus.messages.length).toBe(0);
});
it('after new sender connects', async () => {
fakeConnectedSenders(1);
await Util.shortDelay();
expectMediaInfo('URI A', 1);
expect(mockGenericMessageBus.messages.length).toBe(0);
});
it('for correct manifest after loading new', async () => {
// Change media information, but only after a delay.
mockPlayer.load.and.callFake(async () => {
await Util.shortDelay();
mockVideo.duration = 2;
mockPlayer.getAssetUri = () => 'URI B';
});
fakeIncomingMessage({
type: 'asyncCall',
id: '5',
targetName: 'player',
methodName: 'load',
args: ['URI B'],
}, mockShakaMessageBus, 'senderId');
// Wait for the mockPlayer to finish 'loading' before checking again.
await Util.shortDelay();
await Util.shortDelay(); // Delay again for the delay in "load" above.
expectMediaInfo('URI B', 2); // pollAttributes_
expect(mockGenericMessageBus.messages.length).toBe(0);
});
it('after LOAD system message', async () => {
mockPlayer.load.and.callFake(() => {
mockVideo.duration = 2;
mockPlayer.getAssetUri = () => 'URI B';
return Promise.resolve();
});
const message = {
// Arbitrary number
'requestId': 0,
'type': 'LOAD',
'autoplay': false,
'currentTime': 10,
'media': {
'contentId': 'URI B',
'contentType': 'video/mp4',
'streamType': 'BUFFERED',
},
};
fakeIncomingMessage(message, mockGenericMessageBus);
await Util.shortDelay();
expectMediaInfo('URI B', 2);
expect(mockGenericMessageBus.messages.length).toBe(0);
});
});
describe('respects generic control messages', () => {
beforeEach(async () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeConnectedSenders(1);
mockPlayer.load.and.callFake(() => {
mockVideo.duration = 1;
mockPlayer.getAssetUri = () => 'URI A';
return Promise.resolve();
});
fakeIncomingMessage({
type: 'init',
initState: {manifest: 'URI A'},
appData: {},
}, mockShakaMessageBus);
// The messages will show up asychronously:
await Util.shortDelay();
expectMediaInfo('URI A', 1);
mockGenericMessageBus.messages = [];
});
it('get status', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'GET_STATUS',
};
mockGenericMessageBus.broadcast.calls.reset();
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockGenericMessageBus.broadcast).toHaveBeenCalledTimes(1);
// This covers the lack of scrubber in the Google Home app, as described
// in https://github.com/shaka-project/shaka-player/issues/2606
expectMediaInfo('URI A', 1);
});
it('play', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'PLAY',
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockVideo.play).toHaveBeenCalled();
});
it('pause', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'PAUSE',
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockVideo.pause).toHaveBeenCalled();
});
it('seek', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'SEEK',
'resumeState': 'PLAYBACK_START',
'currentTime': 10,
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockVideo.play).toHaveBeenCalled();
expect(mockVideo.currentTime).toBe(10);
});
it('stop', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'STOP',
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockPlayer.unload).toHaveBeenCalled();
});
it('volume', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'VOLUME',
'volume': {
'level': 0.5,
'muted': true,
},
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockVideo.volume).toBe(0.5);
expect(mockVideo.muted).toBe(true);
});
it('load', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'LOAD',
'autoplay': false,
'currentTime': 10,
'media': {
'contentId': 'manifestUri',
'contentType': 'video/mp4',
'streamType': 'BUFFERED',
},
};
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockPlayer.load).toHaveBeenCalled();
});
it('dispatches error on unrecognized request type', () => {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'UNKNOWN_TYPE',
};
mockGenericMessageBus.broadcast.calls.reset();
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockGenericMessageBus.broadcast).toHaveBeenCalledTimes(1);
expect(mockGenericMessageBus.messages).toEqual(jasmine.arrayContaining([
jasmine.objectContaining({
requestId: 0,
type: 'INVALID_REQUEST',
reason: 'INVALID_COMMAND',
}),
]));
});
});
describe('content metadata methods', () => {
beforeEach(async () => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeConnectedSenders(1);
mockPlayer.load.and.callFake(() => {
mockVideo.duration = 1;
mockPlayer.getAssetUri = () => 'URI A';
return Promise.resolve();
});
fakeIncomingMessage({
type: 'init',
initState: {manifest: 'URI A'},
appData: {},
}, mockShakaMessageBus);
// The messages will show up asychronously:
await Util.shortDelay();
expectMediaInfo('URI A', 1);
mockGenericMessageBus.messages = [];
});
it('setContentMetadata sets arbitrary metadata', () => {
// In reality, this should follow one of the object formats defined by the
// Cast SDK. For unit testing, it can be anything.
const metadata = {
foo: 42,
bar: 'any old bar',
};
receiver.setContentMetadata(metadata);
getStatus();
expectMediaInfo('URI A', 1, metadata);
});
it('setContentTitle sets the title individually', () => {
const title = 'Title of the Song';
receiver.setContentTitle(title);
const expectedMetadata = {
metadataType: cast.receiver.media.MetadataType.GENERIC,
title,
};
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
it('setContentImage sets the image individually', () => {
const imageUrl = 'https://i.ytimg.com/vi/281ax7Ovlsg/hqdefault.jpg';
receiver.setContentImage(imageUrl);
const expectedMetadata = {
metadataType: cast.receiver.media.MetadataType.GENERIC,
images: [
{url: imageUrl},
],
};
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
it('setContentArtist sets the artist individually', () => {
const artist = 'Da Vinci\'s Notebook';
receiver.setContentArtist(artist);
const expectedMetadata = {
metadataType: cast.receiver.media.MetadataType.MUSIC_TRACK,
artist,
};
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
it('setContentArtist creates music metadata after title', () => {
const title = 'Title of the Song';
const artist = 'Da Vinci\'s Notebook';
// https://www.youtube.com/watch?v=734wnHnnNR4
receiver.setContentTitle(title);
receiver.setContentArtist(artist);
const expectedMetadata = {
metadataType: cast.receiver.media.MetadataType.MUSIC_TRACK,
title,
artist,
};
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
it('setContentArtist creates music metadata before title', () => {
const title = 'Title of the Song';
const artist = 'Da Vinci\'s Notebook';
// https://www.youtube.com/watch?v=734wnHnnNR4
receiver.setContentArtist(artist);
receiver.setContentTitle(title);
const expectedMetadata = {
metadataType: cast.receiver.media.MetadataType.MUSIC_TRACK,
title,
artist,
};
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
it('clearContentMetadata clears all metadata', () => {
const title = 'Title of the Song';
receiver.setContentTitle(title);
receiver.clearContentMetadata();
const expectedMetadata = undefined;
getStatus();
expectMediaInfo('URI A', 1, expectedMetadata);
});
function getStatus() {
const message = {
// Arbitrary number
'requestId': 0,
'type': 'GET_STATUS',
};
mockGenericMessageBus.broadcast.calls.reset();
fakeIncomingMessage(message, mockGenericMessageBus);
expect(mockGenericMessageBus.broadcast).toHaveBeenCalledTimes(1);
}
});
describe('destroy', () => {
beforeEach(() => {
receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
});
it('destroys the local player', async () => {
expect(mockPlayer.destroy).not.toHaveBeenCalled();
await receiver.destroy();
expect(mockPlayer.destroy).toHaveBeenCalled();
});
it('stops polling', async () => {
// Start polling:
fakeIncomingMessage({
type: 'init',
initState: {},
appData: {},
}, mockShakaMessageBus);
mockPlayer.getConfiguration.calls.reset();
await shaka.test.Util.shortDelay();
// We have polled at least once, so this getter has been called.
expect(mockPlayer.getConfiguration).toHaveBeenCalled();
mockPlayer.getConfiguration.calls.reset();
// Destroy the receiver.
await receiver.destroy();
// Wait another second.
await shaka.test.Util.shortDelay();
// We have not polled again since destruction.
expect(mockPlayer.getConfiguration).not.toHaveBeenCalled();
});
it('stops the receiver manager', async () => {
expect(mockReceiverManager.stop).not.toHaveBeenCalled();
await receiver.destroy();
expect(mockReceiverManager.stop).toHaveBeenCalled();
});
});
function createMockReceiverApi() {
return {
CastReceiverManager: {
getInstance: () => mockReceiverManager,
},
media: {
// Defined by the SDK, but we aren't loading it here.
MetadataType: {
GENERIC: 0,
MUSIC_TRACK: 3,
},
},
};
}
function createMockReceiverManager() {
return {
start: jasmine.createSpy('CastReceiverManager.start'),
stop: jasmine.createSpy('CastReceiverManager.stop'),
setSystemVolumeLevel:
jasmine.createSpy('CastReceiverManager.setSystemVolumeLevel'),
setSystemVolumeMuted:
jasmine.createSpy('CastReceiverManager.setSystemVolumeMuted'),
getSenders: jasmine.createSpy('CastReceiverManager.getSenders'),
getSystemVolume: () => ({level: 1, muted: false}),
getCastMessageBus: (namespace) => {
if (namespace == shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE) {
return mockShakaMessageBus;
}
return mockGenericMessageBus;
},
};
}
function createMockMessageBus() {
const bus = {
messages: [],
broadcast: jasmine.createSpy('CastMessageBus.broadcast'),
getCastChannel: jasmine.createSpy('CastMessageBus.getCastChannel'),
};
// For convenience, deserialize and store sent messages.
bus.broadcast.and.callFake((message) => {
bus.messages.push(CastUtils.deserialize(message));
});
const channel = {
messages: [],
send: (message) => {
channel.messages.push(CastUtils.deserialize(message));
},
};
bus.getCastChannel.and.returnValue(channel);
return bus;
}
function createMockPlayer() {
const player = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
setMaxHardwareResolution: jasmine.createSpy('setMaxHardwareResolution'),
addEventListener: (eventName, listener) => {
player.listeners[eventName] = listener;
},
removeEventListener: (eventName, listener) => {
player.listeners[eventName] = null;
},
dispatchEvent: jasmine.createSpy('dispatchEvent'),
// For convenience:
listeners: {},
};
for (const name of CastUtils.PlayerVoidMethods) {
player[name] = jasmine.createSpy(name);
}
for (const name in CastUtils.PlayerGetterMethods) {
player[name] = jasmine.createSpy(name);
}
for (const name in CastUtils.LargePlayerGetterMethods) {
player[name] = jasmine.createSpy(name);
}
for (const name in CastUtils.PlayerGetterMethodsThatRequireLive) {
player[name] = jasmine.createSpy(name);
}
for (const name of CastUtils.PlayerPromiseMethods) {
player[name] = jasmine.createSpy(name).and.returnValue(Promise.resolve());
}
return player;
}
/**
* @param {number} num
*/
function fakeConnectedSenders(num) {
const senderArray = [];
while (num--) {
senderArray.push('senderId');
}
mockReceiverManager.getSenders.and.returnValue(senderArray);
mockReceiverManager.onSenderConnected();
}
/**
* @param {*} message
* @param {!cast.receiver.CastMessageBus} bus
* @param {string=} senderId
*/
function fakeIncomingMessage(message, bus, senderId) {
const serialized = CastUtils.serialize(message);
const messageEvent = {
senderId: senderId,
data: serialized,
};
bus.onMessage(messageEvent);
}
/**
* @param {string} expectedUri
* @param {number} expectedDuration
* @param {Object=} metadata
*/
function expectMediaInfo(expectedUri, expectedDuration, metadata=null) {
expect(mockGenericMessageBus.messages.length).toBeGreaterThan(0);
if (mockGenericMessageBus.messages.length == 0) {
return;
}
const expectedMedia = {
contentId: expectedUri,
streamType: 'BUFFERED',
duration: expectedDuration,
contentType: '',
};
if (metadata) {
// This field only sometimes shows up.
// Setting it to null would cause the check below to fail.
expectedMedia.metadata = metadata;
}
expect(mockGenericMessageBus.messages[0]).toEqual({
requestId: 0,
type: 'MEDIA_STATUS',
status: [jasmine.objectContaining({
media: expectedMedia,
})],
});
mockGenericMessageBus.messages.shift();
}
});