Last active
April 4, 2025 16:17
-
-
Save ashtonmeuser/1a65e50a7cc6bcb10b1225a43e4d6a30 to your computer and use it in GitHub Desktop.
Set loop points on HTML video elements
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// bookmarklet-title: Loop | |
// bookmarklet-about: Set loop points on HTML video elements. Enable the console option for a lighter-weight console log in place of toast message. | |
// bookmarklet-var(uuid): uuid | |
// bookmarklet-var(boolean): console | |
import toast from '/ashtonmeuser/e20e5ffd7ea503ff6b0fd80787b00b82/raw/8eb2f8190d7bfd94b0866735546131f94a7e30f1/toast.ts'; | |
import StateManager from '/ashtonmeuser/1a65e50a7cc6bcb10b1225a43e4d6a30/raw//StateManager.ts'; | |
import VideoLoopState from '/ashtonmeuser/1a65e50a7cc6bcb10b1225a43e4d6a30/raw//VideoLoopState.ts'; | |
export class ToastVideoLoopState extends VideoLoopState { | |
constructor() { | |
super(loop => { | |
const message = loop ? `Loop ${this.format(loop.start)} → ${this.format(loop.end)}` : 'Loop cleared'; | |
if (console) console.log(message); | |
else toast(message); | |
}, 100); | |
} | |
private format(seconds?: number): string { | |
if (seconds === undefined) return '—'; | |
const hours = Math.floor(seconds / 3600); | |
const minutes = Math.floor((seconds % 3600) / 60); | |
seconds = Math.floor(seconds % 60); | |
const parts: string[] = []; | |
if (hours > 0) parts.push(hours.toString().padStart(2, '0')); | |
parts.push(minutes.toString().padStart(2, '0')); | |
parts.push(seconds.toString().padStart(2, '0')); | |
return parts.join(':'); | |
} | |
} | |
const manager = new StateManager(uuid, ToastVideoLoopState); | |
manager.state.toggle(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class StateManager<T> { | |
private static _instance: StateManager<any> | null; | |
private readonly type = 'StateManager'; | |
state!: T; | |
constructor(id: string, constructor: new () => T) { | |
if (StateManager._instance) return StateManager._instance; | |
if (typeof globalThis === 'object' && id in globalThis && (globalThis as any)[id]?.type === 'StateManager') { | |
StateManager._instance = (globalThis as any)[id]; | |
return StateManager._instance!; | |
} | |
this.state = new constructor(); | |
StateManager._instance = this; | |
Object.assign(globalThis, { | |
[id]: this, | |
}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface VideoLoop { | |
start?: number; | |
end?: number; | |
} | |
type LoopCallback = (loop: VideoLoop | null) => void; | |
export default class VideoLoopState { | |
private loops = new WeakMap<HTMLMediaElement, VideoLoop>(); | |
protected callback?: LoopCallback; | |
constructor(callback?: LoopCallback, interval: number = 1000) { | |
this.callback = callback; | |
this.startPolling(interval); | |
} | |
toggle() { | |
const videos = this.getVisibleVideos(); | |
for (const video of videos) { | |
const loop = this.loops.get(video); | |
if (!loop || loop.start === undefined) { // Set loop start | |
this.setLoop(video, { start: video.currentTime }); | |
} else if (loop.end === undefined) { // Set loop end | |
this.setLoop(video, video.currentTime < loop.start ? { start: video.currentTime } : { start: loop.start, end: video.currentTime }); | |
} else { // Clear loop | |
this.setLoop(video, null); | |
} | |
} | |
} | |
private setLoop(video: HTMLMediaElement, loop: VideoLoop | null) { | |
loop === null ? this.loops.delete(video) : this.loops.set(video, loop); | |
this.callback?.(loop); | |
} | |
private getVisibleVideos(): HTMLMediaElement[] { | |
return Array.from(document.getElementsByTagName('video')) | |
.filter(video => video.offsetParent !== null && video.getBoundingClientRect().width > 0); | |
} | |
private startPolling(interval: number) { | |
window.setInterval(() => { | |
const videos = this.getVisibleVideos(); | |
for (const video of videos) { | |
const loop = this.loops.get(video); | |
if (!loop) continue; | |
if ((loop.start !== undefined && video.currentTime < loop.start) | |
|| (loop.start !== undefined && loop.end !== undefined && video.currentTime >= loop.end)) { | |
video.currentTime = loop.start; | |
} | |
} | |
}, interval); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment