mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Initial screen/tab casting implementation
This commit is contained in:
@@ -65,6 +65,12 @@ browser.contentScripts.register({
|
||||
});
|
||||
|
||||
|
||||
// Screen/Tab mirroring "Cast..." context menu item
|
||||
browser.menus.create({
|
||||
contexts: [ "browser_action", "page" ]
|
||||
, id: "contextCast"
|
||||
, title: _("context_media_cast")
|
||||
});
|
||||
|
||||
// <video>/<audio> "Cast..." context menu item
|
||||
browser.menus.create({
|
||||
@@ -80,16 +86,46 @@ browser.menus.create({
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
const { frameId } = info;
|
||||
|
||||
// Pass media URL to media sender app
|
||||
// Load cast setup script
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: `const srcUrl = "${info.srcUrl}";`
|
||||
file: "content.js"
|
||||
, frameId
|
||||
});
|
||||
|
||||
// Load app and sender API shim
|
||||
await browser.tabs.executeScript(tab.id, { file: "content.js" , frameId })
|
||||
await browser.tabs.executeScript(tab.id, { file: "mediaCast.js" , frameId });
|
||||
await browser.tabs.executeScript(tab.id, { file: "shim/bundle.js" , frameId });
|
||||
switch (info.menuItemId) {
|
||||
case "contextCast":
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: `const selectedMedia = "${info.pageUrl ? "tab" : "screen"}";`
|
||||
, frameId
|
||||
});
|
||||
|
||||
// Load mirroring sender app
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "mirroringCast.js"
|
||||
, frameId
|
||||
});
|
||||
break;
|
||||
|
||||
case "contextCastMedia":
|
||||
// Pass media URL to media sender app
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: `const srcUrl = "${info.srcUrl}";`
|
||||
, frameId
|
||||
});
|
||||
|
||||
// Load media sender app
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "mediaCast.js"
|
||||
, frameId
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Load cast API
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "shim/bundle.js"
|
||||
, frameId
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
147
ext/src/mirroringCast.js
Normal file
147
ext/src/mirroringCast.js
Normal file
@@ -0,0 +1,147 @@
|
||||
"use strict";
|
||||
|
||||
let chrome;
|
||||
let logMessage;
|
||||
|
||||
const FX_CAST_RECEIVER_APP_ID = "19A6F4AE";
|
||||
const FX_CAST_NAMESPACE = "urn:x-cast:fx_cast";
|
||||
|
||||
let session;
|
||||
let sessionRequested = false;
|
||||
|
||||
let pc;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
function size_canvas (
|
||||
width = window.innerWidth
|
||||
, height = window.innerHeight) {
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
// Set initial size
|
||||
size_canvas();
|
||||
|
||||
// Resize canvas whenever the window resizes
|
||||
window.addEventListener("resize", () => {
|
||||
size_canvas();
|
||||
});
|
||||
|
||||
let interval;
|
||||
|
||||
|
||||
function sendMessage (subject, data) {
|
||||
session.sendMessage(FX_CAST_NAMESPACE, {
|
||||
subject
|
||||
, data
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
sendMessage("close");
|
||||
});
|
||||
|
||||
async function onRequestSessionSuccess (session_, selectedMedia) {
|
||||
logMessage("onRequestSessionSuccess");
|
||||
|
||||
session = session_;
|
||||
|
||||
session.addMessageListener(FX_CAST_NAMESPACE
|
||||
, async (namespace, message) => {
|
||||
|
||||
const { subject, data } = JSON.parse(message);
|
||||
|
||||
switch (subject) {
|
||||
case "peerConnectionAnswer":
|
||||
pc.setRemoteDescription(data);
|
||||
break;
|
||||
|
||||
case "iceCandidate":
|
||||
pc.addIceCandidate(data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
pc = new RTCPeerConnection();
|
||||
pc.addEventListener("icecandidate", (ev) => {
|
||||
sendMessage("iceCandidate", ev.candidate);
|
||||
});
|
||||
|
||||
switch (selectedMedia) {
|
||||
case "tab":
|
||||
interval = setInterval(() => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawWindow(
|
||||
window // window
|
||||
, 0 // x
|
||||
, 0 // y
|
||||
, window.innerWidth // w
|
||||
, window.innerHeight // h
|
||||
, "white" // bgColor
|
||||
, ctx.DRAWWINDOW_DRAW_VIEW); // flags
|
||||
}, 1000 / 30);
|
||||
pc.addStream(canvas.captureStream());
|
||||
break;
|
||||
|
||||
case "screen":
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { mediaSource: "window" }
|
||||
});
|
||||
pc.addStream(stream);
|
||||
break;
|
||||
}
|
||||
|
||||
const desc = await pc.createOffer();
|
||||
await pc.setLocalDescription(desc);
|
||||
|
||||
sendMessage("peerConnectionOffer", desc);
|
||||
}
|
||||
function onRequestSessionError () {
|
||||
logMessage("onRequestSessionError");
|
||||
}
|
||||
|
||||
function sessionListener (session) {
|
||||
logMessage("sessionListener");
|
||||
}
|
||||
function receiverListener (availability) {
|
||||
logMessage("receiverListener");
|
||||
|
||||
if (!sessionRequested && availability === chrome.cast.ReceiverAvailability.AVAILABLE) {
|
||||
sessionRequested = true;
|
||||
chrome.cast.requestSession(
|
||||
onRequestSessionSuccess
|
||||
, onRequestSessionError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onInitializeSuccess () {
|
||||
logMessage("onInitializeSuccess");
|
||||
}
|
||||
function onInitializeError () {
|
||||
logMessage("onInitializeError");
|
||||
}
|
||||
|
||||
|
||||
window.__onGCastApiAvailable = (loaded, errorInfo) => {
|
||||
chrome = window.chrome;
|
||||
logMessage = chrome.cast.logMessage;
|
||||
|
||||
logMessage("__onGCastApiAvailable success");
|
||||
|
||||
const sessionRequest = new chrome.cast.SessionRequest(
|
||||
FX_CAST_RECEIVER_APP_ID);
|
||||
|
||||
const apiConfig = new chrome.cast.ApiConfig(sessionRequest
|
||||
, sessionListener
|
||||
, receiverListener
|
||||
, undefined, undefined
|
||||
, selectedMedia);
|
||||
|
||||
chrome.cast.initialize(apiConfig
|
||||
, onInitializeSuccess
|
||||
, onInitializeError);
|
||||
}
|
||||
@@ -5,6 +5,17 @@ body {
|
||||
font: message-box;
|
||||
}
|
||||
|
||||
.media-select {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||
margin: 0 1em;
|
||||
padding: 0.75em 0;
|
||||
}
|
||||
|
||||
.media-select-dropdown {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.receivers {
|
||||
list-style: none;
|
||||
margin: initial;
|
||||
@@ -18,10 +29,10 @@ body {
|
||||
grid-template-rows: min-content min-content 1fr;
|
||||
grid-template-areas:
|
||||
"name connect"
|
||||
"address connect"
|
||||
". connect";
|
||||
"address connect";
|
||||
justify-content: center;
|
||||
padding: 0.75em 1em;
|
||||
margin: 0 1em;
|
||||
padding: 0.75em 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -46,7 +57,9 @@ body {
|
||||
grid-area: address;
|
||||
}
|
||||
.receiver-connect {
|
||||
align-self: center;
|
||||
grid-area: connect;
|
||||
justify-self: end;
|
||||
min-width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class App extends Component {
|
||||
|
||||
this.state = {
|
||||
receivers: []
|
||||
, selectedMedia: "app"
|
||||
, isLoading: false
|
||||
};
|
||||
|
||||
@@ -38,7 +39,8 @@ class App extends Component {
|
||||
switch (message.subject) {
|
||||
case "popup:populate":
|
||||
this.setState({
|
||||
receivers: message.data
|
||||
receivers: message.data.receivers
|
||||
, selectedMedia: message.data.selectedMedia
|
||||
});
|
||||
|
||||
winHeight = document.body.clientHeight + frameHeight;
|
||||
@@ -63,21 +65,45 @@ class App extends Component {
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
subject: "shim:selectReceiver"
|
||||
, data: receiver
|
||||
, data: {
|
||||
receiver
|
||||
, selectedMedia: this.state.selectedMedia
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSelectChange (ev) {
|
||||
this.setState({
|
||||
selectedMedia: ev.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const shareMedia =
|
||||
this.state.selectedMedia === "tab"
|
||||
|| this.state.selectedMedia === "screen";
|
||||
|
||||
return (
|
||||
<ul className="receivers">
|
||||
{ this.state.receivers.map(receiver => {
|
||||
return (
|
||||
<Receiver receiver={receiver}
|
||||
onCast={this.onCast.bind(this)}
|
||||
isLoading={this.state.isLoading} />
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<div className="media-select">
|
||||
Cast
|
||||
<select value={this.state.selectedMedia} onChange={this.onSelectChange.bind(this)} className="media-select-dropdown">
|
||||
<option value="app" disabled={shareMedia}>this site's app</option>
|
||||
<option value="tab" disabled={!shareMedia}>Tab</option>
|
||||
<option value="screen" disabled={!shareMedia}>Screen</option>
|
||||
</select>
|
||||
to:
|
||||
</div>
|
||||
<ul className="receivers">
|
||||
{ this.state.receivers.map(receiver => {
|
||||
return (
|
||||
<Receiver receiver={receiver}
|
||||
onCast={this.onCast.bind(this)}
|
||||
isLoading={this.state.isLoading} />
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ export default class ApiConfig {
|
||||
, sessionListener
|
||||
, receiverListener
|
||||
, opt_autoJoinPolicy = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
|
||||
, opt_defaultActionPolicy = DefaultActionPolicy.CREATE_SESSION) {
|
||||
, opt_defaultActionPolicy = DefaultActionPolicy.CREATE_SESSION
|
||||
// TODO: Remove awful hack for mirror casting
|
||||
, selectedMedia = "app") {
|
||||
|
||||
this.autoJoinPolicy = opt_autoJoinPolicy;
|
||||
this.defaultActionPolicy = opt_defaultActionPolicy;
|
||||
@@ -20,5 +22,7 @@ export default class ApiConfig {
|
||||
this.additionalSessionRequests = [];
|
||||
this.customDialLaunchCallback = null;
|
||||
this.invisibleSender = false;
|
||||
|
||||
this._selectedMedia = selectedMedia;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,7 +204,7 @@ onMessage(message => {
|
||||
|
||||
case "shim:selectReceiver":
|
||||
console.info("Caster (Debug): Selected receiver");
|
||||
const selectedReceiver = message.data;
|
||||
const selectedReceiver = message.data.receiver;
|
||||
|
||||
const sessionConstructorArgs = [
|
||||
state.sessionList.length // sessionId
|
||||
@@ -218,7 +218,7 @@ onMessage(message => {
|
||||
});
|
||||
|
||||
state.apiConfig.sessionListener(session);
|
||||
sessionSuccessCallback(session);
|
||||
sessionSuccessCallback(session, message.data.selectedMedia);
|
||||
}
|
||||
];
|
||||
|
||||
@@ -247,7 +247,10 @@ onMessage(message => {
|
||||
case "shim:popupReady":
|
||||
sendMessage({
|
||||
subject: "popup:populate"
|
||||
, data: state.receiverList
|
||||
, data: {
|
||||
receivers: state.receiverList
|
||||
, selectedMedia: state.apiConfig._selectedMedia
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ const output_path = path.resolve(__dirname, "../dist/unpacked");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
"main" : `${include_path}/main.js`
|
||||
, "popup/bundle" : `${include_path}/popup/index.js`
|
||||
, "shim/bundle" : `${include_path}/shim/index.js`
|
||||
, "content" : `${include_path}/content.js`
|
||||
, "contentSetup" : `${include_path}/contentSetup.js`
|
||||
, "mediaCast" : `${include_path}/mediaCast.js`
|
||||
"main" : `${include_path}/main.js`
|
||||
, "popup/bundle" : `${include_path}/popup/index.js`
|
||||
, "shim/bundle" : `${include_path}/shim/index.js`
|
||||
, "content" : `${include_path}/content.js`
|
||||
, "contentSetup" : `${include_path}/contentSetup.js`
|
||||
, "mediaCast" : `${include_path}/mediaCast.js`
|
||||
, "mirroringCast" : `${include_path}/mirroringCast.js`
|
||||
}
|
||||
, output: {
|
||||
filename: "[name].js"
|
||||
|
||||
9
receiver/index.css
Normal file
9
receiver/index.css
Normal file
@@ -0,0 +1,9 @@
|
||||
body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#media {
|
||||
max-height: 100vh;
|
||||
max-width: 100vw;
|
||||
}
|
||||
10
receiver/index.html
Normal file
10
receiver/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<video id="media" autoplay></video>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
66
receiver/index.js
Normal file
66
receiver/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
|
||||
const NAMESPACE = "urn:x-cast:fx_cast";
|
||||
|
||||
const castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
|
||||
|
||||
const mediaElement = document.querySelector("#media");
|
||||
const mediaManager = new cast.receiver.MediaManager(mediaElement);
|
||||
mediaElement.width = window.innerWidth;
|
||||
|
||||
|
||||
let senderId;
|
||||
|
||||
const messageBus = castReceiverManager.getCastMessageBus(
|
||||
NAMESPACE
|
||||
, cast.receiver.CastMessageBus.MessageType.JSON);
|
||||
|
||||
|
||||
const pc = new RTCPeerConnection();
|
||||
|
||||
pc.addEventListener("icecandidate", ev => {
|
||||
messageBus.send(senderId, {
|
||||
subject: "iceCandidate"
|
||||
, data: ev.candidate
|
||||
});
|
||||
});
|
||||
pc.addEventListener("addstream", ev => {
|
||||
mediaElement.srcObject = ev.stream;
|
||||
mediaElement.webkitRequestFullscreen();
|
||||
});
|
||||
|
||||
messageBus.onMessage = async message => {
|
||||
const { subject, data } = message.data;
|
||||
|
||||
senderId = message.senderId;
|
||||
|
||||
switch (subject) {
|
||||
case "peerConnectionOffer":
|
||||
await pc.setRemoteDescription(data);
|
||||
const desc = await pc.createAnswer();
|
||||
await pc.setLocalDescription(desc);
|
||||
messageBus.send(message.senderId, {
|
||||
subject: "peerConnectionAnswer"
|
||||
, data: desc
|
||||
});
|
||||
break;
|
||||
|
||||
case "iceCandidate":
|
||||
console.log(data);
|
||||
await pc.addIceCandidate(data);
|
||||
break;
|
||||
|
||||
case "close":
|
||||
window.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Fix APi shim to make this work
|
||||
castReceiverManager.onSenderDisconnected = ev => {
|
||||
if (castReceiverManager.getSenders().length <= 0) {
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
castReceiverManager.start();
|
||||
Reference in New Issue
Block a user