diff --git a/background.js b/background.js index bf2d652..beb2a5e 100644 --- a/background.js +++ b/background.js @@ -8,42 +8,65 @@ * channel to communicate between scripts */ const getMessenger = contentMessenger => { - let currentTab = contentMessenger.sender.tab; - chrome.webNavigation.onHistoryStateUpdated.addListener( details => { - if (contentMessenger.name === "uvpc-b") { - sendToContentScript(contentMessenger, { listeningTo: currentTab.id, title: currentTab.title }); - } else { - chrome.runtime.connect({ name: "uvpc-b" }); - } - contentMessenger.onDisconnect.addListener(() => { - console.info("disconnected from contentMessenger"); - }); - contentMessenger.onMessage.addListener((message, sender) => { + if ((contentMessenger.name = "uvpc-b")) { + const checkForOTTs = (tabId, changeInfo, tab) => { + if (changeInfo.url || changeInfo.audible || changeInfo.status === "complete") { + let currentUrl = changeInfo.url || tab.url; + if (currentUrl && tab.status === "complete") { + if (currentUrl.match(netflixRegex)) { + sendToContentScript(contentMessenger, { + domain: "netflix", + audible: tab.audible, + }, tabId); + return; + } + if (currentUrl.match(apvRegex)) { + sendToContentScript(contentMessenger, { + domain: "primevideo", + audible: tab.audible, + }, tabId); + return; + } + } + } + }; + contentMessenger.onMessage.addListener(message => { + console.log(message); + if (message.initiated) { + console.log(`connected to ${contentMessenger.sender.tab.title}`); + chrome.tabs.onCreated.addListener(checkForOTTs); + chrome.tabs.onUpdated.addListener(checkForOTTs); + } if (message.badgeText) { chrome.browserAction.setBadgeText({ text: message.badgeText + "x", - tabId: sender.sender.tab.id, + tabId: contentMessenger.sender.tab.id, }); chrome.browserAction.setBadgeBackgroundColor({ color: "#000000", - tabId: sender.sender.tab.id, - }); - contentMessenger.postMessage({ - newSpeed: 1.5, + tabId: contentMessenger.sender.tab.id, }); } }); - }) + contentMessenger.onDisconnect.addListener(() => { + console.info("disconnected from backgroundjs"); + chrome.tabs.onCreated.removeListener(checkForOTTs); + chrome.tabs.onUpdated.removeListener(checkForOTTs); + contentMessenger = null; + }); + } +}; + +const sendToContentScript = (port, message, tabId) => { + if (port) { + console.log(`SENDING ${JSON.stringify(message, null, 2)} to Tab #${tabId}`); + chrome.tabs.sendMessage(tabId, message); + } else { + console.log("PORT GONE!"); + } }; -const sendToContentScript = (port, message) => { - port.postMessage(message); -} +const netflixRegex = /(http(s)?:\/\/.)?(www\.)?(netflix.com\/watch)/; +const apvRegex = /(http(s)?:\/\/.)?(www\.)?(primevideo.com\/detail)/; -chrome.runtime.onStartup.addListener(() => { - chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (tab.status === 'complete') { - chrome.runtime.onConnect.addListener(getMessenger); - } - }) -}); +chrome.runtime.onConnect.addListener(getMessenger); diff --git a/content.css b/content.css new file mode 100644 index 0000000..435716f --- /dev/null +++ b/content.css @@ -0,0 +1,78 @@ +div#playbackController div#uvpc-div { + opacity: 0; + background: #282828 !important; + height: 100px !important; + width: 200px !important; + padding: 5px !important; + position: absolute !important; + bottom: calc(100vh - 90vh) !important; + right: calc(100vw - 95vw) !important; + border-radius: 5px !important; + direction: ltr !important; + text-align: center; +} + +div#playbackController div#uvpc-div div.playbackSpeeds h3.playback-header { + margin: 5px auto !important; + font-size: 2em; +} + +div#playbackController div#uvpc-div.active { + opacity: 1; + -o-transition: opacity 160ms ease; + -moz-transition: opacity 160ms ease; + transition: opacity 160ms ease; + will-change: opacity; +} + +div#playbackController div#uvpc-div.active div.playbackSpeeds div.range-stuff { + margin: 50px 15px 0px 15px !important; +} + + +div#playbackController div#uvpc-div.active div.playbackSpeeds input[type=range] { + display: block; + width: 100%; + margin: 0; + -webkit-appearance: none; + outline: none; +} + +div#playbackController div#uvpc-div.active div.playbackSpeeds input[type=range]::-webkit-slider-runnable-track { + position: relative; + height: 12px; + border: 1px solid #b2b2b2; + border-radius: 5px; + background-color: #e2e2e2; + box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.1); +} + +div#playbackController div#uvpc-div.active div.playbackSpeeds input[type=range]::-webkit-slider-thumb { + position: relative; + top: -5px; + width: 20px; + height: 20px; + border: 1px solid #999; + -webkit-appearance: none; + background-color: #fff; + box-shadow: inset 0 -1px 2px 0 rgba(0, 0, 0, 0.25); + border-radius: 100%; + cursor: pointer; +} + +div#playbackController div#uvpc-div.active div.playbackSpeeds output { + position: absolute; + top: 40px !important; + right: 80px !important; + display: block; + font-weight: bold; + width: 50px; + height: 24px; + border: 1px solid #e2e2e2; + background-color: #fff; + border-radius: 3px; + color: #777; + font-size: 1.8em; + line-height: 24px; + text-align: center; +} \ No newline at end of file diff --git a/content.js b/content.js index 43e880a..7e55385 100644 --- a/content.js +++ b/content.js @@ -4,10 +4,147 @@ */ /** - * Waits for video element to be loaded, then resolves promise with the element. - * @returns {Promise} + * Creates a communication channel + * between background.js & content.js + */ +const connectToServices = () => { + const initContent = chrome.runtime.connect({ name: "uvpc-b" }); + console.log("connecting from contentjs", initContent); + initContent.postMessage({ initiated: true }); + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + deployer(msg, initContent); + }); +}; + +window.addEventListener("load", connectToServices); + +/** + * Based on the domain passed, deploy the controller element onto DOM + * @param {string} msg + */ +const deployer = (msg, port) => { + switch (msg.domain) { + case "netflix": + return deployNetflix(port); + case "primevideo": + console.log("APV Deploy!"); + // return deployAPV(port); + default: + console.error("Don't really need this!"); + } +}; + +const deployNetflix = async port => { + var videoElement = await fetchVideoElement(); + if (!videoElement) return false; + updateBadgeText(port, videoElement); + var injectedFa = await injectStyles(); + if (!injectedFa.success) return false; + var injectedController = await injectControllerToNetflix(); + var controllers = document.querySelectorAll("#playbackController"); + if (controllers.length > 1) { + controllers.forEach((element, index) => { + if (index > 0) element.remove(); + }); + } + if (!injectedController.success) return false; + document + .getElementById("uvpc-btn") + .addEventListener("click", playBackToggler); + document + .getElementById("uvpc-value") + .addEventListener("input", event => + valueChanged(port, event, videoElement) + ); + document.getElementById("uvpc-value").value = videoElement.playbackRate; +}; + +/** + * Builds the icon for the cog controller */ +const injectControllerToNetflix = () => { + // OTT specific + let playerDiv = document.querySelector( + ".PlayerControlsNeo__button-control-row" + ); + if (!document.contains(playerDiv)) return false; + // cog + return new Promise((resolve, reject) => { + let main = document.createElement("div"); + main.id = "playbackController"; + let videoControllerDiv = document.createElement("div"); + videoControllerDiv.id = "uvpc-div"; + videoControllerDiv.innerHTML = `
+

