Use receiver selector manager

This commit is contained in:
hensm
2019-04-30 17:43:58 +01:00
committed by Matt Hensman
parent b1cde1b3b5
commit cd7248eefd
7 changed files with 193 additions and 222 deletions

View File

@@ -7,6 +7,10 @@ import messageRouter from "./lib/messageRouter";
import { getChromeUserAgent } from "./lib/userAgents"; import { getChromeUserAgent } from "./lib/userAgents";
import { getWindowCenteredProps } from "./lib/utils"; import { getWindowCenteredProps } from "./lib/utils";
import { ReceiverSelectorMediaType
, ReceiverSelectorSelectedEvent
, PopupReceiverSelectorManager } from "./receiverSelectorManager";
import { Message, Receiver } from "./types"; import { Message, Receiver } from "./types";
import { ReceiverStatusMessage import { ReceiverStatusMessage
@@ -354,60 +358,83 @@ browser.menus.onClicked.addListener(async (info, tab) => {
}); });
let popupWinId: number;
let popupShimId: string;
let popupPort: browser.runtime.Port;
/** interface Shim {
* Creates popup window for cast destination selection. port: browser.runtime.Port;
* Refocusing other browser windows causes the popup window bridgePort: browser.runtime.Port;
* to close and returns an API error. tabId: number;
*/ frameId: number;
async function openPopup (shimId: string) {
// Current window to base centered position on
const win = await browser.windows.getCurrent();
const centeredProps = getWindowCenteredProps(win, 350, 200);
const popup = await browser.windows.create({
url: "ui/popup/index.html"
, type: "popup"
, ...centeredProps
});
// Store popup details for message forwarding
popupWinId = popup.id;
popupShimId = shimId;
// Size/position not set correctly on creation (bug?)
await browser.windows.update(popup.id, {
...centeredProps
});
// Close popup on other browser window focus
browser.windows.onFocusChanged.addListener(function listener (id) {
if (id !== browser.windows.WINDOW_ID_NONE
&& id === win.id) {
browser.windows.onFocusChanged.removeListener(listener);
browser.windows.remove(popup.id);
}
});
} }
// Track popup close const shimMap = new Map<string, Shim>();
browser.windows.onRemoved.addListener(id => {
if (id === popupWinId) {
shimMap.get(popupShimId).port.postMessage({
subject: "shim:/popupClosed"
});
popupWinId = null; const statusBridge = browser.runtime.connectNative(APPLICATION_NAME);
popupShimId = null; const statusBridgeReceivers = new Map<string, Receiver>();
popupPort = null;
statusBridge.onMessage.addListener(async (message: Message) => {
console.log(message);
switch (message.subject) {
case "shim:/serviceUp": {
const receiver = (message as ServiceUpMessage).data;
statusBridgeReceivers.set(receiver.id, receiver);
// Forward update to shims
for (const shim of shimMap.values()) {
shim.port.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "shim:/serviceDown": {
const { id } = (message as ServiceDownMessage).data;
if (statusBridgeReceivers.has(id)) {
statusBridgeReceivers.delete(id);
}
// Forward update to shims
for (const shim of shimMap.values()) {
shim.port.postMessage({
subject: "shim:/serviceDown"
, data: { id }
});
}
break;
}
case "receiverStatus": {
const { id, status } = (message as ReceiverStatusMessage).data;
const receiver = statusBridgeReceivers.get(id);
// Merge new status with old status
statusBridgeReceivers.set(id, {
...receiver
, status: {
...receiver.status
, ...status
}
});
break;
}
}
});
statusBridge.postMessage({
subject: "bridge:/initialize"
, data: {
shouldWatchStatus: true
} }
}); });
const shimMap = new Map();
async function onConnectShim (port: browser.runtime.Port) { async function onConnectShim (port: browser.runtime.Port) {
const bridgeInfo = await getBridgeInfo(); const bridgeInfo = await getBridgeInfo();
@@ -471,31 +498,52 @@ async function onConnectShim (port: browser.runtime.Port) {
} }
switch (message.subject) { switch (message.subject) {
case "main:/openPopup": { case "main:/shimInitialized": {
/**
* If popup already open, reassign to new shim,
* otherwise create a new popup.
*/
if (popupWinId) {
// Reassign popup to new shim // Send existing receivers as serviceUp messages
popupPort.postMessage({ for (const receiver of statusBridgeReceivers.values()) {
subject: "popup:/assignShim" port.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "main:/sessionCreated": {
PopupReceiverSelectorManager.close();
break;
}
case "main:/selectReceiverBegin": {
PopupReceiverSelectorManager.open(
Array.from(statusBridgeReceivers.values())
, message.data.defaultMediaType);
PopupReceiverSelectorManager.addEventListener("selected"
, (ev: ReceiverSelectorSelectedEvent) => {
port.postMessage({
subject: "shim:/selectReceiverEnd"
, data: { , data: {
tabId receiver: ev.detail.receiver
, frameId
} }
}); });
});
/** PopupReceiverSelectorManager.addEventListener("cancelled", () => {
* Notify shim that existing popup has closed and port.postMessage({
* to re-populate receiver list for new popup. subject: "shim:/selectReceiverCancelled"
*/ });
port.postMessage({ subject: "shim:/popupClosed" }); });
port.postMessage({ subject: "shim:/popupReady" });
} else { PopupReceiverSelectorManager.addEventListener("error", () => {
await openPopup(shimId); // TODO: Report errors properly
} port.postMessage({
subject: "shim:/selectReceiverCancelled"
});
});
break; break;
} }
@@ -514,85 +562,11 @@ async function onConnectShim (port: browser.runtime.Port) {
}); });
} }
function onConnectPopup (port: browser.runtime.Port) {
if (popupPort) {
popupPort.disconnect();
}
popupPort = port;
const { tabId, frameId } = shimMap.get(popupShimId);
port.postMessage({
subject: "popup:/assignShim"
, data: {
tabId
, frameId
}
});
}
browser.runtime.onConnect.addListener(port => { browser.runtime.onConnect.addListener(port => {
switch (port.name) { switch (port.name) {
case "shim": case "shim":
onConnectShim(port); onConnectShim(port);
break; break;
case "popup":
onConnectPopup(port);
break;
}
});
const statusBridge = browser.runtime.connectNative(APPLICATION_NAME);
const statusBridgeReceivers = new Map<string, Receiver>();
statusBridge.onMessage.addListener((message: Message) => {
switch (message.subject) {
case "shim:/serviceUp": {
const serviceUpMessage = message as ServiceUpMessage;
const receiver = serviceUpMessage.data;
statusBridgeReceivers.set(receiver.id, receiver);
break;
}
case "shim:/serviceDown": {
const serviceDownMessage = (message as ServiceDownMessage);
const { id } = serviceDownMessage.data;
if (statusBridgeReceivers.has(id)) {
statusBridgeReceivers.delete(id);
}
break;
}
case "receiverStatus": {
const receiverStatusMessage = message as ReceiverStatusMessage;
const { id, status } = receiverStatusMessage.data;
const receiver = statusBridgeReceivers.get(id);
// Merge new status with old status
statusBridgeReceivers.set(id, {
...receiver
, status: {
...receiver.status
, ...status
}
});
break;
}
}
});
statusBridge.postMessage({
subject: "bridge:/initialize"
, data: {
shouldWatchStatus: true
} }
}); });

