mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
7e6a0f38ff
This corrects/normalizes license headers in misc. files, such as config files, docs, build tools, tests, and externs. This does not affect the compiled output, and is only done for consistency. Issue #2638 Change-Id: I9d8da2de55243b08d7df2b743aac73c6f15e858a
959 lines
30 KiB
JavaScript
959 lines
30 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
describe('CastSender', () => {
|
|
const CastSender = shaka.cast.CastSender;
|
|
const CastUtils = shaka.cast.CastUtils;
|
|
const Util = shaka.test.Util;
|
|
|
|
const originalChrome = window['chrome'];
|
|
const originalStatusDelay = shaka.cast.CastSender.STATUS_DELAY;
|
|
|
|
const fakeAppId = 'asdf';
|
|
const fakeInitState = {
|
|
manifest: null,
|
|
player: null,
|
|
startTime: null,
|
|
video: null,
|
|
};
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let onStatusChanged;
|
|
/** @type {!jasmine.Spy} */
|
|
let onFirstCastStateUpdate;
|
|
let onRemoteEvent;
|
|
let onResumeLocal;
|
|
let onInitStateRequired;
|
|
|
|
let mockCastApi;
|
|
let mockSession;
|
|
|
|
/** @type {shaka.cast.CastSender} */
|
|
let sender;
|
|
|
|
beforeEach(() => {
|
|
onStatusChanged = jasmine.createSpy('onStatusChanged');
|
|
onFirstCastStateUpdate = jasmine.createSpy('onFirstCastStateUpdate');
|
|
onRemoteEvent = jasmine.createSpy('onRemoteEvent');
|
|
onResumeLocal = jasmine.createSpy('onResumeLocal');
|
|
onInitStateRequired = jasmine.createSpy('onInitStateRequired')
|
|
.and.returnValue(fakeInitState);
|
|
|
|
mockCastApi = createMockCastApi();
|
|
// We're using quotes to access window.chrome because the compiler
|
|
// knows about lots of Chrome-specific APIs we aren't mocking. We
|
|
// don't need this mock strictly type-checked.
|
|
window['chrome'] = {cast: mockCastApi};
|
|
mockSession = null;
|
|
|
|
sender = new CastSender(
|
|
fakeAppId, Util.spyFunc(onStatusChanged),
|
|
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
|
|
Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await sender.destroy();
|
|
resetClassVariables();
|
|
});
|
|
|
|
beforeAll(() => {
|
|
shaka.cast.CastSender.STATUS_DELAY = 0;
|
|
});
|
|
|
|
afterAll(() => {
|
|
window['chrome'] = originalChrome;
|
|
shaka.cast.CastSender.STATUS_DELAY = originalStatusDelay;
|
|
});
|
|
|
|
describe('init', () => {
|
|
it('installs a callback if the cast API is not available', () => {
|
|
// Remove the mock cast API.
|
|
delete window['chrome'].cast;
|
|
|
|
// Init and expect that apiReady is false and no status is available.
|
|
sender.init();
|
|
expect(sender.apiReady()).toBe(false);
|
|
expect(onStatusChanged).not.toHaveBeenCalled();
|
|
|
|
// Restore the mock cast API.
|
|
window['chrome'].cast = mockCastApi;
|
|
simulateSdkLoaded();
|
|
|
|
// Expect the API to be ready and initialized.
|
|
expect(sender.apiReady()).toBe(true);
|
|
expect(sender.hasReceivers()).toBe(false);
|
|
expect(onStatusChanged).toHaveBeenCalled();
|
|
expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(fakeAppId);
|
|
expect(mockCastApi.initialize).toHaveBeenCalled();
|
|
});
|
|
|
|
it('sets up cast API right away if it is available', () => {
|
|
sender.init();
|
|
// Expect the API to be ready and initialized.
|
|
expect(sender.apiReady()).toBe(true);
|
|
expect(sender.hasReceivers()).toBe(false);
|
|
expect(onStatusChanged).toHaveBeenCalled();
|
|
expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(fakeAppId);
|
|
expect(mockCastApi.initialize).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('hasReceivers', () => {
|
|
it('reflects the most recent receiver status', () => {
|
|
sender.init();
|
|
expect(sender.hasReceivers()).toBe(false);
|
|
|
|
fakeReceiverAvailability(true);
|
|
expect(sender.hasReceivers()).toBe(true);
|
|
|
|
fakeReceiverAvailability(false);
|
|
expect(sender.hasReceivers()).toBe(false);
|
|
});
|
|
|
|
it('remembers status from previous senders', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
await sender.destroy();
|
|
|
|
sender = new CastSender(
|
|
fakeAppId, Util.spyFunc(onStatusChanged),
|
|
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
|
|
Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
|
|
sender.init();
|
|
// You get an initial call to onStatusChanged when it initializes.
|
|
expect(onStatusChanged).toHaveBeenCalledTimes(3);
|
|
await Util.shortDelay();
|
|
|
|
// And then you get another call after it has 'discovered' the
|
|
// existing receivers.
|
|
expect(sender.hasReceivers()).toBe(true);
|
|
expect(onStatusChanged).toHaveBeenCalledTimes(4);
|
|
});
|
|
});
|
|
|
|
describe('cast', () => {
|
|
it('fails when the cast API is not ready', async () => {
|
|
mockCastApi.isAvailable = false;
|
|
sender.init();
|
|
expect(sender.apiReady()).toBe(false);
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.CAST,
|
|
shaka.util.Error.Code.CAST_API_UNAVAILABLE));
|
|
await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected);
|
|
});
|
|
|
|
it('fails when there are no receivers', async () => {
|
|
sender.init();
|
|
expect(sender.apiReady()).toBe(true);
|
|
expect(sender.hasReceivers()).toBe(false);
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.CAST,
|
|
shaka.util.Error.Code.NO_CAST_RECEIVERS));
|
|
await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected);
|
|
});
|
|
|
|
it('creates a session and sends an "init" message', async () => {
|
|
sender.init();
|
|
expect(sender.apiReady()).toBe(true);
|
|
fakeReceiverAvailability(true);
|
|
expect(sender.hasReceivers()).toBe(true);
|
|
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
|
|
await p;
|
|
expect(onStatusChanged).toHaveBeenCalled();
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'init',
|
|
initState: fakeInitState,
|
|
}));
|
|
});
|
|
|
|
// The library is not loaded yet during describe(), so we can't refer to
|
|
// Shaka error codes by name here. Instead, we use the numeric value and
|
|
// put the name in a comment.
|
|
const connectionFailures = [
|
|
{
|
|
condition: 'canceled by the user',
|
|
castErrorCode: 'cancel',
|
|
shakaErrorCode: shaka.util.Error.Code.CAST_CANCELED_BY_USER,
|
|
},
|
|
{
|
|
condition: 'the connection times out',
|
|
castErrorCode: 'timeout',
|
|
shakaErrorCode: shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT,
|
|
},
|
|
{
|
|
condition: 'the receiver is unavailable',
|
|
castErrorCode: 'receiver_unavailable',
|
|
shakaErrorCode: shaka.util.Error.Code.CAST_RECEIVER_APP_UNAVAILABLE,
|
|
},
|
|
{
|
|
condition: 'an unexpected error occurs',
|
|
castErrorCode: 'anything else',
|
|
shakaErrorCode: shaka.util.Error.Code.UNEXPECTED_CAST_ERROR,
|
|
},
|
|
];
|
|
|
|
for (const metadata of connectionFailures) {
|
|
it('fails when ' + metadata.condition, async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnectionFailure(metadata.castErrorCode);
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.CAST,
|
|
metadata.shakaErrorCode,
|
|
jasmine.anything()));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
});
|
|
}
|
|
|
|
it('fails when we are already casting', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
|
|
await p;
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.CAST,
|
|
shaka.util.Error.Code.ALREADY_CASTING));
|
|
await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected);
|
|
});
|
|
});
|
|
|
|
it('re-uses old sessions', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
const oldMockSession = mockSession;
|
|
await p;
|
|
await sender.destroy();
|
|
|
|
// Reset tracking variables.
|
|
mockCastApi.ApiConfig.calls.reset();
|
|
onStatusChanged.calls.reset();
|
|
oldMockSession.messages = [];
|
|
|
|
// Make a new session, to ensure that the sender is correctly using
|
|
// the previous mock session.
|
|
mockSession = createMockCastSession();
|
|
|
|
sender = new CastSender(
|
|
fakeAppId, Util.spyFunc(onStatusChanged),
|
|
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
|
|
Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
|
|
sender.init();
|
|
|
|
// The sender should automatically rejoin the session, without needing
|
|
// to be told to cast.
|
|
expect(onStatusChanged).toHaveBeenCalled();
|
|
expect(sender.isCasting()).toBe(true);
|
|
|
|
// The message should be on the old session, instead of the new one.
|
|
expect(mockSession.messages.length).toBe(0);
|
|
expect(oldMockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'init',
|
|
initState: fakeInitState,
|
|
}));
|
|
});
|
|
|
|
it('doesn\'t re-use stopped sessions', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
await sender.destroy();
|
|
|
|
mockCastApi.ApiConfig.calls.reset();
|
|
|
|
// The session is stopped in the meantime.
|
|
mockSession.status = chrome.cast.SessionStatus.STOPPED;
|
|
|
|
sender = new CastSender(
|
|
fakeAppId, Util.spyFunc(onStatusChanged),
|
|
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
|
|
Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
|
|
sender.init();
|
|
|
|
expect(sender.isCasting()).toBe(false);
|
|
});
|
|
|
|
it('joins existing sessions automatically', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
fakeJoinExistingSession();
|
|
|
|
await Util.shortDelay();
|
|
expect(onStatusChanged).toHaveBeenCalled();
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(onInitStateRequired).toHaveBeenCalled();
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'init',
|
|
initState: fakeInitState,
|
|
}));
|
|
});
|
|
|
|
describe('setAppData', () => {
|
|
const fakeAppData = {
|
|
myKey1: 'myValue1',
|
|
myKey2: 'myValue2',
|
|
};
|
|
|
|
it('sets "appData" for "init" message if not casting', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
sender.setAppData(fakeAppData);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
|
|
await p;
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'init',
|
|
appData: fakeAppData,
|
|
}));
|
|
});
|
|
|
|
it('sends a special "appData" message if casting', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
// init message has no appData
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'init',
|
|
appData: null,
|
|
}));
|
|
// no appData message yet
|
|
expect(mockSession.messages).not.toContain(jasmine.objectContaining({
|
|
type: 'appData',
|
|
}));
|
|
|
|
sender.setAppData(fakeAppData);
|
|
// now there is an appData message
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'appData',
|
|
appData: fakeAppData,
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('onFirstCastStateUpdate', () => {
|
|
it('is triggered by an "update" message', async () => {
|
|
// You have to join an existing session for it to work.
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
fakeJoinExistingSession();
|
|
|
|
await Util.shortDelay();
|
|
expect(onFirstCastStateUpdate).not.toHaveBeenCalled();
|
|
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(onFirstCastStateUpdate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('is not triggered if making a new session', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(onFirstCastStateUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('is triggered once per existing session', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
fakeJoinExistingSession();
|
|
|
|
await Util.shortDelay();
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(onFirstCastStateUpdate).toHaveBeenCalled();
|
|
onFirstCastStateUpdate.calls.reset();
|
|
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(onFirstCastStateUpdate).not.toHaveBeenCalled();
|
|
onFirstCastStateUpdate.calls.reset();
|
|
|
|
// Disconnect and then connect to another existing session.
|
|
fakeJoinExistingSession();
|
|
await Util.shortDelay();
|
|
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(onFirstCastStateUpdate).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('onRemoteEvent', () => {
|
|
it('is triggered by an "event" message', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
const fakeEvent = {
|
|
type: 'eventName',
|
|
detail: {key1: 'value1'},
|
|
};
|
|
fakeSessionMessage({
|
|
type: 'event',
|
|
targetName: 'video',
|
|
event: fakeEvent,
|
|
});
|
|
|
|
expect(onRemoteEvent).toHaveBeenCalledWith(
|
|
'video', jasmine.objectContaining(fakeEvent));
|
|
});
|
|
});
|
|
|
|
describe('onResumeLocal', () => {
|
|
it('is triggered when casting ends', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(onResumeLocal).not.toHaveBeenCalled();
|
|
|
|
fakeRemoteDisconnect();
|
|
expect(sender.isCasting()).toBe(false);
|
|
expect(onResumeLocal).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('showDisconnectDialog', () => {
|
|
it('opens the dialog if we are casting', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(mockSession.leave).not.toHaveBeenCalled();
|
|
expect(mockSession.stop).not.toHaveBeenCalled();
|
|
mockCastApi.requestSession.calls.reset();
|
|
|
|
sender.showDisconnectDialog();
|
|
|
|
// this call opens the dialog:
|
|
expect(mockCastApi.requestSession).toHaveBeenCalled();
|
|
// these were not used:
|
|
expect(mockSession.leave).not.toHaveBeenCalled();
|
|
expect(mockSession.stop).not.toHaveBeenCalled();
|
|
|
|
fakeRemoteDisconnect();
|
|
});
|
|
});
|
|
|
|
describe('get', () => {
|
|
it('returns most recent properties from "update" messages', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
const update = {
|
|
video: {
|
|
currentTime: 12,
|
|
paused: false,
|
|
},
|
|
player: {
|
|
isBuffering: true,
|
|
seekRange: {start: 5, end: 17},
|
|
},
|
|
};
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: update,
|
|
});
|
|
|
|
// These are properties:
|
|
expect(sender.get('video', 'currentTime')).toBe(
|
|
update.video.currentTime);
|
|
expect(sender.get('video', 'paused')).toBe(
|
|
update.video.paused);
|
|
|
|
// These are getter methods:
|
|
expect(sender.get('player', 'isBuffering')()).toBe(
|
|
update.player.isBuffering);
|
|
expect(sender.get('player', 'seekRange')()).toEqual(
|
|
update.player.seekRange);
|
|
});
|
|
|
|
it('returns functions for video and player methods', () => {
|
|
sender.init();
|
|
expect(sender.get('video', 'play')).toEqual(jasmine.any(Function));
|
|
expect(sender.get('player', 'isLive')).toEqual(jasmine.any(Function));
|
|
expect(sender.get('player', 'configure')).toEqual(jasmine.any(Function));
|
|
expect(sender.get('player', 'load')).toEqual(jasmine.any(Function));
|
|
});
|
|
|
|
it('simple methods trigger "call" messages', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
const method = sender.get('video', 'play');
|
|
const retval = method(123, 'abc');
|
|
expect(retval).toBe(undefined);
|
|
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'call',
|
|
targetName: 'video',
|
|
methodName: 'play',
|
|
args: [123, 'abc'],
|
|
}));
|
|
});
|
|
|
|
describe('async player methods', () => {
|
|
let method;
|
|
|
|
beforeEach(async () => {
|
|
method = null;
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
method = sender.get('player', 'load');
|
|
});
|
|
|
|
it('return Promises', () => {
|
|
const p = method();
|
|
expect(p).toEqual(jasmine.any(Promise));
|
|
p.catch(() => {}); // silence logs about uncaught rejections
|
|
});
|
|
|
|
it('trigger "asyncCall" messages', () => {
|
|
const p = method(123, 'abc');
|
|
p.catch(() => {}); // silence logs about uncaught rejections
|
|
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'asyncCall',
|
|
targetName: 'player',
|
|
methodName: 'load',
|
|
args: [123, 'abc'],
|
|
id: jasmine.any(String),
|
|
}));
|
|
});
|
|
|
|
it('resolve when "asyncComplete" messages are received', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(method(123, 'abc'));
|
|
|
|
// Wait a tick for the Promise status to be set.
|
|
await Util.shortDelay();
|
|
expect(p.status).toBe('pending');
|
|
const id = mockSession.messages[mockSession.messages.length - 1].id;
|
|
fakeSessionMessage({
|
|
type: 'asyncComplete',
|
|
id: id,
|
|
error: null,
|
|
});
|
|
|
|
await expectAsync(p).toBeResolved();
|
|
});
|
|
|
|
it('reject when "asyncComplete" messages have an error', async () => {
|
|
const originalError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE,
|
|
'foo://bar');
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(method(123, 'abc'));
|
|
|
|
// Wait a tick for the Promise status to be set.
|
|
await Util.shortDelay();
|
|
expect(p.status).toBe('pending');
|
|
const id = mockSession.messages[mockSession.messages.length - 1].id;
|
|
fakeSessionMessage({
|
|
type: 'asyncComplete',
|
|
id: id,
|
|
error: originalError,
|
|
});
|
|
|
|
await expectAsync(p).toBeRejectedWith(Util.jasmineError(originalError));
|
|
});
|
|
|
|
it('reject when disconnected remotely', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(method(123, 'abc'));
|
|
|
|
// Wait a tick for the Promise status to be set.
|
|
await Util.shortDelay();
|
|
expect(p.status).toBe('pending');
|
|
fakeRemoteDisconnect();
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.LOAD_INTERRUPTED));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('set', () => {
|
|
it('overrides any cached properties', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
const update = {
|
|
video: {muted: false},
|
|
};
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: update,
|
|
});
|
|
expect(sender.get('video', 'muted')).toBe(false);
|
|
|
|
sender.set('video', 'muted', true);
|
|
expect(sender.get('video', 'muted')).toBe(true);
|
|
});
|
|
|
|
it('causes a "set" message to be sent', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
sender.set('video', 'muted', true);
|
|
expect(mockSession.messages).toContain(jasmine.objectContaining({
|
|
type: 'set',
|
|
targetName: 'video',
|
|
property: 'muted',
|
|
value: true,
|
|
}));
|
|
});
|
|
|
|
it('can be used before we have an "update" message', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
expect(sender.get('video', 'muted')).toBe(undefined);
|
|
sender.set('video', 'muted', true);
|
|
expect(sender.get('video', 'muted')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('hasRemoteProperties', () => {
|
|
it('is true only after we have an "update" message', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const p = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await p;
|
|
|
|
expect(sender.hasRemoteProperties()).toBe(false);
|
|
|
|
fakeSessionMessage({
|
|
type: 'update',
|
|
update: {video: {currentTime: 12}, player: {isLive: false}},
|
|
});
|
|
expect(sender.hasRemoteProperties()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('forceDisconnect', () => {
|
|
it('disconnects and cancels all async operations', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const cast = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await cast;
|
|
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(mockSession.leave).not.toHaveBeenCalled();
|
|
expect(mockSession.stop).not.toHaveBeenCalled();
|
|
expect(mockSession.removeUpdateListener).not.toHaveBeenCalled();
|
|
expect(mockSession.removeMessageListener).not.toHaveBeenCalled();
|
|
|
|
const method = sender.get('player', 'load');
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(method());
|
|
|
|
// Wait a tick for the Promise status to be set.
|
|
await Util.shortDelay();
|
|
expect(p.status).toBe('pending');
|
|
sender.forceDisconnect();
|
|
expect(mockSession.leave).not.toHaveBeenCalled();
|
|
expect(mockSession.stop).toHaveBeenCalled();
|
|
expect(mockSession.removeUpdateListener).toHaveBeenCalled();
|
|
expect(mockSession.removeMessageListener).toHaveBeenCalled();
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.LOAD_INTERRUPTED));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
});
|
|
|
|
it('transfers playback to local device', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const cast = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await cast;
|
|
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(onResumeLocal).not.toHaveBeenCalled();
|
|
|
|
sender.forceDisconnect();
|
|
|
|
expect(sender.isCasting()).toBe(false);
|
|
expect(onResumeLocal).toHaveBeenCalled();
|
|
});
|
|
|
|
it('succeeds even if session.stop() throws', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const cast = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await cast;
|
|
|
|
mockSession.stop.and.throwError(new Error('DISCONNECTED!'));
|
|
|
|
expect(() => sender.forceDisconnect()).not.toThrow(jasmine.anything());
|
|
});
|
|
});
|
|
|
|
describe('sendMessage exception', () => {
|
|
/** @type {Error} */
|
|
let originalException;
|
|
|
|
/** @type {Object} */
|
|
let expectedError;
|
|
|
|
beforeEach(async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const cast = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await cast;
|
|
|
|
originalException = new Error('DISCONNECTED!');
|
|
|
|
expectedError = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.CAST,
|
|
shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT,
|
|
originalException));
|
|
|
|
mockSession.sendMessage.and.throwError(originalException);
|
|
});
|
|
|
|
it('propagates to the caller', () => {
|
|
expect(() => sender.set('video', 'muted', true)).toThrow(expectedError);
|
|
});
|
|
|
|
it('triggers an error event on Player', () => {
|
|
expect(() => sender.set('video', 'muted', true)).toThrow(expectedError);
|
|
|
|
const expectedEvent = jasmine.objectContaining({
|
|
type: 'error',
|
|
detail: expectedError,
|
|
});
|
|
|
|
expect(onRemoteEvent).toHaveBeenCalledWith('player', expectedEvent);
|
|
});
|
|
|
|
it('disconnects the sender', () => {
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(onResumeLocal).not.toHaveBeenCalled();
|
|
|
|
expect(() => sender.set('video', 'muted', true)).toThrow(expectedError);
|
|
|
|
expect(sender.isCasting()).toBe(false);
|
|
expect(onResumeLocal).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
it('cancels all async operations', async () => {
|
|
sender.init();
|
|
fakeReceiverAvailability(true);
|
|
const cast = sender.cast(fakeInitState);
|
|
fakeSessionConnection();
|
|
await cast;
|
|
|
|
expect(sender.isCasting()).toBe(true);
|
|
expect(mockSession.stop).not.toHaveBeenCalled();
|
|
expect(mockSession.removeUpdateListener).not.toHaveBeenCalled();
|
|
expect(mockSession.removeMessageListener).not.toHaveBeenCalled();
|
|
|
|
const method = sender.get('player', 'load');
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(method());
|
|
|
|
// Wait a tick for the Promise status to be set.
|
|
await Util.shortDelay();
|
|
expect(p.status).toBe('pending');
|
|
const destroy = sender.destroy();
|
|
expect(mockSession.leave).not.toHaveBeenCalled();
|
|
expect(mockSession.stop).not.toHaveBeenCalled();
|
|
expect(mockSession.removeUpdateListener).toHaveBeenCalled();
|
|
expect(mockSession.removeMessageListener).toHaveBeenCalled();
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.LOAD_INTERRUPTED));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
await destroy;
|
|
});
|
|
});
|
|
|
|
function createMockCastApi() {
|
|
return {
|
|
isAvailable: true,
|
|
SessionRequest: jasmine.createSpy('chrome.cast.SessionRequest'),
|
|
SessionStatus: {STOPPED: 'stopped'},
|
|
ApiConfig: jasmine.createSpy('chrome.cast.ApiConfig'),
|
|
initialize: jasmine.createSpy('chrome.cast.initialize'),
|
|
requestSession: jasmine.createSpy('chrome.cast.requestSession'),
|
|
};
|
|
}
|
|
|
|
function createMockCastSession() {
|
|
const session = {
|
|
messages: [],
|
|
status: 'connected',
|
|
receiver: {friendlyName: 'SomeDevice'},
|
|
addUpdateListener: jasmine.createSpy('Session.addUpdateListener'),
|
|
removeUpdateListener: jasmine.createSpy('Session.removeUpdateListener'),
|
|
addMessageListener: jasmine.createSpy('Session.addMessageListener'),
|
|
removeMessageListener: jasmine.createSpy('Session.removeMessageListener'),
|
|
leave: jasmine.createSpy('Session.leave'),
|
|
sendMessage: jasmine.createSpy('Session.sendMessage'),
|
|
stop: jasmine.createSpy('Session.stop'),
|
|
};
|
|
|
|
// For convenience, deserialize and store sent messages.
|
|
session.sendMessage.and.callFake(
|
|
(namespace, message, successCallback, errorCallback) => {
|
|
session.messages.push(CastUtils.deserialize(message));
|
|
});
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} yes If true, simulate receivers being available.
|
|
*/
|
|
function fakeReceiverAvailability(yes) {
|
|
expect(mockCastApi.ApiConfig).toHaveBeenCalledTimes(1);
|
|
const onReceiverStatusChanged = mockCastApi.ApiConfig.calls.argsFor(0)[2];
|
|
onReceiverStatusChanged(yes ? 'available' : 'unavailable');
|
|
}
|
|
|
|
function fakeSessionConnection() {
|
|
expect(mockCastApi.requestSession).toHaveBeenCalledTimes(1);
|
|
const onSessionInitiated = mockCastApi.requestSession.calls.argsFor(0)[0];
|
|
mockSession = createMockCastSession();
|
|
onSessionInitiated(mockSession);
|
|
}
|
|
|
|
/**
|
|
* @param {string} code
|
|
*/
|
|
function fakeSessionConnectionFailure(code) {
|
|
expect(mockCastApi.requestSession).toHaveBeenCalledTimes(1);
|
|
const onSessionError = mockCastApi.requestSession.calls.argsFor(0)[1];
|
|
onSessionError({code: code});
|
|
}
|
|
|
|
/**
|
|
* @param {?} message
|
|
*/
|
|
function fakeSessionMessage(message) {
|
|
expect(mockSession.addMessageListener).toHaveBeenCalledTimes(1);
|
|
const namespace = mockSession.addMessageListener.calls.argsFor(0)[0];
|
|
const listener = mockSession.addMessageListener.calls.argsFor(0)[1];
|
|
const serialized = CastUtils.serialize(message);
|
|
listener(namespace, serialized);
|
|
}
|
|
|
|
function fakeRemoteDisconnect() {
|
|
mockSession.status = 'disconnected';
|
|
expect(mockSession.addUpdateListener).toHaveBeenCalledTimes(1);
|
|
const onConnectionStatus =
|
|
mockSession.addUpdateListener.calls.argsFor(0)[0];
|
|
onConnectionStatus();
|
|
}
|
|
|
|
function fakeJoinExistingSession() {
|
|
expect(mockCastApi.ApiConfig).toHaveBeenCalledTimes(1);
|
|
const onJoinExistingSession = mockCastApi.ApiConfig.calls.argsFor(0)[1];
|
|
mockSession = createMockCastSession();
|
|
onJoinExistingSession(mockSession);
|
|
}
|
|
|
|
/**
|
|
* @suppress {visibility}
|
|
* "suppress visibility" has function scope, so this is a mini-function that
|
|
* exists solely to suppress visibility rules for these actions.
|
|
*/
|
|
function resetClassVariables() {
|
|
CastSender.hasReceivers_ = false;
|
|
CastSender.session_ = null;
|
|
}
|
|
|
|
/**
|
|
* @suppress {visibility}
|
|
* "suppress visibility" has function scope, so this is a mini-function that
|
|
* exists solely to suppress visibility rules for these actions.
|
|
*/
|
|
function simulateSdkLoaded() {
|
|
shaka.cast.CastSender.onSdkLoaded_(true);
|
|
}
|
|
});
|