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
isWaitingForResume = false;
currentVideo.play();
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);
newKeyboard[32] = function() {
if (currentVideo && isWaitingForResume) {
plugin.handleKeyPress({ keyCode: 32, preventDefault: function(){}, stopPropagation: function(){} });
} else if (originalKeyboard[32]) {
// Call original spacebar handler
if (typeof originalKeyboard[32] === 'function') {
originalKeyboard[32]();
} else {
reveal.next();
}
} else {
reveal.next();
}
};
reveal.configure({ keyboard: newKeyboard });
console.log('VideoCuePoints: Keyboard handling set up');
};
// Setup keyboard handling first
this.setupKeyboardHandling();
// Initialize when reveal.js is ready
reveal.addEventListener('ready', function() {
plugin.setupVideoControls();
});
// Setup when slides change
reveal.addEventListener('slidechanged', function(event) {
plugin.setupVideoControls();
});
// Also setup on slide transitions
reveal.addEventListener('slidetransitionend', function(event) {
plugin.setupVideoControls();
});
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'));
plugin.initializeVideo(video);
currentVideo = video;
// Auto-play the video when slide loads
setTimeout(() => {
if (currentVideo === video && !isWaitingForResume) {
console.log('VideoCuePoints: Auto-playing video');
video.currentTime = 0; // Start from beginning
video.play().catch(e => {
console.log('VideoCuePoints: Auto-play failed (user interaction may be required):', e.message);
});
}
}, 100); // Small delay to ensure slide transition is complete
} else if (!video) {
currentVideo = null;
cuePoints = [];
nextCueIndex = 0;
isWaitingForResume = false;
}
};
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
video.removeEventListener('timeupdate', plugin.handleTimeUpdate);
cuePoints = cuePointsData.split(',').map(time => parseFloat(time.trim())).sort((a, b) => a - b);
nextCueIndex = 0;
isWaitingForResume = false;
console.log('VideoCuePoints: Initialized with cue points:', cuePoints);
// Add timeupdate listener
video.addEventListener('timeupdate', plugin.handleTimeUpdate);
// Reset cue points when video is seeked or restarted from beginning
video.addEventListener('seeked', function() {
if (!isWaitingForResume) {
plugin.updateCuePointIndex();
}
});
}
};
this.updateCuePointIndex = function() {
if (!currentVideo || cuePoints.length === 0) return;
const currentTime = currentVideo.currentTime;
// Find the next cue point that hasn't been passed yet
nextCueIndex = cuePoints.findIndex(cuePoint => cuePoint > currentTime);
if (nextCueIndex === -1) {
nextCueIndex = cuePoints.length; // All cue points have been passed
}
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);
currentVideo.pause();
isWaitingForResume = true;
// Move to next cue point
nextCueIndex++;
}
};
}
};
// Export for reveal.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = RevealVideoCuePoints;
} else if (typeof window !== 'undefined') {
window.RevealVideoCuePoints = RevealVideoCuePoints;
}