Automatically pausing a video in Revealjs with custom pauses
presentations
video
I made a revealjs plugin (using Claude Code) that automatically pauses a video at fixed points.
For my presentations I could potentially use video for diagrams. One example of this is using Procreate to draw a diagram, and then using the video playback feature to save a video of the diagram being drawn. So I can pause the drawing at predetermined points.
I used Claude Code to create a plugin for Revealjs with this functionality. It came up with a solution where I can add cue-points like this:
<video data-cuepoints="10, 30, 60, 90" controls> <source src="your-video.mp4" type="video/mp4"> </video>
Neat and simple. Here it is in action:
Here is the code for the plugin:
// reveal.js Video Cue Points Plugin
// Place this in js/plugins/video-cuepoints.js
const RevealVideoCuePoints = {
id: 'video-cuepoints',
init: function(reveal) {
const plugin = this;
let currentVideo = null;
let cuePoints = [];
let nextCueIndex = 0;
let isWaitingForResume = false;
// Define methods first
this.handleKeyPress = function(event) {
// Only handle spacebar (keyCode 32)
if (event.keyCode === 32 && currentVideo && isWaitingForResume) {
event.preventDefault();
event.stopPropagation();
console.log('VideoCuePoints: Resuming video from', currentVideo.currentTime);
// Simply resume - no seeking needed with new logic
= false;
isWaitingForResume .play();
currentVideo
return false;
};
}
this.setupKeyboardHandling = function() {
// Add keyboard listener with high priority
document.addEventListener('keydown', plugin.handleKeyPress, true);
// Override reveal.js keyboard handling for spacebar when video is paused
const originalKeyboard = reveal.getConfig().keyboard || {};
const newKeyboard = Object.assign({}, originalKeyboard);
32] = function() {
newKeyboard[if (currentVideo && isWaitingForResume) {
.handleKeyPress({ keyCode: 32, preventDefault: function(){}, stopPropagation: function(){} });
pluginelse if (originalKeyboard[32]) {
} // Call original spacebar handler
if (typeof originalKeyboard[32] === 'function') {
32]();
originalKeyboard[else {
} .next();
reveal
}else {
} .next();
reveal
};
}
.configure({ keyboard: newKeyboard });
revealconsole.log('VideoCuePoints: Keyboard handling set up');
;
}
// Setup keyboard handling first
this.setupKeyboardHandling();
// Initialize when reveal.js is ready
.addEventListener('ready', function() {
reveal.setupVideoControls();
plugin;
})
// Setup when slides change
.addEventListener('slidechanged', function(event) {
reveal.setupVideoControls();
plugin;
})
// Also setup on slide transitions
.addEventListener('slidetransitionend', function(event) {
reveal.setupVideoControls();
plugin;
})
this.setupVideoControls = function() {
// Find video in current slide
const currentSlide = reveal.getCurrentSlide();
const video = currentSlide.querySelector('video[data-cuepoints]');
if (video && video !== currentVideo) {
console.log('VideoCuePoints: Initializing video with cue points:', video.getAttribute('data-cuepoints'));
.initializeVideo(video);
plugin= video;
currentVideo
// Auto-play the video when slide loads
setTimeout(() => {
if (currentVideo === video && !isWaitingForResume) {
console.log('VideoCuePoints: Auto-playing video');
.currentTime = 0; // Start from beginning
video.play().catch(e => {
videoconsole.log('VideoCuePoints: Auto-play failed (user interaction may be required):', e.message);
;
})
}, 100); // Small delay to ensure slide transition is complete
}else if (!video) {
} = null;
currentVideo = [];
cuePoints = 0;
nextCueIndex = false;
isWaitingForResume
};
}
this.initializeVideo = function(video) {
// Parse cue points from data attribute
const cuePointsData = video.getAttribute('data-cuepoints');
if (cuePointsData) {
// Remove any existing event listeners to avoid duplicates
.removeEventListener('timeupdate', plugin.handleTimeUpdate);
video
= cuePointsData.split(',').map(time => parseFloat(time.trim())).sort((a, b) => a - b);
cuePoints = 0;
nextCueIndex = false;
isWaitingForResume
console.log('VideoCuePoints: Initialized with cue points:', cuePoints);
// Add timeupdate listener
.addEventListener('timeupdate', plugin.handleTimeUpdate);
video
// Reset cue points when video is seeked or restarted from beginning
.addEventListener('seeked', function() {
videoif (!isWaitingForResume) {
.updateCuePointIndex();
plugin
};
})
};
}
this.updateCuePointIndex = function() {
if (!currentVideo || cuePoints.length === 0) return;
const currentTime = currentVideo.currentTime;
// Find the next cue point that hasn't been passed yet
= cuePoints.findIndex(cuePoint => cuePoint > currentTime);
nextCueIndex if (nextCueIndex === -1) {
= cuePoints.length; // All cue points have been passed
nextCueIndex
}console.log('VideoCuePoints: Updated cue index to', nextCueIndex, 'at time', currentTime);
;
}
this.handleTimeUpdate = function() {
if (!currentVideo || cuePoints.length === 0 || isWaitingForResume) return;
const currentTime = currentVideo.currentTime;
// Check if we've reached the next cue point
if (nextCueIndex < cuePoints.length && currentTime >= cuePoints[nextCueIndex]) {
const triggerTime = cuePoints[nextCueIndex];
console.log('VideoCuePoints: Pausing at cue point:', triggerTime, 'current time:', currentTime);
.pause();
currentVideo= true;
isWaitingForResume
// Move to next cue point
++;
nextCueIndex
};
}
};
}
// Export for reveal.js
if (typeof module !== 'undefined' && module.exports) {
.exports = RevealVideoCuePoints;
moduleelse if (typeof window !== 'undefined') {
} window.RevealVideoCuePoints = RevealVideoCuePoints;
}