Playback Rate

+
+ + 1 +
+
`; + let videoControllerBtn = document.createElement("button"); + videoControllerBtn.id = "uvpc-btn"; + videoControllerBtn.style.background = "none"; + videoControllerBtn.style.border = "none"; + videoControllerBtn.style.fontSize = "inherit"; + videoControllerBtn.style.marginBottom = "8px"; + videoControllerBtn.innerHTML = ''; + main.innerHTML = + videoControllerDiv.outerHTML + videoControllerBtn.outerHTML; + playerDiv.insertBefore(main, playerDiv.lastChild); + if (!playerDiv.contains(main)) + reject("Failed to inject the controller"); + resolve({ success: true, element: main }); + }); +}; + +const playerDivs = { + netflix: ".PlayerControlsNeo__button-control-row", +}; +const playBackToggler = event => { + console.log("Event fired!", event); + document.getElementById("uvpc-div").classList.toggle("active"); +}; + +const valueChanged = (port, event, videoElement) => { + console.log(event); + var controlMin = Number(event.target.min), + controlMax = Number(event.target.max), + controlVal = Number(event.target.value), + range = controlMax - controlMin, + output = event.target.nextElementSibling; + position = ((controlVal - controlMin) / range) * 100; + output.innerHTML = controlVal + "x"; + videoElement.playbackRate = event.target.valueAsNumber; + updateBadgeText(port, videoElement); +}; + +/** + * Builds and injects Stylesheets for font awesome 4.7.0 + * and the playback controller to the page + */ +const injectStyles = () => { + return new Promise((resolve, reject) => { + let headOfDoc = document.head; + let faLink = document.createElement("link"); + faLink.id = "fa-uvpc"; + faLink.href = + "https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"; + faLink.rel = "stylesheet"; + faLink.integrity = + "sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"; + faLink.crossOrigin = "anonymous"; + headOfDoc.appendChild(faLink); + if (!headOfDoc.contains(faLink)) + reject("Failed to inject styles to the Page"); + resolve({ success: true, element: faLink }); + }); +}; + +/** + * Waits for video element to be loaded, then resolves promise with the element. + * @returns {HTMLVideoElement} + */ const fetchVideoElement = () => { return new Promise((resolve, reject) => { new MutationObserver((mutationRecords, observer) => { @@ -23,42 +160,13 @@ const fetchVideoElement = () => { }; /** - * Makes an Async/Await call to fetchVideoElement + * Updates the BadgeText for the Extension + * @param {Port} port + * @param {HTMLVideoElement} video */ - -const findVideo = async () => { - return await fetchVideoElement(); -}; - -/** - * Creates a communication channel - * between background.js & content.js - */ -const connectToServices = () => { - const backgroundMessenger = chrome.runtime.connect({ name: "uvpc-b" }); - backgroundMessenger.onMessage.addListener(message => { - findVideo().then( - video => { - if (message.listeningTo) { - updateBadgeText(backgroundMessenger, video); - } - if (message.newSpeed) { - video.playbackRate = message.newSpeed; - updateBadgeText(backgroundMessenger, video); - } - }, - error => { - console.info("No video found yet\n", error); - backgroundMessenger.disconnect(); - }); - }); -}; - const updateBadgeText = (port, video) => { port.postMessage({ badgeText: video.playbackRate, video, }); -} - -window.addEventListener("load", connectToServices); +}; diff --git a/manifest.json b/manifest.json index bf05a99..b7c062e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Ultimate Video Playback Controller", "description": "An Ultimate Controller for all your HTML5 Video Playback Speed Needs", - "version": "1.0.0", + "version": "1.1.0", "manifest_version": 2, "icons": { "128": "images/icon_128.png" @@ -22,13 +22,9 @@ "content_scripts": [ { "matches": [""], - "js": ["content.js"] + "js": ["content.js"], + "css": ["content.css"] } ], - "permissions": [ - "tabs", - "activeTab", - "webNavigation", - "declarativeContent" - ] + "permissions": ["tabs"] }