View File

@@ -11,7 +11,7 @@ export enum ReceiverSelectorMediaType {
export interface ReceiverSelection { export interface ReceiverSelection {
receiver: Receiver; receiver: Receiver;
castMethod: ReceiverSelectorMediaType; mediaType: ReceiverSelectorMediaType;
} }
export type ReceiverSelectorSelectedEvent = CustomEvent<ReceiverSelection>; export type ReceiverSelectorSelectedEvent = CustomEvent<ReceiverSelection>;

View File

@@ -1,5 +1,11 @@
"use strict"; "use strict";
export { ReceiverSelection
, ReceiverSelectorCancelledEvent
, ReceiverSelectorErrorEvent
, ReceiverSelectorMediaType
, ReceiverSelectorSelectedEvent } from "./ReceiverSelectorManager";
export { default as NativeMacReceiverSelectorManager } export { default as NativeMacReceiverSelectorManager }
from "./selectorManagers/NativeMacReceiverSelectorManager"; from "./selectorManagers/NativeMacReceiverSelectorManager";

View File

@@ -48,7 +48,13 @@ class PopupReceiverSelectorManager
this.messagePort = port; this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage); this.messagePort.onMessage.addListener(this.onPopupMessage);
// TODO: Send initial data this.messagePort.postMessage({
subject: "popup:/populateReceiverList"
, data: {
receivers: this.receivers
, defaultMediaType: this.defaultMediaType
}
})
}); });
} }
@@ -97,8 +103,15 @@ class PopupReceiverSelectorManager
* Handles popup messages. * Handles popup messages.
*/ */
private onPopupMessage (message: Message) { private onPopupMessage (message: Message) {
console.log("popupmsg", message);
switch (message.subject) { switch (message.subject) {
case "selected": { case "receiverSelectorManager:/selected": {
this.wasReceiverSelected = true;
this.dispatchEvent(new CustomEvent("selected", {
detail: message.data
}));
break; break;
} }
} }

View File

@@ -6,6 +6,9 @@ import SessionRequest from "./SessionRequest";
import { AutoJoinPolicy import { AutoJoinPolicy
, DefaultActionPolicy } from "../enums"; , DefaultActionPolicy } from "../enums";
import { ReceiverSelectorMediaType }
from "../../../receiverSelectorManager/ReceiverSelectorManager";
export default class ApiConfig { export default class ApiConfig {
public additionalSessionRequests: any[] = []; public additionalSessionRequests: any[] = [];
@@ -23,6 +26,7 @@ export default class ApiConfig {
= DefaultActionPolicy.CREATE_SESSION = DefaultActionPolicy.CREATE_SESSION
// TODO: Remove awful hack for mirror casting // TODO: Remove awful hack for mirror casting
, public _selectedMedia: string = "app") { , public _selectedMedia: ReceiverSelectorMediaType
= ReceiverSelectorMediaType.App) {
} }
} }

View File

@@ -98,7 +98,7 @@ export function initialize (
apiConfig = newApiConfig; apiConfig = newApiConfig;
sendMessageResponse({ sendMessageResponse({
subject: "bridge:/startDiscovery" subject: "main:/shimInitialized"
}); });
apiConfig.receiverListener(receiverList.length apiConfig.receiverListener(receiverList.length
@@ -157,7 +157,10 @@ export function requestSession (
// Open destination chooser // Open destination chooser
sendMessageResponse({ sendMessageResponse({
subject: "main:/openPopup" subject: "main:/selectReceiverBegin"
, data: {
defaultMediaType: apiConfig._selectedMedia
}
}); });
} }
@@ -186,10 +189,12 @@ export function unescape (escaped: string): string {
} }
onMessage(message => { onMessage(async message => {
console.log(message)
switch (message.subject) { switch (message.subject) {
case "shim:/initialized": { case "shim:/initialized": {
isAvailable = true; isAvailable = true;
break; break;
} }
@@ -228,7 +233,7 @@ onMessage(message => {
break; break;
} }
case "shim:/selectReceiver": { case "shim:/selectReceiverEnd": {
console.info("fx_cast (Debug): Selected receiver"); console.info("fx_cast (Debug): Selected receiver");
const selectedReceiver = new Receiver( const selectedReceiver = new Receiver(
@@ -247,7 +252,7 @@ onMessage(message => {
, selectedReceiver // receiver , selectedReceiver // receiver
, (session: Session) => { , (session: Session) => {
sendMessageResponse({ sendMessageResponse({
subject: "popup:/close" subject: "main:/sessionCreated"
}); });
sessionRequestInProgress = false; sessionRequestInProgress = false;
@@ -272,26 +277,10 @@ onMessage(message => {
break; break;
} }
/**
* Popup is ready to receive data to populate the cast destination
* chooser.
*/
case "shim:/popupReady": {
sendMessageResponse({
subject: "popup:/populateReceiverList"
, data: {
receivers: receiverList
, selectedMedia: apiConfig._selectedMedia
}
});
break;
}
/** /**
* Popup closed before session established. * Popup closed before session established.
*/ */
case "shim:/popupClosed": { case "shim:/selectReceiverCancelled": {
if (sessionRequestInProgress) { if (sessionRequestInProgress) {
sessionRequestInProgress = false; sessionRequestInProgress = false;
sessionErrorCallback(new Error_(ErrorCode.CANCEL)); sessionErrorCallback(new Error_(ErrorCode.CANCEL));

View File

@@ -7,6 +7,10 @@ import ReactDOM from "react-dom";
import { getNextEllipsis } from "../../lib/utils"; import { getNextEllipsis } from "../../lib/utils";
import { Message, Receiver } from "../../types"; import { Message, Receiver } from "../../types";
import { ReceiverSelectorMediaType }
from "../../receiverSelectorManager/ReceiverSelectorManager";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
// macOS styles // macOS styles
@@ -30,7 +34,7 @@ let frameWidth: number;
interface PopupAppState { interface PopupAppState {
receivers: Receiver[]; receivers: Receiver[];
selectedMedia: string; defaultMediaType: ReceiverSelectorMediaType;
isLoading: boolean; isLoading: boolean;
} }
@@ -43,7 +47,7 @@ class PopupApp extends Component<{}, PopupAppState> {
this.state = { this.state = {
receivers: [] receivers: []
, selectedMedia: "app" , defaultMediaType: ReceiverSelectorMediaType.App
, isLoading: false , isLoading: false
}; };
@@ -59,28 +63,46 @@ class PopupApp extends Component<{}, PopupAppState> {
} }
public componentDidMount () { public componentDidMount () {
const backgroundPort = browser.runtime.connect({ this.port = browser.runtime.connect({
name: "popup" name: "popup"
}); });
backgroundPort.onMessage.addListener((message: Message) => { this.port.onMessage.addListener((message: Message) => {
if (message.subject === "popup:/assignShim") { switch (message.subject) {
this.setPort(message.data.tabId case "popup:/populateReceiverList": {
, message.data.frameId); this.setState({
receivers: message.data.receivers
, defaultMediaType: message.data.defaultMediaType
}, () => {
// Get height of content without window decoration
winHeight = document.body.clientHeight + frameHeight;
browser.windows.update(this.win.id, {
height: winHeight
});
});
break;
}
case "popup:/close": {
window.close();
break;
}
} }
}); });
} }
public render () { public render () {
const shareMedia = const shareMedia =
this.state.selectedMedia === "tab" this.state.defaultMediaType === ReceiverSelectorMediaType.Tab
|| this.state.selectedMedia === "screen"; || this.state.defaultMediaType === ReceiverSelectorMediaType.Screen;
return ( return (
<div> <div>
<div className="media-select"> <div className="media-select">
Cast Cast
<select value={ this.state.selectedMedia } <select value={ this.state.defaultMediaType }
onChange={ this.onSelectChange } onChange={ this.onSelectChange }
className="media-select-dropdown"> className="media-select-dropdown">
<option value="app" disabled={ shareMedia }>this site's app</option> <option value="app" disabled={ shareMedia }>this site's app</option>
@@ -103,66 +125,29 @@ class PopupApp extends Component<{}, PopupAppState> {
); );
} }
private async setPort (shimTabId: number, shimFrameId: number) {
if (this.port) {
this.port.disconnect();
}
this.port = browser.tabs.connect(shimTabId, {
name: "popup"
, frameId: shimFrameId
});
this.port.postMessage({
subject: "shim:/popupReady"
});
this.port.onMessage.addListener((message: Message) => {
switch (message.subject) {
case "popup:/populateReceiverList": {
this.setState({
receivers: message.data.receivers
, selectedMedia: message.data.selectedMedia
}, () => {
// Get height of content without window decoration
winHeight = document.body.clientHeight + frameHeight;
// Adjust height to fit content
browser.windows.update(this.win.id, {
height: winHeight
});
});
break;
}
case "popup:/close": {
window.close();
break;
}
}
});
}
private onCast (receiver: Receiver) { private onCast (receiver: Receiver) {
this.setState({ this.setState({
isLoading: true isLoading: true
}); });
this.port.postMessage({ this.port.postMessage({
subject: "shim:/selectReceiver" subject: "receiverSelectorManager:/selected"
, data: { , data: {
receiver receiver
, selectedMedia: this.state.selectedMedia , defaultMediaType: this.state.defaultMediaType
, a: 5
} }
}); });
} }
private onSelectChange (ev: React.ChangeEvent<HTMLSelectElement>) { private onSelectChange (ev: React.ChangeEvent<HTMLSelectElement>) {
const mediaTypeMap: { [key: string]: ReceiverSelectorMediaType } = {
"app": ReceiverSelectorMediaType.App
, "tab": ReceiverSelectorMediaType.Tab
, "screen": ReceiverSelectorMediaType.Screen
};
this.setState({ this.setState({
selectedMedia: ev.target.value defaultMediaType: mediaTypeMap[ev.target.value]
}); });
} }
} }