From 510bec655b4a48d5f5745c445ec95b8e3b117596 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 31 Mar 2022 08:14:39 +1100 Subject: [PATCH] Add marker videojs plugin (#2447) --- .../components/ScenePlayer/ScenePlayer.tsx | 21 ++++ ui/v2.5/src/components/ScenePlayer/markers.ts | 107 ++++++++++++++++++ .../src/components/ScenePlayer/styles.scss | 45 +++++++- 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 ui/v2.5/src/components/ScenePlayer/markers.ts diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 998938e1d..81626e3ad 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -6,6 +6,7 @@ import "videojs-seek-buttons"; import "videojs-landscape-fullscreen"; import "./live"; import "./PlaylistButtons"; +import "./markers"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; @@ -136,6 +137,7 @@ export const ScenePlayer: React.FC = ({ volumePanel: { inline: false, }, + chaptersButton: false, }, nativeControlsForTouch: false, playbackRates: [0.75, 1, 1.5, 2, 3, 4], @@ -160,6 +162,7 @@ export const ScenePlayer: React.FC = ({ }, }); + (player as any).markers(); (player as any).offset(); player.focus(); @@ -270,6 +273,12 @@ export const ScenePlayer: React.FC = ({ // otherwise, the offset will be applied to the next file when // currentTime is called. (player as any).clearOffsetDuration(); + + const tracks = player.remoteTextTracks(); + if (tracks.length > 0) { + player.removeRemoteTextTrack(tracks[0] as any); + } + player.src( scene.sceneStreams.map((stream) => ({ src: stream.url, @@ -277,6 +286,18 @@ export const ScenePlayer: React.FC = ({ label: stream.label ?? undefined, })) ); + + if (scene.paths.chapters_vtt) { + player.addRemoteTextTrack( + { + src: scene.paths.chapters_vtt, + kind: "chapters", + default: true, + }, + true + ); + } + player.currentTime(0); player.loop( diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts new file mode 100644 index 000000000..d61ed9b0d --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -0,0 +1,107 @@ +import videojs, { VideoJsPlayer } from "video.js"; + +const markers = function (this: VideoJsPlayer) { + const player = this; + + function getPosition(marker: VTTCue) { + return (marker.startTime / player.duration()) * 100; + } + + function createMarkerToolTip() { + const tooltip = videojs.dom.createEl("div") as HTMLElement; + tooltip.className = "vjs-marker-tooltip"; + + return tooltip; + } + + function removeMarkerToolTip() { + const div = player + .el() + .querySelector(".vjs-progress-holder .vjs-marker-tooltip"); + if (div) div.remove(); + } + + function createMarkerDiv(marker: VTTCue) { + const markerDiv = videojs.dom.createEl( + "div", + {}, + { + "data-marker-time": marker.startTime, + } + ) as HTMLElement; + + markerDiv.className = "vjs-marker"; + markerDiv.style.left = getPosition(marker) + "%"; + + // bind click event to seek to marker time + markerDiv.addEventListener("click", function () { + const time = this.getAttribute("data-marker-time"); + player.currentTime(Number(time)); + }); + + // show tooltip on hover + markerDiv.addEventListener("mouseenter", function () { + // create and show tooltip + const tooltip = createMarkerToolTip(); + tooltip.innerText = marker.text; + + const parent = player + .el() + .querySelector(".vjs-progress-holder .vjs-mouse-display"); + + parent?.appendChild(tooltip); + + // hide default tooltip + const defaultTooltip = parent?.querySelector( + ".vjs-time-tooltip" + ) as HTMLElement; + defaultTooltip.style.visibility = "hidden"; + }); + + markerDiv.addEventListener("mouseout", function () { + removeMarkerToolTip(); + + // show default tooltip + const defaultTooltip = player + .el() + .querySelector( + ".vjs-progress-holder .vjs-mouse-display .vjs-time-tooltip" + ) as HTMLElement; + if (defaultTooltip) defaultTooltip.style.visibility = "visible"; + }); + + return markerDiv; + } + + function removeMarkerDivs() { + const divs = player + .el() + .querySelectorAll(".vjs-progress-holder .vjs-marker"); + divs.forEach((div) => { + div.remove(); + }); + } + + this.on("loadedmetadata", function () { + removeMarkerDivs(); + removeMarkerToolTip(); + + const textTracks = player.remoteTextTracks(); + const seekBar = player.el().querySelector(".vjs-progress-holder"); + + if (seekBar && textTracks.length > 0) { + const vttTrack = textTracks[0]; + if (!vttTrack || !vttTrack.cues) return; + for (let i = 0; i < vttTrack.cues.length; i++) { + const cue = vttTrack.cues[i]; + const markerDiv = createMarkerDiv(cue as VTTCue); + seekBar.appendChild(markerDiv); + } + } + }); +}; + +// Register the plugin with video.js. +videojs.registerPlugin("markers", markers); + +export default markers; diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index a557ab912..6ed2e7831 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -153,8 +153,9 @@ $sceneTabWidth: 450px; font-size: 10em; } - .jwplayer { - outline: none; + .vjs-progress-control .vjs-play-progress .vjs-time-tooltip, + .vjs-progress-control:hover .vjs-play-progress .vjs-time-tooltip { + visibility: hidden; } } @@ -393,3 +394,43 @@ $sceneTabWidth: 450px; display: inline-block; } } + +.vjs-marker { + background-color: rgba(33, 33, 33, 0.8); + bottom: 0; + height: 100%; + left: 0; + opacity: 1; + position: absolute; + -webkit-transition: opacity 0.2s ease; + -moz-transition: opacity 0.2s ease; + transition: opacity 0.2s ease; + width: 6px; + z-index: 100; + + &:hover { + cursor: pointer; + -webkit-transform: scale(1.3, 1.3); + -moz-transform: scale(1.3, 1.3); + -o-transform: scale(1.3, 1.3); + -ms-transform: scale(1.3, 1.3); + transform: scale(1.3, 1.3); + } +} + +.vjs-marker-tooltip { + border-radius: 0.3em; + color: white; + display: block; + float: right; + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + height: 50px; + padding: 6px 8px 8px 8px; + pointer-events: none; + position: absolute; + right: -80px; + top: -5.4em; + width: 160px; + z-index: 1; +}