Looping and slowing videos via YouTube's IFrame player API
— JavaScript — 2 min read
Introduction
A common method for musicians learning new music is to play along with recordings, isolating short sections and adjusting the tempo if necessary. I have recently been working on a hobby project to curate some of my favourite music by jazz saxophonist Charlie Parker (charlieparkerlicks.com). One hurdle to overcome was the copyright associated with directly hosting his recordings. As a consequence, embeded Youtube IFrames proved a good solution. This blog post provides an overview of how I coded my solution and some of the hurdles I faced while doing so. While I have a running example hosted as a codepen. and Youtube's documentation for their API is quite good, this post will provide an abridged overview of the overall codebase.
Creating the Player
1function onYouTubeIframeAPIReady() {2 player = new YT.Player('player', {3 height: '270',4 width: '480',5 videoId: '-JKDFD1PZ8Y',6 playerVars: {'controls': 0, 'showinfo': 0, 'modestbranding': 0 },7 events: {8 'onReady': onPlayerReady,9 'onStateChange': onPlayerStateChange10 }11 });12 }
In creating the player, I chose to specify two events on which I wanted to call functions. The onReady event is fired whe the youtube API is ready to begin receiving calls. The onStateChange event is fired when the player changes states. In this case the most common states are playing, paused and buffering.
As my use case is looping the audio of a specific tune, I decided to limit the scope of the player's user interface by passing a value of 0 for my chosen properties in the playerVars object.
'onReady' : Get playback rates and create rate slider
1function onPlayerReady(event) {2 playbackRates = player.getAvailablePlaybackRates();3 $("#playback-rate-slider").slider({4 min: 0,5 max: playbackRates.length - 1,6 value: [playbackRates.indexOf(1)],7 slide: function(event, ui) { 8 $('#current-playback-rate').text("Playback Rate: "+ playbackRates[ui.value])9 } 10 });11 }
In this case, the player object is used to call the available playback rates before a slider is created, based on the number of available rates. This is necessary as the number of available playback rates will not always be the same. For example, if the user is on a mobile device, the API will currently only return a single playback rate (1 {Normal}).
While it was sufficient to hard code the range slider for the loop's start and end times, in the case of loading many different videos an obvious benefit would be to access each video's duration via player.getDuration().
Managing loops with Timeouts
1var videoIsPlaying = false;2 var playbackRates;3 var timeoutIds = [];
Timeouts are essential as they fire the callback necessary to restart each loop.
The biggest design issue surrounded access to click events on the IFrame. When starting the video by clicking on the IFrame the user can set multiple timeouts, while in contrast, it is easier to limit timeouts by restricting playback to the start button.
If click events were disabled on the IFrame it would be easier to manage timeout events as it would only be possible to set a single timeout when clicking the start button. In contrast, when clicking the IFrame, the user can set multiple timeouts.
When each callback is fired, the loop restarts, playing the video with the user's desired start and end point and playback speed. The playbackRates variable is used to determine how long the timeout will be, dividing the timeout length by the playback Rate (eg. if playback rate is 2, divide timeout length by 2).
Finally, the videoIsPlaying boolean is referenced so as to determine whether to start a new timeout and reset a loop, or not.
Start / Restart / Stop Looping
1function startVideo() {2 videoIsPlaying = false;3 player.pauseVideo();4
5 clearTimeout(timeoutIds[0]);6 timeoutIds.shift();7
8 let start = $( "#youtube-range-slider" ).slider( "values", 0 );9 let playbackRate = playbackRates[$( "#playback-rate-slider" ).slider( "value" )];10 player.setPlaybackRate(playbackRate);11 player.seekTo(start);12 videoIsPlaying = true;13 player.playVideo();14 }
startVideo() is called when the user starts the video, either by clicking the start button or clicking on the IFrame.
1function restartVideoSection() {2 timeoutIds.shift();3 let start = $( "#youtube-range-slider" ).slider( "values", 0 );4 player.seekTo(start);5 }
restartVideoSection() is called when the onStateChange event is triggered, the videoIsPlaying boolean is true, and there is only one timeout set.
1function stopVideo() {2 videoIsPlaying = false;3 player.pauseVideo();4 clearTimeout(timeoutIds[0])5 timeoutIds.shift();6 }
stopVideo() stops playback and clears the most recent timeout.
'onStateChange' : starting/restarting playback and clearing timeouts
1function onPlayerStateChange(event) {2 if (event.data == YT.PlayerState.PLAYING) {3 if(!videoIsPlaying || timeoutIds.length > 0){4 if(timeoutIds.length > 0){5 player.pauseVideo();6 //if iframe clicks > 1, multiple timeouts are set7 //clear all timeouts then restart loop8 timeoutIds.map(timeoutID => clearTimeout(timeoutID)); 9 timeoutIds=[];10 startVideo();11 }12 else{13 startVideo();14 }15 }16 else{17 //get current playback parameters and restart loop18 let start = $( "#youtube-range-slider" ).slider( "values", 0 );19 let end = $( "#youtube-range-slider" ).slider( "values", 1 );20 let playbackRate = playbackRates[$( "#playback-rate-slider" ).slider( "value" )];21 let duration = ((end - start) / playbackRate) * 1000;22
23 player.setPlaybackRate(playbackRate);24 timeoutIds.push(window.setTimeout(restartVideoSection, duration));25 }26 }27 }
This function is called each time the player's state changes.
If the player's state is playing, either:
- Clear timeouts if multiple timeouts are set, and then start videos
- Else if video is in playing state but the boolean isn't, start video
- Else if loop has completed and is restarting without issue, get current paramenters and restart video
A working demo is hosted here and source code is viewable on GitHub.