Add stop action to receiver selectors

This commit is contained in:
hensm
2020-01-16 00:47:38 +00:00
parent 4858642caa
commit 9295d8ee83
15 changed files with 219 additions and 30 deletions

View File

@@ -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);
}
}

View File

@@ -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")
}
}
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
struct ReceiverSelection : Codable {
let receiver: Receiver
let mediaType: MediaType
let mediaType: MediaType?
let filePath: String?
}

View File

@@ -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<string, Channel>();
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

View File

@@ -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";
/**

View File

@@ -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
});
});

View File

@@ -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;

View File

@@ -56,6 +56,10 @@
}
}
}
, "popupStopButtonTitle": {
"message": "Stop"
, "description": "Alternate action button text displayed instead of popupCastButtonTitle."
}
, "contextCast": {

View File

@@ -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);

View File

@@ -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")

View File

@@ -154,6 +154,14 @@ export default class PopupReceiverSelector
break;
}
case "receiverSelector:/stop": {
this.dispatchEvent(new CustomEvent("stop", {
detail: message.data
}));
break;
}
}
}

View File

@@ -22,6 +22,7 @@ export interface ReceiverSelectorEvents {
"selected": ReceiverSelection;
"error": string;
"cancelled": void;
"stop": { receiver: Receiver };
}
export default interface ReceiverSelector

View File

@@ -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<ReceiverSelection> {
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

View File

@@ -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) => (
<ReceiverEntry receiver={ receiver }
onCast={ this.onCast }
onStop={ this.onStop }
isLoading={ this.state.isLoading }
canCast={ canCast }
key={ 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<HTMLSelectElement>) {
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<ReceiverEntryProps, ReceiverEntryState> {
@@ -244,10 +255,26 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
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<ReceiverEntryProps, ReceiverEntryState> {
, (this.state.isLoading
? this.state.ellipsis
: ""))
: _("popupCastButtonTitle") }
: !application.isIdleScreen && this.state.showAlternateAction
? _("popupStopButtonTitle")
: _("popupCastButtonTitle") }
</button>
</li>
);
}
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);
}
}
}