From 9295d8ee838b58c0a349498f0b7e4cce1003682f Mon Sep 17 00:00:00 2001 From: hensm Date: Thu, 16 Jan 2020 00:47:38 +0000 Subject: [PATCH] Add stop action to receiver selectors --- .../mac/fx_cast_selector/ReceiverView.swift | 23 ++++++++ .../mac/fx_cast_selector/ViewController.swift | 16 +++++ .../fx_cast_selector/models/InitData.swift | 1 + .../models/ReceiverSelection.swift | 2 +- app/src/bridge/Session.ts | 16 ++++- app/src/bridge/StatusListener.ts | 6 +- app/src/bridge/index.ts | 37 ++++++++++-- app/src/bridge/types.ts | 10 ++++ ext/src/_locales/en/messages.json | 4 ++ ext/src/background/StatusManager.ts | 11 ++++ .../NativeReceiverSelector.ts | 7 +++ .../receiverSelector/PopupReceiverSelector.ts | 8 +++ .../receiverSelector/ReceiverSelector.ts | 1 + .../ReceiverSelectorManager.ts | 48 ++++++++++++--- ext/src/ui/popup/index.tsx | 59 +++++++++++++++---- 15 files changed, 219 insertions(+), 30 deletions(-) diff --git a/app/selector/mac/fx_cast_selector/ReceiverView.swift b/app/selector/mac/fx_cast_selector/ReceiverView.swift index 3f94e0f..d187aac 100644 --- a/app/selector/mac/fx_cast_selector/ReceiverView.swift +++ b/app/selector/mac/fx_cast_selector/ReceiverView.swift @@ -2,6 +2,7 @@ import Cocoa protocol ReceiverViewDelegate : AnyObject { func didCast (_ receiver: Receiver) + func didStop (_ receiver: Receiver) } class ReceiverView : NSStackView { @@ -74,6 +75,23 @@ class ReceiverView : NSStackView { self.addArrangedSubview(self.castButton) self.distribution = .fill + + NSEvent.addLocalMonitorForEvents( + matching: .flagsChanged) { event in + + if !self.receiver.status.application.isIdleScreen && + event.modifierFlags.contains(.option) { + self.castButton.title = + InitDataProvider.shared.data.i18n_stopButtonTitle + self.castButton.action = #selector(ReceiverView.onStop) + } else { + self.castButton.title = + InitDataProvider.shared.data.i18n_castButtonTitle + self.castButton.action = #selector(ReceiverView.onCast) + } + + return event + } } override func updateConstraints () { @@ -103,4 +121,9 @@ class ReceiverView : NSStackView { self.castingSpinner.isHidden = false self.castingSpinner.startAnimation(nil) } + + @objc + func onStop () { + self.receiverViewDelegate?.didStop(self.receiver); + } } diff --git a/app/selector/mac/fx_cast_selector/ViewController.swift b/app/selector/mac/fx_cast_selector/ViewController.swift index 3b58e6f..8a36c56 100644 --- a/app/selector/mac/fx_cast_selector/ViewController.swift +++ b/app/selector/mac/fx_cast_selector/ViewController.swift @@ -197,4 +197,20 @@ extension ViewController : ReceiverViewDelegate { fatalError("Error: Failed to encode output data") } } + + func didStop (_ receiver: Receiver) { + // TODO: Use separate type and do proper JSON encoding + let selection = ReceiverSelection( + receiver: receiver + , mediaType: nil + , filePath: nil) + + if let jsonData = try? JSONEncoder().encode(selection) + , let jsonString = String(data: jsonData, encoding: .utf8) { + print(jsonString) + fflush(stdout) + } else { + fatalError("Error: Failed to encode output data") + } + } } diff --git a/app/selector/mac/fx_cast_selector/models/InitData.swift b/app/selector/mac/fx_cast_selector/models/InitData.swift index 8321cc7..c643918 100644 --- a/app/selector/mac/fx_cast_selector/models/InitData.swift +++ b/app/selector/mac/fx_cast_selector/models/InitData.swift @@ -10,6 +10,7 @@ struct InitData : Decodable { let i18n_extensionName: String let i18n_castButtonTitle: String + let i18n_stopButtonTitle: String let i18n_mediaTypeApp: String let i18n_mediaTypeTab: String let i18n_mediaTypeScreen: String diff --git a/app/selector/mac/fx_cast_selector/models/ReceiverSelection.swift b/app/selector/mac/fx_cast_selector/models/ReceiverSelection.swift index 79341bf..e672f02 100644 --- a/app/selector/mac/fx_cast_selector/models/ReceiverSelection.swift +++ b/app/selector/mac/fx_cast_selector/models/ReceiverSelection.swift @@ -1,5 +1,5 @@ struct ReceiverSelection : Codable { let receiver: Receiver - let mediaType: MediaType + let mediaType: MediaType? let filePath: String? } diff --git a/app/src/bridge/Session.ts b/app/src/bridge/Session.ts index 6dc7703..5683722 100644 --- a/app/src/bridge/Session.ts +++ b/app/src/bridge/Session.ts @@ -6,13 +6,16 @@ import { Message , SendMessageCallback } from "./types"; -const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection"; -const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"; -const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver"; +export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection"; +export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"; +export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver"; export default class Session { public channelMap = new Map(); + public host: string; + public port: number; + private sendMessageCallback: SendMessageCallback; private sessionId: number; private referenceId: string; @@ -38,6 +41,9 @@ export default class Session { , referenceId: string , sendMessageCallback: SendMessageCallback) { + this.host = host; + this.port = port; + this.sessionId = sessionId; this.referenceId = referenceId; this.sendMessageCallback = sendMessageCallback; @@ -176,6 +182,10 @@ export default class Session { } } + public stop () { + this.clientConnection!.send({ type: "STOP" }); + } + private sendMessage (subject: string, data: any = {}) { this.sendMessageCallback({ subject diff --git a/app/src/bridge/StatusListener.ts b/app/src/bridge/StatusListener.ts index b924ea5..b2d97cf 100644 --- a/app/src/bridge/StatusListener.ts +++ b/app/src/bridge/StatusListener.ts @@ -3,9 +3,9 @@ import { Channel, Client } from "castv2"; import { EventEmitter } from "events"; -const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection"; -const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"; -const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver"; +import { NS_CONNECTION + , NS_HEARTBEAT + , NS_RECEIVER } from "./Session"; /** diff --git a/app/src/bridge/index.ts b/app/src/bridge/index.ts index 5ff3915..01bc4e1 100755 --- a/app/src/bridge/index.ts +++ b/app/src/bridge/index.ts @@ -16,11 +16,16 @@ import { DecodeTransform import { ReceiverStatus } from "./castTypes"; -import { Message } from "./types"; +import { Message, Receiver } from "./types"; import { __applicationName , __applicationVersion } from "../../package.json"; +import { Channel, Client } from "castv2"; +import { NS_CONNECTION + , NS_HEARTBEAT + , NS_RECEIVER } from "./Session"; + // Increase listener limit events.EventEmitter.defaultMaxListeners = 50; @@ -86,7 +91,9 @@ process.on("SIGTERM", () => { receiverSelectorApp.kill(); } - browser.stop(); + if (browser) { + browser.stop(); + } }); @@ -175,6 +182,24 @@ async function handleMessage (message: Message) { break; } + + case "bridge:/stopReceiverApp": { + const receiver: Receiver = message.data.receiver; + const client = new Client(); + + client.connect({ host: receiver.host, port: receiver.port }, () => { + const sourceId = "sender-0"; + const destinationId = "receiver-0"; + + const clientConnection = client.createChannel( + sourceId, destinationId, NS_CONNECTION, "JSON"); + const clientReceiver = client.createChannel( + sourceId, destinationId, NS_RECEIVER, "JSON"); + + clientConnection.send({ type: "CONNECT" }); + clientReceiver.send({ type: "STOP", requestId: 1 }); + }); + } } } @@ -215,9 +240,13 @@ function handleReceiverSelectorMessage (message: Message) { receiverSelectorApp.stdout!.setEncoding("utf8"); receiverSelectorApp.stdout!.on("data", data => { + const parsedData = JSON.parse(data); + sendMessage({ - subject: "main:/receiverSelector/selected" - , data: JSON.parse(data) + subject: !parsedData.mediaType + ? "main:/receiverSelector/stop" + : "main:/receiverSelector/selected" + , data: parsedData }); }); diff --git a/app/src/bridge/types.ts b/app/src/bridge/types.ts index 2513d99..a6ce696 100644 --- a/app/src/bridge/types.ts +++ b/app/src/bridge/types.ts @@ -1,9 +1,19 @@ "use strict"; +import { ReceiverStatus } from "./castTypes"; + export interface Message { subject: string; data?: any; _id?: string; } +export interface Receiver { + host: string; + friendlyName: string; + id: string; + port: number; + status?: ReceiverStatus; +} + export type SendMessageCallback = (message: Message) => void; diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index 842404e..85c24b7 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -56,6 +56,10 @@ } } } + , "popupStopButtonTitle": { + "message": "Stop" + , "description": "Alternate action button text displayed instead of popupCastButtonTitle." + } , "contextCast": { diff --git a/ext/src/background/StatusManager.ts b/ext/src/background/StatusManager.ts index 32a75be..87d95f1 100644 --- a/ext/src/background/StatusManager.ts +++ b/ext/src/background/StatusManager.ts @@ -64,6 +64,17 @@ export default new class StatusManager } } + public async stopReceiverApp (receiver: Receiver) { + if (!this.bridgePort) { + return; + } + + this.bridgePort.postMessage({ + subject: "bridge:/stopReceiverApp" + , data: { receiver } + }); + } + private async createBridgePort () { const bridgePort = await bridge.connect(); bridgePort.onMessage.addListener(this.onBridgePortMessage); diff --git a/ext/src/background/receiverSelector/NativeReceiverSelector.ts b/ext/src/background/receiverSelector/NativeReceiverSelector.ts index 779d449..a3061c2 100644 --- a/ext/src/background/receiverSelector/NativeReceiverSelector.ts +++ b/ext/src/background/receiverSelector/NativeReceiverSelector.ts @@ -65,6 +65,12 @@ export default class NativeReceiverSelector this.onBridgePortMessageClose(); break; } + case "main:/receiverSelector/stop": { + this.dispatchEvent(new CustomEvent("stop", { + detail: message.data + })); + break; + } } }); @@ -96,6 +102,7 @@ export default class NativeReceiverSelector , i18n_extensionName: _("extensionName") , i18n_castButtonTitle: _("popupCastButtonTitle") + , i18n_stopButtonTitle: _("popupStopButtonTitle") , i18n_mediaTypeApp: knownApps[requestedAppId] ?? _("popupMediaTypeApp") , i18n_mediaTypeTab: _("popupMediaTypeTab") diff --git a/ext/src/background/receiverSelector/PopupReceiverSelector.ts b/ext/src/background/receiverSelector/PopupReceiverSelector.ts index 4fd91b8..1845a4c 100644 --- a/ext/src/background/receiverSelector/PopupReceiverSelector.ts +++ b/ext/src/background/receiverSelector/PopupReceiverSelector.ts @@ -154,6 +154,14 @@ export default class PopupReceiverSelector break; } + + case "receiverSelector:/stop": { + this.dispatchEvent(new CustomEvent("stop", { + detail: message.data + })); + + break; + } } } diff --git a/ext/src/background/receiverSelector/ReceiverSelector.ts b/ext/src/background/receiverSelector/ReceiverSelector.ts index aa7412c..464a5e7 100644 --- a/ext/src/background/receiverSelector/ReceiverSelector.ts +++ b/ext/src/background/receiverSelector/ReceiverSelector.ts @@ -22,6 +22,7 @@ export interface ReceiverSelectorEvents { "selected": ReceiverSelection; "error": string; "cancelled": void; + "stop": { receiver: Receiver }; } export default interface ReceiverSelector diff --git a/ext/src/background/receiverSelector/ReceiverSelectorManager.ts b/ext/src/background/receiverSelector/ReceiverSelectorManager.ts index bafff06..738262b 100644 --- a/ext/src/background/receiverSelector/ReceiverSelectorManager.ts +++ b/ext/src/background/receiverSelector/ReceiverSelectorManager.ts @@ -17,6 +17,8 @@ import { ReceiverSelection import NativeReceiverSelector from "./NativeReceiverSelector"; import PopupReceiverSelector from "./PopupReceiverSelector"; +import { Receiver } from "../../types"; + async function createSelector () { const type = await options.get("receiverSelectorType"); @@ -60,9 +62,18 @@ async function getSelection ( : Promise { return new Promise(async (resolve, reject) => { - const currentShim = ShimManager.getShim( + let currentShim = ShimManager.getShim( contextTabId, contextFrameId); + /** + * If the current context is running the mirroring app, pretend + * it doesn't exist because it shouldn't be launched like this. + */ + if (currentShim?.requestedAppId === + await options.get("mirroringAppId")) { + currentShim = null; + } + let defaultMediaType = ReceiverSelectorMediaType.Tab; let availableMediaTypes; @@ -107,20 +118,43 @@ async function getSelection ( // Get a new selector for each selection sharedSelector = await createSelector(); - sharedSelector.addEventListener("selected", ev => { + sharedSelector.addEventListener("selected", onSelected); + sharedSelector.addEventListener("cancelled", onCancelled); + sharedSelector.addEventListener("error", onError); + sharedSelector.addEventListener("stop", onStop); + + function removeListeners () { + sharedSelector.removeEventListener("selected", onSelected); + sharedSelector.removeEventListener("cancelled", onCancelled); + sharedSelector.removeEventListener("error", onError); + sharedSelector.removeEventListener("stop", onStop); + } + + function onSelected (ev: any) { console.info("fx_cast (Debug): Selected receiver", ev.detail); resolve(ev.detail); - }); + removeListeners(); + } - sharedSelector.addEventListener("cancelled", () => { + function onCancelled () { console.info("fx_cast (Debug): Cancelled receiver selection"); resolve(null); - }); + removeListeners(); + } - sharedSelector.addEventListener("error", () => { + function onError () { console.error("fx_cast (Debug): Failed to select receiver"); reject(); - }); + removeListeners(); + } + + function onStop (ev: any) { + console.info("fx_cast (Debug): Stopped receiver app", ev.detail); + + StatusManager.init().then(() => { + StatusManager.stopReceiverApp(ev.detail.receiver); + }); + } // Ensure status manager is initialized diff --git a/ext/src/ui/popup/index.tsx b/ext/src/ui/popup/index.tsx index f5f1013..dbdb899 100755 --- a/ext/src/ui/popup/index.tsx +++ b/ext/src/ui/popup/index.tsx @@ -59,6 +59,7 @@ class PopupApp extends Component<{}, PopupAppState> { this.onSelectChange = this.onSelectChange.bind(this); this.onCast = this.onCast.bind(this); + this.onStop = this.onStop.bind(this); } public componentDidMount () { @@ -169,6 +170,7 @@ class PopupApp extends Component<{}, PopupAppState> { ? this.state.receivers.map((receiver, i) => ( )) @@ -196,6 +198,13 @@ class PopupApp extends Component<{}, PopupAppState> { }); } + private onStop (receiver: Receiver) { + this.port.postMessage({ + subject: "receiverSelector:/stop" + , data: { receiver } + }); + } + private onSelectChange (ev: React.ChangeEvent) { const mediaType = parseInt(ev.target.value); @@ -232,11 +241,13 @@ interface ReceiverEntryProps { isLoading: boolean; canCast: boolean; onCast (receiver: Receiver): void; + onStop (receiver: Receiver): void; } interface ReceiverEntryState { ellipsis: string; isLoading: boolean; + showAlternateAction: boolean; } class ReceiverEntry extends Component { @@ -244,10 +255,26 @@ class ReceiverEntry extends Component { super(props); this.state = { - isLoading: false - , ellipsis: "" + ellipsis: "" + , isLoading: false + , showAlternateAction: false }; + window.addEventListener("keydown", ev => { + if (ev.key === "Alt") { + this.setState({ + showAlternateAction: true + }); + } + }); + window.addEventListener("keyup", ev => { + if (ev.key === "Alt") { + this.setState({ + showAlternateAction: false + }); + } + }); + this.handleCast = this.handleCast.bind(this); } @@ -273,25 +300,33 @@ class ReceiverEntry extends Component { , (this.state.isLoading ? this.state.ellipsis : "")) - : _("popupCastButtonTitle") } + : !application.isIdleScreen && this.state.showAlternateAction + ? _("popupStopButtonTitle") + : _("popupCastButtonTitle") } ); } private handleCast () { - this.props.onCast(this.props.receiver); + const { application } = this.props.receiver.status; - this.setState({ - isLoading: true - }); + if (!application.isIdleScreen && this.state.showAlternateAction) { + this.props.onStop(this.props.receiver); + } else { + this.props.onCast(this.props.receiver); - setInterval(() => { - this.setState(state => ({ - ellipsis: getNextEllipsis(state.ellipsis) - })); + this.setState({ + isLoading: true + }); - }, 500); + setInterval(() => { + this.setState(state => ({ + ellipsis: getNextEllipsis(state.ellipsis) + })); + + }, 500); + } } }