Initial commit

This commit is contained in:
hensm
2018-06-08 04:56:02 +01:00
commit d815fb7af0
70 changed files with 8370 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
"use strict";
export default class ApiConfig {
constructor (
sessionRequest
, sessionListener
, receiverListener
, opt_autoJoinPolicy
, opt_defaultActionPolicy) {
this.autoJoinPolicy
this.receiverListener = receiverListener;
this.sessionListener = sessionListener;
this.sessionRequest = sessionRequest;
}
};

View File

@@ -0,0 +1,12 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.DialRequest
export default class DialRequest {
constructor (
appName
, opt_launchParameter = null) {
this.appName = appName;
this.launchParameter = opt_launchParameter;
}
};

View File

@@ -0,0 +1,14 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Error
export default class Error {
constructor (
code
, opt_description = null
, opt_details = null) {
this.code = code;
this.description = opt_description;
this.details = opt_details;
}
};

View File

@@ -0,0 +1,11 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Image
export default class Image {
width = null;
height = null;
constructor (url) {
this.url = url;
}
};

View File

@@ -0,0 +1,24 @@
"use strict";
import { Capability
, ReceiverType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Receiver
export default class Receiver {
constructor (
label
, friendlyName
, opt_capabilities = [
Capability.VIDEO_OUT
, Capability.AUDIO_OUT ]
, opt_volume = null) {
this.capabilities = opt_capabilities;
this.displayStatus = null;
this.friendlyName = friendlyName;
this.isActiveInput = null;
this.label = label;
this.receiverType = ReceiverType.CAST;
this.volume = opt_volume;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverDisplayStatus
export default class ReceiverDisplayStatus {
constructor (statusText, appImages) {
this.appImages = appImages;
this.showStop = null;
this.statusText = statusText;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SenderApplication
export default class SenderApplication {
constructor (platform) {
this.packageId = null;
this.platform = platform;
this.url = null;
}
};

View File

@@ -0,0 +1,319 @@
"use strict";
import _Error from "./Error";
import Volume from "./Volume";
import Media from "../../media/classes/Media";
import { SessionStatus
, ErrorCode
, VolumeControlType } from "../enums";
import { onMessage, sendMessage } from "../../messageBridge";
import uuid from "uuid/v1";
export default class Session {
constructor (
sessionId
, appId
, displayName
, appImages
, receiver
, successCallback) {
this._id = uuid();
this._messageListeners = new Map();
this._updateListeners = new Set();
this._sendMessageCallbacks = new Map();
this._setReceiverMutedCallbacks = new Map();
this._setReceiverVolumeLevelCallbacks = new Map();
this._stopCallbacks = new Map();
this.sessionId = sessionId;
this.appId = appId;
this.appImages = appImages;
this.displayName = displayName;
this.receiver = receiver;
this.media = [];
this.namespaces = [];
this.senderApps = [];
this.status = SessionStatus.DISCONNECTED;
this.statusText = null;
this._sendMessage("bridge:bridgesession/initialize", {
address: receiver._address
, port: receiver._port
, appId
, sessionId
});
onMessage(message => {
// Filter other session messages
if (message._id && message._id !== this._id) {
return;
}
switch (message.subject) {
case "shim:session/stopped":
this.status = SessionStatus.STOPPED;
this._updateListeners.forEach(listener => listener());
break;
case "shim:session/connected":
this.status = SessionStatus.CONNECTED;
this.sessionId = message.data.sessionId;
this.namespaces = message.data.namespaces;
this.displayName = message.data.displayName;
this.statusText = message.data.statusText;
if (successCallback) {
successCallback(this);
}
break;
case "shim:session/updateStatus":
if (message.data.volume) {
if (!this.receiver.volume) {
const receiverVolume = new Volume(
message.data.volume.level
, message.data.volume.muted);
receiverVolume.controlType = message.data.volume.controlType;
receiverVolume.stepInterval = message.data.volume.stepInterval;
this.receiver.volume = receiverVolume;
} else {
this.receiver.volume.level = message.data.volume.level;
this.receiver.volume.muted = message.data.volume.muted;
}
}
break;
case "shim:session/impl_addMessageListener": {
const { namespace, data } = message.data;
this._messageListeners.get(namespace).forEach(
listener => listener(namespace, data));
break;
}
case "shim:session/impl_sendMessage": {
const { messageId, error } = message.data;
const [ successCallback, errorCallback ]
= this._sendMessageCallbacks.get(messageId)
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
this._sendMessageCallbacks.delete(messageId);
break;
}
case "shim:session/impl_setReceiverMuted": {
const { volumeId, error } = message.data;
const [ successCallback, errorCallback ]
= this._setReceiverMutedCallbacks.get(volumeId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
break;
this._setReceiverMutedCallbacks.delete(volumeId);
}
case "shim:session/impl_setReceiverVolumeLevel": {
const { volumeId, error } = message.data;
const [ successCallback, errorCallback ]
= this._setReceiverVolumeLevelCallbacks.get(volumeId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
this._setReceiverVolumeLevelCallbacks.delete(volumeId);
break;
}
case "shim:session/impl_stop": {
const { stopId, error } = message.data;
const [ successCallback, errorCallback ]
= this._stopCallbacks.get(stopId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else {
this.status = SessionStatus.STOPPED;
this._updateListeners.forEach(listener => listener());
if (successCallback) {
successCallback();
}
}
this._stopCallbacks.delete(stopId);
break;
}
}
});
}
_sendMessage (subject, data = {}) {
sendMessage({
subject
, data
, _id: this._id
});
}
addMediaListener (listener) {
console.info("STUB :: Session#addMediaListener")
}
addMessageListener (namespace, listener) {
if (!this._messageListeners.has(namespace)) {
this._messageListeners.set(namespace, new Set());
}
this._messageListeners.get(namespace).add(listener);
this._sendMessage("bridge:bridgesession/impl_addMessageListener", {
namespace
});
}
addUpdateListener (listener) {
this._updateListeners.add(listener);
}
leave (successCallback, errorCallback) {
const id = uuid();
this._sendMessage("bridge:bridgesession/impl_leave", { id });
this._leaveCallbacks.set(id, [
successCallback
, errorCallback
]);
}
loadMedia (loadRequest, successCallback, errorCallback) {
this.sendMediaMessage({
type: "LOAD"
, requestId: 0
, media: loadRequest.media
, activeTrackIds: loadRequest.activeTrackIds || []
, autoplay: loadRequest.autoplay || false
, currentTime: loadRequest.currentTime || 0
, customData: loadRequest.customData || {}
, repeatMode: "REPEAT_OFF"
});
let hasResponded = false;
this.addMessageListener("urn:x-cast:com.google.cast.media"
, (namespace, data) => {
if (hasResponded) return;
const mediaObject = JSON.parse(data);
if (mediaObject.status && mediaObject.status.length > 0) {
hasResponded = true;
const media = new Media(
this.sessionId
, mediaObject.status[0].mediaSessionId
, this._id);
media.media = loadRequest.media;
this.media = [ media ];
media.play();
successCallback(media);
} else {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
}
})
}
queueLoad () {
console.info("STUB :: Session#queueLoad");
}
removeMediaListener (listener) {
console.info("STUB :: Session#removeMediaListener");
}
removeMessageListener (namespace, listener) {
this._messageListeners.get(namespace).delete(listener);
}
removeUpdateListener (namespace, listener) {
this._updateListeners.delete(listener);
}
sendMediaMessage (message) {
this.sendMessage(
"urn:x-cast:com.google.cast.media"
, message
, () => {}
, () => {});
}
sendMessage (namespace, message, successCallback, errorCallback) {
const messageId = uuid();
this._sendMessage("bridge:bridgesession/impl_sendMessage", {
namespace
, message
, messageId
});
this._sendMessageCallbacks.set(messageId, [
successCallback
, errorCallback
]);
}
setReceiverMuted (muted, successCallback, errorCallback) {
const volumeId = uuid();
this._sendMessage("bridge:bridgesession/impl_setReceiverMuted", {
muted
, volumeId
});
this._setReceiverMutedCallbacks.set(volumeId, [
successCallback
, errorCallback
]);
}
setReceiverVolumeLevel (newLevel, successCallback, errorCallback) {
const volumeId = uuid();
this._sendMessage("bridge:bridgesession/impl_setReceiverVolumeLevel", {
newLevel
, volumeId
});
this._setReceiverVolumeLevelCallbacks.set(volumeId, [
successCallback
, errorCallback
]);
}
stop (successCallback, errorCallback) {
const stopId = uuid();
this._sendMessage("bridge:bridgesession/impl_stop", { stopId });
this._stopCallbacks.set(stopId, [
successCallback
, errorCallback
]);
}
}

View File

@@ -0,0 +1,20 @@
"use strict";
import { Capability } from "../enums";
import { requestSession as requestSessionTimeout } from "../../timeout.js";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SessionRequest
export default class SessionRequest {
constructor (
appId
, opt_capabilities = [
Capability.VIDEO_OUT
, Capability.AUDIO_OUT ]
, opt_timeout = null) {
this.appId = appId;
this.capabilities = opt_capabilities;
this.language = null;
this.requestSessionTimeout = requestSessionTimeout;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
import * as timeouts from "../../timeout.js";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Timeout
export default class Timeout {
constructor () {
Object.assign(this, timeouts);
}
};

View File

@@ -0,0 +1,13 @@
"use strict";
import { VolumeControlType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Volume
export default class Volume {
constructor (
opt_level = null
, opt_muted = null) {
this.level = opt_level;
this.muted = opt_muted;
}
};

View File

@@ -0,0 +1,28 @@
"use strict";
import ApiConfig from "./ApiConfig";
import DialRequest from "./DialRequest";
import Error_ from "./Error";
import Image_ from "./Image";
import Receiver from "./Receiver";
import ReceiverDisplayStatus from "./ReceiverDisplayStatus";
import SenderApplication from "./SenderApplication";
import Session from "./Session";
import SessionRequest from "./SessionRequest";
import Timeout from "./Timeout";
import Volume from "./Volume";
export default {
AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType
};

View File

@@ -0,0 +1,74 @@
"use strict";
export const AutoJoinPolicy = {
TAB_AND_ORIGIN_SCOPED: "tab_and_origin_scoped"
, ORIGIN_SCOPED: "origin_scoped"
, PAGE_SCOPED: "page_scoped"
};
export const Capability = {
VIDEO_OUT: "video_out"
, AUDIO_OUT: "audio_out"
, VIDEO_IN: "video_in"
, AUDIO_IN: "audio_in"
, MULTIZONE_GROUP: "multizone_group"
};
export const DefaultActionPolicy = {
CREATE_SESSION: "create_session"
, CAST_THIS_TAB: "cast_this_tab"
};
export const DialAppState = {
RUNNING: "running"
, STOPPED: "stopped"
, ERROR: "error"
};
export const ErrorCode = {
CANCEL: "cancel"
, TIMEOUT: "timeout"
, API_NOT_INITIALIZED: "api_not_initialized"
, INVALID_PARAMETER: "invalid_parameter"
, EXTENSION_NOT_COMPATIBLE: "extension_not_compatible"
, EXTENSION_MISSING: "extension_missing"
, RECEIVER_UNAVAILABLE: "receiver_unavailable"
, SESSION_ERROR: "session_error"
, CHANNEL_ERROR: "channel_error"
, LOAD_MEDIA_FAILED: "load_media_failed"
};
export const ReceiverAction = {
CAST: "cast"
, STOP: "stop"
};
export const ReceiverAvailability = {
AVAILABLE: "available"
, UNAVAILABLE: "unavailable"
};
export const ReceiverType = {
CAST: "cast"
, DIAL: "dial"
, HANGOUT: "hangout"
, CUSTOM: "custom"
};
export const SenderPlatform = {
CHROME: "chrome"
, IOS: "ios"
, ANDROID: "android"
};
export const SessionStatus = {
CONNECTED: "connected"
, DISCONNECTED: "disconnected"
, STOPPED: "stopped"
};
export const VolumeControlType = {
ATTENUATION: "attenuation"
, FIXED: "fixed"
, MASTER: "master"
};

261
ext/src/shim/cast/index.js Executable file
View File

@@ -0,0 +1,261 @@
"use strict";
import ApiConfig from "./classes/ApiConfig";
import DialRequest from "./classes/DialRequest";
import Error_ from "./classes/Error";
import Image_ from "./classes/Image";
import Receiver from "./classes/Receiver";
import ReceiverDisplayStatus from "./classes/ReceiverDisplayStatus";
import SenderApplication from "./classes/SenderApplication";
import Session from "./classes/Session";
import SessionRequest from "./classes/SessionRequest";
import Timeout from "./classes/Timeout";
import Volume from "./classes/Volume";
import { AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType } from "./enums";
import { requestSession as requestSessionTimeout } from "../timeout";
import state from "../state";
import { onMessage, sendMessage } from "../messageBridge";
const cast = {
// Enums
AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType
// Classes
, ApiConfig
, DialRequest
, Error: Error_
, Image: Image_
, Receiver
, ReceiverDisplayStatus
, SenderApplication
, Session
, SessionRequest
, Timeout
, Volume
, timeout: new Timeout()
, isAvailable: true
, VERSION: [ 1, 2 ]
};
const receiverListeners = new Set();
let sessionSuccessCallback;
let sessionErrorCallback;
cast.addReceiverActionListener = (listener) => {
console.info("Caster (Debug): cast.addReceiverActionListener");
receiverListeners.add(listener);
};
cast.initialize = (
apiConfig
, successCallback
, errorCallback) => {
console.info("Caster (Debug): cast.initialize");
// Already initialized
if (state.apiConfig) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
state.apiConfig = apiConfig;
sendMessage({
subject: "bridge:discover"
});
apiConfig.receiverListener(state.receiverList.length
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE);
successCallback();
};
cast.logMessage = (message) => {
console.log("CAST MSG:", message);
};
cast.precache = (data) => {
console.info("STUB :: cast.precache");
};
cast.removeReceiverActionListener = (listener) => {
receiverListeners.delete(listener);
}
cast.requestSession = (
successCallback
, errorCallback
, opt_sessionRequest = state.apiConfig.sessionRequest) => {
console.info("Caster (Debug): cast.requestSession");
// Called before initialization
if (!state.apiConfig) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
return;
}
// No available receivers
if (!state.receiverList.length) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
sessionSuccessCallback = successCallback;
sessionErrorCallback = errorCallback;
// Open destination chooser
sendMessage({
subject: "main:openPopup"
});
};
cast.requestSessionById = (sessionId) => {
console.info("STUB :: cast.requestSessionById");
};
cast.setCustomReceivers = (receivers, successCallback, errorCallback) => {
console.info("STUB :: cast.setCustomReceivers");
};
cast.setPageContext = (win) => {
console.info("STUB :: cast.setPageContext");
};
cast.setReceiverDisplayStatus = (sessionId) => {
console.info("STUB :: cast.setReceiverDisplayStatus");
};
cast.unescape = (escaped) => unescape(escaped);
onMessage(message => {
switch (message.subject) {
/**
* Cast destination found (serviceUp). Set the API availability
* property and call the page event function (__onGCastApiAvailable).
*/
case "shim:serviceUp":
const receiver = new Receiver(
message.data.id
, message.data.friendlyName);
receiver._address = message.data.address;
receiver._port = message.data.port;
if (state.receiverList.find(r => r.label === receiver.label)) {
break;
}
state.receiverList.push(receiver);
// Notify listeners of new cast destination
state.apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
receiverListeners.forEach(
listener => listener(ReceiverAvailability.AVAILABLE));
break;
/**
* Cast destination lost (serviceDown). Remove from the receiver list
* and update availability state.
*/
case "shim:serviceDown":
state.receiverList = state.receiverList.filter(
receiver => receiver.label !== message.data.id);
if (state.receiverList.length === 0) {
state.apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE);
}
break;
case "shim:selectReceiver":
console.info("Caster (Debug): Selected receiver");
const selectedReceiver = message.data;
const sessionConstructorArgs = [
state.sessionList.length // sessionId
, state.apiConfig.sessionRequest.appId // appId
, selectedReceiver.friendlyName // displayName
, [] // appImages
, selectedReceiver // receiver
, (session) => {
sendMessage({
subject: "popup:close"
});
state.apiConfig.sessionListener(session);
sessionSuccessCallback(session);
}
];
// If existing session active, stop it and start new one
if (state.sessionList.length) {
const lastSession
= state.sessionList[state.sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) {
lastSession.stop(() => {
state.sessionList.push(new Session(
...sessionConstructorArgs));
});
break;
}
}
state.sessionList.push(new Session(...sessionConstructorArgs));
break;
/**
* Popup is ready to receive data to populate the cast destination
* chooser.
*/
case "shim:popupReady":
sendMessage({
subject: "popup:populate"
, data: state.receiverList
});
break;
}
});
// Trigger bridge mDNS discovery
sendMessage({
subject: "main:initialize"
});
export default cast;