Implement chrome.cast.Session#leave

This commit is contained in:
hensm
2022-09-11 13:42:04 +01:00
parent 0505b0d0b5
commit 16d11651da
4 changed files with 162 additions and 46 deletions

View File

@@ -48,7 +48,7 @@ interface CastSession {
deviceId: string;
appId: string;
sessionId?: string;
autoJoinContexts: ContentContext[];
autoJoinContexts: Set<ContentContext>;
}
/** Creates a cast session object and sets up messaging. */
@@ -71,11 +71,11 @@ async function createCastSession(opts: {
bridgePort: await bridge.connect(),
deviceId: opts.deviceId,
appId: opts.appId,
autoJoinContexts: []
autoJoinContexts: new Set()
};
if (opts.instance.contentContext) {
session.autoJoinContexts.push(opts.instance.contentContext);
session.autoJoinContexts.add(opts.instance.contentContext);
}
opts.instance.session = session;
@@ -150,6 +150,20 @@ function joinSession(instance: CastInstance, session: CastSession) {
}
}
function leaveSession(instance: CastInstance) {
if (!instance.session?.sessionId) return;
instance.contentPort.postMessage({
subject: "cast:sessionDisconnected",
data: { sessionId: instance.session.sessionId }
});
delete instance.session;
if (instance.contentContext?.tabId) {
updateActionState(ActionState.Default, instance.contentContext.tabId);
}
}
export interface CastInstance {
contentPort: AnyPort;
contentContext?: ContentContext;
@@ -230,11 +244,60 @@ function destroyCastInstance(instance: CastInstance) {
activeInstances.delete(instance);
}
/**
* Check instance's auto join policy against a content context to
* determine if it's a valid auto join target.
*/
function isValidAutoJoinContext(
instance: CastInstance,
context: ContentContext
) {
if (!instance.apiConfig?.autoJoinPolicy) return;
const { autoJoinPolicy } = instance.apiConfig;
if (
autoJoinPolicy === AutoJoinPolicy.ORIGIN_SCOPED ||
autoJoinPolicy === AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
) {
if (context.origin !== instance.contentContext?.origin) return false;
if (
autoJoinPolicy === AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED &&
!isSameContext(context, instance.contentContext)
)
return false;
return true;
}
return false;
}
interface AutoJoinTarget {
session: CastSession;
autoJoinContext: ContentContext;
}
function findAutoJoinTarget(instance: CastInstance) {
for (const [, session] of activeSessions) {
if (
!session.sessionId ||
session.appId !== instance.apiConfig?.sessionRequest.appId
)
continue;
for (const context of session.autoJoinContexts) {
if (isValidAutoJoinContext(instance, context)) {
return { session, autoJoinContext: context } as AutoJoinTarget;
}
}
}
}
/** Whitelist of safe message types from content. */
const allowedContentMessages: Array<Message["subject"]> = [
"main:initializeCastSdk",
"main:requestSession",
"main:requestSessionById",
"main:leaveSession",
"bridge:sendCastReceiverMessage",
"bridge:sendCastSessionMessage"
];
@@ -298,7 +361,7 @@ const castManager = new (class {
if (instance.contentContext?.tabId) {
updateActionState(
ActionState.Default,
instance.contentContext?.tabId
instance.contentContext.tabId
);
}
}
@@ -523,7 +586,7 @@ async function handleContentMessage(instance: CastInstance, message: Message) {
}
switch (message.subject) {
case "main:initializeCastSdk":
case "main:initializeCastSdk": {
instance.apiConfig = message.data.apiConfig;
instance.contentPort.postMessage({
subject: "cast:receiverAvailabilityUpdated",
@@ -540,43 +603,11 @@ async function handleContentMessage(instance: CastInstance, message: Message) {
}
// Check existing sessions for a valid auto join target
sessionLoop: for (const [, session] of activeSessions) {
if (
!session.sessionId ||
session.appId !== instance.apiConfig.sessionRequest.appId
) {
continue;
}
// Check for valid auto join sessions
const { autoJoinPolicy } = instance.apiConfig;
if (
autoJoinPolicy === AutoJoinPolicy.ORIGIN_SCOPED ||
autoJoinPolicy === AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
) {
for (const context of session.autoJoinContexts) {
// Check same origin
if (
context.origin !== instance.contentContext?.origin
) {
continue;
}
// Check same context for tab scoped
if (
autoJoinPolicy ===
AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED &&
!isSameContext(context, instance.contentContext)
) {
continue;
}
joinSession(instance, session);
break sessionLoop;
}
}
}
const target = findAutoJoinTarget(instance);
if (target) joinSession(instance, target.session);
break;
}
// User has triggered receiver selection via the cast API
case "main:requestSession": {
@@ -690,12 +721,54 @@ async function handleContentMessage(instance: CastInstance, message: Message) {
// If requesting by ID, add to the list of auto join contexts
if (instance.contentContext) {
session.autoJoinContexts.push(instance.contentContext);
session.autoJoinContexts.add(instance.contentContext);
}
}
break;
}
case "main:leaveSession": {
if (!instance.contentContext || !instance.session?.sessionId) {
logger.error("Cannot leave session, instance invalid!");
break;
}
// Find auto join target for this instance
const target = findAutoJoinTarget(instance);
if (target) {
// Remove auto join context for future instances
instance.session.autoJoinContexts.delete(
target.autoJoinContext
);
const sessionAppId = instance.session.appId;
leaveSession(instance);
/**
* Disconnect other instances within the scope of this
* instances's auto join policy.
*/
for (const activeInstance of activeInstances) {
if (
(activeInstance === instance ||
activeInstance.session?.appId) !== sessionAppId
)
continue;
if (
isValidAutoJoinContext(
activeInstance,
target.autoJoinContext
)
) {
leaveSession(activeInstance);
}
}
} else {
leaveSession(instance);
}
}
}
}

View File

@@ -100,6 +100,11 @@ export const SessionSendMessageCallbacks = new WeakMap<
Map<string, SendMessageCallback>
>();
export const SessionLeaveSuccessCallback = new WeakMap<
Session,
Optional<() => void>
>();
/** Creates a Session object and initializes private data. */
export function createSession(
sessionArgs: ConstructorParameters<typeof Session>
@@ -139,6 +144,13 @@ export default class Session {
return sendMessageCallback;
}
get #leaveSuccessCallback() {
return SessionLeaveSuccessCallback.get(this);
}
set #leaveSuccessCallback(successCallback: Optional<() => void>) {
SessionLeaveSuccessCallback.set(this, successCallback);
}
media: Media[] = [];
namespaces: Array<{ name: string }> = [];
senderApps: SenderApplication[] = [];
@@ -257,10 +269,21 @@ export default class Session {
}
leave(
_successCallback?: () => void,
_errorCallback?: (err: CastError) => void
successCallback?: () => void,
errorCallback?: (err: CastError) => void
) {
logger.info("STUB :: Session#leave");
if (!this.sessionId) {
errorCallback?.(
new CastError(ErrorCode.INVALID_PARAMETER, "Session not active")
);
return;
}
this.#leaveSuccessCallback = successCallback;
pageMessaging.page.sendMessage({
subject: "main:leaveSession"
});
}
loadMedia(

View File

@@ -33,6 +33,7 @@ import {
import Session, {
createSession,
SessionLeaveSuccessCallback,
SessionMessageListeners,
SessionSendMessageCallbacks,
SessionUpdateListeners
@@ -220,8 +221,7 @@ export default class {
}
case "cast:sessionStopped": {
const { sessionId } = message.data;
const session = this.#sessions.get(sessionId);
const session = this.#sessions.get(message.data.sessionId);
if (session?.status === SessionStatus.CONNECTED) {
session.status = SessionStatus.STOPPED;
@@ -236,6 +236,24 @@ export default class {
break;
}
case "cast:sessionDisconnected": {
const session = this.#sessions.get(message.data.sessionId);
if (session?.status === SessionStatus.CONNECTED) {
session.status = SessionStatus.DISCONNECTED;
SessionLeaveSuccessCallback.get(session)?.();
const updateListeners = SessionUpdateListeners.get(session);
if (updateListeners) {
for (const listener of updateListeners) {
listener(true);
}
}
}
break;
}
case "cast:sessionMessageReceived": {
const { sessionId, namespace, messageData } = message.data;
const session = this.#sessions.get(sessionId);

View File

@@ -93,12 +93,14 @@ type ExtMessageDefinitions = {
"cast:sessionRequestCancelled": undefined;
"main:requestSessionById": { sessionId: string };
"main:leaveSession": void;
"cast:instanceCreated": { isAvailable: boolean };
"cast:receiverAvailabilityUpdated": { isAvailable: boolean };
"cast:sessionCreated": CastSessionCreatedDetails & { receiver: Receiver };
"cast:sessionUpdated": CastSessionUpdatedDetails;
"cast:sessionDisconnected": { sessionId: string };
/** Allows the selector popup to send cast NS_RECEIVER messages. */
"main:sendReceiverMessage": ReceiverSelectorReceiverMessage;