From d262d18f080a76578af72908f64638aef68959bd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 1 Apr 2022 08:20:14 +1100 Subject: [PATCH] Add source selector plugin (#2449) --- .../components/ScenePlayer/ScenePlayer.tsx | 2 + ui/v2.5/src/components/ScenePlayer/live.ts | 15 +- .../components/ScenePlayer/source-selector.ts | 150 ++++++++++++++++++ .../src/components/ScenePlayer/styles.scss | 11 ++ 4 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 ui/v2.5/src/components/ScenePlayer/source-selector.ts diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 1d538a204..0c58f92d2 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 "./source-selector"; import "./persist-volume"; import "./markers"; import cx from "classnames"; @@ -165,6 +166,7 @@ export const ScenePlayer: React.FC = ({ (player as any).markers(); (player as any).offset(); + (player as any).sourceSelector(); (player as any).persistVolume(); player.focus(); diff --git a/ui/v2.5/src/components/ScenePlayer/live.ts b/ui/v2.5/src/components/ScenePlayer/live.ts index af1081b2d..aae1e81d7 100644 --- a/ui/v2.5/src/components/ScenePlayer/live.ts +++ b/ui/v2.5/src/components/ScenePlayer/live.ts @@ -36,10 +36,17 @@ const offset = function (this: VideoJsPlayer) { const srcUrl = new URL(this.src()); srcUrl.searchParams.delete("start"); srcUrl.searchParams.append("start", seconds.toString()); - this.src({ - src: srcUrl.toString(), - type: "video/webm", - }); + const currentSrc = this.currentSource(); + const newSources = this.currentSources().map( + (source: videojs.Tech.SourceObject) => { + return { + ...source, + src: + source.src === currentSrc.src ? srcUrl.toString() : source.src, + }; + } + ); + this.src(newSources); this.play(); return seconds; diff --git a/ui/v2.5/src/components/ScenePlayer/source-selector.ts b/ui/v2.5/src/components/ScenePlayer/source-selector.ts new file mode 100644 index 000000000..772f21ad4 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/source-selector.ts @@ -0,0 +1,150 @@ +import videojs, { VideoJsPlayer } from "video.js"; + +interface ISource extends videojs.Tech.SourceObject { + label?: string; + selected?: boolean; + sortIndex?: number; +} + +const MenuButton = videojs.getComponent("MenuButton"); +const MenuItem = videojs.getComponent("MenuItem"); + +class SourceMenuItem extends MenuItem { + private parent: SourceMenuButton; + public source: ISource; + public index: number; + + constructor( + parent: SourceMenuButton, + source: ISource, + index: number, + player: VideoJsPlayer, + options: videojs.MenuItemOptions + ) { + options.selectable = true; + options.multiSelectable = false; + + super(player, options); + + this.parent = parent; + this.source = source; + this.index = index; + } + + handleClick() { + this.parent.trigger("selected", this); + } +} + +class SourceMenuButton extends MenuButton { + createEl() { + return videojs.dom.createEl("div", { + className: + "vjs-source-selector vjs-menu-button vjs-menu-button-popup vjs-control vjs-button", + }); + } + + createItems() { + const player = this.player(); + const menuButton = this; + + // slice so that we don't alter the order of the original array + const sources = player.currentSources().slice() as ISource[]; + + sources.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); + + const hasSelected = sources.some((source) => source.selected); + if (!hasSelected && sources.length > 0) { + sources[0].selected = true; + } + + menuButton.on("selected", function (e, selectedItem) { + // don't do anything if re-selecting the same source + if (selectedItem.source.selected) { + return; + } + + // populate source sortIndex first if not present + const currentSources = (player.currentSources() as ISource[]).map( + (src, i) => { + return { + ...src, + sortIndex: src.sortIndex ?? i, + selected: false, + }; + } + ); + + // put the selected source at the top of the list + const selectedIndex = currentSources.findIndex( + (src) => src.sortIndex === selectedItem.index + ); + const selectedSrc = currentSources.splice(selectedIndex, 1)[0]; + selectedSrc.selected = true; + currentSources.unshift(selectedSrc); + + const currentTime = player.currentTime(); + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (player as any).clearOffsetDuration(); + player.src(currentSources); + player.currentTime(currentTime); + player.play(); + }); + + return sources.map((source, index) => { + const label = source.label || source.type; + const item = new SourceMenuItem( + menuButton, + source, + index, + this.player(), + { + label: label, + selected: source.selected || (!hasSelected && index === 0), + } + ); + + menuButton.on("selected", function (selectedItem) { + if (selectedItem !== item) { + item.selected(false); + } + }); + + item.addClass("vjs-source-menu-item"); + + return item; + }); + } +} + +const sourceSelector = function (this: VideoJsPlayer) { + const player = this; + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + const PlayerConstructor = this.constructor as any; + if (!PlayerConstructor.__sourceSelector) { + PlayerConstructor.__sourceSelector = { + selectSource: PlayerConstructor.prototype.selectSource, + }; + } + + videojs.registerComponent("SourceMenuButton", SourceMenuButton); + + player.on("loadedmetadata", function () { + const { controlBar } = player; + const fullscreenToggle = controlBar.getChild("fullscreenToggle")!.el(); + + const existingMenuButton = controlBar.getChild("SourceMenuButton"); + if (existingMenuButton) controlBar.removeChild(existingMenuButton); + + const menuButton = controlBar.addChild("SourceMenuButton"); + + controlBar.el().insertBefore(menuButton.el(), fullscreenToggle); + }); +}; + +// Register the plugin with video.js. +videojs.registerPlugin("sourceSelector", sourceSelector); + +export default sourceSelector; diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 6ed2e7831..c500ae549 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -395,6 +395,17 @@ $sceneTabWidth: 450px; } } +.vjs-source-selector { + .vjs-menu li { + font-size: 12px; + } + + .vjs-button > .vjs-icon-placeholder::before { + content: "\f110"; + font-family: VideoJS; + } +} + .vjs-marker { background-color: rgba(33, 33, 33, 0.8); bottom: 0;