initial commit
This commit is contained in:
commit
240332c0d2
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Lukas Haubaum
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Muffcast
|
||||||
|
An alternative to Chromecast working with every website with HTML5 Video/Audio elements by just playing HTML5 video/audio on other Firefox instance in full-screen.
|
||||||
|
|
||||||
|
## Client
|
||||||
|
This is the client extension. Go to any website with HTML5 Video elements to play it on the **Muffcast Server** instance by clicking on a **Muffcast**-symbol next to it.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Firefox/Browser
|
||||||
|
- **Muffcast Server** running in same network
|
||||||
|
- Internet access
|
||||||
|
|
||||||
|
### Firefox/Browser Setup
|
||||||
|
In Add-ons Preferences, define URL of the *Muffcast Server*-extension running on any device in your network on port 8128.
|
||||||
|
|
||||||
|
###### Limitations
|
||||||
|
This only works for websites with HTML5 Video/Audio elements. This does not work in native applications.
|
||||||
|
|
||||||
|
To work properly for websites with authentication (like Netflix), the browser on server side also needs valid session. This extension does not handle any authentication, so valid sessions are required. In short: manually login and save session before use.
|
||||||
|
|
||||||
|
There are some websites that required further interactions before the HTML5 Video is loaded properly, e.g. to click a non-standard play button. Those sites do not work without special treatment in the server component. Please feel free to report issues with those sites for being included in server component code.
|
||||||
|
|
||||||
|
This extension is developed and tested in Firefox 57. A port for other browsers like Chrome should be easy due to WebExtensions API, but is not warranted to work properly.
|
||||||
|
|
||||||
|
Video quality is not part of the Media API and any websites handles this on it's own. So like authentication, to control playback quality, manually settings on server side are required. (Hopefully the automatic settings fit your needs, but e.g. on a Raspberry Pi to high quality can cause stuttering.)
|
88
background/client.js
Normal file
88
background/client.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
console.log("muffcast client v0.1");
|
||||||
|
|
||||||
|
var muffcastUrl = "http://localhost:8128";
|
||||||
|
browser.storage.local.get("muffcast").then(function(result) {
|
||||||
|
muffcastUrl = result.muffcast && result.muffcast.url || muffcastUrl;
|
||||||
|
})
|
||||||
|
|
||||||
|
var sendServer = function(message) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.open("POST", muffcastUrl, true);
|
||||||
|
xhttp.addEventListener("load", function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
var response = this.responseText ? JSON.parse(this.responseText) : false;
|
||||||
|
resolve(response);
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: this.status,
|
||||||
|
error: this.statusText,
|
||||||
|
body: this.responseText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhttp.setRequestHeader("content-type", "application/json");
|
||||||
|
xhttp.send(JSON.stringify(message));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var clientUpdate = function() {
|
||||||
|
browser.tabs.query({
|
||||||
|
currentWindow: true,
|
||||||
|
active: true
|
||||||
|
}).then(function(tabs) {
|
||||||
|
var tab = tabs[0];
|
||||||
|
browser.tabs.sendMessage(
|
||||||
|
tab.id, {
|
||||||
|
command: "update"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var injectCss = function(tabId) {
|
||||||
|
browser.tabs.insertCSS(tabId, {
|
||||||
|
code: "@font-face { font-family: 'FontAwesome';" +
|
||||||
|
"src: url('" + browser.extension.getURL("fonts/fontawesome.eot") + "?v=4.7.0');" +
|
||||||
|
"src: url('" + browser.extension.getURL("fonts/fontawesome.eot") + "?#iefix&v=4.7.0') format('embedded-opentype')," +
|
||||||
|
"url('" + browser.extension.getURL("fonts/fontawesome.woff2") + "?v=4.7.0') format('woff2')," +
|
||||||
|
"url('" + browser.extension.getURL("fonts/fontawesome.woff") + "?v=4.7.0') format('woff')," +
|
||||||
|
"url('" + browser.extension.getURL("fonts/fontawesome.ttf") + "?v=4.7.0') format('truetype')," +
|
||||||
|
"url('" + browser.extension.getURL("fonts/fontawesome.svg") + "?v=4.7.0#fontawesomeregular') format('svg');" +
|
||||||
|
"font - weight: normal;" +
|
||||||
|
"font - style: normal;}"
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.insertCSS(tabId, {
|
||||||
|
file: "css/font-awesome.css"
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.insertCSS(tabId, {
|
||||||
|
file: "css/overlay.css"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.tabs.onActivated.addListener(function(tab) {
|
||||||
|
injectCss(tab.id);
|
||||||
|
clientUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.onUpdated.addListener(function(tabId, changeInfo) {
|
||||||
|
injectCss(tabId);
|
||||||
|
if (changeInfo.status === "complete") {
|
||||||
|
clientUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener(function(message) {
|
||||||
|
console.log("send", message);
|
||||||
|
sendServer(message).then(function(response) {
|
||||||
|
console.log("response", response);
|
||||||
|
if (message.command == "load") {
|
||||||
|
clientUpdate();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
2928
css/font-awesome.css
vendored
Normal file
2928
css/font-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
117
css/overlay.css
Normal file
117
css/overlay.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
a.muffcast-loader {
|
||||||
|
position: absolute;
|
||||||
|
color: #d13d29;
|
||||||
|
font-size: 20px;
|
||||||
|
z-index: 8128;
|
||||||
|
padding-left: 3px;
|
||||||
|
padding-top: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.muffcast-loader:hover {
|
||||||
|
color: #9e2e1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.muffcast-loader.active {
|
||||||
|
color: #468ac3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay {
|
||||||
|
font-family: 'Roboto', 'sans-serif', 'sans';
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: -moz-flex;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 55px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #efefef;
|
||||||
|
position: fixed;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay a {
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 35px;
|
||||||
|
color: #d13d29;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay a:hover {
|
||||||
|
color: #9e2e1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay span.time {
|
||||||
|
padding: 11px 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-icon {
|
||||||
|
color: #468ac3;
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-audio {
|
||||||
|
position: relative;
|
||||||
|
width: 35px;
|
||||||
|
min-width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.3s, min-width 0.3s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-audio:hover {
|
||||||
|
width: 160px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-mute {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay input[type="range"] {
|
||||||
|
float: left;
|
||||||
|
margin: 0;
|
||||||
|
height: 35px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay input[type="range"]::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #468ac3;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay input[type="range"]::-moz-range-thumb {
|
||||||
|
box-shadow: 0px 0px 0px #d13d29;
|
||||||
|
border: 0px solid #d13d29;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #d13d29;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
color: #9e2e1f;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-volume {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast-overlay #muffcast-seek {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
BIN
fonts/FontAwesome.otf
Normal file
BIN
fonts/FontAwesome.otf
Normal file
Binary file not shown.
BIN
fonts/fontawesome.eot
Normal file
BIN
fonts/fontawesome.eot
Normal file
Binary file not shown.
2671
fonts/fontawesome.svg
Normal file
2671
fonts/fontawesome.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
BIN
fonts/fontawesome.ttf
Normal file
BIN
fonts/fontawesome.ttf
Normal file
Binary file not shown.
BIN
fonts/fontawesome.woff
Normal file
BIN
fonts/fontawesome.woff
Normal file
Binary file not shown.
BIN
fonts/fontawesome.woff2
Normal file
BIN
fonts/fontawesome.woff2
Normal file
Binary file not shown.
BIN
icons/muffcast-client-32-light.png
Normal file
BIN
icons/muffcast-client-32-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 B |
BIN
icons/muffcast-client-32.png
Normal file
BIN
icons/muffcast-client-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 213 B |
BIN
icons/muffcast-client-48.png
Normal file
BIN
icons/muffcast-client-48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 230 B |
61
manifest.json
Normal file
61
manifest.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"description": "Cast HTML5 video to other Firefox instance. This is the client extension. Setup: define url of the Muffcast Server Extension, running on any device in your network on port 8128. After Setup, go to any website with HTML5 Video elements and cast it on server.",
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "Muffcast",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"homepage_url": "https://www.champonthis.de/projects/muffcast",
|
||||||
|
"icons": {
|
||||||
|
"48": "icons/muffcast-client-48.png"
|
||||||
|
},
|
||||||
|
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "muffcast-client@champonthis.de"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"http://*/",
|
||||||
|
"https://*/",
|
||||||
|
"nativeMessaging",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background/client.js"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": "icons/muffcast-client-32.png",
|
||||||
|
"theme_icons": [{
|
||||||
|
"light": "icons/muffcast-client-32-light.png",
|
||||||
|
"dark": "icons/muffcast-client-32.png",
|
||||||
|
"size": 32
|
||||||
|
}],
|
||||||
|
"default_title": "Muffcast",
|
||||||
|
"default_popup": "popup/popup.html"
|
||||||
|
},
|
||||||
|
|
||||||
|
"content_scripts": [{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["muffcast.js"]
|
||||||
|
}],
|
||||||
|
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"browser_style": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"icons/muffcast-client-32.png",
|
||||||
|
"fonts/fontawesome.eot",
|
||||||
|
"fonts/fontawesome.otf",
|
||||||
|
"fonts/fontawesome.svg",
|
||||||
|
"fonts/fontawesome.ttf",
|
||||||
|
"fonts/fontawesome.woff",
|
||||||
|
"fonts/fontawesome.woff2"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
358
muffcast.js
Normal file
358
muffcast.js
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
console.log("muffcast client v0.1");
|
||||||
|
|
||||||
|
var currentStatus;
|
||||||
|
var syncIntervals = [];
|
||||||
|
var syncIntervalTime = 30000;
|
||||||
|
var seekIntervals = [];
|
||||||
|
var muffcastUrl = "http://localhost:8128";
|
||||||
|
|
||||||
|
browser.storage.local.get("muffcast").then(function(result) {
|
||||||
|
muffcastUrl = result.muffcast && result.muffcast.url || muffcastUrl;
|
||||||
|
syncIntervalTime = result.muffcast && result.muffcast.syncInterval && parseInt(result.muffcast.syncInterval) || syncIntervalTime;
|
||||||
|
})
|
||||||
|
|
||||||
|
var getStatus = function() {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.open("GET", muffcastUrl, true);
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
var response = this.responseText ? JSON.parse(this.responseText) : false;
|
||||||
|
resolve(response);
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: this.status,
|
||||||
|
error: this.statusText,
|
||||||
|
body: this.responseText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhttp.setRequestHeader("Content-type", "application/json");
|
||||||
|
xhttp.send();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var getPlayer = function(type, index, sleep) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
setTimeout(function() {
|
||||||
|
var player = document.getElementsByTagName(type)[index];
|
||||||
|
if (player) {
|
||||||
|
resolve(player);
|
||||||
|
} else if (sleep < 3000) {
|
||||||
|
return getPlayer(type, index, sleep + 500);
|
||||||
|
} else {
|
||||||
|
reject(player);
|
||||||
|
}
|
||||||
|
}, sleep);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var getTimeString = function(seconds) {
|
||||||
|
var hours = parseInt(seconds / 3600);
|
||||||
|
var minutes = parseInt((seconds % 3600) / 60);
|
||||||
|
var seconds = parseInt(seconds % 60);
|
||||||
|
|
||||||
|
return (hours > 0 ? hours + ":" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addCastLinks = function(type) {
|
||||||
|
// add cast links
|
||||||
|
var elements = document.getElementsByTagName(type);
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
var element = elements[i];
|
||||||
|
var position = element.getBoundingClientRect();
|
||||||
|
var castLink = document.createElement("a");
|
||||||
|
castLink.id = "muffcast-cast-link_" + i;
|
||||||
|
castLink.index = i;
|
||||||
|
castLink.classList.add("muffcast-loader");
|
||||||
|
castLink.style["top"] = position.top + "px";
|
||||||
|
castLink.style["left"] = position.left + "px";
|
||||||
|
castLink.innerHTML = '<i class="fa fa-fw fa-television"></i>';
|
||||||
|
document.body.appendChild(castLink);
|
||||||
|
castLink.addEventListener("click", function(event) {
|
||||||
|
var index = event.target.parentNode.index;
|
||||||
|
var player = document.getElementsByTagName(type)[index];
|
||||||
|
player.pause();
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "load",
|
||||||
|
"url": encodeURIComponent(window.location.href),
|
||||||
|
"type": type,
|
||||||
|
"index": index,
|
||||||
|
"seek": player.currentTime,
|
||||||
|
"volume": player.volume,
|
||||||
|
"muted": player.muted
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var setStatus = function() {
|
||||||
|
for (let seekInterval of seekIntervals) {
|
||||||
|
clearInterval(seekInterval);
|
||||||
|
}
|
||||||
|
for (let syncInterval of syncIntervals) {
|
||||||
|
clearInterval(syncInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
var overlay = document.getElementById("muffcast-overlay");
|
||||||
|
|
||||||
|
if (overlay) {
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// remove all cast links
|
||||||
|
for (let castLink of document.getElementsByClassName("muffcast-loader")) {
|
||||||
|
document.body.removeChild(castLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCastLinks("video");
|
||||||
|
addCastLinks("audio");
|
||||||
|
|
||||||
|
getStatus().then(function(status) {
|
||||||
|
currentStatus = status;
|
||||||
|
if (currentStatus.url && currentStatus.url == encodeURIComponent(window.location.href)) {
|
||||||
|
getPlayer(currentStatus.type, currentStatus.index, 0).then(function(player) {
|
||||||
|
player.muted = currentStatus.muted;
|
||||||
|
player.currentTime = currentStatus.currentTime;
|
||||||
|
|
||||||
|
player.addEventListener("canplaythrough", function() {
|
||||||
|
if (currentStatus.playing && currentStatus.url == encodeURIComponent(window.location.href)) {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
overlay = document.createElement("div");
|
||||||
|
overlay.id = "muffcast-overlay";
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
var play = document.createElement("a");
|
||||||
|
play.id = "muffcast-play";
|
||||||
|
play.innerHTML = currentStatus.playing ? '<i class="fa fa-fw fa-pause"></i>' : '<i class="fa fa-fw fa-play"></i>';;
|
||||||
|
|
||||||
|
play.addEventListener("click", function(event) {
|
||||||
|
if (currentStatus.playing) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "pause",
|
||||||
|
"seek": player.currentTime
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "play",
|
||||||
|
"seek": player.currentTime
|
||||||
|
});
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
currentStatus.playing = !currentStatus.playing;
|
||||||
|
play.innerHTML = currentStatus.playing ? '<i class="fa fa-fw fa-pause"></i>' : '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
})
|
||||||
|
|
||||||
|
var stop = document.createElement("a");
|
||||||
|
stop.id = "muffcast-stop";
|
||||||
|
stop.innerHTML = '<i class="fa fa-fw fa-stop"></i>';
|
||||||
|
stop.addEventListener("click", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "stop"
|
||||||
|
});
|
||||||
|
setStatus();
|
||||||
|
})
|
||||||
|
|
||||||
|
var duration = document.createElement("span");
|
||||||
|
duration.id = "muffcast-duration";
|
||||||
|
duration.classList.add("time");
|
||||||
|
duration.textContent = getTimeString(player.duration);
|
||||||
|
|
||||||
|
var currentTime = document.createElement("span");
|
||||||
|
currentTime.id = "muffcast-currenttime";
|
||||||
|
currentTime.classList.add("time");
|
||||||
|
currentTime.textContent = getTimeString(player.currentTime);
|
||||||
|
|
||||||
|
var seek = document.createElement("input");
|
||||||
|
seek.id = "muffcast-seek";
|
||||||
|
seek.setAttribute("type", "range");
|
||||||
|
seek.setAttribute("min", 0);
|
||||||
|
seek.setAttribute("max", currentStatus.duration);
|
||||||
|
seek.setAttribute("value", currentStatus.currentTime);
|
||||||
|
|
||||||
|
seek.addEventListener("change", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "seek",
|
||||||
|
"seek": seek.value
|
||||||
|
});
|
||||||
|
player.currentTime = seek.value;
|
||||||
|
currentTime.textContent = getTimeString(player.currentTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
seekIntervals.push(setInterval(function() {
|
||||||
|
if (seek.value < currentStatus.duration) {
|
||||||
|
if (!player.paused || currentStatus.playing) {
|
||||||
|
seek.value++;
|
||||||
|
currentTime.textContent = getTimeString(seek.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seek.value = 0;
|
||||||
|
clearInterval(seekInterval);
|
||||||
|
}
|
||||||
|
}, 1000));
|
||||||
|
|
||||||
|
var audio = document.createElement("div");
|
||||||
|
audio.id = "muffcast-audio";
|
||||||
|
|
||||||
|
var volume = document.createElement("input");
|
||||||
|
volume.id = "muffcast-volume";
|
||||||
|
volume.setAttribute("type", "range");
|
||||||
|
volume.setAttribute("min", 0);
|
||||||
|
volume.setAttribute("max", 1);
|
||||||
|
volume.setAttribute("step", 0.01);
|
||||||
|
volume.setAttribute("value", status.muted ? 0 : status.volume);
|
||||||
|
|
||||||
|
volume.addEventListener("change", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "volume",
|
||||||
|
"volume": volume.value
|
||||||
|
});
|
||||||
|
player.volume = volume.value;
|
||||||
|
player.muted = volume.value == 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var mute = document.createElement("a");
|
||||||
|
mute.id = "muffcast-mute";
|
||||||
|
mute.innerHTML = status.muted ? '<i class="fa fa-fw fa-volume-off"></i>' : (player.volume < 0.5 ? '<i class="fa fa-fw fa-volume-down"></i>' : '<i class="fa fa-fw fa-volume-up"></i>');
|
||||||
|
mute.addEventListener("click", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "mute",
|
||||||
|
"muted": !player.muted
|
||||||
|
});
|
||||||
|
player.muted = !player.muted;
|
||||||
|
volume.value = player.muted ? 0 : player.volume;
|
||||||
|
mute.innerHTML = player.muted ? '<i class="fa fa-fw fa-volume-off"></i>' : (player.volume < 0.5 ? '<i class="fa fa-fw fa-volume-down"></i>' : '<i class="fa fa-fw fa-volume-up"></i>');
|
||||||
|
})
|
||||||
|
|
||||||
|
var icon = document.createElement("span");
|
||||||
|
icon.id = "muffcast-icon";
|
||||||
|
icon.innerHTML = '<i class="fa fa-television"></i>';
|
||||||
|
|
||||||
|
overlay.appendChild(icon);
|
||||||
|
overlay.appendChild(play);
|
||||||
|
audio.appendChild(mute);
|
||||||
|
audio.appendChild(volume);
|
||||||
|
overlay.appendChild(audio);
|
||||||
|
overlay.appendChild(currentTime);
|
||||||
|
overlay.appendChild(seek);
|
||||||
|
overlay.appendChild(duration);
|
||||||
|
overlay.appendChild(stop);
|
||||||
|
|
||||||
|
var castLink = document.getElementById("muffcast-cast-link_" + status.index);
|
||||||
|
castLink.classList.add("active");
|
||||||
|
|
||||||
|
syncIntervals.push(setInterval(function() {
|
||||||
|
// sync status
|
||||||
|
if (!player.isSyncInterval) {
|
||||||
|
getStatus().then(function(status) {
|
||||||
|
player.isSyncInterval = true;
|
||||||
|
|
||||||
|
currentStatus.playing = status.playing;
|
||||||
|
currentStatus.currentTime = status.currentTime;
|
||||||
|
currentStatus.volume = status.volume;
|
||||||
|
currentStatus.muted = status.muted;
|
||||||
|
|
||||||
|
currentTime.textContent = getTimeString(currentStatus.currentTime);
|
||||||
|
seek.value = currentStatus.currentTime
|
||||||
|
play.innerHTML = currentStatus.playing ? '<i class="fa fa-fw fa-pause"></i>' : '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
volume.value = currentStatus.muted ? 0 : currentStatus.volume;
|
||||||
|
mute.innerHTML = currentStatus.muted ? '<i class="fa fa-fw fa-volume-off"></i>' : (currentStatus.volume < 0.5 ? '<i class="fa fa-fw fa-volume-down"></i>' : '<i class="fa fa-fw fa-volume-up"></i>');
|
||||||
|
|
||||||
|
player.volume = currentStatus.volume;
|
||||||
|
player.muted = currentStatus.muted;
|
||||||
|
player.currentTime = currentStatus.currentTime;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, syncIntervalTime));
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
player.addEventListener("play", function(event) {
|
||||||
|
if (status.playing && status.url == encodeURIComponent(window.location.href)) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "pause",
|
||||||
|
"seek": player.currentTime
|
||||||
|
});
|
||||||
|
play.innerHTML = '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
status.playing = !status.playing;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.addEventListener("pause", function(event) {
|
||||||
|
if (!status.playing && status.url == encodeURIComponent(window.location.href.href)) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "play",
|
||||||
|
"seek": player.currentTime
|
||||||
|
});
|
||||||
|
play.innerHTML = '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
status.playing = !status.playing;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.addEventListener("seeked", function(event) {
|
||||||
|
if (status.playing && status.url == encodeURIComponent(window.location.href)) {
|
||||||
|
if (!player.isSyncInterval) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "seek",
|
||||||
|
"seek": player.currentTime
|
||||||
|
});
|
||||||
|
seek.value = player.currentTime;
|
||||||
|
currentTime.textContent = getTimeString(player.currentTime);
|
||||||
|
} else {
|
||||||
|
player.isSyncInterval = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.addEventListener("volumechange", function(event) {
|
||||||
|
if (status.url == encodeURIComponent(window.location.href)) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "volume",
|
||||||
|
"volume": player.volume
|
||||||
|
});
|
||||||
|
volume.value = player.muted ? 0 : player.volume;
|
||||||
|
mute.innerHTML = player.muted ? '<i class="fa fa-fw fa-volume-off"></i>' : (player.volume < 0.5 ? '<i class="fa fa-fw fa-volume-down"></i>' : '<i class="fa fa-fw fa-volume-up"></i>');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener(function(message) {
|
||||||
|
var videos = document.getElementsByTagName("video");
|
||||||
|
switch (message.command) {
|
||||||
|
case "update":
|
||||||
|
setStatus();
|
||||||
|
break;
|
||||||
|
case "load":
|
||||||
|
var player = videos[message.index];
|
||||||
|
player.pause();
|
||||||
|
player.style.border = "none";
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "load",
|
||||||
|
"url": encodeURIComponent(window.location.href),
|
||||||
|
"type": "video",
|
||||||
|
"index": message.index,
|
||||||
|
"seek": player.currentTime,
|
||||||
|
"volume": player.volume,
|
||||||
|
"muted": player.muted
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mark":
|
||||||
|
var player = videos[message.index];
|
||||||
|
player.style.border = "5px solid red";
|
||||||
|
break;
|
||||||
|
case "unmark":
|
||||||
|
var player = videos[message.index];
|
||||||
|
player.style.border = "none";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
20
options/options.html
Normal file
20
options/options.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label>Muffcast URL<input type="url" id="muffcast-url" /></label> <br />
|
||||||
|
<label>Muffcast Sync Interval<input type="number" id="muffcast-sync-interval" min="1000" step="1000" /></label> <br />
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
19
options/options.js
Normal file
19
options/options.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
function saveOptions(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browser.storage.local.set({
|
||||||
|
"muffcast": {
|
||||||
|
"url": document.querySelector("#muffcast-url").value,
|
||||||
|
"syncInterval": document.querySelector("#muffcast-sync-interval").value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOptions() {
|
||||||
|
browser.storage.local.get("muffcast").then(function(result) {
|
||||||
|
document.querySelector("#muffcast-url").value = result.muffcast && result.muffcast.url || "http://localhost:8128";
|
||||||
|
document.querySelector("#muffcast-sync-interval").value = result.muffcast && result.muffcast.syncInterval || 30000;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||||
|
document.querySelector("form").addEventListener("submit", saveOptions);
|
126
popup/popup.css
Normal file
126
popup/popup.css
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
src: url('" + browser.extension.getURL("fonts/fontawesome.eot") + "?v=4.7.0');
|
||||||
|
src: url('" + browser.extension.getURL("fonts/fontawesome.eot") + "?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome.svg?v=4.7.0#fontawesomeregular') format('svg');
|
||||||
|
font - weight: normal;
|
||||||
|
font - style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'OpenSans', 'Roboto', 'sans-serif', 'sans';
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #d13d29;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #9e2e1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#muffcast {
|
||||||
|
width: 400px;
|
||||||
|
background-color: #fefefe;
|
||||||
|
color: #444;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#idle, #error {
|
||||||
|
width: 100%;
|
||||||
|
display: none;
|
||||||
|
color: #d13d29;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title:hover {}
|
||||||
|
|
||||||
|
#host {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #468ac3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio {
|
||||||
|
display: -moz-flex;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mute {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #468ac3;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume::-moz-range-thumb {
|
||||||
|
box-shadow: 0px 0px 0px #d13d29;
|
||||||
|
border: 0px solid #d13d29;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #d13d29;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume::-moz-range-thumb:hover {
|
||||||
|
color: #9e2e1f;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-menu {
|
||||||
|
display: block;
|
||||||
|
background-color: #efefef;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#play {
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stop {
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
39
popup/popup.html
Normal file
39
popup/popup.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
<link rel="stylesheet" href="../css/font-awesome.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="idle"><i class="icon fa fa-television"></i> Muffcast is ready!</div>
|
||||||
|
<div id="error">
|
||||||
|
<span class="fa-stack fa-lg">
|
||||||
|
<i class="icon fa fa-television fa-stack-1x"></i>
|
||||||
|
<i class="fa fa-ban fa-stack-1x"></i>
|
||||||
|
</span>
|
||||||
|
<span>Muffcast is not reachable!</span>
|
||||||
|
<p class="text-center">Please check settings and server.</p>
|
||||||
|
</div>
|
||||||
|
<div id="muffcast">
|
||||||
|
<div class="info">
|
||||||
|
<a id="title"></a>
|
||||||
|
<span id="host"></span>
|
||||||
|
<div class="audio">
|
||||||
|
<a id="mute"></a>
|
||||||
|
<input id="volume" type="range" min="0" max="1" step="0.01" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-menu">
|
||||||
|
<a id="play"><i class="fa fa-play"></i></a>
|
||||||
|
<a id="stop">Stop Muffcast</a>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
114
popup/popup.js
Normal file
114
popup/popup.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
var muffcastUrl = "http://localhost:8128";
|
||||||
|
browser.storage.local.get("muffcast").then(function(result) {
|
||||||
|
muffcastUrl = result.muffcast && result.muffcast.url || muffcastUrl;
|
||||||
|
})
|
||||||
|
|
||||||
|
var clientUpdate = function() {
|
||||||
|
return browser.tabs.query({
|
||||||
|
currentWindow: true,
|
||||||
|
active: true
|
||||||
|
}).then(function(tabs) {
|
||||||
|
var tab = tabs[0];
|
||||||
|
return browser.tabs.sendMessage(tab.id, {
|
||||||
|
command: "update"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var setStatus = function() {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.open("GET", muffcastUrl, true);
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
var idleElement = document.getElementById("idle");
|
||||||
|
var errorElement = document.getElementById("error");
|
||||||
|
var muffcastElement = document.getElementById("muffcast");
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
var status = this.responseText && JSON.parse(this.responseText);
|
||||||
|
if (status && status.running) {
|
||||||
|
var titleElement = document.getElementById("title");
|
||||||
|
titleElement.textContent = status.title;
|
||||||
|
titleElement.href = decodeURIComponent(status.url);
|
||||||
|
|
||||||
|
var hostElement = document.getElementById("host");
|
||||||
|
hostElement.textContent = decodeURIComponent(status.host);
|
||||||
|
|
||||||
|
var muteElement = document.getElementById("mute");
|
||||||
|
|
||||||
|
var updateMuteElement = function() {
|
||||||
|
muteElement.innerHTML = status.muted ? '<i class="fa fa-fw fa-volume-off"></i>' : (status.volume < 0.5 ? '<i class="fa fa-fw fa-volume-down"></i>' : '<i class="fa fa-fw fa-volume-up"></i>');
|
||||||
|
}
|
||||||
|
|
||||||
|
muteElement.addEventListener("click", function(event) {
|
||||||
|
status.muted = !status.muted;
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "mute",
|
||||||
|
"muted": status.muted
|
||||||
|
});
|
||||||
|
volumeElement.value = status.muted ? 0 : status.volume;
|
||||||
|
updateMuteElement();
|
||||||
|
clientUpdate();
|
||||||
|
})
|
||||||
|
|
||||||
|
updateMuteElement();
|
||||||
|
|
||||||
|
var volumeElement = document.getElementById("volume");
|
||||||
|
volumeElement.setAttribute("value", status.muted ? 0 : status.volume);
|
||||||
|
|
||||||
|
volumeElement.addEventListener("change", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "volume",
|
||||||
|
"volume": volumeElement.value
|
||||||
|
});
|
||||||
|
status.volume = volumeElement.value;
|
||||||
|
status.muted = volumeElement.value == 0;
|
||||||
|
updateMuteElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
var playElement = document.getElementById("play");
|
||||||
|
|
||||||
|
playElement.innerHTML = status.playing ? '<i class="fa fa-fw fa-pause"></i>' : '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
|
||||||
|
playElement.addEventListener("click", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": status.playing ? "pause" : "play"
|
||||||
|
});
|
||||||
|
status.playing = !status.playing;
|
||||||
|
playElement.innerHTML = status.playing ? '<i class="fa fa-fw fa-pause"></i>' : '<i class="fa fa-fw fa-play"></i>';
|
||||||
|
|
||||||
|
clientUpdate();
|
||||||
|
})
|
||||||
|
|
||||||
|
var stopElement = document.getElementById("stop");
|
||||||
|
|
||||||
|
stopElement.addEventListener("click", function(event) {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
"command": "stop"
|
||||||
|
});
|
||||||
|
clientUpdate();
|
||||||
|
muffcastElement.classList.remove("visible");
|
||||||
|
idleElement.classList.add("visible");
|
||||||
|
errorElement.classList.remove("visible");
|
||||||
|
})
|
||||||
|
|
||||||
|
muffcastElement.classList.add("visible");
|
||||||
|
idleElement.classList.remove("visible");
|
||||||
|
errorElement.classList.remove("visible");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
muffcastElement.classList.remove("visible");
|
||||||
|
idleElement.classList.add("visible");
|
||||||
|
errorElement.classList.remove("visible");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
muffcastElement.classList.remove("visible");
|
||||||
|
idleElement.classList.remove("visible");
|
||||||
|
errorElement.classList.add("visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhttp.setRequestHeader("Content-type", "application/json");
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus();
|
Reference in New Issue
Block a user