From cba59ac49e4d89e83ec97dc3428328449e7a9c91 Mon Sep 17 00:00:00 2001 From: jbzdarkid Date: Sun, 20 Jul 2025 18:01:46 -0700 Subject: [PATCH 1/3] embed working? --- index.html | 1 + index.js | 54 +++++++++++++++++------- player.js | 118 ++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 139 insertions(+), 34 deletions(-) diff --git a/index.html b/index.html index c3a7b7b..1ad50bb 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + diff --git a/index.js b/index.js index 9e45077..2a25e5e 100644 --- a/index.js +++ b/index.js @@ -89,14 +89,26 @@ window.onload = function() { // There may be other params, this loop is only for player0, player1, etc if (playerId.startsWith('player') && videoIds.length > 0) { while (document.getElementById(playerId) == null) window.addPlayer() - players.set(playerId, new Player()) - + // Copy the loop variables to avoid javascript lambda-in-loop bug ;((playerId, videoIds) => { - var promise = FEATURES.DO_TWITCH_AUTH ? getTwitchVideosDetails(videoIds.split('-')) : getStubVideosDetails(videoIds.split('-')) - promise - .then(videos => loadVideos(playerId, videos)) - .catch(r => showText(playerId, 'Could not process video "' + videoIds + '":\n' + r, /*isError*/true)) + // Multi-video players should always be from the same source. + var firstVideo = videoIds.split('-')[0] + if (firstVideo.match(YOUTUBE_VIDEO_MATCH) != null) { + getStubVideosDetails(videoIds.split('-')) + .then(videos => loadVideos(playerId, videos, YOUTUBE)) + .catch(r => showText(playerId, 'Could not process youtube video "' + videoIds + '":\n' + r, /*isError*/true)) + } else if (!FEATURES.DO_TWITCH_AUTH) { + getStubVideosDetails(videoIds.split('-')) + .then(videos => loadVideos(playerId, videos, TWITCH)) + .catch(r => showText(playerId, 'Could not process twitch video "' + videoIds + '":\n' + r, /*isError*/true)) + } else if (firstVideo.match(TWITCH_VIDEO_MATCH) != null) { + getTwitchVideosDetails(videoIds.split('-')) + .then(videos => loadVideos(playerId, videos, TWITCH)) + .catch(r => showText(playerId, 'Could not process twitch video "' + videoIds + '":\n' + r, /*isError*/true)) + } else { + showText(playerId, 'Could not parse video string "' + videoIds + '"', /*isError*/true) + } })(playerId, videoIds) } } @@ -267,7 +279,10 @@ function addPlayer() { } function showText(playerId, message, isError) { - if (isError) debugger; + if (isError) { + console.error(playerId, message) + debugger; + } var error = document.getElementById(playerId + '-text') if (message == null) { error.innerText = '' @@ -369,12 +384,21 @@ function searchVideo(event) { return } - // Check to see if the user provided a direct video link + // Check to see if the user provided a direct youtube video (or youtube video id) + m = formText.match(YOUTUBE_VIDEO_MATCH) + if (m != null) { + showText(playerId, 'Loading Youtube video...') + getStubVideosDetails([m[1]]) + .then(videos => loadVideos(playerId, videos, YOUTUBE)) + .catch(r => showText(playerId, 'Could not process youtube video "' + m[1] + '":\n' + r, /*isError*/true)) + return + } + // Check to see if the user provided a direct twitch VOD (or VOD id) m = formText.match(TWITCH_VIDEO_MATCH) if (m != null) { - showText(playerId, 'Loading video...') - getTwitchVideosDetails([m[1]]) - .then(videos => loadVideos(playerId, videos)) + showText(playerId, 'Loading Twitch VOD...') + getTwitchVideosDetails([m[1]]) // TODO: How does this work if we disabled twitch auth? + .then(videos => loadVideos(playerId, videos, TWITCH)) .catch(r => showText(playerId, 'Could not process twitch video "' + m[1] + '":\n' + r, /*isError*/true)) return } @@ -408,7 +432,7 @@ function searchVideo(event) { } if (bestVideo != null) { - loadVideos(playerId, [bestVideo]) // TODO: Load all matching videos once the player can handle multiple videos + loadVideos(playerId, [bestVideo], TWITCH) // TODO: Load all matching videos once the player can handle multiple videos return } @@ -441,14 +465,14 @@ function showVideoPicker(playerId, videos) { videoImg.style = 'width: 320px; height: 180px; object-fit: cover; object-position: top; cursor: pointer' videoImg.onclick = function() { videoGrid.remove() - loadVideos(playerId, [videos[i]]) + loadVideos(playerId, [videos[i]], TWITCH) } })(i) } } var players = new Map() -function loadVideos(playerId, videos) { +function loadVideos(playerId, videos, playerType) { document.getElementById(playerId + '-form').style.display = 'none' var div = document.getElementById(playerId) @@ -457,7 +481,7 @@ function loadVideos(playerId, videos) { params.set(div.id, videos.map(v => v.id).join('-')) history.pushState(null, null, '?' + params.toString()) - var player = new Player(div.id, videos) + var player = window.newPlayer(div.id, videos, playerType) players.set(div.id, player) if (params.has('offset' + div.id)) { player.offset = parseInt(params.get('offset' + div.id)) diff --git a/player.js b/player.js index cbc0bce..4845fbf 100644 --- a/player.js +++ b/player.js @@ -1,6 +1,10 @@ -// Player states function enumValue(name) { return Object.freeze({toString: () => name}) } +// Player types +const TWITCH = enumValue('TWITCH') +const YOUTUBE = enumValue('YOUTUBE') + +// Player states const LOADING = enumValue('LOADING') const READY = enumValue('READY') const SEEKING_PLAY = enumValue('SEEKING_PLAY') @@ -18,28 +22,34 @@ const ASYNC = enumValue('ASYNC') // When a video ends, I want to leave it paused somewhere near the end screen -- so this value represents a safe point to seek to which avoids autoplay. const VIDEO_END_BUFFER = 15000 -class Player { +window.newPlayer = function(divId, videos, playerType) { + if (videos == null || videos.length === 0) throw new Exception('Invalid videos: ' + videos.toString()) + var videoDetails = videos[0] // TODO: Support multiple videos here? + + if (playerType === TWITCH) return new TwitchPlayer(divId, videoDetails) + if (playerType === YOUTUBE) return new YoutubePlayer(divId, videoDetails) + throw new Exception('Unknown player type: ' + playerType.toString()) +} + +class TwitchPlayer { constructor(divId, videos) { this.state = LOADING - if (videos != null && videos.length > 0) { - var videoDetails = videos[0] // TODO: Support multiple videos here (everything else should be wired up) - this.streamer = videoDetails.streamer - this._startTime = videoDetails.startTime - this._endTime = videoDetails.endTime - this.offset = 0 - this.id = divId - - var options = { - width: '100%', - height: '100%', - video: videoDetails.id, - autoplay: false, - muted: true, - } - this._player = new Twitch.Player(divId, options) - this._player.addEventListener('ready', () => this.onPlayerReady()) + this.streamer = videoDetails.streamer + this._startTime = videoDetails.startTime + this._endTime = videoDetails.endTime + this.offset = 0 + this.id = divId + + var options = { + width: '100%', + height: '100%', + video: videoDetails.id, + autoplay: false, + muted: true, } + this._player = new Twitch.Player(divId, options) + this._player.addEventListener('ready', () => this.onPlayerReady()) } onPlayerReady() { @@ -109,3 +119,73 @@ class Player { } } } + +class YoutubePlayer { + constructor(divId, videoDetails) { + this.state = LOADING + + this.streamer = videoDetails.streamer + this._startTime = videoDetails.startTime + this._endTime = videoDetails.endTime + this.offset = 0 + this.id = divId + + var options = { + height: '100%', + width: '100%', + videoId: videoDetails.id, + playerVars: {'autoplay': 0}, + } + + // The 'onReady' event needs to be hooked precisely so that it's not called *during* the new YT.Player invocation, + // and so that 'this' is properly defined inside the callback + this._player = new YT.Player(divId, options) + this._player.addEventListener('onReady', () => this.onPlayerReady()) + } + + onPlayerReady() { + this._player.mute() // Oddly this cannot be set in the options, so we set it on ready. + + // Only hook events once the player has loaded, so we don't have to worry about events in the LOADING state. + this._player.addEventListener('onStateChange', (eventData) => { + switch (event.data) { + case YT.PlayerState.UNSTARTED: + break + case YT.PlayerState.ENDED: + this.eventSink('ended') + break + case YT.PlayerState.PLAYING: + // Since we're not calling the youtube APIs, we don't know the video duration. + // However, it should be available once the player starts playing. + var durationMillis = Math.floor(this._player.getDuration() * 1000) + this._endTime = this._startTime + durationMillis + this.eventSink('play', this) + break + case YT.PlayerState.PAUSED: + this.eventSink('pause') + break + case YT.PlayerState.BUFFERING: + break + case YT.PlayerState.CUED: + break + } + }) + + this.onready(this) // Call back into index.js for the main bulk of 'readying' + } + + get startTime() { return this._startTime + this.offset } + get endTime() { return this._endTime + this.offset } + + getCurrentTimestamp() { + var durationMillis = Math.floor(this._player.getCurrentTime() * 1000) + return this._startTime + this.offset + durationMillis + } + + play() { this._player.playVideo() } + pause() { this._player.pauseVideo() } + seekToEnd() { this.seekTo(this.endTime) } + seekTo(timestamp, targetState) { + // uh + } +} \ No newline at end of file From 77105c2e474731a9aefbd14827ad0c8d6bde0d5d Mon Sep 17 00:00:00 2001 From: jbzdarkid Date: Sun, 20 Jul 2025 21:06:42 -0700 Subject: [PATCH 2/3] these look real similar --- index.html | 4 +--- player.js | 43 +++++++++++++++++++++++++++++++++++-------- twitch.js | 5 +++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 1ad50bb..0a3818d 100644 --- a/index.html +++ b/index.html @@ -39,11 +39,9 @@

-
@@ -53,7 +51,7 @@
- +
diff --git a/player.js b/player.js index 4845fbf..cae8a9e 100644 --- a/player.js +++ b/player.js @@ -32,7 +32,7 @@ window.newPlayer = function(divId, videos, playerType) { } class TwitchPlayer { - constructor(divId, videos) { + constructor(divId, videoDetails) { this.state = LOADING this.streamer = videoDetails.streamer @@ -147,12 +147,10 @@ class YoutubePlayer { this._player.mute() // Oddly this cannot be set in the options, so we set it on ready. // Only hook events once the player has loaded, so we don't have to worry about events in the LOADING state. - this._player.addEventListener('onStateChange', (eventData) => { + this._player.addEventListener('onStateChange', (event) => { switch (event.data) { - case YT.PlayerState.UNSTARTED: - break case YT.PlayerState.ENDED: - this.eventSink('ended') + this.eventSink('ended', this) break case YT.PlayerState.PLAYING: // Since we're not calling the youtube APIs, we don't know the video duration. @@ -162,12 +160,14 @@ class YoutubePlayer { this.eventSink('play', this) break case YT.PlayerState.PAUSED: - this.eventSink('pause') + this.eventSink('pause', this) break + /* + case YT.PlayerState.UNSTARTED: case YT.PlayerState.BUFFERING: - break case YT.PlayerState.CUED: break + */ } }) @@ -186,6 +186,33 @@ class YoutubePlayer { pause() { this._player.pauseVideo() } seekToEnd() { this.seekTo(this.endTime) } seekTo(timestamp, targetState) { - // uh + if (timestamp < this.startTime) { + console.log('Attempted to seek before the startTime, seeking start instead') + var durationSeconds = 0.001 // I think seek(0) does something wrong, so. + this.state = SEEKING_START + this._player.pauseVideo() + this._player.seekTo(durationSeconds, true) + // If we try to seek past the end time (and the end time is known), instead pause the video near the end. + } else if (this._endTime != null && timestamp >= this.endTime - VIDEO_END_BUFFER) { + var durationSeconds = (this.endTime - this.startTime - VIDEO_END_BUFFER) / 1000.0 + this.state = SEEKING_END + this._player.pauseVideo() + this._player.seekTo(durationSeconds, true) + } else { + var durationSeconds = (timestamp - this.startTime) / 1000.0 + if (durationSeconds === 0) durationSeconds = 0.001 // I think seek(0) does something wrong, so. + + if (targetState === PAUSED) { + // We don't want to pause videos which are already paused. It can cause weird behaviors if a seek is interspersed. + if (this.state !== PAUSED) this._player.pauseVideo() + this._player.seekTo(durationSeconds, true) + this.state = SEEKING_PAUSE + } else if (targetState === PLAYING) { + this._player.seekTo(durationSeconds, true) + // We don't want to pause videos which are already playing. It can cause weird behaviors if a seek is interspersed. + if (this.state !== PLAYING) this._player.playVideo() + this.state = SEEKING_PLAY + } + } } } \ No newline at end of file diff --git a/twitch.js b/twitch.js index 208062a..32fe403 100644 --- a/twitch.js +++ b/twitch.js @@ -35,6 +35,11 @@ window.doTwitchRedirect = function(event) { event.preventDefault() var authPrefs = event.target.elements['authPrefs'].value window.localStorage.setItem('authPrefs', authPrefs) + + if (authPrefs == 'disableAuth') { + window.location.reload() + return + } } // If there's somehow already query params, drop them -- we're probably looping. From 04da4e6619a381e92c49d385a268f197bb9314c4 Mon Sep 17 00:00:00 2001 From: jbzdarkid Date: Thu, 24 Jul 2025 01:17:09 -0700 Subject: [PATCH 3/3] seeking_pause is screwing me now --- index.js | 10 +-- player.js | 194 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 115 insertions(+), 89 deletions(-) diff --git a/index.js b/index.js index 2a25e5e..b9b09d5 100644 --- a/index.js +++ b/index.js @@ -615,7 +615,7 @@ console.log = function(...args) { function twitchEvent(event, thisPlayer, seekMillis) { console.log(thisPlayer.id, 'received event', event, 'while in state', thisPlayer.state, seekMillis) - if (event == 'seek') { + if (event == SEEK) { switch (thisPlayer.state) { // These states are expected to have a seek event based on automated seeking actions, // so we assume that any 'seek' event corresponds to that action. @@ -652,7 +652,7 @@ function twitchEvent(event, thisPlayer, seekMillis) { case RESTARTING: // This is the only state (other than LOADING) where the player isn't really loaded. Ignore seeks here. break } - } else if (event == 'play') { + } else if (event == PLAY) { switch (thisPlayer.state) { case PAUSED: // If the user manually starts a fully paused video, sync all other videos to it. case READY: // A manual play on a 'ready' video (before other players have loaded) @@ -683,7 +683,7 @@ function twitchEvent(event, thisPlayer, seekMillis) { console.log('Ignoring unexpected play event for', thisPlayer.id) break } - } else if (event == 'pause') { + } else if (event == PAUSE) { switch (thisPlayer.state) { case SEEKING_PLAY: case PLAYING: @@ -715,7 +715,7 @@ function twitchEvent(event, thisPlayer, seekMillis) { console.log('Ignoring unexpected pause event for', thisPlayer.id) break } - } else if (event == 'ended') { + } else if (event == ENDED) { switch (thisPlayer.state) { case PLAYING: // This is the most likely state: letting a video play out until its natural end. case READY: // All other states are possible by seeking, if the user clicks at the end of the timeline. @@ -758,7 +758,7 @@ function twitchEvent(event, thisPlayer, seekMillis) { var pendingSeekTimestamp = 0 // Will be nonzero after a seek, returns to zero once all videos have finished seeking function seekPlayersTo(timestamp, targetState, exceptFor) { - console.log('Seeking all players to', timestamp, 'and state', targetState, 'except for', exceptFor) + console.log('Seeking all players to', timestamp, 'and state', targetState, (exceptFor != null ? 'except for ' + exceptFor.id : null)) pendingSeekTimestamp = timestamp for (var player of players.values()) { if (player.state === LOADING) continue // We cannot seek a video that hasn't loaded yet. diff --git a/player.js b/player.js index cae8a9e..9a8511e 100644 --- a/player.js +++ b/player.js @@ -18,6 +18,14 @@ const SEEKING_END = enumValue('SEEKING_END') const AFTER_END = enumValue('AFTER_END') const ASYNC = enumValue('ASYNC') +// Callback events +const SEEK = enumValue('SEEK') +const PLAY = enumValue('PLAY') +const PAUSE = enumValue('PAUSE') +const ENDED = enumValue('ENDED') + +const TEST_PLAYING = enumValue('TEST_PLAYING') // Only used in tests + // If you seek (manually or automatically) to a timestamp within the last 10 seconds, twitch ends the video and starts auto-playing the next one. // When a video ends, I want to leave it paused somewhere near the end screen -- so this value represents a safe point to seek to which avoids autoplay. const VIDEO_END_BUFFER = 15000 @@ -31,7 +39,7 @@ window.newPlayer = function(divId, videos, playerType) { throw new Exception('Unknown player type: ' + playerType.toString()) } -class TwitchPlayer { +class Player { constructor(divId, videoDetails) { this.state = LOADING @@ -40,7 +48,44 @@ class TwitchPlayer { this._endTime = videoDetails.endTime this.offset = 0 this.id = divId + } + seekToEnd() { this.seekTo(this.endTime, PAUSED) } + seekTo(timestamp, targetState) { + if (timestamp < this.startTime) { + console.log('Attempted to seek before the startTime, seeking start instead') + var durationSeconds = 0.001 // I think seek(0) does something wrong, so. + this.state = SEEKING_START + this.pause() + this.seek(durationSeconds) + // If we try to seek past the end time (and the end time is known), instead pause the video near the end. + } else if (this._endTime != null && timestamp >= this.endTime - VIDEO_END_BUFFER) { + var durationSeconds = (this.endTime - this.startTime - VIDEO_END_BUFFER) / 1000.0 + this.state = SEEKING_END + this.pause() + this.seek(durationSeconds) + } else { + var durationSeconds = (timestamp - this.startTime) / 1000.0 + if (durationSeconds === 0) durationSeconds = 0.001 // I think seek(0) does something wrong, so. + + if (targetState === PAUSED) { + // We don't want to pause videos which are already paused. It can cause weird behaviors if a seek is interspersed. + if (this.state !== PAUSED) this.pause() + this.seek(durationSeconds) + this.state = SEEKING_PAUSE + } else if (targetState === PLAYING) { + this.seek(durationSeconds) + // We don't want to play videos which are already playing. It can cause weird behaviors if a seek is interspersed. + if (this.state !== PLAYING) this.play() + this.state = SEEKING_PLAY + } + } + } +} + +class TwitchPlayer extends Player { + constructor(divId, videoDetails) { + super(divId, videoDetails) var options = { width: '100%', height: '100%', @@ -58,21 +103,21 @@ class TwitchPlayer { // Twitch sends a seek event immediately after the video is ready, which isn't a seek we're expecting to process. if (this.state === READY && (eventData.position === 0 || eventData.position === 0.01)) return var seekMillis = Math.floor(eventData.position * 1000) - this.eventSink('seek', this, seekMillis) + this.eventSink(SEEK, this, seekMillis) }) this._player.addEventListener('play', () => { // Twitch loads the "true" video duration once it starts playing. We use that to update our end time, // since there's a chance that the video is a live VOD, and its duration doesn't match what the API returned. var durationMillis = Math.floor(this._player.getDuration() * 1000) this._endTime = this._startTime + durationMillis - this.eventSink('play', this) + this.eventSink(PLAY, this) }) - this._player.addEventListener('pause', () => this.eventSink('pause', this)) - this._player.addEventListener('ended', () => this.eventSink('ended', this)) + this._player.addEventListener('pause', () => this.eventSink(PAUSE, this)) + this._player.addEventListener('ended', () => this.eventSink(ENDED, this)) // I did not end up using the 'playing' event -- for the most part, twitch pauses videos when the buffer runs out, // which is a sufficient signal to sync up the videos again (although they don't start playing automatically again). - this._player.addEventListener('playing', () => this.eventSink('test_playing', this)) + this._player.addEventListener('playing', () => this.eventSink(TEST_PLAYING, this)) this.onready(this) } @@ -87,48 +132,12 @@ class TwitchPlayer { play() { this._player.play() } pause() { this._player.pause() } - seekToEnd() { this.seekTo(this.endTime) } - seekTo(timestamp, targetState) { - if (timestamp < this.startTime) { - console.log('Attempted to seek before the startTime, seeking start instead') - var durationSeconds = 0.001 // I think seek(0) does something wrong, so. - this.state = SEEKING_START - this._player.pause() - this._player.seek(durationSeconds) - // If we try to seek past the end time (and the end time is known), instead pause the video near the end. - } else if (this._endTime != null && timestamp >= this.endTime - VIDEO_END_BUFFER) { - var durationSeconds = (this.endTime - this.startTime - VIDEO_END_BUFFER) / 1000.0 - this.state = SEEKING_END - this._player.pause() - this._player.seek(durationSeconds) - } else { - var durationSeconds = (timestamp - this.startTime) / 1000.0 - if (durationSeconds === 0) durationSeconds = 0.001 // I think seek(0) does something wrong, so. - - if (targetState === PAUSED) { - // We don't want to pause videos which are already paused. It can cause weird behaviors if a seek is interspersed. - if (this.state !== PAUSED) this._player.pause() - this._player.seek(durationSeconds) - this.state = SEEKING_PAUSE - } else if (targetState === PLAYING) { - this._player.seek(durationSeconds) - // We don't want to pause videos which are already playing. It can cause weird behaviors if a seek is interspersed. - if (this.state !== PLAYING) this._player.play() - this.state = SEEKING_PLAY - } - } - } + seek(durationSeconds) { this._player.seek(durationSeconds) } } -class YoutubePlayer { +class YoutubePlayer extends Player { constructor(divId, videoDetails) { - this.state = LOADING - - this.streamer = videoDetails.streamer - this._startTime = videoDetails.startTime - this._endTime = videoDetails.endTime - this.offset = 0 - this.id = divId + super(divId, videoDetails) var options = { height: '100%', @@ -141,6 +150,8 @@ class YoutubePlayer { // and so that 'this' is properly defined inside the callback this._player = new YT.Player(divId, options) this._player.addEventListener('onReady', () => this.onPlayerReady()) + this._lastPauseTime = null + this._lastPlayTime = null } onPlayerReady() { @@ -150,28 +161,25 @@ class YoutubePlayer { this._player.addEventListener('onStateChange', (event) => { switch (event.data) { case YT.PlayerState.ENDED: - this.eventSink('ended', this) + this.eventSink(ENDED, this) break case YT.PlayerState.PLAYING: // Since we're not calling the youtube APIs, we don't know the video duration. // However, it should be available once the player starts playing. var durationMillis = Math.floor(this._player.getDuration() * 1000) this._endTime = this._startTime + durationMillis - this.eventSink('play', this) + this.eventSink(PLAY, this) break case YT.PlayerState.PAUSED: - this.eventSink('pause', this) + this.eventSink(PAUSE, this) break - /* - case YT.PlayerState.UNSTARTED: - case YT.PlayerState.BUFFERING: - case YT.PlayerState.CUED: - break - */ } }) this.onready(this) // Call back into index.js for the main bulk of 'readying' + + // Start the seek timer to check for manual seeks + window.setInterval(() => this._seekTimer(), 100) } get startTime() { return this._startTime + this.offset } @@ -182,37 +190,55 @@ class YoutubePlayer { return this._startTime + this.offset + durationMillis } - play() { this._player.playVideo() } - pause() { this._player.pauseVideo() } - seekToEnd() { this.seekTo(this.endTime) } - seekTo(timestamp, targetState) { - if (timestamp < this.startTime) { - console.log('Attempted to seek before the startTime, seeking start instead') - var durationSeconds = 0.001 // I think seek(0) does something wrong, so. - this.state = SEEKING_START - this._player.pauseVideo() - this._player.seekTo(durationSeconds, true) - // If we try to seek past the end time (and the end time is known), instead pause the video near the end. - } else if (this._endTime != null && timestamp >= this.endTime - VIDEO_END_BUFFER) { - var durationSeconds = (this.endTime - this.startTime - VIDEO_END_BUFFER) / 1000.0 - this.state = SEEKING_END - this._player.pauseVideo() - this._player.seekTo(durationSeconds, true) - } else { - var durationSeconds = (timestamp - this.startTime) / 1000.0 - if (durationSeconds === 0) durationSeconds = 0.001 // I think seek(0) does something wrong, so. + play() { + this._player.playVideo() + this._updateSeekAlignment(this._player.getCurrentTime()) + } + pause() { + this._player.pauseVideo() + this._updateSeekAlignment(this._player.getCurrentTime()) + } + seek(durationSeconds) { + if (this._player.getPlayerState() === YT.PlayerState.CUED) return // HACK + this._player.seekTo(durationSeconds) + this._updateSeekAlignment(durationSeconds) + } - if (targetState === PAUSED) { - // We don't want to pause videos which are already paused. It can cause weird behaviors if a seek is interspersed. - if (this.state !== PAUSED) this._player.pauseVideo() - this._player.seekTo(durationSeconds, true) - this.state = SEEKING_PAUSE - } else if (targetState === PLAYING) { - this._player.seekTo(durationSeconds, true) - // We don't want to pause videos which are already playing. It can cause weird behaviors if a seek is interspersed. - if (this.state !== PLAYING) this._player.playVideo() - this.state = SEEKING_PLAY - } + // Whenever we (knowingly) change the player's position, make a note of the 'last known timestamp'. + // We can use this below (in _seekTimer) to track if the player has been manually seeked without our knowledge. + _updateSeekAlignment(durationSeconds) { + if (this._player.getPlayerState() === YT.PlayerState.PAUSED) { + this._lastPauseTime = durationSeconds + this._lastPlayTime = null + } else if (this._player.getPlayerState() === YT.PlayerState.PLAYING) { + this._lastPauseTime = null + this._lastPlayTime = durationSeconds - new Date().getTime() + } + } + + // Unfortunately, the youtube iframe APIs don't actually provide us with a 'onSeek' event. + // The only (reliable) way of detecting a seek while paused is to just set a timer which regularly + // checks the video to see if the time has gotten out of sync with the expectation (i.e. linear time). + _seekTimer() { + var expected = 0 + if (this._player.getPlayerState() === YT.PlayerState.PAUSED) { + if (this._lastPauseTime == null) return // No known comparison time; don't take any action. + + expected = this._lastPauseTime + } else if (this._player.getPlayerState() === YT.PlayerState.PLAYING) { + if (this._lastPlayTime == null) return // No known comparison time; don't take any action. + + expected = this._lastPlayTime + new Date().getTime() + } else { + return // Unknown state + } + + var actual = this._player.getCurrentTime() + if (Math.abs(expected - actual) > 1) { + console.log('something') + this._updateSeekAlignment(actual) + var durationMillis = Math.floor(actual * 1000) + this.eventSink(SEEK, this, durationMillis) } } } \ No newline at end of file