From 71c11ddeb8a4b937db4473301ff1efa164eef0a2 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Sat, 9 Nov 2019 11:34:08 -0600 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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 9920b8080155fb7cbc07e33b4c6a3b6b2e6b9aaf Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 28 Feb 2020 12:30:02 -0600 Subject: [PATCH 22/24] big performance gains by deleting .concat and adding in .push and .flat! --- inject.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inject.js b/inject.js index fb13754..e95ff04 100644 --- a/inject.js +++ b/inject.js @@ -365,17 +365,17 @@ function getShadow(parent) { if (parent.firstElementChild) { var child = parent.firstElementChild; do { - result = result.concat(child); + result.push(child); getChild(child); if (child.shadowRoot) { - result = result.concat(getShadow(child.shadowRoot)); + result.push(getShadow(child.shadowRoot)); } child = child.nextElementSibling; } while (child); } } getChild(parent); - return result; + return result.flat(Infinity); } function getController(id) { return getShadow(document.body).filter(x => { From b5c48abfcec56748021265d61124f02c46e5f084 Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 28 Feb 2020 12:37:35 -0600 Subject: [PATCH 23/24] check if aria-hidden exists before seeing if its value is false. --- inject.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inject.js b/inject.js index e95ff04..d059564 100644 --- a/inject.js +++ b/inject.js @@ -502,7 +502,8 @@ function initializeNow(document) { }); break; case "attributes": - if (mutation.target.attributes["aria-hidden"].value == "false") { + if (mutation.target.attributes["aria-hidden"] && + mutation.target.attributes["aria-hidden"].value == "false") { var flattenedNodes = getShadow(document.body); var node = flattenedNodes.filter(x => x.tagName == "VIDEO")[0]; if (node) { From 72c4e49fbca027ad4ab5d708224e9d39ed980b6c Mon Sep 17 00:00:00 2001 From: Jonathan Dawson Date: Fri, 28 Feb 2020 12:48:14 -0600 Subject: [PATCH 24/24] bump to 0.6.0 in order to run the github action to upload to the store. --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index b911445..021f792 100755 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Video Speed Controller", "short_name": "videospeed", - "version": "0.5.9", + "version": "0.6.0", "manifest_version": 2, "description": "Speed up, slow down, advance and rewind any HTML5 video with quick shortcuts.", "homepage_url": "https://github.com/igrigorik/videospeed",