Skip to content

Instantly share code, notes, and snippets.

@headquarters
Created February 4, 2023 21:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save headquarters/b595e718753d7d75708e4bd662706208 to your computer and use it in GitHub Desktop.
Save headquarters/b595e718753d7d75708e4bd662706208 to your computer and use it in GitHub Desktop.
Bookmarklet: adjust video playback speed
// Bookmarklet:
javascript:(function()%7Bconst%20template%20%3D%20document.createElement(%22template%22)%3B%0Atemplate.innerHTML%20%3D%20%60%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ahost%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-sizing%3A%20border-box%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%200.5rem%201rem%201.5rem%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20320px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border%3A%202px%20solid%20darkgray%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20box-shadow%3A%20grey%200%200%204px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20white%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20position%3A%20absolute%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20right%3A%2010px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20top%3A%2010px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20z-index%3A%2010000%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-family%3A%20sans-serif%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20h1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%201.2rem%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20label%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20align-items%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20input%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-left%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-right%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%3Cdiv%20id%3D%22content%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ch1%3EAdjust%20Video%20Playback%20Rate%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%20%20%3Csection%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20Searching%20for%20a%20%3Ccode%3E%26lt%3Bvideo%20%2F%26gt%3B%3C%2Fcode%3E%20tag...%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fsection%3E%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%60%3B%0A%0Aconst%20playbackRateContent%20%3D%20%60%3Clabel%3E%0A%20%20Playback%20rate%3A%0A%20%20%3Cinput%20type%3D%22range%22%20id%3D%22playback-range%22%20name%3D%22volume%22%20list%3D%22values%22%20min%3D%220.25%22%20max%3D%222%22%20step%3D%220.25%22%3E%0A%20%20(%3Cspan%20id%3D%22currentrate%22%3E%3C%2Fspan%3E)%0A%3C%2Flabel%3E%60%3B%0A%0A%2F%2F%20TODO%3A%20add%20close%20button%2C%20make%20sure%20opening%20bookmarklet%20repeatedly%20doesn't%20break%3B%0Aclass%20VideoPlaybackControls%20extends%20HTMLElement%20%7B%0A%20%20constructor()%20%7B%0A%20%20%20%20super()%3B%0A%20%20%20%20this.attachShadow(%7B%20mode%3A%20%22open%22%20%7D)%3B%0A%0A%20%20%20%20this.update%20%3D%20this.update.bind(this)%3B%0A%20%20%7D%0A%0A%20%20%23videoplayer%20%3D%20null%3B%0A%0A%20%20%23loading%20%3D%20true%3B%0A%0A%20%20get%20videoplayer()%20%7B%0A%20%20%20%20return%20this.%23videoplayer%3B%0A%20%20%7D%0A%0A%20%20set%20videoplayer(v)%20%7B%0A%20%20%20%20this.%23videoplayer%20%3D%20v%3B%0A%20%20%20%20this.render()%3B%0A%20%20%7D%0A%0A%20%20get%20loading()%20%7B%0A%20%20%20%20return%20this.%23loading%3B%0A%20%20%7D%0A%0A%20%20set%20loading(l)%20%7B%0A%20%20%20%20this.%23loading%20%3D%20l%3B%0A%20%20%20%20this.render()%3B%0A%20%20%7D%0A%0A%20%20update(event)%20%7B%0A%20%20%20%20const%20adjustedRate%20%3D%20event.target.value%3B%0A%20%20%20%20this.shadowRoot.getElementById(%22currentrate%22).textContent%20%3D%20adjustedRate%3B%0A%20%20%20%20this.videoplayer.playbackRate%20%3D%20adjustedRate%3B%0A%20%20%7D%0A%0A%20%20render()%20%7B%0A%20%20%20%20if%20(this.loading)%20%7B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(!this.videoplayer)%20%7B%0A%20%20%20%20%20%20this.shadowRoot.querySelector(%0A%20%20%20%20%20%20%20%20%22section%22%0A%20%20%20%20%20%20).innerHTML%20%3D%20%60No%20%3Ccode%3E%26lt%3Bvideo%20%2F%26gt%3B%3C%2Fcode%3E%20elements%20found%20on%20this%20page.%60%3B%0A%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20this.shadowRoot.querySelector(%22section%22).innerHTML%20%3D%20playbackRateContent%3B%0A%0A%20%20%20%20this.currentRate%20%3D%20this.videoplayer%3F.playbackRate%3B%0A%0A%20%20%20%20this.shadowRoot.getElementById(%22currentrate%22).textContent%20%3D%0A%20%20%20%20%20%20this.currentRate%3B%0A%0A%20%20%20%20this.shadowRoot%0A%20%20%20%20%20%20.getElementById(%22playback-range%22)%0A%20%20%20%20%20%20.addEventListener(%22input%22%2C%20this.update)%3B%0A%20%20%7D%0A%0A%20%20connectedCallback()%20%7B%0A%20%20%20%20this.shadowRoot.appendChild(template.content.cloneNode(true))%3B%0A%20%20%20%20%2F%2F%20First%2C%20check%20main%20window%20for%20video%20element%0A%20%20%20%20this.videoplayer%20%3D%20document.querySelector(%22video%22)%3B%0A%0A%20%20%20%20if%20(this.videoplayer)%20%7B%0A%20%20%20%20%20%20this.loading%20%3D%20false%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(!this.videoplayer%20%26%26%20window.frames.length)%20%7B%0A%20%20%20%20%20%20%2F%2F%20No%20video%20in%20main%20window%2C%20but%20there%20is%20an%20iframe.%0A%20%20%20%20%20%20%2F%2F%20Check%20it%20for%20a%20video%20element%20when%20it%20finishes%20loading.%0A%0A%20%20%20%20%20%20%2F%2F%20Is%20the%20iframe%20already%20loaded%3F%20Naive%20check%20of%20body's%20children%20count.%0A%20%20%20%20%20%20if%20(window.frames%5B0%5D.document.body.children.length%20%3E%200)%20%7B%0A%20%20%20%20%20%20%20%20this.loading%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20this.videoplayer%20%3D%20window.frames%5B0%5D.document.querySelector(%22video%22)%3B%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20window.frames%5B0%5D.onload%20%3D%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20this.loading%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20%20%20this.videoplayer%20%3D%20window.frames%5B0%5D.document.querySelector(%22video%22)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20No%20video%20found%0A%20%20%20%20this.loading%20%3D%20false%3B%0A%20%20%7D%0A%0A%20%20disconnectedCallback()%20%7B%7D%0A%7D%0A%0A%2F%2F%20If%20first%20time%20running%2C%20this%20custom%20element%20will%20not%20be%20defined%0Aif%20(!customElements.get(%22video-playback-controls%22))%20%7B%0A%20%20customElements.define(%22video-playback-controls%22%2C%20VideoPlaybackControls)%3B%0A%7D%0A%0A%2F%2F%20Remove%20any%20existing%20element%20to%20replace%20with%20a%20new%20instance%0Adocument.querySelector(%22video-playback-controls%22)%3F.remove()%3B%0A%0Aconst%20v%20%3D%20document.createElement(%22video-playback-controls%22)%3B%0Adocument.body.appendChild(v)%3B%7D)()%3B
// Source:
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
box-sizing: border-box;
padding: 0.5rem 1rem 1.5rem;
width: 320px;
border: 2px solid darkgray;
box-shadow: grey 0 0 4px;
background-color: white;
position: absolute;
right: 10px;
top: 10px;
z-index: 10000;
font-family: sans-serif;
}
h1 {
font-size: 1.2rem;
}
label {
display: flex;
align-items: center;
}
input {
margin-left: 5px;
margin-right: 5px;
}
</style>
<div id="content">
<h1>Adjust Video Playback Rate</h1>
<section>
Searching for a <code>&lt;video /&gt;</code> tag...
</section>
</div>`;
const playbackRateContent = `<label>
Playback rate:
<input type="range" id="playback-range" name="volume" list="values" min="0.25" max="2" step="0.25">
(<span id="currentrate"></span>)
</label>`;
// TODO: add close button, make sure opening bookmarklet repeatedly doesn't break;
class VideoPlaybackControls extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.update = this.update.bind(this);
}
#videoplayer = null;
#loading = true;
get videoplayer() {
return this.#videoplayer;
}
set videoplayer(v) {
this.#videoplayer = v;
this.render();
}
get loading() {
return this.#loading;
}
set loading(l) {
this.#loading = l;
this.render();
}
update(event) {
const adjustedRate = event.target.value;
this.shadowRoot.getElementById("currentrate").textContent = adjustedRate;
this.videoplayer.playbackRate = adjustedRate;
}
render() {
if (this.loading) {
return;
}
if (!this.videoplayer) {
this.shadowRoot.querySelector(
"section"
).innerHTML = `No <code>&lt;video /&gt;</code> elements found on this page.`;
return;
}
this.shadowRoot.querySelector("section").innerHTML = playbackRateContent;
this.currentRate = this.videoplayer?.playbackRate;
this.shadowRoot.getElementById("currentrate").textContent =
this.currentRate;
this.shadowRoot
.getElementById("playback-range")
.addEventListener("input", this.update);
}
connectedCallback() {
this.shadowRoot.appendChild(template.content.cloneNode(true));
// First, check main window for video element
this.videoplayer = document.querySelector("video");
if (this.videoplayer) {
this.loading = false;
return;
}
if (!this.videoplayer && window.frames.length) {
// No video in main window, but there is an iframe.
// Check it for a video element when it finishes loading.
// Is the iframe already loaded? Naive check of body's children count.
if (window.frames[0].document.body.children.length > 0) {
this.loading = false;
this.videoplayer = window.frames[0].document.querySelector("video");
} else {
window.frames[0].onload = () => {
this.loading = false;
this.videoplayer = window.frames[0].document.querySelector("video");
};
}
return;
}
// No video found
this.loading = false;
}
disconnectedCallback() {}
}
// If first time running, this custom element will not be defined
if (!customElements.get("video-playback-controls")) {
customElements.define("video-playback-controls", VideoPlaybackControls);
}
// Remove any existing element to replace with a new instance
document.querySelector("video-playback-controls")?.remove();
const v = document.createElement("video-playback-controls");
document.body.appendChild(v);
@headquarters
Copy link
Author

Adjust playback speed of a <video /> tag on the page, even if the element does not expose that in the UI.

Screenshot 2023-02-04 at 4 56 33 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment