-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Video Controls Overlay #838
Changes from all commits
59f3c10
179b309
22cff64
309b711
1f321a0
e4aef69
1d6f95b
41c7775
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
AFRAME.registerComponent("hover-menu", { | ||
multiple: true, | ||
schema: { | ||
template: { type: "selector" }, | ||
dirs: { type: "array" }, | ||
dim: { default: true } | ||
}, | ||
|
||
async init() { | ||
this.onHoverStateChange = this.onHoverStateChange.bind(this); | ||
this.onFrozenStateChange = this.onFrozenStateChange.bind(this); | ||
|
||
this.hovering = this.el.parentNode.is("hovered"); | ||
|
||
await this.getHoverMenu(); | ||
this.applyHoverState(); | ||
}, | ||
|
||
getHoverMenu() { | ||
if (this.menuPromise) return this.menuPromise; | ||
return (this.menuPromise = new Promise(resolve => { | ||
const menu = this.el.appendChild(document.importNode(this.data.template.content, true).children[0]); | ||
// we have to wait a tick for the attach callbacks to get fired for the elements in a template | ||
setTimeout(() => { | ||
this.menu = menu; | ||
this.el.setAttribute("position-at-box-shape-border", { | ||
target: ".video-toolbar", | ||
dirs: this.data.dirs, | ||
animate: false, | ||
scale: false | ||
}); | ||
resolve(this.menu); | ||
}); | ||
})); | ||
}, | ||
|
||
onFrozenStateChange(e) { | ||
if (!e.detail === "frozen") return; | ||
this.applyHoverState(); | ||
}, | ||
|
||
onHoverStateChange(e) { | ||
this.hovering = e.type === "hover-start"; | ||
this.applyHoverState(); | ||
}, | ||
|
||
applyHoverState() { | ||
if (!this.menu) return; | ||
this.menu.object3D.visible = !this.el.sceneEl.is("frozen") && this.hovering; | ||
if (this.data.dim && this.el.object3DMap.mesh && this.el.object3DMap.mesh.material) { | ||
this.el.object3DMap.mesh.material.color.setScalar(this.menu.object3D.visible ? 0.5 : 1); | ||
} | ||
}, | ||
|
||
play() { | ||
this.el.addEventListener("hover-start", this.onHoverStateChange); | ||
this.el.addEventListener("hover-end", this.onHoverStateChange); | ||
this.el.sceneEl.addEventListener("stateadded", this.onFrozenStateChange); | ||
this.el.sceneEl.addEventListener("stateremoved", this.onFrozenStateChange); | ||
}, | ||
|
||
pause() { | ||
this.el.removeEventListener("hover-start", this.onHoverStateChange); | ||
this.el.removeEventListener("hover-end", this.onHoverStateChange); | ||
this.el.sceneEl.removeEventListener("stateadded", this.onFrozenStateChange); | ||
this.el.sceneEl.removeEventListener("stateremoved", this.onFrozenStateChange); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -176,7 +176,7 @@ function fitToTexture(el, texture) { | |
el.object3DMap.mesh.scale.set(width, height, 1); | ||
el.setAttribute("shape", { | ||
shape: "box", | ||
halfExtents: { x: width / 2, y: height / 2, z: 0.05 } | ||
halfExtents: { x: width / 2, y: height / 2, z: 0.02 } | ||
}); | ||
} | ||
|
||
|
@@ -262,6 +262,18 @@ errorImage.onload = () => { | |
errorTexture.needsUpdate = true; | ||
}; | ||
|
||
function timeFmt(t) { | ||
let s = Math.floor(t), | ||
h = Math.floor(s / 3600); | ||
s -= h * 3600; | ||
let m = Math.floor(s / 60); | ||
s -= m * 60; | ||
if (h < 10) h = `0${h}`; | ||
if (m < 10) m = `0${m}`; | ||
if (s < 10) s = `0${s}`; | ||
return h === "00" ? `${m}:${s}` : `${h}:${m}:${s}`; | ||
} | ||
|
||
AFRAME.registerComponent("media-video", { | ||
schema: { | ||
src: { type: "string" }, | ||
|
@@ -287,15 +299,31 @@ AFRAME.registerComponent("media-video", { | |
this.onPauseStateChange = this.onPauseStateChange.bind(this); | ||
this.tryUpdateVideoPlaybackState = this.tryUpdateVideoPlaybackState.bind(this); | ||
|
||
this._grabStart = this._grabStart.bind(this); | ||
this._grabEnd = this._grabEnd.bind(this); | ||
this.seekForward = this.seekForward.bind(this); | ||
this.seekBack = this.seekBack.bind(this); | ||
this.togglePlaying = this.togglePlaying.bind(this); | ||
|
||
this.lastUpdate = 0; | ||
|
||
this.seekForwardButton = this.el.querySelector(".video-seek-forward-button"); | ||
this.seekBackButton = this.el.querySelector(".video-seek-back-button"); | ||
this.el.setAttribute("hover-menu__video", { template: "#video-hover-menu", dirs: ["forward", "back"] }); | ||
this.el.components["hover-menu__video"].getHoverMenu().then(menu => { | ||
// If we got removed while waiting, do nothing. | ||
if (!this.el.parentNode) return; | ||
|
||
this.hoverMenu = menu; | ||
|
||
this.playPauseButton = this.el.querySelector(".video-playpause-button"); | ||
this.seekForwardButton = this.el.querySelector(".video-seek-forward-button"); | ||
this.seekBackButton = this.el.querySelector(".video-seek-back-button"); | ||
this.timeLabel = this.el.querySelector(".video-time-label"); | ||
this.volumeLabel = this.el.querySelector(".video-volume-label"); | ||
|
||
this.playPauseButton.addEventListener("grab-start", this.togglePlaying); | ||
this.seekForwardButton.addEventListener("grab-start", this.seekForward); | ||
this.seekBackButton.addEventListener("grab-start", this.seekBack); | ||
|
||
this.updatePlaybackState(); | ||
}); | ||
|
||
NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { | ||
this.networkedEl = networkedEl; | ||
|
@@ -323,26 +351,6 @@ AFRAME.registerComponent("media-video", { | |
}); | ||
}, | ||
|
||
// aframe component play, unrelated to video | ||
play() { | ||
this.el.addEventListener("grab-start", this._grabStart); | ||
this.el.addEventListener("grab-end", this._grabEnd); | ||
this.seekForwardButton.addEventListener("grab-start", this.seekForward); | ||
this.seekBackButton.addEventListener("grab-start", this.seekBack); | ||
this.seekForwardButton.object3D.visible = !this.videoIsLive; | ||
this.seekBackButton.object3D.visible = !this.videoIsLive; | ||
}, | ||
|
||
// aframe component pause, unrelated to video | ||
pause() { | ||
this.el.removeEventListener("grab-start", this._grabStart); | ||
this.el.removeEventListener("grab-end", this._grabEnd); | ||
this.seekForwardButton.removeEventListener("grab-start", this.seekForward); | ||
this.seekBackButton.removeEventListener("grab-start", this.seekBack); | ||
this.seekForwardButton.object3D.visible = false; | ||
this.seekBackButton.object3D.visible = false; | ||
}, | ||
|
||
seekForward() { | ||
if ((!this.videoIsLive && NAF.utils.isMine(this.networkedEl)) || NAF.utils.takeOwnership(this.networkedEl)) { | ||
this.video.currentTime += 30; | ||
|
@@ -357,25 +365,8 @@ AFRAME.registerComponent("media-video", { | |
} | ||
}, | ||
|
||
_grabStart() { | ||
if (!this.el.components.grabbable || this.el.components.grabbable.data.maxGrabbers === 0) return; | ||
|
||
if (this.video && this.video.muted && !this.video.paused) { | ||
this.video.muted = false; | ||
} | ||
|
||
this.grabStartPosition = this.el.object3D.position.clone(); | ||
}, | ||
|
||
_grabEnd() { | ||
if (this.grabStartPosition && this.grabStartPosition.distanceToSquared(this.el.object3D.position) < 0.01 * 0.01) { | ||
this.togglePlayingIfOwner(); | ||
this.grabStartPosition = null; | ||
} | ||
}, | ||
|
||
togglePlayingIfOwner() { | ||
if (this.networkedEl && NAF.utils.isMine(this.networkedEl) && this.video) { | ||
togglePlaying() { | ||
if (this.networkedEl && (NAF.utils.isMine(this.networkedEl) || NAF.utils.takeOwnership(this.networkedEl))) { | ||
this.tryUpdateVideoPlaybackState(!this.data.videoPaused); | ||
} | ||
}, | ||
|
@@ -388,6 +379,11 @@ AFRAME.registerComponent("media-video", { | |
this.video.removeEventListener("pause", this.onPauseStateChange); | ||
this.video.removeEventListener("play", this.onPauseStateChange); | ||
} | ||
if (this.hoverMenu) { | ||
this.playPauseButton.removeEventListener("grab-start", this.togglePlaying); | ||
this.seekForwardButton.removeEventListener("grab-start", this.seekForward); | ||
this.seekBackButton.removeEventListener("grab-start", this.seekBack); | ||
} | ||
}, | ||
|
||
onPauseStateChange() { | ||
|
@@ -399,7 +395,13 @@ AFRAME.registerComponent("media-video", { | |
}, | ||
|
||
updatePlaybackState(force) { | ||
// Only update playback posiiton for videos you don't own | ||
if (this.hoverMenu) { | ||
this.playPauseButton.object3D.visible = !!this.video; | ||
this.seekForwardButton.object3D.visible = !!this.video && !this.videoIsLive; | ||
this.seekBackButton.object3D.visible = !!this.video && !this.videoIsLive; | ||
} | ||
|
||
// Only update playback position for videos you don't own | ||
if (force || (this.networkedEl && !NAF.utils.isMine(this.networkedEl) && this.video)) { | ||
if (Math.abs(this.data.time - this.video.currentTime) > this.data.syncTolerance) { | ||
this.tryUpdateVideoPlaybackState(this.data.videoPaused, this.data.time); | ||
|
@@ -424,6 +426,10 @@ AFRAME.registerComponent("media-video", { | |
this.video.currentTime = currentTime; | ||
} | ||
|
||
if (this.hoverMenu) { | ||
this.playPauseButton.setAttribute("icon-button", "active", pause); | ||
} | ||
|
||
if (pause) { | ||
this.video.pause(); | ||
} else { | ||
|
@@ -488,6 +494,7 @@ AFRAME.registerComponent("media-video", { | |
this.videoIsLive = texture.hls.levels[texture.hls.currentLevel].details.live; | ||
this.seekForwardButton.object3D.visible = !this.videoIsLive; | ||
this.seekBackButton.object3D.visible = !this.videoIsLive; | ||
this.timeLabel.setAttribute("text", "value", "LIVE"); | ||
}; | ||
texture.hls.on(HLS.Events.LEVEL_SWITCHED, updateLiveState); | ||
if (texture.hls.currentLevel >= 0) { | ||
|
@@ -545,26 +552,39 @@ AFRAME.registerComponent("media-video", { | |
}, | ||
|
||
tick() { | ||
if (!this.video) return; | ||
|
||
const userinput = this.el.sceneEl.systems.userinput; | ||
const volumeMod = userinput.get(paths.actions.cursor.mediaVolumeMod); | ||
if (volumeMod) { | ||
if (this.el.is("hovered") && volumeMod) { | ||
this.el.setAttribute("media-video", "volume", THREE.Math.clamp(this.data.volume + volumeMod, 0, 1)); | ||
this.volumeLabel.setAttribute( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might avoid some work with:
We have reservations for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would leave the components data in a weird state. Since this is only when adjusting volume I am not too worried about it. |
||
"text", | ||
"value", | ||
this.data.volume === 0 ? "MUTE" : `VOL: ${Math.round(this.data.volume * 100)}%` | ||
); | ||
this.volumeLabel.object3D.visible = true; | ||
clearTimeout(this.hideVolumeLabelTimeout); | ||
if (this.data.volume) { | ||
this.hideVolumeLabelTimeout = setTimeout(() => (this.volumeLabel.object3D.visible = false), 1000); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok to set a timeout every frame? I suppose people aren't messing with volume all the time... An alternative might be to write the time that the volume label should be made invisible here since tick is going to keep being called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah again, since this is only when manipulating volume I don't feel too worried about it. |
||
} | ||
} | ||
|
||
if ( | ||
this.data.videoPaused || | ||
this.videoIsLive || | ||
!this.video || | ||
!this.networkedEl || | ||
!NAF.utils.isMine(this.networkedEl) | ||
) { | ||
return; | ||
if (this.hoverMenu.object3D.visible && !this.videoIsLive) { | ||
this.timeLabel.setAttribute( | ||
"text", | ||
"value", | ||
`${timeFmt(this.video.currentTime)} / ${timeFmt(this.video.duration)}` | ||
); | ||
} | ||
|
||
const now = performance.now(); | ||
if (now - this.lastUpdate > this.data.tickRate) { | ||
this.el.setAttribute("media-video", "time", this.video.currentTime); | ||
this.lastUpdate = now; | ||
// If a non-live video is currently playing and we own it, send out time updates | ||
if (!this.data.videoPaused && !this.videoIsLive && this.networkedEl && NAF.utils.isMine(this.networkedEl)) { | ||
const now = performance.now(); | ||
if (now - this.lastUpdate > this.data.tickRate) { | ||
this.el.setAttribute("media-video", "time", this.video.currentTime); | ||
this.lastUpdate = now; | ||
} | ||
} | ||
} | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might not exist if somehow this
remove
is called before thehoverMenu
promise resolves