From 71c11ddeb8a4b937db4473301ff1efa164eef0a2 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sat, 9 Nov 2019 11:34:08 -0600 Subject: [PATCH 01/40] initial commit for apple tv plus bugfix. --- inject.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/inject.js b/inject.js index a09e49e..e0ee74a 100644 --- a/inject.js +++ b/inject.js @@ -433,7 +433,7 @@ } else { var mediaTags = document.querySelectorAll('video'); } - + forEach.call(mediaTags, function(video) { video.vsc = new tc.videoController(video); }); @@ -444,6 +444,11 @@ try { var childDocument = frame.contentDocument } catch (e) { return } initializeWhenReady(childDocument); }); + + //look for video in shadowRoot + if (document.querySelector('apple-tv-plus-player')) { + console.log('Congratulations. There is the apple-tv-plus-player.') + } } function runAction(action, document, value, e) { @@ -458,13 +463,13 @@ // Get the controller that was used if called from a button press event e if (e) { var targetController = e.target.getRootNode().host; - } + } mediaTags.forEach(function(v) { var id = v.dataset['vscid']; var controller = document.querySelector(`div[data-vscid="${id}"]`); - // Don't change video speed if the video has a different controller + // Don't change video speed if the video has a different controller if (e && !(targetController == controller)) { return; } @@ -543,13 +548,13 @@ function setMark(v) { v.vsc.mark = v.currentTime; } - + function jumpToMark(v) { if (v.vsc.mark && typeof v.vsc.mark === "number") { v.currentTime = v.vsc.mark; } } - + function handleDrag(video, controller, e) { const shadowController = controller.shadowRoot.querySelector('#controller'); From f5280b44afdaf9ba07fa369e456634b82a5a289c Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sat, 9 Nov 2019 12:38:18 -0600 Subject: [PATCH 02/40] imported shadowMutations from ally.js --- inject.js | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/inject.js b/inject.js index e0ee74a..aed1d6b 100644 --- a/inject.js +++ b/inject.js @@ -449,6 +449,215 @@ if (document.querySelector('apple-tv-plus-player')) { console.log('Congratulations. There is the apple-tv-plus-player.') } + + // start of ally.js/src/observe/shadow-mutations.js + // import nodeArray from '../util/node-array'; + // input may be undefined, selector-tring, Node, NodeList, HTMLCollection, array of Nodes + // yes, to some extent this is a bad replica of jQuery's constructor function + function nodeArray(input) { + if (!input) { + return []; + } + + if (Array.isArray(input)) { + return input; + } + + // instanceof Node - does not work with iframes + if (input.nodeType !== undefined) { + return [input]; + } + + if (typeof input === 'string') { + input = document.querySelectorAll(input); + } + + if (input.length !== undefined) { + return [].slice.call(input, 0); + } + + throw new TypeError('unexpected input ' + String(input)); + } + //import queryShadowHosts from '../query/shadow-hosts'; + //import contextToElement from '../util/context-to-element'; + //import nodeArray from '../util/node-array'; already imported + + function contextToElement({ + context, + label = 'context-to-element', + resolveDocument, + defaultToDocument, + }) { + let element = nodeArray(context)[0]; + + if (resolveDocument && element && element.nodeType === Node.DOCUMENT_NODE) { + element = element.documentElement; + } + + if (!element && defaultToDocument) { + return document.documentElement; + } + + if (!element) { + throw new TypeError(label + ' requires valid options.context'); + } + + if (element.nodeType !== Node.ELEMENT_NODE && element.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { + throw new TypeError(label + ' requires options.context to be an Element'); + } + + return element; + } + //import getDocument from '../util/get-document'; + function getDocument(node) { + if (!node) { + return document; + } + + if (node.nodeType === Node.DOCUMENT_NODE) { + return node; + } + + return node.ownerDocument || document; + } + + // see https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter + const filter = function(node) { + if (node.shadowRoot) { + return NodeFilter.FILTER_ACCEPT; + } + + return NodeFilter.FILTER_SKIP; + }; + // IE requires a function, Browsers require {acceptNode: function} + // see http://www.bennadel.com/blog/2607-finding-html-comment-nodes-in-the-dom-using-treewalker.htm + filter.acceptNode = filter; + + function queryShadowHosts({ context } = {}) { + const element = contextToElement({ + label: 'query/shadow-hosts', + resolveDocument: true, + defaultToDocument: true, + context, + }); + + const _document = getDocument(context); + // see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker + const walker = _document.createTreeWalker( + // root element to start search in + element, + // element type filter + NodeFilter.SHOW_ELEMENT, + // custom NodeFilter filter + filter, + // deprecated, but IE requires it + false + ); + + let list = []; + + if (element.shadowRoot) { + // TreeWalker does not run the filter on the context element + list.push(element); + list = list.concat(queryShadowHosts({ + context: element.shadowRoot, + })); + } + + while (walker.nextNode()) { + list.push(walker.currentNode); + list = list.concat(queryShadowHosts({ + context: walker.currentNode.shadowRoot, + })); + } + + return list; + } + //import contextToElement from '../util/context-to-element'; already imported + + const shadowObserverConfig = { + childList: true, + subtree: true, + }; + + class ShadowMutationObserver { + constructor({context, callback, config} = {}) { + this.config = config; + + this.disengage = this.disengage.bind(this); + + this.clientObserver = new MutationObserver(callback); + this.hostObserver = new MutationObserver(mutations => mutations.forEach(this.handleHostMutation, this)); + + this.observeContext(context); + this.observeShadowHosts(context); + } + + disengage() { + this.clientObserver && this.clientObserver.disconnect(); + this.clientObserver = null; + this.hostObserver && this.hostObserver.disconnect(); + this.hostObserver = null; + } + + observeShadowHosts(context) { + const hosts = queryShadowHosts({ + context, + }); + + hosts.forEach(element => this.observeContext(element.shadowRoot)); + } + + observeContext(context) { + this.clientObserver.observe(context, this.config); + this.hostObserver.observe(context, shadowObserverConfig); + } + + handleHostMutation(mutation) { + if (mutation.type !== 'childList') { + return; + } + + const addedElements = nodeArray(mutation.addedNodes).filter(element => element.nodeType === Node.ELEMENT_NODE); + addedElements.forEach(this.observeShadowHosts, this); + } + } + + function shadowMutations(context, callback, config) { + if (typeof callback !== 'function') { + throw new TypeError('observe/shadow-mutations requires options.callback to be a function'); + } + + if (typeof config !== 'object') { + throw new TypeError('observe/shadow-mutations requires options.config to be an object'); + } + + if (!window.MutationObserver) { + // not supporting IE10 via Mutation Events, because they're too expensive + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events + return { + disengage: function() {}, + }; + } + + const element = contextToElement({ + label: 'observe/shadow-mutations', + resolveDocument: true, + defaultToDocument: true, + context, + }); + + const service = new ShadowMutationObserver({ + context: element, + callback, + config, + }); + + return { + disengage: service.disengage, + }; + } + //end of ally.js/src/observe/shadow-mutations.js } function runAction(action, document, value, e) { From da96a33c95b37bf7f98f517e6667e2f0feda379e Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sat, 9 Nov 2019 13:06:26 -0600 Subject: [PATCH 03/40] change the input for shadowMutations function --- inject.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/inject.js b/inject.js index aed1d6b..311c7f1 100644 --- a/inject.js +++ b/inject.js @@ -409,7 +409,7 @@ } } - var observer = new MutationObserver(function(mutations) { + function mutationCallback(mutations) { // Process the DOM nodes lazily requestIdleCallback(_ => { mutations.forEach(function(mutation) { @@ -425,7 +425,8 @@ }); }); }, {timeout: 1000}); - }); + } + var observer = new MutationObserver(mutationCallback); observer.observe(document, { childList: true, subtree: true }); if (tc.settings.audioBoolean) { @@ -445,9 +446,9 @@ initializeWhenReady(childDocument); }); - //look for video in shadowRoot + //look for video in shadowRoot for apple tv if (document.querySelector('apple-tv-plus-player')) { - console.log('Congratulations. There is the apple-tv-plus-player.') + shadowMutations('apple-tv-plus-player', mutationCallback, {childList: true, subtree: true}) } // start of ally.js/src/observe/shadow-mutations.js From 349279013d9b519b72d60f4e7e4aa3b995dfe1de Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sat, 9 Nov 2019 14:03:24 -0600 Subject: [PATCH 04/40] fix ReferenceError --- inject.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/inject.js b/inject.js index 311c7f1..3f64587 100644 --- a/inject.js +++ b/inject.js @@ -446,11 +446,6 @@ initializeWhenReady(childDocument); }); - //look for video in shadowRoot for apple tv - if (document.querySelector('apple-tv-plus-player')) { - shadowMutations('apple-tv-plus-player', mutationCallback, {childList: true, subtree: true}) - } - // start of ally.js/src/observe/shadow-mutations.js // import nodeArray from '../util/node-array'; // input may be undefined, selector-tring, Node, NodeList, HTMLCollection, array of Nodes @@ -659,6 +654,12 @@ }; } //end of ally.js/src/observe/shadow-mutations.js + + //look for video in shadowRoot for apple tv + if (document.querySelector('apple-tv-plus-player')) { + shadowMutations('apple-tv-plus-player', mutationCallback, {childList: true, subtree: true}) + } + } function runAction(action, document, value, e) { From 764ecca262669df5e3f30ba7d37b6ec43ba3d025 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sun, 10 Nov 2019 13:07:42 -0600 Subject: [PATCH 05/40] completely change the logic. keydown events and controls don't work --- inject.js | 235 ++++++------------------------------------------------ 1 file changed, 24 insertions(+), 211 deletions(-) diff --git a/inject.js b/inject.js index 3f64587..8e4ee3d 100644 --- a/inject.js +++ b/inject.js @@ -446,218 +446,31 @@ initializeWhenReady(childDocument); }); - // start of ally.js/src/observe/shadow-mutations.js - // import nodeArray from '../util/node-array'; - // input may be undefined, selector-tring, Node, NodeList, HTMLCollection, array of Nodes - // yes, to some extent this is a bad replica of jQuery's constructor function - function nodeArray(input) { - if (!input) { - return []; - } - - if (Array.isArray(input)) { - return input; - } - - // instanceof Node - does not work with iframes - if (input.nodeType !== undefined) { - return [input]; - } - - if (typeof input === 'string') { - input = document.querySelectorAll(input); - } - - if (input.length !== undefined) { - return [].slice.call(input, 0); - } - - throw new TypeError('unexpected input ' + String(input)); - } - //import queryShadowHosts from '../query/shadow-hosts'; - //import contextToElement from '../util/context-to-element'; - //import nodeArray from '../util/node-array'; already imported - - function contextToElement({ - context, - label = 'context-to-element', - resolveDocument, - defaultToDocument, - }) { - let element = nodeArray(context)[0]; - - if (resolveDocument && element && element.nodeType === Node.DOCUMENT_NODE) { - element = element.documentElement; - } - - if (!element && defaultToDocument) { - return document.documentElement; - } - - if (!element) { - throw new TypeError(label + ' requires valid options.context'); - } - - if (element.nodeType !== Node.ELEMENT_NODE && element.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { - throw new TypeError(label + ' requires options.context to be an Element'); - } - - return element; - } - //import getDocument from '../util/get-document'; - function getDocument(node) { - if (!node) { - return document; - } - - if (node.nodeType === Node.DOCUMENT_NODE) { - return node; - } - - return node.ownerDocument || document; - } - - // see https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter - const filter = function(node) { - if (node.shadowRoot) { - return NodeFilter.FILTER_ACCEPT; - } - - return NodeFilter.FILTER_SKIP; - }; - // IE requires a function, Browsers require {acceptNode: function} - // see http://www.bennadel.com/blog/2607-finding-html-comment-nodes-in-the-dom-using-treewalker.htm - filter.acceptNode = filter; - - function queryShadowHosts({ context } = {}) { - const element = contextToElement({ - label: 'query/shadow-hosts', - resolveDocument: true, - defaultToDocument: true, - context, - }); - - const _document = getDocument(context); - // see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker - const walker = _document.createTreeWalker( - // root element to start search in - element, - // element type filter - NodeFilter.SHOW_ELEMENT, - // custom NodeFilter filter - filter, - // deprecated, but IE requires it - false - ); - - let list = []; - - if (element.shadowRoot) { - // TreeWalker does not run the filter on the context element - list.push(element); - list = list.concat(queryShadowHosts({ - context: element.shadowRoot, - })); - } - - while (walker.nextNode()) { - list.push(walker.currentNode); - list = list.concat(queryShadowHosts({ - context: walker.currentNode.shadowRoot, - })); - } - - return list; - } - //import contextToElement from '../util/context-to-element'; already imported - - const shadowObserverConfig = { - childList: true, - subtree: true, - }; - - class ShadowMutationObserver { - constructor({context, callback, config} = {}) { - this.config = config; - - this.disengage = this.disengage.bind(this); - - this.clientObserver = new MutationObserver(callback); - this.hostObserver = new MutationObserver(mutations => mutations.forEach(this.handleHostMutation, this)); - - this.observeContext(context); - this.observeShadowHosts(context); - } - - disengage() { - this.clientObserver && this.clientObserver.disconnect(); - this.clientObserver = null; - this.hostObserver && this.hostObserver.disconnect(); - this.hostObserver = null; - } - - observeShadowHosts(context) { - const hosts = queryShadowHosts({ - context, - }); - - hosts.forEach(element => this.observeContext(element.shadowRoot)); - } - - observeContext(context) { - this.clientObserver.observe(context, this.config); - this.hostObserver.observe(context, shadowObserverConfig); - } - - handleHostMutation(mutation) { - if (mutation.type !== 'childList') { - return; - } - - const addedElements = nodeArray(mutation.addedNodes).filter(element => element.nodeType === Node.ELEMENT_NODE); - addedElements.forEach(this.observeShadowHosts, this); - } - } - - function shadowMutations(context, callback, config) { - if (typeof callback !== 'function') { - throw new TypeError('observe/shadow-mutations requires options.callback to be a function'); - } - - if (typeof config !== 'object') { - throw new TypeError('observe/shadow-mutations requires options.config to be an object'); - } - - if (!window.MutationObserver) { - // not supporting IE10 via Mutation Events, because they're too expensive - // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events - return { - disengage: function() {}, - }; - } - - const element = contextToElement({ - label: 'observe/shadow-mutations', - resolveDocument: true, - defaultToDocument: true, - context, - }); - - const service = new ShadowMutationObserver({ - context: element, - callback, - config, - }); - - return { - disengage: service.disengage, - }; - } - //end of ally.js/src/observe/shadow-mutations.js - //look for video in shadowRoot for apple tv - if (document.querySelector('apple-tv-plus-player')) { - shadowMutations('apple-tv-plus-player', mutationCallback, {childList: true, subtree: true}) + function deepActiveElement() { + let a = document.activeElement; + while (a && a.shadowRoot && a.shadowRoot.activeElement) { + a = a.shadowRoot.activeElement; + } + return a; + } + var apple_tv = document.querySelector('apple-tv-plus-player') + if (apple_tv) { + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.attributeName == 'aria-hidden' && (apple_tv.getAttribute('aria-hidden') == 'false')) { + setTimeout(() => { + var node = deepActiveElement() + checkForVideo(node, node.parentNode || mutation.target, true); + }, 2000) + } + }); + }); + + observer.observe(apple_tv, { + attributes: true + }); + } } From ee3b0ae19eae8c569888042dd07cd1fa10b75889 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Tue, 12 Nov 2019 12:47:57 -0600 Subject: [PATCH 06/40] use treewalker to find shadow videos --- inject.js | 55 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/inject.js b/inject.js index 8e4ee3d..06fb148 100644 --- a/inject.js +++ b/inject.js @@ -322,6 +322,35 @@ return true; } } + function queryShadowVideo(element) { + const walker = document.createTreeWalker( + element, + NodeFilter.SHOW_ELEMENT, + { acceptNode: function(node) { + if (node.shadowRoot) { + return NodeFilter.FILTER_ACCEPT; + } + return NodeFilter.FILTER_SKIP; + }} + ); + + let list = []; + + if (element.shadowRoot) { + list = list.concat(queryShadowVideo(element.shadowRoot)) + } + + while (walker.nextNode()) { + let video = walker.currentNode.shadowRoot.querySelector('video') + if (video) { + list.push(video); + } + list = list.concat(queryShadowVideo(walker.currentNode.shadowRoot)) + } + + return list; + } + function initializeNow(document) { if (!tc.settings.enabled) return; // enforce init-once due to redundant callers @@ -369,7 +398,12 @@ } // Ignore keydown event if typing in a page without vsc - if (!document.querySelector(".vsc-controller")) { + if (document.querySelector('apple-tv-plus-player')) { + if (queryShadowVideo(document.querySelector('apple-tv-plus-player')).length > 0) { + + } + } + else if (!document.querySelector(".vsc-controller")) { return false; } @@ -447,22 +481,17 @@ }); //look for video in shadowRoot for apple tv - function deepActiveElement() { - let a = document.activeElement; - while (a && a.shadowRoot && a.shadowRoot.activeElement) { - a = a.shadowRoot.activeElement; - } - return a; - } var apple_tv = document.querySelector('apple-tv-plus-player') if (apple_tv) { var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.attributeName == 'aria-hidden' && (apple_tv.getAttribute('aria-hidden') == 'false')) { - setTimeout(() => { - var node = deepActiveElement() + var node = queryShadowVideo(document.querySelector('apple-tv-plus-player'))[0] + if (!node.previousElementSibling) { checkForVideo(node, node.parentNode || mutation.target, true); - }, 2000) + } else { + checkForVideo(node, node.parentNode || mutation.target, false); + } } }); }); @@ -476,7 +505,9 @@ } function runAction(action, document, value, e) { - if (tc.settings.audioBoolean) { + if (document.querySelector('apple-tv-plus-player')) { + var mediaTags = queryShadowVideo(document.querySelector('apple-tv-plus-player')) + } else if (tc.settings.audioBoolean) { var mediaTags = document.querySelectorAll('video,audio'); } else { var mediaTags = document.querySelectorAll('video'); From 4b42da106a0b101272343c80f83b8da6a5f61aaa Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Tue, 12 Nov 2019 12:53:24 -0600 Subject: [PATCH 07/40] revert the second mutation observer back to the one in the master --- inject.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/inject.js b/inject.js index 06fb148..54fc4c3 100644 --- a/inject.js +++ b/inject.js @@ -443,7 +443,7 @@ } } - function mutationCallback(mutations) { + var observer = new MutationObserver(function(mutations) { // Process the DOM nodes lazily requestIdleCallback(_ => { mutations.forEach(function(mutation) { @@ -459,8 +459,7 @@ }); }); }, {timeout: 1000}); - } - var observer = new MutationObserver(mutationCallback); + }); observer.observe(document, { childList: true, subtree: true }); if (tc.settings.audioBoolean) { From 1bd0635325e3d2c7aa395fc14225de9c8d2ef1a9 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Tue, 12 Nov 2019 12:57:22 -0600 Subject: [PATCH 08/40] ignore keydown event when there is no video on apple tv --- inject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject.js b/inject.js index 54fc4c3..3ce918c 100644 --- a/inject.js +++ b/inject.js @@ -399,8 +399,8 @@ // Ignore keydown event if typing in a page without vsc if (document.querySelector('apple-tv-plus-player')) { - if (queryShadowVideo(document.querySelector('apple-tv-plus-player')).length > 0) { - + if (queryShadowVideo(document.querySelector('apple-tv-plus-player')).length == 0) { + return false; } } else if (!document.querySelector(".vsc-controller")) { From 123f37e5167b58291e769e0285078ed4e1b34cf4 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Tue, 12 Nov 2019 13:26:16 -0600 Subject: [PATCH 09/40] extra whitespace deletion --- inject.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inject.js b/inject.js index 3ce918c..68947c0 100644 --- a/inject.js +++ b/inject.js @@ -402,8 +402,7 @@ if (queryShadowVideo(document.querySelector('apple-tv-plus-player')).length == 0) { return false; } - } - else if (!document.querySelector(".vsc-controller")) { + } else if (!document.querySelector(".vsc-controller")) { return false; } From 4de8ae4a0b08362dd9ddea80d044b53fa220ffcd Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 14 Nov 2019 18:05:56 -0600 Subject: [PATCH 10/40] refactor apple tv specific mutationObserver into second mutationObserver --- inject.js | 58 ++++++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/inject.js b/inject.js index 94111a4..f9b9ba8 100644 --- a/inject.js +++ b/inject.js @@ -448,20 +448,34 @@ // Process the DOM nodes lazily requestIdleCallback(_ => { mutations.forEach(function(mutation) { - forEach.call(mutation.addedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, true); - }); - forEach.call(mutation.removedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, false); - }); + switch (mutation.type) { + case 'childList': + forEach.call(mutation.addedNodes, function(node) { + if (typeof node === "function") + return; + checkForVideo(node, node.parentNode || mutation.target, true); + }); + forEach.call(mutation.removedNodes, function(node) { + if (typeof node === "function") + return; + checkForVideo(node, node.parentNode || mutation.target, false); + }); + break; + case 'attributes': + if (mutation.attributeName == 'aria-hidden' && (mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { + var node = queryShadowVideo(document.querySelector('apple-tv-plus-player'))[0] + if (!node.previousElementSibling) { + checkForVideo(node, node.parentNode || mutation.target, true); + } else { + checkForVideo(node, node.parentNode || mutation.target, false); + } + } + break; + }; }); }, {timeout: 1000}); }); - observer.observe(document, { childList: true, subtree: true }); + observer.observe(document, { attributes: true, childList: true, subtree: true }); if (tc.settings.audioBoolean) { var mediaTags = document.querySelectorAll('video,audio'); @@ -480,28 +494,6 @@ initializeWhenReady(childDocument); }); - //look for video in shadowRoot for apple tv - var apple_tv = document.querySelector('apple-tv-plus-player') - if (apple_tv) { - var observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.attributeName == 'aria-hidden' && (apple_tv.getAttribute('aria-hidden') == 'false')) { - var node = queryShadowVideo(document.querySelector('apple-tv-plus-player'))[0] - if (!node.previousElementSibling) { - checkForVideo(node, node.parentNode || mutation.target, true); - } else { - checkForVideo(node, node.parentNode || mutation.target, false); - } - } - }); - }); - - observer.observe(apple_tv, { - attributes: true - }); - - } - } function runAction(action, document, value, e) { From d38daa5ffa972689b0b6f1ab54f8c74ff98d51ee Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 15 Nov 2019 13:11:44 -0600 Subject: [PATCH 11/40] only look for aria-hidden attributes rather than all attribute changes. --- inject.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inject.js b/inject.js index f9b9ba8..b344509 100644 --- a/inject.js +++ b/inject.js @@ -475,7 +475,11 @@ }); }, {timeout: 1000}); }); - observer.observe(document, { attributes: true, childList: true, subtree: true }); + observer.observe(document, { + attributeFilter: ['aria-hidden'], + childList: true, + subtree: true + }); if (tc.settings.audioBoolean) { var mediaTags = document.querySelectorAll('video,audio'); From 30d9d40ecb2281beaef2b8c8c0fdc824b4a57929 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Tue, 19 Nov 2019 19:08:14 -0600 Subject: [PATCH 12/40] put div wrapper in the video's parent node light dom for correct context --- inject.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inject.js b/inject.js index b344509..5fe378f 100644 --- a/inject.js +++ b/inject.js @@ -268,6 +268,8 @@ // insert before parent to bypass overlay this.parent.parentElement.insertBefore(fragment, this.parent); break; + case (location.hostname == 'tv.apple.com'): + this.parent.getRootNode().host.prepend(fragment); default: // Note: when triggered via a MutationRecord, it's possible that the @@ -551,6 +553,7 @@ controller.classList.add('vsc-manual'); controller.classList.toggle('vsc-hidden'); } else if (action === 'blink') { + console.log(controller) // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. if(controller.classList.contains('vsc-hidden') || controller.blinkTimeOut !== undefined){ clearTimeout(controller.blinkTimeOut); From ad86b01cc6b847e010d0598f5010012611ecb988 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Wed, 20 Nov 2019 18:39:38 -0600 Subject: [PATCH 13/40] add comment explaining insertion location. --- inject.js | 1 + 1 file changed, 1 insertion(+) diff --git a/inject.js b/inject.js index d677b33..048dbca 100644 --- a/inject.js +++ b/inject.js @@ -269,6 +269,7 @@ this.parent.parentElement.insertBefore(fragment, this.parent); break; case (location.hostname == 'tv.apple.com'): + // insert after parent for correct stacking context this.parent.getRootNode().host.prepend(fragment); default: From 20a15f8da57e2cffbc9fc4aca3722f36f27c4185 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 21 Nov 2019 16:54:09 -0600 Subject: [PATCH 14/40] change recursive function and change variables that use querySelector. --- inject.js | 79 +++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/inject.js b/inject.js index 048dbca..34fa731 100644 --- a/inject.js +++ b/inject.js @@ -180,7 +180,9 @@ var observer=new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ - var controller = document.querySelector(`div[data-vscid="${this.id}"]`); + var controller = getShadow(document.body).filter(x => { + return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${id}` + })[0] if(!controller){ return; } @@ -269,7 +271,7 @@ this.parent.parentElement.insertBefore(fragment, this.parent); break; case (location.hostname == 'tv.apple.com'): - // insert after parent for correct stacking context + // insert after parent for correct stacking context this.parent.getRootNode().host.prepend(fragment); default: @@ -327,33 +329,23 @@ return true; } } - function queryShadowVideo(element) { - const walker = document.createTreeWalker( - element, - NodeFilter.SHOW_ELEMENT, - { acceptNode: function(node) { - if (node.shadowRoot) { - return NodeFilter.FILTER_ACCEPT; - } - return NodeFilter.FILTER_SKIP; - }} - ); - - let list = []; - - if (element.shadowRoot) { - list = list.concat(queryShadowVideo(element.shadowRoot)) - } - - while (walker.nextNode()) { - let video = walker.currentNode.shadowRoot.querySelector('video') - if (video) { - list.push(video); - } - list = list.concat(queryShadowVideo(walker.currentNode.shadowRoot)) - } - - return list; + function getShadow(parent) { + let result = [] + function getChild(parent) { + if (parent.firstElementChild) { + var child = parent.firstElementChild + do { + result = result.concat(child) + getChild(child) + if (child.shadowRoot) { + result = result.concat(getShadow(child.shadowRoot)) + } + child = child.nextElementSibling + } while (child) + } + } + getChild(parent) + return result } function initializeNow(document) { @@ -403,11 +395,7 @@ } // Ignore keydown event if typing in a page without vsc - if (document.querySelector('apple-tv-plus-player')) { - if (queryShadowVideo(document.querySelector('apple-tv-plus-player')).length == 0) { - return false; - } - } else if (!document.querySelector(".vsc-controller")) { + if (!getShadow(document.body).filter(x => x.tagName == 'vsc-controller')) { return false; } @@ -466,11 +454,10 @@ break; case 'attributes': if (mutation.attributeName == 'aria-hidden' && (mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { - var node = queryShadowVideo(document.querySelector('apple-tv-plus-player'))[0] - if (!node.previousElementSibling) { + var flattenedNodes = getShadow(document.body) + var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] + if (!flattenedNodes.filter(x => x.className == 'vsc-controller')[0]) { checkForVideo(node, node.parentNode || mutation.target, true); - } else { - checkForVideo(node, node.parentNode || mutation.target, false); } } break; @@ -504,12 +491,12 @@ } function runAction(action, document, value, e) { - if (document.querySelector('apple-tv-plus-player')) { - var mediaTags = queryShadowVideo(document.querySelector('apple-tv-plus-player')) - } else if (tc.settings.audioBoolean) { - var mediaTags = document.querySelectorAll('video,audio'); + if (tc.settings.audioBoolean) { + var mediaTags = getShadow(document.body).filter(x => { + return x.tagName == 'AUDIO' || x.tagName == 'VIDEO' + }); } else { - var mediaTags = document.querySelectorAll('video'); + var mediaTags = getShadow(document.body).filter(x => x.tagName == 'VIDEO');; } mediaTags.forEach = Array.prototype.forEach; @@ -521,8 +508,9 @@ mediaTags.forEach(function(v) { var id = v.dataset['vscid']; - var controller = document.querySelector(`div[data-vscid="${id}"]`); - + var controller = getShadow(document.body).filter(x => { + return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${id}` + })[0] // Don't change video speed if the video has a different controller if (e && !(targetController == controller)) { return; @@ -554,7 +542,6 @@ controller.classList.add('vsc-manual'); controller.classList.toggle('vsc-hidden'); } else if (action === 'blink') { - console.log(controller) // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. if(controller.classList.contains('vsc-hidden') || controller.blinkTimeOut !== undefined){ clearTimeout(controller.blinkTimeOut); From 91cf313f52ccc5d5a2f7bcfa9af3ea2dabfa2d48 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 21 Nov 2019 17:03:40 -0600 Subject: [PATCH 15/40] change from tabs to spaces --- inject.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/inject.js b/inject.js index 34fa731..98223f4 100644 --- a/inject.js +++ b/inject.js @@ -330,22 +330,22 @@ } } function getShadow(parent) { - let result = [] - function getChild(parent) { - if (parent.firstElementChild) { - var child = parent.firstElementChild + let result = [] + function getChild(parent) { + if (parent.firstElementChild) { + var child = parent.firstElementChild do { result = result.concat(child) - getChild(child) - if (child.shadowRoot) { - result = result.concat(getShadow(child.shadowRoot)) - } + getChild(child) + if (child.shadowRoot) { + result = result.concat(getShadow(child.shadowRoot)) + } child = child.nextElementSibling } while (child) - } - } - getChild(parent) - return result + } + } + getChild(parent) + return result } function initializeNow(document) { From 196e6ba7b2cc84cdc65d182cdf430f0e90e56154 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 21 Nov 2019 17:40:23 -0600 Subject: [PATCH 16/40] remove old controller when changing to a new video --- inject.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/inject.js b/inject.js index 98223f4..9fdcd73 100644 --- a/inject.js +++ b/inject.js @@ -179,9 +179,10 @@ var observer=new MutationObserver((mutations) => { mutations.forEach((mutation) => { + console.log(mutation) if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ var controller = getShadow(document.body).filter(x => { - return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${id}` + return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${this.id}` })[0] if(!controller){ return; @@ -453,12 +454,14 @@ }); break; case 'attributes': - if (mutation.attributeName == 'aria-hidden' && (mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { + if ((mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { var flattenedNodes = getShadow(document.body) var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] - if (!flattenedNodes.filter(x => x.className == 'vsc-controller')[0]) { - checkForVideo(node, node.parentNode || mutation.target, true); + var oldController = flattenedNodes.filter(x => x.className == 'vsc-controller')[0] + if (oldController) { + oldController.remove() } + checkForVideo(node, node.parentNode || mutation.target, true); } break; }; @@ -496,7 +499,7 @@ return x.tagName == 'AUDIO' || x.tagName == 'VIDEO' }); } else { - var mediaTags = getShadow(document.body).filter(x => x.tagName == 'VIDEO');; + var mediaTags = getShadow(document.body).filter(x => x.tagName == 'VIDEO'); } mediaTags.forEach = Array.prototype.forEach; From 50eaa92bdd64c7d66303a6d5c05c575b45c16450 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 21 Nov 2019 17:41:50 -0600 Subject: [PATCH 17/40] remove errant console.log --- inject.js | 1 - 1 file changed, 1 deletion(-) diff --git a/inject.js b/inject.js index 9fdcd73..acd0f49 100644 --- a/inject.js +++ b/inject.js @@ -179,7 +179,6 @@ var observer=new MutationObserver((mutations) => { mutations.forEach((mutation) => { - console.log(mutation) if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ var controller = getShadow(document.body).filter(x => { return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${this.id}` From d08563305ae3f7743b03f8fbe9dc02824d170409 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Thu, 21 Nov 2019 17:54:58 -0600 Subject: [PATCH 18/40] get rid of errant newline --- inject.js | 1 - 1 file changed, 1 deletion(-) diff --git a/inject.js b/inject.js index acd0f49..bc9a795 100644 --- a/inject.js +++ b/inject.js @@ -489,7 +489,6 @@ try { var childDocument = frame.contentDocument } catch (e) { return } initializeWhenReady(childDocument); }); - } function runAction(action, document, value, e) { From 58e091271213b07671b20a665e0393a7391806ce Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 22 Nov 2019 16:07:50 -0600 Subject: [PATCH 19/40] place controller inside scrim div & better filter to find old controller --- inject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inject.js b/inject.js index bc9a795..4c6f39a 100644 --- a/inject.js +++ b/inject.js @@ -272,7 +272,7 @@ break; case (location.hostname == 'tv.apple.com'): // insert after parent for correct stacking context - this.parent.getRootNode().host.prepend(fragment); + this.parent.getRootNode().querySelector('.scrim').prepend(fragment); default: // Note: when triggered via a MutationRecord, it's possible that the @@ -456,7 +456,7 @@ if ((mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { var flattenedNodes = getShadow(document.body) var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] - var oldController = flattenedNodes.filter(x => x.className == 'vsc-controller')[0] + var oldController = flattenedNodes.filter(x => x.classList.contains('vsc-controller'))[0] if (oldController) { oldController.remove() } From aa055a5e8f53d8327702403036e9d26302fdd729 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 3 Jan 2020 12:33:04 -0600 Subject: [PATCH 20/40] refactor code. create getController --- inject.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/inject.js b/inject.js index 73226ca..ad94d86 100644 --- a/inject.js +++ b/inject.js @@ -180,9 +180,7 @@ var observer=new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ - var controller = getShadow(document.body).filter(x => { - return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${this.id}` - })[0] + var controller = getController(this.id) if(!controller){ return; } @@ -356,6 +354,13 @@ getChild(parent) return result } + function getController(id){ + return getShadow(document.body).filter(x => { + return x.attributes['data-vscid'] && + x.tagName == 'DIV' && + x.attributes['data-vscid'].value==`${id}` + })[0] + } function initializeNow(document) { if (!tc.settings.enabled) return; @@ -518,9 +523,7 @@ mediaTags.forEach(function(v) { var id = v.dataset['vscid']; - var controller = getShadow(document.body).filter(x => { - return x.attributes['data-vscid'] && x.tagName == 'DIV' && x.attributes['data-vscid'].value==`${id}` - })[0] + var controller = getController(id) // Don't change video speed if the video has a different controller if (e && !(targetController == controller)) { return; From 4871ded5b5888cc2761d8871ec152b2956500e82 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 3 Jan 2020 15:34:04 -0600 Subject: [PATCH 21/40] remove apple-tv tag, but add in a check for video tag. --- inject.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/inject.js b/inject.js index ad94d86..4426cbf 100644 --- a/inject.js +++ b/inject.js @@ -467,14 +467,16 @@ }); break; case 'attributes': - if ((mutation.target.tagName == 'APPLE-TV-PLUS-PLAYER') && (mutation.target.attributes['aria-hidden'].value == "false")) { + if (mutation.target.attributes['aria-hidden'].value == "false") { var flattenedNodes = getShadow(document.body) var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] - var oldController = flattenedNodes.filter(x => x.classList.contains('vsc-controller'))[0] - if (oldController) { - oldController.remove() + if (node) { + var oldController = flattenedNodes.filter(x => x.classList.contains('vsc-controller'))[0] + if (oldController) { + oldController.remove() + } + checkForVideo(node, node.parentNode || mutation.target, true); } - checkForVideo(node, node.parentNode || mutation.target, true); } break; }; From 4ac2e458f548681976346615210cb6b1cdbe6576 Mon Sep 17 00:00:00 2001 From: jedawson Date: Thu, 6 Feb 2020 16:07:33 -0600 Subject: [PATCH 22/40] Apple tv plus (#541) * add support for traversing shadowRoot's; enables apple tv support. --- inject.js | 95 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/inject.js b/inject.js index b15455e..0424f38 100644 --- a/inject.js +++ b/inject.js @@ -180,7 +180,7 @@ var observer=new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ - var controller = document.querySelector(`div[data-vscid="${this.id}"]`); + var controller = getController(this.id) if(!controller){ return; } @@ -268,6 +268,9 @@ // insert before parent to bypass overlay this.parent.parentElement.insertBefore(fragment, this.parent); break; + case (location.hostname == 'tv.apple.com'): + // insert after parent for correct stacking context + this.parent.getRootNode().querySelector('.scrim').prepend(fragment); default: // Note: when triggered via a MutationRecord, it's possible that the @@ -333,6 +336,32 @@ return true; } } + function getShadow(parent) { + let result = [] + function getChild(parent) { + if (parent.firstElementChild) { + var child = parent.firstElementChild + do { + result = result.concat(child) + getChild(child) + if (child.shadowRoot) { + result = result.concat(getShadow(child.shadowRoot)) + } + child = child.nextElementSibling + } while (child) + } + } + getChild(parent) + return result + } + function getController(id){ + return getShadow(document.body).filter(x => { + return x.attributes['data-vscid'] && + x.tagName == 'DIV' && + x.attributes['data-vscid'].value==`${id}` + })[0] + } + function initializeNow(document) { if (!tc.settings.enabled) return; // enforce init-once due to redundant callers @@ -380,7 +409,7 @@ } // Ignore keydown event if typing in a page without vsc - if (!document.querySelector(".vsc-controller")) { + if (!getShadow(document.body).filter(x => x.tagName == 'vsc-controller')) { return false; } @@ -424,27 +453,48 @@ // Process the DOM nodes lazily requestIdleCallback(_ => { mutations.forEach(function(mutation) { - forEach.call(mutation.addedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, true); - }); - forEach.call(mutation.removedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, false); - }); + switch (mutation.type) { + case 'childList': + forEach.call(mutation.addedNodes, function(node) { + if (typeof node === "function") + return; + checkForVideo(node, node.parentNode || mutation.target, true); + }); + forEach.call(mutation.removedNodes, function(node) { + if (typeof node === "function") + return; + checkForVideo(node, node.parentNode || mutation.target, false); + }); + break; + case 'attributes': + if (mutation.target.attributes['aria-hidden'].value == "false") { + var flattenedNodes = getShadow(document.body) + var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] + if (node) { + var oldController = flattenedNodes.filter(x => x.classList.contains('vsc-controller'))[0] + if (oldController) { + oldController.remove() + } + checkForVideo(node, node.parentNode || mutation.target, true); + } + } + break; + }; }); }, {timeout: 1000}); }); - observer.observe(document, { childList: true, subtree: true }); + observer.observe(document, { + attributeFilter: ['aria-hidden'], + childList: true, + subtree: true + }); if (tc.settings.audioBoolean) { var mediaTags = document.querySelectorAll('video,audio'); } else { var mediaTags = document.querySelectorAll('video'); } - + forEach.call(mediaTags, function(video) { video.vsc = new tc.videoController(video); }); @@ -459,9 +509,11 @@ function runAction(action, document, value, e) { if (tc.settings.audioBoolean) { - var mediaTags = document.querySelectorAll('video,audio'); + var mediaTags = getShadow(document.body).filter(x => { + return x.tagName == 'AUDIO' || x.tagName == 'VIDEO' + }); } else { - var mediaTags = document.querySelectorAll('video'); + var mediaTags = getShadow(document.body).filter(x => x.tagName == 'VIDEO'); } mediaTags.forEach = Array.prototype.forEach; @@ -469,13 +521,12 @@ // Get the controller that was used if called from a button press event e if (e) { var targetController = e.target.getRootNode().host; - } + } mediaTags.forEach(function(v) { var id = v.dataset['vscid']; - var controller = document.querySelector(`div[data-vscid="${id}"]`); - - // Don't change video speed if the video has a different controller + var controller = getController(id) + // Don't change video speed if the video has a different controller if (e && !(targetController == controller)) { return; } @@ -564,13 +615,13 @@ function setMark(v) { v.vsc.mark = v.currentTime; } - + function jumpToMark(v) { if (v.vsc.mark && typeof v.vsc.mark === "number") { v.currentTime = v.vsc.mark; } } - + function handleDrag(video, controller, e) { const shadowController = controller.shadowRoot.querySelector('#controller'); From 55500fc0107b2b5c7465c5e85a59724630ff3b36 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Sat, 8 Feb 2020 16:26:10 -0600 Subject: [PATCH 23/40] Feature/578 prettier.io formatting (#585) * Reformatted codebase using prettier.io (should not change functional behavior) * Formatted README.md, added CONTRIBUTING.md, .prettierrc configuration file, and installed pre-commit hook --- .pre-commit-config.yaml | 15 + .prettierrc | 8 + CONTRIBUTING.md | 85 +++ README.md | 84 ++- inject.css | 21 +- inject.js | 1188 ++++++++++++++++++++------------------- manifest.json | 15 +- options.css | 123 ++-- options.html | 259 +++++---- options.js | 381 +++++++------ popup.css | 11 +- popup.html | 2 +- popup.js | 57 +- shadow.css | 2 +- 14 files changed, 1295 insertions(+), 956 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .prettierrc create mode 100644 CONTRIBUTING.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1501b94 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/prettier/prettier + rev: 1.19.1 # Use the sha or tag you want to point at + hooks: + - id: prettier diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c8c8ebc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "none", + "tabWidth": 2, + "printWidth": 80, + "semi": true, + "endOfLine": "auto", + "proseWrap": "always" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..87df582 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing + +Video Speed Controller is an open source project licensed under the MIT license +with many contributers. Contributions are welcome, and greatly appreciated. + +If you would like to help, getting started is easy. + +## Get Started + +1. You must have a github account and be logged in +2. Fork the repo by clicking the "Fork" link on the top-right corner of the page +3. Once the fork is ready, clone to your local PC + + ```sh + $ git clone https://github.com//videospeed.git + Cloning into 'videospeed'... + remote: Enumerating objects: 10, done. + remote: Counting objects: 100% (10/10), done. + remote: Compressing objects: 100% (9/9), done. + remote: Total 877 (delta 3), reused 2 (delta 1), pack-reused 867 + Receiving objects: 100% (877/877), 317.65 KiB | 2.17 MiB/s, done. + Resolving deltas: 100% (543/543), done. + ``` + +4. Create a branch for your changes + + ```sh + $ cd videospeed + videospeed$ git checkout -b bugfix/1-fix-double-click + M .github/workflows/chrome-store-upload.yaml + M README.md + M options.js + Switched to a new branch 'bugfix/1-fix-double-click' + videospeed$ + ``` + +5. Open the code in your favorite code editor, make your changes + + ```sh + echo "Awesome changes" > somefile.js + git add . + ``` + + > Important: Your commit must be formatted using + > [prettier](https://prettier.io/). If it is not it may be autoformatted for + > you or your pull request may be rejected. + +6. Next, open Chrome/Brave/Chromium and enable developer mode via + `Settings > Extensions > Manage Extensions` and toggle `Developer mode` in + the top-right corner. +7. Click `Load unpacked` and browse to the folder you cloned videospeed to. +8. Try out your changes, make sure they work as expected +9. Commit and push your changes to github + + ```sh + git commit -m "Awesome description of some awesome changes." + git push + ``` + +10. Open your branch up on the github website then click `New pull request` and + write up a description of your changes. + +## Optional + +### Run Pre-Commit Checks Locally + +Installing [pre-commit](https://pre-commit.com/) is easy to do (click the link +for instructions on your platform). This repo comes with pre-commit already +configured. Doing this will ensure that your project is properly formatted and +runs some very basic tests. Once you have pre-commit installed on your system, +simply enter `pre-commit install` in your terminal in the folder to have these +checks run automatically each time you commit. + +Even better, after issueing the install command you can now manually run +pre-commit checks before committing via `pre-commit run --all-files` + +### Pull Upstream Changes + +You should always be working with the latest version of the tool to make pull +requests easy. If you want to do this easily, just add a second remote to your +local git repo like this +`git push --set-upstream origin feature/578-prettier.io-formatting` + +Now any time you like to pull the latest version in to your local branch you can +simply issue the command `git pull upstream master` diff --git a/README.md b/README.md index 4b22e20..66b1949 100644 --- a/README.md +++ b/README.md @@ -2,48 +2,86 @@ **TL;DR: faster playback translates to better engagement and retention.** -Average adult reads prose text at [250 to 300 words per minute](http://www.paperbecause.com/PIOP/files/f7/f7bb6bc5-2c4a-466f-9ae7-b483a2c0dca4.pdf) (wpm). By contrast, the average rate of speech for English speakers is ~150 wpm, with slide presentations often closer to 100 wpm. As a result, when given the choice, many viewers [speed up video playback to ~1.3\~1.5 its recorded rate](http://research.microsoft.com/en-us/um/redmond/groups/coet/compression/chi99/paper.pdf) to compensate for the difference. - -Many viewers report that [accelerated viewing keeps their attention longer](http://www.enounce.com/docs/BYUPaper020319.pdf): faster delivery keeps the viewer more engaged with the content. In fact, with a little training many end up watching videos at 2x+ the recorded speed. Some studies report that after being exposed to accelerated playback, [listeners become uncomfortable](http://alumni.media.mit.edu/~barons/html/avios92.html#beasleyalteredspeech) if they are forced to return to normal rate of presentation. +Average adult reads prose text at +[250 to 300 words per minute](http://www.paperbecause.com/PIOP/files/f7/f7bb6bc5-2c4a-466f-9ae7-b483a2c0dca4.pdf) +(wpm). By contrast, the average rate of speech for English speakers is ~150 wpm, +with slide presentations often closer to 100 wpm. As a result, when given the +choice, many viewers +[speed up video playback to ~1.3\~1.5 its recorded rate](http://research.microsoft.com/en-us/um/redmond/groups/coet/compression/chi99/paper.pdf) +to compensate for the difference. +Many viewers report that +[accelerated viewing keeps their attention longer](http://www.enounce.com/docs/BYUPaper020319.pdf): +faster delivery keeps the viewer more engaged with the content. In fact, with a +little training many end up watching videos at 2x+ the recorded speed. Some +studies report that after being exposed to accelerated playback, +[listeners become uncomfortable](http://alumni.media.mit.edu/~barons/html/avios92.html#beasleyalteredspeech) +if they are forced to return to normal rate of presentation. ## Faster HTML5 Video -HTML5 video provides a native API to accelerate playback of any video. The problem is, many players either hide, or limit this functionality. For best results playback speed adjustments should be easy and frequent to match the pace and content being covered: we don't read at a fixed speed, and similarly, we need an easy way to accelerate the video, slow it down, and quickly rewind the last point to listen to it a few more times. +HTML5 video provides a native API to accelerate playback of any video. The +problem is, many players either hide, or limit this functionality. For best +results playback speed adjustments should be easy and frequent to match the pace +and content being covered: we don't read at a fixed speed, and similarly, we +need an easy way to accelerate the video, slow it down, and quickly rewind the +last point to listen to it a few more times. ![Player](https://cloud.githubusercontent.com/assets/2400185/24076745/5723e6ae-0c41-11e7-820c-1d8e814a2888.png) -#### *[Install Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk)* +### _[Install Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk)_ -Once the extension is installed simply navigate to any page that offers HTML5 video ([example](http://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll see a speed indicator in top left corner. Hover over the indicator to reveal the controls to accelerate, slowdown, and quickly rewind or advance the video. Or, even better, simply use your keyboard: +\*\* Once the extension is installed simply navigate to any page that offers +HTML5 video ([example](http://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll +see a speed indicator in top left corner. Hover over the indicator to reveal the +controls to accelerate, slowdown, and quickly rewind or advance the video. Or, +even better, simply use your keyboard: -* **S** - decrease playback speed. -* **D** - increase playback speed. -* **R** - reset playback speed to 1.0x. -* **Z** - rewind video by 10 seconds. -* **X** - advance video by 10 seconds. -* **G** - toggle between current and user configurable preferred speed. -* **V** - show/hide the controller. +- **S** - decrease playback speed. +- **D** - increase playback speed. +- **R** - reset playback speed to 1.0x. +- **Z** - rewind video by 10 seconds. +- **X** - advance video by 10 seconds. +- **G** - toggle between current and user configurable preferred speed. +- **V** - show/hide the controller. -You can customize and reassign the default shortcut keys in the extensions settings page, as well as add additional shortcut keys to match your preferences. For example, you can assign multiple different "preferred speed" shortcuts with different values, which will allow you to quickly toggle between your most commonly used speeds. To add a new shortcut, open extension settings and click "Add New". +You can customize and reassign the default shortcut keys in the extensions +settings page, as well as add additional shortcut keys to match your +preferences. For example, you can assign multiple different "preferred speed" +shortcuts with different values, which will allow you to quickly toggle between +your most commonly used speeds. To add a new shortcut, open extension settings +and click "Add New". ![settings Add New shortcut](https://user-images.githubusercontent.com/121805/50726471-50242200-1172-11e9-902f-0e5958387617.jpg) -Some sites may assign other functionality to one of the assigned shortcut keys — these collisions are inevitable, unfortunately. As a workaround, the extension listens both for lower and upper case values (i.e. you can use `Shift-`) if there is other functionality assigned to the lowercase key. This is not a perfect solution, as some sites may listen to both, but works most of the time. +Some sites may assign other functionality to one of the assigned shortcut keys — +these collisions are inevitable, unfortunately. As a workaround, the extension +listens both for lower and upper case values (i.e. you can use +`Shift-`) if there is other functionality assigned to the lowercase +key. This is not a perfect solution, as some sites may listen to both, but works +most of the time. ### FAQ -**The video controls are not showing up?** This extension is only compatible with HTML5 video. If you don't see the controls showing up, chances are you are viewing a Flash video. If you want to confirm, try right-clicking on the video and inspect the menu: if it mentions flash, then that's the issue. That said, most sites will fallback to HTML5 if they detect that Flash it not available. You can try manually disabling Flash plugin in Chrome: +**The video controls are not showing up?** This extension is only compatible +with HTML5 video. If you don't see the controls showing up, chances are you are +viewing a Flash video. If you want to confirm, try right-clicking on the video +and inspect the menu: if it mentions flash, then that's the issue. That said, +most sites will fallback to HTML5 if they detect that Flash it not available. +You can try manually disabling Flash plugin in Chrome: -* In a new tab, navigate to `chrome://settings/content/flash` -* Disable "Allow sites to run Flash" -* Restart your browser and try playing your video again +- In a new tab, navigate to `chrome://settings/content/flash` +- Disable "Allow sites to run Flash" +- Restart your browser and try playing your video again -**The speed controls are not showing up for local videos?** To enable playback of local media (e.g. File > Open File), you need to grant additional permissions to the extension. +**The speed controls are not showing up for local videos?** To enable playback +of local media (e.g. File > Open File), you need to grant additional permissions +to the extension. -* In a new tab, navigate to `chrome://extensions` -* Find "Video Speed Controller" extension in the list and enable "Allow access to file URLs" -* Open a new tab and try opening a local file, the controls should show up +- In a new tab, navigate to `chrome://extensions` +- Find "Video Speed Controller" extension in the list and enable "Allow access + to file URLs" +- Open a new tab and try opening a local file, the controls should show up ### License diff --git a/inject.css b/inject.css index 3487118..33cc655 100644 --- a/inject.css +++ b/inject.css @@ -1,8 +1,12 @@ -.vsc-nosource { display: none !important; } -.vsc-hidden { display: none !important; } +.vsc-nosource { + display: none !important; +} +.vsc-hidden { + display: none !important; +} .vsc-manual { visibility: visible !important; - opacity: 1.0 !important; + opacity: 1 !important; } /* Origin specific overrides */ @@ -14,13 +18,13 @@ .ytp-autohide .vsc-controller { visibility: hidden; - transition: opacity .25s cubic-bezier(0.4,0,0.2,1); + transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0; } .ytp-autohide .vcs-show { visibility: visible; - opacity: 1.0; + opacity: 1; } /* YouTube embedded player */ @@ -32,8 +36,8 @@ /* Facebook player */ #facebook .vsc-controller { - position: relative; - top: 40px; + position: relative; + top: 40px; } /* Google Photos player */ @@ -76,7 +80,8 @@ div.video-wrapper + div.target { } /* Fix black overlay on Kickstarter */ -div.video-player.has_played.vertically_center:before, div.legacy-video-player.has_played.vertically_center:before { +div.video-player.has_played.vertically_center:before, +div.legacy-video-player.has_played.vertically_center:before { content: none !important; } diff --git a/inject.js b/inject.js index 0424f38..f1dda0f 100644 --- a/inject.js +++ b/inject.js @@ -1,154 +1,158 @@ - var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; +var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; - var tc = { - settings: { - lastSpeed: 1.0, // default 1x - enabled: true, // default enabled - speeds: {}, // empty object to hold speed for each source +var tc = { + settings: { + lastSpeed: 1.0, // default 1x + enabled: true, // default enabled + speeds: {}, // empty object to hold speed for each source - displayKeyCode: 86, // default: V - rememberSpeed: false, // default: false - audioBoolean: false, // default: false - startHidden: false, // default: false - controllerOpacity: 0.3, // default: 0.3 - keyBindings: [], - blacklist: ` - www.instagram.com - twitter.com - vine.co - imgur.com - teams.microsoft.com - `.replace(regStrip,'') - } - }; + displayKeyCode: 86, // default: V + rememberSpeed: false, // default: false + audioBoolean: false, // default: false + startHidden: false, // default: false + controllerOpacity: 0.3, // default: 0.3 + keyBindings: [], + blacklist: ` + www.instagram.com + twitter.com + vine.co + imgur.com + teams.microsoft.com + `.replace(regStrip, "") + } +}; - chrome.storage.sync.get(tc.settings, function (storage) { - tc.settings.keyBindings = storage.keyBindings; // Array - if (storage.keyBindings.length == 0) // if first initialization of 0.5.3 - { - // UPDATE - tc.settings.keyBindings.push({ - action: "slower", - key: Number(storage.slowerKeyCode) || 83, - value: Number(storage.speedStep) || 0.1, - force: false, - predefined: true - }); // default S - tc.settings.keyBindings.push({ - action: "faster", - key: Number(storage.fasterKeyCode) || 68, - value: Number(storage.speedStep) || 0.1, - force: false, - predefined: true - }); // default: D - tc.settings.keyBindings.push({ - action: "rewind", - key: Number(storage.rewindKeyCode) || 90, - value: Number(storage.rewindTime) || 10, - force: false, - predefined: true - }); // default: Z - tc.settings.keyBindings.push({ - action: "advance", - key: Number(storage.advanceKeyCode) || 88, - value: Number(storage.advanceTime) || 10, - force: false, - predefined: true - }); // default: X - tc.settings.keyBindings.push({ - action: "reset", - key: Number(storage.resetKeyCode) || 82, - value: 1.0, - force: false, - predefined: true - }); // default: R - tc.settings.keyBindings.push({ - action: "fast", - key: Number(storage.fastKeyCode) || 71, - value: Number(storage.fastSpeed) || 1.8, - force: false, - predefined: true - }); // default: G - tc.settings.version = "0.5.3"; +chrome.storage.sync.get(tc.settings, function(storage) { + tc.settings.keyBindings = storage.keyBindings; // Array + if (storage.keyBindings.length == 0) { + // if first initialization of 0.5.3 + // UPDATE + tc.settings.keyBindings.push({ + action: "slower", + key: Number(storage.slowerKeyCode) || 83, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }); // default S + tc.settings.keyBindings.push({ + action: "faster", + key: Number(storage.fasterKeyCode) || 68, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }); // default: D + tc.settings.keyBindings.push({ + action: "rewind", + key: Number(storage.rewindKeyCode) || 90, + value: Number(storage.rewindTime) || 10, + force: false, + predefined: true + }); // default: Z + tc.settings.keyBindings.push({ + action: "advance", + key: Number(storage.advanceKeyCode) || 88, + value: Number(storage.advanceTime) || 10, + force: false, + predefined: true + }); // default: X + tc.settings.keyBindings.push({ + action: "reset", + key: Number(storage.resetKeyCode) || 82, + value: 1.0, + force: false, + predefined: true + }); // default: R + tc.settings.keyBindings.push({ + action: "fast", + key: Number(storage.fastKeyCode) || 71, + value: Number(storage.fastSpeed) || 1.8, + force: false, + predefined: true + }); // default: G + tc.settings.version = "0.5.3"; - chrome.storage.sync.set({ - keyBindings: tc.settings.keyBindings, - version: tc.settings.version, - displayKeyCode: tc.settings.displayKeyCode, - rememberSpeed: tc.settings.rememberSpeed, - audioBoolean: tc.settings.audioBoolean, - startHidden: tc.settings.startHidden, - enabled: tc.settings.enabled, - controllerOpacity: tc.settings.controllerOpacity, - blacklist: tc.settings.blacklist.replace(regStrip, '') - }); - } - tc.settings.lastSpeed = Number(storage.lastSpeed); - tc.settings.displayKeyCode = Number(storage.displayKeyCode); - tc.settings.rememberSpeed = Boolean(storage.rememberSpeed); - tc.settings.audioBoolean = Boolean(storage.audioBoolean); - tc.settings.enabled = Boolean(storage.enabled); - tc.settings.startHidden = Boolean(storage.startHidden); - tc.settings.controllerOpacity = Number(storage.controllerOpacity); - tc.settings.blacklist = String(storage.blacklist); + chrome.storage.sync.set({ + keyBindings: tc.settings.keyBindings, + version: tc.settings.version, + displayKeyCode: tc.settings.displayKeyCode, + rememberSpeed: tc.settings.rememberSpeed, + audioBoolean: tc.settings.audioBoolean, + startHidden: tc.settings.startHidden, + enabled: tc.settings.enabled, + controllerOpacity: tc.settings.controllerOpacity, + blacklist: tc.settings.blacklist.replace(regStrip, "") + }); + } + tc.settings.lastSpeed = Number(storage.lastSpeed); + tc.settings.displayKeyCode = Number(storage.displayKeyCode); + tc.settings.rememberSpeed = Boolean(storage.rememberSpeed); + tc.settings.audioBoolean = Boolean(storage.audioBoolean); + tc.settings.enabled = Boolean(storage.enabled); + tc.settings.startHidden = Boolean(storage.startHidden); + tc.settings.controllerOpacity = Number(storage.controllerOpacity); + tc.settings.blacklist = String(storage.blacklist); - // ensure that there is a "display" binding (for upgrades from versions that had it as a separate binding) - if (tc.settings.keyBindings.filter(x => x.action == "display").length == 0) { - tc.settings.keyBindings.push({ - action: "display", - key: Number(storage.displayKeyCode) || 86, - value: 0, - force: false, - predefined: true - }); // default V - } - - initializeWhenReady(document); - }); - - var forEach = Array.prototype.forEach; - - function getKeyBindings(action, what = "value") { - try { - return tc.settings.keyBindings.find(item => item.action === action)[what]; - } catch (e) { - return false; - } + // ensure that there is a "display" binding (for upgrades from versions that had it as a separate binding) + if (tc.settings.keyBindings.filter(x => x.action == "display").length == 0) { + tc.settings.keyBindings.push({ + action: "display", + key: Number(storage.displayKeyCode) || 86, + value: 0, + force: false, + predefined: true + }); // default V } - function setKeyBindings(action, value) { - tc.settings.keyBindings.find(item => item.action === action)["value"] = value; + initializeWhenReady(document); +}); + +var forEach = Array.prototype.forEach; + +function getKeyBindings(action, what = "value") { + try { + return tc.settings.keyBindings.find(item => item.action === action)[what]; + } catch (e) { + return false; } +} - function defineVideoController() { - tc.videoController = function(target, parent) { - if (target.dataset['vscid']) { - return target.vsc; +function setKeyBindings(action, value) { + tc.settings.keyBindings.find(item => item.action === action)["value"] = value; +} + +function defineVideoController() { + tc.videoController = function(target, parent) { + if (target.dataset["vscid"]) { + return target.vsc; + } + + this.video = target; + this.parent = target.parentElement || parent; + this.document = target.ownerDocument; + this.id = Math.random() + .toString(36) + .substr(2, 9); + + // settings.speeds[] ensures that same source used across video tags (e.g. fullscreen on YT) retains speed setting + // this.speed is a controller level variable that retains speed setting across source switches (e.g. video quality, playlist change) + this.speed = 1.0; + + if (!tc.settings.rememberSpeed) { + if (!tc.settings.speeds[target.currentSrc]) { + tc.settings.speeds[target.currentSrc] = this.speed; } + setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed + } else { + tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; + } - this.video = target; - this.parent = target.parentElement || parent; - this.document = target.ownerDocument; - this.id = Math.random().toString(36).substr(2, 9); + target.playbackRate = tc.settings.speeds[target.currentSrc]; - // settings.speeds[] ensures that same source used across video tags (e.g. fullscreen on YT) retains speed setting - // this.speed is a controller level variable that retains speed setting across source switches (e.g. video quality, playlist change) - this.speed = 1.0; + this.div = this.initializeControls(); - if (!tc.settings.rememberSpeed) { - if (!tc.settings.speeds[target.currentSrc]) { - tc.settings.speeds[target.currentSrc] = this.speed; - } - setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed - } else { - tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; - } - - target.playbackRate = tc.settings.speeds[target.currentSrc]; - - this.div=this.initializeControls(); - - target.addEventListener('play', this.handlePlay = function(event) { + target.addEventListener( + "play", + (this.handlePlay = function(event) { if (!tc.settings.rememberSpeed) { if (!tc.settings.speeds[target.currentSrc]) { tc.settings.speeds[target.currentSrc] = this.speed; @@ -158,9 +162,12 @@ tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; } target.playbackRate = tc.settings.speeds[target.currentSrc]; - }.bind(this)); + }.bind(this)) + ); - target.addEventListener('ratechange', this.handleRatechange = function(event) { + target.addEventListener( + "ratechange", + (this.handleRatechange = function(event) { // Ignore ratechange events on unitialized videos. // 0 == No information is available about the media resource. if (event.target.readyState > 0) { @@ -169,71 +176,80 @@ tc.settings.speeds[this.video.currentSrc] = speed; tc.settings.lastSpeed = speed; this.speed = speed; - chrome.storage.sync.set({'lastSpeed': speed}, function() { - console.log('Speed setting saved: ' + speed); + chrome.storage.sync.set({ lastSpeed: speed }, function() { + console.log("Speed setting saved: " + speed); }); // show the controller for 1000ms if it's hidden. - runAction('blink', document, null, null); + runAction("blink", document, null, null); } - }.bind(this)); + }.bind(this)) + ); - var observer=new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ - var controller = getController(this.id) - if(!controller){ - return; - } - if (!mutation.target.src && !mutation.target.currentSrc) { - controller.classList.add('vsc-nosource'); - } else { - controller.classList.remove('vsc-nosource'); - } + var observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if ( + mutation.type === "attributes" && + (mutation.attributeName === "src" || + mutation.attributeName === "currentSrc") + ) { + var controller = getController(this.id); + if (!controller) { + return; } - }); + if (!mutation.target.src && !mutation.target.currentSrc) { + controller.classList.add("vsc-nosource"); + } else { + controller.classList.remove("vsc-nosource"); + } + } }); - observer.observe(target, { - attributeFilter: ["src", "currentSrc"] - }); - }; + }); + observer.observe(target, { + attributeFilter: ["src", "currentSrc"] + }); + }; - tc.videoController.prototype.getSpeed = function() { - return parseFloat(this.video.playbackRate).toFixed(2); + tc.videoController.prototype.getSpeed = function() { + return parseFloat(this.video.playbackRate).toFixed(2); + }; + + tc.videoController.prototype.remove = function() { + this.div.remove(); + this.video.removeEventListener("play", this.handlePlay); + this.video.removeEventListener("ratechange", this.handleRatechange); + delete this.video.dataset["vscid"]; + delete this.video.vsc; + }; + + tc.videoController.prototype.initializeControls = function() { + var document = this.document; + var speed = parseFloat(tc.settings.speeds[this.video.currentSrc]).toFixed( + 2 + ), + top = Math.max(this.video.offsetTop, 0) + "px", + left = Math.max(this.video.offsetLeft, 0) + "px"; + + var wrapper = document.createElement("div"); + wrapper.classList.add("vsc-controller"); + wrapper.dataset["vscid"] = this.id; + + if (!this.video.currentSrc) { + wrapper.classList.add("vsc-nosource"); } - tc.videoController.prototype.remove = function() { - this.div.remove(); - this.video.removeEventListener('play',this.handlePlay); - this.video.removeEventListener('ratechange',this.handleRatechange); - delete this.video.dataset['vscid']; - delete this.video.vsc; + if (tc.settings.startHidden) { + wrapper.classList.add("vsc-hidden"); } - tc.videoController.prototype.initializeControls = function() { - var document = this.document; - var speed = parseFloat(tc.settings.speeds[this.video.currentSrc]).toFixed(2), - top = Math.max(this.video.offsetTop, 0) + "px", - left = Math.max(this.video.offsetLeft, 0) + "px"; - - var wrapper = document.createElement('div'); - wrapper.classList.add('vsc-controller'); - wrapper.dataset['vscid'] = this.id; - - if (!this.video.currentSrc) { - wrapper.classList.add('vsc-nosource'); - } - - if (tc.settings.startHidden) { - wrapper.classList.add('vsc-hidden'); - } - - var shadow = wrapper.attachShadow({ mode: 'open' }); - var shadowTemplate = ` + var shadow = wrapper.attachShadow({ mode: "open" }); + var shadowTemplate = ` -
+
${speed} @@ -244,437 +260,475 @@
`; - shadow.innerHTML = shadowTemplate; - shadow.querySelector('.draggable').addEventListener('mousedown', (e) => { - runAction(e.target.dataset['action'], document, false, e); - }); + shadow.innerHTML = shadowTemplate; + shadow.querySelector(".draggable").addEventListener("mousedown", e => { + runAction(e.target.dataset["action"], document, false, e); + }); - forEach.call(shadow.querySelectorAll('button'), function(button) { - button.onclick = (e) => { - runAction(e.target.dataset['action'], document, getKeyBindings(e.target.dataset['action']), e); - } - }); + forEach.call(shadow.querySelectorAll("button"), function(button) { + button.onclick = e => { + runAction( + e.target.dataset["action"], + document, + getKeyBindings(e.target.dataset["action"]), + e + ); + }; + }); - this.speedIndicator = shadow.querySelector('span'); - var fragment = document.createDocumentFragment(); - fragment.appendChild(wrapper); + this.speedIndicator = shadow.querySelector("span"); + var fragment = document.createDocumentFragment(); + fragment.appendChild(wrapper); - this.video.dataset['vscid'] = this.id; + this.video.dataset["vscid"] = this.id; - switch (true) { - case (location.hostname == 'www.amazon.com'): - case (location.hostname == 'www.reddit.com'): - case (/hbogo\./).test(location.hostname): - // insert before parent to bypass overlay - this.parent.parentElement.insertBefore(fragment, this.parent); - break; - case (location.hostname == 'tv.apple.com'): - // insert after parent for correct stacking context - this.parent.getRootNode().querySelector('.scrim').prepend(fragment); + switch (true) { + case location.hostname == "www.amazon.com": + case location.hostname == "www.reddit.com": + case /hbogo\./.test(location.hostname): + // insert before parent to bypass overlay + this.parent.parentElement.insertBefore(fragment, this.parent); + break; + case location.hostname == "tv.apple.com": + // insert after parent for correct stacking context + this.parent + .getRootNode() + .querySelector(".scrim") + .prepend(fragment); - default: - // Note: when triggered via a MutationRecord, it's possible that the - // target is not the immediate parent. This appends the controller as - // the first element of the target, which may not be the parent. - this.parent.insertBefore(fragment, this.parent.firstChild); - } - return wrapper; + default: + // Note: when triggered via a MutationRecord, it's possible that the + // target is not the immediate parent. This appends the controller as + // the first element of the target, which may not be the parent. + this.parent.insertBefore(fragment, this.parent.firstChild); } + return wrapper; + }; +} + +function initializeWhenReady(document) { + escapeStringRegExp.matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; + function escapeStringRegExp(str) { + return str.replace(escapeStringRegExp.matchOperatorsRe, "\\$&"); } - function initializeWhenReady(document) { - escapeStringRegExp.matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; - function escapeStringRegExp(str) { - return str.replace(escapeStringRegExp.matchOperatorsRe, '\\$&'); + var blacklisted = false; + tc.settings.blacklist.split("\n").forEach(match => { + match = match.replace(regStrip, ""); + if (match.length == 0) { + return; } - var blacklisted = false; - tc.settings.blacklist.split("\n").forEach(match => { - match = match.replace(regStrip,'') - if (match.length == 0) { + if (match.startsWith("/")) { + try { + var regexp = new RegExp(match); + } catch (err) { return; } + } else { + var regexp = new RegExp(escapeStringRegExp(match)); + } - if (match.startsWith('/')) { - try { - var regexp = new RegExp(match); - } catch(err) { + if (regexp.test(location.href)) { + blacklisted = true; + return; + } + }); + + if (blacklisted) return; + + window.onload = () => { + initializeNow(window.document); + }; + if (document) { + if (document.readyState === "complete") { + initializeNow(document); + } else { + document.onreadystatechange = () => { + if (document.readyState === "complete") { + initializeNow(document); + } + }; + } + } +} +function inIframe() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +} +function getShadow(parent) { + let result = []; + function getChild(parent) { + if (parent.firstElementChild) { + var child = parent.firstElementChild; + do { + result = result.concat(child); + getChild(child); + if (child.shadowRoot) { + result = result.concat(getShadow(child.shadowRoot)); + } + child = child.nextElementSibling; + } while (child); + } + } + getChild(parent); + return result; +} +function getController(id) { + return getShadow(document.body).filter(x => { + return ( + x.attributes["data-vscid"] && + x.tagName == "DIV" && + x.attributes["data-vscid"].value == `${id}` + ); + })[0]; +} + +function initializeNow(document) { + if (!tc.settings.enabled) return; + // enforce init-once due to redundant callers + if (!document.body || document.body.classList.contains("vsc-initialized")) { + return; + } + document.body.classList.add("vsc-initialized"); + + if (document === window.document) { + defineVideoController(); + } else { + var link = document.createElement("link"); + link.href = chrome.runtime.getURL("inject.css"); + link.type = "text/css"; + link.rel = "stylesheet"; + document.head.appendChild(link); + } + var docs = Array(document); + try { + if (inIframe()) docs.push(window.top.document); + } catch (e) {} + + docs.forEach(function(doc) { + doc.addEventListener( + "keydown", + function(event) { + var keyCode = event.keyCode; + + // Ignore if following modifier is active. + if ( + !event.getModifierState || + event.getModifierState("Alt") || + event.getModifierState("Control") || + event.getModifierState("Fn") || + event.getModifierState("Meta") || + event.getModifierState("Hyper") || + event.getModifierState("OS") + ) { return; } - } else { - var regexp = new RegExp(escapeStringRegExp(match)); - } - if (regexp.test(location.href)) { - blacklisted = true; - return; - } - }) - - if (blacklisted) - return; - - window.onload = () => { - initializeNow(window.document) - }; - if (document) { - if (document.readyState === "complete") { - initializeNow(document); - } else { - document.onreadystatechange = () => { - if (document.readyState === "complete") { - initializeNow(document); - } + // Ignore keydown event if typing in an input box + if ( + event.target.nodeName === "INPUT" || + event.target.nodeName === "TEXTAREA" || + event.target.isContentEditable + ) { + return false; } - } - } - } - function inIframe () { - try { - return window.self !== window.top; - } catch (e) { - return true; - } - } - function getShadow(parent) { - let result = [] - function getChild(parent) { - if (parent.firstElementChild) { - var child = parent.firstElementChild - do { - result = result.concat(child) - getChild(child) - if (child.shadowRoot) { - result = result.concat(getShadow(child.shadowRoot)) - } - child = child.nextElementSibling - } while (child) - } - } - getChild(parent) - return result - } - function getController(id){ - return getShadow(document.body).filter(x => { - return x.attributes['data-vscid'] && - x.tagName == 'DIV' && - x.attributes['data-vscid'].value==`${id}` - })[0] - } - function initializeNow(document) { - if (!tc.settings.enabled) return; - // enforce init-once due to redundant callers - if (!document.body || document.body.classList.contains('vsc-initialized')) { - return; - } - document.body.classList.add('vsc-initialized'); - - if (document === window.document) { - defineVideoController(); - } else { - var link = document.createElement('link'); - link.href = chrome.runtime.getURL('inject.css'); - link.type = 'text/css'; - link.rel = 'stylesheet'; - document.head.appendChild(link); - } - var docs = Array(document) - try { - if (inIframe()) - docs.push(window.top.document); - } catch (e) { - } - - docs.forEach(function(doc) { - doc.addEventListener('keydown', function(event) { - var keyCode = event.keyCode; - - // Ignore if following modifier is active. - if (!event.getModifierState - || event.getModifierState("Alt") - || event.getModifierState("Control") - || event.getModifierState("Fn") - || event.getModifierState("Meta") - || event.getModifierState("Hyper") - || event.getModifierState("OS")) { - return; - } - - // Ignore keydown event if typing in an input box - if (event.target.nodeName === 'INPUT' - || event.target.nodeName === 'TEXTAREA' - || event.target.isContentEditable) { - return false; - } - - // Ignore keydown event if typing in a page without vsc - if (!getShadow(document.body).filter(x => x.tagName == 'vsc-controller')) { - return false; - } + // Ignore keydown event if typing in a page without vsc + if ( + !getShadow(document.body).filter(x => x.tagName == "vsc-controller") + ) { + return false; + } var item = tc.settings.keyBindings.find(item => item.key === keyCode); if (item) { runAction(item.action, document, item.value); - if (item.force === "true") {// disable websites key bindings + if (item.force === "true") { + // disable websites key bindings event.preventDefault(); event.stopPropagation(); } } - return false; - }, true); - }); + return false; + }, + true + ); + }); - - function checkForVideo(node, parent, added) { - // Only proceed with supposed removal if node is missing from DOM - if (!added && document.body.contains(node)) { - return; - } - if (node.nodeName === 'VIDEO' || (node.nodeName === 'AUDIO' && tc.settings.audioBoolean)) { - if (added) { - node.vsc = new tc.videoController(node, parent); - } else { - let id = node.dataset['vscid']; - if (id) { - node.vsc.remove(); - } - } - } else if (node.children != undefined) { - for (var i = 0; i < node.children.length; i++) { - const child = node.children[i]; - checkForVideo(child, child.parentNode || parent, added); - } + function checkForVideo(node, parent, added) { + // Only proceed with supposed removal if node is missing from DOM + if (!added && document.body.contains(node)) { + return; + } + if ( + node.nodeName === "VIDEO" || + (node.nodeName === "AUDIO" && tc.settings.audioBoolean) + ) { + if (added) { + node.vsc = new tc.videoController(node, parent); + } else { + let id = node.dataset["vscid"]; + if (id) { + node.vsc.remove(); } } + } else if (node.children != undefined) { + for (var i = 0; i < node.children.length; i++) { + const child = node.children[i]; + checkForVideo(child, child.parentNode || parent, added); + } + } + } - var observer = new MutationObserver(function(mutations) { - // Process the DOM nodes lazily - requestIdleCallback(_ => { - mutations.forEach(function(mutation) { - switch (mutation.type) { - case 'childList': - forEach.call(mutation.addedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, true); - }); - forEach.call(mutation.removedNodes, function(node) { - if (typeof node === "function") - return; - checkForVideo(node, node.parentNode || mutation.target, false); - }); - break; - case 'attributes': - if (mutation.target.attributes['aria-hidden'].value == "false") { - var flattenedNodes = getShadow(document.body) - var node = flattenedNodes.filter(x => x.tagName == 'VIDEO')[0] - if (node) { - var oldController = flattenedNodes.filter(x => x.classList.contains('vsc-controller'))[0] - if (oldController) { - oldController.remove() - } - checkForVideo(node, node.parentNode || mutation.target, true); + var observer = new MutationObserver(function(mutations) { + // Process the DOM nodes lazily + requestIdleCallback( + _ => { + mutations.forEach(function(mutation) { + switch (mutation.type) { + case "childList": + forEach.call(mutation.addedNodes, function(node) { + if (typeof node === "function") return; + checkForVideo(node, node.parentNode || mutation.target, true); + }); + forEach.call(mutation.removedNodes, function(node) { + if (typeof node === "function") return; + checkForVideo(node, node.parentNode || mutation.target, false); + }); + break; + case "attributes": + if (mutation.target.attributes["aria-hidden"].value == "false") { + var flattenedNodes = getShadow(document.body); + var node = flattenedNodes.filter(x => x.tagName == "VIDEO")[0]; + if (node) { + var oldController = flattenedNodes.filter(x => + x.classList.contains("vsc-controller") + )[0]; + if (oldController) { + oldController.remove(); } + checkForVideo(node, node.parentNode || mutation.target, true); } - break; - }; - }); - }, {timeout: 1000}); - }); - observer.observe(document, { - attributeFilter: ['aria-hidden'], - childList: true, - subtree: true - }); + } + break; + } + }); + }, + { timeout: 1000 } + ); + }); + observer.observe(document, { + attributeFilter: ["aria-hidden"], + childList: true, + subtree: true + }); - if (tc.settings.audioBoolean) { - var mediaTags = document.querySelectorAll('video,audio'); - } else { - var mediaTags = document.querySelectorAll('video'); - } - - forEach.call(mediaTags, function(video) { - video.vsc = new tc.videoController(video); - }); - - var frameTags = document.getElementsByTagName('iframe'); - forEach.call(frameTags, function(frame) { - // Ignore frames we don't have permission to access (different origin). - try { var childDocument = frame.contentDocument } catch (e) { return } - initializeWhenReady(childDocument); - }); + if (tc.settings.audioBoolean) { + var mediaTags = document.querySelectorAll("video,audio"); + } else { + var mediaTags = document.querySelectorAll("video"); } - function runAction(action, document, value, e) { - if (tc.settings.audioBoolean) { - var mediaTags = getShadow(document.body).filter(x => { - return x.tagName == 'AUDIO' || x.tagName == 'VIDEO' - }); - } else { - var mediaTags = getShadow(document.body).filter(x => x.tagName == 'VIDEO'); + forEach.call(mediaTags, function(video) { + video.vsc = new tc.videoController(video); + }); + + var frameTags = document.getElementsByTagName("iframe"); + forEach.call(frameTags, function(frame) { + // Ignore frames we don't have permission to access (different origin). + try { + var childDocument = frame.contentDocument; + } catch (e) { + return; } + initializeWhenReady(childDocument); + }); +} - mediaTags.forEach = Array.prototype.forEach; - - // Get the controller that was used if called from a button press event e - if (e) { - var targetController = e.target.getRootNode().host; - } - - mediaTags.forEach(function(v) { - var id = v.dataset['vscid']; - var controller = getController(id) - // Don't change video speed if the video has a different controller - if (e && !(targetController == controller)) { - return; - } - - // Controller may have been (force) removed by the site, guard to prevent crashes but run the command - if (controller) { - showController(controller); - } - - if (!v.classList.contains('vsc-cancelled')) { - if (action === 'rewind') { - v.currentTime -= value; - } else if (action === 'advance') { - v.currentTime += value; - } else if (action === 'faster') { - // Maximum playback speed in Chrome is set to 16: - // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=166 - var s = Math.min((v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + value, 16); - v.playbackRate = Number(s.toFixed(2)); - } else if (action === 'slower') { - // Video min rate is 0.0625: - // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=165 - var s = Math.max(v.playbackRate - value, 0.07); - v.playbackRate = Number(s.toFixed(2)); - } else if (action === 'reset') { - resetSpeed(v, 1.0); - } else if (action === 'display') { - controller.classList.add('vsc-manual'); - controller.classList.toggle('vsc-hidden'); - } else if (action === 'blink') { - // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. - if(controller.classList.contains('vsc-hidden') || controller.blinkTimeOut !== undefined){ - clearTimeout(controller.blinkTimeOut); - controller.classList.remove('vsc-hidden'); - controller.blinkTimeOut = setTimeout(()=>{ - controller.classList.add('vsc-hidden'); - controller.blinkTimeOut = undefined; - }, value ? value : 1000); - } - } else if (action === 'drag') { - handleDrag(v, controller, e); - } else if (action === 'fast') { - resetSpeed(v, value); - } else if (action === 'pause') { - pause(v); - } else if (action === 'muted') { - muted(v, value); - } else if (action === 'mark') { - setMark(v); - } else if (action === 'jump') { - jumpToMark(v); - } - } +function runAction(action, document, value, e) { + if (tc.settings.audioBoolean) { + var mediaTags = getShadow(document.body).filter(x => { + return x.tagName == "AUDIO" || x.tagName == "VIDEO"; }); + } else { + var mediaTags = getShadow(document.body).filter(x => x.tagName == "VIDEO"); } - function pause(v) { - if (v.paused) { - v.play(); - } else { - v.pause(); + mediaTags.forEach = Array.prototype.forEach; + + // Get the controller that was used if called from a button press event e + if (e) { + var targetController = e.target.getRootNode().host; + } + + mediaTags.forEach(function(v) { + var id = v.dataset["vscid"]; + var controller = getController(id); + // Don't change video speed if the video has a different controller + if (e && !(targetController == controller)) { + return; } - } - function resetSpeed(v, target) { - if (v.playbackRate === target) { - if (v.playbackRate === getKeyBindings("reset")) { // resetSpeed - if (target !== 1.0) { - v.playbackRate = 1.0; - } else { - v.playbackRate = getKeyBindings("fast"); // fastSpeed + // Controller may have been (force) removed by the site, guard to prevent crashes but run the command + if (controller) { + showController(controller); + } + + if (!v.classList.contains("vsc-cancelled")) { + if (action === "rewind") { + v.currentTime -= value; + } else if (action === "advance") { + v.currentTime += value; + } else if (action === "faster") { + // Maximum playback speed in Chrome is set to 16: + // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=166 + var s = Math.min( + (v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + value, + 16 + ); + v.playbackRate = Number(s.toFixed(2)); + } else if (action === "slower") { + // Video min rate is 0.0625: + // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=165 + var s = Math.max(v.playbackRate - value, 0.07); + v.playbackRate = Number(s.toFixed(2)); + } else if (action === "reset") { + resetSpeed(v, 1.0); + } else if (action === "display") { + controller.classList.add("vsc-manual"); + controller.classList.toggle("vsc-hidden"); + } else if (action === "blink") { + // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. + if ( + controller.classList.contains("vsc-hidden") || + controller.blinkTimeOut !== undefined + ) { + clearTimeout(controller.blinkTimeOut); + controller.classList.remove("vsc-hidden"); + controller.blinkTimeOut = setTimeout( + () => { + controller.classList.add("vsc-hidden"); + controller.blinkTimeOut = undefined; + }, + value ? value : 1000 + ); } + } else if (action === "drag") { + handleDrag(v, controller, e); + } else if (action === "fast") { + resetSpeed(v, value); + } else if (action === "pause") { + pause(v); + } else if (action === "muted") { + muted(v, value); + } else if (action === "mark") { + setMark(v); + } else if (action === "jump") { + jumpToMark(v); + } + } + }); +} + +function pause(v) { + if (v.paused) { + v.play(); + } else { + v.pause(); + } +} + +function resetSpeed(v, target) { + if (v.playbackRate === target) { + if (v.playbackRate === getKeyBindings("reset")) { + // resetSpeed + if (target !== 1.0) { + v.playbackRate = 1.0; } else { - v.playbackRate = getKeyBindings("reset"); // resetSpeed + v.playbackRate = getKeyBindings("fast"); // fastSpeed } } else { - setKeyBindings("reset", v.playbackRate); // resetSpeed - v.playbackRate = target; + v.playbackRate = getKeyBindings("reset"); // resetSpeed } + } else { + setKeyBindings("reset", v.playbackRate); // resetSpeed + v.playbackRate = target; + } +} + +function muted(v, value) { + v.muted = v.muted !== true; +} + +function setMark(v) { + v.vsc.mark = v.currentTime; +} + +function jumpToMark(v) { + if (v.vsc.mark && typeof v.vsc.mark === "number") { + v.currentTime = v.vsc.mark; + } +} + +function handleDrag(video, controller, e) { + const shadowController = controller.shadowRoot.querySelector("#controller"); + + // Find nearest parent of same size as video parent. + var parentElement = controller.parentElement; + while ( + parentElement.parentNode && + parentElement.parentNode.offsetHeight === parentElement.offsetHeight && + parentElement.parentNode.offsetWidth === parentElement.offsetWidth + ) { + parentElement = parentElement.parentNode; } - function muted(v, value) { - v.muted = v.muted !== true; - } + video.classList.add("vcs-dragging"); + shadowController.classList.add("dragging"); - function setMark(v) { - v.vsc.mark = v.currentTime; - } + const initialMouseXY = [e.clientX, e.clientY]; + const initialControllerXY = [ + parseInt(shadowController.style.left), + parseInt(shadowController.style.top) + ]; - function jumpToMark(v) { - if (v.vsc.mark && typeof v.vsc.mark === "number") { - v.currentTime = v.vsc.mark; - } - } + const startDragging = e => { + let style = shadowController.style; + let dx = e.clientX - initialMouseXY[0]; + let dy = e.clientY - initialMouseXY[1]; + style.left = initialControllerXY[0] + dx + "px"; + style.top = initialControllerXY[1] + dy + "px"; + }; - function handleDrag(video, controller, e) { - const shadowController = controller.shadowRoot.querySelector('#controller'); + const stopDragging = () => { + parentElement.removeEventListener("mousemove", startDragging); + parentElement.removeEventListener("mouseup", stopDragging); + parentElement.removeEventListener("mouseleave", stopDragging); - // Find nearest parent of same size as video parent. - var parentElement = controller.parentElement; - while (parentElement.parentNode && - parentElement.parentNode.offsetHeight === parentElement.offsetHeight && - parentElement.parentNode.offsetWidth === parentElement.offsetWidth) { - parentElement = parentElement.parentNode; - } + shadowController.classList.remove("dragging"); + video.classList.remove("vcs-dragging"); + }; - video.classList.add('vcs-dragging'); - shadowController.classList.add('dragging'); + parentElement.addEventListener("mouseup", stopDragging); + parentElement.addEventListener("mouseleave", stopDragging); + parentElement.addEventListener("mousemove", startDragging); +} - const initialMouseXY = [e.clientX, e.clientY]; - const initialControllerXY = [ - parseInt(shadowController.style.left), - parseInt(shadowController.style.top) - ]; +var timer; +var animation = false; +function showController(controller) { + controller.classList.add("vcs-show"); - const startDragging = (e) => { - let style = shadowController.style; - let dx = e.clientX - initialMouseXY[0]; - let dy = e.clientY -initialMouseXY[1]; - style.left = (initialControllerXY[0] + dx) + 'px'; - style.top = (initialControllerXY[1] + dy) + 'px'; - } + if (animation) clearTimeout(timer); - const stopDragging = () => { - parentElement.removeEventListener('mousemove', startDragging); - parentElement.removeEventListener('mouseup', stopDragging); - parentElement.removeEventListener('mouseleave', stopDragging); - - shadowController.classList.remove('dragging'); - video.classList.remove('vcs-dragging'); - } - - parentElement.addEventListener('mouseup',stopDragging); - parentElement.addEventListener('mouseleave',stopDragging); - parentElement.addEventListener('mousemove', startDragging); - } - - var timer; - var animation = false; - function showController(controller) { - controller.classList.add('vcs-show'); - - if (animation) - clearTimeout(timer); - - animation = true; - timer = setTimeout(function() { - controller.classList.remove('vcs-show'); - animation = false; - }, 2000); - } + animation = true; + timer = setTimeout(function() { + controller.classList.remove("vcs-show"); + animation = false; + }, 2000); +} diff --git a/manifest.json b/manifest.json index 0eaab53..b911445 100755 --- a/manifest.json +++ b/manifest.json @@ -10,7 +10,7 @@ "48": "icons/icon48.png", "128": "icons/icon128.png" }, - "permissions": [ "activeTab", "storage" ], + "permissions": ["activeTab", "storage"], "options_page": "options.html", "browser_action": { "default_icon": { @@ -20,9 +20,10 @@ }, "default_popup": "popup.html" }, - "content_scripts": [{ + "content_scripts": [ + { "all_frames": true, - "matches": [ "http://*/*", "https://*/*", "file:///*" ], + "matches": ["http://*/*", "https://*/*", "file:///*"], "match_about_blank": true, "exclude_matches": [ "https://plus.google.com/hangouts/*", @@ -31,11 +32,9 @@ "https://teamtreehouse.com/*", "http://www.hitbox.tv/*" ], - "css": [ "inject.css" ], - "js": [ "inject.js" ] + "css": ["inject.css"], + "js": ["inject.js"] } ], - "web_accessible_resources": [ - "inject.css", "shadow.css" - ] + "web_accessible_resources": ["inject.css", "shadow.css"] } diff --git a/options.css b/options.css index 352ca29..3e5541d 100644 --- a/options.css +++ b/options.css @@ -1,109 +1,112 @@ body { - margin: 0; - padding-left: 15px; - padding-top: 53px; - font-family: sans-serif; - font-size: 12px; - color: rgb(48, 57, 66); + margin: 0; + padding-left: 15px; + padding-top: 53px; + font-family: sans-serif; + font-size: 12px; + color: rgb(48, 57, 66); } -h1, h2, h3 { - font-weight: normal; - line-height: 1; - user-select: none; - cursor: default; +h1, +h2, +h3 { + font-weight: normal; + line-height: 1; + user-select: none; + cursor: default; } h1 { - font-size: 1.5em; - margin: 21px 0 13px; + font-size: 1.5em; + margin: 21px 0 13px; } h3 { - font-size: 1.2em; - margin-bottom: 0.8em; - color: black; + font-size: 1.2em; + margin-bottom: 0.8em; + color: black; } p { - margin: 0.65em 0; + margin: 0.65em 0; } header { - position: fixed; - top: 0; - left: 15px; - right: 0; - border-bottom: 1px solid #EEE; - background: linear-gradient(white, white 40%, rgba(255, 255, 255, 0.92)); + position: fixed; + top: 0; + left: 15px; + right: 0; + border-bottom: 1px solid #eee; + background: linear-gradient(white, white 40%, rgba(255, 255, 255, 0.92)); } -header, section { - min-width: 600px; - max-width: 738px; +header, +section { + min-width: 600px; + max-width: 738px; } section { - padding-left: 18px; - margin-top: 8px; - margin-bottom: 24px; + padding-left: 18px; + margin-top: 8px; + margin-bottom: 24px; } section h3 { - margin-left: -18px; + margin-left: -18px; } button { - -webkit-appearance: none; - position: relative; + -webkit-appearance: none; + position: relative; - margin: 0 1px 0 0; - padding: 0 10px; - min-width: 4em; - min-height: 2em; + margin: 0 1px 0 0; + padding: 0 10px; + min-width: 4em; + min-height: 2em; - background-image: linear-gradient(#EDEDED, #EDEDED 38%, #DEDEDE); - border: 1px solid rgba(0,0,0,0.25); - border-radius: 2px; - outline: none; - box-shadow: 0 1px 0 rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.75); - color: #444; - text-shadow: 0 1px 0 rgb(240,240,240); - font: inherit; + background-image: linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + outline: none; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + text-shadow: 0 1px 0 rgb(240, 240, 240); + font: inherit; - user-select: none; + user-select: none; } - input[type="text"] { - width: 75px; - text-align: center; + width: 75px; + text-align: center; } .row { - margin: 5px 0px; + margin: 5px 0px; } label { - display: inline-block; - width: 170px; - vertical-align: top; + display: inline-block; + width: 170px; + vertical-align: top; } #status { - color: #9D9D9D; - display: inline-block; - margin-left: 50px; + color: #9d9d9d; + display: inline-block; + margin-left: 50px; } #faq { - margin-top: 2em; + margin-top: 2em; } select { - width: 170px; + width: 170px; } .customForce { - display: none; - width: 250px; + display: none; + width: 250px; } .customKey { color: transparent; text-shadow: 0 0 0 #000000; -} \ No newline at end of file +} diff --git a/options.html b/options.html index 765809b..36c9f3c 100644 --- a/options.html +++ b/options.html @@ -1,119 +1,170 @@ - - Video Speed Controller: Options - - - - -
-

Video Speed Controller

-
+ + Video Speed Controller: Options + + + + +
+

Video Speed Controller

+

Shortcuts

-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
- +

Other

- +
- +
- +
- +
- +
-
@@ -129,21 +180,39 @@

The video controls are not showing up?

-

This extension is only compatible with HTML5 video. If you don't see the controls showing up, chances are you are viewing a Flash video. If you want to confirm, try right-clicking on the video and inspect the menu: if it mentions flash, then that's the issue. That said, most sites will fallback to HTML5 if they detect that Flash it not available. You can try manually disabling Flash plugin in Chrome:

+

+ This extension is only compatible with HTML5 video. If you don't see the + controls showing up, chances are you are viewing a Flash video. If you + want to confirm, try right-clicking on the video and inspect the menu: + if it mentions flash, then that's the issue. That said, + most sites will fallback to HTML5 if they detect that Flash it + not available. You can try manually disabling Flash plugin in Chrome: +

    -
  • In a new tab, navigate to chrome://settings/content/flash
  • +
  • + In a new tab, navigate to chrome://settings/content/flash +
  • Disable "Allow sites to run Flash"
  • Restart your browser and try playing your video again

The speed controls are not showing up for local videos?

-

To enable playback of local media (e.g. File > Open File), you need to grant additional permissions to the extension.

+

+ To enable playback of local media (e.g. File > Open File), you need + to grant additional permissions to the extension. +

  • In a new tab, navigate to chrome://extensions
  • -
  • Find "Video Speed Controller" extension in the list and enable "Allow access to file URLs"
  • -
  • Open a new tab and try opening a local file, the controls should show up
  • +
  • + Find "Video Speed Controller" extension in the list and enable "Allow + access to file URLs" +
  • +
  • + Open a new tab and try opening a local file, the controls should show + up +
diff --git a/options.js b/options.js index 8bc0719..721c83a 100644 --- a/options.js +++ b/options.js @@ -1,21 +1,21 @@ -var regStrip=/^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; +var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; var tcDefaults = { - speed: 1.0, // default: - displayKeyCode: 86, // default: V + speed: 1.0, // default: + displayKeyCode: 86, // default: V rememberSpeed: false, // default: false audioBoolean: false, // default: false - startHidden: false, // default: false - enabled: true, // default enabled + startHidden: false, // default: false + enabled: true, // default enabled controllerOpacity: 0.3, // default: 0.3 keyBindings: [ - {action: "display", key: 86, value: 0, force: false, predefined: true }, // V - {action: "slower", key: 83, value: 0.1, force: false, predefined: true}, // S - {action: "faster", key: 68, value: 0.1, force: false, predefined: true}, // D - {action: "rewind", key: 90, value: 10, force: false, predefined: true}, // Z - {action: "advance", key: 88, value: 10, force: false, predefined: true}, // X - {action: "reset", key: 82, value: 1, force: false, predefined: true}, // R - {action: "fast", key: 71, value: 1.8, force: false, predefined: true} // G + { action: "display", key: 86, value: 0, force: false, predefined: true }, // V + { action: "slower", key: 83, value: 0.1, force: false, predefined: true }, // S + { action: "faster", key: 68, value: 0.1, force: false, predefined: true }, // D + { action: "rewind", key: 90, value: 10, force: false, predefined: true }, // Z + { action: "advance", key: 88, value: 10, force: false, predefined: true }, // X + { action: "reset", key: 82, value: 1, force: false, predefined: true }, // R + { action: "fast", key: 71, value: 1.8, force: false, predefined: true } // G ], blacklist: ` www.instagram.com @@ -23,78 +23,81 @@ var tcDefaults = { vine.co imgur.com teams.microsoft.com - `.replace(regStrip, '') + `.replace(regStrip, "") }; var keyBindings = []; var keyCodeAliases = { - 0: 'null', - null: 'null', - undefined: 'null', - 32: 'Space', - 37: 'Left', - 38: 'Up', - 39: 'Right', - 40: 'Down', - 96: 'Num 0', - 97: 'Num 1', - 98: 'Num 2', - 99: 'Num 3', - 100: 'Num 4', - 101: 'Num 5', - 102: 'Num 6', - 103: 'Num 7', - 104: 'Num 8', - 105: 'Num 9', - 106: 'Num *', - 107: 'Num +', - 109: 'Num -', - 110: 'Num .', - 111: 'Num /', - 112: 'F1', - 113: 'F2', - 114: 'F3', - 115: 'F4', - 116: 'F5', - 117: 'F6', - 118: 'F7', - 119: 'F8', - 120: 'F9', - 121: 'F10', - 122: 'F11', - 123: 'F12', - 186: ';', - 188: '<', - 189: '-', - 187: '+', - 190: '>', - 191: '/', - 192: '~', - 219: '[', - 220: '\\', - 221: ']', - 222: '\'', -} + 0: "null", + null: "null", + undefined: "null", + 32: "Space", + 37: "Left", + 38: "Up", + 39: "Right", + 40: "Down", + 96: "Num 0", + 97: "Num 1", + 98: "Num 2", + 99: "Num 3", + 100: "Num 4", + 101: "Num 5", + 102: "Num 6", + 103: "Num 7", + 104: "Num 8", + 105: "Num 9", + 106: "Num *", + 107: "Num +", + 109: "Num -", + 110: "Num .", + 111: "Num /", + 112: "F1", + 113: "F2", + 114: "F3", + 115: "F4", + 116: "F5", + 117: "F6", + 118: "F7", + 119: "F8", + 120: "F9", + 121: "F10", + 122: "F11", + 123: "F12", + 186: ";", + 188: "<", + 189: "-", + 187: "+", + 190: ">", + 191: "/", + 192: "~", + 219: "[", + 220: "\\", + 221: "]", + 222: "'" +}; function recordKeyPress(e) { if ( - (e.keyCode >= 48 && e.keyCode <= 57) // Numbers 0-9 - || (e.keyCode >= 65 && e.keyCode <= 90) // Letters A-Z - || keyCodeAliases[e.keyCode] // Other character keys + (e.keyCode >= 48 && e.keyCode <= 57) || // Numbers 0-9 + (e.keyCode >= 65 && e.keyCode <= 90) || // Letters A-Z + keyCodeAliases[e.keyCode] // Other character keys ) { - e.target.value = keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode); + e.target.value = + keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode); e.target.keyCode = e.keyCode; e.preventDefault(); e.stopPropagation(); - } else if (e.keyCode === 8) { // Clear input when backspace pressed - e.target.value = ''; - } else if (e.keyCode === 27) { // When esc clicked, clear input - e.target.value = 'null'; + } else if (e.keyCode === 8) { + // Clear input when backspace pressed + e.target.value = ""; + } else if (e.keyCode === 27) { + // When esc clicked, clear input + e.target.value = "null"; e.target.keyCode = null; } -}; +} function inputFilterNumbersOnly(e) { var char = String.fromCharCode(e.keyCode); @@ -102,18 +105,20 @@ function inputFilterNumbersOnly(e) { e.preventDefault(); e.stopPropagation(); } -}; +} function inputFocus(e) { - e.target.value = ""; -}; + e.target.value = ""; +} function inputBlur(e) { - e.target.value = keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode); -}; + e.target.value = + keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode); +} function updateShortcutInputText(inputId, keyCode) { - document.getElementById(inputId).value = keyCodeAliases[keyCode] || String.fromCharCode(keyCode); + document.getElementById(inputId).value = + keyCodeAliases[keyCode] || String.fromCharCode(keyCode); document.getElementById(inputId).keyCode = keyCode; } @@ -123,7 +128,7 @@ function updateCustomShortcutInputText(inputItem, keyCode) { } // List of custom actions for which customValue should be disabled -var customActionsNoValues=["pause","muted","mark","jump","display"]; +var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"]; function add_shortcut() { var html = ` `; - var div = document.createElement('div'); - div.setAttribute('class', 'row customs'); + var div = document.createElement("div"); + div.setAttribute("class", "row customs"); div.innerHTML = html; var customs_element = document.getElementById("customs"); - customs_element.insertBefore(div, customs_element.children[customs_element.childElementCount - 1]); + customs_element.insertBefore( + div, + customs_element.children[customs_element.childElementCount - 1] + ); } function createKeyBindings(item) { @@ -158,28 +166,37 @@ function createKeyBindings(item) { const key = item.querySelector(".customKey").keyCode; const value = Number(item.querySelector(".customValue").value); const force = item.querySelector(".customForce").value; - const predefined = !!item.id;//item.id ? true : false; + const predefined = !!item.id; //item.id ? true : false; - keyBindings.push({action: action, key: key, value: value, force: force, predefined: predefined}); + keyBindings.push({ + action: action, + key: key, + value: value, + force: force, + predefined: predefined + }); } // Validates settings before saving function validate() { var valid = true; - var status = document.getElementById('status'); - document.getElementById('blacklist').value.split("\n").forEach(match => { - match = match.replace(regStrip,'') - if (match.startsWith('/')) { - try { - var regexp = new RegExp(match); - } catch(err) { - status.textContent = 'Error: Invalid Regex: ' + match - + '. Unable to save'; - valid = false; - return; + var status = document.getElementById("status"); + document + .getElementById("blacklist") + .value.split("\n") + .forEach(match => { + match = match.replace(regStrip, ""); + if (match.startsWith("/")) { + try { + var regexp = new RegExp(match); + } catch (err) { + status.textContent = + "Error: Invalid Regex: " + match + ". Unable to save"; + valid = false; + return; + } } - } - }) + }); return valid; } @@ -189,47 +206,70 @@ function save_options() { return; } keyBindings = []; - Array.from(document.querySelectorAll(".customs")).forEach(item => createKeyBindings(item)); // Remove added shortcuts + Array.from(document.querySelectorAll(".customs")).forEach(item => + createKeyBindings(item) + ); // Remove added shortcuts - var rememberSpeed = document.getElementById('rememberSpeed').checked; - var audioBoolean = document.getElementById('audioBoolean').checked; - var enabled = document.getElementById('enabled').checked; - var startHidden = document.getElementById('startHidden').checked; - var controllerOpacity = document.getElementById('controllerOpacity').value; - var blacklist = document.getElementById('blacklist').value; + var rememberSpeed = document.getElementById("rememberSpeed").checked; + var audioBoolean = document.getElementById("audioBoolean").checked; + var enabled = document.getElementById("enabled").checked; + var startHidden = document.getElementById("startHidden").checked; + var controllerOpacity = document.getElementById("controllerOpacity").value; + var blacklist = document.getElementById("blacklist").value; - chrome.storage.sync.remove(["resetSpeed", "speedStep", "fastSpeed", "rewindTime", "advanceTime", "resetKeyCode", "slowerKeyCode", "fasterKeyCode", "rewindKeyCode", "advanceKeyCode", "fastKeyCode"]); - chrome.storage.sync.set({ - rememberSpeed: rememberSpeed, - audioBoolean: audioBoolean, - enabled: enabled, - startHidden: startHidden, - controllerOpacity: controllerOpacity, - keyBindings: keyBindings, - blacklist: blacklist.replace(regStrip,'') - }, function() { - // Update status to let user know options were saved. - var status = document.getElementById('status'); - status.textContent = 'Options saved'; - setTimeout(function() { - status.textContent = ''; - }, 1000); - }); + chrome.storage.sync.remove([ + "resetSpeed", + "speedStep", + "fastSpeed", + "rewindTime", + "advanceTime", + "resetKeyCode", + "slowerKeyCode", + "fasterKeyCode", + "rewindKeyCode", + "advanceKeyCode", + "fastKeyCode" + ]); + chrome.storage.sync.set( + { + rememberSpeed: rememberSpeed, + audioBoolean: audioBoolean, + enabled: enabled, + startHidden: startHidden, + controllerOpacity: controllerOpacity, + keyBindings: keyBindings, + blacklist: blacklist.replace(regStrip, "") + }, + function() { + // Update status to let user know options were saved. + var status = document.getElementById("status"); + status.textContent = "Options saved"; + setTimeout(function() { + status.textContent = ""; + }, 1000); + } + ); } // Restores options from chrome.storage function restore_options() { chrome.storage.sync.get(tcDefaults, function(storage) { - document.getElementById('rememberSpeed').checked = storage.rememberSpeed; - document.getElementById('audioBoolean').checked = storage.audioBoolean; - document.getElementById('enabled').checked = storage.enabled; - document.getElementById('startHidden').checked = storage.startHidden; - document.getElementById('controllerOpacity').value = storage.controllerOpacity; - document.getElementById('blacklist').value = storage.blacklist; + document.getElementById("rememberSpeed").checked = storage.rememberSpeed; + document.getElementById("audioBoolean").checked = storage.audioBoolean; + document.getElementById("enabled").checked = storage.enabled; + document.getElementById("startHidden").checked = storage.startHidden; + document.getElementById("controllerOpacity").value = + storage.controllerOpacity; + document.getElementById("blacklist").value = storage.blacklist; // ensure that there is a "display" binding for upgrades from versions that had it as a separate binding - if(storage.keyBindings.filter(x => x.action == "display").length == 0){ - storage.keyBindings.push({ action: "display", value: 0, force: false, predefined: true }); + if (storage.keyBindings.filter(x => x.action == "display").length == 0) { + storage.keyBindings.push({ + action: "display", + value: 0, + force: false, + predefined: true + }); } for (let i in storage.keyBindings) { @@ -237,27 +277,36 @@ function restore_options() { if (item.predefined) { //do predefined ones because their value needed for overlay // document.querySelector("#" + item["action"] + " .customDo").value = item["action"]; - if (item["action"] == "display" && typeof (item["key"]) === "undefined"){ + if (item["action"] == "display" && typeof item["key"] === "undefined") { item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; // V } if (customActionsNoValues.includes(item["action"])) - document.querySelector("#" + item["action"] + " .customValue").disabled = true; + document.querySelector( + "#" + item["action"] + " .customValue" + ).disabled = true; - updateCustomShortcutInputText(document.querySelector("#" + item["action"] + " .customKey"), item["key"]); - document.querySelector("#" + item["action"] + " .customValue").value = item["value"]; - document.querySelector("#" + item["action"] + " .customForce").value = item["force"]; - } - else { + updateCustomShortcutInputText( + document.querySelector("#" + item["action"] + " .customKey"), + item["key"] + ); + document.querySelector("#" + item["action"] + " .customValue").value = + item["value"]; + document.querySelector("#" + item["action"] + " .customForce").value = + item["force"]; + } else { // new ones add_shortcut(); - const dom = document.querySelector(".customs:last-of-type") + const dom = document.querySelector(".customs:last-of-type"); dom.querySelector(".customDo").value = item["action"]; if (customActionsNoValues.includes(item["action"])) dom.querySelector(".customValue").disabled = true; - updateCustomShortcutInputText(dom.querySelector(".customKey"), item["key"]); + updateCustomShortcutInputText( + dom.querySelector(".customKey"), + item["key"] + ); dom.querySelector(".customValue").value = item["value"]; dom.querySelector(".customForce").value = item["force"]; } @@ -268,60 +317,68 @@ function restore_options() { function restore_defaults() { chrome.storage.sync.set(tcDefaults, function() { restore_options(); - document.querySelectorAll(".removeParent").forEach(button => button.click()); // Remove added shortcuts + document + .querySelectorAll(".removeParent") + .forEach(button => button.click()); // Remove added shortcuts // Update status to let user know options were saved. - var status = document.getElementById('status'); - status.textContent = 'Default options restored'; + var status = document.getElementById("status"); + status.textContent = "Default options restored"; setTimeout(function() { - status.textContent = ''; + status.textContent = ""; }, 1000); }); } function show_experimental() { - document.querySelectorAll(".customForce").forEach(item => item.style.display = 'inline-block'); + document + .querySelectorAll(".customForce") + .forEach(item => (item.style.display = "inline-block")); } -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener("DOMContentLoaded", function() { restore_options(); - document.getElementById('save').addEventListener('click', save_options); - document.getElementById('add').addEventListener('click', add_shortcut); - document.getElementById('restore').addEventListener('click', restore_defaults); - document.getElementById('experimental').addEventListener('click', show_experimental); + document.getElementById("save").addEventListener("click", save_options); + document.getElementById("add").addEventListener("click", add_shortcut); + document + .getElementById("restore") + .addEventListener("click", restore_defaults); + document + .getElementById("experimental") + .addEventListener("click", show_experimental); function eventCaller(event, className, funcName) { if (!event.target.classList.contains(className)) { - return + return; } funcName(event); } - document.addEventListener('keypress', (event) => { - eventCaller(event, "customValue", inputFilterNumbersOnly) + document.addEventListener("keypress", event => { + eventCaller(event, "customValue", inputFilterNumbersOnly); }); - document.addEventListener('focus', (event) => { - eventCaller(event, "customKey", inputFocus) + document.addEventListener("focus", event => { + eventCaller(event, "customKey", inputFocus); }); - document.addEventListener('blur', (event) => { - eventCaller(event, "customKey", inputBlur) + document.addEventListener("blur", event => { + eventCaller(event, "customKey", inputBlur); }); - document.addEventListener('keydown', (event) => { - eventCaller(event, "customKey", recordKeyPress) + document.addEventListener("keydown", event => { + eventCaller(event, "customKey", recordKeyPress); }); - document.addEventListener('click', (event) => { - eventCaller(event, "removeParent", function () { - event.target.parentNode.remove() - }) + document.addEventListener("click", event => { + eventCaller(event, "removeParent", function() { + event.target.parentNode.remove(); + }); }); - document.addEventListener('change', (event) => { - eventCaller(event, "customDo", function () { + document.addEventListener("change", event => { + eventCaller(event, "customDo", function() { if (customActionsNoValues.includes(event.target.value)) { event.target.nextElementSibling.nextElementSibling.disabled = true; event.target.nextElementSibling.nextElementSibling.value = 0; } else { event.target.nextElementSibling.nextElementSibling.disabled = false; } - }) + }); }); -}) +}); diff --git a/popup.css b/popup.css index f8b9a92..1787075 100644 --- a/popup.css +++ b/popup.css @@ -1,5 +1,5 @@ body { - min-width: 8em + min-width: 8em; } hr { @@ -12,13 +12,14 @@ hr { button { width: 100%; - background-image: linear-gradient(#EDEDED, #EDEDED 38%, #DEDEDE); - border: 1px solid rgba(0,0,0,0.25); + background-image: linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); border-radius: 2px; outline: none; - box-shadow: 0 1px 0 rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.75); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); color: #444; - text-shadow: 0 1px 0 rgb(240,240,240); + text-shadow: 0 1px 0 rgb(240, 240, 240); font: inherit; user-select: none; } diff --git a/popup.html b/popup.html index 51536d5..28354b9 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ - + Video Speed Controller: Popup diff --git a/popup.js b/popup.js index 1a0dfb4..a783635 100644 --- a/popup.js +++ b/popup.js @@ -1,44 +1,47 @@ -document.addEventListener('DOMContentLoaded', function () { - document.querySelector('#config').addEventListener('click', function() { +document.addEventListener("DOMContentLoaded", function() { + document.querySelector("#config").addEventListener("click", function() { window.open(chrome.runtime.getURL("options.html")); }); - - document.querySelector('#about').addEventListener('click', function() { + + document.querySelector("#about").addEventListener("click", function() { window.open("https://github.com/igrigorik/videospeed"); }); - document.querySelector('#feedback').addEventListener('click', function() { + document.querySelector("#feedback").addEventListener("click", function() { window.open("https://github.com/igrigorik/videospeed/issues"); }); - document.querySelector('#enable').addEventListener('click', function() { - toggleEnabled(true, settingsSavedReloadMessage); + document.querySelector("#enable").addEventListener("click", function() { + toggleEnabled(true, settingsSavedReloadMessage); }); - document.querySelector('#disable').addEventListener('click', function() { - toggleEnabled(false, settingsSavedReloadMessage); + document.querySelector("#disable").addEventListener("click", function() { + toggleEnabled(false, settingsSavedReloadMessage); }); - chrome.storage.sync.get({enabled: true}, function(storage) { + chrome.storage.sync.get({ enabled: true }, function(storage) { toggleEnabledUI(storage.enabled); }); - function toggleEnabled(enabled, callback){ - chrome.storage.sync.set({ - enabled: enabled, - }, function() { - toggleEnabledUI(enabled); - if(callback) callback(enabled); - }); + function toggleEnabled(enabled, callback) { + chrome.storage.sync.set( + { + enabled: enabled + }, + function() { + toggleEnabledUI(enabled); + if (callback) callback(enabled); + } + ); } - function toggleEnabledUI(enabled){ - document.querySelector('#enable').classList.toggle("hide", enabled); - document.querySelector('#disable').classList.toggle("hide", !enabled); + function toggleEnabledUI(enabled) { + document.querySelector("#enable").classList.toggle("hide", enabled); + document.querySelector("#disable").classList.toggle("hide", !enabled); - const suffix = `${(enabled ? "" : "_disabled")}.png` + const suffix = `${enabled ? "" : "_disabled"}.png`; chrome.browserAction.setIcon({ - "path": { + path: { "19": "icons/icon19" + suffix, "38": "icons/icon38" + suffix, "48": "icons/icon48" + suffix @@ -46,12 +49,14 @@ document.addEventListener('DOMContentLoaded', function () { }); } - function settingsSavedReloadMessage(enabled){ - setStatusMessage(`${enabled ? "Enabled" : "Disabled"}. Reload page to see changes`); + function settingsSavedReloadMessage(enabled) { + setStatusMessage( + `${enabled ? "Enabled" : "Disabled"}. Reload page to see changes` + ); } - function setStatusMessage(str){ - const status_element = document.querySelector('#status') + function setStatusMessage(str) { + const status_element = document.querySelector("#status"); status_element.classList.toggle("hide", false); status_element.innerText = str; } diff --git a/shadow.css b/shadow.css index 3b9a7e1..0766347 100644 --- a/shadow.css +++ b/shadow.css @@ -72,7 +72,7 @@ button:focus { } button:hover { - opacity: 1.0; + opacity: 1; } button:active { From eaf5ac47f308fb4a1fc608bc862de5e01885f128 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Sat, 15 Feb 2020 16:11:57 -0600 Subject: [PATCH 24/40] A few small fixes #586 (#591) * Trimmed trailing whitespace * Fixed erroneous newline at beginning of blacklist * Fixed erroneous instructions for adding remote in contribution guide (Fixes #586) --- .github/workflows/chrome-store-upload.yaml | 8 ++++---- CONTRIBUTING.md | 2 +- inject.js | 2 +- options.js | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/chrome-store-upload.yaml b/.github/workflows/chrome-store-upload.yaml index eb1adcf..2d909e8 100644 --- a/.github/workflows/chrome-store-upload.yaml +++ b/.github/workflows/chrome-store-upload.yaml @@ -1,6 +1,6 @@ name: Upload To Chrome Web Store # This workflow is triggered on pushes to the repository. -on: +on: push: tags: - v** @@ -11,13 +11,13 @@ jobs: steps: - name: checkout uses: actions/checkout@v1 - + - name: zip uses: montudor/action-zip@v0.1.0 with: args: zip -r ./vscd.zip . -i * -x *.git* - - - name: Upload to + + - name: Upload to uses: trmcnvn/chrome-addon@v1 with: # extension is only necessary when updating an existing addon, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87df582..ce54189 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,7 @@ pre-commit checks before committing via `pre-commit run --all-files` You should always be working with the latest version of the tool to make pull requests easy. If you want to do this easily, just add a second remote to your local git repo like this -`git push --set-upstream origin feature/578-prettier.io-formatting` +`git remote add upstream https://github.com/igrigorik/videospeed.git` Now any time you like to pull the latest version in to your local branch you can simply issue the command `git pull upstream master` diff --git a/inject.js b/inject.js index f1dda0f..fb13754 100644 --- a/inject.js +++ b/inject.js @@ -12,7 +12,7 @@ var tc = { startHidden: false, // default: false controllerOpacity: 0.3, // default: 0.3 keyBindings: [], - blacklist: ` + blacklist: `\ www.instagram.com twitter.com vine.co diff --git a/options.js b/options.js index 721c83a..865195f 100644 --- a/options.js +++ b/options.js @@ -17,7 +17,7 @@ var tcDefaults = { { action: "reset", key: 82, value: 1, force: false, predefined: true }, // R { action: "fast", key: 71, value: 1.8, force: false, predefined: true } // G ], - blacklist: ` + blacklist: `\ www.instagram.com twitter.com vine.co @@ -143,9 +143,9 @@ function add_shortcut() { - - - + + + +
+ + +
diff --git a/options.js b/options.js index 865195f..657b8ad 100644 --- a/options.js +++ b/options.js @@ -23,6 +23,11 @@ var tcDefaults = { vine.co imgur.com teams.microsoft.com + `.replace(regStrip, ""), + blacklistrc: `\ + twitch.tv + pluralsight.com + teamtreehouse.com `.replace(regStrip, "") }; @@ -191,7 +196,25 @@ function validate() { var regexp = new RegExp(match); } catch (err) { status.textContent = - "Error: Invalid Regex: " + match + ". Unable to save"; + "Error: Invalid blacklist regex: " + match + ". Unable to save"; + valid = false; + return; + } + } + }); + document + .getElementById("blacklistrc") + .value.split("\n") + .forEach(match => { + match = match.replace(regStrip, ""); + if (match.startsWith("/")) { + try { + var regexp = new RegExp(match); + } catch (err) { + status.textContent = + "Error: Invalid ratechange blacklist regex: " + + match + + ". Unable to save"; valid = false; return; } @@ -216,6 +239,7 @@ function save_options() { var startHidden = document.getElementById("startHidden").checked; var controllerOpacity = document.getElementById("controllerOpacity").value; var blacklist = document.getElementById("blacklist").value; + var blacklistrc = document.getElementById("blacklistrc").value; chrome.storage.sync.remove([ "resetSpeed", @@ -238,7 +262,8 @@ function save_options() { startHidden: startHidden, controllerOpacity: controllerOpacity, keyBindings: keyBindings, - blacklist: blacklist.replace(regStrip, "") + blacklist: blacklist.replace(regStrip, ""), + blacklistrc: blacklistrc.replace(regStrip, "") }, function() { // Update status to let user know options were saved. @@ -261,6 +286,7 @@ function restore_options() { document.getElementById("controllerOpacity").value = storage.controllerOpacity; document.getElementById("blacklist").value = storage.blacklist; + document.getElementById("blacklistrc").value = storage.blacklistrc; // ensure that there is a "display" binding for upgrades from versions that had it as a separate binding if (storage.keyBindings.filter(x => x.action == "display").length == 0) { From dde52368a23cc7c4fb429acf63b31b44faf76ba7 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Tue, 3 Mar 2020 22:33:19 -0600 Subject: [PATCH 29/40] Far better fix now utilizes a short cooldown window after issuing a speed change command --- inject.js | 58 ++++++++++++++++++---------------------------------- options.html | 16 --------------- options.js | 28 +------------------------ 3 files changed, 21 insertions(+), 81 deletions(-) diff --git a/inject.js b/inject.js index 04816af..be775b6 100644 --- a/inject.js +++ b/inject.js @@ -18,11 +18,6 @@ var tc = { vine.co imgur.com teams.microsoft.com - `.replace(regStrip, ""), - blacklistrc: `\ - twitch.tv - pluralsight.com - teamtreehouse.com `.replace(regStrip, "") } }; @@ -85,8 +80,7 @@ chrome.storage.sync.get(tc.settings, function(storage) { startHidden: tc.settings.startHidden, enabled: tc.settings.enabled, controllerOpacity: tc.settings.controllerOpacity, - blacklist: tc.settings.blacklist.replace(regStrip, ""), - blacklistrc: tc.settings.blacklistrc.replace(regStrip, "") + blacklist: tc.settings.blacklist.replace(regStrip, "") }); } tc.settings.lastSpeed = Number(storage.lastSpeed); @@ -97,7 +91,6 @@ chrome.storage.sync.get(tc.settings, function(storage) { tc.settings.startHidden = Boolean(storage.startHidden); tc.settings.controllerOpacity = Number(storage.controllerOpacity); tc.settings.blacklist = String(storage.blacklist); - tc.settings.blacklistrc = String(storage.blacklistrc); // ensure that there is a "display" binding (for upgrades from versions that had it as a separate binding) if (tc.settings.keyBindings.filter(x => x.action == "display").length == 0) { @@ -344,43 +337,31 @@ function isBlacklisted() { return blacklisted; } -function isRateChangeBlocked() { - blockRateChange = false; - tc.settings.blacklistrc.split("\n").forEach(match => { - match = match.replace(regStrip, ""); - if (match.length == 0) { - return; - } - if (match.startsWith("/")) { - try { - var regexp = new RegExp(match); - } catch (err) { - return; - } - } else { - var regexp = new RegExp(escapeStringRegExp(match)); - } - if (regexp.test(location.href)) { - blockRateChange = true; - return; - } - }); - return blockRateChange; +var coolDown = false; +function refreshCoolDown() { + if (coolDown) { + clearTimeout(coolDown); + } + coolDown = setTimeout(function() { + coolDown = false; + }, 1000); } function initializeWhenReady(document) { if (isBlacklisted()) { return; } - if (isRateChangeBlocked()) { - document.body.addEventListener( - "ratechange", - function(event) { + document.body.addEventListener( + "ratechange", + function(event) { + if (coolDown) { + refreshCoolDown(); + console.log("Speed event propagation blocked"); event.stopImmediatePropagation(); - }, - true - ); - } + } + }, + true + ); window.onload = () => { initializeNow(window.document); }; @@ -602,6 +583,7 @@ function setSpeed(controller, video, speed) { video.playbackRate = Number(speedvalue); var speedIndicator = controller.shadowRoot.querySelector("span"); speedIndicator.textContent = speedvalue; + refreshCoolDown(); } function runAction(action, document, value, e) { diff --git a/options.html b/options.html index 2ab49bc..eb979bb 100644 --- a/options.html +++ b/options.html @@ -168,22 +168,6 @@ -
- - -
diff --git a/options.js b/options.js index 657b8ad..cc1acab 100644 --- a/options.js +++ b/options.js @@ -23,11 +23,6 @@ var tcDefaults = { vine.co imgur.com teams.microsoft.com - `.replace(regStrip, ""), - blacklistrc: `\ - twitch.tv - pluralsight.com - teamtreehouse.com `.replace(regStrip, "") }; @@ -202,24 +197,6 @@ function validate() { } } }); - document - .getElementById("blacklistrc") - .value.split("\n") - .forEach(match => { - match = match.replace(regStrip, ""); - if (match.startsWith("/")) { - try { - var regexp = new RegExp(match); - } catch (err) { - status.textContent = - "Error: Invalid ratechange blacklist regex: " + - match + - ". Unable to save"; - valid = false; - return; - } - } - }); return valid; } @@ -239,7 +216,6 @@ function save_options() { var startHidden = document.getElementById("startHidden").checked; var controllerOpacity = document.getElementById("controllerOpacity").value; var blacklist = document.getElementById("blacklist").value; - var blacklistrc = document.getElementById("blacklistrc").value; chrome.storage.sync.remove([ "resetSpeed", @@ -262,8 +238,7 @@ function save_options() { startHidden: startHidden, controllerOpacity: controllerOpacity, keyBindings: keyBindings, - blacklist: blacklist.replace(regStrip, ""), - blacklistrc: blacklistrc.replace(regStrip, "") + blacklist: blacklist.replace(regStrip, "") }, function() { // Update status to let user know options were saved. @@ -286,7 +261,6 @@ function restore_options() { document.getElementById("controllerOpacity").value = storage.controllerOpacity; document.getElementById("blacklist").value = storage.blacklist; - document.getElementById("blacklistrc").value = storage.blacklistrc; // ensure that there is a "display" binding for upgrades from versions that had it as a separate binding if (storage.keyBindings.filter(x => x.action == "display").length == 0) { From 00781ceaa3124089fbababc1548376561fd614b1 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Thu, 5 Mar 2020 19:08:30 -0600 Subject: [PATCH 30/40] Moved logic in rate change event listener into its own function to be triggered by setSpeed() --- inject.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/inject.js b/inject.js index be775b6..e1d989e 100644 --- a/inject.js +++ b/inject.js @@ -171,16 +171,7 @@ function defineVideoController() { // Ignore ratechange events on unitialized videos. // 0 == No information is available about the media resource. if (event.target.readyState > 0) { - var speed = this.getSpeed(); - this.speedIndicator.textContent = speed; - tc.settings.speeds[this.video.currentSrc] = speed; - tc.settings.lastSpeed = speed; - this.speed = speed; - chrome.storage.sync.set({ lastSpeed: speed }, function() { - console.log("Speed setting saved: " + speed); - }); - // show the controller for 1000ms if it's hidden. - runAction("blink", document, null, null); + rateChanged(this); } }.bind(this)) ); @@ -586,6 +577,19 @@ function setSpeed(controller, video, speed) { refreshCoolDown(); } +function rateChanged(controller) { + var speed = parseFloat(controller.video.playbackRate).toFixed(2); + controller.speedIndicator.textContent = speed; + tc.settings.speeds[controller.video.currentSrc] = speed; + tc.settings.lastSpeed = speed; + controller.speed = speed; + chrome.storage.sync.set({ lastSpeed: speed }, function() { + console.log("Speed setting saved: " + speed); + }); + // show the controller for 1000ms if it's hidden. + runAction("blink", document, null, null); +} + function runAction(action, document, value, e) { if (tc.settings.audioBoolean) { var mediaTags = getShadow(document.body).filter(x => { From 588a04fe581bc1b9f27a9e831c20317bfd460a05 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 6 Mar 2020 18:26:04 -0600 Subject: [PATCH 31/40] Added node_modules to .gitignore - We may or may not use later, but no harm in adding --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 03c6b3a..4686867 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ local # IntelliJ IDEA .idea/ +node_modules From 73d5673031d4c2edf3915bcf041f290931fe521c Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 6 Mar 2020 19:59:04 -0600 Subject: [PATCH 32/40] Fixed issue preventing last speed from being properly stored --- inject.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/inject.js b/inject.js index e1d989e..6ec1154 100644 --- a/inject.js +++ b/inject.js @@ -171,7 +171,7 @@ function defineVideoController() { // Ignore ratechange events on unitialized videos. // 0 == No information is available about the media resource. if (event.target.readyState > 0) { - rateChanged(this); + rateChanged(this.div); } }.bind(this)) ); @@ -346,7 +346,6 @@ function initializeWhenReady(document) { "ratechange", function(event) { if (coolDown) { - refreshCoolDown(); console.log("Speed event propagation blocked"); event.stopImmediatePropagation(); } @@ -572,17 +571,19 @@ function initializeNow(document) { function setSpeed(controller, video, speed) { var speedvalue = speed.toFixed(2); video.playbackRate = Number(speedvalue); - var speedIndicator = controller.shadowRoot.querySelector("span"); - speedIndicator.textContent = speedvalue; refreshCoolDown(); + rateChanged(controller); } function rateChanged(controller) { - var speed = parseFloat(controller.video.playbackRate).toFixed(2); - controller.speedIndicator.textContent = speed; - tc.settings.speeds[controller.video.currentSrc] = speed; + var speedIndicator = controller.shadowRoot.querySelector("span"); + var video = controller.parentElement.querySelector("video"); + var src = video.currentSrc; + var speed = video.playbackRate.toFixed(2); + + speedIndicator.textContent = speed; + tc.settings.speeds[src] = speed; tc.settings.lastSpeed = speed; - controller.speed = speed; chrome.storage.sync.set({ lastSpeed: speed }, function() { console.log("Speed setting saved: " + speed); }); From ff3ea3113bf784abe08196c32db8809ca8337b4a Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 6 Mar 2020 22:44:01 -0600 Subject: [PATCH 33/40] Added logging function and logging entries Moved all on ratechange logic to document level listener Removed dead .getSpeed method Fixed bug causing controller to sometimes initialize with NaN --- inject.js | 148 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 39 deletions(-) diff --git a/inject.js b/inject.js index 6ec1154..3c7c308 100644 --- a/inject.js +++ b/inject.js @@ -18,10 +18,41 @@ var tc = { vine.co imgur.com teams.microsoft.com - `.replace(regStrip, "") + `.replace(regStrip, ""), + defaultLogLevel: 4, + logLevel: 3 } }; +/* Log levels (depends on caller specifying the correct level) + 1 - none + 2 - error + 3 - warning + 4 - info + 5 - debug + 6 - debug high verbosity + stack trace on each message +*/ +function log(message, level) { + verbosity = tc.settings.logLevel; + if (typeof level === "undefined") { + level = tc.settings.defaultLogLevel; + } + if (verbosity >= level) { + if (level === 2) { + console.log("ERROR:" + message); + } else if (level === 3) { + console.log("WARNING:" + message); + } else if (level === 4) { + console.log("INFO:" + message); + } else if (level === 5) { + console.log("DEBUG:" + message); + } else if (level === 6) { + console.log("DEBUG (VERBOSE):" + message); + console.trace(); + } + } +} + chrome.storage.sync.get(tc.settings, function(storage) { tc.settings.keyBindings = storage.keyBindings; // Array if (storage.keyBindings.length == 0) { @@ -132,47 +163,54 @@ function defineVideoController() { this.id = Math.random() .toString(36) .substr(2, 9); - - // settings.speeds[] ensures that same source used across video tags (e.g. fullscreen on YT) retains speed setting - // this.speed is a controller level variable that retains speed setting across source switches (e.g. video quality, playlist change) - this.speed = 1.0; - + storedSpeed = tc.settings.speeds[target.currentSrc]; if (!tc.settings.rememberSpeed) { - if (!tc.settings.speeds[target.currentSrc]) { - tc.settings.speeds[target.currentSrc] = this.speed; + if (!storedSpeed) { + log( + "Overwriting stored speed to 1.0 due to rememberSpeed being disabled", + 5 + ); + storedSpeed = 1.0; } setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed } else { - tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; + log("Recalling stored speed due to rememberSpeed being enabled", 5); + storedSpeed = tc.settings.lastSpeed; } - target.playbackRate = tc.settings.speeds[target.currentSrc]; + log("Explicitly setting playbackRate to: " + storedSpeed, 5); + target.playbackRate = storedSpeed; this.div = this.initializeControls(); target.addEventListener( "play", (this.handlePlay = function(event) { + storedSpeed = tc.settings.speeds[event.target.currentSrc]; if (!tc.settings.rememberSpeed) { - if (!tc.settings.speeds[target.currentSrc]) { - tc.settings.speeds[target.currentSrc] = this.speed; + if (!storedSpeed) { + log( + "Overwriting stored speed to 1.0 (rememberSpeed not enabled)", + 4 + ); + storedSpeed = 1.0; } + // resetSpeed isn't really a reset, it's a toggle + log("Setting reset keybinding to fast", 5); setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed } else { - tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; - } - target.playbackRate = tc.settings.speeds[target.currentSrc]; - }.bind(this)) - ); - - target.addEventListener( - "ratechange", - (this.handleRatechange = function(event) { - // Ignore ratechange events on unitialized videos. - // 0 == No information is available about the media resource. - if (event.target.readyState > 0) { - rateChanged(this.div); + log( + "Storing lastSpeed into tc.settings.speeds (rememberSpeed enabled)", + 5 + ); + storedSpeed = tc.settings.lastSpeed; } + // TODO: Check if explicitly setting the playback rate to 1.0 is + // necessary when rememberSpeed is disabled (this may accidentally + // override a website's intentional initial speed setting interfering + // with the site's default behavior) + log("Explicitly setting playbackRate to: " + storedSpeed, 4); + event.target.playbackRate = storedSpeed; }.bind(this)) ); @@ -200,26 +238,22 @@ function defineVideoController() { }); }; - tc.videoController.prototype.getSpeed = function() { - return parseFloat(this.video.playbackRate).toFixed(2); - }; - tc.videoController.prototype.remove = function() { this.div.remove(); this.video.removeEventListener("play", this.handlePlay); - this.video.removeEventListener("ratechange", this.handleRatechange); delete this.video.dataset["vscid"]; delete this.video.vsc; }; tc.videoController.prototype.initializeControls = function() { + log("initializeControls Begin", 5); var document = this.document; - var speed = parseFloat(tc.settings.speeds[this.video.currentSrc]).toFixed( - 2 - ), + var speed = this.video.playbackRate.toFixed(2), top = Math.max(this.video.offsetTop, 0) + "px", left = Math.max(this.video.offsetLeft, 0) + "px"; + log("Speed variable set to: " + speed, 5); + var wrapper = document.createElement("div"); wrapper.classList.add("vsc-controller"); wrapper.dataset["vscid"] = this.id; @@ -330,23 +364,28 @@ function isBlacklisted() { var coolDown = false; function refreshCoolDown() { + log("Begin refreshCoolDown", 5); if (coolDown) { clearTimeout(coolDown); } coolDown = setTimeout(function() { coolDown = false; }, 1000); + log("End refreshCoolDown", 5); } function initializeWhenReady(document) { + log("Begin initializeWhenReady", 5); if (isBlacklisted()) { return; } document.body.addEventListener( "ratechange", function(event) { + controller = event.target.parentElement.querySelector(".vsc-controller"); + rateChanged(controller); if (coolDown) { - console.log("Speed event propagation blocked"); + log("Speed event propagation blocked", 4); event.stopImmediatePropagation(); } }, @@ -366,6 +405,7 @@ function initializeWhenReady(document) { }; } } + log("End initializeWhenReady", 5); } function inIframe() { try { @@ -403,12 +443,14 @@ function getController(id) { } function initializeNow(document) { + log("Begin initializeNow", 5); if (!tc.settings.enabled) return; // enforce init-once due to redundant callers if (!document.body || document.body.classList.contains("vsc-initialized")) { return; } document.body.classList.add("vsc-initialized"); + log("initializeNow: vsc-initialized added to document body", 5); if (document === window.document) { defineVideoController(); @@ -429,6 +471,7 @@ function initializeNow(document) { "keydown", function(event) { var keyCode = event.keyCode; + log("Processing keydown event: " + keyCode, 6); // Ignore if following modifier is active. if ( @@ -440,6 +483,7 @@ function initializeNow(document) { event.getModifierState("Hyper") || event.getModifierState("OS") ) { + log("Keydown event ignored due to active modifier: " + keyCode, 5); return; } @@ -566,13 +610,16 @@ function initializeNow(document) { } initializeWhenReady(childDocument); }); + log("End initializeNow", 5); } function setSpeed(controller, video, speed) { + log("setSpeed started: " + speed, 5); var speedvalue = speed.toFixed(2); video.playbackRate = Number(speedvalue); refreshCoolDown(); rateChanged(controller); + log("setSpeed finished: " + speed, 5); } function rateChanged(controller) { @@ -581,17 +628,23 @@ function rateChanged(controller) { var src = video.currentSrc; var speed = video.playbackRate.toFixed(2); + log("Playback rate changed to " + speed, 4); + + log("Updating controller with new speed", 5); speedIndicator.textContent = speed; tc.settings.speeds[src] = speed; + log("Storing lastSpeed in settings for the rememberSpeed feature", 5); tc.settings.lastSpeed = speed; + log("Syncing chrome settings for lastSpeed", 5); chrome.storage.sync.set({ lastSpeed: speed }, function() { - console.log("Speed setting saved: " + speed); + log("Speed setting saved: " + speed, 5); }); // show the controller for 1000ms if it's hidden. runAction("blink", document, null, null); } function runAction(action, document, value, e) { + log("runAction Begin", 5); if (tc.settings.audioBoolean) { var mediaTags = getShadow(document.body).filter(x => { return x.tagName == "AUDIO" || x.tagName == "VIDEO"; @@ -622,10 +675,13 @@ function runAction(action, document, value, e) { if (!v.classList.contains("vsc-cancelled")) { if (action === "rewind") { + log("Rewind", 5); v.currentTime -= value; } else if (action === "advance") { + log("Fast forward", 5); v.currentTime += value; } else if (action === "faster") { + log("Increase speed", 5); // Maximum playback speed in Chrome is set to 16: // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=166 var s = Math.min( @@ -634,16 +690,20 @@ function runAction(action, document, value, e) { ); setSpeed(controller, v, s); } else if (action === "slower") { + log("Decrease speed", 5); // Video min rate is 0.0625: // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=165 var s = Math.max(v.playbackRate - value, 0.07); setSpeed(controller, v, s); } else if (action === "reset") { + log("Reset speed", 5); resetSpeed(v, controller, 1.0); } else if (action === "display") { + log("Showing controller", 5); controller.classList.add("vsc-manual"); controller.classList.toggle("vsc-hidden"); } else if (action === "blink") { + log("Showing controller momentarily", 5); // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. if ( controller.classList.contains("vsc-hidden") || @@ -674,12 +734,15 @@ function runAction(action, document, value, e) { } } }); + log("runAction End", 5); } function pause(v) { if (v.paused) { + log("Resuming video", 5); v.play(); } else { + log("Pausing video", 5); v.pause(); } } @@ -687,17 +750,20 @@ function pause(v) { function resetSpeed(v, controller, target) { if (v.playbackRate === target) { if (v.playbackRate === getKeyBindings("reset")) { - // resetSpeed if (target !== 1.0) { + log("Resetting playback speed to 1.0", 4); setSpeed(controller, v, 1.0); } else { - setSpeed(controller, v, getKeyBindings("fast")); // fastSpeed + log('Toggling playback speed to "fast" speed', 4); + setSpeed(controller, v, getKeyBindings("fast")); } } else { - setSpeed(controller, v, getKeyBindings("reset")); // resetSpeed + log('Toggling playback speed to "reset" speed', 4); + setSpeed(controller, v, getKeyBindings("reset")); } } else { - setKeyBindings("reset", v.playbackRate); // resetSpeed + log('Toggling playback speed to "reset" speed', 4); + setKeyBindings("reset", v.playbackRate); setSpeed(controller, v, target); } } @@ -707,10 +773,12 @@ function muted(v, value) { } function setMark(v) { + log("Adding marker", 5); v.vsc.mark = v.currentTime; } function jumpToMark(v) { + log("Recalling marker", 5); if (v.vsc.mark && typeof v.vsc.mark === "number") { v.currentTime = v.vsc.mark; } @@ -763,6 +831,7 @@ function handleDrag(video, controller, e) { var timer; var animation = false; function showController(controller) { + log("Showing controller", 4); controller.classList.add("vcs-show"); if (animation) clearTimeout(timer); @@ -771,5 +840,6 @@ function showController(controller) { timer = setTimeout(function() { controller.classList.remove("vcs-show"); animation = false; + log("Hiding controller", 5); }, 2000); } From c32eec1bb2e25c01cbc209d0380beab6cbe7029b Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Sat, 7 Mar 2020 18:23:55 -0600 Subject: [PATCH 34/40] Moved ratechange listener into its own function Placed call for adding listener after call to document ready/disabled check Placed setupListener within try/catch block --- inject.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/inject.js b/inject.js index 3c7c308..15ecb5f 100644 --- a/inject.js +++ b/inject.js @@ -374,11 +374,7 @@ function refreshCoolDown() { log("End refreshCoolDown", 5); } -function initializeWhenReady(document) { - log("Begin initializeWhenReady", 5); - if (isBlacklisted()) { - return; - } +function setupListener() { document.body.addEventListener( "ratechange", function(event) { @@ -391,6 +387,13 @@ function initializeWhenReady(document) { }, true ); +} + +function initializeWhenReady(document) { + log("Begin initializeWhenReady", 5); + if (isBlacklisted()) { + return; + } window.onload = () => { initializeNow(window.document); }; @@ -449,6 +452,11 @@ function initializeNow(document) { if (!document.body || document.body.classList.contains("vsc-initialized")) { return; } + try { + setupListener(); + } catch { + // no operation + } document.body.classList.add("vsc-initialized"); log("initializeNow: vsc-initialized added to document body", 5); From b4f6bcc292583a8658131578c328ef36519d7b96 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Sun, 8 Mar 2020 19:19:56 -0500 Subject: [PATCH 35/40] Removed redundant rateChanged() function --- inject.js | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/inject.js b/inject.js index 15ecb5f..fd7d166 100644 --- a/inject.js +++ b/inject.js @@ -378,12 +378,31 @@ function setupListener() { document.body.addEventListener( "ratechange", function(event) { - controller = event.target.parentElement.querySelector(".vsc-controller"); - rateChanged(controller); if (coolDown) { log("Speed event propagation blocked", 4); event.stopImmediatePropagation(); } + var controller = event.target.parentElement.querySelector( + ".vsc-controller" + ); + var speedIndicator = controller.shadowRoot.querySelector("span"); + var video = controller.parentElement.querySelector("video"); + var src = video.currentSrc; + var speed = video.playbackRate.toFixed(2); + + log("Playback rate changed to " + speed, 4); + + log("Updating controller with new speed", 5); + speedIndicator.textContent = speed; + tc.settings.speeds[src] = speed; + log("Storing lastSpeed in settings for the rememberSpeed feature", 5); + tc.settings.lastSpeed = speed; + log("Syncing chrome settings for lastSpeed", 5); + chrome.storage.sync.set({ lastSpeed: speed }, function() { + log("Speed setting saved: " + speed, 5); + }); + // show the controller for 1000ms if it's hidden. + runAction("blink", document, null, null); }, true ); @@ -626,31 +645,9 @@ function setSpeed(controller, video, speed) { var speedvalue = speed.toFixed(2); video.playbackRate = Number(speedvalue); refreshCoolDown(); - rateChanged(controller); log("setSpeed finished: " + speed, 5); } -function rateChanged(controller) { - var speedIndicator = controller.shadowRoot.querySelector("span"); - var video = controller.parentElement.querySelector("video"); - var src = video.currentSrc; - var speed = video.playbackRate.toFixed(2); - - log("Playback rate changed to " + speed, 4); - - log("Updating controller with new speed", 5); - speedIndicator.textContent = speed; - tc.settings.speeds[src] = speed; - log("Storing lastSpeed in settings for the rememberSpeed feature", 5); - tc.settings.lastSpeed = speed; - log("Syncing chrome settings for lastSpeed", 5); - chrome.storage.sync.set({ lastSpeed: speed }, function() { - log("Speed setting saved: " + speed, 5); - }); - // show the controller for 1000ms if it's hidden. - runAction("blink", document, null, null); -} - function runAction(action, document, value, e) { log("runAction Begin", 5); if (tc.settings.audioBoolean) { From 884be009a45b80a2ea95ac28391c4e2b53e220bd Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Wed, 25 Mar 2020 22:12:36 -0700 Subject: [PATCH 36/40] update chrome action --- .github/workflows/chrome-store-upload.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/chrome-store-upload.yaml b/.github/workflows/chrome-store-upload.yaml index 2d909e8..e1dfc68 100644 --- a/.github/workflows/chrome-store-upload.yaml +++ b/.github/workflows/chrome-store-upload.yaml @@ -18,7 +18,7 @@ jobs: args: zip -r ./vscd.zip . -i * -x *.git* - name: Upload to - uses: trmcnvn/chrome-addon@v1 + uses: trmcnvn/chrome-addon@v2 with: # extension is only necessary when updating an existing addon, # omitting it will create a new addon From 66cd77f572e6a27eabd1b3539c87b32533c28ab9 Mon Sep 17 00:00:00 2001 From: Gitoffthelawn Date: Sat, 28 Mar 2020 02:09:28 -0700 Subject: [PATCH 37/40] Manifest Improvements 1) Removed deprecated URL. a. That old hitbox.tv URL does redirect somewhere else now. Does that site need to be excluded? b. Why are sites excluded here vs. being added to the default exclusions list in Options? 2) Shortened description and added "audio" to reflect new functionality 3) The Firefox fork of this extension benefits from adding: "options_ui": { "page": "options.html" } Would it hurt to add that to this manifest too? --- manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index f489342..7ff40a2 100755 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "short_name": "videospeed", "version": "0.6.1", "manifest_version": 2, - "description": "Speed up, slow down, advance and rewind any HTML5 video with quick shortcuts.", + "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts", "homepage_url": "https://github.com/igrigorik/videospeed", "icons": { "16": "icons/icon16.png", @@ -28,8 +28,7 @@ "exclude_matches": [ "https://plus.google.com/hangouts/*", "https://hangouts.google.com/*", - "https://meet.google.com/*", - "http://www.hitbox.tv/*" + "https://meet.google.com/*" ], "css": ["inject.css"], "js": ["inject.js"] From 92c004f1dd8ff2b1a0745c2d17d8d67fb62a6736 Mon Sep 17 00:00:00 2001 From: Gitoffthelawn Date: Sat, 28 Mar 2020 02:18:47 -0700 Subject: [PATCH 38/40] Options JavaScript Improvements 1. Remove blank line in exceptions list 2. Remove deprecated website (vine.co) 3. Grammatical consistency --- options.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/options.js b/options.js index cc1acab..711b04c 100644 --- a/options.js +++ b/options.js @@ -17,10 +17,9 @@ var tcDefaults = { { action: "reset", key: 82, value: 1, force: false, predefined: true }, // R { action: "fast", key: 71, value: 1.8, force: false, predefined: true } // G ], - blacklist: `\ - www.instagram.com + blacklist: + `www.instagram.com twitter.com - vine.co imgur.com teams.microsoft.com `.replace(regStrip, "") @@ -148,7 +147,7 @@ function add_shortcut() { `; var div = document.createElement("div"); From c6fbdc70d4f2c736deea1c47c51535904fd2f67a Mon Sep 17 00:00:00 2001 From: Gitoffthelawn Date: Sat, 28 Mar 2020 02:32:32 -0700 Subject: [PATCH 39/40] Update options.html Thank you for the great extension. Here are some improvements to the Options page. --- options.html | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/options.html b/options.html index eb979bb..0c57b64 100644 --- a/options.html +++ b/options.html @@ -25,7 +25,7 @@
@@ -41,7 +41,7 @@
@@ -57,7 +57,7 @@
@@ -73,7 +73,7 @@
@@ -89,7 +89,7 @@
@@ -110,7 +110,7 @@ />
@@ -126,7 +126,7 @@
@@ -136,7 +136,7 @@

Other

- +
@@ -144,7 +144,7 @@
- +
@@ -157,12 +157,12 @@
@@ -172,21 +172,21 @@ - +

-

The video controls are not showing up?

+

Extension controls not appearing?

- This extension is only compatible with HTML5 video. If you don't see the - controls showing up, chances are you are viewing a Flash video. If you - want to confirm, try right-clicking on the video and inspect the menu: - if it mentions flash, then that's the issue. That said, - most sites will fallback to HTML5 if they detect that Flash it - not available. You can try manually disabling Flash plugin in Chrome: + This extension is only compatible with HTML5 audio and video. If you don't + see the controls showing up, chances are you are viewing a Flash content. + If you want to confirm, try right-clicking on the content and inspect the + menu: if it mentions flash, then that's the issue. That said, most sites + will fallback to HTML5 if they detect that Flash is not available. You + can try manually disabling Flash plugin in the browser:

    @@ -194,7 +194,7 @@ In a new tab, navigate to chrome://settings/content/flash
  • Disable "Allow sites to run Flash"
  • -
  • Restart your browser and try playing your video again
  • +
  • Restart your browser and try playing your audio or video again

The speed controls are not showing up for local videos?

From 44fda63a53d177fb7818ec3c8d3ef4c4b836af00 Mon Sep 17 00:00:00 2001 From: cristiancundari <37112275+cristiancundari@users.noreply.github.com> Date: Sat, 28 Mar 2020 16:46:57 +0100 Subject: [PATCH 40/40] Fixing #609 --- inject.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inject.js b/inject.js index fd7d166..a8c8f2c 100644 --- a/inject.js +++ b/inject.js @@ -600,6 +600,10 @@ function initializeNow(document) { )[0]; if (oldController) { oldController.remove(); + if (node.vsc) { + delete node.dataset.vscid; + delete node.vsc; + } } checkForVideo(node, node.parentNode || mutation.target, true); }