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.
Published

September 7, 2025

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:

View this in a new window.

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;
}