Skip to content

Commit

Permalink
[v1.1.0] Re-imagined the concept, and got a prototype ready
Browse files Browse the repository at this point in the history
Changelog:
- Completely changed the way the extension is supposed to work
- Managed to get the background script & content script interacting and working as required
- Implemented a regex URL matching system working for AmazonPrimeVideo & Netflix
- Implemented a working playback controller (injected to the DOM [only Netflix, for now])
  PrimeVideo is next, differentiating between playback screen and details screen is currently not possible.
  So I had to devote my attention to Netflix. Next up - APV, Hotstar... et al
- Stylesheets (user and FontAwesome) injected to the DOM.

Took about 8 hours of my Sunday!
  • Loading branch information
BRAiNCHiLD95 committed Nov 10, 2019
1 parent 8d0762c commit a4b2c6a
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 70 deletions.
77 changes: 50 additions & 27 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
78 changes: 78 additions & 0 deletions content.css
Original file line number Diff line number Diff line change
@@ -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;
}
178 changes: 143 additions & 35 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div class="playbackSpeeds">
<h3 class="playback-header">Playback Rate</h3>
<div class="range-stuff">
<input id="uvpc-value" type="range" name="speed" data-thumbwidth="20" step="0.25" min="0" max="4">
<output name="rangeVal">1</output>
</div>
</div>`;
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 = '<i class="fa fa-cogs fa-4x"></fa>';
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) => {
Expand All @@ -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);
};
12 changes: 4 additions & 8 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -22,13 +22,9 @@
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
"js": ["content.js"],
"css": ["content.css"]
}
],
"permissions": [
"tabs",
"activeTab",
"webNavigation",
"declarativeContent"
]
"permissions": ["tabs"]
}

0 comments on commit a4b2c6a

Please sign in to comment.