From 77b70702d2193d78bf6fbd07f0fc7335e34957f8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 28 Aug 2023 10:55:04 +0200 Subject: Add video chapters support --- client/src/assets/player/peertube-player.ts | 7 +++ .../player/shared/control-bar/chapters-plugin.ts | 64 ++++++++++++++++++++++ .../src/assets/player/shared/control-bar/index.ts | 2 + .../control-bar/progress-bar-marker-component.ts | 24 ++++++++ .../player/shared/control-bar/storyboard-plugin.ts | 4 +- .../player/shared/control-bar/time-tooltip.ts | 20 +++++++ .../assets/player/types/peertube-player-options.ts | 3 +- .../player/types/peertube-videojs-typings.ts | 15 ++++- 8 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 client/src/assets/player/shared/control-bar/chapters-plugin.ts create mode 100644 client/src/assets/player/shared/control-bar/progress-bar-marker-component.ts create mode 100644 client/src/assets/player/shared/control-bar/time-tooltip.ts (limited to 'client/src/assets') diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts index 111b4645b..192b2e124 100644 --- a/client/src/assets/player/peertube-player.ts +++ b/client/src/assets/player/peertube-player.ts @@ -7,6 +7,8 @@ import './shared/bezels/bezels-plugin' import './shared/peertube/peertube-plugin' import './shared/resolutions/peertube-resolutions-plugin' import './shared/control-bar/storyboard-plugin' +import './shared/control-bar/chapters-plugin' +import './shared/control-bar/time-tooltip' import './shared/control-bar/next-previous-video-button' import './shared/control-bar/p2p-info-button' import './shared/control-bar/peertube-link-button' @@ -227,6 +229,7 @@ export class PeerTubePlayer { if (this.player.usingPlugin('upnext')) this.player.upnext().dispose() if (this.player.usingPlugin('stats')) this.player.stats().dispose() if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose() + if (this.player.usingPlugin('chapters')) this.player.chapters().dispose() if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose() @@ -273,6 +276,10 @@ export class PeerTubePlayer { this.player.storyboard(this.currentLoadOptions.storyboard) } + if (this.currentLoadOptions.videoChapters) { + this.player.chapters({ chapters: this.currentLoadOptions.videoChapters }) + } + if (this.currentLoadOptions.dock) { this.player.peertubeDock(this.currentLoadOptions.dock) } diff --git a/client/src/assets/player/shared/control-bar/chapters-plugin.ts b/client/src/assets/player/shared/control-bar/chapters-plugin.ts new file mode 100644 index 000000000..5be081694 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/chapters-plugin.ts @@ -0,0 +1,64 @@ +import videojs from 'video.js' +import { ChaptersOptions } from '../../types' +import { VideoChapter } from '@peertube/peertube-models' +import { ProgressBarMarkerComponent } from './progress-bar-marker-component' + +const Plugin = videojs.getPlugin('plugin') + +class ChaptersPlugin extends Plugin { + private chapters: VideoChapter[] = [] + private markers: ProgressBarMarkerComponent[] = [] + + constructor (player: videojs.Player, options: videojs.ComponentOptions & ChaptersOptions) { + super(player, options) + + this.chapters = options.chapters + + this.player.ready(() => { + player.addClass('vjs-chapters') + + this.player.one('durationchange', () => { + for (const chapter of this.chapters) { + if (chapter.timecode === 0) continue + + const marker = new ProgressBarMarkerComponent(player, { timecode: chapter.timecode }) + + this.markers.push(marker) + this.getSeekBar().addChild(marker) + } + }) + }) + } + + dispose () { + for (const marker of this.markers) { + this.getSeekBar().removeChild(marker) + } + } + + getChapter (timecode: number) { + if (this.chapters.length !== 0) { + for (let i = this.chapters.length - 1; i >= 0; i--) { + const chapter = this.chapters[i] + + if (chapter.timecode <= timecode) { + this.player.addClass('has-chapter') + + return chapter.title + } + } + } + + this.player.removeClass('has-chapter') + + return '' + } + + private getSeekBar () { + return this.player.getDescendant('ControlBar', 'ProgressControl', 'SeekBar') + } +} + +videojs.registerPlugin('chapters', ChaptersPlugin) + +export { ChaptersPlugin } diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index 9307027f6..091e876e2 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts @@ -1,6 +1,8 @@ +export * from './chapters-plugin' export * from './next-previous-video-button' export * from './p2p-info-button' export * from './peertube-link-button' export * from './peertube-live-display' export * from './storyboard-plugin' export * from './theater-button' +export * from './time-tooltip' diff --git a/client/src/assets/player/shared/control-bar/progress-bar-marker-component.ts b/client/src/assets/player/shared/control-bar/progress-bar-marker-component.ts new file mode 100644 index 000000000..50965ec71 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/progress-bar-marker-component.ts @@ -0,0 +1,24 @@ +import videojs from 'video.js' +import { ProgressBarMarkerComponentOptions } from '../../types' + +const Component = videojs.getComponent('Component') + +export class ProgressBarMarkerComponent extends Component { + options_: ProgressBarMarkerComponentOptions & videojs.ComponentOptions + + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor (player: videojs.Player, options?: ProgressBarMarkerComponentOptions & videojs.ComponentOptions) { + super(player, options) + } + + createEl () { + const left = (this.options_.timecode / this.player().duration()) * 100 + + return videojs.dom.createEl('span', { + className: 'vjs-marker', + style: `left: ${left}%` + }) as HTMLButtonElement + } +} + +videojs.registerComponent('ProgressBarMarkerComponent', ProgressBarMarkerComponent) diff --git a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts index 80c69b5f2..91d7f451e 100644 --- a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts +++ b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts @@ -141,7 +141,9 @@ class StoryboardPlugin extends Plugin { const ctop = Math.floor(position / columns) * -scaledHeight const bgSize = `${imgWidth * scaleFactor}px ${imgHeight * scaleFactor}px` - const topOffset = -scaledHeight - 60 + + const timeTooltip = this.player.el().querySelector('.vjs-time-tooltip') + const topOffset = -scaledHeight + parseInt(getComputedStyle(timeTooltip).top.replace('px', '')) - 20 const previewHalfSize = Math.round(scaledWidth / 2) let left = seekBarRect.width * seekBarX - previewHalfSize diff --git a/client/src/assets/player/shared/control-bar/time-tooltip.ts b/client/src/assets/player/shared/control-bar/time-tooltip.ts new file mode 100644 index 000000000..2ed4f9acd --- /dev/null +++ b/client/src/assets/player/shared/control-bar/time-tooltip.ts @@ -0,0 +1,20 @@ +import { timeToInt } from '@peertube/peertube-core-utils' +import videojs, { VideoJsPlayer } from 'video.js' + +const TimeToolTip = videojs.getComponent('TimeTooltip') as any // FIXME: typings don't have write method + +class TimeTooltip extends TimeToolTip { + + write (timecode: string) { + const player: VideoJsPlayer = this.player() + + if (player.usingPlugin('chapters')) { + const chapterTitle = player.chapters().getChapter(timeToInt(timecode)) + if (chapterTitle) return super.write(chapterTitle + '\r\n' + timecode) + } + + return super.write(timecode) + } +} + +videojs.registerComponent('TimeTooltip', TimeTooltip) diff --git a/client/src/assets/player/types/peertube-player-options.ts b/client/src/assets/player/types/peertube-player-options.ts index 6fb2f7913..32f26fa9e 100644 --- a/client/src/assets/player/types/peertube-player-options.ts +++ b/client/src/assets/player/types/peertube-player-options.ts @@ -1,4 +1,4 @@ -import { LiveVideoLatencyModeType, VideoFile } from '@peertube/peertube-models' +import { LiveVideoLatencyModeType, VideoChapter, VideoFile } from '@peertube/peertube-models' import { PluginsManager } from '@root-helpers/plugins-manager' import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' @@ -68,6 +68,7 @@ export type PeerTubePlayerLoadOptions = { } videoCaptions: VideoJSCaption[] + videoChapters: VideoChapter[] storyboard: VideoJSStoryboard videoUUID: string diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index 27fbda31d..6293404ab 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts @@ -1,7 +1,7 @@ import { HlsConfig, Level } from 'hls.js' import videojs from 'video.js' import { Engine } from '@peertube/p2p-media-loader-hlsjs' -import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models' +import { VideoChapter, VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models' import { BezelsPlugin } from '../shared/bezels/bezels-plugin' import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin' import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' @@ -19,6 +19,7 @@ import { UpNextPlugin } from '../shared/upnext/upnext-plugin' import { WebVideoPlugin } from '../shared/web-video/web-video-plugin' import { PlayerMode } from './peertube-player-options' import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator' +import { ChaptersPlugin } from '../shared/control-bar/chapters-plugin' declare module 'video.js' { @@ -62,6 +63,8 @@ declare module 'video.js' { peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin + chapters (options?: ChaptersOptions): ChaptersPlugin + upnext (options?: UpNextPluginOptions): UpNextPlugin playlist (options?: PlaylistPluginOptions): PlaylistPlugin @@ -142,6 +145,10 @@ type StoryboardOptions = { interval: number } +type ChaptersOptions = { + chapters: VideoChapter[] +} + type PlaylistPluginOptions = { elements: VideoPlaylistElement[] @@ -161,6 +168,10 @@ type UpNextPluginOptions = { isSuspended: () => boolean } +type ProgressBarMarkerComponentOptions = { + timecode: number +} + type NextPreviousVideoButtonOptions = { type: 'next' | 'previous' handler?: () => void @@ -273,6 +284,7 @@ export { NextPreviousVideoButtonOptions, ResolutionUpdateData, AutoResolutionUpdateData, + ProgressBarMarkerComponentOptions, PlaylistPluginOptions, MetricsPluginOptions, VideoJSCaption, @@ -284,5 +296,6 @@ export { UpNextPluginOptions, LoadedQualityData, StoryboardOptions, + ChaptersOptions, PeerTubeLinkButtonOptions } -- cgit v1.2.3