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
|
// <video>/<audio> "Cast..." context menu item
|
||||||
browser.menus.create({
|
browser.menus.create({
|
||||||
@@ -80,16 +86,46 @@ browser.menus.create({
|
|||||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||||
const { frameId } = info;
|
const { frameId } = info;
|
||||||
|
|
||||||
// Pass media URL to media sender app
|
// Load cast setup script
|
||||||
await browser.tabs.executeScript(tab.id, {
|
await browser.tabs.executeScript(tab.id, {
|
||||||
code: `const srcUrl = "${info.srcUrl}";`
|
file: "content.js"
|
||||||
, frameId
|
, frameId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load app and sender API shim
|
switch (info.menuItemId) {
|
||||||
await browser.tabs.executeScript(tab.id, { file: "content.js" , frameId })
|
case "contextCast":
|
||||||
await browser.tabs.executeScript(tab.id, { file: "mediaCast.js" , frameId });
|
await browser.tabs.executeScript(tab.id, {
|
||||||
await browser.tabs.executeScript(tab.id, { file: "shim/bundle.js" , frameId });
|
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;
|
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 {
|
.receivers {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: initial;
|
margin: initial;
|
||||||
@@ -18,10 +29,10 @@ body {
|
|||||||
grid-template-rows: min-content min-content 1fr;
|
grid-template-rows: min-content min-content 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"name connect"
|
"name connect"
|
||||||
"address connect"
|
"address connect";
|
||||||
". connect";
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.75em 1em;
|
margin: 0 1em;
|
||||||
|
padding: 0.75em 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +57,9 @@ body {
|
|||||||
grid-area: address;
|
grid-area: address;
|
||||||
}
|
}
|
||||||
.receiver-connect {
|
.receiver-connect {
|
||||||
|
align-self: center;
|
||||||
grid-area: connect;
|
grid-area: connect;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class App extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
receivers: []
|
receivers: []
|
||||||
|
, selectedMedia: "app"
|
||||||
, isLoading: false
|
, isLoading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,7 +39,8 @@ class App extends Component {
|
|||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "popup:populate":
|
case "popup:populate":
|
||||||
this.setState({
|
this.setState({
|
||||||
receivers: message.data
|
receivers: message.data.receivers
|
||||||
|
, selectedMedia: message.data.selectedMedia
|
||||||
});
|
});
|
||||||
|
|
||||||
winHeight = document.body.clientHeight + frameHeight;
|
winHeight = document.body.clientHeight + frameHeight;
|
||||||
@@ -63,21 +65,45 @@ class App extends Component {
|
|||||||
|
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
subject: "shim:selectReceiver"
|
subject: "shim:selectReceiver"
|
||||||
, data: receiver
|
, data: {
|
||||||
|
receiver
|
||||||
|
, selectedMedia: this.state.selectedMedia
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectChange (ev) {
|
||||||
|
this.setState({
|
||||||
|
selectedMedia: ev.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const shareMedia =
|
||||||
|
this.state.selectedMedia === "tab"
|
||||||
|
|| this.state.selectedMedia === "screen";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="receivers">
|
<div>
|
||||||
{ this.state.receivers.map(receiver => {
|
<div className="media-select">
|
||||||
return (
|
Cast
|
||||||
<Receiver receiver={receiver}
|
<select value={this.state.selectedMedia} onChange={this.onSelectChange.bind(this)} className="media-select-dropdown">
|
||||||
onCast={this.onCast.bind(this)}
|
<option value="app" disabled={shareMedia}>this site's app</option>
|
||||||
isLoading={this.state.isLoading} />
|
<option value="tab" disabled={!shareMedia}>Tab</option>
|
||||||
);
|
<option value="screen" disabled={!shareMedia}>Screen</option>
|
||||||
})}
|
</select>
|
||||||
</ul>
|
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
|
, sessionListener
|
||||||
, receiverListener
|
, receiverListener
|
||||||
, opt_autoJoinPolicy = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
|
, 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.autoJoinPolicy = opt_autoJoinPolicy;
|
||||||
this.defaultActionPolicy = opt_defaultActionPolicy;
|
this.defaultActionPolicy = opt_defaultActionPolicy;
|
||||||
@@ -20,5 +22,7 @@ export default class ApiConfig {
|
|||||||
this.additionalSessionRequests = [];
|
this.additionalSessionRequests = [];
|
||||||
this.customDialLaunchCallback = null;
|
this.customDialLaunchCallback = null;
|
||||||
this.invisibleSender = false;
|
this.invisibleSender = false;
|
||||||
|
|
||||||
|
this._selectedMedia = selectedMedia;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ onMessage(message => {
|
|||||||
|
|
||||||
case "shim:selectReceiver":
|
case "shim:selectReceiver":
|
||||||
console.info("Caster (Debug): Selected receiver");
|
console.info("Caster (Debug): Selected receiver");
|
||||||
const selectedReceiver = message.data;
|
const selectedReceiver = message.data.receiver;
|
||||||
|
|
||||||
const sessionConstructorArgs = [
|
const sessionConstructorArgs = [
|
||||||
state.sessionList.length // sessionId
|
state.sessionList.length // sessionId
|
||||||
@@ -218,7 +218,7 @@ onMessage(message => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
state.apiConfig.sessionListener(session);
|
state.apiConfig.sessionListener(session);
|
||||||
sessionSuccessCallback(session);
|
sessionSuccessCallback(session, message.data.selectedMedia);
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -247,7 +247,10 @@ onMessage(message => {
|
|||||||
case "shim:popupReady":
|
case "shim:popupReady":
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "popup:populate"
|
subject: "popup:populate"
|
||||||
, data: state.receiverList
|
, data: {
|
||||||
|
receivers: state.receiverList
|
||||||
|
, selectedMedia: state.apiConfig._selectedMedia
|
||||||
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ const output_path = path.resolve(__dirname, "../dist/unpacked");
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
"main" : `${include_path}/main.js`
|
"main" : `${include_path}/main.js`
|
||||||
, "popup/bundle" : `${include_path}/popup/index.js`
|
, "popup/bundle" : `${include_path}/popup/index.js`
|
||||||
, "shim/bundle" : `${include_path}/shim/index.js`
|
, "shim/bundle" : `${include_path}/shim/index.js`
|
||||||
, "content" : `${include_path}/content.js`
|
, "content" : `${include_path}/content.js`
|
||||||
, "contentSetup" : `${include_path}/contentSetup.js`
|
, "contentSetup" : `${include_path}/contentSetup.js`
|
||||||
, "mediaCast" : `${include_path}/mediaCast.js`
|
, "mediaCast" : `${include_path}/mediaCast.js`
|
||||||
|
, "mirroringCast" : `${include_path}/mirroringCast.js`
|
||||||
}
|
}
|
||||||
, output: {
|
, output: {
|
||||||
filename: "[name].js"
|
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