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; deviceId: string;
appId: string; appId: string;
sessionId?: string; sessionId?: string;
autoJoinContexts: ContentContext[]; autoJoinContexts: Set<ContentContext>;
} }
/** Creates a cast session object and sets up messaging. */ /** Creates a cast session object and sets up messaging. */
@@ -71,11 +71,11 @@ async function createCastSession(opts: {
bridgePort: await bridge.connect(), bridgePort: await bridge.connect(),
deviceId: opts.deviceId, deviceId: opts.deviceId,
appId: opts.appId, appId: opts.appId,
autoJoinContexts: [] autoJoinContexts: new Set()
}; };
if (opts.instance.contentContext) { if (opts.instance.contentContext) {
session.autoJoinContexts.push(opts.instance.contentContext); session.autoJoinContexts.add(opts.instance.contentContext);
} }
opts.instance.session = session; 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 { export interface CastInstance {
contentPort: AnyPort; contentPort: AnyPort;
contentContext?: ContentContext; contentContext?: ContentContext;
@@ -230,11 +244,60 @@ function destroyCastInstance(instance: CastInstance) {
activeInstances.delete(instance); 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. */ /** Whitelist of safe message types from content. */
const allowedContentMessages: Array<Message["subject"]> = [ const allowedContentMessages: Array<Message["subject"]> = [
"main:initializeCastSdk", "main:initializeCastSdk",
"main:requestSession", "main:requestSession",
"main:requestSessionById", "main:requestSessionById",
"main:leaveSession",
"bridge:sendCastReceiverMessage", "bridge:sendCastReceiverMessage",
"bridge:sendCastSessionMessage" "bridge:sendCastSessionMessage"
]; ];
@@ -298,7 +361,7 @@ const castManager = new (class {
if (instance.contentContext?.tabId) { if (instance.contentContext?.tabId) {
updateActionState( updateActionState(
ActionState.Default, ActionState.Default,
instance.contentContext?.tabId instance.contentContext.tabId
); );
} }
} }
@@ -523,7 +586,7 @@ async function handleContentMessage(instance: CastInstance, message: Message) {
} }
switch (message.subject) { switch (message.subject) {
case "main:initializeCastSdk": case "main:initializeCastSdk": {
instance.apiConfig = message.data.apiConfig; instance.apiConfig = message.data.apiConfig;
instance.contentPort.postMessage({ instance.contentPort.postMessage({
subject: "cast:receiverAvailabilityUpdated", subject: "cast:receiverAvailabilityUpdated",
@@ -540,43 +603,11 @@ async function handleContentMessage(instance: CastInstance, message: Message) {
} }
// Check existing sessions for a valid auto join target // Check existing sessions for a valid auto join target
sessionLoop: for (const [, session] of activeSessions) { const target = findAutoJoinTarget(instance);
if ( if (target) joinSession(instance, target.session);
!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;
}
}
}
break; break;
}
// User has triggered receiver selection via the cast API // User has triggered receiver selection via the cast API
case "main:requestSession": { 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 requesting by ID, add to the list of auto join contexts
if (instance.contentContext) { if (instance.contentContext) {
session.autoJoinContexts.push(instance.contentContext); session.autoJoinContexts.add(instance.contentContext);
} }
} }
break; 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> Map<string, SendMessageCallback>
>(); >();
export const SessionLeaveSuccessCallback = new WeakMap<
Session,
Optional<() => void>
>();
/** Creates a Session object and initializes private data. */ /** Creates a Session object and initializes private data. */
export function createSession( export function createSession(
sessionArgs: ConstructorParameters<typeof Session> sessionArgs: ConstructorParameters<typeof Session>
@@ -139,6 +144,13 @@ export default class Session {
return sendMessageCallback; return sendMessageCallback;
} }
get #leaveSuccessCallback() {
return SessionLeaveSuccessCallback.get(this);
}
set #leaveSuccessCallback(successCallback: Optional<() => void>) {
SessionLeaveSuccessCallback.set(this, successCallback);
}
media: Media[] = []; media: Media[] = [];
namespaces: Array<{ name: string }> = []; namespaces: Array<{ name: string }> = [];
senderApps: SenderApplication[] = []; senderApps: SenderApplication[] = [];
@@ -257,10 +269,21 @@ export default class Session {
} }
leave( leave(
_successCallback?: () => void, successCallback?: () => void,
_errorCallback?: (err: CastError) => 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( loadMedia(

View File

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

View File

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