mirror of
https://github.com/SoPat712/videospeed.git
synced 2025-08-21 18:08:46 -04:00

Moved all on ratechange logic to document level listener Removed dead .getSpeed method Fixed bug causing controller to sometimes initialize with NaN
846 lines
25 KiB
JavaScript
846 lines
25 KiB
JavaScript
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
|
|
|
|
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, ""),
|
|
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) {
|
|
// 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);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
storedSpeed = tc.settings.speeds[target.currentSrc];
|
|
if (!tc.settings.rememberSpeed) {
|
|
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 {
|
|
log("Recalling stored speed due to rememberSpeed being enabled", 5);
|
|
storedSpeed = tc.settings.lastSpeed;
|
|
}
|
|
|
|
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 (!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 {
|
|
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))
|
|
);
|
|
|
|
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"]
|
|
});
|
|
};
|
|
|
|
tc.videoController.prototype.remove = function() {
|
|
this.div.remove();
|
|
this.video.removeEventListener("play", this.handlePlay);
|
|
delete this.video.dataset["vscid"];
|
|
delete this.video.vsc;
|
|
};
|
|
|
|
tc.videoController.prototype.initializeControls = function() {
|
|
log("initializeControls Begin", 5);
|
|
var document = this.document;
|
|
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;
|
|
|
|
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 = `
|
|
<style>
|
|
@import "${chrome.runtime.getURL("shadow.css")}";
|
|
</style>
|
|
|
|
<div id="controller" style="top:${top}; left:${left}; opacity:${
|
|
tc.settings.controllerOpacity
|
|
}">
|
|
<span data-action="drag" class="draggable">${speed}</span>
|
|
<span id="controls">
|
|
<button data-action="rewind" class="rw">«</button>
|
|
<button data-action="slower">-</button>
|
|
<button data-action="faster">+</button>
|
|
<button data-action="advance" class="rw">»</button>
|
|
<button data-action="display" class="hideButton">x</button>
|
|
</span>
|
|
</div>
|
|
`;
|
|
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
|
|
);
|
|
};
|
|
});
|
|
|
|
this.speedIndicator = shadow.querySelector("span");
|
|
var fragment = document.createDocumentFragment();
|
|
fragment.appendChild(wrapper);
|
|
|
|
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);
|
|
|
|
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 escapeStringRegExp(str) {
|
|
matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
|
|
return str.replace(matchOperatorsRe, "\\$&");
|
|
}
|
|
|
|
function isBlacklisted() {
|
|
blacklisted = false;
|
|
tc.settings.blacklist.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)) {
|
|
blacklisted = true;
|
|
return;
|
|
}
|
|
});
|
|
return blacklisted;
|
|
}
|
|
|
|
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) {
|
|
log("Speed event propagation blocked", 4);
|
|
event.stopImmediatePropagation();
|
|
}
|
|
},
|
|
true
|
|
);
|
|
window.onload = () => {
|
|
initializeNow(window.document);
|
|
};
|
|
if (document) {
|
|
if (document.readyState === "complete") {
|
|
initializeNow(document);
|
|
} else {
|
|
document.onreadystatechange = () => {
|
|
if (document.readyState === "complete") {
|
|
initializeNow(document);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
log("End initializeWhenReady", 5);
|
|
}
|
|
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.push(child);
|
|
getChild(child);
|
|
if (child.shadowRoot) {
|
|
result.push(getShadow(child.shadowRoot));
|
|
}
|
|
child = child.nextElementSibling;
|
|
} while (child);
|
|
}
|
|
}
|
|
getChild(parent);
|
|
return result.flat(Infinity);
|
|
}
|
|
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) {
|
|
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();
|
|
} 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;
|
|
log("Processing keydown event: " + keyCode, 6);
|
|
|
|
// 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")
|
|
) {
|
|
log("Keydown event ignored due to active modifier: " + keyCode, 5);
|
|
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;
|
|
}
|
|
|
|
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
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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"] &&
|
|
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
|
|
});
|
|
|
|
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);
|
|
});
|
|
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) {
|
|
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) {
|
|
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");
|
|
}
|
|
|
|
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") {
|
|
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(
|
|
(v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + value,
|
|
16
|
|
);
|
|
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") ||
|
|
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, controller, 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);
|
|
}
|
|
}
|
|
});
|
|
log("runAction End", 5);
|
|
}
|
|
|
|
function pause(v) {
|
|
if (v.paused) {
|
|
log("Resuming video", 5);
|
|
v.play();
|
|
} else {
|
|
log("Pausing video", 5);
|
|
v.pause();
|
|
}
|
|
}
|
|
|
|
function resetSpeed(v, controller, target) {
|
|
if (v.playbackRate === target) {
|
|
if (v.playbackRate === getKeyBindings("reset")) {
|
|
if (target !== 1.0) {
|
|
log("Resetting playback speed to 1.0", 4);
|
|
setSpeed(controller, v, 1.0);
|
|
} else {
|
|
log('Toggling playback speed to "fast" speed', 4);
|
|
setSpeed(controller, v, getKeyBindings("fast"));
|
|
}
|
|
} else {
|
|
log('Toggling playback speed to "reset" speed', 4);
|
|
setSpeed(controller, v, getKeyBindings("reset"));
|
|
}
|
|
} else {
|
|
log('Toggling playback speed to "reset" speed', 4);
|
|
setKeyBindings("reset", v.playbackRate);
|
|
setSpeed(controller, v, target);
|
|
}
|
|
}
|
|
|
|
function muted(v, value) {
|
|
v.muted = v.muted !== true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
video.classList.add("vcs-dragging");
|
|
shadowController.classList.add("dragging");
|
|
|
|
const initialMouseXY = [e.clientX, e.clientY];
|
|
const initialControllerXY = [
|
|
parseInt(shadowController.style.left),
|
|
parseInt(shadowController.style.top)
|
|
];
|
|
|
|
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";
|
|
};
|
|
|
|
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) {
|
|
log("Showing controller", 4);
|
|
controller.classList.add("vcs-show");
|
|
|
|
if (animation) clearTimeout(timer);
|
|
|
|
animation = true;
|
|
timer = setTimeout(function() {
|
|
controller.classList.remove("vcs-show");
|
|
animation = false;
|
|
log("Hiding controller", 5);
|
|
}, 2000);
|
|
}
|