diff options
author | Chocobozzz <me@florianbigard.com> | 2023-08-28 10:55:04 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-28 16:17:31 +0200 |
commit | 77b70702d2193d78bf6fbd07f0fc7335e34957f8 (patch) | |
tree | 1a0aed540054286c9a8b10c4890cc0f718e00458 /client/src/assets | |
parent | 7113f32a87bd6b2868154fed20bde1a1633c190e (diff) | |
download | PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.tar.gz PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.tar.zst PeerTube-77b70702d2193d78bf6fbd07f0fc7335e34957f8.zip |
Add video chapters support
Diffstat (limited to 'client/src/assets')
8 files changed, 136 insertions, 3 deletions
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' | |||
7 | import './shared/peertube/peertube-plugin' | 7 | import './shared/peertube/peertube-plugin' |
8 | import './shared/resolutions/peertube-resolutions-plugin' | 8 | import './shared/resolutions/peertube-resolutions-plugin' |
9 | import './shared/control-bar/storyboard-plugin' | 9 | import './shared/control-bar/storyboard-plugin' |
10 | import './shared/control-bar/chapters-plugin' | ||
11 | import './shared/control-bar/time-tooltip' | ||
10 | import './shared/control-bar/next-previous-video-button' | 12 | import './shared/control-bar/next-previous-video-button' |
11 | import './shared/control-bar/p2p-info-button' | 13 | import './shared/control-bar/p2p-info-button' |
12 | import './shared/control-bar/peertube-link-button' | 14 | import './shared/control-bar/peertube-link-button' |
@@ -227,6 +229,7 @@ export class PeerTubePlayer { | |||
227 | if (this.player.usingPlugin('upnext')) this.player.upnext().dispose() | 229 | if (this.player.usingPlugin('upnext')) this.player.upnext().dispose() |
228 | if (this.player.usingPlugin('stats')) this.player.stats().dispose() | 230 | if (this.player.usingPlugin('stats')) this.player.stats().dispose() |
229 | if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose() | 231 | if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose() |
232 | if (this.player.usingPlugin('chapters')) this.player.chapters().dispose() | ||
230 | 233 | ||
231 | if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose() | 234 | if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose() |
232 | 235 | ||
@@ -273,6 +276,10 @@ export class PeerTubePlayer { | |||
273 | this.player.storyboard(this.currentLoadOptions.storyboard) | 276 | this.player.storyboard(this.currentLoadOptions.storyboard) |
274 | } | 277 | } |
275 | 278 | ||
279 | if (this.currentLoadOptions.videoChapters) { | ||
280 | this.player.chapters({ chapters: this.currentLoadOptions.videoChapters }) | ||
281 | } | ||
282 | |||
276 | if (this.currentLoadOptions.dock) { | 283 | if (this.currentLoadOptions.dock) { |
277 | this.player.peertubeDock(this.currentLoadOptions.dock) | 284 | this.player.peertubeDock(this.currentLoadOptions.dock) |
278 | } | 285 | } |
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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { ChaptersOptions } from '../../types' | ||
3 | import { VideoChapter } from '@peertube/peertube-models' | ||
4 | import { ProgressBarMarkerComponent } from './progress-bar-marker-component' | ||
5 | |||
6 | const Plugin = videojs.getPlugin('plugin') | ||
7 | |||
8 | class ChaptersPlugin extends Plugin { | ||
9 | private chapters: VideoChapter[] = [] | ||
10 | private markers: ProgressBarMarkerComponent[] = [] | ||
11 | |||
12 | constructor (player: videojs.Player, options: videojs.ComponentOptions & ChaptersOptions) { | ||
13 | super(player, options) | ||
14 | |||
15 | this.chapters = options.chapters | ||
16 | |||
17 | this.player.ready(() => { | ||
18 | player.addClass('vjs-chapters') | ||
19 | |||
20 | this.player.one('durationchange', () => { | ||
21 | for (const chapter of this.chapters) { | ||
22 | if (chapter.timecode === 0) continue | ||
23 | |||
24 | const marker = new ProgressBarMarkerComponent(player, { timecode: chapter.timecode }) | ||
25 | |||
26 | this.markers.push(marker) | ||
27 | this.getSeekBar().addChild(marker) | ||
28 | } | ||
29 | }) | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | dispose () { | ||
34 | for (const marker of this.markers) { | ||
35 | this.getSeekBar().removeChild(marker) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | getChapter (timecode: number) { | ||
40 | if (this.chapters.length !== 0) { | ||
41 | for (let i = this.chapters.length - 1; i >= 0; i--) { | ||
42 | const chapter = this.chapters[i] | ||
43 | |||
44 | if (chapter.timecode <= timecode) { | ||
45 | this.player.addClass('has-chapter') | ||
46 | |||
47 | return chapter.title | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | this.player.removeClass('has-chapter') | ||
53 | |||
54 | return '' | ||
55 | } | ||
56 | |||
57 | private getSeekBar () { | ||
58 | return this.player.getDescendant('ControlBar', 'ProgressControl', 'SeekBar') | ||
59 | } | ||
60 | } | ||
61 | |||
62 | videojs.registerPlugin('chapters', ChaptersPlugin) | ||
63 | |||
64 | 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 @@ | |||
1 | export * from './chapters-plugin' | ||
1 | export * from './next-previous-video-button' | 2 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 3 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 4 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | 5 | export * from './peertube-live-display' |
5 | export * from './storyboard-plugin' | 6 | export * from './storyboard-plugin' |
6 | export * from './theater-button' | 7 | export * from './theater-button' |
8 | 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { ProgressBarMarkerComponentOptions } from '../../types' | ||
3 | |||
4 | const Component = videojs.getComponent('Component') | ||
5 | |||
6 | export class ProgressBarMarkerComponent extends Component { | ||
7 | options_: ProgressBarMarkerComponentOptions & videojs.ComponentOptions | ||
8 | |||
9 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor | ||
10 | constructor (player: videojs.Player, options?: ProgressBarMarkerComponentOptions & videojs.ComponentOptions) { | ||
11 | super(player, options) | ||
12 | } | ||
13 | |||
14 | createEl () { | ||
15 | const left = (this.options_.timecode / this.player().duration()) * 100 | ||
16 | |||
17 | return videojs.dom.createEl('span', { | ||
18 | className: 'vjs-marker', | ||
19 | style: `left: ${left}%` | ||
20 | }) as HTMLButtonElement | ||
21 | } | ||
22 | } | ||
23 | |||
24 | 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 { | |||
141 | const ctop = Math.floor(position / columns) * -scaledHeight | 141 | const ctop = Math.floor(position / columns) * -scaledHeight |
142 | 142 | ||
143 | const bgSize = `${imgWidth * scaleFactor}px ${imgHeight * scaleFactor}px` | 143 | const bgSize = `${imgWidth * scaleFactor}px ${imgHeight * scaleFactor}px` |
144 | const topOffset = -scaledHeight - 60 | 144 | |
145 | const timeTooltip = this.player.el().querySelector('.vjs-time-tooltip') | ||
146 | const topOffset = -scaledHeight + parseInt(getComputedStyle(timeTooltip).top.replace('px', '')) - 20 | ||
145 | 147 | ||
146 | const previewHalfSize = Math.round(scaledWidth / 2) | 148 | const previewHalfSize = Math.round(scaledWidth / 2) |
147 | let left = seekBarRect.width * seekBarX - previewHalfSize | 149 | 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 @@ | |||
1 | import { timeToInt } from '@peertube/peertube-core-utils' | ||
2 | import videojs, { VideoJsPlayer } from 'video.js' | ||
3 | |||
4 | const TimeToolTip = videojs.getComponent('TimeTooltip') as any // FIXME: typings don't have write method | ||
5 | |||
6 | class TimeTooltip extends TimeToolTip { | ||
7 | |||
8 | write (timecode: string) { | ||
9 | const player: VideoJsPlayer = this.player() | ||
10 | |||
11 | if (player.usingPlugin('chapters')) { | ||
12 | const chapterTitle = player.chapters().getChapter(timeToInt(timecode)) | ||
13 | if (chapterTitle) return super.write(chapterTitle + '\r\n' + timecode) | ||
14 | } | ||
15 | |||
16 | return super.write(timecode) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | 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 @@ | |||
1 | import { LiveVideoLatencyModeType, VideoFile } from '@peertube/peertube-models' | 1 | import { LiveVideoLatencyModeType, VideoChapter, VideoFile } from '@peertube/peertube-models' |
2 | import { PluginsManager } from '@root-helpers/plugins-manager' | 2 | import { PluginsManager } from '@root-helpers/plugins-manager' |
3 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 3 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
4 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' | 4 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' |
@@ -68,6 +68,7 @@ export type PeerTubePlayerLoadOptions = { | |||
68 | } | 68 | } |
69 | 69 | ||
70 | videoCaptions: VideoJSCaption[] | 70 | videoCaptions: VideoJSCaption[] |
71 | videoChapters: VideoChapter[] | ||
71 | storyboard: VideoJSStoryboard | 72 | storyboard: VideoJSStoryboard |
72 | 73 | ||
73 | videoUUID: string | 74 | 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 @@ | |||
1 | import { HlsConfig, Level } from 'hls.js' | 1 | import { HlsConfig, Level } from 'hls.js' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models' | 4 | import { VideoChapter, VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models' |
5 | import { BezelsPlugin } from '../shared/bezels/bezels-plugin' | 5 | import { BezelsPlugin } from '../shared/bezels/bezels-plugin' |
6 | import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin' | 6 | import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin' |
7 | import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 7 | import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
@@ -19,6 +19,7 @@ import { UpNextPlugin } from '../shared/upnext/upnext-plugin' | |||
19 | import { WebVideoPlugin } from '../shared/web-video/web-video-plugin' | 19 | import { WebVideoPlugin } from '../shared/web-video/web-video-plugin' |
20 | import { PlayerMode } from './peertube-player-options' | 20 | import { PlayerMode } from './peertube-player-options' |
21 | import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator' | 21 | import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator' |
22 | import { ChaptersPlugin } from '../shared/control-bar/chapters-plugin' | ||
22 | 23 | ||
23 | declare module 'video.js' { | 24 | declare module 'video.js' { |
24 | 25 | ||
@@ -62,6 +63,8 @@ declare module 'video.js' { | |||
62 | 63 | ||
63 | peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin | 64 | peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin |
64 | 65 | ||
66 | chapters (options?: ChaptersOptions): ChaptersPlugin | ||
67 | |||
65 | upnext (options?: UpNextPluginOptions): UpNextPlugin | 68 | upnext (options?: UpNextPluginOptions): UpNextPlugin |
66 | 69 | ||
67 | playlist (options?: PlaylistPluginOptions): PlaylistPlugin | 70 | playlist (options?: PlaylistPluginOptions): PlaylistPlugin |
@@ -142,6 +145,10 @@ type StoryboardOptions = { | |||
142 | interval: number | 145 | interval: number |
143 | } | 146 | } |
144 | 147 | ||
148 | type ChaptersOptions = { | ||
149 | chapters: VideoChapter[] | ||
150 | } | ||
151 | |||
145 | type PlaylistPluginOptions = { | 152 | type PlaylistPluginOptions = { |
146 | elements: VideoPlaylistElement[] | 153 | elements: VideoPlaylistElement[] |
147 | 154 | ||
@@ -161,6 +168,10 @@ type UpNextPluginOptions = { | |||
161 | isSuspended: () => boolean | 168 | isSuspended: () => boolean |
162 | } | 169 | } |
163 | 170 | ||
171 | type ProgressBarMarkerComponentOptions = { | ||
172 | timecode: number | ||
173 | } | ||
174 | |||
164 | type NextPreviousVideoButtonOptions = { | 175 | type NextPreviousVideoButtonOptions = { |
165 | type: 'next' | 'previous' | 176 | type: 'next' | 'previous' |
166 | handler?: () => void | 177 | handler?: () => void |
@@ -273,6 +284,7 @@ export { | |||
273 | NextPreviousVideoButtonOptions, | 284 | NextPreviousVideoButtonOptions, |
274 | ResolutionUpdateData, | 285 | ResolutionUpdateData, |
275 | AutoResolutionUpdateData, | 286 | AutoResolutionUpdateData, |
287 | ProgressBarMarkerComponentOptions, | ||
276 | PlaylistPluginOptions, | 288 | PlaylistPluginOptions, |
277 | MetricsPluginOptions, | 289 | MetricsPluginOptions, |
278 | VideoJSCaption, | 290 | VideoJSCaption, |
@@ -284,5 +296,6 @@ export { | |||
284 | UpNextPluginOptions, | 296 | UpNextPluginOptions, |
285 | LoadedQualityData, | 297 | LoadedQualityData, |
286 | StoryboardOptions, | 298 | StoryboardOptions, |
299 | ChaptersOptions, | ||
287 | PeerTubeLinkButtonOptions | 300 | PeerTubeLinkButtonOptions |
288 | } | 301 | } |