Skip to content
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

Merged
merged 8 commits into from
Jan 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/assets/video-overlay/pause-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/pause.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/play-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/play.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/seek_back-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/seek_back.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/seek_fwd-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/video-overlay/seek_fwd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions src/components/hover-menu.js
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);
}
});
4 changes: 2 additions & 2 deletions src/components/media-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ AFRAME.registerComponent("media-loader", {
"media-video",
Object.assign({}, this.data.mediaOptions, { src: accessibleUrl, time: startTime, contentType })
);
if (this.el.components["position-at-box-shape-border"]) {
this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] });
if (this.el.components["position-at-box-shape-border__freeze"]) {
this.el.setAttribute("position-at-box-shape-border__freeze", { dirs: ["forward", "back"] });
}
} else if (contentType.startsWith("image/")) {
this.el.removeAttribute("gltf-model-plus");
Expand Down
136 changes: 78 additions & 58 deletions src/components/media-views.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
});
}

Expand Down Expand Up @@ -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" },
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
},
Expand All @@ -388,6 +379,11 @@ AFRAME.registerComponent("media-video", {
this.video.removeEventListener("pause", this.onPauseStateChange);
this.video.removeEventListener("play", this.onPauseStateChange);
}
if (this.hoverMenu) {
Copy link
Contributor

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 the hoverMenu promise resolves

this.playPauseButton.removeEventListener("grab-start", this.togglePlaying);
this.seekForwardButton.removeEventListener("grab-start", this.seekForward);
this.seekBackButton.removeEventListener("grab-start", this.seekBack);
}
},

onPauseStateChange() {
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might avoid some work with:

      this.volumeLabel.components.text.updateGeometry(this.geometry, font);
      this.volumeLabel.components.text.updateLayout();

We have reservations for setAttribute and also the update for the text component does some checks that aren't necessary for you (because you aren't changing font or shader or texture here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
}
}
});
Expand Down
8 changes: 5 additions & 3 deletions src/components/position-at-box-shape-border.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ AFRAME.registerComponent("position-at-box-shape-border", {
multiple: true,
schema: {
target: { type: "string" },
dirs: { default: ["left", "right", "forward", "back"] }
dirs: { default: ["left", "right", "forward", "back"] },
animate: { default: true },
scale: { default: true }
},

init() {
Expand Down Expand Up @@ -87,7 +89,7 @@ AFRAME.registerComponent("position-at-box-shape-border", {
// If the target is being shown or the scale changed while the opening animation is being run,
// we need to start or re-start the animation.
if (opening || (scaleChanged && isAnimating)) {
this._updateBox(true);
this._updateBox(this.data.animate);
}

this.wasVisible = isVisible;
Expand Down Expand Up @@ -151,7 +153,7 @@ AFRAME.registerComponent("position-at-box-shape-border", {
const distance = Math.sqrt(minSquareDistance);
const scale = this.halfExtents[inverseHalfExtents[targetHalfExtentStr]] * distance;
const targetScale = Math.min(2.0, Math.max(0.5, scale * tempParentWorldScale.x));
const finalScale = targetScale / tempParentWorldScale.x;
const finalScale = this.data.scale ? targetScale / tempParentWorldScale.x : 1;

if (animate) {
this.targetEl.removeAttribute("animation__show");
Expand Down
Loading