diff options
89 files changed, 2558 insertions, 3861 deletions
diff --git a/client/package.json b/client/package.json index 564e56ae7..149322192 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -71,7 +71,6 @@ | |||
71 | "@types/sanitize-html": "2.6.2", | 71 | "@types/sanitize-html": "2.6.2", |
72 | "@types/sha.js": "^2.4.0", | 72 | "@types/sha.js": "^2.4.0", |
73 | "@types/video.js": "^7.3.40", | 73 | "@types/video.js": "^7.3.40", |
74 | "@types/webtorrent": "^0.109.0", | ||
75 | "@typescript-eslint/eslint-plugin": "^5.43.0", | 74 | "@typescript-eslint/eslint-plugin": "^5.43.0", |
76 | "@typescript-eslint/parser": "^5.43.0", | 75 | "@typescript-eslint/parser": "^5.43.0", |
77 | "@wdio/browserstack-service": "^8.10.5", | 76 | "@wdio/browserstack-service": "^8.10.5", |
@@ -85,14 +84,12 @@ | |||
85 | "babel-loader": "^9.1.0", | 84 | "babel-loader": "^9.1.0", |
86 | "bootstrap": "^5.1.3", | 85 | "bootstrap": "^5.1.3", |
87 | "buffer": "^6.0.3", | 86 | "buffer": "^6.0.3", |
88 | "cache-chunk-store": "^3.0.0", | ||
89 | "chart.js": "^4.3.0", | 87 | "chart.js": "^4.3.0", |
90 | "chartjs-plugin-zoom": "~2.0.1", | 88 | "chartjs-plugin-zoom": "~2.0.1", |
91 | "chromedriver": "^113.0.0", | 89 | "chromedriver": "^113.0.0", |
92 | "core-js": "^3.22.8", | 90 | "core-js": "^3.22.8", |
93 | "css-loader": "^6.2.0", | 91 | "css-loader": "^6.2.0", |
94 | "debug": "^4.3.1", | 92 | "debug": "^4.3.1", |
95 | "dexie": "^3.2.2", | ||
96 | "eslint": "^8.28.0", | 93 | "eslint": "^8.28.0", |
97 | "eslint-plugin-import": "2.27.5", | 94 | "eslint-plugin-import": "2.27.5", |
98 | "eslint-plugin-jsdoc": "^44.2.4", | 95 | "eslint-plugin-jsdoc": "^44.2.4", |
@@ -103,7 +100,6 @@ | |||
103 | "hls.js": "~1.3", | 100 | "hls.js": "~1.3", |
104 | "html-loader": "^4.1.0", | 101 | "html-loader": "^4.1.0", |
105 | "html-webpack-plugin": "^5.3.1", | 102 | "html-webpack-plugin": "^5.3.1", |
106 | "https-browserify": "^1.0.0", | ||
107 | "intl-messageformat": "^10.1.0", | 103 | "intl-messageformat": "^10.1.0", |
108 | "jschannel": "^1.0.2", | 104 | "jschannel": "^1.0.2", |
109 | "linkify-html": "^4.0.2", | 105 | "linkify-html": "^4.0.2", |
@@ -115,9 +111,7 @@ | |||
115 | "path-browserify": "^1.0.0", | 111 | "path-browserify": "^1.0.0", |
116 | "postcss": "^8.4.14", | 112 | "postcss": "^8.4.14", |
117 | "primeng": "^16.0.0-rc.2", | 113 | "primeng": "^16.0.0-rc.2", |
118 | "process": "^0.11.10", | ||
119 | "purify-css": "^1.2.5", | 114 | "purify-css": "^1.2.5", |
120 | "querystring": "^0.2.1", | ||
121 | "raw-loader": "^4.0.2", | 115 | "raw-loader": "^4.0.2", |
122 | "rxjs": "^7.3.0", | 116 | "rxjs": "^7.3.0", |
123 | "sanitize-html": "^2.1.2", | 117 | "sanitize-html": "^2.1.2", |
@@ -125,23 +119,17 @@ | |||
125 | "sass-loader": "^13.2.0", | 119 | "sass-loader": "^13.2.0", |
126 | "sha.js": "^2.4.11", | 120 | "sha.js": "^2.4.11", |
127 | "socket.io-client": "^4.5.4", | 121 | "socket.io-client": "^4.5.4", |
128 | "stream-browserify": "^3.0.0", | ||
129 | "stream-http": "^3.0.0", | ||
130 | "stylelint": "^15.1.0", | 122 | "stylelint": "^15.1.0", |
131 | "stylelint-config-sass-guidelines": "^10.0.0", | 123 | "stylelint-config-sass-guidelines": "^10.0.0", |
132 | "ts-loader": "^9.3.0", | 124 | "ts-loader": "^9.3.0", |
133 | "tslib": "^2.4.0", | 125 | "tslib": "^2.4.0", |
134 | "typescript": "~4.9.5", | 126 | "typescript": "~4.9.5", |
135 | "url": "^0.11.0", | ||
136 | "video.js": "^7.19.2", | 127 | "video.js": "^7.19.2", |
137 | "videostream": "~3.2.1", | ||
138 | "wdio-chromedriver-service": "^8.1.1", | 128 | "wdio-chromedriver-service": "^8.1.1", |
139 | "wdio-geckodriver-service": "^5.0.1", | 129 | "wdio-geckodriver-service": "^5.0.1", |
140 | "webpack": "^5.73.0", | 130 | "webpack": "^5.73.0", |
141 | "webpack-bundle-analyzer": "^4.4.2", | 131 | "webpack-bundle-analyzer": "^4.4.2", |
142 | "webpack-cli": "^5.0.1", | 132 | "webpack-cli": "^5.0.1", |
143 | "webtorrent": "1.8.26", | ||
144 | "whatwg-fetch": "^3.0.0", | ||
145 | "zone.js": "~0.13.0" | 133 | "zone.js": "~0.13.0" |
146 | }, | 134 | }, |
147 | "dependencies": {} | 135 | "dependencies": {} |
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts index ec85db0ff..97d71a510 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts | |||
@@ -152,12 +152,24 @@ export class VideoWatchPlaylistComponent { | |||
152 | this.onPlaylistVideosNearOfBottom(position) | 152 | this.onPlaylistVideosNearOfBottom(position) |
153 | } | 153 | } |
154 | 154 | ||
155 | // --------------------------------------------------------------------------- | ||
156 | |||
155 | hasPreviousVideo () { | 157 | hasPreviousVideo () { |
156 | return !!this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | 158 | return !!this.getPreviousVideo() |
159 | } | ||
160 | |||
161 | getPreviousVideo () { | ||
162 | return this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | ||
157 | } | 163 | } |
158 | 164 | ||
165 | // --------------------------------------------------------------------------- | ||
166 | |||
159 | hasNextVideo () { | 167 | hasNextVideo () { |
160 | return !!this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | 168 | return !!this.getNextVideo() |
169 | } | ||
170 | |||
171 | getNextVideo () { | ||
172 | return this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | ||
161 | } | 173 | } |
162 | 174 | ||
163 | navigateToPreviousPlaylistVideo () { | 175 | navigateToPreviousPlaylistVideo () { |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index 80fd6e40f..294ff4b3a 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | </div> | 8 | </div> |
9 | 9 | ||
10 | <div id="videojs-wrapper"> | 10 | <div id="videojs-wrapper"> |
11 | <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> | 11 | <video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video> |
12 | </div> | 12 | </div> |
13 | 13 | ||
14 | <my-video-watch-playlist | 14 | <my-video-watch-playlist |
@@ -51,7 +51,7 @@ | |||
51 | </div> | 51 | </div> |
52 | 52 | ||
53 | <my-action-buttons | 53 | <my-action-buttons |
54 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions" | 54 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions" |
55 | [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" | 55 | [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" |
56 | ></my-action-buttons> | 56 | ></my-action-buttons> |
57 | </div> | 57 | </div> |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 54e0649ba..aebec52fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' | 2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' |
3 | import { VideoJsPlayer } from 'video.js' | ||
4 | import { PlatformLocation } from '@angular/common' | 3 | import { PlatformLocation } from '@angular/common' |
5 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
6 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -19,13 +18,13 @@ import { | |||
19 | UserService | 18 | UserService |
20 | } from '@app/core' | 19 | } from '@app/core' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 20 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 23 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 24 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 26 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled, videoRequiresUserAuth, videoRequiresFileToken } from '@root-helpers/video' | 27 | import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 28 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 29 | import { |
31 | HTMLServerConfig, | 30 | HTMLServerConfig, |
@@ -39,10 +38,10 @@ import { | |||
39 | VideoState | 38 | VideoState |
40 | } from '@shared/models' | 39 | } from '@shared/models' |
41 | import { | 40 | import { |
42 | CustomizationOptions, | 41 | HLSOptions, |
43 | P2PMediaLoaderOptions, | 42 | PeerTubePlayer, |
44 | PeertubePlayerManager, | 43 | PeerTubePlayerContructorOptions, |
45 | PeertubePlayerManagerOptions, | 44 | PeerTubePlayerLoadOptions, |
46 | PlayerMode, | 45 | PlayerMode, |
47 | videojs | 46 | videojs |
48 | } from '../../../assets/player' | 47 | } from '../../../assets/player' |
@@ -50,7 +49,24 @@ import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from | |||
50 | import { environment } from '../../../environments/environment' | 49 | import { environment } from '../../../environments/environment' |
51 | import { VideoWatchPlaylistComponent } from './shared' | 50 | import { VideoWatchPlaylistComponent } from './shared' |
52 | 51 | ||
53 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 52 | type URLOptions = { |
53 | playerMode: PlayerMode | ||
54 | |||
55 | startTime: number | string | ||
56 | stopTime: number | string | ||
57 | |||
58 | controls?: boolean | ||
59 | controlBar?: boolean | ||
60 | |||
61 | muted?: boolean | ||
62 | loop?: boolean | ||
63 | subtitle?: string | ||
64 | resume?: string | ||
65 | |||
66 | peertubeLink: boolean | ||
67 | |||
68 | playbackRate?: number | string | ||
69 | } | ||
54 | 70 | ||
55 | @Component({ | 71 | @Component({ |
56 | selector: 'my-video-watch', | 72 | selector: 'my-video-watch', |
@@ -60,10 +76,9 @@ type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | |||
60 | export class VideoWatchComponent implements OnInit, OnDestroy { | 76 | export class VideoWatchComponent implements OnInit, OnDestroy { |
61 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 77 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
62 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 78 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
79 | @ViewChild('playerElement') playerElement: ElementRef<HTMLVideoElement> | ||
63 | 80 | ||
64 | player: VideoJsPlayer | 81 | peertubePlayer: PeerTubePlayer |
65 | playerElement: HTMLVideoElement | ||
66 | playerPlaceholderImgSrc: string | ||
67 | theaterEnabled = false | 82 | theaterEnabled = false |
68 | 83 | ||
69 | video: VideoDetails = null | 84 | video: VideoDetails = null |
@@ -78,8 +93,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
78 | remoteServerDown = false | 93 | remoteServerDown = false |
79 | noPlaylistVideoFound = false | 94 | noPlaylistVideoFound = false |
80 | 95 | ||
81 | private nextVideoUUID = '' | 96 | private nextRecommendedVideoUUID = '' |
82 | private nextVideoTitle = '' | 97 | private nextRecommendedVideoTitle = '' |
83 | 98 | ||
84 | private videoFileToken: string | 99 | private videoFileToken: string |
85 | 100 | ||
@@ -130,11 +145,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
130 | return this.userService.getAnonymousUser() | 145 | return this.userService.getAnonymousUser() |
131 | } | 146 | } |
132 | 147 | ||
133 | ngOnInit () { | 148 | async ngOnInit () { |
134 | this.serverConfig = this.serverService.getHTMLConfig() | 149 | this.serverConfig = this.serverService.getHTMLConfig() |
135 | 150 | ||
136 | PeertubePlayerManager.initState() | ||
137 | |||
138 | this.loadRouteParams() | 151 | this.loadRouteParams() |
139 | this.loadRouteQuery() | 152 | this.loadRouteQuery() |
140 | 153 | ||
@@ -143,10 +156,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
143 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 156 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
144 | 157 | ||
145 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI | 158 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI |
159 | |||
160 | const constructorOptions = await this.hooks.wrapFun( | ||
161 | this.buildPeerTubePlayerConstructorOptions.bind(this), | ||
162 | { urlOptions: this.getUrlOptions() }, | ||
163 | 'video-watch', | ||
164 | 'filter:internal.video-watch.player.build-options.params', | ||
165 | 'filter:internal.video-watch.player.build-options.result' | ||
166 | ) | ||
167 | |||
168 | this.peertubePlayer = new PeerTubePlayer(constructorOptions) | ||
146 | } | 169 | } |
147 | 170 | ||
148 | ngOnDestroy () { | 171 | ngOnDestroy () { |
149 | this.flushPlayer() | 172 | if (this.peertubePlayer) this.peertubePlayer.destroy() |
150 | 173 | ||
151 | // Unsubscribe subscriptions | 174 | // Unsubscribe subscriptions |
152 | if (this.paramsSub) this.paramsSub.unsubscribe() | 175 | if (this.paramsSub) this.paramsSub.unsubscribe() |
@@ -171,14 +194,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
171 | 194 | ||
172 | // The recommended videos's first element should be the next video | 195 | // The recommended videos's first element should be the next video |
173 | const video = videos[0] | 196 | const video = videos[0] |
174 | this.nextVideoUUID = video.uuid | 197 | this.nextRecommendedVideoUUID = video.uuid |
175 | this.nextVideoTitle = video.name | 198 | this.nextRecommendedVideoTitle = video.name |
176 | } | 199 | } |
177 | 200 | ||
178 | handleTimestampClicked (timestamp: number) { | 201 | handleTimestampClicked (timestamp: number) { |
179 | if (!this.player || this.video.isLive) return | 202 | if (!this.peertubePlayer || this.video.isLive) return |
180 | 203 | ||
181 | this.player.currentTime(timestamp) | 204 | this.peertubePlayer.getPlayer().currentTime(timestamp) |
182 | scrollToTop() | 205 | scrollToTop() |
183 | } | 206 | } |
184 | 207 | ||
@@ -243,7 +266,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
243 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) | 266 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) |
244 | 267 | ||
245 | const start = queryParams['start'] | 268 | const start = queryParams['start'] |
246 | if (this.player && start) this.player.currentTime(parseInt(start, 10)) | 269 | if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10)) |
247 | }) | 270 | }) |
248 | } | 271 | } |
249 | 272 | ||
@@ -256,8 +279,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
256 | 279 | ||
257 | if (this.isSameElement(this.video, videoId)) return | 280 | if (this.isSameElement(this.video, videoId)) return |
258 | 281 | ||
259 | if (this.player) this.player.pause() | ||
260 | |||
261 | this.video = undefined | 282 | this.video = undefined |
262 | 283 | ||
263 | const videoObs = this.hooks.wrapObsFun( | 284 | const videoObs = this.hooks.wrapObsFun( |
@@ -291,23 +312,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
291 | this.userService.getAnonymousOrLoggedUser() | 312 | this.userService.getAnonymousOrLoggedUser() |
292 | ]).subscribe({ | 313 | ]).subscribe({ |
293 | next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => { | 314 | next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => { |
294 | const queryParams = this.route.snapshot.queryParams | ||
295 | |||
296 | const urlOptions = { | ||
297 | resume: queryParams.resume, | ||
298 | |||
299 | startTime: queryParams.start, | ||
300 | stopTime: queryParams.stop, | ||
301 | |||
302 | muted: queryParams.muted, | ||
303 | loop: queryParams.loop, | ||
304 | subtitle: queryParams.subtitle, | ||
305 | |||
306 | playerMode: queryParams.mode, | ||
307 | playbackRate: queryParams.playbackRate, | ||
308 | peertubeLink: false | ||
309 | } | ||
310 | |||
311 | this.onVideoFetched({ | 315 | this.onVideoFetched({ |
312 | video, | 316 | video, |
313 | live, | 317 | live, |
@@ -316,7 +320,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
316 | videoFileToken, | 320 | videoFileToken, |
317 | videoPassword, | 321 | videoPassword, |
318 | loggedInOrAnonymousUser, | 322 | loggedInOrAnonymousUser, |
319 | urlOptions, | ||
320 | forceAutoplay | 323 | forceAutoplay |
321 | }).catch(err => { | 324 | }).catch(err => { |
322 | this.handleGlobalError(err) | 325 | this.handleGlobalError(err) |
@@ -386,14 +389,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
386 | const errorMessage: string = typeof err === 'string' ? err : err.message | 389 | const errorMessage: string = typeof err === 'string' ? err : err.message |
387 | if (!errorMessage) return | 390 | if (!errorMessage) return |
388 | 391 | ||
389 | // Display a message in the video player instead of a notification | ||
390 | if (errorMessage.includes('from xs param')) { | ||
391 | this.flushPlayer() | ||
392 | this.remoteServerDown = true | ||
393 | |||
394 | return | ||
395 | } | ||
396 | |||
397 | this.notifier.error(errorMessage) | 392 | this.notifier.error(errorMessage) |
398 | } | 393 | } |
399 | 394 | ||
@@ -422,7 +417,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
422 | videoFileToken: string | 417 | videoFileToken: string |
423 | videoPassword: string | 418 | videoPassword: string |
424 | 419 | ||
425 | urlOptions: URLOptions | ||
426 | loggedInOrAnonymousUser: User | 420 | loggedInOrAnonymousUser: User |
427 | forceAutoplay: boolean | 421 | forceAutoplay: boolean |
428 | }) { | 422 | }) { |
@@ -431,7 +425,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
431 | live, | 425 | live, |
432 | videoCaptions, | 426 | videoCaptions, |
433 | storyboards, | 427 | storyboards, |
434 | urlOptions, | ||
435 | videoFileToken, | 428 | videoFileToken, |
436 | videoPassword, | 429 | videoPassword, |
437 | loggedInOrAnonymousUser, | 430 | loggedInOrAnonymousUser, |
@@ -448,7 +441,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
448 | this.storyboards = storyboards | 441 | this.storyboards = storyboards |
449 | 442 | ||
450 | // Re init attributes | 443 | // Re init attributes |
451 | this.playerPlaceholderImgSrc = undefined | ||
452 | this.remoteServerDown = false | 444 | this.remoteServerDown = false |
453 | this.currentTime = undefined | 445 | this.currentTime = undefined |
454 | 446 | ||
@@ -462,7 +454,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
462 | 454 | ||
463 | this.buildHotkeysHelp(video) | 455 | this.buildHotkeysHelp(video) |
464 | 456 | ||
465 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 457 | this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay }) |
466 | .catch(err => logger.error('Cannot build the player', err)) | 458 | .catch(err => logger.error('Cannot build the player', err)) |
467 | 459 | ||
468 | this.setOpenGraphTags() | 460 | this.setOpenGraphTags() |
@@ -475,28 +467,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
475 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) | 467 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) |
476 | } | 468 | } |
477 | 469 | ||
478 | private async buildPlayer (options: { | 470 | private async loadPlayer (options: { |
479 | urlOptions: URLOptions | ||
480 | loggedInOrAnonymousUser: User | 471 | loggedInOrAnonymousUser: User |
481 | forceAutoplay: boolean | 472 | forceAutoplay: boolean |
482 | }) { | 473 | }) { |
483 | const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options | 474 | const { loggedInOrAnonymousUser, forceAutoplay } = options |
484 | |||
485 | // Flush old player if needed | ||
486 | this.flushPlayer() | ||
487 | 475 | ||
488 | const videoState = this.video.state.id | 476 | const videoState = this.video.state.id |
489 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { | 477 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { |
490 | this.playerPlaceholderImgSrc = this.video.previewPath | 478 | this.updatePlayerOnNoLive() |
491 | return | 479 | return |
492 | } | 480 | } |
493 | 481 | ||
494 | // Build video element, because videojs removes it on dispose | 482 | this.peertubePlayer?.enable() |
495 | const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper') | ||
496 | this.playerElement = document.createElement('video') | ||
497 | this.playerElement.className = 'video-js vjs-peertube-skin' | ||
498 | this.playerElement.setAttribute('playsinline', 'true') | ||
499 | playerElementWrapper.appendChild(this.playerElement) | ||
500 | 483 | ||
501 | const params = { | 484 | const params = { |
502 | video: this.video, | 485 | video: this.video, |
@@ -505,86 +488,49 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
505 | liveVideo: this.liveVideo, | 488 | liveVideo: this.liveVideo, |
506 | videoFileToken: this.videoFileToken, | 489 | videoFileToken: this.videoFileToken, |
507 | videoPassword: this.videoPassword, | 490 | videoPassword: this.videoPassword, |
508 | urlOptions, | 491 | urlOptions: this.getUrlOptions(), |
509 | loggedInOrAnonymousUser, | 492 | loggedInOrAnonymousUser, |
510 | forceAutoplay, | 493 | forceAutoplay, |
511 | user: this.user | 494 | user: this.user |
512 | } | 495 | } |
513 | const { playerMode, playerOptions } = await this.hooks.wrapFun( | 496 | |
514 | this.buildPlayerManagerOptions.bind(this), | 497 | const loadOptions = await this.hooks.wrapFun( |
498 | this.buildPeerTubePlayerLoadOptions.bind(this), | ||
515 | params, | 499 | params, |
516 | 'video-watch', | 500 | 'video-watch', |
517 | 'filter:internal.video-watch.player.build-options.params', | 501 | 'filter:internal.video-watch.player.load-options.params', |
518 | 'filter:internal.video-watch.player.build-options.result' | 502 | 'filter:internal.video-watch.player.load-options.result' |
519 | ) | 503 | ) |
520 | 504 | ||
521 | this.zone.runOutsideAngular(async () => { | 505 | this.zone.runOutsideAngular(async () => { |
522 | this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player) | 506 | await this.peertubePlayer.load(loadOptions) |
523 | 507 | ||
524 | this.player.on('customError', (_e, data: any) => { | 508 | const player = this.peertubePlayer.getPlayer() |
525 | this.zone.run(() => this.handleGlobalError(data.err)) | ||
526 | }) | ||
527 | 509 | ||
528 | this.player.on('timeupdate', () => { | 510 | player.on('timeupdate', () => { |
529 | // Don't need to trigger angular change for this variable, that is sent to children components on click | 511 | // Don't need to trigger angular change for this variable, that is sent to children components on click |
530 | this.currentTime = Math.floor(this.player.currentTime()) | 512 | this.currentTime = Math.floor(player.currentTime()) |
531 | }) | 513 | }) |
532 | 514 | ||
533 | /** | 515 | if (this.video.isLive) { |
534 | * condition: true to make the upnext functionality trigger, false to disable the upnext functionality | 516 | player.one('ended', () => { |
535 | * go to the next video in 'condition()' if you don't want of the timer. | 517 | this.zone.run(() => { |
536 | * next: function triggered at the end of the timer. | 518 | // We changed the video, it's not a live anymore |
537 | * suspended: function used at each click of the timer checking if we need to reset progress | 519 | if (!this.video.isLive) return |
538 | * and wait until suspended becomes truthy again. | ||
539 | */ | ||
540 | this.player.upnext({ | ||
541 | timeout: 5000, // 5s | ||
542 | |||
543 | headText: $localize`Up Next`, | ||
544 | cancelText: $localize`Cancel`, | ||
545 | suspendedText: $localize`Autoplay is suspended`, | ||
546 | |||
547 | getTitle: () => this.nextVideoTitle, | ||
548 | 520 | ||
549 | next: () => this.zone.run(() => this.playNextVideoInAngularZone()), | 521 | this.video.state.id = VideoState.LIVE_ENDED |
550 | condition: () => { | ||
551 | if (!this.playlist) return this.isAutoPlayNext() | ||
552 | 522 | ||
553 | // Don't wait timeout to play the next playlist video | 523 | this.updatePlayerOnNoLive() |
554 | if (this.isPlaylistAutoPlayNext()) { | 524 | }) |
555 | this.playNextVideoInAngularZone() | 525 | }) |
556 | return undefined | 526 | } |
557 | } | ||
558 | |||
559 | return false | ||
560 | }, | ||
561 | |||
562 | suspended: () => { | ||
563 | return ( | ||
564 | !isXPercentInViewport(this.player.el() as HTMLElement, 80) || | ||
565 | !document.getElementById('content').contains(document.activeElement) | ||
566 | ) | ||
567 | } | ||
568 | }) | ||
569 | |||
570 | this.player.one('stopped', () => { | ||
571 | if (this.playlist && this.isPlaylistAutoPlayNext()) { | ||
572 | this.playNextVideoInAngularZone() | ||
573 | } | ||
574 | }) | ||
575 | |||
576 | this.player.one('ended', () => { | ||
577 | if (this.video.isLive) { | ||
578 | this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED) | ||
579 | } | ||
580 | }) | ||
581 | 527 | ||
582 | this.player.on('theaterChange', (_: any, enabled: boolean) => { | 528 | player.on('theater-change', (_: any, enabled: boolean) => { |
583 | this.zone.run(() => this.theaterEnabled = enabled) | 529 | this.zone.run(() => this.theaterEnabled = enabled) |
584 | }) | 530 | }) |
585 | 531 | ||
586 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { | 532 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { |
587 | player: this.player, | 533 | player, |
588 | playlist: this.playlist, | 534 | playlist: this.playlist, |
589 | playlistPosition: this.playlistPosition, | 535 | playlistPosition: this.playlistPosition, |
590 | videojs, | 536 | videojs, |
@@ -601,15 +547,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
601 | return true | 547 | return true |
602 | } | 548 | } |
603 | 549 | ||
604 | private playNextVideoInAngularZone () { | 550 | private getNextVideoTitle () { |
605 | if (this.playlist) { | 551 | if (this.playlist) { |
606 | this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) | 552 | return this.videoWatchPlaylist.getNextVideo()?.video?.name || '' |
607 | return | ||
608 | } | 553 | } |
609 | 554 | ||
610 | if (this.nextVideoUUID) { | 555 | return this.nextRecommendedVideoTitle |
611 | this.router.navigate([ '/w', this.nextVideoUUID ]) | 556 | } |
612 | } | 557 | |
558 | private playNextVideoInAngularZone () { | ||
559 | this.zone.run(() => { | ||
560 | if (this.playlist) { | ||
561 | this.videoWatchPlaylist.navigateToNextPlaylistVideo() | ||
562 | return | ||
563 | } | ||
564 | |||
565 | if (this.nextRecommendedVideoUUID) { | ||
566 | this.router.navigate([ '/w', this.nextRecommendedVideoUUID ]) | ||
567 | } | ||
568 | }) | ||
613 | } | 569 | } |
614 | 570 | ||
615 | private isAutoplay () { | 571 | private isAutoplay () { |
@@ -637,19 +593,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
637 | ) | 593 | ) |
638 | } | 594 | } |
639 | 595 | ||
640 | private flushPlayer () { | 596 | private buildPeerTubePlayerConstructorOptions (options: { |
641 | // Remove player if it exists | 597 | urlOptions: URLOptions |
642 | if (!this.player) return | 598 | }): PeerTubePlayerContructorOptions { |
599 | const { urlOptions } = options | ||
600 | |||
601 | return { | ||
602 | playerElement: () => this.playerElement.nativeElement, | ||
603 | |||
604 | enableHotkeys: true, | ||
605 | inactivityTimeout: 2500, | ||
606 | |||
607 | theaterButton: true, | ||
608 | |||
609 | controls: urlOptions.controls, | ||
610 | controlBar: urlOptions.controlBar, | ||
611 | |||
612 | muted: urlOptions.muted, | ||
613 | loop: urlOptions.loop, | ||
614 | |||
615 | playbackRate: urlOptions.playbackRate, | ||
616 | |||
617 | instanceName: this.serverConfig.instance.name, | ||
618 | language: this.localeId, | ||
619 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | ||
620 | |||
621 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | ||
622 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | ||
623 | |||
624 | serverUrl: environment.originServerUrl || window.location.origin, | ||
643 | 625 | ||
644 | try { | 626 | errorNotifier: (message: string) => this.notifier.error(message), |
645 | this.player.dispose() | 627 | |
646 | this.player = undefined | 628 | peertubeLink: () => false, |
647 | } catch (err) { | 629 | |
648 | logger.error('Cannot dispose player.', err) | 630 | pluginsManager: this.pluginService.getPluginsManager() |
649 | } | 631 | } |
650 | } | 632 | } |
651 | 633 | ||
652 | private buildPlayerManagerOptions (params: { | 634 | private buildPeerTubePlayerLoadOptions (options: { |
653 | video: VideoDetails | 635 | video: VideoDetails |
654 | liveVideo: LiveVideo | 636 | liveVideo: LiveVideo |
655 | videoCaptions: VideoCaption[] | 637 | videoCaptions: VideoCaption[] |
@@ -658,12 +640,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
658 | videoFileToken: string | 640 | videoFileToken: string |
659 | videoPassword: string | 641 | videoPassword: string |
660 | 642 | ||
661 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 643 | urlOptions: URLOptions |
662 | 644 | ||
663 | loggedInOrAnonymousUser: User | 645 | loggedInOrAnonymousUser: User |
664 | forceAutoplay: boolean | 646 | forceAutoplay: boolean |
665 | user?: AuthUser // Keep for plugins | 647 | user?: AuthUser // Keep for plugins |
666 | }) { | 648 | }): PeerTubePlayerLoadOptions { |
667 | const { | 649 | const { |
668 | video, | 650 | video, |
669 | liveVideo, | 651 | liveVideo, |
@@ -674,7 +656,30 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
674 | urlOptions, | 656 | urlOptions, |
675 | loggedInOrAnonymousUser, | 657 | loggedInOrAnonymousUser, |
676 | forceAutoplay | 658 | forceAutoplay |
677 | } = params | 659 | } = options |
660 | |||
661 | let mode: PlayerMode | ||
662 | |||
663 | if (urlOptions.playerMode) { | ||
664 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | ||
665 | else mode = 'web-video' | ||
666 | } else { | ||
667 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
668 | else mode = 'web-video' | ||
669 | } | ||
670 | |||
671 | let hlsOptions: HLSOptions | ||
672 | if (video.hasHlsPlaylist()) { | ||
673 | const hlsPlaylist = video.getHlsPlaylist() | ||
674 | |||
675 | hlsOptions = { | ||
676 | playlistUrl: hlsPlaylist.playlistUrl, | ||
677 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
678 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
679 | trackerAnnounce: video.trackerUrls, | ||
680 | videoFiles: hlsPlaylist.files | ||
681 | } | ||
682 | } | ||
678 | 683 | ||
679 | const getStartTime = () => { | 684 | const getStartTime = () => { |
680 | const byUrl = urlOptions.startTime !== undefined | 685 | const byUrl = urlOptions.startTime !== undefined |
@@ -714,118 +719,80 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
714 | ? { latencyMode: liveVideo.latencyMode } | 719 | ? { latencyMode: liveVideo.latencyMode } |
715 | : undefined | 720 | : undefined |
716 | 721 | ||
717 | const options: PeertubePlayerManagerOptions = { | 722 | return { |
718 | common: { | 723 | mode, |
719 | autoplay: this.isAutoplay(), | ||
720 | forceAutoplay, | ||
721 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
722 | |||
723 | hasNextVideo: () => this.hasNextVideo(), | ||
724 | nextVideo: () => this.playNextVideoInAngularZone(), | ||
725 | |||
726 | playerElement: this.playerElement, | ||
727 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, | ||
728 | 724 | ||
729 | videoDuration: video.duration, | 725 | autoplay: this.isAutoplay(), |
730 | enableHotkeys: true, | 726 | forceAutoplay, |
731 | inactivityTimeout: 2500, | ||
732 | poster: video.previewUrl, | ||
733 | |||
734 | startTime, | ||
735 | stopTime: urlOptions.stopTime, | ||
736 | controlBar: urlOptions.controlBar, | ||
737 | controls: urlOptions.controls, | ||
738 | muted: urlOptions.muted, | ||
739 | loop: urlOptions.loop, | ||
740 | subtitle: urlOptions.subtitle, | ||
741 | playbackRate: urlOptions.playbackRate, | ||
742 | |||
743 | peertubeLink: urlOptions.peertubeLink, | ||
744 | 727 | ||
745 | theaterButton: true, | 728 | duration: this.video.duration, |
746 | captions: videoCaptions.length !== 0, | 729 | poster: video.previewUrl, |
730 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
747 | 731 | ||
748 | embedUrl: video.embedUrl, | 732 | startTime, |
749 | embedTitle: video.name, | 733 | stopTime: urlOptions.stopTime, |
750 | instanceName: this.serverConfig.instance.name, | ||
751 | 734 | ||
752 | isLive: video.isLive, | 735 | embedUrl: video.embedUrl, |
753 | liveOptions, | 736 | embedTitle: video.name, |
754 | 737 | ||
755 | language: this.localeId, | 738 | isLive: video.isLive, |
739 | liveOptions, | ||
756 | 740 | ||
757 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | 741 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE |
742 | ? this.videoService.getVideoViewUrl(video.uuid) | ||
743 | : null, | ||
758 | 744 | ||
759 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE | 745 | videoFileToken: () => videoFileToken, |
760 | ? this.videoService.getVideoViewUrl(video.uuid) | 746 | requiresUserAuth: videoRequiresUserAuth(video, videoPassword), |
761 | : null, | 747 | requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && |
762 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | 748 | !video.canAccessPasswordProtectedVideoWithoutPassword(this.user), |
763 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | 749 | videoPassword: () => videoPassword, |
764 | 750 | ||
765 | serverUrl: environment.originServerUrl || window.location.origin, | 751 | videoCaptions: playerCaptions, |
752 | storyboard, | ||
766 | 753 | ||
767 | videoFileToken: () => videoFileToken, | 754 | videoShortUUID: video.shortUUID, |
768 | requiresUserAuth: videoRequiresUserAuth(video, videoPassword), | 755 | videoUUID: video.uuid, |
769 | requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && | ||
770 | !video.canAccessPasswordProtectedVideoWithoutPassword(this.user), | ||
771 | videoPassword: () => videoPassword, | ||
772 | 756 | ||
773 | videoCaptions: playerCaptions, | 757 | previousVideo: { |
774 | storyboard, | 758 | enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(), |
775 | 759 | ||
776 | videoShortUUID: video.shortUUID, | 760 | handler: this.playlist |
777 | videoUUID: video.uuid, | 761 | ? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) |
762 | : undefined, | ||
778 | 763 | ||
779 | errorNotifier: (message: string) => this.notifier.error(message) | 764 | displayControlBarButton: !!this.playlist |
780 | }, | 765 | }, |
781 | 766 | ||
782 | webtorrent: { | 767 | nextVideo: { |
783 | videoFiles: video.files | 768 | enabled: this.hasNextVideo(), |
769 | handler: () => this.playNextVideoInAngularZone(), | ||
770 | getVideoTitle: () => this.getNextVideoTitle(), | ||
771 | displayControlBarButton: this.hasNextVideo() | ||
784 | }, | 772 | }, |
785 | 773 | ||
786 | pluginsManager: this.pluginService.getPluginsManager() | 774 | upnext: { |
787 | } | 775 | isEnabled: () => { |
788 | 776 | if (this.playlist) return this.isPlaylistAutoPlayNext() | |
789 | // Only set this if we're in a playlist | ||
790 | if (this.playlist) { | ||
791 | options.common.hasPreviousVideo = () => this.videoWatchPlaylist.hasPreviousVideo() | ||
792 | |||
793 | options.common.previousVideo = () => { | ||
794 | this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) | ||
795 | } | ||
796 | } | ||
797 | |||
798 | let mode: PlayerMode | ||
799 | 777 | ||
800 | if (urlOptions.playerMode) { | 778 | return this.isAutoPlayNext() |
801 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | 779 | }, |
802 | else mode = 'webtorrent' | ||
803 | } else { | ||
804 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
805 | else mode = 'webtorrent' | ||
806 | } | ||
807 | 780 | ||
808 | // FIXME: remove, we don't support these old web browsers anymore | 781 | isSuspended: (player: videojs.Player) => { |
809 | // p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available | 782 | return !isXPercentInViewport(player.el() as HTMLElement, 80) |
810 | if (typeof TextEncoder === 'undefined') { | 783 | }, |
811 | mode = 'webtorrent' | ||
812 | } | ||
813 | 784 | ||
814 | if (mode === 'p2p-media-loader') { | 785 | timeout: this.playlist |
815 | const hlsPlaylist = video.getHlsPlaylist() | 786 | ? 0 // Don't wait to play next video in playlist |
787 | : 5000 // 5 seconds for a recommended video | ||
788 | }, | ||
816 | 789 | ||
817 | const p2pMediaLoader = { | 790 | hls: hlsOptions, |
818 | playlistUrl: hlsPlaylist.playlistUrl, | ||
819 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
820 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
821 | trackerAnnounce: video.trackerUrls, | ||
822 | videoFiles: hlsPlaylist.files | ||
823 | } as P2PMediaLoaderOptions | ||
824 | 791 | ||
825 | Object.assign(options, { p2pMediaLoader }) | 792 | webVideo: { |
793 | videoFiles: video.files | ||
794 | } | ||
826 | } | 795 | } |
827 | |||
828 | return { playerMode: mode, playerOptions: options } | ||
829 | } | 796 | } |
830 | 797 | ||
831 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 798 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
@@ -873,6 +840,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
873 | this.video.viewers = newViewers | 840 | this.video.viewers = newViewers |
874 | } | 841 | } |
875 | 842 | ||
843 | private updatePlayerOnNoLive () { | ||
844 | this.peertubePlayer.unload() | ||
845 | this.peertubePlayer.disable() | ||
846 | this.peertubePlayer.setPoster(this.video.previewPath) | ||
847 | } | ||
848 | |||
876 | private buildHotkeysHelp (video: Video) { | 849 | private buildHotkeysHelp (video: Video) { |
877 | if (this.hotkeys.length !== 0) { | 850 | if (this.hotkeys.length !== 0) { |
878 | this.hotkeysService.remove(this.hotkeys) | 851 | this.hotkeysService.remove(this.hotkeys) |
@@ -944,4 +917,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
944 | this.metaService.setTag('og:url', window.location.href) | 917 | this.metaService.setTag('og:url', window.location.href) |
945 | this.metaService.setTag('url', window.location.href) | 918 | this.metaService.setTag('url', window.location.href) |
946 | } | 919 | } |
920 | |||
921 | private getUrlOptions (): URLOptions { | ||
922 | const queryParams = this.route.snapshot.queryParams | ||
923 | |||
924 | return { | ||
925 | resume: queryParams.resume, | ||
926 | |||
927 | startTime: queryParams.start, | ||
928 | stopTime: queryParams.stop, | ||
929 | |||
930 | muted: toBoolean(queryParams.muted), | ||
931 | loop: toBoolean(queryParams.loop), | ||
932 | subtitle: queryParams.subtitle, | ||
933 | |||
934 | playerMode: queryParams.mode, | ||
935 | playbackRate: queryParams.playbackRate, | ||
936 | |||
937 | controlBar: toBoolean(queryParams.controlBar), | ||
938 | |||
939 | peertubeLink: false | ||
940 | } | ||
941 | } | ||
947 | } | 942 | } |
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts index 69b2b18c0..b69e31edf 100644 --- a/client/src/app/helpers/utils/object.ts +++ b/client/src/app/helpers/utils/object.ts | |||
@@ -34,6 +34,8 @@ function toBoolean (value: any) { | |||
34 | 34 | ||
35 | if (value === 'true') return true | 35 | if (value === 'true') return true |
36 | if (value === 'false') return false | 36 | if (value === 'false') return false |
37 | if (value === '1') return true | ||
38 | if (value === '0') return false | ||
37 | 39 | ||
38 | return undefined | 40 | return undefined |
39 | } | 41 | } |
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index 45df0be38..14a5abd7a 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -241,7 +241,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
241 | } | 241 | } |
242 | 242 | ||
243 | reloadVideos () { | 243 | reloadVideos () { |
244 | console.log('reload') | ||
245 | this.pagination.currentPage = 1 | 244 | this.pagination.currentPage = 1 |
246 | this.loadMoreVideos(true) | 245 | this.loadMoreVideos(true) |
247 | } | 246 | } |
@@ -420,8 +419,9 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
420 | this.lastQueryLength = data.length | 419 | this.lastQueryLength = data.length |
421 | 420 | ||
422 | if (reset) this.videos = [] | 421 | if (reset) this.videos = [] |
422 | |||
423 | this.videos = this.videos.concat(data) | 423 | this.videos = this.videos.concat(data) |
424 | console.log('subscribe') | 424 | |
425 | if (this.groupByDate) this.buildGroupedDateLabels() | 425 | if (this.groupByDate) this.buildGroupedDateLabels() |
426 | 426 | ||
427 | this.onDataSubject.next(data) | 427 | this.onDataSubject.next(data) |
diff --git a/client/src/assets/player/index.ts b/client/src/assets/player/index.ts index 9b87afc4a..d34188ea7 100644 --- a/client/src/assets/player/index.ts +++ b/client/src/assets/player/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './peertube-player-manager' | 1 | export * from './peertube-player' |
2 | export * from './types' | 2 | export * from './types' |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts deleted file mode 100644 index 66d9c7298..000000000 --- a/client/src/assets/player/peertube-player-manager.ts +++ /dev/null | |||
@@ -1,277 +0,0 @@ | |||
1 | import '@peertube/videojs-contextmenu' | ||
2 | import './shared/upnext/end-card' | ||
3 | import './shared/upnext/upnext-plugin' | ||
4 | import './shared/stats/stats-card' | ||
5 | import './shared/stats/stats-plugin' | ||
6 | import './shared/bezels/bezels-plugin' | ||
7 | import './shared/peertube/peertube-plugin' | ||
8 | import './shared/resolutions/peertube-resolutions-plugin' | ||
9 | import './shared/control-bar/storyboard-plugin' | ||
10 | import './shared/control-bar/next-previous-video-button' | ||
11 | import './shared/control-bar/p2p-info-button' | ||
12 | import './shared/control-bar/peertube-link-button' | ||
13 | import './shared/control-bar/peertube-load-progress-bar' | ||
14 | import './shared/control-bar/theater-button' | ||
15 | import './shared/control-bar/peertube-live-display' | ||
16 | import './shared/settings/resolution-menu-button' | ||
17 | import './shared/settings/resolution-menu-item' | ||
18 | import './shared/settings/settings-dialog' | ||
19 | import './shared/settings/settings-menu-button' | ||
20 | import './shared/settings/settings-menu-item' | ||
21 | import './shared/settings/settings-panel' | ||
22 | import './shared/settings/settings-panel-child' | ||
23 | import './shared/playlist/playlist-plugin' | ||
24 | import './shared/mobile/peertube-mobile-plugin' | ||
25 | import './shared/mobile/peertube-mobile-buttons' | ||
26 | import './shared/hotkeys/peertube-hotkeys-plugin' | ||
27 | import './shared/metrics/metrics-plugin' | ||
28 | import videojs from 'video.js' | ||
29 | import { logger } from '@root-helpers/logger' | ||
30 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
31 | import { isMobile } from '@root-helpers/web-browser' | ||
32 | import { saveAverageBandwidth } from './peertube-player-local-storage' | ||
33 | import { ManagerOptionsBuilder } from './shared/manager-options' | ||
34 | import { TranslationsManager } from './translations-manager' | ||
35 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode, PlayerNetworkInfo } from './types' | ||
36 | |||
37 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
38 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | ||
39 | |||
40 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
41 | // Change Captions to Subtitles/CC | ||
42 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' | ||
43 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
44 | CaptionsButton.prototype.label_ = ' ' | ||
45 | |||
46 | // TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged | ||
47 | const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any | ||
48 | if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) { | ||
49 | PlayProgressBar.prototype.options_.children.push('timeTooltip') | ||
50 | } | ||
51 | |||
52 | export class PeertubePlayerManager { | ||
53 | private static playerElementClassName: string | ||
54 | private static playerElementAttributes: { name: string, value: string }[] = [] | ||
55 | |||
56 | private static onPlayerChange: (player: videojs.Player) => void | ||
57 | private static alreadyPlayed = false | ||
58 | private static pluginsManager: PluginsManager | ||
59 | |||
60 | private static videojsDecodeErrors = 0 | ||
61 | |||
62 | private static p2pMediaLoaderModule: any | ||
63 | |||
64 | static initState () { | ||
65 | this.alreadyPlayed = false | ||
66 | } | ||
67 | |||
68 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) { | ||
69 | this.pluginsManager = options.pluginsManager | ||
70 | |||
71 | this.onPlayerChange = onPlayerChange | ||
72 | |||
73 | this.playerElementClassName = options.common.playerElement.className | ||
74 | |||
75 | for (const name of options.common.playerElement.getAttributeNames()) { | ||
76 | this.playerElementAttributes.push({ name, value: options.common.playerElement.getAttribute(name) }) | ||
77 | } | ||
78 | |||
79 | if (mode === 'webtorrent') await import('./shared/webtorrent/webtorrent-plugin') | ||
80 | if (mode === 'p2p-media-loader') { | ||
81 | const [ p2pMediaLoaderModule ] = await Promise.all([ | ||
82 | import('@peertube/p2p-media-loader-hlsjs'), | ||
83 | import('./shared/p2p-media-loader/p2p-media-loader-plugin') | ||
84 | ]) | ||
85 | |||
86 | this.p2pMediaLoaderModule = p2pMediaLoaderModule | ||
87 | } | ||
88 | |||
89 | await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs) | ||
90 | |||
91 | return this.buildPlayer(mode, options) | ||
92 | } | ||
93 | |||
94 | private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> { | ||
95 | const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule) | ||
96 | |||
97 | const videojsOptions = await this.pluginsManager.runHook( | ||
98 | 'filter:internal.player.videojs.options.result', | ||
99 | videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed) | ||
100 | ) | ||
101 | |||
102 | const self = this | ||
103 | return new Promise(res => { | ||
104 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | ||
105 | const player = this | ||
106 | |||
107 | if (!isNaN(+options.common.playbackRate)) { | ||
108 | player.playbackRate(+options.common.playbackRate) | ||
109 | } | ||
110 | |||
111 | let alreadyFallback = false | ||
112 | |||
113 | const handleError = () => { | ||
114 | if (alreadyFallback) return | ||
115 | alreadyFallback = true | ||
116 | |||
117 | if (mode === 'p2p-media-loader') { | ||
118 | self.tryToRecoverHLSError(player.error(), player, options) | ||
119 | } else { | ||
120 | self.maybeFallbackToWebTorrent(mode, player, options) | ||
121 | } | ||
122 | } | ||
123 | |||
124 | player.one('error', () => handleError()) | ||
125 | |||
126 | player.one('play', () => { | ||
127 | self.alreadyPlayed = true | ||
128 | }) | ||
129 | |||
130 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | ||
131 | |||
132 | if (isMobile()) player.peertubeMobile() | ||
133 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) | ||
134 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | ||
135 | |||
136 | player.bezels() | ||
137 | |||
138 | player.stats({ | ||
139 | videoUUID: options.common.videoUUID, | ||
140 | videoIsLive: options.common.isLive, | ||
141 | mode, | ||
142 | p2pEnabled: options.common.p2pEnabled | ||
143 | }) | ||
144 | |||
145 | if (options.common.storyboard) { | ||
146 | player.storyboard(options.common.storyboard) | ||
147 | } | ||
148 | |||
149 | player.on('p2pInfo', (_, data: PlayerNetworkInfo) => { | ||
150 | if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return | ||
151 | |||
152 | saveAverageBandwidth(data.bandwidthEstimate) | ||
153 | }) | ||
154 | |||
155 | const offlineNotificationElem = document.createElement('div') | ||
156 | offlineNotificationElem.classList.add('vjs-peertube-offline-notification') | ||
157 | offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work') | ||
158 | |||
159 | let offlineNotificationElemAdded = false | ||
160 | |||
161 | const handleOnline = () => { | ||
162 | if (!offlineNotificationElemAdded) return | ||
163 | |||
164 | player.el().removeChild(offlineNotificationElem) | ||
165 | offlineNotificationElemAdded = false | ||
166 | |||
167 | logger.info('The browser is online') | ||
168 | } | ||
169 | |||
170 | const handleOffline = () => { | ||
171 | if (offlineNotificationElemAdded) return | ||
172 | |||
173 | player.el().appendChild(offlineNotificationElem) | ||
174 | offlineNotificationElemAdded = true | ||
175 | |||
176 | logger.info('The browser is offline') | ||
177 | } | ||
178 | |||
179 | window.addEventListener('online', handleOnline) | ||
180 | window.addEventListener('offline', handleOffline) | ||
181 | |||
182 | player.on('dispose', () => { | ||
183 | window.removeEventListener('online', handleOnline) | ||
184 | window.removeEventListener('offline', handleOffline) | ||
185 | }) | ||
186 | |||
187 | return res(player) | ||
188 | }) | ||
189 | }) | ||
190 | } | ||
191 | |||
192 | private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) { | ||
193 | if (err.code === MediaError.MEDIA_ERR_DECODE) { | ||
194 | |||
195 | // Display a notification to user | ||
196 | if (this.videojsDecodeErrors === 0) { | ||
197 | options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.')) | ||
198 | } | ||
199 | |||
200 | if (this.videojsDecodeErrors === 20) { | ||
201 | this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options) | ||
202 | return | ||
203 | } | ||
204 | |||
205 | logger.info('Fast forwarding HLS to recover from an error.') | ||
206 | |||
207 | this.videojsDecodeErrors++ | ||
208 | |||
209 | options.common.startTime = currentPlayer.currentTime() + 2 | ||
210 | options.common.autoplay = true | ||
211 | this.rebuildAndUpdateVideoElement(currentPlayer, options.common) | ||
212 | |||
213 | const newPlayer = await this.buildPlayer('p2p-media-loader', options) | ||
214 | this.onPlayerChange(newPlayer) | ||
215 | } else { | ||
216 | this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options) | ||
217 | } | ||
218 | } | ||
219 | |||
220 | private static async maybeFallbackToWebTorrent ( | ||
221 | currentMode: PlayerMode, | ||
222 | currentPlayer: videojs.Player, | ||
223 | options: PeertubePlayerManagerOptions | ||
224 | ) { | ||
225 | if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') { | ||
226 | currentPlayer.peertube().displayFatalError() | ||
227 | return | ||
228 | } | ||
229 | |||
230 | logger.info('Fallback to webtorrent.') | ||
231 | |||
232 | this.rebuildAndUpdateVideoElement(currentPlayer, options.common) | ||
233 | |||
234 | await import('./shared/webtorrent/webtorrent-plugin') | ||
235 | |||
236 | const newPlayer = await this.buildPlayer('webtorrent', options) | ||
237 | this.onPlayerChange(newPlayer) | ||
238 | } | ||
239 | |||
240 | private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) { | ||
241 | const newVideoElement = document.createElement('video') | ||
242 | |||
243 | // Reset class | ||
244 | newVideoElement.className = this.playerElementClassName | ||
245 | |||
246 | // Reapply attributes | ||
247 | for (const { name, value } of this.playerElementAttributes) { | ||
248 | newVideoElement.setAttribute(name, value) | ||
249 | } | ||
250 | |||
251 | // VideoJS wraps our video element inside a div | ||
252 | let currentParentPlayerElement = commonOptions.playerElement.parentNode | ||
253 | // Fix on IOS, don't ask me why | ||
254 | if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode | ||
255 | |||
256 | currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement) | ||
257 | |||
258 | commonOptions.playerElement = newVideoElement | ||
259 | commonOptions.onPlayerElementChange(newVideoElement) | ||
260 | |||
261 | player.dispose() | ||
262 | |||
263 | return newVideoElement | ||
264 | } | ||
265 | |||
266 | private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) { | ||
267 | const options = optionsBuilder.getContextMenuOptions(player, commonOptions) | ||
268 | |||
269 | player.contextmenuUI(options) | ||
270 | } | ||
271 | } | ||
272 | |||
273 | // ############################################################################ | ||
274 | |||
275 | export { | ||
276 | videojs | ||
277 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts new file mode 100644 index 000000000..a7a2b4065 --- /dev/null +++ b/client/src/assets/player/peertube-player.ts | |||
@@ -0,0 +1,522 @@ | |||
1 | import '@peertube/videojs-contextmenu' | ||
2 | import './shared/upnext/end-card' | ||
3 | import './shared/upnext/upnext-plugin' | ||
4 | import './shared/stats/stats-card' | ||
5 | import './shared/stats/stats-plugin' | ||
6 | import './shared/bezels/bezels-plugin' | ||
7 | import './shared/peertube/peertube-plugin' | ||
8 | import './shared/resolutions/peertube-resolutions-plugin' | ||
9 | import './shared/control-bar/storyboard-plugin' | ||
10 | import './shared/control-bar/next-previous-video-button' | ||
11 | import './shared/control-bar/p2p-info-button' | ||
12 | import './shared/control-bar/peertube-link-button' | ||
13 | import './shared/control-bar/theater-button' | ||
14 | import './shared/control-bar/peertube-live-display' | ||
15 | import './shared/settings/resolution-menu-button' | ||
16 | import './shared/settings/resolution-menu-item' | ||
17 | import './shared/settings/settings-dialog' | ||
18 | import './shared/settings/settings-menu-button' | ||
19 | import './shared/settings/settings-menu-item' | ||
20 | import './shared/settings/settings-panel' | ||
21 | import './shared/settings/settings-panel-child' | ||
22 | import './shared/playlist/playlist-plugin' | ||
23 | import './shared/mobile/peertube-mobile-plugin' | ||
24 | import './shared/mobile/peertube-mobile-buttons' | ||
25 | import './shared/hotkeys/peertube-hotkeys-plugin' | ||
26 | import './shared/metrics/metrics-plugin' | ||
27 | import videojs, { VideoJsPlayer } from 'video.js' | ||
28 | import { logger } from '@root-helpers/logger' | ||
29 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
30 | import { copyToClipboard } from '@root-helpers/utils' | ||
31 | import { buildVideoOrPlaylistEmbed } from '@root-helpers/video' | ||
32 | import { isMobile } from '@root-helpers/web-browser' | ||
33 | import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@shared/core-utils' | ||
34 | import { saveAverageBandwidth } from './peertube-player-local-storage' | ||
35 | import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder' | ||
36 | import { TranslationsManager } from './translations-manager' | ||
37 | import { PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types' | ||
38 | |||
39 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
40 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | ||
41 | |||
42 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
43 | // Change Captions to Subtitles/CC | ||
44 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' | ||
45 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
46 | CaptionsButton.prototype.label_ = ' ' | ||
47 | |||
48 | // TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged | ||
49 | const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any | ||
50 | if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) { | ||
51 | PlayProgressBar.prototype.options_.children.push('timeTooltip') | ||
52 | } | ||
53 | |||
54 | export class PeerTubePlayer { | ||
55 | private pluginsManager: PluginsManager | ||
56 | |||
57 | private videojsDecodeErrors = 0 | ||
58 | |||
59 | private p2pMediaLoaderModule: any | ||
60 | |||
61 | private player: VideoJsPlayer | ||
62 | |||
63 | private currentLoadOptions: PeerTubePlayerLoadOptions | ||
64 | |||
65 | private moduleLoaded = { | ||
66 | webVideo: false, | ||
67 | p2pMediaLoader: false | ||
68 | } | ||
69 | |||
70 | constructor (private options: PeerTubePlayerContructorOptions) { | ||
71 | this.pluginsManager = options.pluginsManager | ||
72 | } | ||
73 | |||
74 | unload () { | ||
75 | if (!this.player) return | ||
76 | |||
77 | this.disposeDynamicPluginsIfNeeded() | ||
78 | |||
79 | this.player.reset() | ||
80 | } | ||
81 | |||
82 | async load (loadOptions: PeerTubePlayerLoadOptions) { | ||
83 | this.currentLoadOptions = loadOptions | ||
84 | |||
85 | this.setPoster('') | ||
86 | |||
87 | this.disposeDynamicPluginsIfNeeded() | ||
88 | |||
89 | await this.lazyLoadModulesIfNeeded() | ||
90 | await this.buildPlayerIfNeeded() | ||
91 | |||
92 | if (this.currentLoadOptions.mode === 'p2p-media-loader') { | ||
93 | await this.loadP2PMediaLoader() | ||
94 | } else { | ||
95 | this.loadWebVideo() | ||
96 | } | ||
97 | |||
98 | this.loadDynamicPlugins() | ||
99 | |||
100 | if (this.options.controlBar === false) this.player.controlBar.hide() | ||
101 | else this.player.controlBar.show() | ||
102 | |||
103 | this.player.autoplay(this.getAutoPlayValue(this.currentLoadOptions.autoplay)) | ||
104 | |||
105 | this.player.trigger('video-change') | ||
106 | } | ||
107 | |||
108 | getPlayer () { | ||
109 | return this.player | ||
110 | } | ||
111 | |||
112 | destroy () { | ||
113 | if (this.player) this.player.dispose() | ||
114 | } | ||
115 | |||
116 | setPoster (url: string) { | ||
117 | this.player?.poster(url) | ||
118 | this.options.playerElement().poster = url | ||
119 | } | ||
120 | |||
121 | enable () { | ||
122 | if (!this.player) return | ||
123 | |||
124 | (this.player.el() as HTMLElement).style.pointerEvents = 'auto' | ||
125 | } | ||
126 | |||
127 | disable () { | ||
128 | if (!this.player) return | ||
129 | |||
130 | if (this.player.isFullscreen()) { | ||
131 | this.player.exitFullscreen() | ||
132 | } | ||
133 | |||
134 | // Disable player | ||
135 | this.player.hasStarted(false) | ||
136 | this.player.removeClass('vjs-has-autoplay') | ||
137 | this.player.bigPlayButton.hide(); | ||
138 | |||
139 | (this.player.el() as HTMLElement).style.pointerEvents = 'none' | ||
140 | } | ||
141 | |||
142 | private async loadP2PMediaLoader () { | ||
143 | const hlsOptionsBuilder = new HLSOptionsBuilder({ | ||
144 | ...pick(this.options, [ 'pluginsManager', 'serverUrl', 'authorizationHeader' ]), | ||
145 | ...pick(this.currentLoadOptions, [ | ||
146 | 'videoPassword', | ||
147 | 'requiresUserAuth', | ||
148 | 'videoFileToken', | ||
149 | 'requiresPassword', | ||
150 | 'isLive', | ||
151 | 'p2pEnabled', | ||
152 | 'liveOptions', | ||
153 | 'hls' | ||
154 | ]) | ||
155 | }, this.p2pMediaLoaderModule) | ||
156 | |||
157 | const { hlsjs, p2pMediaLoader } = await hlsOptionsBuilder.getPluginOptions() | ||
158 | |||
159 | this.player.hlsjs(hlsjs) | ||
160 | this.player.p2pMediaLoader(p2pMediaLoader) | ||
161 | } | ||
162 | |||
163 | private loadWebVideo () { | ||
164 | const webVideoOptionsBuilder = new WebVideoOptionsBuilder(pick(this.currentLoadOptions, [ | ||
165 | 'videoFileToken', | ||
166 | 'webVideo', | ||
167 | 'hls', | ||
168 | 'startTime' | ||
169 | ])) | ||
170 | |||
171 | this.player.webVideo(webVideoOptionsBuilder.getPluginOptions()) | ||
172 | } | ||
173 | |||
174 | private async buildPlayerIfNeeded () { | ||
175 | if (this.player) return | ||
176 | |||
177 | await TranslationsManager.loadLocaleInVideoJS(this.options.serverUrl, this.options.language, videojs) | ||
178 | |||
179 | const videojsOptions = await this.pluginsManager.runHook( | ||
180 | 'filter:internal.player.videojs.options.result', | ||
181 | this.getVideojsOptions() | ||
182 | ) | ||
183 | |||
184 | this.player = videojs(this.options.playerElement(), videojsOptions) | ||
185 | |||
186 | this.player.ready(() => { | ||
187 | if (!isNaN(+this.options.playbackRate)) { | ||
188 | this.player.playbackRate(+this.options.playbackRate) | ||
189 | } | ||
190 | |||
191 | let alreadyFallback = false | ||
192 | |||
193 | const handleError = () => { | ||
194 | if (alreadyFallback) return | ||
195 | alreadyFallback = true | ||
196 | |||
197 | if (this.currentLoadOptions.mode === 'p2p-media-loader') { | ||
198 | this.tryToRecoverHLSError(this.player.error()) | ||
199 | } else { | ||
200 | this.maybeFallbackToWebVideo() | ||
201 | } | ||
202 | } | ||
203 | |||
204 | this.player.one('error', () => handleError()) | ||
205 | |||
206 | this.player.on('p2p-info', (_, data: PlayerNetworkInfo) => { | ||
207 | if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return | ||
208 | |||
209 | saveAverageBandwidth(data.bandwidthEstimate) | ||
210 | }) | ||
211 | |||
212 | this.player.contextmenuUI(this.getContextMenuOptions()) | ||
213 | |||
214 | this.displayNotificationWhenOffline() | ||
215 | }) | ||
216 | } | ||
217 | |||
218 | private disposeDynamicPluginsIfNeeded () { | ||
219 | if (!this.player) return | ||
220 | |||
221 | if (this.player.usingPlugin('peertubeMobile')) this.player.peertubeMobile().dispose() | ||
222 | if (this.player.usingPlugin('peerTubeHotkeysPlugin')) this.player.peerTubeHotkeysPlugin().dispose() | ||
223 | if (this.player.usingPlugin('playlist')) this.player.playlist().dispose() | ||
224 | if (this.player.usingPlugin('bezels')) this.player.bezels().dispose() | ||
225 | if (this.player.usingPlugin('upnext')) this.player.upnext().dispose() | ||
226 | if (this.player.usingPlugin('stats')) this.player.stats().dispose() | ||
227 | if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose() | ||
228 | |||
229 | if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose() | ||
230 | |||
231 | if (this.player.usingPlugin('p2pMediaLoader')) this.player.p2pMediaLoader().dispose() | ||
232 | if (this.player.usingPlugin('hlsjs')) this.player.hlsjs().dispose() | ||
233 | |||
234 | if (this.player.usingPlugin('webVideo')) this.player.webVideo().dispose() | ||
235 | } | ||
236 | |||
237 | private loadDynamicPlugins () { | ||
238 | if (isMobile()) this.player.peertubeMobile() | ||
239 | |||
240 | this.player.bezels() | ||
241 | |||
242 | this.player.stats({ | ||
243 | videoUUID: this.currentLoadOptions.videoUUID, | ||
244 | videoIsLive: this.currentLoadOptions.isLive, | ||
245 | mode: this.currentLoadOptions.mode, | ||
246 | p2pEnabled: this.currentLoadOptions.p2pEnabled | ||
247 | }) | ||
248 | |||
249 | if (this.options.enableHotkeys === true) { | ||
250 | this.player.peerTubeHotkeysPlugin({ isLive: this.currentLoadOptions.isLive }) | ||
251 | } | ||
252 | |||
253 | if (this.currentLoadOptions.playlist) { | ||
254 | this.player.playlist(this.currentLoadOptions.playlist) | ||
255 | } | ||
256 | |||
257 | if (this.currentLoadOptions.upnext) { | ||
258 | this.player.upnext({ | ||
259 | timeout: this.currentLoadOptions.upnext.timeout, | ||
260 | |||
261 | getTitle: () => this.currentLoadOptions.nextVideo.getVideoTitle(), | ||
262 | |||
263 | next: () => this.currentLoadOptions.nextVideo.handler(), | ||
264 | isDisplayed: () => this.currentLoadOptions.nextVideo.enabled && this.currentLoadOptions.upnext.isEnabled(), | ||
265 | |||
266 | isSuspended: () => this.currentLoadOptions.upnext.isSuspended(this.player) | ||
267 | }) | ||
268 | } | ||
269 | |||
270 | if (this.currentLoadOptions.storyboard) { | ||
271 | this.player.storyboard(this.currentLoadOptions.storyboard) | ||
272 | } | ||
273 | |||
274 | if (this.currentLoadOptions.dock) { | ||
275 | this.player.peertubeDock(this.currentLoadOptions.dock) | ||
276 | } | ||
277 | } | ||
278 | |||
279 | private async lazyLoadModulesIfNeeded () { | ||
280 | if (this.currentLoadOptions.mode === 'web-video' && this.moduleLoaded.webVideo !== true) { | ||
281 | await import('./shared/web-video/web-video-plugin') | ||
282 | } | ||
283 | |||
284 | if (this.currentLoadOptions.mode === 'p2p-media-loader' && this.moduleLoaded.p2pMediaLoader !== true) { | ||
285 | const [ p2pMediaLoaderModule ] = await Promise.all([ | ||
286 | import('@peertube/p2p-media-loader-hlsjs'), | ||
287 | import('./shared/p2p-media-loader/hls-plugin'), | ||
288 | import('./shared/p2p-media-loader/p2p-media-loader-plugin') | ||
289 | ]) | ||
290 | |||
291 | this.p2pMediaLoaderModule = p2pMediaLoaderModule | ||
292 | } | ||
293 | } | ||
294 | |||
295 | private async tryToRecoverHLSError (err: any) { | ||
296 | if (err.code === MediaError.MEDIA_ERR_DECODE) { | ||
297 | |||
298 | // Display a notification to user | ||
299 | if (this.videojsDecodeErrors === 0) { | ||
300 | this.options.errorNotifier(this.player.localize('The video failed to play, will try to fast forward.')) | ||
301 | } | ||
302 | |||
303 | if (this.videojsDecodeErrors === 20) { | ||
304 | this.maybeFallbackToWebVideo() | ||
305 | return | ||
306 | } | ||
307 | |||
308 | logger.info('Fast forwarding HLS to recover from an error.') | ||
309 | |||
310 | this.videojsDecodeErrors++ | ||
311 | |||
312 | await this.load({ | ||
313 | ...this.currentLoadOptions, | ||
314 | |||
315 | mode: 'p2p-media-loader', | ||
316 | startTime: this.player.currentTime() + 2, | ||
317 | autoplay: true | ||
318 | }) | ||
319 | } else { | ||
320 | this.maybeFallbackToWebVideo() | ||
321 | } | ||
322 | } | ||
323 | |||
324 | private async maybeFallbackToWebVideo () { | ||
325 | if (this.currentLoadOptions.webVideo.videoFiles.length === 0 || this.currentLoadOptions.mode === 'web-video') { | ||
326 | this.player.peertube().displayFatalError() | ||
327 | return | ||
328 | } | ||
329 | |||
330 | logger.info('Fallback to web-video.') | ||
331 | |||
332 | await this.load({ | ||
333 | ...this.currentLoadOptions, | ||
334 | |||
335 | mode: 'web-video', | ||
336 | startTime: this.player.currentTime(), | ||
337 | autoplay: true | ||
338 | }) | ||
339 | } | ||
340 | |||
341 | getVideojsOptions (): videojs.PlayerOptions { | ||
342 | const html5 = { | ||
343 | preloadTextTracks: false | ||
344 | } | ||
345 | |||
346 | const plugins: VideoJSPluginOptions = { | ||
347 | peertube: { | ||
348 | hasAutoplay: () => this.getAutoPlayValue(this.currentLoadOptions.autoplay), | ||
349 | |||
350 | videoViewUrl: () => this.currentLoadOptions.videoViewUrl, | ||
351 | videoViewIntervalMs: this.options.videoViewIntervalMs, | ||
352 | |||
353 | authorizationHeader: this.options.authorizationHeader, | ||
354 | |||
355 | videoDuration: () => this.currentLoadOptions.duration, | ||
356 | |||
357 | startTime: () => this.currentLoadOptions.startTime, | ||
358 | stopTime: () => this.currentLoadOptions.stopTime, | ||
359 | |||
360 | videoCaptions: () => this.currentLoadOptions.videoCaptions, | ||
361 | isLive: () => this.currentLoadOptions.isLive, | ||
362 | videoUUID: () => this.currentLoadOptions.videoUUID, | ||
363 | subtitle: () => this.currentLoadOptions.subtitle | ||
364 | }, | ||
365 | metrics: { | ||
366 | mode: () => this.currentLoadOptions.mode, | ||
367 | |||
368 | metricsUrl: () => this.options.metricsUrl, | ||
369 | videoUUID: () => this.currentLoadOptions.videoUUID | ||
370 | } | ||
371 | } | ||
372 | |||
373 | const controlBarOptionsBuilder = new ControlBarOptionsBuilder({ | ||
374 | ...this.options, | ||
375 | |||
376 | videoShortUUID: () => this.currentLoadOptions.videoShortUUID, | ||
377 | p2pEnabled: () => this.currentLoadOptions.p2pEnabled, | ||
378 | |||
379 | nextVideo: () => this.currentLoadOptions.nextVideo, | ||
380 | previousVideo: () => this.currentLoadOptions.previousVideo | ||
381 | }) | ||
382 | |||
383 | const videojsOptions = { | ||
384 | html5, | ||
385 | |||
386 | // We don't use text track settings for now | ||
387 | textTrackSettings: false as any, // FIXME: typings | ||
388 | controls: this.options.controls !== undefined ? this.options.controls : true, | ||
389 | loop: this.options.loop !== undefined ? this.options.loop : false, | ||
390 | |||
391 | muted: this.options.muted !== undefined | ||
392 | ? this.options.muted | ||
393 | : undefined, // Undefined so the player knows it has to check the local storage | ||
394 | |||
395 | autoplay: this.getAutoPlayValue(this.currentLoadOptions.autoplay), | ||
396 | |||
397 | poster: this.currentLoadOptions.poster, | ||
398 | inactivityTimeout: this.options.inactivityTimeout, | ||
399 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], | ||
400 | |||
401 | plugins, | ||
402 | |||
403 | controlBar: { | ||
404 | children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings | ||
405 | }, | ||
406 | |||
407 | language: this.options.language && !isDefaultLocale(this.options.language) | ||
408 | ? this.options.language | ||
409 | : undefined | ||
410 | } | ||
411 | |||
412 | return videojsOptions | ||
413 | } | ||
414 | |||
415 | private getAutoPlayValue (autoplay: boolean): videojs.Autoplay { | ||
416 | if (autoplay !== true) return false | ||
417 | |||
418 | return this.currentLoadOptions.forceAutoplay | ||
419 | ? 'any' | ||
420 | : 'play' | ||
421 | } | ||
422 | |||
423 | private displayNotificationWhenOffline () { | ||
424 | const offlineNotificationElem = document.createElement('div') | ||
425 | offlineNotificationElem.classList.add('vjs-peertube-offline-notification') | ||
426 | offlineNotificationElem.innerText = this.player.localize('You seem to be offline and the video may not work') | ||
427 | |||
428 | let offlineNotificationElemAdded = false | ||
429 | |||
430 | const handleOnline = () => { | ||
431 | if (!offlineNotificationElemAdded) return | ||
432 | |||
433 | this.player.el().removeChild(offlineNotificationElem) | ||
434 | offlineNotificationElemAdded = false | ||
435 | |||
436 | logger.info('The browser is online') | ||
437 | } | ||
438 | |||
439 | const handleOffline = () => { | ||
440 | if (offlineNotificationElemAdded) return | ||
441 | |||
442 | this.player.el().appendChild(offlineNotificationElem) | ||
443 | offlineNotificationElemAdded = true | ||
444 | |||
445 | logger.info('The browser is offline') | ||
446 | } | ||
447 | |||
448 | window.addEventListener('online', handleOnline) | ||
449 | window.addEventListener('offline', handleOffline) | ||
450 | |||
451 | this.player.on('dispose', () => { | ||
452 | window.removeEventListener('online', handleOnline) | ||
453 | window.removeEventListener('offline', handleOffline) | ||
454 | }) | ||
455 | } | ||
456 | |||
457 | private getContextMenuOptions () { | ||
458 | |||
459 | const content = () => { | ||
460 | const self = this | ||
461 | const player = this.player | ||
462 | |||
463 | const shortUUID = self.currentLoadOptions.videoShortUUID | ||
464 | const isLoopEnabled = player.options_['loop'] | ||
465 | |||
466 | const items = [ | ||
467 | { | ||
468 | icon: 'repeat', | ||
469 | label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''), | ||
470 | listener: function () { | ||
471 | player.options_['loop'] = !isLoopEnabled | ||
472 | } | ||
473 | }, | ||
474 | { | ||
475 | label: player.localize('Copy the video URL'), | ||
476 | listener: function () { | ||
477 | copyToClipboard(buildVideoLink({ shortUUID })) | ||
478 | } | ||
479 | }, | ||
480 | { | ||
481 | label: player.localize('Copy the video URL at the current time'), | ||
482 | listener: function () { | ||
483 | const url = buildVideoLink({ shortUUID }) | ||
484 | |||
485 | copyToClipboard(decorateVideoLink({ url, startTime: player.currentTime() })) | ||
486 | } | ||
487 | }, | ||
488 | { | ||
489 | icon: 'code', | ||
490 | label: player.localize('Copy embed code'), | ||
491 | listener: () => { | ||
492 | copyToClipboard(buildVideoOrPlaylistEmbed({ | ||
493 | embedUrl: self.currentLoadOptions.embedUrl, | ||
494 | embedTitle: self.currentLoadOptions.embedTitle | ||
495 | })) | ||
496 | } | ||
497 | } | ||
498 | ] | ||
499 | |||
500 | items.push({ | ||
501 | icon: 'info', | ||
502 | label: player.localize('Stats for nerds'), | ||
503 | listener: () => { | ||
504 | player.stats().show() | ||
505 | } | ||
506 | }) | ||
507 | |||
508 | return items.map(i => ({ | ||
509 | ...i, | ||
510 | label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label | ||
511 | })) | ||
512 | } | ||
513 | |||
514 | return { content } | ||
515 | } | ||
516 | } | ||
517 | |||
518 | // ############################################################################ | ||
519 | |||
520 | export { | ||
521 | videojs | ||
522 | } | ||
diff --git a/client/src/assets/player/shared/bezels/bezels-plugin.ts b/client/src/assets/player/shared/bezels/bezels-plugin.ts index ca88bc1f9..6afb2c6a3 100644 --- a/client/src/assets/player/shared/bezels/bezels-plugin.ts +++ b/client/src/assets/player/shared/bezels/bezels-plugin.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import './pause-bezel' | 2 | import { PauseBezel } from './pause-bezel' |
3 | 3 | ||
4 | const Plugin = videojs.getPlugin('plugin') | 4 | const Plugin = videojs.getPlugin('plugin') |
5 | 5 | ||
@@ -12,7 +12,7 @@ class BezelsPlugin extends Plugin { | |||
12 | player.addClass('vjs-bezels') | 12 | player.addClass('vjs-bezels') |
13 | }) | 13 | }) |
14 | 14 | ||
15 | player.addChild('PauseBezel', options) | 15 | player.addChild(new PauseBezel(player, options)) |
16 | } | 16 | } |
17 | } | 17 | } |
18 | 18 | ||
diff --git a/client/src/assets/player/shared/bezels/pause-bezel.ts b/client/src/assets/player/shared/bezels/pause-bezel.ts index e35c39a5f..d364ad0dd 100644 --- a/client/src/assets/player/shared/bezels/pause-bezel.ts +++ b/client/src/assets/player/shared/bezels/pause-bezel.ts | |||
@@ -32,26 +32,61 @@ function getPlayBezel () { | |||
32 | } | 32 | } |
33 | 33 | ||
34 | const Component = videojs.getComponent('Component') | 34 | const Component = videojs.getComponent('Component') |
35 | class PauseBezel extends Component { | 35 | export class PauseBezel extends Component { |
36 | container: HTMLDivElement | 36 | container: HTMLDivElement |
37 | 37 | ||
38 | private firstPlayDone = false | ||
39 | private paused = false | ||
40 | |||
41 | private playerPauseHandler: () => void | ||
42 | private playerPlayHandler: () => void | ||
43 | private videoChangeHandler: () => void | ||
44 | |||
38 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { | 45 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { |
39 | super(player, options) | 46 | super(player, options) |
40 | 47 | ||
41 | // Hide bezels on mobile since we already have our mobile overlay | 48 | // Hide bezels on mobile since we already have our mobile overlay |
42 | if (isMobile()) return | 49 | if (isMobile()) return |
43 | 50 | ||
44 | player.on('pause', (_: any) => { | 51 | this.playerPauseHandler = () => { |
45 | if (player.seeking() || player.ended()) return | 52 | if (player.seeking()) return |
53 | |||
54 | this.paused = true | ||
55 | |||
56 | if (player.ended()) return | ||
57 | |||
46 | this.container.innerHTML = getPauseBezel() | 58 | this.container.innerHTML = getPauseBezel() |
47 | this.showBezel() | 59 | this.showBezel() |
48 | }) | 60 | } |
61 | |||
62 | this.playerPlayHandler = () => { | ||
63 | if (player.seeking() || !this.firstPlayDone || !this.paused) { | ||
64 | this.firstPlayDone = true | ||
65 | return | ||
66 | } | ||
67 | |||
68 | this.paused = false | ||
69 | this.firstPlayDone = true | ||
49 | 70 | ||
50 | player.on('play', (_: any) => { | ||
51 | if (player.seeking()) return | ||
52 | this.container.innerHTML = getPlayBezel() | 71 | this.container.innerHTML = getPlayBezel() |
53 | this.showBezel() | 72 | this.showBezel() |
54 | }) | 73 | } |
74 | |||
75 | this.videoChangeHandler = () => { | ||
76 | this.firstPlayDone = false | ||
77 | } | ||
78 | |||
79 | player.on('video-change', () => this.videoChangeHandler) | ||
80 | player.on('pause', this.playerPauseHandler) | ||
81 | player.on('play', this.playerPlayHandler) | ||
82 | } | ||
83 | |||
84 | dispose () { | ||
85 | if (this.playerPauseHandler) this.player().off('pause', this.playerPauseHandler) | ||
86 | if (this.playerPlayHandler) this.player().off('play', this.playerPlayHandler) | ||
87 | if (this.videoChangeHandler) this.player().off('video-change', this.videoChangeHandler) | ||
88 | |||
89 | super.dispose() | ||
55 | } | 90 | } |
56 | 91 | ||
57 | createEl () { | 92 | createEl () { |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index 24877c267..9307027f6 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -2,6 +2,5 @@ export * from './next-previous-video-button' | |||
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | 4 | export * from './peertube-live-display' |
5 | export * from './peertube-load-progress-bar' | ||
6 | export * from './storyboard-plugin' | 5 | export * from './storyboard-plugin' |
7 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts index b7b986806..18a107f52 100644 --- a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts +++ b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts | |||
@@ -4,14 +4,18 @@ import { NextPreviousVideoButtonOptions } from '../../types' | |||
4 | const Button = videojs.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
5 | 5 | ||
6 | class NextPreviousVideoButton extends Button { | 6 | class NextPreviousVideoButton extends Button { |
7 | private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions | 7 | options_: NextPreviousVideoButtonOptions & videojs.ComponentOptions |
8 | 8 | ||
9 | constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) { | 9 | constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions & videojs.ComponentOptions) { |
10 | super(player, options as any) | 10 | super(player, options) |
11 | 11 | ||
12 | this.nextPreviousVideoButtonOptions = options | 12 | this.player().on('video-change', () => { |
13 | this.updateDisabled() | ||
14 | this.updateShowing() | ||
15 | }) | ||
13 | 16 | ||
14 | this.update() | 17 | this.updateDisabled() |
18 | this.updateShowing() | ||
15 | } | 19 | } |
16 | 20 | ||
17 | createEl () { | 21 | createEl () { |
@@ -35,15 +39,20 @@ class NextPreviousVideoButton extends Button { | |||
35 | } | 39 | } |
36 | 40 | ||
37 | handleClick () { | 41 | handleClick () { |
38 | this.nextPreviousVideoButtonOptions.handler() | 42 | this.options_.handler() |
39 | } | 43 | } |
40 | 44 | ||
41 | update () { | 45 | updateDisabled () { |
42 | const disabled = this.nextPreviousVideoButtonOptions.isDisabled() | 46 | const disabled = this.options_.isDisabled() |
43 | 47 | ||
44 | if (disabled) this.addClass('vjs-disabled') | 48 | if (disabled) this.addClass('vjs-disabled') |
45 | else this.removeClass('vjs-disabled') | 49 | else this.removeClass('vjs-disabled') |
46 | } | 50 | } |
51 | |||
52 | updateShowing () { | ||
53 | if (this.options_.isDisplayed()) this.show() | ||
54 | else this.hide() | ||
55 | } | ||
47 | } | 56 | } |
48 | 57 | ||
49 | videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) | 58 | videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) |
diff --git a/client/src/assets/player/shared/control-bar/p2p-info-button.ts b/client/src/assets/player/shared/control-bar/p2p-info-button.ts index 1979654ad..4177b3280 100644 --- a/client/src/assets/player/shared/control-bar/p2p-info-button.ts +++ b/client/src/assets/player/shared/control-bar/p2p-info-button.ts | |||
@@ -1,71 +1,44 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { PeerTubeP2PInfoButtonOptions, PlayerNetworkInfo } from '../../types' | 2 | import { PlayerNetworkInfo } from '../../types' |
3 | import { bytes } from '../common' | 3 | import { bytes } from '../common' |
4 | 4 | ||
5 | const Button = videojs.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
6 | class P2pInfoButton extends Button { | 6 | class P2PInfoButton extends Button { |
7 | 7 | el_: HTMLElement | |
8 | constructor (player: videojs.Player, options?: PeerTubeP2PInfoButtonOptions) { | ||
9 | super(player, options as any) | ||
10 | } | ||
11 | 8 | ||
12 | createEl () { | 9 | createEl () { |
13 | const div = videojs.dom.createEl('div', { | 10 | const div = videojs.dom.createEl('div', { className: 'vjs-peertube' }) |
14 | className: 'vjs-peertube' | 11 | const subDivP2P = videojs.dom.createEl('div', { |
15 | }) | ||
16 | const subDivWebtorrent = videojs.dom.createEl('div', { | ||
17 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info | 12 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info |
18 | }) as HTMLDivElement | 13 | }) as HTMLDivElement |
19 | div.appendChild(subDivWebtorrent) | 14 | div.appendChild(subDivP2P) |
20 | 15 | ||
21 | // Stop here if P2P is not enabled | 16 | const downloadIcon = videojs.dom.createEl('span', { className: 'icon icon-download' }) |
22 | const p2pEnabled = (this.options_ as PeerTubeP2PInfoButtonOptions).p2pEnabled | 17 | subDivP2P.appendChild(downloadIcon) |
23 | if (!p2pEnabled) return div as HTMLButtonElement | ||
24 | 18 | ||
25 | const downloadIcon = videojs.dom.createEl('span', { | 19 | const downloadSpeedText = videojs.dom.createEl('span', { className: 'download-speed-text' }) |
26 | className: 'icon icon-download' | 20 | const downloadSpeedNumber = videojs.dom.createEl('span', { className: 'download-speed-number' }) |
27 | }) | ||
28 | subDivWebtorrent.appendChild(downloadIcon) | ||
29 | |||
30 | const downloadSpeedText = videojs.dom.createEl('span', { | ||
31 | className: 'download-speed-text' | ||
32 | }) | ||
33 | const downloadSpeedNumber = videojs.dom.createEl('span', { | ||
34 | className: 'download-speed-number' | ||
35 | }) | ||
36 | const downloadSpeedUnit = videojs.dom.createEl('span') | 21 | const downloadSpeedUnit = videojs.dom.createEl('span') |
37 | downloadSpeedText.appendChild(downloadSpeedNumber) | 22 | downloadSpeedText.appendChild(downloadSpeedNumber) |
38 | downloadSpeedText.appendChild(downloadSpeedUnit) | 23 | downloadSpeedText.appendChild(downloadSpeedUnit) |
39 | subDivWebtorrent.appendChild(downloadSpeedText) | 24 | subDivP2P.appendChild(downloadSpeedText) |
40 | 25 | ||
41 | const uploadIcon = videojs.dom.createEl('span', { | 26 | const uploadIcon = videojs.dom.createEl('span', { className: 'icon icon-upload' }) |
42 | className: 'icon icon-upload' | 27 | subDivP2P.appendChild(uploadIcon) |
43 | }) | ||
44 | subDivWebtorrent.appendChild(uploadIcon) | ||
45 | 28 | ||
46 | const uploadSpeedText = videojs.dom.createEl('span', { | 29 | const uploadSpeedText = videojs.dom.createEl('span', { className: 'upload-speed-text' }) |
47 | className: 'upload-speed-text' | 30 | const uploadSpeedNumber = videojs.dom.createEl('span', { className: 'upload-speed-number' }) |
48 | }) | ||
49 | const uploadSpeedNumber = videojs.dom.createEl('span', { | ||
50 | className: 'upload-speed-number' | ||
51 | }) | ||
52 | const uploadSpeedUnit = videojs.dom.createEl('span') | 31 | const uploadSpeedUnit = videojs.dom.createEl('span') |
53 | uploadSpeedText.appendChild(uploadSpeedNumber) | 32 | uploadSpeedText.appendChild(uploadSpeedNumber) |
54 | uploadSpeedText.appendChild(uploadSpeedUnit) | 33 | uploadSpeedText.appendChild(uploadSpeedUnit) |
55 | subDivWebtorrent.appendChild(uploadSpeedText) | 34 | subDivP2P.appendChild(uploadSpeedText) |
56 | 35 | ||
57 | const peersText = videojs.dom.createEl('span', { | 36 | const peersText = videojs.dom.createEl('span', { className: 'peers-text' }) |
58 | className: 'peers-text' | 37 | const peersNumber = videojs.dom.createEl('span', { className: 'peers-number' }) |
59 | }) | 38 | subDivP2P.appendChild(peersNumber) |
60 | const peersNumber = videojs.dom.createEl('span', { | 39 | subDivP2P.appendChild(peersText) |
61 | className: 'peers-number' | ||
62 | }) | ||
63 | subDivWebtorrent.appendChild(peersNumber) | ||
64 | subDivWebtorrent.appendChild(peersText) | ||
65 | 40 | ||
66 | const subDivHttp = videojs.dom.createEl('div', { | 41 | const subDivHttp = videojs.dom.createEl('div', { className: 'vjs-peertube-hidden' }) as HTMLElement |
67 | className: 'vjs-peertube-hidden' | ||
68 | }) | ||
69 | const subDivHttpText = videojs.dom.createEl('span', { | 42 | const subDivHttpText = videojs.dom.createEl('span', { |
70 | className: 'http-fallback', | 43 | className: 'http-fallback', |
71 | textContent: 'HTTP' | 44 | textContent: 'HTTP' |
@@ -74,14 +47,9 @@ class P2pInfoButton extends Button { | |||
74 | subDivHttp.appendChild(subDivHttpText) | 47 | subDivHttp.appendChild(subDivHttpText) |
75 | div.appendChild(subDivHttp) | 48 | div.appendChild(subDivHttp) |
76 | 49 | ||
77 | this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { | 50 | this.player_.on('p2p-info', (_event: any, data: PlayerNetworkInfo) => { |
78 | // We are in HTTP fallback | 51 | subDivP2P.className = 'vjs-peertube-displayed' |
79 | if (!data) { | 52 | subDivHttp.className = 'vjs-peertube-hidden' |
80 | subDivHttp.className = 'vjs-peertube-displayed' | ||
81 | subDivWebtorrent.className = 'vjs-peertube-hidden' | ||
82 | |||
83 | return | ||
84 | } | ||
85 | 53 | ||
86 | const p2pStats = data.p2p | 54 | const p2pStats = data.p2p |
87 | const httpStats = data.http | 55 | const httpStats = data.http |
@@ -92,17 +60,17 @@ class P2pInfoButton extends Button { | |||
92 | const totalUploaded = bytes(p2pStats.uploaded) | 60 | const totalUploaded = bytes(p2pStats.uploaded) |
93 | const numPeers = p2pStats.numPeers | 61 | const numPeers = p2pStats.numPeers |
94 | 62 | ||
95 | subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' | 63 | subDivP2P.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' |
96 | 64 | ||
97 | if (data.source === 'p2p-media-loader') { | 65 | if (data.source === 'p2p-media-loader') { |
98 | const downloadedFromServer = bytes(httpStats.downloaded).join(' ') | 66 | const downloadedFromServer = bytes(httpStats.downloaded).join(' ') |
99 | const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | 67 | const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') |
100 | 68 | ||
101 | subDivWebtorrent.title += | 69 | subDivP2P.title += |
102 | ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + | 70 | ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + |
103 | ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' | 71 | ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' |
104 | } | 72 | } |
105 | subDivWebtorrent.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') | 73 | subDivP2P.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') |
106 | 74 | ||
107 | downloadSpeedNumber.textContent = downloadSpeed[0] | 75 | downloadSpeedNumber.textContent = downloadSpeed[0] |
108 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] | 76 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] |
@@ -114,11 +82,24 @@ class P2pInfoButton extends Button { | |||
114 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) | 82 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) |
115 | 83 | ||
116 | subDivHttp.className = 'vjs-peertube-hidden' | 84 | subDivHttp.className = 'vjs-peertube-hidden' |
117 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 85 | subDivP2P.className = 'vjs-peertube-displayed' |
86 | }) | ||
87 | |||
88 | this.player_.on('http-info', (_event, data: PlayerNetworkInfo) => { | ||
89 | // We are in HTTP fallback | ||
90 | subDivHttp.className = 'vjs-peertube-displayed' | ||
91 | subDivP2P.className = 'vjs-peertube-hidden' | ||
92 | |||
93 | subDivHttp.title = this.player().localize('Total downloaded: ') + bytes(data.http.downloaded).join(' ') | ||
94 | }) | ||
95 | |||
96 | this.player_.on('video-change', () => { | ||
97 | subDivP2P.className = 'vjs-peertube-hidden' | ||
98 | subDivHttp.className = 'vjs-peertube-hidden' | ||
118 | }) | 99 | }) |
119 | 100 | ||
120 | return div as HTMLButtonElement | 101 | return div as HTMLButtonElement |
121 | } | 102 | } |
122 | } | 103 | } |
123 | 104 | ||
124 | videojs.registerComponent('P2PInfoButton', P2pInfoButton) | 105 | videojs.registerComponent('P2PInfoButton', P2PInfoButton) |
diff --git a/client/src/assets/player/shared/control-bar/peertube-link-button.ts b/client/src/assets/player/shared/control-bar/peertube-link-button.ts index 45d7ac42f..8242b9cea 100644 --- a/client/src/assets/player/shared/control-bar/peertube-link-button.ts +++ b/client/src/assets/player/shared/control-bar/peertube-link-button.ts | |||
@@ -3,37 +3,58 @@ import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | |||
3 | import { PeerTubeLinkButtonOptions } from '../../types' | 3 | import { PeerTubeLinkButtonOptions } from '../../types' |
4 | 4 | ||
5 | const Component = videojs.getComponent('Component') | 5 | const Component = videojs.getComponent('Component') |
6 | |||
6 | class PeerTubeLinkButton extends Component { | 7 | class PeerTubeLinkButton extends Component { |
8 | private mouseEnterHandler: () => void | ||
9 | private clickHandler: () => void | ||
7 | 10 | ||
8 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | 11 | options_: PeerTubeLinkButtonOptions & videojs.ComponentOptions |
9 | super(player, options as any) | ||
10 | } | ||
11 | 12 | ||
12 | createEl () { | 13 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions & videojs.ComponentOptions) { |
13 | return this.buildElement() | 14 | super(player, options) |
15 | |||
16 | this.updateShowing() | ||
17 | this.player().on('video-change', () => this.updateShowing()) | ||
14 | } | 18 | } |
15 | 19 | ||
16 | updateHref () { | 20 | dispose () { |
17 | this.el().setAttribute('href', this.buildLink()) | 21 | if (this.el()) return |
22 | |||
23 | this.el().removeEventListener('mouseenter', this.mouseEnterHandler) | ||
24 | this.el().removeEventListener('click', this.clickHandler) | ||
25 | |||
26 | super.dispose() | ||
18 | } | 27 | } |
19 | 28 | ||
20 | private buildElement () { | 29 | createEl () { |
21 | const el = videojs.dom.createEl('a', { | 30 | const el = videojs.dom.createEl('a', { |
22 | href: this.buildLink(), | 31 | href: this.buildLink(), |
23 | innerHTML: (this.options_ as PeerTubeLinkButtonOptions).instanceName, | 32 | innerHTML: this.options_.instanceName, |
24 | title: this.player().localize('Video page (new window)'), | 33 | title: this.player().localize('Video page (new window)'), |
25 | className: 'vjs-peertube-link', | 34 | className: 'vjs-peertube-link', |
26 | target: '_blank' | 35 | target: '_blank' |
27 | }) | 36 | }) |
28 | 37 | ||
29 | el.addEventListener('mouseenter', () => this.updateHref()) | 38 | this.mouseEnterHandler = () => this.updateHref() |
30 | el.addEventListener('click', () => this.player().pause()) | 39 | this.clickHandler = () => this.player().pause() |
40 | |||
41 | el.addEventListener('mouseenter', this.mouseEnterHandler) | ||
42 | el.addEventListener('click', this.clickHandler) | ||
43 | |||
44 | return el | ||
45 | } | ||
46 | |||
47 | updateShowing () { | ||
48 | if (this.options_.isDisplayed()) this.show() | ||
49 | else this.hide() | ||
50 | } | ||
31 | 51 | ||
32 | return el as HTMLButtonElement | 52 | updateHref () { |
53 | this.el().setAttribute('href', this.buildLink()) | ||
33 | } | 54 | } |
34 | 55 | ||
35 | private buildLink () { | 56 | private buildLink () { |
36 | const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID }) | 57 | const url = buildVideoLink({ shortUUID: this.options_.shortUUID() }) |
37 | 58 | ||
38 | return decorateVideoLink({ url, startTime: this.player().currentTime() }) | 59 | return decorateVideoLink({ url, startTime: this.player().currentTime() }) |
39 | } | 60 | } |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts index 649eb0b00..f9f6bf12f 100644 --- a/client/src/assets/player/shared/control-bar/peertube-live-display.ts +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -13,7 +13,6 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
13 | 13 | ||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | 14 | this.interval = this.setInterval(() => this.updateClass(), 1000) |
15 | 15 | ||
16 | this.show() | ||
17 | this.updateSync(true) | 16 | this.updateSync(true) |
18 | } | 17 | } |
19 | 18 | ||
@@ -30,7 +29,7 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
30 | 29 | ||
31 | createEl () { | 30 | createEl () { |
32 | const el = super.createEl('div', { | 31 | const el = super.createEl('div', { |
33 | className: 'vjs-live-control vjs-control' | 32 | className: 'vjs-pt-live-control vjs-control' |
34 | }) | 33 | }) |
35 | 34 | ||
36 | this.contentEl_ = videojs.dom.createEl('div', { | 35 | this.contentEl_ = videojs.dom.createEl('div', { |
@@ -83,10 +82,9 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
83 | } | 82 | } |
84 | 83 | ||
85 | private getHLSJS () { | 84 | private getHLSJS () { |
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | 85 | if (!this.player()?.usingPlugin('p2pMediaLoader')) return |
87 | if (!p2pMediaLoader) return undefined | ||
88 | 86 | ||
89 | return p2pMediaLoader().getHLSJS() | 87 | return this.player().p2pMediaLoader().getHLSJS() |
90 | } | 88 | } |
91 | } | 89 | } |
92 | 90 | ||
diff --git a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts b/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts deleted file mode 100644 index 623e70eb2..000000000 --- a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class PeerTubeLoadProgressBar extends Component { | ||
6 | |||
7 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | |||
10 | this.on(player, 'progress', this.update) | ||
11 | } | ||
12 | |||
13 | createEl () { | ||
14 | return super.createEl('div', { | ||
15 | className: 'vjs-load-progress', | ||
16 | innerHTML: `<span class="vjs-control-text"><span>${this.localize('Loaded')}</span>: 0%</span>` | ||
17 | }) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | super.dispose() | ||
22 | } | ||
23 | |||
24 | update () { | ||
25 | const torrent = this.player().webtorrent().getTorrent() | ||
26 | if (!torrent) return | ||
27 | |||
28 | (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' | ||
29 | } | ||
30 | |||
31 | } | ||
32 | |||
33 | Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar) | ||
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 81ab60842..80c69b5f2 100644 --- a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts +++ b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts | |||
@@ -24,6 +24,8 @@ class StoryboardPlugin extends Plugin { | |||
24 | 24 | ||
25 | private readonly boundedHijackMouseTooltip: typeof StoryboardPlugin.prototype.hijackMouseTooltip | 25 | private readonly boundedHijackMouseTooltip: typeof StoryboardPlugin.prototype.hijackMouseTooltip |
26 | 26 | ||
27 | private onReadyOrLoadstartHandler: (event: { type: 'ready' }) => void | ||
28 | |||
27 | constructor (player: videojs.Player, options: videojs.ComponentOptions & StoryboardOptions) { | 29 | constructor (player: videojs.Player, options: videojs.ComponentOptions & StoryboardOptions) { |
28 | super(player, options) | 30 | super(player, options) |
29 | 31 | ||
@@ -54,7 +56,7 @@ class StoryboardPlugin extends Plugin { | |||
54 | this.spritePlaceholder = videojs.dom.createEl('div', { className: 'vjs-storyboard-sprite-placeholder' }) as HTMLElement | 56 | this.spritePlaceholder = videojs.dom.createEl('div', { className: 'vjs-storyboard-sprite-placeholder' }) as HTMLElement |
55 | this.seekBar?.el()?.appendChild(this.spritePlaceholder) | 57 | this.seekBar?.el()?.appendChild(this.spritePlaceholder) |
56 | 58 | ||
57 | this.player.on([ 'ready', 'loadstart' ], event => { | 59 | this.onReadyOrLoadstartHandler = event => { |
58 | if (event.type !== 'ready') { | 60 | if (event.type !== 'ready') { |
59 | const spriteSource = this.player.currentSources().find(source => { | 61 | const spriteSource = this.player.currentSources().find(source => { |
60 | return Object.prototype.hasOwnProperty.call(source, 'storyboard') | 62 | return Object.prototype.hasOwnProperty.call(source, 'storyboard') |
@@ -72,7 +74,18 @@ class StoryboardPlugin extends Plugin { | |||
72 | this.cached = !!this.sprites[this.url] | 74 | this.cached = !!this.sprites[this.url] |
73 | 75 | ||
74 | this.load() | 76 | this.load() |
75 | }) | 77 | } |
78 | |||
79 | this.player.on([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler) | ||
80 | } | ||
81 | |||
82 | dispose () { | ||
83 | if (this.onReadyOrLoadstartHandler) this.player.off([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler) | ||
84 | if (this.progress) this.progress.off([ 'mousemove', 'touchmove' ], this.boundedHijackMouseTooltip) | ||
85 | |||
86 | this.seekBar?.el()?.removeChild(this.spritePlaceholder) | ||
87 | |||
88 | super.dispose() | ||
76 | } | 89 | } |
77 | 90 | ||
78 | private load () { | 91 | private load () { |
diff --git a/client/src/assets/player/shared/control-bar/theater-button.ts b/client/src/assets/player/shared/control-bar/theater-button.ts index 56c349d6b..a5feb56ee 100644 --- a/client/src/assets/player/shared/control-bar/theater-button.ts +++ b/client/src/assets/player/shared/control-bar/theater-button.ts | |||
@@ -1,14 +1,19 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { getStoredTheater, saveTheaterInStore } from '../../peertube-player-local-storage' | 2 | import { getStoredTheater, saveTheaterInStore } from '../../peertube-player-local-storage' |
3 | import { TheaterButtonOptions } from '../../types' | ||
3 | 4 | ||
4 | const Button = videojs.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
5 | class TheaterButton extends Button { | 6 | class TheaterButton extends Button { |
6 | 7 | ||
7 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' | 8 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' |
8 | 9 | ||
9 | constructor (player: videojs.Player, options: videojs.ComponentOptions) { | 10 | private theaterButtonOptions: TheaterButtonOptions |
11 | |||
12 | constructor (player: videojs.Player, options: TheaterButtonOptions & videojs.ComponentOptions) { | ||
10 | super(player, options) | 13 | super(player, options) |
11 | 14 | ||
15 | this.theaterButtonOptions = options | ||
16 | |||
12 | const enabled = getStoredTheater() | 17 | const enabled = getStoredTheater() |
13 | if (enabled === true) { | 18 | if (enabled === true) { |
14 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) | 19 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) |
@@ -19,6 +24,9 @@ class TheaterButton extends Button { | |||
19 | this.controlText('Theater mode') | 24 | this.controlText('Theater mode') |
20 | 25 | ||
21 | this.player().theaterEnabled = enabled | 26 | this.player().theaterEnabled = enabled |
27 | |||
28 | this.updateShowing() | ||
29 | this.player().on('video-change', () => this.updateShowing()) | ||
22 | } | 30 | } |
23 | 31 | ||
24 | buildCSSClass () { | 32 | buildCSSClass () { |
@@ -36,7 +44,7 @@ class TheaterButton extends Button { | |||
36 | 44 | ||
37 | saveTheaterInStore(theaterEnabled) | 45 | saveTheaterInStore(theaterEnabled) |
38 | 46 | ||
39 | this.player_.trigger('theaterChange', theaterEnabled) | 47 | this.player_.trigger('theater-change', theaterEnabled) |
40 | } | 48 | } |
41 | 49 | ||
42 | handleClick () { | 50 | handleClick () { |
@@ -48,6 +56,11 @@ class TheaterButton extends Button { | |||
48 | private isTheaterEnabled () { | 56 | private isTheaterEnabled () { |
49 | return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) | 57 | return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) |
50 | } | 58 | } |
59 | |||
60 | private updateShowing () { | ||
61 | if (this.theaterButtonOptions.isDisplayed()) this.show() | ||
62 | else this.hide() | ||
63 | } | ||
51 | } | 64 | } |
52 | 65 | ||
53 | videojs.registerComponent('TheaterButton', TheaterButton) | 66 | videojs.registerComponent('TheaterButton', TheaterButton) |
diff --git a/client/src/assets/player/shared/dock/peertube-dock-component.ts b/client/src/assets/player/shared/dock/peertube-dock-component.ts index 183c7a00f..c13ca647b 100644 --- a/client/src/assets/player/shared/dock/peertube-dock-component.ts +++ b/client/src/assets/player/shared/dock/peertube-dock-component.ts | |||
@@ -10,17 +10,20 @@ export type PeerTubeDockComponentOptions = { | |||
10 | 10 | ||
11 | class PeerTubeDockComponent extends Component { | 11 | class PeerTubeDockComponent extends Component { |
12 | 12 | ||
13 | createEl () { | 13 | options_: videojs.ComponentOptions & PeerTubeDockComponentOptions |
14 | const options = this.options_ as PeerTubeDockComponentOptions | ||
15 | 14 | ||
16 | const el = super.createEl('div', { | 15 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor |
17 | className: 'peertube-dock' | 16 | constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeDockComponentOptions) { |
18 | }) | 17 | super(player, options) |
18 | } | ||
19 | |||
20 | createEl () { | ||
21 | const el = super.createEl('div', { className: 'peertube-dock' }) | ||
19 | 22 | ||
20 | if (options.avatarUrl) { | 23 | if (this.options_.avatarUrl) { |
21 | const avatar = videojs.dom.createEl('img', { | 24 | const avatar = videojs.dom.createEl('img', { |
22 | className: 'peertube-dock-avatar', | 25 | className: 'peertube-dock-avatar', |
23 | src: options.avatarUrl | 26 | src: this.options_.avatarUrl |
24 | }) | 27 | }) |
25 | 28 | ||
26 | el.appendChild(avatar) | 29 | el.appendChild(avatar) |
@@ -30,27 +33,27 @@ class PeerTubeDockComponent extends Component { | |||
30 | className: 'peertube-dock-title-description' | 33 | className: 'peertube-dock-title-description' |
31 | }) | 34 | }) |
32 | 35 | ||
33 | if (options.title) { | 36 | if (this.options_.title) { |
34 | const title = videojs.dom.createEl('div', { | 37 | const title = videojs.dom.createEl('div', { |
35 | className: 'peertube-dock-title', | 38 | className: 'peertube-dock-title', |
36 | title: options.title, | 39 | title: this.options_.title, |
37 | innerHTML: options.title | 40 | innerHTML: this.options_.title |
38 | }) | 41 | }) |
39 | 42 | ||
40 | elWrapperTitleDescription.appendChild(title) | 43 | elWrapperTitleDescription.appendChild(title) |
41 | } | 44 | } |
42 | 45 | ||
43 | if (options.description) { | 46 | if (this.options_.description) { |
44 | const description = videojs.dom.createEl('div', { | 47 | const description = videojs.dom.createEl('div', { |
45 | className: 'peertube-dock-description', | 48 | className: 'peertube-dock-description', |
46 | title: options.description, | 49 | title: this.options_.description, |
47 | innerHTML: options.description | 50 | innerHTML: this.options_.description |
48 | }) | 51 | }) |
49 | 52 | ||
50 | elWrapperTitleDescription.appendChild(description) | 53 | elWrapperTitleDescription.appendChild(description) |
51 | } | 54 | } |
52 | 55 | ||
53 | if (options.title || options.description) { | 56 | if (this.options_.title || this.options_.description) { |
54 | el.appendChild(elWrapperTitleDescription) | 57 | el.appendChild(elWrapperTitleDescription) |
55 | } | 58 | } |
56 | 59 | ||
diff --git a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts index 245981692..fc71a8c4b 100644 --- a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts +++ b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts | |||
@@ -10,14 +10,25 @@ export type PeerTubeDockPluginOptions = { | |||
10 | } | 10 | } |
11 | 11 | ||
12 | class PeerTubeDockPlugin extends Plugin { | 12 | class PeerTubeDockPlugin extends Plugin { |
13 | private dockComponent: PeerTubeDockComponent | ||
14 | |||
13 | constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeDockPluginOptions) { | 15 | constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeDockPluginOptions) { |
14 | super(player, options) | 16 | super(player, options) |
15 | 17 | ||
16 | this.player.addClass('peertube-dock') | 18 | player.ready(() => { |
17 | 19 | player.addClass('peertube-dock') | |
18 | this.player.ready(() => { | ||
19 | this.player.addChild('PeerTubeDockComponent', options) as PeerTubeDockComponent | ||
20 | }) | 20 | }) |
21 | |||
22 | this.dockComponent = new PeerTubeDockComponent(player, options) | ||
23 | player.addChild(this.dockComponent) | ||
24 | } | ||
25 | |||
26 | dispose () { | ||
27 | this.dockComponent?.dispose() | ||
28 | this.player.removeChild(this.dockComponent) | ||
29 | this.player.removeClass('peertube-dock') | ||
30 | |||
31 | super.dispose() | ||
21 | } | 32 | } |
22 | } | 33 | } |
23 | 34 | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index 2742b21a1..e77b7dc6d 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -31,6 +31,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
31 | 31 | ||
32 | dispose () { | 32 | dispose () { |
33 | document.removeEventListener('keydown', this.handleKeyFunction) | 33 | document.removeEventListener('keydown', this.handleKeyFunction) |
34 | |||
35 | super.dispose() | ||
34 | } | 36 | } |
35 | 37 | ||
36 | private onKeyDown (event: KeyboardEvent) { | 38 | private onKeyDown (event: KeyboardEvent) { |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts deleted file mode 100644 index 26f923e92..000000000 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ /dev/null | |||
@@ -1,155 +0,0 @@ | |||
1 | import { | ||
2 | CommonOptions, | ||
3 | NextPreviousVideoButtonOptions, | ||
4 | PeerTubeLinkButtonOptions, | ||
5 | PeertubePlayerManagerOptions, | ||
6 | PlayerMode | ||
7 | } from '../../types' | ||
8 | |||
9 | export class ControlBarOptionsBuilder { | ||
10 | private options: CommonOptions | ||
11 | |||
12 | constructor ( | ||
13 | globalOptions: PeertubePlayerManagerOptions, | ||
14 | private mode: PlayerMode | ||
15 | ) { | ||
16 | this.options = globalOptions.common | ||
17 | } | ||
18 | |||
19 | getChildrenOptions () { | ||
20 | const children = {} | ||
21 | |||
22 | if (this.options.previousVideo) { | ||
23 | Object.assign(children, this.getPreviousVideo()) | ||
24 | } | ||
25 | |||
26 | Object.assign(children, { playToggle: {} }) | ||
27 | |||
28 | if (this.options.nextVideo) { | ||
29 | Object.assign(children, this.getNextVideo()) | ||
30 | } | ||
31 | |||
32 | Object.assign(children, { | ||
33 | ...this.getTimeControls(), | ||
34 | |||
35 | flexibleWidthSpacer: {}, | ||
36 | |||
37 | ...this.getProgressControl(), | ||
38 | |||
39 | p2PInfoButton: { | ||
40 | p2pEnabled: this.options.p2pEnabled | ||
41 | }, | ||
42 | |||
43 | muteToggle: {}, | ||
44 | volumeControl: {}, | ||
45 | |||
46 | ...this.getSettingsButton() | ||
47 | }) | ||
48 | |||
49 | if (this.options.peertubeLink === true) { | ||
50 | Object.assign(children, { | ||
51 | peerTubeLinkButton: { | ||
52 | shortUUID: this.options.videoShortUUID, | ||
53 | instanceName: this.options.instanceName | ||
54 | } as PeerTubeLinkButtonOptions | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | if (this.options.theaterButton === true) { | ||
59 | Object.assign(children, { | ||
60 | theaterButton: {} | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | Object.assign(children, { | ||
65 | fullscreenToggle: {} | ||
66 | }) | ||
67 | |||
68 | return children | ||
69 | } | ||
70 | |||
71 | private getSettingsButton () { | ||
72 | const settingEntries: string[] = [] | ||
73 | |||
74 | if (!this.options.isLive) { | ||
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
77 | |||
78 | if (this.options.captions === true) settingEntries.push('captionsButton') | ||
79 | |||
80 | settingEntries.push('resolutionMenuButton') | ||
81 | |||
82 | return { | ||
83 | settingsButton: { | ||
84 | setup: { | ||
85 | maxHeightOffset: 40 | ||
86 | }, | ||
87 | entries: settingEntries | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private getProgressControl () { | ||
107 | if (this.options.isLive) return {} | ||
108 | |||
109 | const loadProgressBar = this.mode === 'webtorrent' | ||
110 | ? 'peerTubeLoadProgressBar' | ||
111 | : 'loadProgressBar' | ||
112 | |||
113 | return { | ||
114 | progressControl: { | ||
115 | children: { | ||
116 | seekBar: { | ||
117 | children: { | ||
118 | [loadProgressBar]: {}, | ||
119 | mouseTimeDisplay: {}, | ||
120 | playProgressBar: {} | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | |||
128 | private getPreviousVideo () { | ||
129 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
130 | type: 'previous', | ||
131 | handler: this.options.previousVideo, | ||
132 | isDisabled: () => { | ||
133 | if (!this.options.hasPreviousVideo) return false | ||
134 | |||
135 | return !this.options.hasPreviousVideo() | ||
136 | } | ||
137 | } | ||
138 | |||
139 | return { previousVideoButton: buttonOptions } | ||
140 | } | ||
141 | |||
142 | private getNextVideo () { | ||
143 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
144 | type: 'next', | ||
145 | handler: this.options.nextVideo, | ||
146 | isDisabled: () => { | ||
147 | if (!this.options.hasNextVideo) return false | ||
148 | |||
149 | return !this.options.hasNextVideo() | ||
150 | } | ||
151 | } | ||
152 | |||
153 | return { nextVideoButton: buttonOptions } | ||
154 | } | ||
155 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/index.ts b/client/src/assets/player/shared/manager-options/index.ts deleted file mode 100644 index 4934d8302..000000000 --- a/client/src/assets/player/shared/manager-options/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './manager-options-builder' | ||
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts deleted file mode 100644 index 5d3ee4c4a..000000000 --- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts +++ /dev/null | |||
@@ -1,186 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { copyToClipboard } from '@root-helpers/utils' | ||
3 | import { buildVideoOrPlaylistEmbed } from '@root-helpers/video' | ||
4 | import { isIOS, isSafari } from '@root-helpers/web-browser' | ||
5 | import { buildVideoLink, decorateVideoLink, pick } from '@shared/core-utils' | ||
6 | import { isDefaultLocale } from '@shared/core-utils/i18n' | ||
7 | import { VideoJSPluginOptions } from '../../types' | ||
8 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../types/manager-options' | ||
9 | import { ControlBarOptionsBuilder } from './control-bar-options-builder' | ||
10 | import { HLSOptionsBuilder } from './hls-options-builder' | ||
11 | import { WebTorrentOptionsBuilder } from './webtorrent-options-builder' | ||
12 | |||
13 | export class ManagerOptionsBuilder { | ||
14 | |||
15 | constructor ( | ||
16 | private mode: PlayerMode, | ||
17 | private options: PeertubePlayerManagerOptions, | ||
18 | private p2pMediaLoaderModule?: any | ||
19 | ) { | ||
20 | |||
21 | } | ||
22 | |||
23 | async getVideojsOptions (alreadyPlayed: boolean): Promise<videojs.PlayerOptions> { | ||
24 | const commonOptions = this.options.common | ||
25 | |||
26 | let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) | ||
27 | const html5 = { | ||
28 | preloadTextTracks: false | ||
29 | } | ||
30 | |||
31 | const plugins: VideoJSPluginOptions = { | ||
32 | peertube: { | ||
33 | mode: this.mode, | ||
34 | autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent | ||
35 | |||
36 | ...pick(commonOptions, [ | ||
37 | 'videoViewUrl', | ||
38 | 'videoViewIntervalMs', | ||
39 | 'authorizationHeader', | ||
40 | 'startTime', | ||
41 | 'videoDuration', | ||
42 | 'subtitle', | ||
43 | 'videoCaptions', | ||
44 | 'stopTime', | ||
45 | 'isLive', | ||
46 | 'videoUUID' | ||
47 | ]) | ||
48 | }, | ||
49 | metrics: { | ||
50 | mode: this.mode, | ||
51 | |||
52 | ...pick(commonOptions, [ | ||
53 | 'metricsUrl', | ||
54 | 'videoUUID' | ||
55 | ]) | ||
56 | } | ||
57 | } | ||
58 | |||
59 | if (commonOptions.playlist) { | ||
60 | plugins.playlist = commonOptions.playlist | ||
61 | } | ||
62 | |||
63 | if (this.mode === 'p2p-media-loader') { | ||
64 | const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule) | ||
65 | const options = await hlsOptionsBuilder.getPluginOptions() | ||
66 | |||
67 | Object.assign(plugins, pick(options, [ 'hlsjs', 'p2pMediaLoader' ])) | ||
68 | Object.assign(html5, options.html5) | ||
69 | } else if (this.mode === 'webtorrent') { | ||
70 | const webtorrentOptionsBuilder = new WebTorrentOptionsBuilder(this.options, this.getAutoPlayValue(autoplay, alreadyPlayed)) | ||
71 | |||
72 | Object.assign(plugins, webtorrentOptionsBuilder.getPluginOptions()) | ||
73 | |||
74 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
75 | autoplay = false | ||
76 | } | ||
77 | |||
78 | const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode) | ||
79 | |||
80 | const videojsOptions = { | ||
81 | html5, | ||
82 | |||
83 | // We don't use text track settings for now | ||
84 | textTrackSettings: false as any, // FIXME: typings | ||
85 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
86 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
87 | |||
88 | muted: commonOptions.muted !== undefined | ||
89 | ? commonOptions.muted | ||
90 | : undefined, // Undefined so the player knows it has to check the local storage | ||
91 | |||
92 | autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed), | ||
93 | |||
94 | poster: commonOptions.poster, | ||
95 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
96 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], | ||
97 | |||
98 | plugins, | ||
99 | |||
100 | controlBar: { | ||
101 | children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings | ||
102 | } | ||
103 | } | ||
104 | |||
105 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
106 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
107 | } | ||
108 | |||
109 | return videojsOptions | ||
110 | } | ||
111 | |||
112 | private getAutoPlayValue (autoplay: videojs.Autoplay, alreadyPlayed: boolean) { | ||
113 | if (autoplay !== true) return autoplay | ||
114 | |||
115 | // On first play, disable autoplay to avoid issues | ||
116 | // But if the player already played videos, we can safely autoplay next ones | ||
117 | if (isIOS() || isSafari()) { | ||
118 | return alreadyPlayed ? 'play' : false | ||
119 | } | ||
120 | |||
121 | return this.options.common.forceAutoplay | ||
122 | ? 'any' | ||
123 | : 'play' | ||
124 | } | ||
125 | |||
126 | getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) { | ||
127 | const content = () => { | ||
128 | const isLoopEnabled = player.options_['loop'] | ||
129 | |||
130 | const items = [ | ||
131 | { | ||
132 | icon: 'repeat', | ||
133 | label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''), | ||
134 | listener: function () { | ||
135 | player.options_['loop'] = !isLoopEnabled | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | label: player.localize('Copy the video URL'), | ||
140 | listener: function () { | ||
141 | copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID })) | ||
142 | } | ||
143 | }, | ||
144 | { | ||
145 | label: player.localize('Copy the video URL at the current time'), | ||
146 | listener: function (this: videojs.Player) { | ||
147 | const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID }) | ||
148 | |||
149 | copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() })) | ||
150 | } | ||
151 | }, | ||
152 | { | ||
153 | icon: 'code', | ||
154 | label: player.localize('Copy embed code'), | ||
155 | listener: () => { | ||
156 | copyToClipboard(buildVideoOrPlaylistEmbed({ embedUrl: commonOptions.embedUrl, embedTitle: commonOptions.embedTitle })) | ||
157 | } | ||
158 | } | ||
159 | ] | ||
160 | |||
161 | if (this.mode === 'webtorrent') { | ||
162 | items.push({ | ||
163 | label: player.localize('Copy magnet URI'), | ||
164 | listener: function (this: videojs.Player) { | ||
165 | copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri) | ||
166 | } | ||
167 | }) | ||
168 | } | ||
169 | |||
170 | items.push({ | ||
171 | icon: 'info', | ||
172 | label: player.localize('Stats for nerds'), | ||
173 | listener: () => { | ||
174 | player.stats().show() | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | return items.map(i => ({ | ||
179 | ...i, | ||
180 | label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label | ||
181 | })) | ||
182 | } | ||
183 | |||
184 | return { content } | ||
185 | } | ||
186 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts deleted file mode 100644 index 80eec02cf..000000000 --- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts +++ /dev/null | |||
@@ -1,47 +0,0 @@ | |||
1 | import { addQueryParams } from '../../../../../../shared/core-utils' | ||
2 | import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types' | ||
3 | |||
4 | export class WebTorrentOptionsBuilder { | ||
5 | |||
6 | constructor ( | ||
7 | private options: PeertubePlayerManagerOptions, | ||
8 | private autoPlayValue: any | ||
9 | ) { | ||
10 | |||
11 | } | ||
12 | |||
13 | getPluginOptions () { | ||
14 | const commonOptions = this.options.common | ||
15 | const webtorrentOptions = this.options.webtorrent | ||
16 | const p2pMediaLoaderOptions = this.options.p2pMediaLoader | ||
17 | |||
18 | const autoplay = this.autoPlayValue === 'play' | ||
19 | |||
20 | const webtorrent: WebtorrentPluginOptions = { | ||
21 | autoplay, | ||
22 | |||
23 | playerRefusedP2P: commonOptions.p2pEnabled === false, | ||
24 | videoDuration: commonOptions.videoDuration, | ||
25 | playerElement: commonOptions.playerElement, | ||
26 | |||
27 | videoFileToken: commonOptions.videoFileToken, | ||
28 | |||
29 | requiresUserAuth: commonOptions.requiresUserAuth, | ||
30 | |||
31 | buildWebSeedUrls: file => { | ||
32 | if (!commonOptions.requiresUserAuth && !commonOptions.requiresPassword) return [] | ||
33 | |||
34 | return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ] | ||
35 | }, | ||
36 | |||
37 | videoFiles: webtorrentOptions.videoFiles.length !== 0 | ||
38 | ? webtorrentOptions.videoFiles | ||
39 | // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode | ||
40 | : p2pMediaLoaderOptions?.videoFiles || [], | ||
41 | |||
42 | startTime: commonOptions.startTime | ||
43 | } | ||
44 | |||
45 | return { webtorrent } | ||
46 | } | ||
47 | } | ||
diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts index 2aae3e90a..48363a724 100644 --- a/client/src/assets/player/shared/metrics/metrics-plugin.ts +++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts | |||
@@ -1,14 +1,15 @@ | |||
1 | import debug from 'debug' | ||
1 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
2 | import { PlaybackMetricCreate } from '../../../../../../shared/models' | ||
3 | import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types' | ||
4 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { PlaybackMetricCreate } from '../../../../../../shared/models' | ||
5 | import { MetricsPluginOptions, PlayerNetworkInfo } from '../../types' | ||
6 | |||
7 | const debugLogger = debug('peertube:player:metrics') | ||
5 | 8 | ||
6 | const Plugin = videojs.getPlugin('plugin') | 9 | const Plugin = videojs.getPlugin('plugin') |
7 | 10 | ||
8 | class MetricsPlugin extends Plugin { | 11 | class MetricsPlugin extends Plugin { |
9 | private readonly metricsUrl: string | 12 | options_: MetricsPluginOptions |
10 | private readonly videoUUID: string | ||
11 | private readonly mode: PlayerMode | ||
12 | 13 | ||
13 | private downloadedBytesP2P = 0 | 14 | private downloadedBytesP2P = 0 |
14 | private downloadedBytesHTTP = 0 | 15 | private downloadedBytesHTTP = 0 |
@@ -28,29 +29,54 @@ class MetricsPlugin extends Plugin { | |||
28 | constructor (player: videojs.Player, options: MetricsPluginOptions) { | 29 | constructor (player: videojs.Player, options: MetricsPluginOptions) { |
29 | super(player) | 30 | super(player) |
30 | 31 | ||
31 | this.metricsUrl = options.metricsUrl | 32 | this.options_ = options |
32 | this.videoUUID = options.videoUUID | ||
33 | this.mode = options.mode | ||
34 | 33 | ||
35 | this.player.one('play', () => { | 34 | this.trackBytes() |
36 | this.runMetricsInterval() | 35 | this.trackResolutionChange() |
36 | this.trackErrors() | ||
37 | 37 | ||
38 | this.trackBytes() | 38 | this.one('play', () => { |
39 | this.trackResolutionChange() | 39 | this.player.on('video-change', () => { |
40 | this.trackErrors() | 40 | this.runMetricsIntervalOnPlay() |
41 | }) | ||
41 | }) | 42 | }) |
43 | |||
44 | this.runMetricsIntervalOnPlay() | ||
42 | } | 45 | } |
43 | 46 | ||
44 | dispose () { | 47 | dispose () { |
45 | if (this.metricsInterval) clearInterval(this.metricsInterval) | 48 | if (this.metricsInterval) clearInterval(this.metricsInterval) |
49 | |||
50 | super.dispose() | ||
51 | } | ||
52 | |||
53 | private runMetricsIntervalOnPlay () { | ||
54 | this.downloadedBytesP2P = 0 | ||
55 | this.downloadedBytesHTTP = 0 | ||
56 | this.uploadedBytesP2P = 0 | ||
57 | |||
58 | this.resolutionChanges = 0 | ||
59 | this.errors = 0 | ||
60 | |||
61 | this.lastPlayerNetworkInfo = undefined | ||
62 | |||
63 | debugLogger('Will track metrics on next play') | ||
64 | |||
65 | this.player.one('play', () => { | ||
66 | debugLogger('Tracking metrics') | ||
67 | |||
68 | this.runMetricsInterval() | ||
69 | }) | ||
46 | } | 70 | } |
47 | 71 | ||
48 | private runMetricsInterval () { | 72 | private runMetricsInterval () { |
73 | if (this.metricsInterval) clearInterval(this.metricsInterval) | ||
74 | |||
49 | this.metricsInterval = setInterval(() => { | 75 | this.metricsInterval = setInterval(() => { |
50 | let resolution: number | 76 | let resolution: number |
51 | let fps: number | 77 | let fps: number |
52 | 78 | ||
53 | if (this.mode === 'p2p-media-loader') { | 79 | if (this.player.usingPlugin('p2pMediaLoader')) { |
54 | const level = this.player.p2pMediaLoader().getCurrentLevel() | 80 | const level = this.player.p2pMediaLoader().getCurrentLevel() |
55 | if (!level) return | 81 | if (!level) return |
56 | 82 | ||
@@ -60,21 +86,23 @@ class MetricsPlugin extends Plugin { | |||
60 | fps = framerate | 86 | fps = framerate |
61 | ? parseInt(framerate, 10) | 87 | ? parseInt(framerate, 10) |
62 | : undefined | 88 | : undefined |
63 | } else { // webtorrent | 89 | } else if (this.player.usingPlugin('webVideo')) { |
64 | const videoFile = this.player.webtorrent().getCurrentVideoFile() | 90 | const videoFile = this.player.webVideo().getCurrentVideoFile() |
65 | if (!videoFile) return | 91 | if (!videoFile) return |
66 | 92 | ||
67 | resolution = videoFile.resolution.id | 93 | resolution = videoFile.resolution.id |
68 | fps = videoFile.fps && videoFile.fps !== -1 | 94 | fps = videoFile.fps && videoFile.fps !== -1 |
69 | ? videoFile.fps | 95 | ? videoFile.fps |
70 | : undefined | 96 | : undefined |
97 | } else { | ||
98 | return | ||
71 | } | 99 | } |
72 | 100 | ||
73 | const body: PlaybackMetricCreate = { | 101 | const body: PlaybackMetricCreate = { |
74 | resolution, | 102 | resolution, |
75 | fps, | 103 | fps, |
76 | 104 | ||
77 | playerMode: this.mode, | 105 | playerMode: this.options_.mode(), |
78 | 106 | ||
79 | resolutionChanges: this.resolutionChanges, | 107 | resolutionChanges: this.resolutionChanges, |
80 | 108 | ||
@@ -85,7 +113,7 @@ class MetricsPlugin extends Plugin { | |||
85 | 113 | ||
86 | uploadedBytesP2P: this.uploadedBytesP2P, | 114 | uploadedBytesP2P: this.uploadedBytesP2P, |
87 | 115 | ||
88 | videoId: this.videoUUID | 116 | videoId: this.options_.videoUUID() |
89 | } | 117 | } |
90 | 118 | ||
91 | this.resolutionChanges = 0 | 119 | this.resolutionChanges = 0 |
@@ -99,15 +127,13 @@ class MetricsPlugin extends Plugin { | |||
99 | 127 | ||
100 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) | 128 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) |
101 | 129 | ||
102 | return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers }) | 130 | return fetch(this.options_.metricsUrl(), { method: 'POST', body: JSON.stringify(body), headers }) |
103 | .catch(err => logger.error('Cannot send metrics to the server.', err)) | 131 | .catch(err => logger.error('Cannot send metrics to the server.', err)) |
104 | }, this.CONSTANTS.METRICS_INTERVAL) | 132 | }, this.CONSTANTS.METRICS_INTERVAL) |
105 | } | 133 | } |
106 | 134 | ||
107 | private trackBytes () { | 135 | private trackBytes () { |
108 | this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => { | 136 | this.player.on('p2p-info', (_event, data: PlayerNetworkInfo) => { |
109 | if (!data) return | ||
110 | |||
111 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) | 137 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) |
112 | this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) | 138 | this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) |
113 | 139 | ||
@@ -115,10 +141,18 @@ class MetricsPlugin extends Plugin { | |||
115 | 141 | ||
116 | this.lastPlayerNetworkInfo = data | 142 | this.lastPlayerNetworkInfo = data |
117 | }) | 143 | }) |
144 | |||
145 | this.player.on('http-info', (_event, data: PlayerNetworkInfo) => { | ||
146 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) | ||
147 | }) | ||
118 | } | 148 | } |
119 | 149 | ||
120 | private trackResolutionChange () { | 150 | private trackResolutionChange () { |
121 | this.player.on('engineResolutionChange', () => { | 151 | this.player.on('engine-resolution-change', () => { |
152 | this.resolutionChanges++ | ||
153 | }) | ||
154 | |||
155 | this.player.on('user-resolution-change', () => { | ||
122 | this.resolutionChanges++ | 156 | this.resolutionChanges++ |
123 | }) | 157 | }) |
124 | } | 158 | } |
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts index 09cb98f2e..1bc3ca38d 100644 --- a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts +++ b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts | |||
@@ -2,22 +2,20 @@ import videojs from 'video.js' | |||
2 | 2 | ||
3 | const Component = videojs.getComponent('Component') | 3 | const Component = videojs.getComponent('Component') |
4 | class PeerTubeMobileButtons extends Component { | 4 | class PeerTubeMobileButtons extends Component { |
5 | private mainButton: HTMLDivElement | ||
5 | 6 | ||
6 | private rewind: Element | 7 | private rewind: Element |
7 | private forward: Element | 8 | private forward: Element |
8 | private rewindText: Element | 9 | private rewindText: Element |
9 | private forwardText: Element | 10 | private forwardText: Element |
10 | 11 | ||
11 | createEl () { | 12 | private touchStartHandler: (e: TouchEvent) => void |
12 | const container = super.createEl('div', { | ||
13 | className: 'vjs-mobile-buttons-overlay' | ||
14 | }) as HTMLDivElement | ||
15 | 13 | ||
16 | const mainButton = super.createEl('div', { | 14 | createEl () { |
17 | className: 'main-button' | 15 | const container = super.createEl('div', { className: 'vjs-mobile-buttons-overlay' }) as HTMLDivElement |
18 | }) as HTMLDivElement | 16 | this.mainButton = super.createEl('div', { className: 'main-button' }) as HTMLDivElement |
19 | 17 | ||
20 | mainButton.addEventListener('touchstart', e => { | 18 | this.touchStartHandler = e => { |
21 | e.stopPropagation() | 19 | e.stopPropagation() |
22 | 20 | ||
23 | if (this.player_.paused() || this.player_.ended()) { | 21 | if (this.player_.paused() || this.player_.ended()) { |
@@ -26,7 +24,9 @@ class PeerTubeMobileButtons extends Component { | |||
26 | } | 24 | } |
27 | 25 | ||
28 | this.player_.pause() | 26 | this.player_.pause() |
29 | }) | 27 | } |
28 | |||
29 | this.mainButton.addEventListener('touchstart', this.touchStartHandler, { passive: true }) | ||
30 | 30 | ||
31 | this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) | 31 | this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) |
32 | this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) | 32 | this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) |
@@ -40,12 +40,18 @@ class PeerTubeMobileButtons extends Component { | |||
40 | this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) | 40 | this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) |
41 | 41 | ||
42 | container.appendChild(this.rewind) | 42 | container.appendChild(this.rewind) |
43 | container.appendChild(mainButton) | 43 | container.appendChild(this.mainButton) |
44 | container.appendChild(this.forward) | 44 | container.appendChild(this.forward) |
45 | 45 | ||
46 | return container | 46 | return container |
47 | } | 47 | } |
48 | 48 | ||
49 | dispose () { | ||
50 | if (this.touchStartHandler) this.mainButton.removeEventListener('touchstart', this.touchStartHandler) | ||
51 | |||
52 | super.dispose() | ||
53 | } | ||
54 | |||
49 | displayFastSeek (amount: number) { | 55 | displayFastSeek (amount: number) { |
50 | if (amount === 0) { | 56 | if (amount === 0) { |
51 | this.hideRewind() | 57 | this.hideRewind() |
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts index 646e9f8c6..f31fa7ddb 100644 --- a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts +++ b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts | |||
@@ -21,6 +21,15 @@ class PeerTubeMobilePlugin extends Plugin { | |||
21 | 21 | ||
22 | private setCurrentTimeTimeout: ReturnType<typeof setTimeout> | 22 | private setCurrentTimeTimeout: ReturnType<typeof setTimeout> |
23 | 23 | ||
24 | private onPlayHandler: () => void | ||
25 | private onFullScreenChangeHandler: () => void | ||
26 | private onTouchStartHandler: (event: TouchEvent) => void | ||
27 | private onMobileButtonTouchStartHandler: (event: TouchEvent) => void | ||
28 | private sliderActiveHandler: () => void | ||
29 | private sliderInactiveHandler: () => void | ||
30 | |||
31 | private seekBar: videojs.Component | ||
32 | |||
24 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 33 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { |
25 | super(player, options) | 34 | super(player, options) |
26 | 35 | ||
@@ -36,18 +45,38 @@ class PeerTubeMobilePlugin extends Plugin { | |||
36 | (this.player.options_.userActions as any).click = false | 45 | (this.player.options_.userActions as any).click = false |
37 | this.player.options_.userActions.doubleClick = false | 46 | this.player.options_.userActions.doubleClick = false |
38 | 47 | ||
39 | this.player.one('play', () => { | 48 | this.onPlayHandler = () => this.initTouchStartEvents() |
40 | this.initTouchStartEvents() | 49 | this.player.one('play', this.onPlayHandler) |
41 | }) | 50 | |
51 | this.seekBar = this.player.getDescendant([ 'controlBar', 'progressControl', 'seekBar' ]) | ||
52 | |||
53 | this.sliderActiveHandler = () => this.player.addClass('vjs-mobile-sliding') | ||
54 | this.sliderInactiveHandler = () => this.player.removeClass('vjs-mobile-sliding') | ||
55 | |||
56 | this.seekBar.on('slideractive', this.sliderActiveHandler) | ||
57 | this.seekBar.on('sliderinactive', this.sliderInactiveHandler) | ||
58 | } | ||
59 | |||
60 | dispose () { | ||
61 | if (this.onPlayHandler) this.player.off('play', this.onPlayHandler) | ||
62 | if (this.onFullScreenChangeHandler) this.player.off('fullscreenchange', this.onFullScreenChangeHandler) | ||
63 | if (this.onTouchStartHandler) this.player.off('touchstart', this.onFullScreenChangeHandler) | ||
64 | if (this.onMobileButtonTouchStartHandler) { | ||
65 | this.peerTubeMobileButtons?.el().removeEventListener('touchstart', this.onMobileButtonTouchStartHandler) | ||
66 | } | ||
67 | |||
68 | super.dispose() | ||
42 | } | 69 | } |
43 | 70 | ||
44 | private handleFullscreenRotation () { | 71 | private handleFullscreenRotation () { |
45 | this.player.on('fullscreenchange', () => { | 72 | this.onFullScreenChangeHandler = () => { |
46 | if (!this.player.isFullscreen() || this.isPortraitVideo()) return | 73 | if (!this.player.isFullscreen() || this.isPortraitVideo()) return |
47 | 74 | ||
48 | screen.orientation.lock('landscape') | 75 | screen.orientation.lock('landscape') |
49 | .catch(err => logger.error('Cannot lock screen to landscape.', err)) | 76 | .catch(err => logger.error('Cannot lock screen to landscape.', err)) |
50 | }) | 77 | } |
78 | |||
79 | this.player.on('fullscreenchange', this.onFullScreenChangeHandler) | ||
51 | } | 80 | } |
52 | 81 | ||
53 | private isPortraitVideo () { | 82 | private isPortraitVideo () { |
@@ -80,19 +109,22 @@ class PeerTubeMobilePlugin extends Plugin { | |||
80 | this.lastTapEvent = event | 109 | this.lastTapEvent = event |
81 | } | 110 | } |
82 | 111 | ||
83 | this.player.on('touchstart', (event: TouchEvent) => { | 112 | this.onTouchStartHandler = event => { |
84 | // Only enable user active on player touch, we listen event on peertube mobile buttons to disable it | 113 | // Only enable user active on player touch, we listen event on peertube mobile buttons to disable it |
85 | if (this.player.userActive()) return | 114 | if (this.player.userActive()) return |
86 | 115 | ||
87 | handleTouchStart(event) | 116 | handleTouchStart(event) |
88 | }) | 117 | } |
118 | this.player.on('touchstart', this.onTouchStartHandler) | ||
89 | 119 | ||
90 | this.peerTubeMobileButtons.el().addEventListener('touchstart', (event: TouchEvent) => { | 120 | this.onMobileButtonTouchStartHandler = event => { |
91 | // Prevent mousemove/click events firing on the player, that conflict with our user active logic | 121 | // Prevent mousemove/click events firing on the player, that conflict with our user active logic |
92 | event.preventDefault() | 122 | event.preventDefault() |
93 | 123 | ||
94 | handleTouchStart(event) | 124 | handleTouchStart(event) |
95 | }, { passive: false }) | 125 | } |
126 | |||
127 | this.peerTubeMobileButtons.el().addEventListener('touchstart', this.onMobileButtonTouchStartHandler, { passive: false }) | ||
96 | } | 128 | } |
97 | 129 | ||
98 | private onDoubleTap (event: TouchEvent) { | 130 | private onDoubleTap (event: TouchEvent) { |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index d05d6193c..d83ec625a 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -14,6 +14,10 @@ type Metadata = { | |||
14 | levels: Level[] | 14 | levels: Level[] |
15 | } | 15 | } |
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | ||
18 | // Source handler registration | ||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
17 | type HookFn = (player: videojs.Player, hljs: Hlsjs) => void | 21 | type HookFn = (player: videojs.Player, hljs: Hlsjs) => void |
18 | 22 | ||
19 | const registerSourceHandler = function (vjs: typeof videojs) { | 23 | const registerSourceHandler = function (vjs: typeof videojs) { |
@@ -25,10 +29,13 @@ const registerSourceHandler = function (vjs: typeof videojs) { | |||
25 | const html5 = vjs.getTech('Html5') | 29 | const html5 = vjs.getTech('Html5') |
26 | 30 | ||
27 | if (!html5) { | 31 | if (!html5) { |
28 | logger.error('No Hml5 tech found in videojs') | 32 | logger.error('No "Html5" tech found in videojs') |
29 | return | 33 | return |
30 | } | 34 | } |
31 | 35 | ||
36 | // Already registered | ||
37 | if ((html5 as any).canPlaySource({ type: 'application/x-mpegURL' })) return | ||
38 | |||
32 | // FIXME: typings | 39 | // FIXME: typings |
33 | (html5 as any).registerSourceHandler({ | 40 | (html5 as any).registerSourceHandler({ |
34 | canHandleSource: function (source: videojs.Tech.SourceObject) { | 41 | canHandleSource: function (source: videojs.Tech.SourceObject) { |
@@ -56,32 +63,55 @@ const registerSourceHandler = function (vjs: typeof videojs) { | |||
56 | (vjs as any).Html5Hlsjs = Html5Hlsjs | 63 | (vjs as any).Html5Hlsjs = Html5Hlsjs |
57 | } | 64 | } |
58 | 65 | ||
59 | function hlsjsConfigHandler (this: videojs.Player, options: HlsjsConfigHandlerOptions) { | 66 | // --------------------------------------------------------------------------- |
60 | const player = this | 67 | // HLS options plugin |
68 | // --------------------------------------------------------------------------- | ||
61 | 69 | ||
62 | if (!options) return | 70 | const Plugin = videojs.getPlugin('plugin') |
63 | 71 | ||
64 | if (!player.srOptions_) { | 72 | class HLSJSConfigHandler extends Plugin { |
65 | player.srOptions_ = {} | 73 | |
66 | } | 74 | constructor (player: videojs.Player, options: HlsjsConfigHandlerOptions) { |
75 | super(player, options) | ||
76 | |||
77 | if (!options) return | ||
78 | |||
79 | if (!player.srOptions_) { | ||
80 | player.srOptions_ = {} | ||
81 | } | ||
82 | |||
83 | if (!player.srOptions_.hlsjsConfig) { | ||
84 | player.srOptions_.hlsjsConfig = options.hlsjsConfig | ||
85 | } | ||
67 | 86 | ||
68 | if (!player.srOptions_.hlsjsConfig) { | 87 | if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) { |
69 | player.srOptions_.hlsjsConfig = options.hlsjsConfig | 88 | player.srOptions_.levelLabelHandler = options.levelLabelHandler |
89 | } | ||
90 | |||
91 | registerSourceHandler(videojs) | ||
70 | } | 92 | } |
71 | 93 | ||
72 | if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) { | 94 | dispose () { |
73 | player.srOptions_.levelLabelHandler = options.levelLabelHandler | 95 | this.player.srOptions_ = undefined |
96 | |||
97 | const tech = this.player.tech(true) as any | ||
98 | if (tech.hlsProvider) { | ||
99 | tech.hlsProvider.dispose() | ||
100 | tech.hlsProvider = undefined | ||
101 | } | ||
102 | |||
103 | super.dispose() | ||
74 | } | 104 | } |
75 | } | 105 | } |
76 | 106 | ||
77 | const registerConfigPlugin = function (vjs: typeof videojs) { | 107 | videojs.registerPlugin('hlsjs', HLSJSConfigHandler) |
78 | // Used in Brightcove since we don't pass options directly there | 108 | |
79 | const registerVjsPlugin = vjs.registerPlugin || vjs.plugin | 109 | // --------------------------------------------------------------------------- |
80 | registerVjsPlugin('hlsjs', hlsjsConfigHandler) | 110 | // HLS JS source handler |
81 | } | 111 | // --------------------------------------------------------------------------- |
82 | 112 | ||
83 | class Html5Hlsjs { | 113 | export class Html5Hlsjs { |
84 | private static readonly hooks: { [id: string]: HookFn[] } = {} | 114 | private static hooks: { [id: string]: HookFn[] } = {} |
85 | 115 | ||
86 | private readonly videoElement: HTMLVideoElement | 116 | private readonly videoElement: HTMLVideoElement |
87 | private readonly errorCounts: ErrorCounts = {} | 117 | private readonly errorCounts: ErrorCounts = {} |
@@ -101,8 +131,9 @@ class Html5Hlsjs { | |||
101 | private dvrDuration: number = null | 131 | private dvrDuration: number = null |
102 | private edgeMargin: number = null | 132 | private edgeMargin: number = null |
103 | 133 | ||
104 | private handlers: { [ id in 'play' ]: EventListener } = { | 134 | private handlers: { [ id in 'play' | 'error' ]: EventListener } = { |
105 | play: null | 135 | play: null, |
136 | error: null | ||
106 | } | 137 | } |
107 | 138 | ||
108 | constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { | 139 | constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { |
@@ -115,7 +146,7 @@ class Html5Hlsjs { | |||
115 | this.videoElement = tech.el() as HTMLVideoElement | 146 | this.videoElement = tech.el() as HTMLVideoElement |
116 | this.player = vjs((tech.options_ as any).playerId) | 147 | this.player = vjs((tech.options_ as any).playerId) |
117 | 148 | ||
118 | this.videoElement.addEventListener('error', event => { | 149 | this.handlers.error = event => { |
119 | let errorTxt: string | 150 | let errorTxt: string |
120 | const mediaError = ((event.currentTarget || event.target) as HTMLVideoElement).error | 151 | const mediaError = ((event.currentTarget || event.target) as HTMLVideoElement).error |
121 | 152 | ||
@@ -143,7 +174,8 @@ class Html5Hlsjs { | |||
143 | } | 174 | } |
144 | 175 | ||
145 | logger.error(`MEDIA_ERROR: ${errorTxt}`) | 176 | logger.error(`MEDIA_ERROR: ${errorTxt}`) |
146 | }) | 177 | } |
178 | this.videoElement.addEventListener('error', this.handlers.error) | ||
147 | 179 | ||
148 | this.initialize() | 180 | this.initialize() |
149 | } | 181 | } |
@@ -174,6 +206,7 @@ class Html5Hlsjs { | |||
174 | // See comment for `initialize` method. | 206 | // See comment for `initialize` method. |
175 | dispose () { | 207 | dispose () { |
176 | this.videoElement.removeEventListener('play', this.handlers.play) | 208 | this.videoElement.removeEventListener('play', this.handlers.play) |
209 | this.videoElement.removeEventListener('error', this.handlers.error) | ||
177 | 210 | ||
178 | // FIXME: https://github.com/video-dev/hls.js/issues/4092 | 211 | // FIXME: https://github.com/video-dev/hls.js/issues/4092 |
179 | const untypedHLS = this.hls as any | 212 | const untypedHLS = this.hls as any |
@@ -200,6 +233,10 @@ class Html5Hlsjs { | |||
200 | return true | 233 | return true |
201 | } | 234 | } |
202 | 235 | ||
236 | static removeAllHooks () { | ||
237 | Html5Hlsjs.hooks = {} | ||
238 | } | ||
239 | |||
203 | private _executeHooksFor (type: string) { | 240 | private _executeHooksFor (type: string) { |
204 | if (Html5Hlsjs.hooks[type] === undefined) { | 241 | if (Html5Hlsjs.hooks[type] === undefined) { |
205 | return | 242 | return |
@@ -421,7 +458,7 @@ class Html5Hlsjs { | |||
421 | ? data.level | 458 | ? data.level |
422 | : -1 | 459 | : -1 |
423 | 460 | ||
424 | this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, byEngine: true }) | 461 | this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, fireCallback: false }) |
425 | }) | 462 | }) |
426 | 463 | ||
427 | this.hls.attachMedia(this.videoElement) | 464 | this.hls.attachMedia(this.videoElement) |
@@ -433,9 +470,3 @@ class Html5Hlsjs { | |||
433 | this._initHlsjs() | 470 | this._initHlsjs() |
434 | } | 471 | } |
435 | } | 472 | } |
436 | |||
437 | export { | ||
438 | Html5Hlsjs, | ||
439 | registerSourceHandler, | ||
440 | registerConfigPlugin | ||
441 | } | ||
diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts index e6f525fea..fe967a730 100644 --- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -3,19 +3,12 @@ import videojs from 'video.js' | |||
3 | import { Events, Segment } from '@peertube/p2p-media-loader-core' | 3 | import { Events, Segment } from '@peertube/p2p-media-loader-core' |
4 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' | 4 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' |
5 | import { logger } from '@root-helpers/logger' | 5 | import { logger } from '@root-helpers/logger' |
6 | import { addQueryParams, timeToInt } from '@shared/core-utils' | 6 | import { addQueryParams } from '@shared/core-utils' |
7 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' | 7 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' |
8 | import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' | 8 | import { SettingsButton } from '../settings/settings-menu-button' |
9 | |||
10 | registerConfigPlugin(videojs) | ||
11 | registerSourceHandler(videojs) | ||
12 | 9 | ||
13 | const Plugin = videojs.getPlugin('plugin') | 10 | const Plugin = videojs.getPlugin('plugin') |
14 | class P2pMediaLoaderPlugin extends Plugin { | 11 | class P2pMediaLoaderPlugin extends Plugin { |
15 | |||
16 | private readonly CONSTANTS = { | ||
17 | INFO_SCHEDULER: 1000 // Don't change this | ||
18 | } | ||
19 | private readonly options: P2PMediaLoaderPluginOptions | 12 | private readonly options: P2PMediaLoaderPluginOptions |
20 | 13 | ||
21 | private hlsjs: Hlsjs | 14 | private hlsjs: Hlsjs |
@@ -31,7 +24,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
31 | pendingDownload: [] as number[], | 24 | pendingDownload: [] as number[], |
32 | totalDownload: 0 | 25 | totalDownload: 0 |
33 | } | 26 | } |
34 | private startTime: number | ||
35 | 27 | ||
36 | private networkInfoInterval: any | 28 | private networkInfoInterval: any |
37 | 29 | ||
@@ -39,7 +31,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
39 | super(player) | 31 | super(player) |
40 | 32 | ||
41 | this.options = options | 33 | this.options = options |
42 | this.startTime = timeToInt(options.startTime) | ||
43 | 34 | ||
44 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 | 35 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
45 | if (!(videojs as any).Html5Hlsjs) { | 36 | if (!(videojs as any).Html5Hlsjs) { |
@@ -77,17 +68,22 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
77 | }) | 68 | }) |
78 | 69 | ||
79 | player.ready(() => { | 70 | player.ready(() => { |
80 | this.initializeCore() | ||
81 | |||
82 | this.initializePlugin() | 71 | this.initializePlugin() |
83 | }) | 72 | }) |
84 | } | 73 | } |
85 | 74 | ||
86 | dispose () { | 75 | dispose () { |
87 | if (this.hlsjs) this.hlsjs.destroy() | 76 | this.p2pEngine?.removeAllListeners() |
88 | if (this.p2pEngine) this.p2pEngine.destroy() | 77 | this.p2pEngine?.destroy() |
78 | |||
79 | this.hlsjs?.destroy() | ||
80 | this.options.segmentValidator?.destroy(); | ||
81 | |||
82 | (videojs as any).Html5Hlsjs?.removeAllHooks() | ||
89 | 83 | ||
90 | clearInterval(this.networkInfoInterval) | 84 | clearInterval(this.networkInfoInterval) |
85 | |||
86 | super.dispose() | ||
91 | } | 87 | } |
92 | 88 | ||
93 | getCurrentLevel () { | 89 | getCurrentLevel () { |
@@ -104,18 +100,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
104 | return this.hlsjs | 100 | return this.hlsjs |
105 | } | 101 | } |
106 | 102 | ||
107 | private initializeCore () { | ||
108 | this.player.one('play', () => { | ||
109 | this.player.addClass('vjs-has-big-play-button-clicked') | ||
110 | }) | ||
111 | |||
112 | this.player.one('canplay', () => { | ||
113 | if (this.startTime) { | ||
114 | this.player.currentTime(this.startTime) | ||
115 | } | ||
116 | }) | ||
117 | } | ||
118 | |||
119 | private initializePlugin () { | 103 | private initializePlugin () { |
120 | initHlsJsPlayer(this.hlsjs) | 104 | initHlsJsPlayer(this.hlsjs) |
121 | 105 | ||
@@ -133,7 +117,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
133 | 117 | ||
134 | this.runStats() | 118 | this.runStats() |
135 | 119 | ||
136 | this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange')) | 120 | this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engine-resolution-change')) |
137 | } | 121 | } |
138 | 122 | ||
139 | private runStats () { | 123 | private runStats () { |
@@ -167,7 +151,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
167 | this.statsP2PBytes.pendingUpload = [] | 151 | this.statsP2PBytes.pendingUpload = [] |
168 | this.statsHTTPBytes.pendingDownload = [] | 152 | this.statsHTTPBytes.pendingDownload = [] |
169 | 153 | ||
170 | return this.player.trigger('p2pInfo', { | 154 | return this.player.trigger('p2p-info', { |
171 | source: 'p2p-media-loader', | 155 | source: 'p2p-media-loader', |
172 | http: { | 156 | http: { |
173 | downloadSpeed: httpDownloadSpeed, | 157 | downloadSpeed: httpDownloadSpeed, |
@@ -182,7 +166,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
182 | }, | 166 | }, |
183 | bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8 | 167 | bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8 |
184 | } as PlayerNetworkInfo) | 168 | } as PlayerNetworkInfo) |
185 | }, this.CONSTANTS.INFO_SCHEDULER) | 169 | }, 1000) |
186 | } | 170 | } |
187 | 171 | ||
188 | private arraySum (data: number[]) { | 172 | private arraySum (data: number[]) { |
@@ -190,10 +174,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
190 | } | 174 | } |
191 | 175 | ||
192 | private fallbackToBuiltInIOS () { | 176 | private fallbackToBuiltInIOS () { |
193 | logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.'); | 177 | logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.') |
194 | |||
195 | // Workaround to force video.js to not re create a video element | ||
196 | (this.player as any).playerElIngest_ = this.player.el().parentNode | ||
197 | 178 | ||
198 | this.player.src({ | 179 | this.player.src({ |
199 | type: this.options.type, | 180 | type: this.options.type, |
@@ -203,9 +184,14 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
203 | }) | 184 | }) |
204 | }) | 185 | }) |
205 | 186 | ||
206 | this.player.ready(() => { | 187 | // Resolution button is not supported in built-in HLS player |
207 | this.initializeCore() | 188 | this.getResolutionButton().hide() |
208 | }) | 189 | } |
190 | |||
191 | private getResolutionButton () { | ||
192 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
193 | |||
194 | return settingsButton.menu.getChild('resolutionMenuButton') | ||
209 | } | 195 | } |
210 | } | 196 | } |
211 | 197 | ||
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts index e86d3d159..a2f7e676d 100644 --- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts +++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts | |||
@@ -9,30 +9,29 @@ type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string | |||
9 | 9 | ||
10 | const maxRetries = 10 | 10 | const maxRetries = 10 |
11 | 11 | ||
12 | function segmentValidatorFactory (options: { | 12 | export class SegmentValidator { |
13 | serverUrl: string | 13 | |
14 | segmentsSha256Url: string | 14 | private readonly bytesRangeRegex = /bytes=(\d+)-(\d+)/ |
15 | authorizationHeader: () => string | 15 | |
16 | requiresUserAuth: boolean | 16 | private destroyed = false |
17 | requiresPassword: boolean | 17 | |
18 | videoPassword: () => string | 18 | constructor (private readonly options: { |
19 | }) { | 19 | serverUrl: string |
20 | const { serverUrl, segmentsSha256Url, authorizationHeader, requiresUserAuth, requiresPassword, videoPassword } = options | 20 | segmentsSha256Url: string |
21 | 21 | authorizationHeader: () => string | |
22 | let segmentsJSON = fetchSha256Segments({ | 22 | requiresUserAuth: boolean |
23 | serverUrl, | 23 | requiresPassword: boolean |
24 | segmentsSha256Url, | 24 | videoPassword: () => string |
25 | authorizationHeader, | 25 | }) { |
26 | requiresUserAuth, | 26 | |
27 | requiresPassword, | 27 | } |
28 | videoPassword | 28 | |
29 | }) | 29 | async validate (segment: Segment, _method: string, _peerId: string, retry = 1) { |
30 | const regex = /bytes=(\d+)-(\d+)/ | 30 | if (this.destroyed) return |
31 | 31 | ||
32 | return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { | ||
33 | const filename = basename(removeQueryParams(segment.url)) | 32 | const filename = basename(removeQueryParams(segment.url)) |
34 | 33 | ||
35 | const segmentValue = (await segmentsJSON)[filename] | 34 | const segmentValue = (await this.fetchSha256Segments())[filename] |
36 | 35 | ||
37 | if (!segmentValue && retry > maxRetries) { | 36 | if (!segmentValue && retry > maxRetries) { |
38 | throw new Error(`Unknown segment name ${filename} in segment validator`) | 37 | throw new Error(`Unknown segment name ${filename} in segment validator`) |
@@ -43,15 +42,7 @@ function segmentValidatorFactory (options: { | |||
43 | 42 | ||
44 | await wait(500) | 43 | await wait(500) |
45 | 44 | ||
46 | segmentsJSON = fetchSha256Segments({ | 45 | await this.validate(segment, _method, _peerId, retry + 1) |
47 | serverUrl, | ||
48 | segmentsSha256Url, | ||
49 | authorizationHeader, | ||
50 | requiresUserAuth, | ||
51 | requiresPassword, | ||
52 | videoPassword | ||
53 | }) | ||
54 | await segmentValidator(segment, _method, _peerId, retry + 1) | ||
55 | 46 | ||
56 | return | 47 | return |
57 | } | 48 | } |
@@ -62,7 +53,7 @@ function segmentValidatorFactory (options: { | |||
62 | if (typeof segmentValue === 'string') { | 53 | if (typeof segmentValue === 'string') { |
63 | hashShouldBe = segmentValue | 54 | hashShouldBe = segmentValue |
64 | } else { | 55 | } else { |
65 | const captured = regex.exec(segment.range) | 56 | const captured = this.bytesRangeRegex.exec(segment.range) |
66 | range = captured[1] + '-' + captured[2] | 57 | range = captured[1] + '-' + captured[2] |
67 | 58 | ||
68 | hashShouldBe = segmentValue[range] | 59 | hashShouldBe = segmentValue[range] |
@@ -72,7 +63,7 @@ function segmentValidatorFactory (options: { | |||
72 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) | 63 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) |
73 | } | 64 | } |
74 | 65 | ||
75 | const calculatedSha = await sha256Hex(segment.data) | 66 | const calculatedSha = await this.sha256Hex(segment.data) |
76 | if (calculatedSha !== hashShouldBe) { | 67 | if (calculatedSha !== hashShouldBe) { |
77 | throw new Error( | 68 | throw new Error( |
78 | `Hashes does not correspond for segment ${filename}/${range}` + | 69 | `Hashes does not correspond for segment ${filename}/${range}` + |
@@ -80,65 +71,53 @@ function segmentValidatorFactory (options: { | |||
80 | ) | 71 | ) |
81 | } | 72 | } |
82 | } | 73 | } |
83 | } | ||
84 | 74 | ||
85 | // --------------------------------------------------------------------------- | 75 | destroy () { |
76 | this.destroyed = true | ||
77 | } | ||
86 | 78 | ||
87 | export { | 79 | private fetchSha256Segments (): Promise<SegmentsJSON> { |
88 | segmentValidatorFactory | 80 | let headers: { [ id: string ]: string } = {} |
89 | } | ||
90 | 81 | ||
91 | // --------------------------------------------------------------------------- | 82 | if (isSameOrigin(this.options.serverUrl, this.options.segmentsSha256Url)) { |
92 | 83 | if (this.options.requiresPassword) headers = { 'x-peertube-video-password': this.options.videoPassword() } | |
93 | function fetchSha256Segments (options: { | 84 | else if (this.options.requiresUserAuth) headers = { Authorization: this.options.authorizationHeader() } |
94 | serverUrl: string | 85 | } |
95 | segmentsSha256Url: string | 86 | |
96 | authorizationHeader: () => string | 87 | return fetch(this.options.segmentsSha256Url, { headers }) |
97 | requiresUserAuth: boolean | 88 | .then(res => res.json() as Promise<SegmentsJSON>) |
98 | requiresPassword: boolean | 89 | .catch(err => { |
99 | videoPassword: () => string | 90 | logger.error('Cannot get sha256 segments', err) |
100 | }): Promise<SegmentsJSON> { | 91 | return {} |
101 | const { serverUrl, segmentsSha256Url, requiresUserAuth, authorizationHeader, requiresPassword, videoPassword } = options | 92 | }) |
102 | |||
103 | let headers: { [ id: string ]: string } = {} | ||
104 | if (isSameOrigin(serverUrl, segmentsSha256Url)) { | ||
105 | if (requiresPassword) headers = { 'x-peertube-video-password': videoPassword() } | ||
106 | else if (requiresUserAuth) headers = { Authorization: authorizationHeader() } | ||
107 | } | 93 | } |
108 | 94 | ||
109 | return fetch(segmentsSha256Url, { headers }) | 95 | private async sha256Hex (data?: ArrayBuffer) { |
110 | .then(res => res.json() as Promise<SegmentsJSON>) | 96 | if (!data) return undefined |
111 | .catch(err => { | ||
112 | logger.error('Cannot get sha256 segments', err) | ||
113 | return {} | ||
114 | }) | ||
115 | } | ||
116 | 97 | ||
117 | async function sha256Hex (data?: ArrayBuffer) { | 98 | if (window.crypto.subtle) { |
118 | if (!data) return undefined | 99 | return window.crypto.subtle.digest('SHA-256', data) |
100 | .then(data => this.bufferToHex(data)) | ||
101 | } | ||
119 | 102 | ||
120 | if (window.crypto.subtle) { | 103 | // Fallback for non HTTPS context |
121 | return window.crypto.subtle.digest('SHA-256', data) | 104 | const shaModule = (await import('sha.js') as any).default |
122 | .then(data => bufferToHex(data)) | 105 | // eslint-disable-next-line new-cap |
106 | return new shaModule.sha256().update(Buffer.from(data)).digest('hex') | ||
123 | } | 107 | } |
124 | 108 | ||
125 | // Fallback for non HTTPS context | 109 | // Thanks: https://stackoverflow.com/a/53307879 |
126 | const shaModule = (await import('sha.js') as any).default | 110 | private bufferToHex (buffer?: ArrayBuffer) { |
127 | // eslint-disable-next-line new-cap | 111 | if (!buffer) return '' |
128 | return new shaModule.sha256().update(Buffer.from(data)).digest('hex') | ||
129 | } | ||
130 | |||
131 | // Thanks: https://stackoverflow.com/a/53307879 | ||
132 | function bufferToHex (buffer?: ArrayBuffer) { | ||
133 | if (!buffer) return '' | ||
134 | 112 | ||
135 | let s = '' | 113 | let s = '' |
136 | const h = '0123456789abcdef' | 114 | const h = '0123456789abcdef' |
137 | const o = new Uint8Array(buffer) | 115 | const o = new Uint8Array(buffer) |
138 | 116 | ||
139 | o.forEach((v: any) => { | 117 | o.forEach((v: any) => { |
140 | s += h[v >> 4] + h[v & 15] | 118 | s += h[v >> 4] + h[v & 15] |
141 | }) | 119 | }) |
142 | 120 | ||
143 | return s | 121 | return s |
122 | } | ||
144 | } | 123 | } |
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index af2147749..f52ec75f4 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import debug from 'debug' | 1 | import debug from 'debug' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { isMobile } from '@root-helpers/web-browser' | 4 | import { isIOS, isMobile } from '@root-helpers/web-browser' |
5 | import { timeToInt } from '@shared/core-utils' | 5 | import { timeToInt } from '@shared/core-utils' |
6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' | 6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' |
7 | import { | 7 | import { |
@@ -13,7 +13,7 @@ import { | |||
13 | saveVideoWatchHistory, | 13 | saveVideoWatchHistory, |
14 | saveVolumeInStore | 14 | saveVolumeInStore |
15 | } from '../../peertube-player-local-storage' | 15 | } from '../../peertube-player-local-storage' |
16 | import { PeerTubePluginOptions, VideoJSCaption } from '../../types' | 16 | import { PeerTubePluginOptions } from '../../types' |
17 | import { SettingsButton } from '../settings/settings-menu-button' | 17 | import { SettingsButton } from '../settings/settings-menu-button' |
18 | 18 | ||
19 | const debugLogger = debug('peertube:player:peertube') | 19 | const debugLogger = debug('peertube:player:peertube') |
@@ -21,43 +21,59 @@ const debugLogger = debug('peertube:player:peertube') | |||
21 | const Plugin = videojs.getPlugin('plugin') | 21 | const Plugin = videojs.getPlugin('plugin') |
22 | 22 | ||
23 | class PeerTubePlugin extends Plugin { | 23 | class PeerTubePlugin extends Plugin { |
24 | private readonly videoViewUrl: string | 24 | private readonly videoViewUrl: () => string |
25 | private readonly authorizationHeader: () => string | 25 | private readonly authorizationHeader: () => string |
26 | private readonly initialInactivityTimeout: number | ||
26 | 27 | ||
27 | private readonly videoUUID: string | 28 | private readonly hasAutoplay: () => videojs.Autoplay |
28 | private readonly startTime: number | ||
29 | |||
30 | private readonly videoViewIntervalMs: number | ||
31 | 29 | ||
32 | private videoCaptions: VideoJSCaption[] | 30 | private currentSubtitle: string |
33 | private defaultSubtitle: string | 31 | private currentPlaybackRate: number |
34 | 32 | ||
35 | private videoViewInterval: any | 33 | private videoViewInterval: any |
36 | 34 | ||
37 | private menuOpened = false | 35 | private menuOpened = false |
38 | private mouseInControlBar = false | 36 | private mouseInControlBar = false |
39 | private mouseInSettings = false | 37 | private mouseInSettings = false |
40 | private readonly initialInactivityTimeout: number | ||
41 | 38 | ||
42 | constructor (player: videojs.Player, options?: PeerTubePluginOptions) { | 39 | private videoViewOnPlayHandler: (...args: any[]) => void |
40 | private videoViewOnSeekedHandler: (...args: any[]) => void | ||
41 | private videoViewOnEndedHandler: (...args: any[]) => void | ||
42 | |||
43 | private stopTimeHandler: (...args: any[]) => void | ||
44 | |||
45 | constructor (player: videojs.Player, private readonly options: PeerTubePluginOptions) { | ||
43 | super(player) | 46 | super(player) |
44 | 47 | ||
45 | this.videoViewUrl = options.videoViewUrl | 48 | this.videoViewUrl = options.videoViewUrl |
46 | this.authorizationHeader = options.authorizationHeader | 49 | this.authorizationHeader = options.authorizationHeader |
47 | this.videoUUID = options.videoUUID | 50 | this.hasAutoplay = options.hasAutoplay |
48 | this.startTime = timeToInt(options.startTime) | ||
49 | this.videoViewIntervalMs = options.videoViewIntervalMs | ||
50 | 51 | ||
51 | this.videoCaptions = options.videoCaptions | ||
52 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout | 52 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout |
53 | 53 | ||
54 | if (options.autoplay !== false) this.player.addClass('vjs-has-autoplay') | 54 | this.currentSubtitle = this.options.subtitle() || getStoredLastSubtitle() |
55 | |||
56 | this.initializePlayer() | ||
57 | this.initOnVideoChange() | ||
58 | |||
59 | this.deleteLegacyIndexedDB() | ||
55 | 60 | ||
56 | this.player.on('autoplay-failure', () => { | 61 | this.player.on('autoplay-failure', () => { |
62 | debugLogger('Autoplay failed') | ||
63 | |||
57 | this.player.removeClass('vjs-has-autoplay') | 64 | this.player.removeClass('vjs-has-autoplay') |
65 | |||
66 | // Fix a bug on iOS where the big play button is not displayed when autoplay fails | ||
67 | if (isIOS()) this.player.hasStarted(false) | ||
58 | }) | 68 | }) |
59 | 69 | ||
60 | this.player.ready(() => { | 70 | this.player.on('ratechange', () => { |
71 | this.currentPlaybackRate = this.player.playbackRate() | ||
72 | |||
73 | this.player.defaultPlaybackRate(this.currentPlaybackRate) | ||
74 | }) | ||
75 | |||
76 | this.player.one('canplay', () => { | ||
61 | const playerOptions = this.player.options_ | 77 | const playerOptions = this.player.options_ |
62 | 78 | ||
63 | const volume = getStoredVolume() | 79 | const volume = getStoredVolume() |
@@ -65,28 +81,15 @@ class PeerTubePlugin extends Plugin { | |||
65 | 81 | ||
66 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | 82 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() |
67 | if (muted !== undefined) this.player.muted(muted) | 83 | if (muted !== undefined) this.player.muted(muted) |
84 | }) | ||
68 | 85 | ||
69 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | 86 | this.player.ready(() => { |
70 | 87 | ||
71 | this.player.on('volumechange', () => { | 88 | this.player.on('volumechange', () => { |
72 | saveVolumeInStore(this.player.volume()) | 89 | saveVolumeInStore(this.player.volume()) |
73 | saveMuteInStore(this.player.muted()) | 90 | saveMuteInStore(this.player.muted()) |
74 | }) | 91 | }) |
75 | 92 | ||
76 | if (options.stopTime) { | ||
77 | const stopTime = timeToInt(options.stopTime) | ||
78 | const self = this | ||
79 | |||
80 | this.player.on('timeupdate', function onTimeUpdate () { | ||
81 | if (self.player.currentTime() > stopTime) { | ||
82 | self.player.pause() | ||
83 | self.player.trigger('stopped') | ||
84 | |||
85 | self.player.off('timeupdate', onTimeUpdate) | ||
86 | } | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | this.player.textTracks().addEventListener('change', () => { | 93 | this.player.textTracks().addEventListener('change', () => { |
91 | const showing = this.player.textTracks().tracks_.find(t => { | 94 | const showing = this.player.textTracks().tracks_.find(t => { |
92 | return t.kind === 'captions' && t.mode === 'showing' | 95 | return t.kind === 'captions' && t.mode === 'showing' |
@@ -94,23 +97,24 @@ class PeerTubePlugin extends Plugin { | |||
94 | 97 | ||
95 | if (!showing) { | 98 | if (!showing) { |
96 | saveLastSubtitle('off') | 99 | saveLastSubtitle('off') |
100 | this.currentSubtitle = undefined | ||
97 | return | 101 | return |
98 | } | 102 | } |
99 | 103 | ||
104 | this.currentSubtitle = showing.language | ||
100 | saveLastSubtitle(showing.language) | 105 | saveLastSubtitle(showing.language) |
101 | }) | 106 | }) |
102 | 107 | ||
103 | this.player.on('sourcechange', () => this.initCaptions()) | 108 | this.player.on('video-change', () => { |
104 | 109 | this.initOnVideoChange() | |
105 | this.player.duration(options.videoDuration) | 110 | }) |
106 | |||
107 | this.initializePlayer() | ||
108 | this.runUserViewing() | ||
109 | }) | 111 | }) |
110 | } | 112 | } |
111 | 113 | ||
112 | dispose () { | 114 | dispose () { |
113 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) | 115 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
116 | |||
117 | super.dispose() | ||
114 | } | 118 | } |
115 | 119 | ||
116 | onMenuOpened () { | 120 | onMenuOpened () { |
@@ -162,40 +166,70 @@ class PeerTubePlugin extends Plugin { | |||
162 | 166 | ||
163 | this.initSmoothProgressBar() | 167 | this.initSmoothProgressBar() |
164 | 168 | ||
165 | this.initCaptions() | 169 | this.player.ready(() => { |
166 | 170 | this.listenControlBarMouse() | |
167 | this.listenControlBarMouse() | 171 | }) |
168 | 172 | ||
169 | this.listenFullScreenChange() | 173 | this.listenFullScreenChange() |
170 | } | 174 | } |
171 | 175 | ||
176 | private initOnVideoChange () { | ||
177 | if (this.hasAutoplay() !== false) this.player.addClass('vjs-has-autoplay') | ||
178 | else this.player.removeClass('vjs-has-autoplay') | ||
179 | |||
180 | if (this.currentPlaybackRate && this.currentPlaybackRate !== 1) { | ||
181 | debugLogger('Setting playback rate to ' + this.currentPlaybackRate) | ||
182 | |||
183 | this.player.playbackRate(this.currentPlaybackRate) | ||
184 | } | ||
185 | |||
186 | this.player.ready(() => { | ||
187 | this.initCaptions() | ||
188 | this.updateControlBar() | ||
189 | }) | ||
190 | |||
191 | this.handleStartStopTime() | ||
192 | this.runUserViewing() | ||
193 | } | ||
194 | |||
172 | // --------------------------------------------------------------------------- | 195 | // --------------------------------------------------------------------------- |
173 | 196 | ||
174 | private runUserViewing () { | 197 | private runUserViewing () { |
175 | let lastCurrentTime = this.startTime | 198 | const startTime = timeToInt(this.options.startTime()) |
199 | |||
200 | let lastCurrentTime = startTime | ||
176 | let lastViewEvent: VideoViewEvent | 201 | let lastViewEvent: VideoViewEvent |
177 | 202 | ||
178 | this.player.one('play', () => { | 203 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
179 | this.notifyUserIsWatching(this.startTime, lastViewEvent) | 204 | if (this.videoViewOnPlayHandler) this.player.off('play', this.videoViewOnPlayHandler) |
180 | }) | 205 | if (this.videoViewOnSeekedHandler) this.player.off('seeked', this.videoViewOnSeekedHandler) |
206 | if (this.videoViewOnEndedHandler) this.player.off('ended', this.videoViewOnEndedHandler) | ||
181 | 207 | ||
182 | this.player.on('seeked', () => { | 208 | this.videoViewOnPlayHandler = () => { |
209 | this.notifyUserIsWatching(startTime, lastViewEvent) | ||
210 | } | ||
211 | |||
212 | this.videoViewOnSeekedHandler = () => { | ||
183 | const diff = Math.floor(this.player.currentTime()) - lastCurrentTime | 213 | const diff = Math.floor(this.player.currentTime()) - lastCurrentTime |
184 | 214 | ||
185 | // Don't take into account small forwards | 215 | // Don't take into account small forwards |
186 | if (diff > 0 && diff < 3) return | 216 | if (diff > 0 && diff < 3) return |
187 | 217 | ||
188 | lastViewEvent = 'seek' | 218 | lastViewEvent = 'seek' |
189 | }) | 219 | } |
190 | 220 | ||
191 | this.player.one('ended', () => { | 221 | this.videoViewOnEndedHandler = () => { |
192 | const currentTime = Math.floor(this.player.duration()) | 222 | const currentTime = Math.floor(this.player.duration()) |
193 | lastCurrentTime = currentTime | 223 | lastCurrentTime = currentTime |
194 | 224 | ||
195 | this.notifyUserIsWatching(currentTime, lastViewEvent) | 225 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
196 | 226 | ||
197 | lastViewEvent = undefined | 227 | lastViewEvent = undefined |
198 | }) | 228 | } |
229 | |||
230 | this.player.one('play', this.videoViewOnPlayHandler) | ||
231 | this.player.on('seeked', this.videoViewOnSeekedHandler) | ||
232 | this.player.one('ended', this.videoViewOnEndedHandler) | ||
199 | 233 | ||
200 | this.videoViewInterval = setInterval(() => { | 234 | this.videoViewInterval = setInterval(() => { |
201 | const currentTime = Math.floor(this.player.currentTime()) | 235 | const currentTime = Math.floor(this.player.currentTime()) |
@@ -209,13 +243,13 @@ class PeerTubePlugin extends Plugin { | |||
209 | .catch(err => logger.error('Cannot notify user is watching.', err)) | 243 | .catch(err => logger.error('Cannot notify user is watching.', err)) |
210 | 244 | ||
211 | lastViewEvent = undefined | 245 | lastViewEvent = undefined |
212 | }, this.videoViewIntervalMs) | 246 | }, this.options.videoViewIntervalMs) |
213 | } | 247 | } |
214 | 248 | ||
215 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { | 249 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { |
216 | // Server won't save history, so save the video position in local storage | 250 | // Server won't save history, so save the video position in local storage |
217 | if (!this.authorizationHeader()) { | 251 | if (!this.authorizationHeader()) { |
218 | saveVideoWatchHistory(this.videoUUID, currentTime) | 252 | saveVideoWatchHistory(this.options.videoUUID(), currentTime) |
219 | } | 253 | } |
220 | 254 | ||
221 | if (!this.videoViewUrl) return Promise.resolve(true) | 255 | if (!this.videoViewUrl) return Promise.resolve(true) |
@@ -225,7 +259,7 @@ class PeerTubePlugin extends Plugin { | |||
225 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) | 259 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) |
226 | if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader()) | 260 | if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader()) |
227 | 261 | ||
228 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) | 262 | return fetch(this.videoViewUrl(), { method: 'POST', body: JSON.stringify(body), headers }) |
229 | } | 263 | } |
230 | 264 | ||
231 | // --------------------------------------------------------------------------- | 265 | // --------------------------------------------------------------------------- |
@@ -279,18 +313,89 @@ class PeerTubePlugin extends Plugin { | |||
279 | } | 313 | } |
280 | 314 | ||
281 | private initCaptions () { | 315 | private initCaptions () { |
282 | for (const caption of this.videoCaptions) { | 316 | debugLogger('Init captions with current subtitle ' + this.currentSubtitle) |
317 | |||
318 | this.player.tech(true).clearTracks('text') | ||
319 | |||
320 | for (const caption of this.options.videoCaptions()) { | ||
283 | this.player.addRemoteTextTrack({ | 321 | this.player.addRemoteTextTrack({ |
284 | kind: 'captions', | 322 | kind: 'captions', |
285 | label: caption.label, | 323 | label: caption.label, |
286 | language: caption.language, | 324 | language: caption.language, |
287 | id: caption.language, | 325 | id: caption.language, |
288 | src: caption.src, | 326 | src: caption.src, |
289 | default: this.defaultSubtitle === caption.language | 327 | default: this.currentSubtitle === caption.language |
290 | }, false) | 328 | }, true) |
329 | } | ||
330 | |||
331 | this.player.trigger('captions-changed') | ||
332 | } | ||
333 | |||
334 | private updateControlBar () { | ||
335 | debugLogger('Updating control bar') | ||
336 | |||
337 | if (this.options.isLive()) { | ||
338 | this.getPlaybackRateButton().hide() | ||
339 | |||
340 | this.player.controlBar.getChild('progressControl').hide() | ||
341 | this.player.controlBar.getChild('currentTimeDisplay').hide() | ||
342 | this.player.controlBar.getChild('timeDivider').hide() | ||
343 | this.player.controlBar.getChild('durationDisplay').hide() | ||
344 | |||
345 | this.player.controlBar.getChild('peerTubeLiveDisplay').show() | ||
346 | } else { | ||
347 | this.getPlaybackRateButton().show() | ||
348 | |||
349 | this.player.controlBar.getChild('progressControl').show() | ||
350 | this.player.controlBar.getChild('currentTimeDisplay').show() | ||
351 | this.player.controlBar.getChild('timeDivider').show() | ||
352 | this.player.controlBar.getChild('durationDisplay').show() | ||
353 | |||
354 | this.player.controlBar.getChild('peerTubeLiveDisplay').hide() | ||
291 | } | 355 | } |
292 | 356 | ||
293 | this.player.trigger('captionsChanged') | 357 | if (this.options.videoCaptions().length === 0) { |
358 | this.getCaptionsButton().hide() | ||
359 | } else { | ||
360 | this.getCaptionsButton().show() | ||
361 | } | ||
362 | } | ||
363 | |||
364 | private handleStartStopTime () { | ||
365 | this.player.duration(this.options.videoDuration()) | ||
366 | |||
367 | if (this.stopTimeHandler) { | ||
368 | this.player.off('timeupdate', this.stopTimeHandler) | ||
369 | this.stopTimeHandler = undefined | ||
370 | } | ||
371 | |||
372 | // Prefer canplaythrough instead of canplay because Chrome has issues with the second one | ||
373 | this.player.one('canplaythrough', () => { | ||
374 | if (this.options.startTime()) { | ||
375 | debugLogger('Start the video at ' + this.options.startTime()) | ||
376 | |||
377 | this.player.currentTime(timeToInt(this.options.startTime())) | ||
378 | } | ||
379 | |||
380 | if (this.options.stopTime()) { | ||
381 | const stopTime = timeToInt(this.options.stopTime()) | ||
382 | |||
383 | this.stopTimeHandler = () => { | ||
384 | if (this.player.currentTime() <= stopTime) return | ||
385 | |||
386 | debugLogger('Stopping the video at ' + this.options.stopTime()) | ||
387 | |||
388 | // Time top stop | ||
389 | this.player.pause() | ||
390 | this.player.trigger('auto-stopped') | ||
391 | |||
392 | this.player.off('timeupdate', this.stopTimeHandler) | ||
393 | this.stopTimeHandler = undefined | ||
394 | } | ||
395 | |||
396 | this.player.on('timeupdate', this.stopTimeHandler) | ||
397 | } | ||
398 | }) | ||
294 | } | 399 | } |
295 | 400 | ||
296 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 401 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
@@ -314,6 +419,37 @@ class PeerTubePlugin extends Plugin { | |||
314 | this.update() | 419 | this.update() |
315 | } | 420 | } |
316 | } | 421 | } |
422 | |||
423 | private getCaptionsButton () { | ||
424 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
425 | |||
426 | return settingsButton.menu.getChild('captionsButton') as videojs.CaptionsButton | ||
427 | } | ||
428 | |||
429 | private getPlaybackRateButton () { | ||
430 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
431 | |||
432 | return settingsButton.menu.getChild('playbackRateMenuButton') | ||
433 | } | ||
434 | |||
435 | // We don't use webtorrent anymore, so we can safely remove old chunks from IndexedDB | ||
436 | private deleteLegacyIndexedDB () { | ||
437 | try { | ||
438 | if (typeof window.indexedDB === 'undefined') return | ||
439 | if (!window.indexedDB) return | ||
440 | if (typeof window.indexedDB.databases !== 'function') return | ||
441 | |||
442 | window.indexedDB.databases() | ||
443 | .then(databases => { | ||
444 | for (const db of databases) { | ||
445 | window.indexedDB.deleteDatabase(db.name) | ||
446 | } | ||
447 | }) | ||
448 | } catch (err) { | ||
449 | debugLogger('Cannot delete legacy indexed DB', err) | ||
450 | // Nothing to do | ||
451 | } | ||
452 | } | ||
317 | } | 453 | } |
318 | 454 | ||
319 | videojs.registerPlugin('peertube', PeerTubePlugin) | 455 | videojs.registerPlugin('peertube', PeerTubePlugin) |
diff --git a/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts new file mode 100644 index 000000000..b467e3637 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts | |||
@@ -0,0 +1,136 @@ | |||
1 | import { | ||
2 | NextPreviousVideoButtonOptions, | ||
3 | PeerTubeLinkButtonOptions, | ||
4 | PeerTubePlayerContructorOptions, | ||
5 | PeerTubePlayerLoadOptions, | ||
6 | TheaterButtonOptions | ||
7 | } from '../../types' | ||
8 | |||
9 | type ControlBarOptionsBuilderConstructorOptions = | ||
10 | Pick<PeerTubePlayerContructorOptions, 'peertubeLink' | 'instanceName' | 'theaterButton'> & | ||
11 | { | ||
12 | videoShortUUID: () => string | ||
13 | p2pEnabled: () => boolean | ||
14 | |||
15 | previousVideo: () => PeerTubePlayerLoadOptions['previousVideo'] | ||
16 | nextVideo: () => PeerTubePlayerLoadOptions['nextVideo'] | ||
17 | } | ||
18 | |||
19 | export class ControlBarOptionsBuilder { | ||
20 | |||
21 | constructor (private options: ControlBarOptionsBuilderConstructorOptions) { | ||
22 | } | ||
23 | |||
24 | getChildrenOptions () { | ||
25 | const children = { | ||
26 | ...this.getPreviousVideo(), | ||
27 | |||
28 | playToggle: {}, | ||
29 | |||
30 | ...this.getNextVideo(), | ||
31 | |||
32 | ...this.getTimeControls(), | ||
33 | |||
34 | ...this.getProgressControl(), | ||
35 | |||
36 | p2PInfoButton: {}, | ||
37 | muteToggle: {}, | ||
38 | volumeControl: {}, | ||
39 | |||
40 | ...this.getSettingsButton(), | ||
41 | |||
42 | ...this.getPeerTubeLinkButton(), | ||
43 | |||
44 | ...this.getTheaterButton(), | ||
45 | |||
46 | fullscreenToggle: {} | ||
47 | } | ||
48 | |||
49 | return children | ||
50 | } | ||
51 | |||
52 | private getSettingsButton () { | ||
53 | const settingEntries: string[] = [] | ||
54 | |||
55 | settingEntries.push('playbackRateMenuButton') | ||
56 | settingEntries.push('captionsButton') | ||
57 | settingEntries.push('resolutionMenuButton') | ||
58 | |||
59 | return { | ||
60 | settingsButton: { | ||
61 | setup: { | ||
62 | maxHeightOffset: 40 | ||
63 | }, | ||
64 | entries: settingEntries | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | private getTimeControls () { | ||
70 | return { | ||
71 | peerTubeLiveDisplay: {}, | ||
72 | |||
73 | currentTimeDisplay: {}, | ||
74 | timeDivider: {}, | ||
75 | durationDisplay: {} | ||
76 | } | ||
77 | } | ||
78 | |||
79 | private getProgressControl () { | ||
80 | return { | ||
81 | progressControl: { | ||
82 | children: { | ||
83 | seekBar: { | ||
84 | children: { | ||
85 | loadProgressBar: {}, | ||
86 | mouseTimeDisplay: {}, | ||
87 | playProgressBar: {} | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | |||
95 | private getPreviousVideo () { | ||
96 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
97 | type: 'previous', | ||
98 | handler: () => this.options.previousVideo().handler(), | ||
99 | isDisabled: () => !this.options.previousVideo().enabled, | ||
100 | isDisplayed: () => this.options.previousVideo().displayControlBarButton | ||
101 | } | ||
102 | |||
103 | return { previousVideoButton: buttonOptions } | ||
104 | } | ||
105 | |||
106 | private getNextVideo () { | ||
107 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
108 | type: 'next', | ||
109 | handler: () => this.options.nextVideo().handler(), | ||
110 | isDisabled: () => !this.options.nextVideo().enabled, | ||
111 | isDisplayed: () => this.options.nextVideo().displayControlBarButton | ||
112 | } | ||
113 | |||
114 | return { nextVideoButton: buttonOptions } | ||
115 | } | ||
116 | |||
117 | private getPeerTubeLinkButton () { | ||
118 | const options: PeerTubeLinkButtonOptions = { | ||
119 | isDisplayed: this.options.peertubeLink, | ||
120 | shortUUID: this.options.videoShortUUID, | ||
121 | instanceName: this.options.instanceName | ||
122 | } | ||
123 | |||
124 | return { peerTubeLinkButton: options } | ||
125 | } | ||
126 | |||
127 | private getTheaterButton () { | ||
128 | const options: TheaterButtonOptions = { | ||
129 | isDisplayed: () => this.options.theaterButton | ||
130 | } | ||
131 | |||
132 | return { | ||
133 | theaterButton: options | ||
134 | } | ||
135 | } | ||
136 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts index 8091110bc..10df2db5d 100644 --- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts +++ b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts | |||
@@ -3,49 +3,61 @@ import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | |||
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { LiveVideoLatencyMode } from '@shared/models' | 4 | import { LiveVideoLatencyMode } from '@shared/models' |
5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' | 5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' |
6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' | 6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions } from '../../types' |
7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' | ||
8 | import { getRtcConfig, isSameOrigin } from '../common' | 7 | import { getRtcConfig, isSameOrigin } from '../common' |
9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' | 8 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' |
10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' | 9 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' |
11 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' | 10 | import { SegmentValidator } from '../p2p-media-loader/segment-validator' |
11 | |||
12 | type ConstructorOptions = | ||
13 | Pick<PeerTubePlayerContructorOptions, 'pluginsManager' | 'serverUrl' | 'authorizationHeader'> & | ||
14 | Pick<PeerTubePlayerLoadOptions, 'videoPassword' | 'requiresUserAuth' | 'videoFileToken' | 'requiresPassword' | | ||
15 | 'isLive' | 'liveOptions' | 'p2pEnabled' | 'hls'> | ||
12 | 16 | ||
13 | export class HLSOptionsBuilder { | 17 | export class HLSOptionsBuilder { |
14 | 18 | ||
15 | constructor ( | 19 | constructor ( |
16 | private options: PeertubePlayerManagerOptions, | 20 | private options: ConstructorOptions, |
17 | private p2pMediaLoaderModule?: any | 21 | private p2pMediaLoaderModule?: any |
18 | ) { | 22 | ) { |
19 | 23 | ||
20 | } | 24 | } |
21 | 25 | ||
22 | async getPluginOptions () { | 26 | async getPluginOptions () { |
23 | const commonOptions = this.options.common | 27 | const redundancyUrlManager = new RedundancyUrlManager(this.options.hls.redundancyBaseUrls) |
24 | 28 | const segmentValidator = new SegmentValidator({ | |
25 | const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls) | 29 | segmentsSha256Url: this.options.hls.segmentsSha256Url, |
30 | authorizationHeader: this.options.authorizationHeader, | ||
31 | requiresUserAuth: this.options.requiresUserAuth, | ||
32 | serverUrl: this.options.serverUrl, | ||
33 | requiresPassword: this.options.requiresPassword, | ||
34 | videoPassword: this.options.videoPassword | ||
35 | }) | ||
26 | 36 | ||
27 | const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook( | 37 | const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook( |
28 | 'filter:internal.player.p2p-media-loader.options.result', | 38 | 'filter:internal.player.p2p-media-loader.options.result', |
29 | this.getP2PMediaLoaderOptions(redundancyUrlManager) | 39 | this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator }) |
30 | ) | 40 | ) |
31 | const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader | 41 | const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader |
32 | 42 | ||
33 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | 43 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { |
34 | requiresUserAuth: commonOptions.requiresUserAuth, | 44 | requiresUserAuth: this.options.requiresUserAuth, |
35 | videoFileToken: commonOptions.videoFileToken, | 45 | videoFileToken: this.options.videoFileToken, |
36 | 46 | ||
37 | redundancyUrlManager, | 47 | redundancyUrlManager, |
38 | type: 'application/x-mpegURL', | 48 | type: 'application/x-mpegURL', |
39 | startTime: commonOptions.startTime, | 49 | src: this.options.hls.playlistUrl, |
40 | src: this.options.p2pMediaLoader.playlistUrl, | 50 | segmentValidator, |
41 | loader | 51 | loader |
42 | } | 52 | } |
43 | 53 | ||
44 | const hlsjs = { | 54 | const hlsjs = { |
55 | hlsjsConfig: this.getHLSJSOptions(loader), | ||
56 | |||
45 | levelLabelHandler: (level: { height: number, width: number }) => { | 57 | levelLabelHandler: (level: { height: number, width: number }) => { |
46 | const resolution = Math.min(level.height || 0, level.width || 0) | 58 | const resolution = Math.min(level.height || 0, level.width || 0) |
47 | 59 | ||
48 | const file = this.options.p2pMediaLoader.videoFiles.find(f => f.resolution.id === resolution) | 60 | const file = this.options.hls.videoFiles.find(f => f.resolution.id === resolution) |
49 | // We don't have files for live videos | 61 | // We don't have files for live videos |
50 | if (!file) return level.height | 62 | if (!file) return level.height |
51 | 63 | ||
@@ -56,26 +68,27 @@ export class HLSOptionsBuilder { | |||
56 | } | 68 | } |
57 | } | 69 | } |
58 | 70 | ||
59 | const html5 = { | 71 | return { p2pMediaLoader, hlsjs } |
60 | hlsjsConfig: this.getHLSJSOptions(loader) | ||
61 | } | ||
62 | |||
63 | return { p2pMediaLoader, hlsjs, html5 } | ||
64 | } | 72 | } |
65 | 73 | ||
66 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
67 | 75 | ||
68 | private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings { | 76 | private getP2PMediaLoaderOptions (options: { |
77 | redundancyUrlManager: RedundancyUrlManager | ||
78 | segmentValidator: SegmentValidator | ||
79 | }): HlsJsEngineSettings { | ||
80 | const { redundancyUrlManager, segmentValidator } = options | ||
81 | |||
69 | let consumeOnly = false | 82 | let consumeOnly = false |
70 | if ((navigator as any)?.connection?.type === 'cellular') { | 83 | if ((navigator as any)?.connection?.type === 'cellular') { |
71 | logger.info('We are on a cellular connection: disabling seeding.') | 84 | logger.info('We are on a cellular connection: disabling seeding.') |
72 | consumeOnly = true | 85 | consumeOnly = true |
73 | } | 86 | } |
74 | 87 | ||
75 | const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce | 88 | const trackerAnnounce = this.options.hls.trackerAnnounce |
76 | .filter(t => t.startsWith('ws')) | 89 | .filter(t => t.startsWith('ws')) |
77 | 90 | ||
78 | const specificLiveOrVODOptions = this.options.common.isLive | 91 | const specificLiveOrVODOptions = this.options.isLive |
79 | ? this.getP2PMediaLoaderLiveOptions() | 92 | ? this.getP2PMediaLoaderLiveOptions() |
80 | : this.getP2PMediaLoaderVODOptions() | 93 | : this.getP2PMediaLoaderVODOptions() |
81 | 94 | ||
@@ -88,35 +101,28 @@ export class HLSOptionsBuilder { | |||
88 | httpFailedSegmentTimeout: 1000, | 101 | httpFailedSegmentTimeout: 1000, |
89 | 102 | ||
90 | xhrSetup: (xhr, url) => { | 103 | xhrSetup: (xhr, url) => { |
91 | const { requiresUserAuth, requiresPassword } = this.options.common | 104 | const { requiresUserAuth, requiresPassword } = this.options |
92 | 105 | ||
93 | if (!(requiresUserAuth || requiresPassword)) return | 106 | if (!(requiresUserAuth || requiresPassword)) return |
94 | 107 | ||
95 | if (!isSameOrigin(this.options.common.serverUrl, url)) return | 108 | if (!isSameOrigin(this.options.serverUrl, url)) return |
96 | 109 | ||
97 | if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.common.videoPassword()) | 110 | if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword()) |
98 | 111 | ||
99 | else xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader()) | 112 | else xhr.setRequestHeader('Authorization', this.options.authorizationHeader()) |
100 | }, | 113 | }, |
101 | 114 | ||
102 | segmentValidator: segmentValidatorFactory({ | 115 | segmentValidator: segmentValidator.validate.bind(segmentValidator), |
103 | segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url, | ||
104 | authorizationHeader: this.options.common.authorizationHeader, | ||
105 | requiresUserAuth: this.options.common.requiresUserAuth, | ||
106 | serverUrl: this.options.common.serverUrl, | ||
107 | requiresPassword: this.options.common.requiresPassword, | ||
108 | videoPassword: this.options.common.videoPassword | ||
109 | }), | ||
110 | 116 | ||
111 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), | 117 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), |
112 | 118 | ||
113 | useP2P: this.options.common.p2pEnabled, | 119 | useP2P: this.options.p2pEnabled, |
114 | consumeOnly, | 120 | consumeOnly, |
115 | 121 | ||
116 | ...specificLiveOrVODOptions | 122 | ...specificLiveOrVODOptions |
117 | }, | 123 | }, |
118 | segments: { | 124 | segments: { |
119 | swarmId: this.options.p2pMediaLoader.playlistUrl, | 125 | swarmId: this.options.hls.playlistUrl, |
120 | forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20 | 126 | forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20 |
121 | } | 127 | } |
122 | } | 128 | } |
@@ -127,7 +133,7 @@ export class HLSOptionsBuilder { | |||
127 | requiredSegmentsPriority: 1 | 133 | requiredSegmentsPriority: 1 |
128 | } | 134 | } |
129 | 135 | ||
130 | const latencyMode = this.options.common.liveOptions.latencyMode | 136 | const latencyMode = this.options.liveOptions.latencyMode |
131 | 137 | ||
132 | switch (latencyMode) { | 138 | switch (latencyMode) { |
133 | case LiveVideoLatencyMode.SMALL_LATENCY: | 139 | case LiveVideoLatencyMode.SMALL_LATENCY: |
@@ -165,7 +171,7 @@ export class HLSOptionsBuilder { | |||
165 | // --------------------------------------------------------------------------- | 171 | // --------------------------------------------------------------------------- |
166 | 172 | ||
167 | private getHLSJSOptions (loader: P2PMediaLoader) { | 173 | private getHLSJSOptions (loader: P2PMediaLoader) { |
168 | const specificLiveOrVODOptions = this.options.common.isLive | 174 | const specificLiveOrVODOptions = this.options.isLive |
169 | ? this.getHLSLiveOptions() | 175 | ? this.getHLSLiveOptions() |
170 | : this.getHLSVODOptions() | 176 | : this.getHLSVODOptions() |
171 | 177 | ||
@@ -193,7 +199,7 @@ export class HLSOptionsBuilder { | |||
193 | } | 199 | } |
194 | 200 | ||
195 | private getHLSLiveOptions () { | 201 | private getHLSLiveOptions () { |
196 | const latencyMode = this.options.common.liveOptions.latencyMode | 202 | const latencyMode = this.options.liveOptions.latencyMode |
197 | 203 | ||
198 | switch (latencyMode) { | 204 | switch (latencyMode) { |
199 | case LiveVideoLatencyMode.SMALL_LATENCY: | 205 | case LiveVideoLatencyMode.SMALL_LATENCY: |
diff --git a/client/src/assets/player/shared/player-options-builder/index.ts b/client/src/assets/player/shared/player-options-builder/index.ts new file mode 100644 index 000000000..674754a94 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './control-bar-options-builder' | ||
2 | export * from './hls-options-builder' | ||
3 | export * from './web-video-options-builder' | ||
diff --git a/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts new file mode 100644 index 000000000..a3c3c3f27 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { PeerTubePlayerLoadOptions, WebVideoPluginOptions } from '../../types' | ||
2 | |||
3 | type ConstructorOptions = Pick<PeerTubePlayerLoadOptions, 'videoFileToken' | 'webVideo' | 'hls' | 'startTime'> | ||
4 | |||
5 | export class WebVideoOptionsBuilder { | ||
6 | |||
7 | constructor (private options: ConstructorOptions) { | ||
8 | |||
9 | } | ||
10 | |||
11 | getPluginOptions (): WebVideoPluginOptions { | ||
12 | return { | ||
13 | videoFileToken: this.options.videoFileToken, | ||
14 | |||
15 | videoFiles: this.options.webVideo.videoFiles.length !== 0 | ||
16 | ? this.options.webVideo.videoFiles | ||
17 | : this.options?.hls.videoFiles || [], | ||
18 | |||
19 | startTime: this.options.startTime | ||
20 | } | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-button.ts b/client/src/assets/player/shared/playlist/playlist-button.ts index 6cfaf4158..45cbb4899 100644 --- a/client/src/assets/player/shared/playlist/playlist-button.ts +++ b/client/src/assets/player/shared/playlist/playlist-button.ts | |||
@@ -8,8 +8,15 @@ class PlaylistButton extends ClickableComponent { | |||
8 | private playlistInfoElement: HTMLElement | 8 | private playlistInfoElement: HTMLElement |
9 | private wrapper: HTMLElement | 9 | private wrapper: HTMLElement |
10 | 10 | ||
11 | constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) { | 11 | options_: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions |
12 | super(player, options as any) | 12 | |
13 | // FIXME: eslint -> it's not a useless constructor, we need to extend constructor options typings | ||
14 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor | ||
15 | constructor ( | ||
16 | player: videojs.Player, | ||
17 | options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions | ||
18 | ) { | ||
19 | super(player, options) | ||
13 | } | 20 | } |
14 | 21 | ||
15 | createEl () { | 22 | createEl () { |
@@ -40,20 +47,15 @@ class PlaylistButton extends ClickableComponent { | |||
40 | } | 47 | } |
41 | 48 | ||
42 | update () { | 49 | update () { |
43 | const options = this.options_ as PlaylistPluginOptions | 50 | this.playlistInfoElement.innerHTML = this.options_.getCurrentPosition() + '/' + this.options_.playlist.videosLength |
44 | 51 | ||
45 | this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength | 52 | this.wrapper.title = this.player().localize('Playlist: {1}', [ this.options_.playlist.displayName ]) |
46 | this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ]) | ||
47 | } | 53 | } |
48 | 54 | ||
49 | handleClick () { | 55 | handleClick () { |
50 | const playlistMenu = this.getPlaylistMenu() | 56 | const playlistMenu = this.options_.playlistMenu |
51 | playlistMenu.open() | 57 | playlistMenu.open() |
52 | } | 58 | } |
53 | |||
54 | private getPlaylistMenu () { | ||
55 | return (this.options_ as any).playlistMenu as PlaylistMenu | ||
56 | } | ||
57 | } | 59 | } |
58 | 60 | ||
59 | videojs.registerComponent('PlaylistButton', PlaylistButton) | 61 | videojs.registerComponent('PlaylistButton', PlaylistButton) |
diff --git a/client/src/assets/player/shared/playlist/playlist-menu-item.ts b/client/src/assets/player/shared/playlist/playlist-menu-item.ts index 81b5acf30..f9366332d 100644 --- a/client/src/assets/player/shared/playlist/playlist-menu-item.ts +++ b/client/src/assets/player/shared/playlist/playlist-menu-item.ts | |||
@@ -8,6 +8,11 @@ const Component = videojs.getComponent('Component') | |||
8 | class PlaylistMenuItem extends Component { | 8 | class PlaylistMenuItem extends Component { |
9 | private element: VideoPlaylistElement | 9 | private element: VideoPlaylistElement |
10 | 10 | ||
11 | private clickHandler: () => void | ||
12 | private keyDownHandler: (event: KeyboardEvent) => void | ||
13 | |||
14 | options_: videojs.ComponentOptions & PlaylistItemOptions | ||
15 | |||
11 | constructor (player: videojs.Player, options?: PlaylistItemOptions) { | 16 | constructor (player: videojs.Player, options?: PlaylistItemOptions) { |
12 | super(player, options as any) | 17 | super(player, options as any) |
13 | 18 | ||
@@ -15,19 +20,27 @@ class PlaylistMenuItem extends Component { | |||
15 | 20 | ||
16 | this.element = options.element | 21 | this.element = options.element |
17 | 22 | ||
18 | this.on([ 'click', 'tap' ], () => this.switchPlaylistItem()) | 23 | this.clickHandler = () => this.switchPlaylistItem() |
19 | this.on('keydown', event => this.handleKeyDown(event)) | 24 | this.keyDownHandler = event => this.handleKeyDown(event) |
25 | |||
26 | this.on([ 'click', 'tap' ], this.clickHandler) | ||
27 | this.on('keydown', this.keyDownHandler) | ||
20 | } | 28 | } |
21 | 29 | ||
22 | createEl () { | 30 | dispose () { |
23 | const options = this.options_ as PlaylistItemOptions | 31 | this.off([ 'click', 'tap' ], this.clickHandler) |
32 | this.off('keydown', this.keyDownHandler) | ||
24 | 33 | ||
34 | super.dispose() | ||
35 | } | ||
36 | |||
37 | createEl () { | ||
25 | const li = super.createEl('li', { | 38 | const li = super.createEl('li', { |
26 | className: 'vjs-playlist-menu-item', | 39 | className: 'vjs-playlist-menu-item', |
27 | innerHTML: '' | 40 | innerHTML: '' |
28 | }) as HTMLElement | 41 | }) as HTMLElement |
29 | 42 | ||
30 | if (!options.element.video) { | 43 | if (!this.options_.element.video) { |
31 | li.classList.add('vjs-disabled') | 44 | li.classList.add('vjs-disabled') |
32 | } | 45 | } |
33 | 46 | ||
@@ -37,14 +50,14 @@ class PlaylistMenuItem extends Component { | |||
37 | 50 | ||
38 | const position = super.createEl('div', { | 51 | const position = super.createEl('div', { |
39 | className: 'item-position', | 52 | className: 'item-position', |
40 | innerHTML: options.element.position | 53 | innerHTML: this.options_.element.position |
41 | }) | 54 | }) |
42 | 55 | ||
43 | positionBlock.appendChild(position) | 56 | positionBlock.appendChild(position) |
44 | li.appendChild(positionBlock) | 57 | li.appendChild(positionBlock) |
45 | 58 | ||
46 | if (options.element.video) { | 59 | if (this.options_.element.video) { |
47 | this.buildAvailableVideo(li, positionBlock, options) | 60 | this.buildAvailableVideo(li, positionBlock, this.options_) |
48 | } else { | 61 | } else { |
49 | this.buildUnavailableVideo(li) | 62 | this.buildUnavailableVideo(li) |
50 | } | 63 | } |
@@ -125,9 +138,7 @@ class PlaylistMenuItem extends Component { | |||
125 | } | 138 | } |
126 | 139 | ||
127 | private switchPlaylistItem () { | 140 | private switchPlaylistItem () { |
128 | const options = this.options_ as PlaylistItemOptions | 141 | this.options_.onClicked() |
129 | |||
130 | options.onClicked() | ||
131 | } | 142 | } |
132 | } | 143 | } |
133 | 144 | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-menu.ts b/client/src/assets/player/shared/playlist/playlist-menu.ts index 1ec9ac804..53a5a7274 100644 --- a/client/src/assets/player/shared/playlist/playlist-menu.ts +++ b/client/src/assets/player/shared/playlist/playlist-menu.ts | |||
@@ -6,26 +6,32 @@ import { PlaylistMenuItem } from './playlist-menu-item' | |||
6 | const Component = videojs.getComponent('Component') | 6 | const Component = videojs.getComponent('Component') |
7 | 7 | ||
8 | class PlaylistMenu extends Component { | 8 | class PlaylistMenu extends Component { |
9 | private menuItems: PlaylistMenuItem[] | 9 | private menuItems: PlaylistMenuItem[] = [] |
10 | 10 | ||
11 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { | 11 | private readonly userInactiveHandler: () => void |
12 | super(player, options as any) | 12 | private readonly onMouseEnter: () => void |
13 | private readonly onMouseLeave: () => void | ||
13 | 14 | ||
14 | const self = this | 15 | private readonly onPlayerCick: (event: Event) => void |
15 | 16 | ||
16 | function userInactiveHandler () { | 17 | options_: PlaylistPluginOptions & videojs.ComponentOptions |
17 | self.close() | 18 | |
19 | constructor (player: videojs.Player, options?: PlaylistPluginOptions & videojs.ComponentOptions) { | ||
20 | super(player, options) | ||
21 | |||
22 | this.userInactiveHandler = () => { | ||
23 | this.close() | ||
18 | } | 24 | } |
19 | 25 | ||
20 | this.el().addEventListener('mouseenter', () => { | 26 | this.onMouseEnter = () => { |
21 | this.player().off('userinactive', userInactiveHandler) | 27 | this.player().off('userinactive', this.userInactiveHandler) |
22 | }) | 28 | } |
23 | 29 | ||
24 | this.el().addEventListener('mouseleave', () => { | 30 | this.onMouseLeave = () => { |
25 | this.player().one('userinactive', userInactiveHandler) | 31 | this.player().one('userinactive', this.userInactiveHandler) |
26 | }) | 32 | } |
27 | 33 | ||
28 | this.player().on('click', event => { | 34 | this.onPlayerCick = event => { |
29 | let current = event.target as HTMLElement | 35 | let current = event.target as HTMLElement |
30 | 36 | ||
31 | do { | 37 | do { |
@@ -40,14 +46,31 @@ class PlaylistMenu extends Component { | |||
40 | } while (current) | 46 | } while (current) |
41 | 47 | ||
42 | this.close() | 48 | this.close() |
43 | }) | 49 | } |
50 | |||
51 | this.el().addEventListener('mouseenter', this.onMouseEnter) | ||
52 | this.el().addEventListener('mouseleave', this.onMouseLeave) | ||
53 | |||
54 | this.player().on('click', this.onPlayerCick) | ||
55 | } | ||
56 | |||
57 | dispose () { | ||
58 | this.el().removeEventListener('mouseenter', this.onMouseEnter) | ||
59 | this.el().removeEventListener('mouseleave', this.onMouseLeave) | ||
60 | |||
61 | this.player().off('userinactive', this.userInactiveHandler) | ||
62 | this.player().off('click', this.onPlayerCick) | ||
63 | |||
64 | for (const item of this.menuItems) { | ||
65 | item.dispose() | ||
66 | } | ||
67 | |||
68 | super.dispose() | ||
44 | } | 69 | } |
45 | 70 | ||
46 | createEl () { | 71 | createEl () { |
47 | this.menuItems = [] | 72 | this.menuItems = [] |
48 | 73 | ||
49 | const options = this.getOptions() | ||
50 | |||
51 | const menu = super.createEl('div', { | 74 | const menu = super.createEl('div', { |
52 | className: 'vjs-playlist-menu', | 75 | className: 'vjs-playlist-menu', |
53 | innerHTML: '', | 76 | innerHTML: '', |
@@ -61,11 +84,11 @@ class PlaylistMenu extends Component { | |||
61 | const headerLeft = super.createEl('div') | 84 | const headerLeft = super.createEl('div') |
62 | 85 | ||
63 | const leftTitle = super.createEl('div', { | 86 | const leftTitle = super.createEl('div', { |
64 | innerHTML: options.playlist.displayName, | 87 | innerHTML: this.options_.playlist.displayName, |
65 | className: 'title' | 88 | className: 'title' |
66 | }) | 89 | }) |
67 | 90 | ||
68 | const playlistChannel = options.playlist.videoChannel | 91 | const playlistChannel = this.options_.playlist.videoChannel |
69 | const leftSubtitle = super.createEl('div', { | 92 | const leftSubtitle = super.createEl('div', { |
70 | innerHTML: playlistChannel | 93 | innerHTML: playlistChannel |
71 | ? this.player().localize('By {1}', [ playlistChannel.displayName ]) | 94 | ? this.player().localize('By {1}', [ playlistChannel.displayName ]) |
@@ -86,7 +109,7 @@ class PlaylistMenu extends Component { | |||
86 | 109 | ||
87 | const list = super.createEl('ol') | 110 | const list = super.createEl('ol') |
88 | 111 | ||
89 | for (const playlistElement of options.elements) { | 112 | for (const playlistElement of this.options_.elements) { |
90 | const item = new PlaylistMenuItem(this.player(), { | 113 | const item = new PlaylistMenuItem(this.player(), { |
91 | element: playlistElement, | 114 | element: playlistElement, |
92 | onClicked: () => this.onItemClicked(playlistElement) | 115 | onClicked: () => this.onItemClicked(playlistElement) |
@@ -100,13 +123,13 @@ class PlaylistMenu extends Component { | |||
100 | menu.appendChild(header) | 123 | menu.appendChild(header) |
101 | menu.appendChild(list) | 124 | menu.appendChild(list) |
102 | 125 | ||
126 | this.update() | ||
127 | |||
103 | return menu | 128 | return menu |
104 | } | 129 | } |
105 | 130 | ||
106 | update () { | 131 | update () { |
107 | const options = this.getOptions() | 132 | this.updateSelected(this.options_.getCurrentPosition()) |
108 | |||
109 | this.updateSelected(options.getCurrentPosition()) | ||
110 | } | 133 | } |
111 | 134 | ||
112 | open () { | 135 | open () { |
@@ -123,12 +146,8 @@ class PlaylistMenu extends Component { | |||
123 | } | 146 | } |
124 | } | 147 | } |
125 | 148 | ||
126 | private getOptions () { | ||
127 | return this.options_ as PlaylistPluginOptions | ||
128 | } | ||
129 | |||
130 | private onItemClicked (element: VideoPlaylistElement) { | 149 | private onItemClicked (element: VideoPlaylistElement) { |
131 | this.getOptions().onItemClicked(element) | 150 | this.options_.onItemClicked(element) |
132 | } | 151 | } |
133 | } | 152 | } |
134 | 153 | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-plugin.ts b/client/src/assets/player/shared/playlist/playlist-plugin.ts index 44de0da5a..c00e45843 100644 --- a/client/src/assets/player/shared/playlist/playlist-plugin.ts +++ b/client/src/assets/player/shared/playlist/playlist-plugin.ts | |||
@@ -8,17 +8,10 @@ const Plugin = videojs.getPlugin('plugin') | |||
8 | class PlaylistPlugin extends Plugin { | 8 | class PlaylistPlugin extends Plugin { |
9 | private playlistMenu: PlaylistMenu | 9 | private playlistMenu: PlaylistMenu |
10 | private playlistButton: PlaylistButton | 10 | private playlistButton: PlaylistButton |
11 | private options: PlaylistPluginOptions | ||
12 | 11 | ||
13 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { | 12 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { |
14 | super(player, options) | 13 | super(player, options) |
15 | 14 | ||
16 | this.options = options | ||
17 | |||
18 | this.player.ready(() => { | ||
19 | player.addClass('vjs-playlist') | ||
20 | }) | ||
21 | |||
22 | this.playlistMenu = new PlaylistMenu(player, options) | 15 | this.playlistMenu = new PlaylistMenu(player, options) |
23 | this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu }) | 16 | this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu }) |
24 | 17 | ||
@@ -26,8 +19,16 @@ class PlaylistPlugin extends Plugin { | |||
26 | player.addChild(this.playlistButton, options) | 19 | player.addChild(this.playlistButton, options) |
27 | } | 20 | } |
28 | 21 | ||
29 | updateSelected () { | 22 | dispose () { |
30 | this.playlistMenu.updateSelected(this.options.getCurrentPosition()) | 23 | this.player.removeClass('vjs-playlist') |
24 | |||
25 | this.playlistMenu.dispose() | ||
26 | this.playlistButton.dispose() | ||
27 | |||
28 | this.player.removeChild(this.playlistMenu) | ||
29 | this.player.removeChild(this.playlistButton) | ||
30 | |||
31 | super.dispose() | ||
31 | } | 32 | } |
32 | } | 33 | } |
33 | 34 | ||
diff --git a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts index 4fafd27b1..4d6701003 100644 --- a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts +++ b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts | |||
@@ -8,7 +8,16 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
8 | private resolutions: PeerTubeResolution[] = [] | 8 | private resolutions: PeerTubeResolution[] = [] |
9 | 9 | ||
10 | private autoResolutionChosenId: number | 10 | private autoResolutionChosenId: number |
11 | private autoResolutionEnabled = true | 11 | |
12 | constructor (player: videojs.Player) { | ||
13 | super(player) | ||
14 | |||
15 | player.on('video-change', () => { | ||
16 | this.resolutions = [] | ||
17 | |||
18 | this.trigger('resolutions-removed') | ||
19 | }) | ||
20 | } | ||
12 | 21 | ||
13 | add (resolutions: PeerTubeResolution[]) { | 22 | add (resolutions: PeerTubeResolution[]) { |
14 | for (const r of resolutions) { | 23 | for (const r of resolutions) { |
@@ -18,12 +27,12 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
18 | this.currentSelection = this.getSelected() | 27 | this.currentSelection = this.getSelected() |
19 | 28 | ||
20 | this.sort() | 29 | this.sort() |
21 | this.trigger('resolutionsAdded') | 30 | this.trigger('resolutions-added') |
22 | } | 31 | } |
23 | 32 | ||
24 | remove (resolutionIndex: number) { | 33 | remove (resolutionIndex: number) { |
25 | this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex) | 34 | this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex) |
26 | this.trigger('resolutionRemoved') | 35 | this.trigger('resolutions-removed') |
27 | } | 36 | } |
28 | 37 | ||
29 | getResolutions () { | 38 | getResolutions () { |
@@ -40,10 +49,10 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
40 | 49 | ||
41 | select (options: { | 50 | select (options: { |
42 | id: number | 51 | id: number |
43 | byEngine: boolean | 52 | fireCallback: boolean |
44 | autoResolutionChosenId?: number | 53 | autoResolutionChosenId?: number |
45 | }) { | 54 | }) { |
46 | const { id, autoResolutionChosenId, byEngine } = options | 55 | const { id, autoResolutionChosenId, fireCallback } = options |
47 | 56 | ||
48 | if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return | 57 | if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return |
49 | 58 | ||
@@ -55,25 +64,11 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
55 | if (r.selected) { | 64 | if (r.selected) { |
56 | this.currentSelection = r | 65 | this.currentSelection = r |
57 | 66 | ||
58 | if (!byEngine) r.selectCallback() | 67 | if (fireCallback) r.selectCallback() |
59 | } | 68 | } |
60 | } | 69 | } |
61 | 70 | ||
62 | this.trigger('resolutionChanged') | 71 | this.trigger('resolutions-changed') |
63 | } | ||
64 | |||
65 | disableAutoResolution () { | ||
66 | this.autoResolutionEnabled = false | ||
67 | this.trigger('autoResolutionEnabledChanged') | ||
68 | } | ||
69 | |||
70 | enabledAutoResolution () { | ||
71 | this.autoResolutionEnabled = true | ||
72 | this.trigger('autoResolutionEnabledChanged') | ||
73 | } | ||
74 | |||
75 | isAutoResolutionEnabeld () { | ||
76 | return this.autoResolutionEnabled | ||
77 | } | 72 | } |
78 | 73 | ||
79 | private sort () { | 74 | private sort () { |
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts index 672411c11..c39894284 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-button.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts | |||
@@ -11,12 +11,12 @@ class ResolutionMenuButton extends MenuButton { | |||
11 | 11 | ||
12 | this.controlText('Quality') | 12 | this.controlText('Quality') |
13 | 13 | ||
14 | player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) | 14 | player.peertubeResolutions().on('resolutions-added', () => this.update()) |
15 | player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities()) | 15 | player.peertubeResolutions().on('resolutions-removed', () => this.update()) |
16 | 16 | ||
17 | // For parent | 17 | // For parent |
18 | player.peertubeResolutions().on('resolutionChanged', () => { | 18 | player.peertubeResolutions().on('resolutions-changed', () => { |
19 | setTimeout(() => this.trigger('labelUpdated')) | 19 | setTimeout(() => this.trigger('label-updated')) |
20 | }) | 20 | }) |
21 | } | 21 | } |
22 | 22 | ||
@@ -37,69 +37,42 @@ class ResolutionMenuButton extends MenuButton { | |||
37 | } | 37 | } |
38 | 38 | ||
39 | createMenu () { | 39 | createMenu () { |
40 | return new Menu(this.player_) | 40 | const menu: videojs.Menu = new Menu(this.player_, { menuButton: this }) |
41 | } | 41 | const resolutions = this.player().peertubeResolutions().getResolutions() |
42 | |||
43 | buildCSSClass () { | ||
44 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
45 | } | ||
46 | |||
47 | buildWrapperCSSClass () { | ||
48 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
49 | } | ||
50 | |||
51 | private addClickListener (component: any) { | ||
52 | component.on('click', () => { | ||
53 | const children = this.menu.children() | ||
54 | |||
55 | for (const child of children) { | ||
56 | if (component !== child) { | ||
57 | (child as videojs.MenuItem).selected(false) | ||
58 | } | ||
59 | } | ||
60 | }) | ||
61 | } | ||
62 | 42 | ||
63 | private buildQualities () { | 43 | for (const r of resolutions) { |
64 | for (const d of this.player().peertubeResolutions().getResolutions()) { | 44 | const label = r.label === '0p' |
65 | const label = d.label === '0p' | ||
66 | ? this.player().localize('Audio-only') | 45 | ? this.player().localize('Audio-only') |
67 | : d.label | 46 | : r.label |
68 | 47 | ||
69 | this.menu.addChild(new ResolutionMenuItem( | 48 | const component = new ResolutionMenuItem( |
70 | this.player_, | 49 | this.player_, |
71 | { | 50 | { |
72 | id: d.id + '', | 51 | id: r.id + '', |
73 | resolutionId: d.id, | 52 | resolutionId: r.id, |
74 | label, | 53 | label, |
75 | selected: d.selected | 54 | selected: r.selected |
76 | }) | 55 | } |
77 | ) | 56 | ) |
78 | } | ||
79 | 57 | ||
80 | for (const m of this.menu.children()) { | 58 | menu.addItem(component) |
81 | this.addClickListener(m) | ||
82 | } | 59 | } |
83 | 60 | ||
84 | this.trigger('menuChanged') | 61 | return menu |
85 | } | 62 | } |
86 | 63 | ||
87 | private cleanupQualities () { | 64 | update () { |
88 | const resolutions = this.player().peertubeResolutions().getResolutions() | 65 | super.update() |
89 | |||
90 | this.menu.children().forEach((children: ResolutionMenuItem) => { | ||
91 | if (children.resolutionId === undefined) { | ||
92 | return | ||
93 | } | ||
94 | 66 | ||
95 | if (resolutions.find(r => r.id === children.resolutionId)) { | 67 | this.trigger('menu-changed') |
96 | return | 68 | } |
97 | } | ||
98 | 69 | ||
99 | this.menu.removeChild(children) | 70 | buildCSSClass () { |
100 | }) | 71 | return super.buildCSSClass() + ' vjs-resolution-button' |
72 | } | ||
101 | 73 | ||
102 | this.trigger('menuChanged') | 74 | buildWrapperCSSClass () { |
75 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
103 | } | 76 | } |
104 | } | 77 | } |
105 | 78 | ||
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts index c59b8b891..86387f533 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-item.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts | |||
@@ -10,35 +10,32 @@ class ResolutionMenuItem extends MenuItem { | |||
10 | readonly resolutionId: number | 10 | readonly resolutionId: number |
11 | private readonly label: string | 11 | private readonly label: string |
12 | 12 | ||
13 | private autoResolutionEnabled: boolean | ||
14 | private autoResolutionChosen: string | 13 | private autoResolutionChosen: string |
15 | 14 | ||
16 | constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { | 15 | private updateSelectionHandler: () => void |
17 | options.selectable = true | ||
18 | 16 | ||
19 | super(player, options) | 17 | constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { |
18 | super(player, { ...options, selectable: true }) | ||
20 | 19 | ||
21 | this.autoResolutionEnabled = true | ||
22 | this.autoResolutionChosen = '' | 20 | this.autoResolutionChosen = '' |
23 | 21 | ||
24 | this.resolutionId = options.resolutionId | 22 | this.resolutionId = options.resolutionId |
25 | this.label = options.label | 23 | this.label = options.label |
26 | 24 | ||
27 | player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection()) | 25 | this.updateSelectionHandler = () => this.updateSelection() |
26 | player.peertubeResolutions().on('resolutions-changed', this.updateSelectionHandler) | ||
27 | } | ||
28 | |||
29 | dispose () { | ||
30 | this.player().peertubeResolutions().off('resolutions-changed', this.updateSelectionHandler) | ||
28 | 31 | ||
29 | // We only want to disable the "Auto" item | 32 | super.dispose() |
30 | if (this.resolutionId === -1) { | ||
31 | player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution()) | ||
32 | } | ||
33 | } | 33 | } |
34 | 34 | ||
35 | handleClick (event: any) { | 35 | handleClick (event: any) { |
36 | // Auto button disabled? | ||
37 | if (this.autoResolutionEnabled === false && this.resolutionId === -1) return | ||
38 | |||
39 | super.handleClick(event) | 36 | super.handleClick(event) |
40 | 37 | ||
41 | this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false }) | 38 | this.player().peertubeResolutions().select({ id: this.resolutionId, fireCallback: true }) |
42 | } | 39 | } |
43 | 40 | ||
44 | updateSelection () { | 41 | updateSelection () { |
@@ -51,19 +48,6 @@ class ResolutionMenuItem extends MenuItem { | |||
51 | this.selected(this.resolutionId === selectedResolution.id) | 48 | this.selected(this.resolutionId === selectedResolution.id) |
52 | } | 49 | } |
53 | 50 | ||
54 | updateAutoResolution () { | ||
55 | const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld() | ||
56 | |||
57 | // Check if the auto resolution is enabled or not | ||
58 | if (enabled === false) { | ||
59 | this.addClass('disabled') | ||
60 | } else { | ||
61 | this.removeClass('disabled') | ||
62 | } | ||
63 | |||
64 | this.autoResolutionEnabled = enabled | ||
65 | } | ||
66 | |||
67 | getLabel () { | 51 | getLabel () { |
68 | if (this.resolutionId === -1) { | 52 | if (this.resolutionId === -1) { |
69 | return this.label + ' <small>' + this.autoResolutionChosen + '</small>' | 53 | return this.label + ' <small>' + this.autoResolutionChosen + '</small>' |
diff --git a/client/src/assets/player/shared/settings/settings-dialog.ts b/client/src/assets/player/shared/settings/settings-dialog.ts index f5fbbe7ad..ba39d0f45 100644 --- a/client/src/assets/player/shared/settings/settings-dialog.ts +++ b/client/src/assets/player/shared/settings/settings-dialog.ts | |||
@@ -28,6 +28,18 @@ class SettingsDialog extends Component { | |||
28 | 'aria-describedby': dialogDescriptionId | 28 | 'aria-describedby': dialogDescriptionId |
29 | }) | 29 | }) |
30 | } | 30 | } |
31 | |||
32 | show () { | ||
33 | this.player().addClass('vjs-settings-dialog-opened') | ||
34 | |||
35 | super.show() | ||
36 | } | ||
37 | |||
38 | hide () { | ||
39 | this.player().removeClass('vjs-settings-dialog-opened') | ||
40 | |||
41 | super.hide() | ||
42 | } | ||
31 | } | 43 | } |
32 | 44 | ||
33 | Component.registerComponent('SettingsDialog', SettingsDialog) | 45 | Component.registerComponent('SettingsDialog', SettingsDialog) |
diff --git a/client/src/assets/player/shared/settings/settings-menu-button.ts b/client/src/assets/player/shared/settings/settings-menu-button.ts index 4cf29866b..9499a43eb 100644 --- a/client/src/assets/player/shared/settings/settings-menu-button.ts +++ b/client/src/assets/player/shared/settings/settings-menu-button.ts | |||
@@ -71,7 +71,7 @@ class SettingsButton extends Button { | |||
71 | } | 71 | } |
72 | } | 72 | } |
73 | 73 | ||
74 | onDisposeSettingsItem (event: any, name: string) { | 74 | onDisposeSettingsItem (_event: any, name: string) { |
75 | if (name === undefined) { | 75 | if (name === undefined) { |
76 | const children = this.menu.children() | 76 | const children = this.menu.children() |
77 | 77 | ||
@@ -103,6 +103,8 @@ class SettingsButton extends Button { | |||
103 | if (this.isInIframe()) { | 103 | if (this.isInIframe()) { |
104 | window.removeEventListener('blur', this.documentClickHandler) | 104 | window.removeEventListener('blur', this.documentClickHandler) |
105 | } | 105 | } |
106 | |||
107 | super.dispose() | ||
106 | } | 108 | } |
107 | 109 | ||
108 | onAddSettingsItem (event: any, data: any) { | 110 | onAddSettingsItem (event: any, data: any) { |
@@ -249,8 +251,8 @@ class SettingsButton extends Button { | |||
249 | } | 251 | } |
250 | 252 | ||
251 | resetChildren () { | 253 | resetChildren () { |
252 | for (const menuChild of this.menu.children()) { | 254 | for (const menuChild of this.menu.children() as SettingsMenuItem[]) { |
253 | (menuChild as SettingsMenuItem).reset() | 255 | menuChild.reset() |
254 | } | 256 | } |
255 | } | 257 | } |
256 | 258 | ||
@@ -258,8 +260,8 @@ class SettingsButton extends Button { | |||
258 | * Hide all the sub menus | 260 | * Hide all the sub menus |
259 | */ | 261 | */ |
260 | hideChildren () { | 262 | hideChildren () { |
261 | for (const menuChild of this.menu.children()) { | 263 | for (const menuChild of this.menu.children() as SettingsMenuItem[]) { |
262 | (menuChild as SettingsMenuItem).hideSubMenu() | 264 | menuChild.hideSubMenu() |
263 | } | 265 | } |
264 | } | 266 | } |
265 | 267 | ||
diff --git a/client/src/assets/player/shared/settings/settings-menu-item.ts b/client/src/assets/player/shared/settings/settings-menu-item.ts index 288e3b233..9916ae27f 100644 --- a/client/src/assets/player/shared/settings/settings-menu-item.ts +++ b/client/src/assets/player/shared/settings/settings-menu-item.ts | |||
@@ -70,17 +70,22 @@ class SettingsMenuItem extends MenuItem { | |||
70 | this.build() | 70 | this.build() |
71 | 71 | ||
72 | // Update on rate change | 72 | // Update on rate change |
73 | player.on('ratechange', this.submenuClickHandler) | 73 | if (subMenuName === 'PlaybackRateMenuButton') { |
74 | player.on('ratechange', this.submenuClickHandler) | ||
75 | } | ||
74 | 76 | ||
75 | if (subMenuName === 'CaptionsButton') { | 77 | if (subMenuName === 'CaptionsButton') { |
76 | // Hack to regenerate captions on HTTP fallback | 78 | player.on('captions-changed', () => { |
77 | player.on('captionsChanged', () => { | 79 | // Wait menu component rebuild |
78 | setTimeout(() => { | 80 | setTimeout(() => { |
79 | this.settingsSubMenuEl_.innerHTML = '' | 81 | this.rebuildAfterMenuChange() |
80 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | 82 | }, 150) |
81 | this.update() | 83 | }) |
82 | this.bindClickEvents() | 84 | } |
83 | }, 0) | 85 | |
86 | if (subMenuName === 'ResolutionMenuButton') { | ||
87 | this.subMenu.on('menu-changed', () => { | ||
88 | this.rebuildAfterMenuChange() | ||
84 | }) | 89 | }) |
85 | } | 90 | } |
86 | 91 | ||
@@ -89,6 +94,12 @@ class SettingsMenuItem extends MenuItem { | |||
89 | }) | 94 | }) |
90 | } | 95 | } |
91 | 96 | ||
97 | dispose () { | ||
98 | this.settingsSubMenuEl_.removeEventListener('transitionend', this.transitionEndHandler) | ||
99 | |||
100 | super.dispose() | ||
101 | } | ||
102 | |||
92 | eventHandlers () { | 103 | eventHandlers () { |
93 | this.submenuClickHandler = this.onSubmenuClick.bind(this) | 104 | this.submenuClickHandler = this.onSubmenuClick.bind(this) |
94 | this.transitionEndHandler = this.onTransitionEnd.bind(this) | 105 | this.transitionEndHandler = this.onTransitionEnd.bind(this) |
@@ -190,27 +201,6 @@ class SettingsMenuItem extends MenuItem { | |||
190 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) | 201 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) |
191 | } | 202 | } |
192 | 203 | ||
193 | /** | ||
194 | * Add/remove prefixed event listener for CSS Transition | ||
195 | * | ||
196 | * @method PrefixedEvent | ||
197 | */ | ||
198 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { | ||
199 | const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] | ||
200 | |||
201 | for (let p = 0; p < prefix.length; p++) { | ||
202 | if (!prefix[p]) { | ||
203 | type = type.toLowerCase() | ||
204 | } | ||
205 | |||
206 | if (action === 'addEvent') { | ||
207 | element.addEventListener(prefix[p] + type, callback, false) | ||
208 | } else if (action === 'removeEvent') { | ||
209 | element.removeEventListener(prefix[p] + type, callback, false) | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | |||
214 | onTransitionEnd (event: any) { | 204 | onTransitionEnd (event: any) { |
215 | if (event.propertyName !== 'margin-right') { | 205 | if (event.propertyName !== 'margin-right') { |
216 | return | 206 | return |
@@ -254,12 +244,7 @@ class SettingsMenuItem extends MenuItem { | |||
254 | } | 244 | } |
255 | 245 | ||
256 | build () { | 246 | build () { |
257 | this.subMenu.on('labelUpdated', () => { | 247 | this.subMenu.on('label-updated', () => { |
258 | this.update() | ||
259 | }) | ||
260 | this.subMenu.on('menuChanged', () => { | ||
261 | this.bindClickEvents() | ||
262 | this.setSize() | ||
263 | this.update() | 248 | this.update() |
264 | }) | 249 | }) |
265 | 250 | ||
@@ -272,25 +257,12 @@ class SettingsMenuItem extends MenuItem { | |||
272 | this.setSize() | 257 | this.setSize() |
273 | this.bindClickEvents() | 258 | this.bindClickEvents() |
274 | 259 | ||
275 | // prefixed event listeners for CSS TransitionEnd | 260 | this.settingsSubMenuEl_.addEventListener('transitionend', this.transitionEndHandler, false) |
276 | this.PrefixedEvent( | ||
277 | this.settingsSubMenuEl_, | ||
278 | 'TransitionEnd', | ||
279 | this.transitionEndHandler, | ||
280 | 'addEvent' | ||
281 | ) | ||
282 | } | 261 | } |
283 | 262 | ||
284 | update (event?: any) { | 263 | update (event?: any) { |
285 | let target: HTMLElement = null | ||
286 | const subMenu = this.subMenu.name() | 264 | const subMenu = this.subMenu.name() |
287 | 265 | ||
288 | if (event && event.type === 'tap') { | ||
289 | target = event.target | ||
290 | } else if (event) { | ||
291 | target = event.currentTarget | ||
292 | } | ||
293 | |||
294 | // Playback rate menu button doesn't get a vjs-selected class | 266 | // Playback rate menu button doesn't get a vjs-selected class |
295 | // or sets options_['selected'] on the selected playback rate. | 267 | // or sets options_['selected'] on the selected playback rate. |
296 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | 268 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton |
@@ -321,6 +293,13 @@ class SettingsMenuItem extends MenuItem { | |||
321 | } | 293 | } |
322 | } | 294 | } |
323 | 295 | ||
296 | let target: HTMLElement = null | ||
297 | if (event && event.type === 'tap') { | ||
298 | target = event.target | ||
299 | } else if (event) { | ||
300 | target = event.currentTarget | ||
301 | } | ||
302 | |||
324 | if (target && !target.classList.contains('vjs-back-button')) { | 303 | if (target && !target.classList.contains('vjs-back-button')) { |
325 | this.settingsButton.hideDialog() | 304 | this.settingsButton.hideDialog() |
326 | } | 305 | } |
@@ -369,6 +348,15 @@ class SettingsMenuItem extends MenuItem { | |||
369 | } | 348 | } |
370 | } | 349 | } |
371 | 350 | ||
351 | private rebuildAfterMenuChange () { | ||
352 | this.settingsSubMenuEl_.innerHTML = '' | ||
353 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | ||
354 | this.update() | ||
355 | this.createBackButton() | ||
356 | this.setSize() | ||
357 | this.bindClickEvents() | ||
358 | } | ||
359 | |||
372 | } | 360 | } |
373 | 361 | ||
374 | (SettingsMenuItem as any).prototype.contentElType = 'button' | 362 | (SettingsMenuItem as any).prototype.contentElType = 'button' |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index 471a5e46c..fad68cec9 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -7,7 +7,7 @@ import { bytes } from '../common' | |||
7 | interface StatsCardOptions extends videojs.ComponentOptions { | 7 | interface StatsCardOptions extends videojs.ComponentOptions { |
8 | videoUUID: string | 8 | videoUUID: string |
9 | videoIsLive: boolean | 9 | videoIsLive: boolean |
10 | mode: 'webtorrent' | 'p2p-media-loader' | 10 | mode: 'web-video' | 'p2p-media-loader' |
11 | p2pEnabled: boolean | 11 | p2pEnabled: boolean |
12 | } | 12 | } |
13 | 13 | ||
@@ -34,7 +34,7 @@ class StatsCard extends Component { | |||
34 | 34 | ||
35 | updateInterval: any | 35 | updateInterval: any |
36 | 36 | ||
37 | mode: 'webtorrent' | 'p2p-media-loader' | 37 | mode: 'web-video' | 'p2p-media-loader' |
38 | 38 | ||
39 | metadataStore: any = {} | 39 | metadataStore: any = {} |
40 | 40 | ||
@@ -63,6 +63,9 @@ class StatsCard extends Component { | |||
63 | 63 | ||
64 | private liveLatency: InfoElement | 64 | private liveLatency: InfoElement |
65 | 65 | ||
66 | private onP2PInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void | ||
67 | private onHTTPInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void | ||
68 | |||
66 | createEl () { | 69 | createEl () { |
67 | this.containerEl = videojs.dom.createEl('div', { | 70 | this.containerEl = videojs.dom.createEl('div', { |
68 | className: 'vjs-stats-content' | 71 | className: 'vjs-stats-content' |
@@ -86,9 +89,7 @@ class StatsCard extends Component { | |||
86 | 89 | ||
87 | this.populateInfoBlocks() | 90 | this.populateInfoBlocks() |
88 | 91 | ||
89 | this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => { | 92 | this.onP2PInfoHandler = (_event, data) => { |
90 | if (!data) return // HTTP fallback | ||
91 | |||
92 | this.mode = data.source | 93 | this.mode = data.source |
93 | 94 | ||
94 | const p2pStats = data.p2p | 95 | const p2pStats = data.p2p |
@@ -105,11 +106,29 @@ class StatsCard extends Component { | |||
105 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') | 106 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') |
106 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | 107 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') |
107 | } | 108 | } |
108 | }) | 109 | } |
110 | |||
111 | this.onHTTPInfoHandler = (_event, data) => { | ||
112 | this.mode = data.source | ||
113 | |||
114 | this.playerNetworkInfo.totalDownloaded = bytes(data.http.downloaded).join(' ') | ||
115 | } | ||
116 | |||
117 | this.player().on('p2p-info', this.onP2PInfoHandler) | ||
118 | this.player().on('http-info', this.onHTTPInfoHandler) | ||
109 | 119 | ||
110 | return this.containerEl | 120 | return this.containerEl |
111 | } | 121 | } |
112 | 122 | ||
123 | dispose () { | ||
124 | if (this.updateInterval) clearInterval(this.updateInterval) | ||
125 | |||
126 | this.player().off('p2p-info', this.onP2PInfoHandler) | ||
127 | this.player().off('http-info', this.onHTTPInfoHandler) | ||
128 | |||
129 | super.dispose() | ||
130 | } | ||
131 | |||
113 | toggle () { | 132 | toggle () { |
114 | if (this.updateInterval) this.hide() | 133 | if (this.updateInterval) this.hide() |
115 | else this.show() | 134 | else this.show() |
@@ -122,7 +141,7 @@ class StatsCard extends Component { | |||
122 | try { | 141 | try { |
123 | const options = this.mode === 'p2p-media-loader' | 142 | const options = this.mode === 'p2p-media-loader' |
124 | ? this.buildHLSOptions() | 143 | ? this.buildHLSOptions() |
125 | : await this.buildWebTorrentOptions() // Default | 144 | : await this.buildWebVideoOptions() // Default |
126 | 145 | ||
127 | this.populateInfoValues(options) | 146 | this.populateInfoValues(options) |
128 | } catch (err) { | 147 | } catch (err) { |
@@ -170,8 +189,8 @@ class StatsCard extends Component { | |||
170 | } | 189 | } |
171 | } | 190 | } |
172 | 191 | ||
173 | private async buildWebTorrentOptions () { | 192 | private async buildWebVideoOptions () { |
174 | const videoFile = this.player_.webtorrent().getCurrentVideoFile() | 193 | const videoFile = this.player_.webVideo().getCurrentVideoFile() |
175 | 194 | ||
176 | if (!this.metadataStore[videoFile.fileUrl]) { | 195 | if (!this.metadataStore[videoFile.fileUrl]) { |
177 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) | 196 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) |
@@ -194,7 +213,7 @@ class StatsCard extends Component { | |||
194 | 213 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 214 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player_.buffered()) | 215 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 216 | const progress = this.player_.bufferedPercent() |
198 | 217 | ||
199 | return { | 218 | return { |
200 | playerNetworkInfo: this.playerNetworkInfo, | 219 | playerNetworkInfo: this.playerNetworkInfo, |
@@ -284,8 +303,10 @@ class StatsCard extends Component { | |||
284 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` | 303 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` |
285 | : undefined | 304 | : undefined |
286 | 305 | ||
287 | this.setInfoValue(this.playerMode, this.mode || 'HTTP') | 306 | const p2pEnabled = this.options_.p2pEnabled && this.mode === 'p2p-media-loader' |
288 | this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled')) | 307 | |
308 | this.setInfoValue(this.playerMode, this.mode) | ||
309 | this.setInfoValue(this.p2p, player.localize(p2pEnabled ? 'enabled' : 'disabled')) | ||
289 | this.setInfoValue(this.uuid, this.options_.videoUUID) | 310 | this.setInfoValue(this.uuid, this.options_.videoUUID) |
290 | 311 | ||
291 | this.setInfoValue(this.viewport, frames) | 312 | this.setInfoValue(this.viewport, frames) |
diff --git a/client/src/assets/player/shared/stats/stats-plugin.ts b/client/src/assets/player/shared/stats/stats-plugin.ts index 8aad80e8a..86684a78c 100644 --- a/client/src/assets/player/shared/stats/stats-plugin.ts +++ b/client/src/assets/player/shared/stats/stats-plugin.ts | |||
@@ -7,10 +7,6 @@ class StatsForNerdsPlugin extends Plugin { | |||
7 | private statsCard: StatsCard | 7 | private statsCard: StatsCard |
8 | 8 | ||
9 | constructor (player: videojs.Player, options: StatsCardOptions) { | 9 | constructor (player: videojs.Player, options: StatsCardOptions) { |
10 | const settings = { | ||
11 | ...options | ||
12 | } | ||
13 | |||
14 | super(player) | 10 | super(player) |
15 | 11 | ||
16 | this.player.ready(() => { | 12 | this.player.ready(() => { |
@@ -19,7 +15,17 @@ class StatsForNerdsPlugin extends Plugin { | |||
19 | 15 | ||
20 | this.statsCard = new StatsCard(player, options) | 16 | this.statsCard = new StatsCard(player, options) |
21 | 17 | ||
22 | player.addChild(this.statsCard, settings) | 18 | // Copy options |
19 | player.addChild(this.statsCard) | ||
20 | } | ||
21 | |||
22 | dispose () { | ||
23 | if (this.statsCard) { | ||
24 | this.statsCard.dispose() | ||
25 | this.player.removeChild(this.statsCard) | ||
26 | } | ||
27 | |||
28 | super.dispose() | ||
23 | } | 29 | } |
24 | 30 | ||
25 | show () { | 31 | show () { |
diff --git a/client/src/assets/player/shared/upnext/end-card.ts b/client/src/assets/player/shared/upnext/end-card.ts index 61668e407..3589e1fd8 100644 --- a/client/src/assets/player/shared/upnext/end-card.ts +++ b/client/src/assets/player/shared/upnext/end-card.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { UpNextPluginOptions } from '../../types' | ||
2 | 3 | ||
3 | function getMainTemplate (options: any) { | 4 | function getMainTemplate (options: EndCardOptions) { |
4 | return ` | 5 | return ` |
5 | <div class="vjs-upnext-top"> | 6 | <div class="vjs-upnext-top"> |
6 | <span class="vjs-upnext-headtext">${options.headText}</span> | 7 | <span class="vjs-upnext-headtext">${options.headText}</span> |
@@ -23,15 +24,10 @@ function getMainTemplate (options: any) { | |||
23 | ` | 24 | ` |
24 | } | 25 | } |
25 | 26 | ||
26 | export interface EndCardOptions extends videojs.ComponentOptions { | 27 | export interface EndCardOptions extends videojs.ComponentOptions, UpNextPluginOptions { |
27 | next: () => void | ||
28 | getTitle: () => string | ||
29 | timeout: number | ||
30 | cancelText: string | 28 | cancelText: string |
31 | headText: string | 29 | headText: string |
32 | suspendedText: string | 30 | suspendedText: string |
33 | condition: () => boolean | ||
34 | suspended: () => boolean | ||
35 | } | 31 | } |
36 | 32 | ||
37 | const Component = videojs.getComponent('Component') | 33 | const Component = videojs.getComponent('Component') |
@@ -52,27 +48,43 @@ class EndCard extends Component { | |||
52 | suspendedMessage: HTMLElement | 48 | suspendedMessage: HTMLElement |
53 | nextButton: HTMLElement | 49 | nextButton: HTMLElement |
54 | 50 | ||
51 | private onEndedHandler: () => void | ||
52 | private onPlayingHandler: () => void | ||
53 | |||
55 | constructor (player: videojs.Player, options: EndCardOptions) { | 54 | constructor (player: videojs.Player, options: EndCardOptions) { |
56 | super(player, options) | 55 | super(player, options) |
57 | 56 | ||
58 | this.totalTicks = this.options_.timeout / this.interval | 57 | this.totalTicks = this.options_.timeout / this.interval |
59 | 58 | ||
60 | player.on('ended', (_: any) => { | 59 | this.onEndedHandler = () => { |
61 | if (!this.options_.condition()) return | 60 | if (!this.options_.isDisplayed()) return |
62 | 61 | ||
63 | player.addClass('vjs-upnext--showing') | 62 | player.addClass('vjs-upnext--showing') |
64 | this.showCard((canceled: boolean) => { | 63 | |
64 | this.showCard(canceled => { | ||
65 | player.removeClass('vjs-upnext--showing') | 65 | player.removeClass('vjs-upnext--showing') |
66 | |||
66 | this.container.style.display = 'none' | 67 | this.container.style.display = 'none' |
68 | |||
67 | if (!canceled) { | 69 | if (!canceled) { |
68 | this.options_.next() | 70 | this.options_.next() |
69 | } | 71 | } |
70 | }) | 72 | }) |
71 | }) | 73 | } |
72 | 74 | ||
73 | player.on('playing', () => { | 75 | this.onPlayingHandler = () => { |
74 | this.upNextEvents.trigger('playing') | 76 | this.upNextEvents.trigger('playing') |
75 | }) | 77 | } |
78 | |||
79 | player.on([ 'auto-stopped', 'ended' ], this.onEndedHandler) | ||
80 | player.on('playing', this.onPlayingHandler) | ||
81 | } | ||
82 | |||
83 | dispose () { | ||
84 | if (this.onEndedHandler) this.player().off([ 'auto-stopped', 'ended' ], this.onEndedHandler) | ||
85 | if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler) | ||
86 | |||
87 | super.dispose() | ||
76 | } | 88 | } |
77 | 89 | ||
78 | createEl () { | 90 | createEl () { |
@@ -101,7 +113,7 @@ class EndCard extends Component { | |||
101 | return container | 113 | return container |
102 | } | 114 | } |
103 | 115 | ||
104 | showCard (cb: (value: boolean) => void) { | 116 | showCard (cb: (canceled: boolean) => void) { |
105 | let timeout: any | 117 | let timeout: any |
106 | 118 | ||
107 | this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`) | 119 | this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`) |
@@ -109,6 +121,10 @@ class EndCard extends Component { | |||
109 | 121 | ||
110 | this.title.innerHTML = this.options_.getTitle() | 122 | this.title.innerHTML = this.options_.getTitle() |
111 | 123 | ||
124 | if (this.totalTicks === 0) { | ||
125 | return cb(false) | ||
126 | } | ||
127 | |||
112 | this.upNextEvents.one('cancel', () => { | 128 | this.upNextEvents.one('cancel', () => { |
113 | clearTimeout(timeout) | 129 | clearTimeout(timeout) |
114 | cb(true) | 130 | cb(true) |
@@ -134,7 +150,7 @@ class EndCard extends Component { | |||
134 | } | 150 | } |
135 | 151 | ||
136 | const update = () => { | 152 | const update = () => { |
137 | if (this.options_.suspended()) { | 153 | if (this.options_.isSuspended()) { |
138 | this.suspendedMessage.innerText = this.options_.suspendedText | 154 | this.suspendedMessage.innerText = this.options_.suspendedText |
139 | goToPercent(0) | 155 | goToPercent(0) |
140 | this.ticks = 0 | 156 | this.ticks = 0 |
diff --git a/client/src/assets/player/shared/upnext/upnext-plugin.ts b/client/src/assets/player/shared/upnext/upnext-plugin.ts index e12e8c503..0badcd68c 100644 --- a/client/src/assets/player/shared/upnext/upnext-plugin.ts +++ b/client/src/assets/player/shared/upnext/upnext-plugin.ts | |||
@@ -1,27 +1,25 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { UpNextPluginOptions } from '../../types' | ||
2 | import { EndCardOptions } from './end-card' | 3 | import { EndCardOptions } from './end-card' |
3 | 4 | ||
4 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
5 | 6 | ||
6 | class UpNextPlugin extends Plugin { | 7 | class UpNextPlugin extends Plugin { |
7 | 8 | ||
8 | constructor (player: videojs.Player, options: Partial<EndCardOptions> = {}) { | 9 | constructor (player: videojs.Player, options: UpNextPluginOptions) { |
9 | const settings = { | 10 | super(player) |
11 | |||
12 | const settings: EndCardOptions = { | ||
10 | next: options.next, | 13 | next: options.next, |
11 | getTitle: options.getTitle, | 14 | getTitle: options.getTitle, |
12 | timeout: options.timeout || 5000, | 15 | timeout: options.timeout, |
13 | cancelText: options.cancelText || 'Cancel', | 16 | cancelText: player.localize('Cancel'), |
14 | headText: options.headText || 'Up Next', | 17 | headText: player.localize('Up Next'), |
15 | suspendedText: options.suspendedText || 'Autoplay is suspended', | 18 | suspendedText: player.localize('Autoplay is suspended'), |
16 | condition: options.condition, | 19 | isDisplayed: options.isDisplayed, |
17 | suspended: options.suspended | 20 | isSuspended: options.isSuspended |
18 | } | 21 | } |
19 | 22 | ||
20 | super(player) | ||
21 | |||
22 | // UpNext plugin can be called later, so ensure the player is not disposed | ||
23 | if (this.player.isDisposed()) return | ||
24 | |||
25 | this.player.ready(() => { | 23 | this.player.ready(() => { |
26 | player.addClass('vjs-upnext') | 24 | player.addClass('vjs-upnext') |
27 | }) | 25 | }) |
diff --git a/client/src/assets/player/shared/web-video/web-video-plugin.ts b/client/src/assets/player/shared/web-video/web-video-plugin.ts new file mode 100644 index 000000000..80e56795b --- /dev/null +++ b/client/src/assets/player/shared/web-video/web-video-plugin.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | import debug from 'debug' | ||
2 | import videojs from 'video.js' | ||
3 | import { logger } from '@root-helpers/logger' | ||
4 | import { addQueryParams } from '@shared/core-utils' | ||
5 | import { VideoFile } from '@shared/models' | ||
6 | import { PeerTubeResolution, PlayerNetworkInfo, WebVideoPluginOptions } from '../../types' | ||
7 | |||
8 | const debugLogger = debug('peertube:player:web-video-plugin') | ||
9 | |||
10 | const Plugin = videojs.getPlugin('plugin') | ||
11 | |||
12 | class WebVideoPlugin extends Plugin { | ||
13 | private readonly videoFiles: VideoFile[] | ||
14 | |||
15 | private currentVideoFile: VideoFile | ||
16 | private videoFileToken: () => string | ||
17 | |||
18 | private networkInfoInterval: any | ||
19 | |||
20 | private onErrorHandler: () => void | ||
21 | private onPlayHandler: () => void | ||
22 | |||
23 | constructor (player: videojs.Player, options?: WebVideoPluginOptions) { | ||
24 | super(player, options) | ||
25 | |||
26 | this.videoFiles = options.videoFiles | ||
27 | this.videoFileToken = options.videoFileToken | ||
28 | |||
29 | this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false }) | ||
30 | |||
31 | player.ready(() => { | ||
32 | this.buildQualities() | ||
33 | |||
34 | this.setupNetworkInfoInterval() | ||
35 | |||
36 | if (this.videoFiles.length === 0) { | ||
37 | this.player.addClass('disabled') | ||
38 | return | ||
39 | } | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | dispose () { | ||
44 | clearInterval(this.networkInfoInterval) | ||
45 | |||
46 | if (this.onErrorHandler) this.player.off('error', this.onErrorHandler) | ||
47 | if (this.onPlayHandler) this.player.off('canplay', this.onPlayHandler) | ||
48 | |||
49 | super.dispose() | ||
50 | } | ||
51 | |||
52 | getCurrentResolutionId () { | ||
53 | return this.currentVideoFile.resolution.id | ||
54 | } | ||
55 | |||
56 | updateVideoFile (options: { | ||
57 | videoFile: VideoFile | ||
58 | isUserResolutionChange: boolean | ||
59 | }) { | ||
60 | this.currentVideoFile = options.videoFile | ||
61 | |||
62 | debugLogger('Updating web video file to ' + this.currentVideoFile.fileUrl) | ||
63 | |||
64 | const paused = this.player.paused() | ||
65 | const playbackRate = this.player.playbackRate() | ||
66 | const currentTime = this.player.currentTime() | ||
67 | |||
68 | // Enable error display now this is our last fallback | ||
69 | this.onErrorHandler = () => this.player.peertube().displayFatalError() | ||
70 | this.player.one('error', this.onErrorHandler) | ||
71 | |||
72 | let httpUrl = this.currentVideoFile.fileUrl | ||
73 | |||
74 | if (this.videoFileToken()) { | ||
75 | httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) | ||
76 | } | ||
77 | |||
78 | const oldAutoplayValue = this.player.autoplay() | ||
79 | if (options.isUserResolutionChange) { | ||
80 | this.player.autoplay(false) | ||
81 | this.player.addClass('vjs-updating-resolution') | ||
82 | } | ||
83 | |||
84 | this.player.src(httpUrl) | ||
85 | |||
86 | this.onPlayHandler = () => { | ||
87 | this.player.playbackRate(playbackRate) | ||
88 | this.player.currentTime(currentTime) | ||
89 | |||
90 | this.adaptPosterForAudioOnly() | ||
91 | |||
92 | if (options.isUserResolutionChange) { | ||
93 | this.player.trigger('user-resolution-change') | ||
94 | this.player.trigger('web-video-source-change') | ||
95 | |||
96 | this.tryToPlay() | ||
97 | .then(() => { | ||
98 | if (paused) this.player.pause() | ||
99 | |||
100 | this.player.autoplay(oldAutoplayValue) | ||
101 | }) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | this.player.one('canplay', this.onPlayHandler) | ||
106 | } | ||
107 | |||
108 | getCurrentVideoFile () { | ||
109 | return this.currentVideoFile | ||
110 | } | ||
111 | |||
112 | private adaptPosterForAudioOnly () { | ||
113 | // Audio-only (resolutionId === 0) gets special treatment | ||
114 | if (this.currentVideoFile.resolution.id === 0) { | ||
115 | this.player.audioPosterMode(true) | ||
116 | } else { | ||
117 | this.player.audioPosterMode(false) | ||
118 | } | ||
119 | } | ||
120 | |||
121 | private tryToPlay () { | ||
122 | debugLogger('Try to play manually the video') | ||
123 | |||
124 | const playPromise = this.player.play() | ||
125 | if (playPromise === undefined) return | ||
126 | |||
127 | return playPromise | ||
128 | .catch((err: Error) => { | ||
129 | if (err.message.includes('The play() request was interrupted by a call to pause()')) { | ||
130 | return | ||
131 | } | ||
132 | |||
133 | logger.warn(err) | ||
134 | this.player.pause() | ||
135 | this.player.posterImage.show() | ||
136 | this.player.removeClass('vjs-has-autoplay') | ||
137 | this.player.removeClass('vjs-playing-audio-only-content') | ||
138 | }) | ||
139 | .finally(() => { | ||
140 | this.player.removeClass('vjs-updating-resolution') | ||
141 | }) | ||
142 | } | ||
143 | |||
144 | private pickAverageVideoFile () { | ||
145 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
146 | |||
147 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
148 | return files[Math.floor(files.length / 2)] | ||
149 | } | ||
150 | |||
151 | private buildQualities () { | ||
152 | const resolutions: PeerTubeResolution[] = this.videoFiles.map(videoFile => ({ | ||
153 | id: videoFile.resolution.id, | ||
154 | label: this.buildQualityLabel(videoFile), | ||
155 | height: videoFile.resolution.id, | ||
156 | selected: videoFile.id === this.currentVideoFile.id, | ||
157 | selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true }) | ||
158 | })) | ||
159 | |||
160 | this.player.peertubeResolutions().add(resolutions) | ||
161 | } | ||
162 | |||
163 | private buildQualityLabel (file: VideoFile) { | ||
164 | let label = file.resolution.label | ||
165 | |||
166 | if (file.fps && file.fps >= 50) { | ||
167 | label += file.fps | ||
168 | } | ||
169 | |||
170 | return label | ||
171 | } | ||
172 | |||
173 | private setupNetworkInfoInterval () { | ||
174 | this.networkInfoInterval = setInterval(() => { | ||
175 | return this.player.trigger('http-info', { | ||
176 | source: 'web-video', | ||
177 | http: { | ||
178 | downloaded: this.player.bufferedPercent() * this.currentVideoFile.size | ||
179 | } | ||
180 | } as PlayerNetworkInfo) | ||
181 | }, 1000) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | videojs.registerPlugin('webVideo', WebVideoPlugin) | ||
186 | export { WebVideoPlugin } | ||
diff --git a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts b/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts deleted file mode 100644 index 74ae17704..000000000 --- a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts +++ /dev/null | |||
@@ -1,234 +0,0 @@ | |||
1 | // From https://github.com/MinEduTDF/idb-chunk-store | ||
2 | // We use temporary IndexDB (all data are removed on destroy) to avoid RAM issues | ||
3 | // Thanks @santiagogil and @Feross | ||
4 | |||
5 | import Dexie from 'dexie' | ||
6 | import { EventEmitter } from 'events' | ||
7 | import { logger } from '@root-helpers/logger' | ||
8 | |||
9 | class ChunkDatabase extends Dexie { | ||
10 | chunks: Dexie.Table<{ id: number, buf: Buffer }, number> | ||
11 | |||
12 | constructor (dbname: string) { | ||
13 | super(dbname) | ||
14 | |||
15 | this.version(1).stores({ | ||
16 | chunks: 'id' | ||
17 | }) | ||
18 | } | ||
19 | } | ||
20 | |||
21 | class ExpirationDatabase extends Dexie { | ||
22 | databases: Dexie.Table<{ name: string, expiration: number }, number> | ||
23 | |||
24 | constructor () { | ||
25 | super('webtorrent-expiration') | ||
26 | |||
27 | this.version(1).stores({ | ||
28 | databases: 'name,expiration' | ||
29 | }) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | export class PeertubeChunkStore extends EventEmitter { | ||
34 | private static readonly BUFFERING_PUT_MS = 1000 | ||
35 | private static readonly CLEANER_INTERVAL_MS = 1000 * 60 // 1 minute | ||
36 | private static readonly CLEANER_EXPIRATION_MS = 1000 * 60 * 5 // 5 minutes | ||
37 | |||
38 | chunkLength: number | ||
39 | |||
40 | private pendingPut: { id: number, buf: Buffer, cb: (err?: Error) => void }[] = [] | ||
41 | // If the store is full | ||
42 | private memoryChunks: { [ id: number ]: Buffer | true } = {} | ||
43 | private databaseName: string | ||
44 | private putBulkTimeout: any | ||
45 | private cleanerInterval: any | ||
46 | private db: ChunkDatabase | ||
47 | private expirationDB: ExpirationDatabase | ||
48 | private readonly length: number | ||
49 | private readonly lastChunkLength: number | ||
50 | private readonly lastChunkIndex: number | ||
51 | |||
52 | constructor (chunkLength: number, opts: any) { | ||
53 | super() | ||
54 | |||
55 | this.databaseName = 'webtorrent-chunks-' | ||
56 | |||
57 | if (!opts) opts = {} | ||
58 | if (opts.torrent?.infoHash) this.databaseName += opts.torrent.infoHash | ||
59 | else this.databaseName += '-default' | ||
60 | |||
61 | this.setMaxListeners(100) | ||
62 | |||
63 | this.chunkLength = Number(chunkLength) | ||
64 | if (!this.chunkLength) throw new Error('First argument must be a chunk length') | ||
65 | |||
66 | this.length = Number(opts.length) || Infinity | ||
67 | |||
68 | if (this.length !== Infinity) { | ||
69 | this.lastChunkLength = (this.length % this.chunkLength) || this.chunkLength | ||
70 | this.lastChunkIndex = Math.ceil(this.length / this.chunkLength) - 1 | ||
71 | } | ||
72 | |||
73 | this.db = new ChunkDatabase(this.databaseName) | ||
74 | // Track databases that expired | ||
75 | this.expirationDB = new ExpirationDatabase() | ||
76 | |||
77 | this.runCleaner() | ||
78 | } | ||
79 | |||
80 | put (index: number, buf: Buffer, cb: (err?: Error) => void) { | ||
81 | const isLastChunk = (index === this.lastChunkIndex) | ||
82 | if (isLastChunk && buf.length !== this.lastChunkLength) { | ||
83 | return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength)) | ||
84 | } | ||
85 | if (!isLastChunk && buf.length !== this.chunkLength) { | ||
86 | return this.nextTick(cb, new Error('Chunk length must be ' + this.chunkLength)) | ||
87 | } | ||
88 | |||
89 | // Specify we have this chunk | ||
90 | this.memoryChunks[index] = true | ||
91 | |||
92 | // Add it to the pending put | ||
93 | this.pendingPut.push({ id: index, buf, cb }) | ||
94 | // If it's already planned, return | ||
95 | if (this.putBulkTimeout) return | ||
96 | |||
97 | // Plan a future bulk insert | ||
98 | this.putBulkTimeout = setTimeout(async () => { | ||
99 | const processing = this.pendingPut | ||
100 | this.pendingPut = [] | ||
101 | this.putBulkTimeout = undefined | ||
102 | |||
103 | try { | ||
104 | await this.db.transaction('rw', this.db.chunks, () => { | ||
105 | return this.db.chunks.bulkPut(processing.map(p => ({ id: p.id, buf: p.buf }))) | ||
106 | }) | ||
107 | } catch (err) { | ||
108 | logger.info('Cannot bulk insert chunks. Store them in memory.', err) | ||
109 | |||
110 | processing.forEach(p => { | ||
111 | this.memoryChunks[p.id] = p.buf | ||
112 | }) | ||
113 | } finally { | ||
114 | processing.forEach(p => p.cb()) | ||
115 | } | ||
116 | }, PeertubeChunkStore.BUFFERING_PUT_MS) | ||
117 | } | ||
118 | |||
119 | get (index: number, opts: any, cb: (err?: Error, buf?: Buffer) => void): void { | ||
120 | if (typeof opts === 'function') return this.get(index, null, opts) | ||
121 | |||
122 | // IndexDB could be slow, use our memory index first | ||
123 | const memoryChunk = this.memoryChunks[index] | ||
124 | if (memoryChunk === undefined) { | ||
125 | const err = new Error('Chunk not found') as any | ||
126 | err['notFound'] = true | ||
127 | |||
128 | return process.nextTick(() => cb(err)) | ||
129 | } | ||
130 | |||
131 | // Chunk in memory | ||
132 | if (memoryChunk !== true) return cb(null, memoryChunk) | ||
133 | |||
134 | // Chunk in store | ||
135 | this.db.transaction('r', this.db.chunks, async () => { | ||
136 | const result = await this.db.chunks.get({ id: index }) | ||
137 | if (result === undefined) return cb(null, Buffer.alloc(0)) | ||
138 | |||
139 | const buf = result.buf | ||
140 | if (!opts) return this.nextTick(cb, null, buf) | ||
141 | |||
142 | const offset = opts.offset || 0 | ||
143 | const len = opts.length || (buf.length - offset) | ||
144 | return cb(null, buf.slice(offset, len + offset)) | ||
145 | }) | ||
146 | .catch(err => { | ||
147 | logger.error(err) | ||
148 | return cb(err) | ||
149 | }) | ||
150 | } | ||
151 | |||
152 | close (cb: (err?: Error) => void) { | ||
153 | return this.destroy(cb) | ||
154 | } | ||
155 | |||
156 | async destroy (cb: (err?: Error) => void) { | ||
157 | try { | ||
158 | if (this.pendingPut) { | ||
159 | clearTimeout(this.putBulkTimeout) | ||
160 | this.pendingPut = null | ||
161 | } | ||
162 | if (this.cleanerInterval) { | ||
163 | clearInterval(this.cleanerInterval) | ||
164 | this.cleanerInterval = null | ||
165 | } | ||
166 | |||
167 | if (this.db) { | ||
168 | this.db.close() | ||
169 | |||
170 | await this.dropDatabase(this.databaseName) | ||
171 | } | ||
172 | |||
173 | if (this.expirationDB) { | ||
174 | this.expirationDB.close() | ||
175 | this.expirationDB = null | ||
176 | } | ||
177 | |||
178 | return cb() | ||
179 | } catch (err) { | ||
180 | logger.error('Cannot destroy peertube chunk store.', err) | ||
181 | return cb(err) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | private runCleaner () { | ||
186 | this.checkExpiration() | ||
187 | |||
188 | this.cleanerInterval = setInterval(() => { | ||
189 | this.checkExpiration() | ||
190 | }, PeertubeChunkStore.CLEANER_INTERVAL_MS) | ||
191 | } | ||
192 | |||
193 | private async checkExpiration () { | ||
194 | let databasesToDeleteInfo: { name: string }[] = [] | ||
195 | |||
196 | try { | ||
197 | await this.expirationDB.transaction('rw', this.expirationDB.databases, async () => { | ||
198 | // Update our database expiration since we are alive | ||
199 | await this.expirationDB.databases.put({ | ||
200 | name: this.databaseName, | ||
201 | expiration: new Date().getTime() + PeertubeChunkStore.CLEANER_EXPIRATION_MS | ||
202 | }) | ||
203 | |||
204 | const now = new Date().getTime() | ||
205 | databasesToDeleteInfo = await this.expirationDB.databases.where('expiration').below(now).toArray() | ||
206 | }) | ||
207 | } catch (err) { | ||
208 | logger.error('Cannot update expiration of fetch expired databases.', err) | ||
209 | } | ||
210 | |||
211 | for (const databaseToDeleteInfo of databasesToDeleteInfo) { | ||
212 | await this.dropDatabase(databaseToDeleteInfo.name) | ||
213 | } | ||
214 | } | ||
215 | |||
216 | private async dropDatabase (databaseName: string) { | ||
217 | const dbToDelete = new ChunkDatabase(databaseName) | ||
218 | logger.info(`Destroying IndexDB database ${databaseName}`) | ||
219 | |||
220 | try { | ||
221 | await dbToDelete.delete() | ||
222 | |||
223 | await this.expirationDB.transaction('rw', this.expirationDB.databases, () => { | ||
224 | return this.expirationDB.databases.where({ name: databaseName }).delete() | ||
225 | }) | ||
226 | } catch (err) { | ||
227 | logger.error(`Cannot delete ${databaseName}.`, err) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | private nextTick <T> (cb: (err?: Error, val?: T) => void, err: Error, val?: T) { | ||
232 | process.nextTick(() => cb(err, val), undefined) | ||
233 | } | ||
234 | } | ||
diff --git a/client/src/assets/player/shared/webtorrent/video-renderer.ts b/client/src/assets/player/shared/webtorrent/video-renderer.ts deleted file mode 100644 index a85d7a838..000000000 --- a/client/src/assets/player/shared/webtorrent/video-renderer.ts +++ /dev/null | |||
@@ -1,134 +0,0 @@ | |||
1 | // Thanks: https://github.com/feross/render-media | ||
2 | |||
3 | const MediaElementWrapper = require('mediasource') | ||
4 | import { logger } from '@root-helpers/logger' | ||
5 | import { extname } from 'path' | ||
6 | const Videostream = require('videostream') | ||
7 | |||
8 | const VIDEOSTREAM_EXTS = [ | ||
9 | '.m4a', | ||
10 | '.m4v', | ||
11 | '.mp4' | ||
12 | ] | ||
13 | |||
14 | type RenderMediaOptions = { | ||
15 | controls: boolean | ||
16 | autoplay: boolean | ||
17 | } | ||
18 | |||
19 | function renderVideo ( | ||
20 | file: any, | ||
21 | elem: HTMLVideoElement, | ||
22 | opts: RenderMediaOptions, | ||
23 | callback: (err: Error, renderer: any) => void | ||
24 | ) { | ||
25 | validateFile(file) | ||
26 | |||
27 | return renderMedia(file, elem, opts, callback) | ||
28 | } | ||
29 | |||
30 | function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { | ||
31 | const extension = extname(file.name).toLowerCase() | ||
32 | let preparedElem: any | ||
33 | let currentTime = 0 | ||
34 | let renderer: any | ||
35 | |||
36 | try { | ||
37 | if (VIDEOSTREAM_EXTS.includes(extension)) { | ||
38 | renderer = useVideostream() | ||
39 | } else { | ||
40 | renderer = useMediaSource() | ||
41 | } | ||
42 | } catch (err) { | ||
43 | return callback(err) | ||
44 | } | ||
45 | |||
46 | function useVideostream () { | ||
47 | prepareElem() | ||
48 | preparedElem.addEventListener('error', function onError (err: Error) { | ||
49 | preparedElem.removeEventListener('error', onError) | ||
50 | |||
51 | return callback(err) | ||
52 | }) | ||
53 | preparedElem.addEventListener('loadstart', onLoadStart) | ||
54 | return new Videostream(file, preparedElem) | ||
55 | } | ||
56 | |||
57 | function useMediaSource (useVP9 = false) { | ||
58 | const codecs = getCodec(file.name, useVP9) | ||
59 | |||
60 | prepareElem() | ||
61 | preparedElem.addEventListener('error', function onError (err: Error) { | ||
62 | preparedElem.removeEventListener('error', onError) | ||
63 | |||
64 | // Try with vp9 before returning an error | ||
65 | if (codecs.includes('vp8')) return fallbackToMediaSource(true) | ||
66 | |||
67 | return callback(err) | ||
68 | }) | ||
69 | preparedElem.addEventListener('loadstart', onLoadStart) | ||
70 | |||
71 | const wrapper = new MediaElementWrapper(preparedElem) | ||
72 | const writable = wrapper.createWriteStream(codecs) | ||
73 | file.createReadStream().pipe(writable) | ||
74 | |||
75 | if (currentTime) preparedElem.currentTime = currentTime | ||
76 | |||
77 | return wrapper | ||
78 | } | ||
79 | |||
80 | function fallbackToMediaSource (useVP9 = false) { | ||
81 | if (useVP9 === true) logger.info('Falling back to media source with VP9 enabled.') | ||
82 | else logger.info('Falling back to media source..') | ||
83 | |||
84 | useMediaSource(useVP9) | ||
85 | } | ||
86 | |||
87 | function prepareElem () { | ||
88 | if (preparedElem === undefined) { | ||
89 | preparedElem = elem | ||
90 | |||
91 | preparedElem.addEventListener('progress', function () { | ||
92 | currentTime = elem.currentTime | ||
93 | }) | ||
94 | } | ||
95 | } | ||
96 | |||
97 | function onLoadStart () { | ||
98 | preparedElem.removeEventListener('loadstart', onLoadStart) | ||
99 | if (opts.autoplay) preparedElem.play() | ||
100 | |||
101 | callback(null, renderer) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | function validateFile (file: any) { | ||
106 | if (file == null) { | ||
107 | throw new Error('file cannot be null or undefined') | ||
108 | } | ||
109 | if (typeof file.name !== 'string') { | ||
110 | throw new Error('missing or invalid file.name property') | ||
111 | } | ||
112 | if (typeof file.createReadStream !== 'function') { | ||
113 | throw new Error('missing or invalid file.createReadStream property') | ||
114 | } | ||
115 | } | ||
116 | |||
117 | function getCodec (name: string, useVP9 = false) { | ||
118 | const ext = extname(name).toLowerCase() | ||
119 | if (ext === '.mp4') { | ||
120 | return 'video/mp4; codecs="avc1.640029, mp4a.40.5"' | ||
121 | } | ||
122 | |||
123 | if (ext === '.webm') { | ||
124 | if (useVP9 === true) return 'video/webm; codecs="vp9, opus"' | ||
125 | |||
126 | return 'video/webm; codecs="vp8, vorbis"' | ||
127 | } | ||
128 | |||
129 | return undefined | ||
130 | } | ||
131 | |||
132 | export { | ||
133 | renderVideo | ||
134 | } | ||
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts deleted file mode 100644 index e2e220c03..000000000 --- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts +++ /dev/null | |||
@@ -1,663 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import * as WebTorrent from 'webtorrent' | ||
3 | import { logger } from '@root-helpers/logger' | ||
4 | import { isIOS } from '@root-helpers/web-browser' | ||
5 | import { addQueryParams, timeToInt } from '@shared/core-utils' | ||
6 | import { VideoFile } from '@shared/models' | ||
7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' | ||
8 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' | ||
9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../common' | ||
10 | import { PeertubeChunkStore } from './peertube-chunk-store' | ||
11 | import { renderVideo } from './video-renderer' | ||
12 | |||
13 | const CacheChunkStore = require('cache-chunk-store') | ||
14 | |||
15 | type PlayOptions = { | ||
16 | forcePlay?: boolean | ||
17 | seek?: number | ||
18 | delay?: number | ||
19 | } | ||
20 | |||
21 | const Plugin = videojs.getPlugin('plugin') | ||
22 | |||
23 | class WebTorrentPlugin extends Plugin { | ||
24 | readonly videoFiles: VideoFile[] | ||
25 | |||
26 | private readonly playerElement: HTMLVideoElement | ||
27 | |||
28 | private readonly autoplay: boolean | string = false | ||
29 | private readonly startTime: number = 0 | ||
30 | private readonly savePlayerSrcFunction: videojs.Player['src'] | ||
31 | private readonly videoDuration: number | ||
32 | private readonly CONSTANTS = { | ||
33 | INFO_SCHEDULER: 1000, // Don't change this | ||
34 | AUTO_QUALITY_SCHEDULER: 3000, // Check quality every 3 seconds | ||
35 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it | ||
36 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check | ||
37 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds | ||
38 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth | ||
39 | } | ||
40 | |||
41 | private readonly buildWebSeedUrls: (file: VideoFile) => string[] | ||
42 | |||
43 | private readonly webtorrent = new WebTorrent({ | ||
44 | tracker: { | ||
45 | rtcConfig: getRtcConfig() | ||
46 | }, | ||
47 | dht: false | ||
48 | }) | ||
49 | |||
50 | private currentVideoFile: VideoFile | ||
51 | private torrent: WebTorrent.Torrent | ||
52 | |||
53 | private renderer: any | ||
54 | private fakeRenderer: any | ||
55 | private destroyingFakeRenderer = false | ||
56 | |||
57 | private autoResolution = true | ||
58 | private autoResolutionPossible = true | ||
59 | private isAutoResolutionObservation = false | ||
60 | private playerRefusedP2P = false | ||
61 | |||
62 | private requiresUserAuth: boolean | ||
63 | private videoFileToken: () => string | ||
64 | |||
65 | private torrentInfoInterval: any | ||
66 | private autoQualityInterval: any | ||
67 | private addTorrentDelay: any | ||
68 | private qualityObservationTimer: any | ||
69 | private runAutoQualitySchedulerTimer: any | ||
70 | |||
71 | private downloadSpeeds: number[] = [] | ||
72 | |||
73 | constructor (player: videojs.Player, options?: WebtorrentPluginOptions) { | ||
74 | super(player) | ||
75 | |||
76 | this.startTime = timeToInt(options.startTime) | ||
77 | |||
78 | // Custom autoplay handled by webtorrent because we lazy play the video | ||
79 | this.autoplay = options.autoplay | ||
80 | |||
81 | this.playerRefusedP2P = options.playerRefusedP2P | ||
82 | |||
83 | this.videoFiles = options.videoFiles | ||
84 | this.videoDuration = options.videoDuration | ||
85 | |||
86 | this.savePlayerSrcFunction = this.player.src | ||
87 | this.playerElement = options.playerElement | ||
88 | |||
89 | this.requiresUserAuth = options.requiresUserAuth | ||
90 | this.videoFileToken = options.videoFileToken | ||
91 | |||
92 | this.buildWebSeedUrls = options.buildWebSeedUrls | ||
93 | |||
94 | this.player.ready(() => { | ||
95 | const playerOptions = this.player.options_ | ||
96 | |||
97 | const volume = getStoredVolume() | ||
98 | if (volume !== undefined) this.player.volume(volume) | ||
99 | |||
100 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
101 | if (muted !== undefined) this.player.muted(muted) | ||
102 | |||
103 | this.player.duration(options.videoDuration) | ||
104 | |||
105 | this.initializePlayer() | ||
106 | this.runTorrentInfoScheduler() | ||
107 | |||
108 | this.player.one('play', () => { | ||
109 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | ||
110 | this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER) | ||
111 | }) | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | dispose () { | ||
116 | clearTimeout(this.addTorrentDelay) | ||
117 | clearTimeout(this.qualityObservationTimer) | ||
118 | clearTimeout(this.runAutoQualitySchedulerTimer) | ||
119 | |||
120 | clearInterval(this.torrentInfoInterval) | ||
121 | clearInterval(this.autoQualityInterval) | ||
122 | |||
123 | // Don't need to destroy renderer, video player will be destroyed | ||
124 | this.flushVideoFile(this.currentVideoFile, false) | ||
125 | |||
126 | this.destroyFakeRenderer() | ||
127 | } | ||
128 | |||
129 | getCurrentResolutionId () { | ||
130 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | ||
131 | } | ||
132 | |||
133 | updateVideoFile ( | ||
134 | videoFile?: VideoFile, | ||
135 | options: { | ||
136 | forcePlay?: boolean | ||
137 | seek?: number | ||
138 | delay?: number | ||
139 | } = {}, | ||
140 | done: () => void = () => { /* empty */ } | ||
141 | ) { | ||
142 | // Automatically choose the adapted video file | ||
143 | if (!videoFile) { | ||
144 | const savedAverageBandwidth = getAverageBandwidthInStore() | ||
145 | videoFile = savedAverageBandwidth | ||
146 | ? this.getAppropriateFile(savedAverageBandwidth) | ||
147 | : this.pickAverageVideoFile() | ||
148 | } | ||
149 | |||
150 | if (!videoFile) { | ||
151 | throw Error(`Can't update video file since videoFile is undefined.`) | ||
152 | } | ||
153 | |||
154 | // Don't add the same video file once again | ||
155 | if (this.currentVideoFile !== undefined && this.currentVideoFile.magnetUri === videoFile.magnetUri) { | ||
156 | return | ||
157 | } | ||
158 | |||
159 | // Do not display error to user because we will have multiple fallback | ||
160 | this.player.peertube().hideFatalError(); | ||
161 | |||
162 | // Hack to "simulate" src link in video.js >= 6 | ||
163 | // Without this, we can't play the video after pausing it | ||
164 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 | ||
165 | (this.player as any).src = () => true | ||
166 | const oldPlaybackRate = this.player.playbackRate() | ||
167 | |||
168 | const previousVideoFile = this.currentVideoFile | ||
169 | this.currentVideoFile = videoFile | ||
170 | |||
171 | // Don't try on iOS that does not support MediaSource | ||
172 | // Or don't use P2P if webtorrent is disabled | ||
173 | if (isIOS() || this.playerRefusedP2P) { | ||
174 | return this.fallbackToHttp(options, () => { | ||
175 | this.player.playbackRate(oldPlaybackRate) | ||
176 | return done() | ||
177 | }) | ||
178 | } | ||
179 | |||
180 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => { | ||
181 | this.player.playbackRate(oldPlaybackRate) | ||
182 | return done() | ||
183 | }) | ||
184 | |||
185 | this.selectAppropriateResolution(true) | ||
186 | } | ||
187 | |||
188 | updateEngineResolution (resolutionId: number, delay = 0) { | ||
189 | // Remember player state | ||
190 | const currentTime = this.player.currentTime() | ||
191 | const isPaused = this.player.paused() | ||
192 | |||
193 | // Hide bigPlayButton | ||
194 | if (!isPaused) { | ||
195 | this.player.bigPlayButton.hide() | ||
196 | } | ||
197 | |||
198 | // Audio-only (resolutionId === 0) gets special treatment | ||
199 | if (resolutionId === 0) { | ||
200 | // Audio-only: show poster, do not auto-hide controls | ||
201 | this.player.addClass('vjs-playing-audio-only-content') | ||
202 | this.player.posterImage.show() | ||
203 | } else { | ||
204 | // Hide poster to have black background | ||
205 | this.player.removeClass('vjs-playing-audio-only-content') | ||
206 | this.player.posterImage.hide() | ||
207 | } | ||
208 | |||
209 | const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) | ||
210 | const options = { | ||
211 | forcePlay: false, | ||
212 | delay, | ||
213 | seek: currentTime + (delay / 1000) | ||
214 | } | ||
215 | |||
216 | this.updateVideoFile(newVideoFile, options) | ||
217 | |||
218 | this.player.trigger('engineResolutionChange') | ||
219 | } | ||
220 | |||
221 | flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { | ||
222 | if (videoFile !== undefined && this.webtorrent.get(videoFile.magnetUri)) { | ||
223 | if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy() | ||
224 | |||
225 | this.webtorrent.remove(videoFile.magnetUri) | ||
226 | logger.info(`Removed ${videoFile.magnetUri}`) | ||
227 | } | ||
228 | } | ||
229 | |||
230 | disableAutoResolution () { | ||
231 | this.autoResolution = false | ||
232 | this.autoResolutionPossible = false | ||
233 | this.player.peertubeResolutions().disableAutoResolution() | ||
234 | } | ||
235 | |||
236 | isAutoResolutionPossible () { | ||
237 | return this.autoResolutionPossible | ||
238 | } | ||
239 | |||
240 | getTorrent () { | ||
241 | return this.torrent | ||
242 | } | ||
243 | |||
244 | getCurrentVideoFile () { | ||
245 | return this.currentVideoFile | ||
246 | } | ||
247 | |||
248 | changeQuality (id: number) { | ||
249 | if (id === -1) { | ||
250 | if (this.autoResolutionPossible === true) { | ||
251 | this.autoResolution = true | ||
252 | |||
253 | this.selectAppropriateResolution(false) | ||
254 | } | ||
255 | |||
256 | return | ||
257 | } | ||
258 | |||
259 | this.autoResolution = false | ||
260 | this.updateEngineResolution(id) | ||
261 | this.selectAppropriateResolution(false) | ||
262 | } | ||
263 | |||
264 | private addTorrent ( | ||
265 | magnetOrTorrentUrl: string, | ||
266 | previousVideoFile: VideoFile, | ||
267 | options: PlayOptions, | ||
268 | done: (err?: Error) => void | ||
269 | ) { | ||
270 | if (!magnetOrTorrentUrl) return this.fallbackToHttp(options, done) | ||
271 | |||
272 | logger.info(`Adding ${magnetOrTorrentUrl}.`) | ||
273 | |||
274 | const oldTorrent = this.torrent | ||
275 | const torrentOptions = { | ||
276 | // Don't use arrow function: it breaks webtorrent (that uses `new` keyword) | ||
277 | store: function (chunkLength: number, storeOpts: any) { | ||
278 | return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { | ||
279 | max: 100 | ||
280 | }) | ||
281 | }, | ||
282 | urlList: this.buildWebSeedUrls(this.currentVideoFile) | ||
283 | } | ||
284 | |||
285 | this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { | ||
286 | logger.info(`Added ${magnetOrTorrentUrl}.`) | ||
287 | |||
288 | if (oldTorrent) { | ||
289 | // Pause the old torrent | ||
290 | this.stopTorrent(oldTorrent) | ||
291 | |||
292 | // We use a fake renderer so we download correct pieces of the next file | ||
293 | if (options.delay) this.renderFileInFakeElement(torrent.files[0], options.delay) | ||
294 | } | ||
295 | |||
296 | // Render the video in a few seconds? (on resolution change for example, we wait some seconds of the new video resolution) | ||
297 | this.addTorrentDelay = setTimeout(() => { | ||
298 | // We don't need the fake renderer anymore | ||
299 | this.destroyFakeRenderer() | ||
300 | |||
301 | const paused = this.player.paused() | ||
302 | |||
303 | this.flushVideoFile(previousVideoFile) | ||
304 | |||
305 | // Update progress bar (just for the UI), do not wait rendering | ||
306 | if (options.seek) this.player.currentTime(options.seek) | ||
307 | |||
308 | const renderVideoOptions = { autoplay: false, controls: true } | ||
309 | renderVideo(torrent.files[0], this.playerElement, renderVideoOptions, (err, renderer) => { | ||
310 | this.renderer = renderer | ||
311 | |||
312 | if (err) return this.fallbackToHttp(options, done) | ||
313 | |||
314 | return this.tryToPlay(err => { | ||
315 | if (err) return done(err) | ||
316 | |||
317 | if (options.seek) this.seek(options.seek) | ||
318 | if (options.forcePlay === false && paused === true) this.player.pause() | ||
319 | |||
320 | return done() | ||
321 | }) | ||
322 | }) | ||
323 | }, options.delay || 0) | ||
324 | }) | ||
325 | |||
326 | this.torrent.on('error', (err: any) => logger.error(err)) | ||
327 | |||
328 | this.torrent.on('warning', (err: any) => { | ||
329 | // We don't support HTTP tracker but we don't care -> we use the web socket tracker | ||
330 | if (err.message.indexOf('Unsupported tracker protocol') !== -1) return | ||
331 | |||
332 | // Users don't care about issues with WebRTC, but developers do so log it in the console | ||
333 | if (err.message.indexOf('Ice connection failed') !== -1) { | ||
334 | logger.info(err) | ||
335 | return | ||
336 | } | ||
337 | |||
338 | // Magnet hash is not up to date with the torrent file, add directly the torrent file | ||
339 | if (err.message.indexOf('incorrect info hash') !== -1) { | ||
340 | logger.error('Incorrect info hash detected, falling back to torrent file.') | ||
341 | const newOptions = { forcePlay: true, seek: options.seek } | ||
342 | return this.addTorrent((this.torrent as any)['xs'], previousVideoFile, newOptions, done) | ||
343 | } | ||
344 | |||
345 | // Remote instance is down | ||
346 | if (err.message.indexOf('from xs param') !== -1) { | ||
347 | this.handleError(err) | ||
348 | } | ||
349 | |||
350 | logger.warn(err) | ||
351 | }) | ||
352 | } | ||
353 | |||
354 | private tryToPlay (done?: (err?: Error) => void) { | ||
355 | if (!done) done = function () { /* empty */ } | ||
356 | |||
357 | const playPromise = this.player.play() | ||
358 | if (playPromise !== undefined) { | ||
359 | return playPromise.then(() => done()) | ||
360 | .catch((err: Error) => { | ||
361 | if (err.message.includes('The play() request was interrupted by a call to pause()')) { | ||
362 | return | ||
363 | } | ||
364 | |||
365 | logger.warn(err) | ||
366 | this.player.pause() | ||
367 | this.player.posterImage.show() | ||
368 | this.player.removeClass('vjs-has-autoplay') | ||
369 | this.player.removeClass('vjs-has-big-play-button-clicked') | ||
370 | this.player.removeClass('vjs-playing-audio-only-content') | ||
371 | |||
372 | return done() | ||
373 | }) | ||
374 | } | ||
375 | |||
376 | return done() | ||
377 | } | ||
378 | |||
379 | private seek (time: number) { | ||
380 | this.player.currentTime(time) | ||
381 | this.player.handleTechSeeked_() | ||
382 | } | ||
383 | |||
384 | private getAppropriateFile (averageDownloadSpeed?: number): VideoFile { | ||
385 | if (this.videoFiles === undefined) return undefined | ||
386 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
387 | |||
388 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
389 | if (files.length === 0) return undefined | ||
390 | |||
391 | // Don't change the torrent if the player ended | ||
392 | if (this.torrent && this.torrent.progress === 1 && this.player.ended()) return this.currentVideoFile | ||
393 | |||
394 | if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed() | ||
395 | |||
396 | // Limit resolution according to player height | ||
397 | const playerHeight = this.playerElement.offsetHeight | ||
398 | |||
399 | // We take the first resolution just above the player height | ||
400 | // Example: player height is 530px, we want the 720p file instead of 480p | ||
401 | let maxResolution = files[0].resolution.id | ||
402 | for (let i = files.length - 1; i >= 0; i--) { | ||
403 | const resolutionId = files[i].resolution.id | ||
404 | if (resolutionId !== 0 && resolutionId >= playerHeight) { | ||
405 | maxResolution = resolutionId | ||
406 | break | ||
407 | } | ||
408 | } | ||
409 | |||
410 | // Filter videos we can play according to our screen resolution and bandwidth | ||
411 | const filteredFiles = files.filter(f => f.resolution.id <= maxResolution) | ||
412 | .filter(f => { | ||
413 | const fileBitrate = (f.size / this.videoDuration) | ||
414 | let threshold = fileBitrate | ||
415 | |||
416 | // If this is for a higher resolution or an initial load: add a margin | ||
417 | if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) { | ||
418 | threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100) | ||
419 | } | ||
420 | |||
421 | return averageDownloadSpeed > threshold | ||
422 | }) | ||
423 | |||
424 | // If the download speed is too bad, return the lowest resolution we have | ||
425 | if (filteredFiles.length === 0) return videoFileMinByResolution(files) | ||
426 | |||
427 | return videoFileMaxByResolution(filteredFiles) | ||
428 | } | ||
429 | |||
430 | private getAndSaveActualDownloadSpeed () { | ||
431 | const start = Math.max(this.downloadSpeeds.length - this.CONSTANTS.BANDWIDTH_AVERAGE_NUMBER_OF_VALUES, 0) | ||
432 | const lastDownloadSpeeds = this.downloadSpeeds.slice(start, this.downloadSpeeds.length) | ||
433 | if (lastDownloadSpeeds.length === 0) return -1 | ||
434 | |||
435 | const sum = lastDownloadSpeeds.reduce((a, b) => a + b) | ||
436 | const averageBandwidth = Math.round(sum / lastDownloadSpeeds.length) | ||
437 | |||
438 | // Save the average bandwidth for future use | ||
439 | saveAverageBandwidth(averageBandwidth) | ||
440 | |||
441 | return averageBandwidth | ||
442 | } | ||
443 | |||
444 | private initializePlayer () { | ||
445 | this.buildQualities() | ||
446 | |||
447 | if (this.videoFiles.length === 0) { | ||
448 | this.player.addClass('disabled') | ||
449 | return | ||
450 | } | ||
451 | |||
452 | if (this.autoplay !== false) { | ||
453 | this.player.posterImage.hide() | ||
454 | |||
455 | return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) | ||
456 | } | ||
457 | |||
458 | // Proxy first play | ||
459 | const oldPlay = this.player.play.bind(this.player); | ||
460 | (this.player as any).play = () => { | ||
461 | this.player.addClass('vjs-has-big-play-button-clicked') | ||
462 | this.player.play = oldPlay | ||
463 | |||
464 | this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) | ||
465 | } | ||
466 | } | ||
467 | |||
468 | private runAutoQualityScheduler () { | ||
469 | this.autoQualityInterval = setInterval(() => { | ||
470 | |||
471 | // Not initialized or in HTTP fallback | ||
472 | if (this.torrent === undefined || this.torrent === null) return | ||
473 | if (this.autoResolution === false) return | ||
474 | if (this.isAutoResolutionObservation === true) return | ||
475 | |||
476 | const file = this.getAppropriateFile() | ||
477 | let changeResolution = false | ||
478 | let changeResolutionDelay = 0 | ||
479 | |||
480 | // Lower resolution | ||
481 | if (this.isPlayerWaiting() && file.resolution.id < this.currentVideoFile.resolution.id) { | ||
482 | logger.info(`Downgrading automatically the resolution to: ${file.resolution.label}`) | ||
483 | changeResolution = true | ||
484 | } else if (file.resolution.id > this.currentVideoFile.resolution.id) { // Higher resolution | ||
485 | logger.info(`Upgrading automatically the resolution to: ${file.resolution.label}`) | ||
486 | changeResolution = true | ||
487 | changeResolutionDelay = this.CONSTANTS.AUTO_QUALITY_HIGHER_RESOLUTION_DELAY | ||
488 | } | ||
489 | |||
490 | if (changeResolution === true) { | ||
491 | this.updateEngineResolution(file.resolution.id, changeResolutionDelay) | ||
492 | |||
493 | // Wait some seconds in observation of our new resolution | ||
494 | this.isAutoResolutionObservation = true | ||
495 | |||
496 | this.qualityObservationTimer = setTimeout(() => { | ||
497 | this.isAutoResolutionObservation = false | ||
498 | }, this.CONSTANTS.AUTO_QUALITY_OBSERVATION_TIME) | ||
499 | } | ||
500 | }, this.CONSTANTS.AUTO_QUALITY_SCHEDULER) | ||
501 | } | ||
502 | |||
503 | private isPlayerWaiting () { | ||
504 | return this.player?.hasClass('vjs-waiting') | ||
505 | } | ||
506 | |||
507 | private runTorrentInfoScheduler () { | ||
508 | this.torrentInfoInterval = setInterval(() => { | ||
509 | // Not initialized yet | ||
510 | if (this.torrent === undefined) return | ||
511 | |||
512 | // Http fallback | ||
513 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) | ||
514 | |||
515 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | ||
516 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | ||
517 | |||
518 | return this.player.trigger('p2pInfo', { | ||
519 | source: 'webtorrent', | ||
520 | http: { | ||
521 | downloadSpeed: 0, | ||
522 | downloaded: 0 | ||
523 | }, | ||
524 | p2p: { | ||
525 | downloadSpeed: this.torrent.downloadSpeed, | ||
526 | numPeers: this.torrent.numPeers, | ||
527 | uploadSpeed: this.torrent.uploadSpeed, | ||
528 | downloaded: this.torrent.downloaded, | ||
529 | uploaded: this.torrent.uploaded | ||
530 | }, | ||
531 | bandwidthEstimate: this.webtorrent.downloadSpeed | ||
532 | } as PlayerNetworkInfo) | ||
533 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
534 | } | ||
535 | |||
536 | private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) { | ||
537 | const paused = this.player.paused() | ||
538 | |||
539 | this.disableAutoResolution() | ||
540 | |||
541 | this.flushVideoFile(this.currentVideoFile, true) | ||
542 | this.torrent = null | ||
543 | |||
544 | // Enable error display now this is our last fallback | ||
545 | this.player.one('error', () => this.player.peertube().displayFatalError()) | ||
546 | |||
547 | let httpUrl = this.currentVideoFile.fileUrl | ||
548 | |||
549 | if (this.videoFileToken) { | ||
550 | httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) | ||
551 | } | ||
552 | |||
553 | this.player.src = this.savePlayerSrcFunction | ||
554 | this.player.src(httpUrl) | ||
555 | |||
556 | this.selectAppropriateResolution(true) | ||
557 | |||
558 | // We changed the source, so reinit captions | ||
559 | this.player.trigger('sourcechange') | ||
560 | |||
561 | return this.tryToPlay(err => { | ||
562 | if (err && done) return done(err) | ||
563 | |||
564 | if (options.seek) this.seek(options.seek) | ||
565 | if (options.forcePlay === false && paused === true) this.player.pause() | ||
566 | |||
567 | if (done) return done() | ||
568 | }) | ||
569 | } | ||
570 | |||
571 | private handleError (err: Error | string) { | ||
572 | return this.player.trigger('customError', { err }) | ||
573 | } | ||
574 | |||
575 | private pickAverageVideoFile () { | ||
576 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
577 | |||
578 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
579 | return files[Math.floor(files.length / 2)] | ||
580 | } | ||
581 | |||
582 | private stopTorrent (torrent: WebTorrent.Torrent) { | ||
583 | torrent.pause() | ||
584 | // Pause does not remove actual peers (in particular the webseed peer) | ||
585 | torrent.removePeer((torrent as any)['ws']) | ||
586 | } | ||
587 | |||
588 | private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) { | ||
589 | this.destroyingFakeRenderer = false | ||
590 | |||
591 | const fakeVideoElem = document.createElement('video') | ||
592 | renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => { | ||
593 | this.fakeRenderer = renderer | ||
594 | |||
595 | // The renderer returns an error when we destroy it, so skip them | ||
596 | if (this.destroyingFakeRenderer === false && err) { | ||
597 | logger.error('Cannot render new torrent in fake video element.', err) | ||
598 | } | ||
599 | |||
600 | // Load the future file at the correct time (in delay MS - 2 seconds) | ||
601 | fakeVideoElem.currentTime = this.player.currentTime() + (delay - 2000) | ||
602 | }) | ||
603 | } | ||
604 | |||
605 | private destroyFakeRenderer () { | ||
606 | if (this.fakeRenderer) { | ||
607 | this.destroyingFakeRenderer = true | ||
608 | |||
609 | if (this.fakeRenderer.destroy) { | ||
610 | try { | ||
611 | this.fakeRenderer.destroy() | ||
612 | } catch (err) { | ||
613 | logger.info('Cannot destroy correctly fake renderer.', err) | ||
614 | } | ||
615 | } | ||
616 | this.fakeRenderer = undefined | ||
617 | } | ||
618 | } | ||
619 | |||
620 | private buildQualities () { | ||
621 | const resolutions: PeerTubeResolution[] = this.videoFiles.map(file => ({ | ||
622 | id: file.resolution.id, | ||
623 | label: this.buildQualityLabel(file), | ||
624 | height: file.resolution.id, | ||
625 | selected: false, | ||
626 | selectCallback: () => this.changeQuality(file.resolution.id) | ||
627 | })) | ||
628 | |||
629 | resolutions.push({ | ||
630 | id: -1, | ||
631 | label: this.player.localize('Auto'), | ||
632 | selected: true, | ||
633 | selectCallback: () => this.changeQuality(-1) | ||
634 | }) | ||
635 | |||
636 | this.player.peertubeResolutions().add(resolutions) | ||
637 | } | ||
638 | |||
639 | private buildQualityLabel (file: VideoFile) { | ||
640 | let label = file.resolution.label | ||
641 | |||
642 | if (file.fps && file.fps >= 50) { | ||
643 | label += file.fps | ||
644 | } | ||
645 | |||
646 | return label | ||
647 | } | ||
648 | |||
649 | private selectAppropriateResolution (byEngine: boolean) { | ||
650 | const resolution = this.autoResolution | ||
651 | ? -1 | ||
652 | : this.getCurrentResolutionId() | ||
653 | |||
654 | const autoResolutionChosen = this.autoResolution | ||
655 | ? this.getCurrentResolutionId() | ||
656 | : undefined | ||
657 | |||
658 | this.player.peertubeResolutions().select({ id: resolution, autoResolutionChosenId: autoResolutionChosen, byEngine }) | ||
659 | } | ||
660 | } | ||
661 | |||
662 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) | ||
663 | export { WebTorrentPlugin } | ||
diff --git a/client/src/assets/player/types/index.ts b/client/src/assets/player/types/index.ts index b73e0b3cb..4bf49f65c 100644 --- a/client/src/assets/player/types/index.ts +++ b/client/src/assets/player/types/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './manager-options' | 1 | export * from './peertube-player-options' |
2 | export * from './peertube-videojs-typings' | 2 | export * from './peertube-videojs-typings' |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/peertube-player-options.ts index a73341b4c..e1b8c7fab 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/peertube-player-options.ts | |||
@@ -1,101 +1,117 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | 1 | import { PluginsManager } from '@root-helpers/plugins-manager' |
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | 2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' |
3 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | ||
3 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' | 4 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' |
4 | 5 | ||
5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 6 | export type PlayerMode = 'web-video' | 'p2p-media-loader' |
6 | 7 | ||
7 | export type WebtorrentOptions = { | 8 | export type PeerTubePlayerContructorOptions = { |
8 | videoFiles: VideoFile[] | 9 | playerElement: () => HTMLVideoElement |
9 | } | ||
10 | 10 | ||
11 | export type P2PMediaLoaderOptions = { | 11 | controls: boolean |
12 | playlistUrl: string | 12 | controlBar: boolean |
13 | segmentsSha256Url: string | ||
14 | trackerAnnounce: string[] | ||
15 | redundancyBaseUrls: string[] | ||
16 | videoFiles: VideoFile[] | ||
17 | } | ||
18 | 13 | ||
19 | export interface CustomizationOptions { | 14 | muted: boolean |
20 | startTime: number | string | 15 | loop: boolean |
21 | stopTime: number | string | ||
22 | 16 | ||
23 | controls?: boolean | 17 | peertubeLink: () => boolean |
24 | controlBar?: boolean | ||
25 | 18 | ||
26 | muted?: boolean | 19 | playbackRate?: number | string |
27 | loop?: boolean | ||
28 | subtitle?: string | ||
29 | resume?: string | ||
30 | 20 | ||
31 | peertubeLink: boolean | 21 | enableHotkeys: boolean |
22 | inactivityTimeout: number | ||
32 | 23 | ||
33 | playbackRate?: number | string | 24 | videoViewIntervalMs: number |
34 | } | ||
35 | 25 | ||
36 | export interface CommonOptions extends CustomizationOptions { | 26 | instanceName: string |
37 | playerElement: HTMLVideoElement | ||
38 | onPlayerElementChange: (element: HTMLVideoElement) => void | ||
39 | 27 | ||
40 | autoplay: boolean | 28 | theaterButton: boolean |
41 | forceAutoplay: boolean | ||
42 | 29 | ||
43 | p2pEnabled: boolean | 30 | authorizationHeader: () => string |
44 | 31 | ||
45 | nextVideo?: () => void | 32 | metricsUrl: string |
46 | hasNextVideo?: () => boolean | 33 | serverUrl: string |
47 | 34 | ||
48 | previousVideo?: () => void | 35 | errorNotifier: (message: string) => void |
49 | hasPreviousVideo?: () => boolean | ||
50 | 36 | ||
51 | playlist?: PlaylistPluginOptions | 37 | // Current web browser language |
38 | language: string | ||
52 | 39 | ||
53 | videoDuration: number | 40 | pluginsManager: PluginsManager |
54 | enableHotkeys: boolean | 41 | } |
55 | inactivityTimeout: number | ||
56 | poster: string | ||
57 | 42 | ||
58 | videoViewIntervalMs: number | 43 | export type PeerTubePlayerLoadOptions = { |
44 | mode: PlayerMode | ||
59 | 45 | ||
60 | instanceName: string | 46 | startTime?: number | string |
47 | stopTime?: number | string | ||
61 | 48 | ||
62 | theaterButton: boolean | 49 | autoplay: boolean |
63 | captions: boolean | 50 | forceAutoplay: boolean |
64 | 51 | ||
52 | poster: string | ||
53 | subtitle?: string | ||
65 | videoViewUrl: string | 54 | videoViewUrl: string |
66 | authorizationHeader?: () => string | ||
67 | |||
68 | metricsUrl: string | ||
69 | 55 | ||
70 | embedUrl: string | 56 | embedUrl: string |
71 | embedTitle: string | 57 | embedTitle: string |
72 | 58 | ||
73 | isLive: boolean | 59 | isLive: boolean |
60 | |||
74 | liveOptions?: { | 61 | liveOptions?: { |
75 | latencyMode: LiveVideoLatencyMode | 62 | latencyMode: LiveVideoLatencyMode |
76 | } | 63 | } |
77 | 64 | ||
78 | language?: string | ||
79 | |||
80 | videoCaptions: VideoJSCaption[] | 65 | videoCaptions: VideoJSCaption[] |
81 | storyboard: VideoJSStoryboard | 66 | storyboard: VideoJSStoryboard |
82 | 67 | ||
83 | videoUUID: string | 68 | videoUUID: string |
84 | videoShortUUID: string | 69 | videoShortUUID: string |
85 | 70 | ||
86 | serverUrl: string | 71 | duration: number |
72 | |||
87 | requiresUserAuth: boolean | 73 | requiresUserAuth: boolean |
88 | videoFileToken: () => string | 74 | videoFileToken: () => string |
89 | requiresPassword: boolean | 75 | requiresPassword: boolean |
90 | videoPassword: () => string | 76 | videoPassword: () => string |
91 | 77 | ||
92 | errorNotifier: (message: string) => void | 78 | nextVideo: { |
79 | enabled: boolean | ||
80 | getVideoTitle: () => string | ||
81 | handler?: () => void | ||
82 | displayControlBarButton: boolean | ||
83 | } | ||
84 | |||
85 | previousVideo: { | ||
86 | enabled: boolean | ||
87 | handler?: () => void | ||
88 | displayControlBarButton: boolean | ||
89 | } | ||
90 | |||
91 | upnext?: { | ||
92 | isEnabled: () => boolean | ||
93 | isSuspended: (player: videojs.VideoJsPlayer) => boolean | ||
94 | timeout: number | ||
95 | } | ||
96 | |||
97 | dock?: PeerTubeDockPluginOptions | ||
98 | |||
99 | playlist?: PlaylistPluginOptions | ||
100 | |||
101 | p2pEnabled: boolean | ||
102 | |||
103 | hls?: HLSOptions | ||
104 | webVideo?: WebVideoOptions | ||
93 | } | 105 | } |
94 | 106 | ||
95 | export type PeertubePlayerManagerOptions = { | 107 | export type WebVideoOptions = { |
96 | common: CommonOptions | 108 | videoFiles: VideoFile[] |
97 | webtorrent: WebtorrentOptions | 109 | } |
98 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
99 | 110 | ||
100 | pluginsManager: PluginsManager | 111 | export type HLSOptions = { |
112 | playlistUrl: string | ||
113 | segmentsSha256Url: string | ||
114 | trackerAnnounce: string[] | ||
115 | redundancyBaseUrls: string[] | ||
116 | videoFiles: VideoFile[] | ||
101 | } | 117 | } |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index 30d2b287f..f10fc03a8 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -2,8 +2,11 @@ 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 '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { BezelsPlugin } from '../shared/bezels/bezels-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | 6 | import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin' |
7 | import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | ||
8 | import { HotkeysOptions, PeerTubeHotkeysPlugin } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
9 | import { PeerTubeMobilePlugin } from '../shared/mobile/peertube-mobile-plugin' | ||
7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 10 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 11 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 12 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -12,9 +15,10 @@ import { PlaylistPlugin } from '../shared/playlist/playlist-plugin' | |||
12 | import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin' | 15 | import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin' |
13 | import { StatsCardOptions } from '../shared/stats/stats-card' | 16 | import { StatsCardOptions } from '../shared/stats/stats-card' |
14 | import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin' | 17 | import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin' |
15 | import { EndCardOptions } from '../shared/upnext/end-card' | 18 | import { UpNextPlugin } from '../shared/upnext/upnext-plugin' |
16 | import { WebTorrentPlugin } from '../shared/webtorrent/webtorrent-plugin' | 19 | import { WebVideoPlugin } from '../shared/web-video/web-video-plugin' |
17 | import { PlayerMode } from './manager-options' | 20 | import { PlayerMode } from './peertube-player-options' |
21 | import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator' | ||
18 | 22 | ||
19 | declare module 'video.js' { | 23 | declare module 'video.js' { |
20 | 24 | ||
@@ -31,35 +35,36 @@ declare module 'video.js' { | |||
31 | 35 | ||
32 | handleTechSeeked_ (): void | 36 | handleTechSeeked_ (): void |
33 | 37 | ||
38 | textTracks (): TextTrackList & { | ||
39 | tracks_: (TextTrack & { id: string, label: string, src: string })[] | ||
40 | } | ||
41 | |||
34 | // Plugins | 42 | // Plugins |
35 | 43 | ||
36 | peertube (): PeerTubePlugin | 44 | peertube (): PeerTubePlugin |
37 | 45 | ||
38 | webtorrent (): WebTorrentPlugin | 46 | webVideo (options?: any): WebVideoPlugin |
39 | 47 | ||
40 | p2pMediaLoader (): P2pMediaLoaderPlugin | 48 | p2pMediaLoader (options?: any): P2pMediaLoaderPlugin |
49 | hlsjs (options?: any): any | ||
41 | 50 | ||
42 | peertubeResolutions (): PeerTubeResolutionsPlugin | 51 | peertubeResolutions (): PeerTubeResolutionsPlugin |
43 | 52 | ||
44 | contextmenuUI (options: any): any | 53 | contextmenuUI (options?: any): any |
45 | 54 | ||
46 | bezels (): void | 55 | bezels (): BezelsPlugin |
47 | peertubeMobile (): void | 56 | peertubeMobile (): PeerTubeMobilePlugin |
48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void | 57 | peerTubeHotkeysPlugin (options?: HotkeysOptions): PeerTubeHotkeysPlugin |
49 | 58 | ||
50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 59 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
51 | 60 | ||
52 | storyboard (options: StoryboardOptions): void | 61 | storyboard (options?: StoryboardOptions): StoryboardPlugin |
53 | |||
54 | textTracks (): TextTrackList & { | ||
55 | tracks_: (TextTrack & { id: string, label: string, src: string })[] | ||
56 | } | ||
57 | 62 | ||
58 | peertubeDock (options: PeerTubeDockPluginOptions): void | 63 | peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin |
59 | 64 | ||
60 | upnext (options: Partial<EndCardOptions>): void | 65 | upnext (options?: UpNextPluginOptions): UpNextPlugin |
61 | 66 | ||
62 | playlist (): PlaylistPlugin | 67 | playlist (options?: PlaylistPluginOptions): PlaylistPlugin |
63 | } | 68 | } |
64 | } | 69 | } |
65 | 70 | ||
@@ -99,32 +104,28 @@ type VideoJSStoryboard = { | |||
99 | } | 104 | } |
100 | 105 | ||
101 | type PeerTubePluginOptions = { | 106 | type PeerTubePluginOptions = { |
102 | mode: PlayerMode | 107 | hasAutoplay: () => videojs.Autoplay |
103 | 108 | ||
104 | autoplay: videojs.Autoplay | 109 | videoViewUrl: () => string |
105 | videoDuration: number | 110 | videoViewIntervalMs: number |
106 | 111 | ||
107 | videoViewUrl: string | ||
108 | authorizationHeader?: () => string | 112 | authorizationHeader?: () => string |
109 | 113 | ||
110 | subtitle?: string | 114 | videoDuration: () => number |
111 | 115 | ||
112 | videoCaptions: VideoJSCaption[] | 116 | startTime: () => number | string |
113 | 117 | stopTime: () => number | string | |
114 | startTime: number | string | ||
115 | stopTime: number | string | ||
116 | 118 | ||
117 | isLive: boolean | 119 | videoCaptions: () => VideoJSCaption[] |
118 | 120 | isLive: () => boolean | |
119 | videoUUID: string | 121 | videoUUID: () => string |
120 | 122 | subtitle: () => string | |
121 | videoViewIntervalMs: number | ||
122 | } | 123 | } |
123 | 124 | ||
124 | type MetricsPluginOptions = { | 125 | type MetricsPluginOptions = { |
125 | mode: PlayerMode | 126 | mode: () => PlayerMode |
126 | metricsUrl: string | 127 | metricsUrl: () => string |
127 | videoUUID: string | 128 | videoUUID: () => string |
128 | } | 129 | } |
129 | 130 | ||
130 | type StoryboardOptions = { | 131 | type StoryboardOptions = { |
@@ -144,37 +145,36 @@ type PlaylistPluginOptions = { | |||
144 | onItemClicked: (element: VideoPlaylistElement) => void | 145 | onItemClicked: (element: VideoPlaylistElement) => void |
145 | } | 146 | } |
146 | 147 | ||
148 | type UpNextPluginOptions = { | ||
149 | timeout: number | ||
150 | |||
151 | next: () => void | ||
152 | getTitle: () => string | ||
153 | isDisplayed: () => boolean | ||
154 | isSuspended: () => boolean | ||
155 | } | ||
156 | |||
147 | type NextPreviousVideoButtonOptions = { | 157 | type NextPreviousVideoButtonOptions = { |
148 | type: 'next' | 'previous' | 158 | type: 'next' | 'previous' |
149 | handler: () => void | 159 | handler?: () => void |
160 | isDisplayed: () => boolean | ||
150 | isDisabled: () => boolean | 161 | isDisabled: () => boolean |
151 | } | 162 | } |
152 | 163 | ||
153 | type PeerTubeLinkButtonOptions = { | 164 | type PeerTubeLinkButtonOptions = { |
154 | shortUUID: string | 165 | isDisplayed: () => boolean |
166 | shortUUID: () => string | ||
155 | instanceName: string | 167 | instanceName: string |
156 | } | 168 | } |
157 | 169 | ||
158 | type PeerTubeP2PInfoButtonOptions = { | 170 | type TheaterButtonOptions = { |
159 | p2pEnabled: boolean | 171 | isDisplayed: () => boolean |
160 | } | 172 | } |
161 | 173 | ||
162 | type WebtorrentPluginOptions = { | 174 | type WebVideoPluginOptions = { |
163 | playerElement: HTMLVideoElement | ||
164 | |||
165 | autoplay: videojs.Autoplay | ||
166 | videoDuration: number | ||
167 | |||
168 | videoFiles: VideoFile[] | 175 | videoFiles: VideoFile[] |
169 | |||
170 | startTime: number | string | 176 | startTime: number | string |
171 | |||
172 | playerRefusedP2P: boolean | ||
173 | |||
174 | requiresUserAuth: boolean | ||
175 | videoFileToken: () => string | 177 | videoFileToken: () => string |
176 | |||
177 | buildWebSeedUrls: (file: VideoFile) => string[] | ||
178 | } | 178 | } |
179 | 179 | ||
180 | type P2PMediaLoaderPluginOptions = { | 180 | type P2PMediaLoaderPluginOptions = { |
@@ -182,9 +182,8 @@ type P2PMediaLoaderPluginOptions = { | |||
182 | type: string | 182 | type: string |
183 | src: string | 183 | src: string |
184 | 184 | ||
185 | startTime: number | string | ||
186 | |||
187 | loader: P2PMediaLoader | 185 | loader: P2PMediaLoader |
186 | segmentValidator: SegmentValidator | ||
188 | 187 | ||
189 | requiresUserAuth: boolean | 188 | requiresUserAuth: boolean |
190 | videoFileToken: () => string | 189 | videoFileToken: () => string |
@@ -192,6 +191,8 @@ type P2PMediaLoaderPluginOptions = { | |||
192 | 191 | ||
193 | export type P2PMediaLoader = { | 192 | export type P2PMediaLoader = { |
194 | getEngine(): Engine | 193 | getEngine(): Engine |
194 | |||
195 | destroy: () => void | ||
195 | } | 196 | } |
196 | 197 | ||
197 | type VideoJSPluginOptions = { | 198 | type VideoJSPluginOptions = { |
@@ -200,7 +201,7 @@ type VideoJSPluginOptions = { | |||
200 | peertube: PeerTubePluginOptions | 201 | peertube: PeerTubePluginOptions |
201 | metrics: MetricsPluginOptions | 202 | metrics: MetricsPluginOptions |
202 | 203 | ||
203 | webtorrent?: WebtorrentPluginOptions | 204 | webVideo?: WebVideoPluginOptions |
204 | 205 | ||
205 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | 206 | p2pMediaLoader?: P2PMediaLoaderPluginOptions |
206 | } | 207 | } |
@@ -227,14 +228,14 @@ type AutoResolutionUpdateData = { | |||
227 | } | 228 | } |
228 | 229 | ||
229 | type PlayerNetworkInfo = { | 230 | type PlayerNetworkInfo = { |
230 | source: 'webtorrent' | 'p2p-media-loader' | 231 | source: 'web-video' | 'p2p-media-loader' |
231 | 232 | ||
232 | http: { | 233 | http: { |
233 | downloadSpeed: number | 234 | downloadSpeed?: number |
234 | downloaded: number | 235 | downloaded: number |
235 | } | 236 | } |
236 | 237 | ||
237 | p2p: { | 238 | p2p?: { |
238 | downloadSpeed: number | 239 | downloadSpeed: number |
239 | uploadSpeed: number | 240 | uploadSpeed: number |
240 | downloaded: number | 241 | downloaded: number |
@@ -243,7 +244,7 @@ type PlayerNetworkInfo = { | |||
243 | } | 244 | } |
244 | 245 | ||
245 | // In bytes | 246 | // In bytes |
246 | bandwidthEstimate: number | 247 | bandwidthEstimate?: number |
247 | } | 248 | } |
248 | 249 | ||
249 | type PlaylistItemOptions = { | 250 | type PlaylistItemOptions = { |
@@ -254,6 +255,7 @@ type PlaylistItemOptions = { | |||
254 | 255 | ||
255 | export { | 256 | export { |
256 | PlayerNetworkInfo, | 257 | PlayerNetworkInfo, |
258 | TheaterButtonOptions, | ||
257 | VideoJSStoryboard, | 259 | VideoJSStoryboard, |
258 | PlaylistItemOptions, | 260 | PlaylistItemOptions, |
259 | NextPreviousVideoButtonOptions, | 261 | NextPreviousVideoButtonOptions, |
@@ -263,12 +265,12 @@ export { | |||
263 | MetricsPluginOptions, | 265 | MetricsPluginOptions, |
264 | VideoJSCaption, | 266 | VideoJSCaption, |
265 | PeerTubePluginOptions, | 267 | PeerTubePluginOptions, |
266 | WebtorrentPluginOptions, | 268 | WebVideoPluginOptions, |
267 | P2PMediaLoaderPluginOptions, | 269 | P2PMediaLoaderPluginOptions, |
268 | PeerTubeResolution, | 270 | PeerTubeResolution, |
269 | VideoJSPluginOptions, | 271 | VideoJSPluginOptions, |
272 | UpNextPluginOptions, | ||
270 | LoadedQualityData, | 273 | LoadedQualityData, |
271 | StoryboardOptions, | 274 | StoryboardOptions, |
272 | PeerTubeLinkButtonOptions, | 275 | PeerTubeLinkButtonOptions |
273 | PeerTubeP2PInfoButtonOptions | ||
274 | } | 276 | } |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 02d5fa169..09a75e2fd 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -3,20 +3,6 @@ | |||
3 | @use '_mixins' as *; | 3 | @use '_mixins' as *; |
4 | @use './_player-variables' as *; | 4 | @use './_player-variables' as *; |
5 | 5 | ||
6 | // Like the time tooltip | ||
7 | .video-js .vjs-progress-holder .vjs-storyboard-sprite-placeholder { | ||
8 | display: none; | ||
9 | } | ||
10 | |||
11 | .video-js .vjs-progress-control:hover .vjs-storyboard-sprite-placeholder, | ||
12 | .video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-storyboard-sprite-placeholder { | ||
13 | display: block; | ||
14 | |||
15 | // Ensure that we maintain a font-size of ~10px. | ||
16 | font-size: 0.6em; | ||
17 | visibility: visible; | ||
18 | } | ||
19 | |||
20 | .video-js.vjs-peertube-skin .vjs-control-bar { | 6 | .video-js.vjs-peertube-skin .vjs-control-bar { |
21 | z-index: 100; | 7 | z-index: 100; |
22 | 8 | ||
@@ -26,11 +12,8 @@ | |||
26 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); | 12 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); |
27 | transition: visibility 0.3s, opacity 0.3s !important; | 13 | transition: visibility 0.3s, opacity 0.3s !important; |
28 | 14 | ||
29 | &.control-bar-hidden { | 15 | > button:not(.vjs-hidden):first-child, |
30 | display: none !important; | 16 | > button.vjs-hidden + button:not(.vjs-hidden) { |
31 | } | ||
32 | |||
33 | > button:first-child { | ||
34 | @include margin-left($first-control-bar-element-margin-left); | 17 | @include margin-left($first-control-bar-element-margin-left); |
35 | } | 18 | } |
36 | 19 | ||
@@ -167,7 +150,7 @@ | |||
167 | } | 150 | } |
168 | } | 151 | } |
169 | 152 | ||
170 | .vjs-live-control { | 153 | .vjs-pt-live-control { |
171 | padding: 5px 7px; | 154 | padding: 5px 7px; |
172 | border-radius: 3px; | 155 | border-radius: 3px; |
173 | height: fit-content; | 156 | height: fit-content; |
@@ -245,6 +228,7 @@ | |||
245 | .vjs-next-video, | 228 | .vjs-next-video, |
246 | .vjs-previous-video { | 229 | .vjs-previous-video { |
247 | width: $control-bar-button-width - 4px; | 230 | width: $control-bar-button-width - 4px; |
231 | cursor: pointer; | ||
248 | 232 | ||
249 | &.vjs-disabled { | 233 | &.vjs-disabled { |
250 | cursor: default; | 234 | cursor: default; |
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss index 5d0307d95..4bfd67a26 100644 --- a/client/src/sass/player/index.scss +++ b/client/src/sass/player/index.scss | |||
@@ -10,3 +10,4 @@ | |||
10 | @use './playlist'; | 10 | @use './playlist'; |
11 | @use './stats'; | 11 | @use './stats'; |
12 | @use './offline-notification'; | 12 | @use './offline-notification'; |
13 | @use './storyboard.scss'; | ||
diff --git a/client/src/sass/player/mobile.scss b/client/src/sass/player/mobile.scss index d150c54ee..b0019d2c9 100644 --- a/client/src/sass/player/mobile.scss +++ b/client/src/sass/player/mobile.scss | |||
@@ -170,7 +170,8 @@ | |||
170 | } | 170 | } |
171 | } | 171 | } |
172 | 172 | ||
173 | &.vjs-scrubbing { | 173 | &.vjs-scrubbing, |
174 | &.vjs-mobile-sliding { | ||
174 | .vjs-mobile-buttons-overlay { | 175 | .vjs-mobile-buttons-overlay { |
175 | display: none; | 176 | display: none; |
176 | } | 177 | } |
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index 4df8dbaf0..572ae7050 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -84,7 +84,9 @@ body { | |||
84 | } | 84 | } |
85 | 85 | ||
86 | // Do not display poster when video is starting | 86 | // Do not display poster when video is starting |
87 | &.vjs-has-autoplay:not(.vjs-has-started) { | 87 | // Or if we change resolution manually |
88 | &.vjs-has-autoplay:not(.vjs-has-started), | ||
89 | &.vjs-updating-resolution { | ||
88 | .vjs-poster { | 90 | .vjs-poster { |
89 | opacity: 0; | 91 | opacity: 0; |
90 | visibility: hidden; | 92 | visibility: hidden; |
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss index d2346c126..369c827f7 100644 --- a/client/src/sass/player/settings-menu.scss +++ b/client/src/sass/player/settings-menu.scss | |||
@@ -75,6 +75,7 @@ $setting-transition-easing: ease-out; | |||
75 | > .vjs-menu { | 75 | > .vjs-menu { |
76 | flex: 1; | 76 | flex: 1; |
77 | min-width: 200px; | 77 | min-width: 200px; |
78 | padding: 5px 0; | ||
78 | } | 79 | } |
79 | 80 | ||
80 | > .vjs-menu, | 81 | > .vjs-menu, |
@@ -90,14 +91,6 @@ $setting-transition-easing: ease-out; | |||
90 | background-color: rgba(255, 255, 255, 0.2); | 91 | background-color: rgba(255, 255, 255, 0.2); |
91 | } | 92 | } |
92 | 93 | ||
93 | &:first-child { | ||
94 | margin-top: 5px; | ||
95 | } | ||
96 | |||
97 | &:last-child { | ||
98 | margin-bottom: 5px; | ||
99 | } | ||
100 | |||
101 | &.disabled { | 94 | &.disabled { |
102 | opacity: 0.5; | 95 | opacity: 0.5; |
103 | cursor: default !important; | 96 | cursor: default !important; |
diff --git a/client/src/sass/player/storyboard.scss b/client/src/sass/player/storyboard.scss new file mode 100644 index 000000000..c80d1b59d --- /dev/null +++ b/client/src/sass/player/storyboard.scss | |||
@@ -0,0 +1,26 @@ | |||
1 | @use 'sass:math'; | ||
2 | @use '_variables' as *; | ||
3 | @use '_mixins' as *; | ||
4 | @use './_player-variables' as *; | ||
5 | |||
6 | // Like the time tooltip | ||
7 | .video-js .vjs-progress-holder .vjs-storyboard-sprite-placeholder { | ||
8 | display: none; | ||
9 | } | ||
10 | |||
11 | .video-js .vjs-progress-control:hover .vjs-storyboard-sprite-placeholder, | ||
12 | .video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-storyboard-sprite-placeholder { | ||
13 | display: block; | ||
14 | |||
15 | // Ensure that we maintain a font-size of ~10px. | ||
16 | font-size: 0.6em; | ||
17 | visibility: visible; | ||
18 | } | ||
19 | |||
20 | .video-js.vjs-settings-dialog-opened { | ||
21 | .vjs-storyboard-sprite-placeholder, | ||
22 | .vjs-time-tooltip, | ||
23 | .vjs-mouse-display { | ||
24 | display: none !important; | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/shims/http.ts b/client/src/shims/http.ts deleted file mode 100644 index 1b1767aab..000000000 --- a/client/src/shims/http.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('stream-http') | ||
diff --git a/client/src/shims/https.ts b/client/src/shims/https.ts deleted file mode 100644 index f5ef70430..000000000 --- a/client/src/shims/https.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('https-browserify') | ||
diff --git a/client/src/shims/stream.ts b/client/src/shims/stream.ts deleted file mode 100644 index 977fd05a0..000000000 --- a/client/src/shims/stream.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('stream-browserify') | ||
diff --git a/client/src/standalone/player/.npmignore b/client/src/standalone/embed-player-api/.npmignore index 870b6315b..870b6315b 100644 --- a/client/src/standalone/player/.npmignore +++ b/client/src/standalone/embed-player-api/.npmignore | |||
diff --git a/client/src/standalone/player/README.md b/client/src/standalone/embed-player-api/README.md index 7b47e8f02..7b47e8f02 100644 --- a/client/src/standalone/player/README.md +++ b/client/src/standalone/embed-player-api/README.md | |||
diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/embed-player-api/definitions.ts index 495f1a98c..495f1a98c 100644 --- a/client/src/standalone/player/definitions.ts +++ b/client/src/standalone/embed-player-api/definitions.ts | |||
diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/embed-player-api/events.ts index 77d21c78c..77d21c78c 100644 --- a/client/src/standalone/player/events.ts +++ b/client/src/standalone/embed-player-api/events.ts | |||
diff --git a/client/src/standalone/player/package.json b/client/src/standalone/embed-player-api/package.json index b549fbf52..b549fbf52 100644 --- a/client/src/standalone/player/package.json +++ b/client/src/standalone/embed-player-api/package.json | |||
diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/embed-player-api/player.ts index 75487258b..75487258b 100644 --- a/client/src/standalone/player/player.ts +++ b/client/src/standalone/embed-player-api/player.ts | |||
diff --git a/client/src/standalone/player/tsconfig.json b/client/src/standalone/embed-player-api/tsconfig.json index eecc63dfb..eecc63dfb 100644 --- a/client/src/standalone/player/tsconfig.json +++ b/client/src/standalone/embed-player-api/tsconfig.json | |||
diff --git a/client/src/standalone/player/webpack.config.js b/client/src/standalone/embed-player-api/webpack.config.js index 48d350edf..48d350edf 100644 --- a/client/src/standalone/player/webpack.config.js +++ b/client/src/standalone/embed-player-api/webpack.config.js | |||
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts index a99f1edae..6227c378e 100644 --- a/client/src/standalone/videos/embed-api.ts +++ b/client/src/standalone/videos/embed-api.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | import * as Channel from 'jschannel' | 2 | import * as Channel from 'jschannel' |
3 | import { logger } from '../../root-helpers' | 3 | import { logger } from '../../root-helpers' |
4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../player/definitions' | 4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../embed-player-api/definitions' |
5 | import { PeerTubeEmbed } from './embed' | 5 | import { PeerTubeEmbed } from './embed' |
6 | 6 | ||
7 | /** | 7 | /** |
@@ -72,15 +72,12 @@ export class PeerTubeEmbedApi { | |||
72 | private setResolution (resolutionId: number) { | 72 | private setResolution (resolutionId: number) { |
73 | logger.info(`Set resolution ${resolutionId}`) | 73 | logger.info(`Set resolution ${resolutionId}`) |
74 | 74 | ||
75 | if (this.isWebtorrent()) { | 75 | if (this.isWebVideo() && resolutionId === -1) { |
76 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return | 76 | logger.error('Auto resolution cannot be set in web video player mode') |
77 | |||
78 | this.embed.player.webtorrent().changeQuality(resolutionId) | ||
79 | |||
80 | return | 77 | return |
81 | } | 78 | } |
82 | 79 | ||
83 | this.embed.player.p2pMediaLoader().getHLSJS().currentLevel = resolutionId | 80 | this.embed.player.peertubeResolutions().select({ id: resolutionId, fireCallback: true }) |
84 | } | 81 | } |
85 | 82 | ||
86 | private getCaptions (): PeerTubeTextTrack[] { | 83 | private getCaptions (): PeerTubeTextTrack[] { |
@@ -152,8 +149,8 @@ export class PeerTubeEmbedApi { | |||
152 | // --------------------------------------------------------------------------- | 149 | // --------------------------------------------------------------------------- |
153 | 150 | ||
154 | // PeerTube specific capabilities | 151 | // PeerTube specific capabilities |
155 | this.embed.player.peertubeResolutions().on('resolutionsAdded', () => this.loadResolutions()) | 152 | this.embed.player.peertubeResolutions().on('resolutions-added', () => this.loadResolutions()) |
156 | this.embed.player.peertubeResolutions().on('resolutionChanged', () => this.loadResolutions()) | 153 | this.embed.player.peertubeResolutions().on('resolutions-changed', () => this.loadResolutions()) |
157 | 154 | ||
158 | this.loadResolutions() | 155 | this.loadResolutions() |
159 | 156 | ||
@@ -193,7 +190,7 @@ export class PeerTubeEmbedApi { | |||
193 | }) | 190 | }) |
194 | } | 191 | } |
195 | 192 | ||
196 | private isWebtorrent () { | 193 | private isWebVideo () { |
197 | return !!this.embed.player.webtorrent | 194 | return !!this.embed.player.webVideo |
198 | } | 195 | } |
199 | } | 196 | } |
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index a74bb4cee..e2dc02b60 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -44,11 +44,11 @@ | |||
44 | <div id="video-password-block"> | 44 | <div id="video-password-block"> |
45 | <!-- eslint-disable-next-line @angular-eslint/template/elements-content --> | 45 | <!-- eslint-disable-next-line @angular-eslint/template/elements-content --> |
46 | <h1 id="video-password-title"></h1> | 46 | <h1 id="video-password-title"></h1> |
47 | 47 | ||
48 | <div id="video-password-content"></div> | 48 | <div id="video-password-content"></div> |
49 | 49 | ||
50 | <form id="video-password-form"> | 50 | <form id="video-password-form"> |
51 | <input type="password" id="video-password-input" name="video-password" required> | 51 | <input type="password" id="video-password-input" name="video-password" autocomplete="user-password" required> |
52 | <button type="submit" id="video-password-submit"> </button> | 52 | <button type="submit" id="video-password-submit"> </button> |
53 | </form> | 53 | </form> |
54 | 54 | ||
@@ -60,8 +60,6 @@ | |||
60 | 60 | ||
61 | <div id="video-wrapper"></div> | 61 | <div id="video-wrapper"></div> |
62 | 62 | ||
63 | <div id="placeholder-preview"></div> | ||
64 | |||
65 | <script type="text/javascript"> | 63 | <script type="text/javascript"> |
66 | // Can be called in embed.ts | 64 | // Can be called in embed.ts |
67 | window.displayIncompatibleBrowser = function () { | 65 | window.displayIncompatibleBrowser = function () { |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 6e37ce193..78b812ffd 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -3,7 +3,6 @@ import '../../assets/player/shared/dock/peertube-dock-component' | |||
3 | import '../../assets/player/shared/dock/peertube-dock-plugin' | 3 | import '../../assets/player/shared/dock/peertube-dock-plugin' |
4 | import { PeerTubeServerError } from 'src/types' | 4 | import { PeerTubeServerError } from 'src/types' |
5 | import videojs from 'video.js' | 5 | import videojs from 'video.js' |
6 | import { peertubeTranslate } from '../../../../shared/core-utils/i18n' | ||
7 | import { | 6 | import { |
8 | HTMLServerConfig, | 7 | HTMLServerConfig, |
9 | ResultList, | 8 | ResultList, |
@@ -13,7 +12,7 @@ import { | |||
13 | VideoPlaylistElement, | 12 | VideoPlaylistElement, |
14 | VideoState | 13 | VideoState |
15 | } from '../../../../shared/models' | 14 | } from '../../../../shared/models' |
16 | import { PeertubePlayerManager } from '../../assets/player' | 15 | import { PeerTubePlayer } from '../../assets/player/peertube-player' |
17 | import { TranslationsManager } from '../../assets/player/translations-manager' | 16 | import { TranslationsManager } from '../../assets/player/translations-manager' |
18 | import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers' | 17 | import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers' |
19 | import { PeerTubeEmbedApi } from './embed-api' | 18 | import { PeerTubeEmbedApi } from './embed-api' |
@@ -21,7 +20,7 @@ import { | |||
21 | AuthHTTP, | 20 | AuthHTTP, |
22 | LiveManager, | 21 | LiveManager, |
23 | PeerTubePlugin, | 22 | PeerTubePlugin, |
24 | PlayerManagerOptions, | 23 | PlayerOptionsBuilder, |
25 | PlaylistFetcher, | 24 | PlaylistFetcher, |
26 | PlaylistTracker, | 25 | PlaylistTracker, |
27 | Translations, | 26 | Translations, |
@@ -36,17 +35,23 @@ export class PeerTubeEmbed { | |||
36 | config: HTMLServerConfig | 35 | config: HTMLServerConfig |
37 | 36 | ||
38 | private translationsPromise: Promise<{ [id: string]: string }> | 37 | private translationsPromise: Promise<{ [id: string]: string }> |
39 | private PeertubePlayerManagerModulePromise: Promise<any> | 38 | private PeerTubePlayerManagerModulePromise: Promise<any> |
40 | 39 | ||
41 | private readonly http: AuthHTTP | 40 | private readonly http: AuthHTTP |
42 | private readonly videoFetcher: VideoFetcher | 41 | private readonly videoFetcher: VideoFetcher |
43 | private readonly playlistFetcher: PlaylistFetcher | 42 | private readonly playlistFetcher: PlaylistFetcher |
44 | private readonly peertubePlugin: PeerTubePlugin | 43 | private readonly peertubePlugin: PeerTubePlugin |
45 | private readonly playerHTML: PlayerHTML | 44 | private readonly playerHTML: PlayerHTML |
46 | private readonly playerManagerOptions: PlayerManagerOptions | 45 | private readonly playerOptionsBuilder: PlayerOptionsBuilder |
47 | private readonly liveManager: LiveManager | 46 | private readonly liveManager: LiveManager |
48 | 47 | ||
48 | private peertubePlayer: PeerTubePlayer | ||
49 | |||
49 | private playlistTracker: PlaylistTracker | 50 | private playlistTracker: PlaylistTracker |
51 | |||
52 | private alreadyInitialized = false | ||
53 | private alreadyPlayed = false | ||
54 | |||
50 | private videoPassword: string | 55 | private videoPassword: string |
51 | private requiresPassword: boolean | 56 | private requiresPassword: boolean |
52 | 57 | ||
@@ -59,7 +64,7 @@ export class PeerTubeEmbed { | |||
59 | this.playlistFetcher = new PlaylistFetcher(this.http) | 64 | this.playlistFetcher = new PlaylistFetcher(this.http) |
60 | this.peertubePlugin = new PeerTubePlugin(this.http) | 65 | this.peertubePlugin = new PeerTubePlugin(this.http) |
61 | this.playerHTML = new PlayerHTML(videoWrapperId) | 66 | this.playerHTML = new PlayerHTML(videoWrapperId) |
62 | this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) | 67 | this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin) |
63 | this.liveManager = new LiveManager(this.playerHTML) | 68 | this.liveManager = new LiveManager(this.playerHTML) |
64 | this.requiresPassword = false | 69 | this.requiresPassword = false |
65 | 70 | ||
@@ -81,14 +86,14 @@ export class PeerTubeEmbed { | |||
81 | } | 86 | } |
82 | 87 | ||
83 | getScope () { | 88 | getScope () { |
84 | return this.playerManagerOptions.getScope() | 89 | return this.playerOptionsBuilder.getScope() |
85 | } | 90 | } |
86 | 91 | ||
87 | // --------------------------------------------------------------------------- | 92 | // --------------------------------------------------------------------------- |
88 | 93 | ||
89 | async init () { | 94 | async init () { |
90 | this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) | 95 | this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) |
91 | this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') | 96 | this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player') |
92 | 97 | ||
93 | // Issue when we parsed config from HTML, fallback to API | 98 | // Issue when we parsed config from HTML, fallback to API |
94 | if (!this.config) { | 99 | if (!this.config) { |
@@ -102,7 +107,7 @@ export class PeerTubeEmbed { | |||
102 | 107 | ||
103 | if (!videoId) return | 108 | if (!videoId) return |
104 | 109 | ||
105 | return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) | 110 | return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false }) |
106 | } | 111 | } |
107 | 112 | ||
108 | private async initPlaylist () { | 113 | private async initPlaylist () { |
@@ -137,7 +142,7 @@ export class PeerTubeEmbed { | |||
137 | } | 142 | } |
138 | 143 | ||
139 | private initializeApi () { | 144 | private initializeApi () { |
140 | if (this.playerManagerOptions.hasAPIEnabled()) { | 145 | if (this.playerOptionsBuilder.hasAPIEnabled()) { |
141 | if (this.api) { | 146 | if (this.api) { |
142 | this.api.reInit() | 147 | this.api.reInit() |
143 | return | 148 | return |
@@ -159,7 +164,7 @@ export class PeerTubeEmbed { | |||
159 | 164 | ||
160 | this.playlistTracker.setCurrentElement(next) | 165 | this.playlistTracker.setCurrentElement(next) |
161 | 166 | ||
162 | return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) | 167 | return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false }) |
163 | } | 168 | } |
164 | 169 | ||
165 | async playPreviousPlaylistVideo () { | 170 | async playPreviousPlaylistVideo () { |
@@ -171,7 +176,7 @@ export class PeerTubeEmbed { | |||
171 | 176 | ||
172 | this.playlistTracker.setCurrentElement(previous) | 177 | this.playlistTracker.setCurrentElement(previous) |
173 | 178 | ||
174 | await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) | 179 | await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false }) |
175 | } | 180 | } |
176 | 181 | ||
177 | getCurrentPlaylistPosition () { | 182 | getCurrentPlaylistPosition () { |
@@ -182,10 +187,9 @@ export class PeerTubeEmbed { | |||
182 | 187 | ||
183 | private async loadVideoAndBuildPlayer (options: { | 188 | private async loadVideoAndBuildPlayer (options: { |
184 | uuid: string | 189 | uuid: string |
185 | autoplayFromPreviousVideo: boolean | ||
186 | forceAutoplay: boolean | 190 | forceAutoplay: boolean |
187 | }) { | 191 | }) { |
188 | const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options | 192 | const { uuid, forceAutoplay } = options |
189 | 193 | ||
190 | try { | 194 | try { |
191 | const { | 195 | const { |
@@ -194,7 +198,7 @@ export class PeerTubeEmbed { | |||
194 | storyboardsPromise | 198 | storyboardsPromise |
195 | } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword }) | 199 | } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword }) |
196 | 200 | ||
197 | return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, autoplayFromPreviousVideo, forceAutoplay }) | 201 | return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, forceAutoplay }) |
198 | } catch (err) { | 202 | } catch (err) { |
199 | 203 | ||
200 | if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options }) | 204 | if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options }) |
@@ -206,20 +210,14 @@ export class PeerTubeEmbed { | |||
206 | videoResponse: Response | 210 | videoResponse: Response |
207 | storyboardsPromise: Promise<Response> | 211 | storyboardsPromise: Promise<Response> |
208 | captionsPromise: Promise<Response> | 212 | captionsPromise: Promise<Response> |
209 | autoplayFromPreviousVideo: boolean | ||
210 | forceAutoplay: boolean | 213 | forceAutoplay: boolean |
211 | }) { | 214 | }) { |
212 | const { videoResponse, captionsPromise, storyboardsPromise, autoplayFromPreviousVideo, forceAutoplay } = options | 215 | const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options |
213 | |||
214 | this.resetPlayerElement() | ||
215 | 216 | ||
216 | const videoInfoPromise = videoResponse.json() | 217 | const videoInfoPromise = videoResponse.json() |
217 | .then(async (videoInfo: VideoDetails) => { | 218 | .then(async (videoInfo: VideoDetails) => { |
218 | this.playerManagerOptions.loadParams(this.config, videoInfo) | 219 | this.playerOptionsBuilder.loadParams(this.config, videoInfo) |
219 | 220 | ||
220 | if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) { | ||
221 | this.playerHTML.buildPlaceholder(videoInfo) | ||
222 | } | ||
223 | const live = videoInfo.isLive | 221 | const live = videoInfo.isLive |
224 | ? await this.videoFetcher.loadLive(videoInfo) | 222 | ? await this.videoFetcher.loadLive(videoInfo) |
225 | : undefined | 223 | : undefined |
@@ -235,89 +233,78 @@ export class PeerTubeEmbed { | |||
235 | { video, live, videoFileToken }, | 233 | { video, live, videoFileToken }, |
236 | translations, | 234 | translations, |
237 | captionsResponse, | 235 | captionsResponse, |
238 | storyboardsResponse, | 236 | storyboardsResponse |
239 | PeertubePlayerManagerModule | ||
240 | ] = await Promise.all([ | 237 | ] = await Promise.all([ |
241 | videoInfoPromise, | 238 | videoInfoPromise, |
242 | this.translationsPromise, | 239 | this.translationsPromise, |
243 | captionsPromise, | 240 | captionsPromise, |
244 | storyboardsPromise, | 241 | storyboardsPromise, |
245 | this.PeertubePlayerManagerModulePromise | 242 | this.buildPlayerIfNeeded() |
246 | ]) | 243 | ]) |
247 | 244 | ||
248 | await this.peertubePlugin.loadPlugins(this.config, translations) | 245 | // If already played, we are in a playlist so we don't want to display the poster between videos |
246 | if (!this.alreadyPlayed) { | ||
247 | this.peertubePlayer.setPoster(window.location.origin + video.previewPath) | ||
248 | } | ||
249 | 249 | ||
250 | const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager | 250 | const playlist = this.playlistTracker |
251 | ? { | ||
252 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }), | ||
251 | 253 | ||
252 | const playerOptions = await this.playerManagerOptions.getPlayerOptions({ | 254 | playlistTracker: this.playlistTracker, |
255 | playNext: () => this.playNextPlaylistVideo(), | ||
256 | playPrevious: () => this.playPreviousPlaylistVideo() | ||
257 | } | ||
258 | : undefined | ||
259 | |||
260 | const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({ | ||
253 | video, | 261 | video, |
254 | captionsResponse, | 262 | captionsResponse, |
255 | autoplayFromPreviousVideo, | ||
256 | translations, | 263 | translations, |
257 | serverConfig: this.config, | ||
258 | 264 | ||
259 | storyboardsResponse, | 265 | storyboardsResponse, |
260 | 266 | ||
261 | authorizationHeader: () => this.http.getHeaderTokenValue(), | ||
262 | videoFileToken: () => videoFileToken, | 267 | videoFileToken: () => videoFileToken, |
263 | videoPassword: () => this.videoPassword, | 268 | videoPassword: () => this.videoPassword, |
264 | requiresPassword: this.requiresPassword, | 269 | requiresPassword: this.requiresPassword, |
265 | 270 | ||
266 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), | 271 | playlist, |
267 | |||
268 | playlistTracker: this.playlistTracker, | ||
269 | playNextPlaylistVideo: () => this.playNextPlaylistVideo(), | ||
270 | playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(), | ||
271 | 272 | ||
272 | live, | 273 | live, |
273 | forceAutoplay | 274 | forceAutoplay, |
275 | alreadyPlayed: this.alreadyPlayed | ||
274 | }) | 276 | }) |
277 | await this.peertubePlayer.load(loadOptions) | ||
275 | 278 | ||
276 | this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { | 279 | if (!this.alreadyInitialized) { |
277 | this.player = player | 280 | this.player = this.peertubePlayer.getPlayer(); |
278 | }) | ||
279 | 281 | ||
280 | this.player.on('customError', (event: any, data: any) => { | 282 | (window as any)['videojsPlayer'] = this.player |
281 | const message = data?.err?.message || '' | ||
282 | if (!message.includes('from xs param')) return | ||
283 | 283 | ||
284 | this.player.dispose() | 284 | this.buildCSS() |
285 | this.playerHTML.removePlayerElement() | 285 | this.initializeApi() |
286 | this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations) | 286 | } |
287 | }); | ||
288 | 287 | ||
289 | (window as any)['videojsPlayer'] = this.player | 288 | this.alreadyInitialized = true |
290 | 289 | ||
291 | this.buildCSS() | 290 | this.player.one('play', () => { |
292 | this.buildPlayerDock(video) | 291 | this.alreadyPlayed = true |
293 | this.initializeApi() | 292 | }) |
294 | 293 | ||
295 | this.playerHTML.removePlaceholder() | ||
296 | if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock() | 294 | if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock() |
297 | 295 | ||
298 | if (this.isPlaylistEmbed()) { | ||
299 | await this.buildPlayerPlaylistUpnext() | ||
300 | |||
301 | this.player.playlist().updateSelected() | ||
302 | |||
303 | this.player.on('stopped', () => { | ||
304 | this.playNextPlaylistVideo() | ||
305 | }) | ||
306 | } | ||
307 | |||
308 | if (video.isLive) { | 296 | if (video.isLive) { |
309 | this.liveManager.listenForChanges({ | 297 | this.liveManager.listenForChanges({ |
310 | video, | 298 | video, |
311 | onPublishedVideo: () => { | 299 | onPublishedVideo: () => { |
312 | this.liveManager.stopListeningForChanges(video) | 300 | this.liveManager.stopListeningForChanges(video) |
313 | this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) | 301 | this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true }) |
314 | } | 302 | } |
315 | }) | 303 | }) |
316 | 304 | ||
317 | if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { | 305 | if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { |
318 | this.liveManager.displayInfo({ state: video.state.id, translations }) | 306 | this.liveManager.displayInfo({ state: video.state.id, translations }) |
319 | 307 | this.peertubePlayer.disable() | |
320 | this.disablePlayer() | ||
321 | } else { | 308 | } else { |
322 | this.correctlyHandleLiveEnding(translations) | 309 | this.correctlyHandleLiveEnding(translations) |
323 | } | 310 | } |
@@ -326,74 +313,15 @@ export class PeerTubeEmbed { | |||
326 | this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) | 313 | this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) |
327 | } | 314 | } |
328 | 315 | ||
329 | private resetPlayerElement () { | ||
330 | if (this.player) { | ||
331 | this.player.dispose() | ||
332 | this.player = undefined | ||
333 | } | ||
334 | |||
335 | const playerElement = document.createElement('video') | ||
336 | playerElement.className = 'video-js vjs-peertube-skin' | ||
337 | playerElement.setAttribute('playsinline', 'true') | ||
338 | |||
339 | this.playerHTML.setPlayerElement(playerElement) | ||
340 | this.playerHTML.addPlayerElementToDOM() | ||
341 | } | ||
342 | |||
343 | private async buildPlayerPlaylistUpnext () { | ||
344 | const translations = await this.translationsPromise | ||
345 | |||
346 | this.player.upnext({ | ||
347 | timeout: 10000, // 10s | ||
348 | headText: peertubeTranslate('Up Next', translations), | ||
349 | cancelText: peertubeTranslate('Cancel', translations), | ||
350 | suspendedText: peertubeTranslate('Autoplay is suspended', translations), | ||
351 | getTitle: () => this.playlistTracker.nextVideoTitle(), | ||
352 | next: () => this.playNextPlaylistVideo(), | ||
353 | condition: () => !!this.playlistTracker.getNextPlaylistElement(), | ||
354 | suspended: () => false | ||
355 | }) | ||
356 | } | ||
357 | |||
358 | private buildPlayerDock (videoInfo: VideoDetails) { | ||
359 | if (!this.playerManagerOptions.hasControls()) return | ||
360 | |||
361 | // On webtorrent fallback, player may have been disposed | ||
362 | if (!this.player.player_) return | ||
363 | |||
364 | const title = this.playerManagerOptions.hasTitle() | ||
365 | ? videoInfo.name | ||
366 | : undefined | ||
367 | |||
368 | const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled() | ||
369 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | ||
370 | : undefined | ||
371 | |||
372 | if (!title && !description) return | ||
373 | |||
374 | const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) | ||
375 | const avatar = availableAvatars.length !== 0 | ||
376 | ? availableAvatars[0] | ||
377 | : undefined | ||
378 | |||
379 | this.player.peertubeDock({ | ||
380 | title, | ||
381 | description, | ||
382 | avatarUrl: title && avatar | ||
383 | ? avatar.path | ||
384 | : undefined | ||
385 | }) | ||
386 | } | ||
387 | |||
388 | private buildCSS () { | 316 | private buildCSS () { |
389 | const body = document.getElementById('custom-css') | 317 | const body = document.getElementById('custom-css') |
390 | 318 | ||
391 | if (this.playerManagerOptions.hasBigPlayBackgroundColor()) { | 319 | if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) { |
392 | body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor()) | 320 | body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor()) |
393 | } | 321 | } |
394 | 322 | ||
395 | if (this.playerManagerOptions.hasForegroundColor()) { | 323 | if (this.playerOptionsBuilder.hasForegroundColor()) { |
396 | body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor()) | 324 | body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor()) |
397 | } | 325 | } |
398 | } | 326 | } |
399 | 327 | ||
@@ -415,23 +343,10 @@ export class PeerTubeEmbed { | |||
415 | // Display the live ended information | 343 | // Display the live ended information |
416 | this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) | 344 | this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) |
417 | 345 | ||
418 | this.disablePlayer() | 346 | this.peertubePlayer.disable() |
419 | }) | 347 | }) |
420 | } | 348 | } |
421 | 349 | ||
422 | private disablePlayer () { | ||
423 | if (this.player.isFullscreen()) { | ||
424 | this.player.exitFullscreen() | ||
425 | } | ||
426 | |||
427 | // Disable player | ||
428 | this.player.hasStarted(false) | ||
429 | this.player.removeClass('vjs-has-autoplay') | ||
430 | this.player.bigPlayButton.hide(); | ||
431 | |||
432 | (this.player.el() as HTMLElement).style.pointerEvents = 'none' | ||
433 | } | ||
434 | |||
435 | private async handlePasswordError (err: PeerTubeServerError) { | 350 | private async handlePasswordError (err: PeerTubeServerError) { |
436 | let incorrectPassword: boolean = null | 351 | let incorrectPassword: boolean = null |
437 | if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false | 352 | if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false |
@@ -447,6 +362,33 @@ export class PeerTubeEmbed { | |||
447 | return true | 362 | return true |
448 | } | 363 | } |
449 | 364 | ||
365 | private async buildPlayerIfNeeded () { | ||
366 | if (this.peertubePlayer) { | ||
367 | this.peertubePlayer.enable() | ||
368 | |||
369 | return | ||
370 | } | ||
371 | |||
372 | const playerElement = document.createElement('video') | ||
373 | playerElement.className = 'video-js vjs-peertube-skin' | ||
374 | playerElement.setAttribute('playsinline', 'true') | ||
375 | |||
376 | this.playerHTML.setPlayerElement(playerElement) | ||
377 | this.playerHTML.addPlayerElementToDOM() | ||
378 | |||
379 | const [ { PeerTubePlayer } ] = await Promise.all([ | ||
380 | this.PeerTubePlayerManagerModulePromise, | ||
381 | this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise) | ||
382 | ]) | ||
383 | |||
384 | const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({ | ||
385 | serverConfig: this.config, | ||
386 | authorizationHeader: () => this.http.getHeaderTokenValue() | ||
387 | }) | ||
388 | this.peertubePlayer = new PeerTubePlayer(constructorOptions) | ||
389 | |||
390 | this.player = this.peertubePlayer.getPlayer() | ||
391 | } | ||
450 | } | 392 | } |
451 | 393 | ||
452 | PeerTubeEmbed.main() | 394 | PeerTubeEmbed.main() |
diff --git a/client/src/standalone/videos/shared/index.ts b/client/src/standalone/videos/shared/index.ts index 928b8e270..dcc522ac6 100644 --- a/client/src/standalone/videos/shared/index.ts +++ b/client/src/standalone/videos/shared/index.ts | |||
@@ -2,7 +2,7 @@ export * from './auth-http' | |||
2 | export * from './peertube-plugin' | 2 | export * from './peertube-plugin' |
3 | export * from './live-manager' | 3 | export * from './live-manager' |
4 | export * from './player-html' | 4 | export * from './player-html' |
5 | export * from './player-manager-options' | 5 | export * from './player-options-builder' |
6 | export * from './playlist-fetcher' | 6 | export * from './playlist-fetcher' |
7 | export * from './playlist-tracker' | 7 | export * from './playlist-tracker' |
8 | export * from './translations' | 8 | export * from './translations' |
diff --git a/client/src/standalone/videos/shared/player-html.ts b/client/src/standalone/videos/shared/player-html.ts index a0846d9d7..0defa0d70 100644 --- a/client/src/standalone/videos/shared/player-html.ts +++ b/client/src/standalone/videos/shared/player-html.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' | 1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' |
2 | import { VideoDetails } from '../../../../../shared/models' | ||
3 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
4 | import { Translations } from './translations' | 3 | import { Translations } from './translations' |
5 | 4 | ||
@@ -59,7 +58,6 @@ export class PlayerHTML { | |||
59 | const { incorrectPassword, translations } = options | 58 | const { incorrectPassword, translations } = options |
60 | return new Promise((resolve) => { | 59 | return new Promise((resolve) => { |
61 | 60 | ||
62 | this.removePlaceholder() | ||
63 | this.wrapperElement.style.display = 'none' | 61 | this.wrapperElement.style.display = 'none' |
64 | 62 | ||
65 | const translatedTitle = peertubeTranslate('This video is password protected', translations) | 63 | const translatedTitle = peertubeTranslate('This video is password protected', translations) |
@@ -107,19 +105,6 @@ export class PlayerHTML { | |||
107 | this.wrapperElement.style.display = 'block' | 105 | this.wrapperElement.style.display = 'block' |
108 | } | 106 | } |
109 | 107 | ||
110 | buildPlaceholder (video: VideoDetails) { | ||
111 | const placeholder = this.getPlaceholderElement() | ||
112 | |||
113 | const url = window.location.origin + video.previewPath | ||
114 | placeholder.style.backgroundImage = `url("${url}")` | ||
115 | placeholder.style.display = 'block' | ||
116 | } | ||
117 | |||
118 | removePlaceholder () { | ||
119 | const placeholder = this.getPlaceholderElement() | ||
120 | placeholder.style.display = 'none' | ||
121 | } | ||
122 | |||
123 | displayInformation (text: string, translations: Translations) { | 108 | displayInformation (text: string, translations: Translations) { |
124 | if (this.informationElement) this.removeInformation() | 109 | if (this.informationElement) this.removeInformation() |
125 | 110 | ||
@@ -137,10 +122,6 @@ export class PlayerHTML { | |||
137 | this.informationElement = undefined | 122 | this.informationElement = undefined |
138 | } | 123 | } |
139 | 124 | ||
140 | private getPlaceholderElement () { | ||
141 | return document.getElementById('placeholder-preview') | ||
142 | } | ||
143 | |||
144 | private removeElement (element: HTMLElement) { | 125 | private removeElement (element: HTMLElement) { |
145 | element.parentElement.removeChild(element) | 126 | element.parentElement.removeChild(element) |
146 | } | 127 | } |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-options-builder.ts index 3c7521bc2..8a4e32444 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-options-builder.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | VideoState, | 10 | VideoState, |
11 | VideoStreamingPlaylistType | 11 | VideoStreamingPlaylistType |
12 | } from '../../../../../shared/models' | 12 | } from '../../../../../shared/models' |
13 | import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' | 13 | import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' |
14 | import { | 14 | import { |
15 | getBoolOrDefault, | 15 | getBoolOrDefault, |
16 | getParamString, | 16 | getParamString, |
@@ -27,7 +27,7 @@ import { PlaylistTracker } from './playlist-tracker' | |||
27 | import { Translations } from './translations' | 27 | import { Translations } from './translations' |
28 | import { VideoFetcher } from './video-fetcher' | 28 | import { VideoFetcher } from './video-fetcher' |
29 | 29 | ||
30 | export class PlayerManagerOptions { | 30 | export class PlayerOptionsBuilder { |
31 | private autoplay: boolean | 31 | private autoplay: boolean |
32 | 32 | ||
33 | private controls: boolean | 33 | private controls: boolean |
@@ -141,10 +141,10 @@ export class PlayerManagerOptions { | |||
141 | 141 | ||
142 | if (modeParam) { | 142 | if (modeParam) { |
143 | if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' | 143 | if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' |
144 | else this.mode = 'webtorrent' | 144 | else this.mode = 'web-video' |
145 | } else { | 145 | } else { |
146 | if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' | 146 | if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' |
147 | else this.mode = 'webtorrent' | 147 | else this.mode = 'web-video' |
148 | } | 148 | } |
149 | } catch (err) { | 149 | } catch (err) { |
150 | logger.error('Cannot get params from URL.', err) | 150 | logger.error('Cannot get params from URL.', err) |
@@ -153,7 +153,47 @@ export class PlayerManagerOptions { | |||
153 | 153 | ||
154 | // --------------------------------------------------------------------------- | 154 | // --------------------------------------------------------------------------- |
155 | 155 | ||
156 | async getPlayerOptions (options: { | 156 | getPlayerConstructorOptions (options: { |
157 | serverConfig: HTMLServerConfig | ||
158 | authorizationHeader: () => string | ||
159 | }): PeerTubePlayerContructorOptions { | ||
160 | const { serverConfig, authorizationHeader } = options | ||
161 | |||
162 | return { | ||
163 | controls: this.controls, | ||
164 | controlBar: this.controlBar, | ||
165 | |||
166 | muted: this.muted, | ||
167 | loop: this.loop, | ||
168 | |||
169 | playbackRate: this.playbackRate, | ||
170 | |||
171 | inactivityTimeout: 2500, | ||
172 | videoViewIntervalMs: 5000, | ||
173 | metricsUrl: window.location.origin + '/api/v1/metrics/playback', | ||
174 | |||
175 | authorizationHeader, | ||
176 | |||
177 | playerElement: () => this.playerHTML.getPlayerElement(), | ||
178 | enableHotkeys: true, | ||
179 | |||
180 | peertubeLink: () => this.peertubeLink, | ||
181 | instanceName: serverConfig.instance.name, | ||
182 | |||
183 | theaterButton: false, | ||
184 | |||
185 | serverUrl: window.location.origin, | ||
186 | language: navigator.language, | ||
187 | |||
188 | pluginsManager: this.peertubePlugin.getPluginsManager(), | ||
189 | |||
190 | errorNotifier: () => { | ||
191 | // Empty, we don't have a notifier in the embed | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | async getPlayerLoadOptions (options: { | ||
157 | video: VideoDetails | 197 | video: VideoDetails |
158 | captionsResponse: Response | 198 | captionsResponse: Response |
159 | 199 | ||
@@ -161,39 +201,35 @@ export class PlayerManagerOptions { | |||
161 | 201 | ||
162 | live?: LiveVideo | 202 | live?: LiveVideo |
163 | 203 | ||
204 | alreadyPlayed: boolean | ||
164 | forceAutoplay: boolean | 205 | forceAutoplay: boolean |
165 | 206 | ||
166 | authorizationHeader: () => string | ||
167 | videoFileToken: () => string | 207 | videoFileToken: () => string |
168 | 208 | ||
169 | videoPassword: () => string | 209 | videoPassword: () => string |
170 | requiresPassword: boolean | 210 | requiresPassword: boolean |
171 | 211 | ||
172 | serverConfig: HTMLServerConfig | ||
173 | |||
174 | autoplayFromPreviousVideo: boolean | ||
175 | |||
176 | translations: Translations | 212 | translations: Translations |
177 | 213 | ||
178 | playlistTracker?: PlaylistTracker | 214 | playlist?: { |
179 | playNextPlaylistVideo?: () => any | 215 | playlistTracker: PlaylistTracker |
180 | playPreviousPlaylistVideo?: () => any | 216 | playNext: () => any |
181 | onVideoUpdate?: (uuid: string) => any | 217 | playPrevious: () => any |
182 | }) { | 218 | onVideoUpdate: (uuid: string) => any |
219 | } | ||
220 | }): Promise<PeerTubePlayerLoadOptions> { | ||
183 | const { | 221 | const { |
184 | video, | 222 | video, |
185 | captionsResponse, | 223 | captionsResponse, |
186 | autoplayFromPreviousVideo, | ||
187 | videoFileToken, | 224 | videoFileToken, |
188 | videoPassword, | 225 | videoPassword, |
189 | requiresPassword, | 226 | requiresPassword, |
190 | translations, | 227 | translations, |
228 | alreadyPlayed, | ||
191 | forceAutoplay, | 229 | forceAutoplay, |
192 | playlistTracker, | 230 | playlist, |
193 | live, | 231 | live, |
194 | storyboardsResponse, | 232 | storyboardsResponse |
195 | authorizationHeader, | ||
196 | serverConfig | ||
197 | } = options | 233 | } = options |
198 | 234 | ||
199 | const [ videoCaptions, storyboard ] = await Promise.all([ | 235 | const [ videoCaptions, storyboard ] = await Promise.all([ |
@@ -201,88 +237,56 @@ export class PlayerManagerOptions { | |||
201 | this.buildStoryboard(storyboardsResponse) | 237 | this.buildStoryboard(storyboardsResponse) |
202 | ]) | 238 | ]) |
203 | 239 | ||
204 | const playerOptions: PeertubePlayerManagerOptions = { | 240 | return { |
205 | common: { | 241 | mode: this.mode, |
206 | // Autoplay in playlist mode | ||
207 | autoplay: autoplayFromPreviousVideo ? true : this.autoplay, | ||
208 | forceAutoplay, | ||
209 | |||
210 | controls: this.controls, | ||
211 | controlBar: this.controlBar, | ||
212 | |||
213 | muted: this.muted, | ||
214 | loop: this.loop, | ||
215 | |||
216 | p2pEnabled: this.p2pEnabled, | ||
217 | |||
218 | captions: videoCaptions.length !== 0, | ||
219 | subtitle: this.subtitle, | ||
220 | 242 | ||
221 | storyboard, | 243 | autoplay: forceAutoplay || alreadyPlayed || this.autoplay, |
244 | forceAutoplay, | ||
222 | 245 | ||
223 | startTime: playlistTracker | 246 | p2pEnabled: this.p2pEnabled, |
224 | ? playlistTracker.getCurrentElement().startTimestamp | ||
225 | : this.startTime, | ||
226 | stopTime: playlistTracker | ||
227 | ? playlistTracker.getCurrentElement().stopTimestamp | ||
228 | : this.stopTime, | ||
229 | 247 | ||
230 | playbackRate: this.playbackRate, | 248 | subtitle: this.subtitle, |
231 | 249 | ||
232 | videoCaptions, | 250 | storyboard, |
233 | inactivityTimeout: 2500, | ||
234 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | ||
235 | videoViewIntervalMs: 5000, | ||
236 | metricsUrl: window.location.origin + '/api/v1/metrics/playback', | ||
237 | 251 | ||
238 | videoShortUUID: video.shortUUID, | 252 | startTime: playlist |
239 | videoUUID: video.uuid, | 253 | ? playlist.playlistTracker.getCurrentElement().startTimestamp |
254 | : this.startTime, | ||
255 | stopTime: playlist | ||
256 | ? playlist.playlistTracker.getCurrentElement().stopTimestamp | ||
257 | : this.stopTime, | ||
240 | 258 | ||
241 | playerElement: this.playerHTML.getPlayerElement(), | 259 | videoCaptions, |
242 | onPlayerElementChange: (element: HTMLVideoElement) => { | 260 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |
243 | this.playerHTML.setPlayerElement(element) | ||
244 | }, | ||
245 | 261 | ||
246 | videoDuration: video.duration, | 262 | videoShortUUID: video.shortUUID, |
247 | enableHotkeys: true, | 263 | videoUUID: video.uuid, |
248 | 264 | ||
249 | peertubeLink: this.peertubeLink, | 265 | duration: video.duration, |
250 | instanceName: serverConfig.instance.name, | ||
251 | 266 | ||
252 | poster: window.location.origin + video.previewPath, | 267 | poster: window.location.origin + video.previewPath, |
253 | theaterButton: false, | ||
254 | 268 | ||
255 | serverUrl: window.location.origin, | 269 | embedUrl: window.location.origin + video.embedPath, |
256 | language: navigator.language, | 270 | embedTitle: video.name, |
257 | embedUrl: window.location.origin + video.embedPath, | ||
258 | embedTitle: video.name, | ||
259 | 271 | ||
260 | requiresUserAuth: videoRequiresUserAuth(video), | 272 | requiresUserAuth: videoRequiresUserAuth(video), |
261 | authorizationHeader, | 273 | videoFileToken, |
262 | videoFileToken, | ||
263 | 274 | ||
264 | requiresPassword, | 275 | requiresPassword, |
265 | videoPassword, | 276 | videoPassword, |
266 | 277 | ||
267 | errorNotifier: () => { | 278 | ...this.buildLiveOptions(video, live), |
268 | // Empty, we don't have a notifier in the embed | ||
269 | }, | ||
270 | 279 | ||
271 | ...this.buildLiveOptions(video, live), | 280 | ...this.buildPlaylistOptions(playlist), |
272 | 281 | ||
273 | ...this.buildPlaylistOptions(options) | 282 | dock: this.buildDockOptions(video), |
274 | }, | ||
275 | 283 | ||
276 | webtorrent: { | 284 | webVideo: { |
277 | videoFiles: video.files | 285 | videoFiles: video.files |
278 | }, | 286 | }, |
279 | 287 | ||
280 | ...this.buildP2PMediaLoaderOptions(video), | 288 | hls: this.buildHLSOptions(video) |
281 | |||
282 | pluginsManager: this.peertubePlugin.getPluginsManager() | ||
283 | } | 289 | } |
284 | |||
285 | return playerOptions | ||
286 | } | 290 | } |
287 | 291 | ||
288 | private buildLiveOptions (video: VideoDetails, live: LiveVideo) { | 292 | private buildLiveOptions (video: VideoDetails, live: LiveVideo) { |
@@ -308,15 +312,27 @@ export class PlayerManagerOptions { | |||
308 | } | 312 | } |
309 | } | 313 | } |
310 | 314 | ||
311 | private buildPlaylistOptions (options: { | 315 | private buildPlaylistOptions (options?: { |
312 | playlistTracker?: PlaylistTracker | 316 | playlistTracker: PlaylistTracker |
313 | playNextPlaylistVideo?: () => any | 317 | playNext: () => any |
314 | playPreviousPlaylistVideo?: () => any | 318 | playPrevious: () => any |
315 | onVideoUpdate?: (uuid: string) => any | 319 | onVideoUpdate: (uuid: string) => any |
316 | }) { | 320 | }) { |
317 | const { playlistTracker, playNextPlaylistVideo, playPreviousPlaylistVideo, onVideoUpdate } = options | 321 | if (!options) { |
322 | return { | ||
323 | nextVideo: { | ||
324 | enabled: false, | ||
325 | displayControlBarButton: false, | ||
326 | getVideoTitle: () => '' | ||
327 | }, | ||
328 | previousVideo: { | ||
329 | enabled: false, | ||
330 | displayControlBarButton: false | ||
331 | } | ||
332 | } | ||
333 | } | ||
318 | 334 | ||
319 | if (!playlistTracker) return {} | 335 | const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options |
320 | 336 | ||
321 | return { | 337 | return { |
322 | playlist: { | 338 | playlist: { |
@@ -332,27 +348,37 @@ export class PlayerManagerOptions { | |||
332 | } | 348 | } |
333 | }, | 349 | }, |
334 | 350 | ||
335 | nextVideo: () => playNextPlaylistVideo(), | 351 | previousVideo: { |
336 | hasNextVideo: () => playlistTracker.hasNextPlaylistElement(), | 352 | enabled: playlistTracker.hasPreviousPlaylistElement(), |
353 | handler: () => playPrevious(), | ||
354 | displayControlBarButton: true | ||
355 | }, | ||
356 | |||
357 | nextVideo: { | ||
358 | enabled: playlistTracker.hasNextPlaylistElement(), | ||
359 | handler: () => playNext(), | ||
360 | getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name, | ||
361 | displayControlBarButton: true | ||
362 | }, | ||
337 | 363 | ||
338 | previousVideo: () => playPreviousPlaylistVideo(), | 364 | upnext: { |
339 | hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement() | 365 | isEnabled: () => true, |
366 | isSuspended: () => false, | ||
367 | timeout: 0 | ||
368 | } | ||
340 | } | 369 | } |
341 | } | 370 | } |
342 | 371 | ||
343 | private buildP2PMediaLoaderOptions (video: VideoDetails) { | 372 | private buildHLSOptions (video: VideoDetails): HLSOptions { |
344 | if (this.mode !== 'p2p-media-loader') return {} | ||
345 | |||
346 | const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 373 | const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
374 | if (!hlsPlaylist) return undefined | ||
347 | 375 | ||
348 | return { | 376 | return { |
349 | p2pMediaLoader: { | 377 | playlistUrl: hlsPlaylist.playlistUrl, |
350 | playlistUrl: hlsPlaylist.playlistUrl, | 378 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, |
351 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | 379 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), |
352 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | 380 | trackerAnnounce: video.trackerUrls, |
353 | trackerAnnounce: video.trackerUrls, | 381 | videoFiles: hlsPlaylist.files |
354 | videoFiles: hlsPlaylist.files | ||
355 | } as P2PMediaLoaderOptions | ||
356 | } | 382 | } |
357 | } | 383 | } |
358 | 384 | ||
@@ -374,6 +400,35 @@ export class PlayerManagerOptions { | |||
374 | 400 | ||
375 | // --------------------------------------------------------------------------- | 401 | // --------------------------------------------------------------------------- |
376 | 402 | ||
403 | private buildDockOptions (videoInfo: VideoDetails) { | ||
404 | if (!this.hasControls()) return undefined | ||
405 | |||
406 | const title = this.hasTitle() | ||
407 | ? videoInfo.name | ||
408 | : undefined | ||
409 | |||
410 | const description = this.hasWarningTitle() && this.hasP2PEnabled() | ||
411 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | ||
412 | : undefined | ||
413 | |||
414 | if (!title && !description) return | ||
415 | |||
416 | const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) | ||
417 | const avatar = availableAvatars.length !== 0 | ||
418 | ? availableAvatars[0] | ||
419 | : undefined | ||
420 | |||
421 | return { | ||
422 | title, | ||
423 | description, | ||
424 | avatarUrl: title && avatar | ||
425 | ? avatar.path | ||
426 | : undefined | ||
427 | } | ||
428 | } | ||
429 | |||
430 | // --------------------------------------------------------------------------- | ||
431 | |||
377 | private isP2PEnabled (config: HTMLServerConfig, video: Video) { | 432 | private isP2PEnabled (config: HTMLServerConfig, video: Video) { |
378 | const userP2PEnabled = getBoolOrDefault( | 433 | const userP2PEnabled = getBoolOrDefault( |
379 | peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), | 434 | peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), |
diff --git a/client/src/standalone/videos/test-embed.ts b/client/src/standalone/videos/test-embed.ts index b34df11ee..b7a283c4d 100644 --- a/client/src/standalone/videos/test-embed.ts +++ b/client/src/standalone/videos/test-embed.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import './test-embed.scss' | 1 | import './test-embed.scss' |
2 | import { PeerTubeResolution, PlayerEventType } from '../player/definitions' | 2 | import { PeerTubeResolution, PlayerEventType } from '../embed-player-api/definitions' |
3 | import { PeerTubePlayer } from '../player/player' | 3 | import { PeerTubePlayer } from '../embed-player-api/player' |
4 | import { logger } from '../../root-helpers' | 4 | import { logger } from '../../root-helpers' |
5 | 5 | ||
6 | window.addEventListener('load', async () => { | 6 | window.addEventListener('load', async () => { |
diff --git a/client/tsconfig.json b/client/tsconfig.json index f6409402a..5dee39362 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json | |||
@@ -61,18 +61,9 @@ | |||
61 | "fs": [ | 61 | "fs": [ |
62 | "src/shims/noop.ts" | 62 | "src/shims/noop.ts" |
63 | ], | 63 | ], |
64 | "http": [ | ||
65 | "src/shims/http.ts" | ||
66 | ], | ||
67 | "https": [ | ||
68 | "src/shims/https.ts" | ||
69 | ], | ||
70 | "path": [ | 64 | "path": [ |
71 | "src/shims/path.ts" | 65 | "src/shims/path.ts" |
72 | ], | 66 | ], |
73 | "stream": [ | ||
74 | "src/shims/stream.ts" | ||
75 | ], | ||
76 | "crypto": [ | 67 | "crypto": [ |
77 | "src/shims/noop.ts" | 68 | "src/shims/noop.ts" |
78 | ] | 69 | ] |
diff --git a/client/webpack/webpack.video-embed.js b/client/webpack/webpack.video-embed.js index e25677872..558481473 100644 --- a/client/webpack/webpack.video-embed.js +++ b/client/webpack/webpack.video-embed.js | |||
@@ -36,10 +36,7 @@ module.exports = function () { | |||
36 | 36 | ||
37 | fallback: { | 37 | fallback: { |
38 | fs: [ path.resolve('src/shims/noop.ts') ], | 38 | fs: [ path.resolve('src/shims/noop.ts') ], |
39 | http: [ path.resolve('src/shims/http.ts') ], | ||
40 | https: [ path.resolve('src/shims/https.ts') ], | ||
41 | path: [ path.resolve('src/shims/path.ts') ], | 39 | path: [ path.resolve('src/shims/path.ts') ], |
42 | stream: [ path.resolve('src/shims/stream.ts') ], | ||
43 | crypto: [ path.resolve('src/shims/noop.ts') ] | 40 | crypto: [ path.resolve('src/shims/noop.ts') ] |
44 | } | 41 | } |
45 | }, | 42 | }, |
diff --git a/client/yarn.lock b/client/yarn.lock index 282c851ec..5c9f4bf42 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -2217,13 +2217,6 @@ | |||
2217 | "@tufjs/canonical-json" "1.0.0" | 2217 | "@tufjs/canonical-json" "1.0.0" |
2218 | minimatch "^9.0.0" | 2218 | minimatch "^9.0.0" |
2219 | 2219 | ||
2220 | "@types/bittorrent-protocol@*": | ||
2221 | version "3.1.2" | ||
2222 | resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-3.1.2.tgz#884cf1589fa8b1f7a6cc39bd516922a96ce08221" | ||
2223 | integrity sha512-7k9nivNeG7Sc8wVuBs+XjBp2u7pH8tqW3BB93/SAg3xht/cZEK+Rqkj79xSyJqyj86eA0F6n85EKkkyGki8afg== | ||
2224 | dependencies: | ||
2225 | "@types/node" "*" | ||
2226 | |||
2227 | "@types/body-parser@*": | 2220 | "@types/body-parser@*": |
2228 | version "1.19.2" | 2221 | version "1.19.2" |
2229 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" | 2222 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" |
@@ -2394,13 +2387,6 @@ | |||
2394 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" | 2387 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" |
2395 | integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== | 2388 | integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== |
2396 | 2389 | ||
2397 | "@types/magnet-uri@*": | ||
2398 | version "5.1.3" | ||
2399 | resolved "https://registry.yarnpkg.com/@types/magnet-uri/-/magnet-uri-5.1.3.tgz#cdf974721012bd758c0f559cabcad7bab87f9008" | ||
2400 | integrity sha512-FvJN1yYdLhvU6zWJ2YnWQ2GnpFLsA8bt+85WY0tLh6ehzGNrvBorjlcc53/zY43r/IKn+ctFs1nt7andwGnQCQ== | ||
2401 | dependencies: | ||
2402 | "@types/node" "*" | ||
2403 | |||
2404 | "@types/markdown-it@^12.0.1": | 2390 | "@types/markdown-it@^12.0.1": |
2405 | version "12.2.3" | 2391 | version "12.2.3" |
2406 | resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" | 2392 | resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" |
@@ -2459,22 +2445,6 @@ | |||
2459 | resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" | 2445 | resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" |
2460 | integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== | 2446 | integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== |
2461 | 2447 | ||
2462 | "@types/parse-torrent-file@*": | ||
2463 | version "4.0.3" | ||
2464 | resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.3.tgz#045b023426d168e0253c932cb782b231b1ee2d62" | ||
2465 | integrity sha512-dFkPnJPKiFWiGX+HXmyTVt2js3k0d9dThmUxX8nfGC22hbyZ5BTmetsEl45sQhHLcFo43njVrIKMXM3F1ahXRw== | ||
2466 | dependencies: | ||
2467 | "@types/node" "*" | ||
2468 | |||
2469 | "@types/parse-torrent@*": | ||
2470 | version "5.8.4" | ||
2471 | resolved "https://registry.yarnpkg.com/@types/parse-torrent/-/parse-torrent-5.8.4.tgz#c095834a9a815507c59014a79517ad403e4329d0" | ||
2472 | integrity sha512-FdKs5yN5iYO5Cu9gVz1Zl30CbZe6HTsqloWmCf+LfbImgSzlsUkov2+npQWCQSQ3zi/a2G5C824K0UpZ2sRufA== | ||
2473 | dependencies: | ||
2474 | "@types/magnet-uri" "*" | ||
2475 | "@types/node" "*" | ||
2476 | "@types/parse-torrent-file" "*" | ||
2477 | |||
2478 | "@types/prop-types@*": | 2448 | "@types/prop-types@*": |
2479 | version "15.7.5" | 2449 | version "15.7.5" |
2480 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" | 2450 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" |
@@ -2558,13 +2528,6 @@ | |||
2558 | dependencies: | 2528 | dependencies: |
2559 | "@types/node" "*" | 2529 | "@types/node" "*" |
2560 | 2530 | ||
2561 | "@types/simple-peer@*": | ||
2562 | version "9.11.5" | ||
2563 | resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.5.tgz#6baa00edbbd0f632f8561e8fb03b4d21d62f076e" | ||
2564 | integrity sha512-haXgWcAa3Y3Sn+T8lzkE4ErQUpYzhW6Cz2lh00RhQTyWt+xZ3s87wJPztUxlqSdFRqGhe2MQIBd0XsyHP3No4w== | ||
2565 | dependencies: | ||
2566 | "@types/node" "*" | ||
2567 | |||
2568 | "@types/sockjs@^0.3.33": | 2531 | "@types/sockjs@^0.3.33": |
2569 | version "0.3.33" | 2532 | version "0.3.33" |
2570 | resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" | 2533 | resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" |
@@ -2582,16 +2545,6 @@ | |||
2582 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.51.tgz#ce69e02681ed6ed8abe61bb3802dd032a74d63e8" | 2545 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.51.tgz#ce69e02681ed6ed8abe61bb3802dd032a74d63e8" |
2583 | integrity sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ== | 2546 | integrity sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ== |
2584 | 2547 | ||
2585 | "@types/webtorrent@^0.109.0": | ||
2586 | version "0.109.3" | ||
2587 | resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.109.3.tgz#95df708d98bcea235b37f49a9a348b11f3511670" | ||
2588 | integrity sha512-EJLsxMEcEjPXHcBqL6TRAbUwIpxAul5ULrXHJ0zwig7Oe70FS6dAzCWLq4MBafX3QrQG1DzGAS0fS8iJEOjD0g== | ||
2589 | dependencies: | ||
2590 | "@types/bittorrent-protocol" "*" | ||
2591 | "@types/node" "*" | ||
2592 | "@types/parse-torrent" "*" | ||
2593 | "@types/simple-peer" "*" | ||
2594 | |||
2595 | "@types/which@^2.0.1": | 2548 | "@types/which@^2.0.1": |
2596 | version "2.0.2" | 2549 | version "2.0.2" |
2597 | resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" | 2550 | resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" |
@@ -3124,14 +3077,6 @@ | |||
3124 | resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f" | 3077 | resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f" |
3125 | integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A== | 3078 | integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A== |
3126 | 3079 | ||
3127 | "@webtorrent/http-node@^1.3.0": | ||
3128 | version "1.3.0" | ||
3129 | resolved "https://registry.yarnpkg.com/@webtorrent/http-node/-/http-node-1.3.0.tgz#bd8aacf13f08bb19ee25b5f5364e8d261eaa5c3c" | ||
3130 | integrity sha512-GWZQKroPES4z91Ijx6zsOsb7+USOxjy66s8AoTWg0HiBBdfnbtf9aeh3Uav0MgYn4BL8Q7tVSUpd0gGpngKGEQ== | ||
3131 | dependencies: | ||
3132 | freelist "^1.0.3" | ||
3133 | http-parser-js "^0.4.3" | ||
3134 | |||
3135 | "@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.7": | 3080 | "@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.7": |
3136 | version "0.8.7" | 3081 | version "0.8.7" |
3137 | resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" | 3082 | resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" |
@@ -3205,7 +3150,7 @@ acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: | |||
3205 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" | 3150 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" |
3206 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== | 3151 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== |
3207 | 3152 | ||
3208 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.4: | 3153 | addr-to-ip-port@^1.0.1: |
3209 | version "1.5.4" | 3154 | version "1.5.4" |
3210 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" | 3155 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" |
3211 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== | 3156 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== |
@@ -3583,11 +3528,6 @@ axobject-query@3.1.1: | |||
3583 | dependencies: | 3528 | dependencies: |
3584 | deep-equal "^2.0.5" | 3529 | deep-equal "^2.0.5" |
3585 | 3530 | ||
3586 | b4a@^1.3.1: | ||
3587 | version "1.6.4" | ||
3588 | resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" | ||
3589 | integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== | ||
3590 | |||
3591 | babel-loader@9.1.2, babel-loader@^9.1.0: | 3531 | babel-loader@9.1.2, babel-loader@^9.1.0: |
3592 | version "9.1.2" | 3532 | version "9.1.2" |
3593 | resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" | 3533 | resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" |
@@ -3656,16 +3596,11 @@ batch@0.6.1: | |||
3656 | resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" | 3596 | resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" |
3657 | integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== | 3597 | integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== |
3658 | 3598 | ||
3659 | bencode@^2.0.0, bencode@^2.0.1, bencode@^2.0.2, bencode@^2.0.3: | 3599 | bencode@^2.0.1: |
3660 | version "2.0.3" | 3600 | version "2.0.3" |
3661 | resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.3.tgz#89b9c80ea1b8573554915a7d0c15f62b0aa7fc52" | 3601 | resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.3.tgz#89b9c80ea1b8573554915a7d0c15f62b0aa7fc52" |
3662 | integrity sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w== | 3602 | integrity sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w== |
3663 | 3603 | ||
3664 | bep53-range@^1.1.0: | ||
3665 | version "1.1.1" | ||
3666 | resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-1.1.1.tgz#20fd125b00a413254a77d42f63a43750ca7e64ac" | ||
3667 | integrity sha512-ct6s33iiwRCUPp9KXnJ4QMWDgHIgaw36caK/5XEQ9L8dCzSQlJt1Vk6VmHh1VD4AlGCAI4C2zmtfItifBBPrhQ== | ||
3668 | |||
3669 | big-integer@^1.6.17: | 3604 | big-integer@^1.6.17: |
3670 | version "1.6.51" | 3605 | version "1.6.51" |
3671 | resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" | 3606 | resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" |
@@ -3681,11 +3616,6 @@ binary-extensions@^2.0.0: | |||
3681 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" | 3616 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" |
3682 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== | 3617 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== |
3683 | 3618 | ||
3684 | binary-search@^1.3.4: | ||
3685 | version "1.3.6" | ||
3686 | resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" | ||
3687 | integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== | ||
3688 | |||
3689 | binary@~0.3.0: | 3619 | binary@~0.3.0: |
3690 | version "0.3.0" | 3620 | version "0.3.0" |
3691 | resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" | 3621 | resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" |
@@ -3694,54 +3624,11 @@ binary@~0.3.0: | |||
3694 | buffers "~0.1.1" | 3624 | buffers "~0.1.1" |
3695 | chainsaw "~0.1.0" | 3625 | chainsaw "~0.1.0" |
3696 | 3626 | ||
3697 | bitfield@^4.0.0, bitfield@^4.1.0: | ||
3698 | version "4.1.0" | ||
3699 | resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.1.0.tgz#77f3ef4e915e58adaf758b23cbff156959e0fd8e" | ||
3700 | integrity sha512-6cEDG3K+PK9f+B7WyhWYjp09bqSa+uaAaecVA7Y5giFixyVe1s6HKGnvOqYNR4Mi4fBMjfDPLBpHkKvzzgP7kg== | ||
3701 | |||
3702 | bittorrent-dht@^10.0.4, bittorrent-dht@^10.0.7: | ||
3703 | version "10.0.7" | ||
3704 | resolved "https://registry.yarnpkg.com/bittorrent-dht/-/bittorrent-dht-10.0.7.tgz#fbe0f56349e7aab951d6d8625e0f78495ad74684" | ||
3705 | integrity sha512-o6elCANGteECXz82LFqG1Ov2fG4uNzfUU7pBMx9ixxKUh99ZXNrhbiNLRNN2F2vBnqKSN7SHlUW4LJ5Z2u1eKw== | ||
3706 | dependencies: | ||
3707 | bencode "^2.0.3" | ||
3708 | debug "^4.3.4" | ||
3709 | k-bucket "^5.1.0" | ||
3710 | k-rpc "^5.1.0" | ||
3711 | last-one-wins "^1.0.4" | ||
3712 | lru "^3.1.0" | ||
3713 | randombytes "^2.1.0" | ||
3714 | record-cache "^1.2.0" | ||
3715 | simple-sha1 "^3.1.0" | ||
3716 | |||
3717 | bittorrent-lsd@^1.1.1: | ||
3718 | version "1.1.1" | ||
3719 | resolved "https://registry.yarnpkg.com/bittorrent-lsd/-/bittorrent-lsd-1.1.1.tgz#427044bfcc05d0c2f286b6d1db70a91c04daa0c9" | ||
3720 | integrity sha512-dWxU2Mr2lU6jzIKgZrTsXgeXDCIcYpR1b6f2n89fn7juwPAYbNU04OgWjcQPLiNliY0filsX5CQAWntVErpk+Q== | ||
3721 | dependencies: | ||
3722 | chrome-dgram "^3.0.6" | ||
3723 | debug "^4.2.0" | ||
3724 | |||
3725 | bittorrent-peerid@^1.3.3: | 3627 | bittorrent-peerid@^1.3.3: |
3726 | version "1.3.6" | 3628 | version "1.3.6" |
3727 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.6.tgz#3688705a64937a8176ac2ded1178fc7bd91b61db" | 3629 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.6.tgz#3688705a64937a8176ac2ded1178fc7bd91b61db" |
3728 | integrity sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg== | 3630 | integrity sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg== |
3729 | 3631 | ||
3730 | bittorrent-protocol@^3.5.5: | ||
3731 | version "3.5.5" | ||
3732 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.5.5.tgz#d89233da11996d8978146f8b80ed91fec9e0e9b8" | ||
3733 | integrity sha512-cfzO//WtJGNLHXS58a4exJCSq1U0dkP2DZCQxgADInYFPdOfV1EmtpEN9toLOluVCXJRYAdwW5H6Li/hrn697A== | ||
3734 | dependencies: | ||
3735 | bencode "^2.0.2" | ||
3736 | bitfield "^4.0.0" | ||
3737 | debug "^4.3.4" | ||
3738 | randombytes "^2.1.0" | ||
3739 | rc4 "^0.1.5" | ||
3740 | readable-stream "^3.6.0" | ||
3741 | simple-sha1 "^3.1.0" | ||
3742 | speedometer "^1.1.0" | ||
3743 | unordered-array-remove "^1.0.2" | ||
3744 | |||
3745 | bittorrent-tracker@^9.19.0: | 3632 | bittorrent-tracker@^9.19.0: |
3746 | version "9.19.0" | 3633 | version "9.19.0" |
3747 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.19.0.tgz#2266bfa8a45a57b09f8d8b184710ba531712d8ef" | 3634 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.19.0.tgz#2266bfa8a45a57b09f8d8b184710ba531712d8ef" |
@@ -3783,23 +3670,6 @@ bl@^4.0.3, bl@^4.1.0: | |||
3783 | inherits "^2.0.4" | 3670 | inherits "^2.0.4" |
3784 | readable-stream "^3.4.0" | 3671 | readable-stream "^3.4.0" |
3785 | 3672 | ||
3786 | blob-to-buffer@^1.2.9: | ||
3787 | version "1.2.9" | ||
3788 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" | ||
3789 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== | ||
3790 | |||
3791 | block-iterator@^1.0.1: | ||
3792 | version "1.1.1" | ||
3793 | resolved "https://registry.yarnpkg.com/block-iterator/-/block-iterator-1.1.1.tgz#3c8a94e083febf8da59d8baad1006ffee1a74694" | ||
3794 | integrity sha512-DrjdVWZemVO4iBf4tiOXjUrY5cNesjzy0t7sIiu2rdl8cOCHRxAgKjSJFc3vBZYYMMmshUAxajl8QQh/uxXTKQ== | ||
3795 | |||
3796 | block-stream2@^2.0.0: | ||
3797 | version "2.1.0" | ||
3798 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-2.1.0.tgz#ac0c5ef4298b3857796e05be8ebed72196fa054b" | ||
3799 | integrity sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg== | ||
3800 | dependencies: | ||
3801 | readable-stream "^3.4.0" | ||
3802 | |||
3803 | bluebird@~3.4.1: | 3673 | bluebird@~3.4.1: |
3804 | version "3.4.7" | 3674 | version "3.4.7" |
3805 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" | 3675 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" |
@@ -3875,11 +3745,6 @@ browser-stdout@1.3.1: | |||
3875 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" | 3745 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" |
3876 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== | 3746 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== |
3877 | 3747 | ||
3878 | browserify-package-json@^1.0.0: | ||
3879 | version "1.0.1" | ||
3880 | resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea" | ||
3881 | integrity sha512-CikZxJGNyNOBERbeALo0NUUeJgHs5NyEvuYChX/PcsBV91TAvEq4hYDaWSenSieT8XwAutNnS3FGvyzIMOughQ== | ||
3882 | |||
3883 | browserslist@4.21.5, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.5: | 3748 | browserslist@4.21.5, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.5: |
3884 | version "4.21.5" | 3749 | version "4.21.5" |
3885 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" | 3750 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" |
@@ -3901,29 +3766,11 @@ browserstack-local@^1.5.1: | |||
3901 | ps-tree "=1.2.0" | 3766 | ps-tree "=1.2.0" |
3902 | temp-fs "^0.9.9" | 3767 | temp-fs "^0.9.9" |
3903 | 3768 | ||
3904 | buffer-alloc-unsafe@^1.1.0: | ||
3905 | version "1.1.0" | ||
3906 | resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" | ||
3907 | integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== | ||
3908 | |||
3909 | buffer-alloc@^1.1.0: | ||
3910 | version "1.2.0" | ||
3911 | resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" | ||
3912 | integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== | ||
3913 | dependencies: | ||
3914 | buffer-alloc-unsafe "^1.1.0" | ||
3915 | buffer-fill "^1.0.0" | ||
3916 | |||
3917 | buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: | 3769 | buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: |
3918 | version "0.2.13" | 3770 | version "0.2.13" |
3919 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" | 3771 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" |
3920 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== | 3772 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== |
3921 | 3773 | ||
3922 | buffer-fill@^1.0.0: | ||
3923 | version "1.0.0" | ||
3924 | resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" | ||
3925 | integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== | ||
3926 | |||
3927 | buffer-from@^1.0.0: | 3774 | buffer-from@^1.0.0: |
3928 | version "1.1.2" | 3775 | version "1.1.2" |
3929 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" | 3776 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" |
@@ -3962,11 +3809,6 @@ bufferutil@^4.0.3: | |||
3962 | dependencies: | 3809 | dependencies: |
3963 | node-gyp-build "^4.3.0" | 3810 | node-gyp-build "^4.3.0" |
3964 | 3811 | ||
3965 | builtin-status-codes@^3.0.0: | ||
3966 | version "3.0.0" | ||
3967 | resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" | ||
3968 | integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== | ||
3969 | |||
3970 | builtins@^5.0.0: | 3812 | builtins@^5.0.0: |
3971 | version "5.0.1" | 3813 | version "5.0.1" |
3972 | resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" | 3814 | resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" |
@@ -4058,14 +3900,6 @@ cacache@^17.0.0: | |||
4058 | tar "^6.1.11" | 3900 | tar "^6.1.11" |
4059 | unique-filename "^3.0.0" | 3901 | unique-filename "^3.0.0" |
4060 | 3902 | ||
4061 | cache-chunk-store@^3.0.0, cache-chunk-store@^3.2.2: | ||
4062 | version "3.2.2" | ||
4063 | resolved "https://registry.yarnpkg.com/cache-chunk-store/-/cache-chunk-store-3.2.2.tgz#19bb55d61252cd2174da4686548d52bc2dd44120" | ||
4064 | integrity sha512-2lJdWbgHFFxcSth9s2wpId3CR3v1YC63KjP4T9WhpW7LWlY7Hiiei3QwwqzkWqlJTfR8lSy9F5kRQECeyj+yQA== | ||
4065 | dependencies: | ||
4066 | lru "^3.1.0" | ||
4067 | queue-microtask "^1.2.3" | ||
4068 | |||
4069 | cacheable-lookup@^7.0.0: | 3903 | cacheable-lookup@^7.0.0: |
4070 | version "7.0.0" | 3904 | version "7.0.0" |
4071 | resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" | 3905 | resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" |
@@ -4231,7 +4065,7 @@ chownr@^2.0.0: | |||
4231 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" | 4065 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" |
4232 | integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== | 4066 | integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== |
4233 | 4067 | ||
4234 | chrome-dgram@^3.0.2, chrome-dgram@^3.0.6: | 4068 | chrome-dgram@^3.0.6: |
4235 | version "3.0.6" | 4069 | version "3.0.6" |
4236 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" | 4070 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" |
4237 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== | 4071 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== |
@@ -4239,13 +4073,6 @@ chrome-dgram@^3.0.2, chrome-dgram@^3.0.6: | |||
4239 | inherits "^2.0.4" | 4073 | inherits "^2.0.4" |
4240 | run-series "^1.1.9" | 4074 | run-series "^1.1.9" |
4241 | 4075 | ||
4242 | chrome-dns@^1.0.0: | ||
4243 | version "1.0.1" | ||
4244 | resolved "https://registry.yarnpkg.com/chrome-dns/-/chrome-dns-1.0.1.tgz#6870af680a40d2c4b2efc2154a378793f5a4ce4b" | ||
4245 | integrity sha512-HqsYJgIc8ljJJOqOzLphjAs79EUuWSX3nzZi2LNkzlw3GIzAeZbaSektC8iT/tKvLqZq8yl1GJu5o6doA4TRbg== | ||
4246 | dependencies: | ||
4247 | chrome-net "^3.3.2" | ||
4248 | |||
4249 | chrome-launcher@^0.15.0: | 4076 | chrome-launcher@^0.15.0: |
4250 | version "0.15.2" | 4077 | version "0.15.2" |
4251 | resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" | 4078 | resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" |
@@ -4256,13 +4083,6 @@ chrome-launcher@^0.15.0: | |||
4256 | is-wsl "^2.2.0" | 4083 | is-wsl "^2.2.0" |
4257 | lighthouse-logger "^1.0.0" | 4084 | lighthouse-logger "^1.0.0" |
4258 | 4085 | ||
4259 | chrome-net@^3.3.2, chrome-net@^3.3.4: | ||
4260 | version "3.3.4" | ||
4261 | resolved "https://registry.yarnpkg.com/chrome-net/-/chrome-net-3.3.4.tgz#0e604a31d226ebfb8d2d1c381cab47d35309825d" | ||
4262 | integrity sha512-Jzy2EnzmE+ligqIZUsmWnck9RBXLuUy6CaKyuNMtowFG3ZvLt8d+WBJCTPEludV0DHpIKjAOlwjFmTaEdfdWCw== | ||
4263 | dependencies: | ||
4264 | inherits "^2.0.1" | ||
4265 | |||
4266 | chrome-trace-event@^1.0.2: | 4086 | chrome-trace-event@^1.0.2: |
4267 | version "1.0.3" | 4087 | version "1.0.3" |
4268 | resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" | 4088 | resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" |
@@ -4288,14 +4108,6 @@ chromium-bidi@0.4.9: | |||
4288 | dependencies: | 4108 | dependencies: |
4289 | mitt "3.0.0" | 4109 | mitt "3.0.0" |
4290 | 4110 | ||
4291 | chunk-store-stream@^4.3.0: | ||
4292 | version "4.3.0" | ||
4293 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" | ||
4294 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== | ||
4295 | dependencies: | ||
4296 | block-stream2 "^2.0.0" | ||
4297 | readable-stream "^3.6.0" | ||
4298 | |||
4299 | ci-info@^3.2.0: | 4111 | ci-info@^3.2.0: |
4300 | version "3.8.0" | 4112 | version "3.8.0" |
4301 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" | 4113 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" |
@@ -4628,11 +4440,6 @@ cosmiconfig@^8.1.3: | |||
4628 | parse-json "^5.0.0" | 4440 | parse-json "^5.0.0" |
4629 | path-type "^4.0.0" | 4441 | path-type "^4.0.0" |
4630 | 4442 | ||
4631 | cpus@^1.0.3: | ||
4632 | version "1.0.3" | ||
4633 | resolved "https://registry.yarnpkg.com/cpus/-/cpus-1.0.3.tgz#4ef6deea461968d6329d07dd01205685df2934a2" | ||
4634 | integrity sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA== | ||
4635 | |||
4636 | crc-32@^1.2.0: | 4443 | crc-32@^1.2.0: |
4637 | version "1.2.2" | 4444 | version "1.2.2" |
4638 | resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" | 4445 | resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" |
@@ -4646,23 +4453,6 @@ crc32-stream@^4.0.2: | |||
4646 | crc-32 "^1.2.0" | 4453 | crc-32 "^1.2.0" |
4647 | readable-stream "^3.4.0" | 4454 | readable-stream "^3.4.0" |
4648 | 4455 | ||
4649 | create-torrent@^5.0.4: | ||
4650 | version "5.0.9" | ||
4651 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-5.0.9.tgz#850f198f7568e3d0e1e73b6858d43d44659a69d0" | ||
4652 | integrity sha512-WQ/bMe+aCBSa5EonIkgw7CTM/1JnJDQuLJhA78omSWvuEbXDwaUy0rG3a+IYt+EiO+rdTLxdsBwrsn/wfWOMQA== | ||
4653 | dependencies: | ||
4654 | bencode "^2.0.3" | ||
4655 | block-iterator "^1.0.1" | ||
4656 | fast-readable-async-iterator "^1.1.1" | ||
4657 | is-file "^1.0.0" | ||
4658 | join-async-iterator "^1.1.1" | ||
4659 | junk "^3.1.0" | ||
4660 | minimist "^1.2.7" | ||
4661 | piece-length "^2.0.1" | ||
4662 | queue-microtask "^1.2.3" | ||
4663 | run-parallel "^1.2.0" | ||
4664 | simple-sha1 "^3.1.0" | ||
4665 | |||
4666 | critters@0.0.16: | 4456 | critters@0.0.16: |
4667 | version "0.0.16" | 4457 | version "0.0.16" |
4668 | resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" | 4458 | resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" |
@@ -4812,7 +4602,7 @@ debug@2.6.9, debug@^2.6.9: | |||
4812 | dependencies: | 4602 | dependencies: |
4813 | ms "2.0.0" | 4603 | ms "2.0.0" |
4814 | 4604 | ||
4815 | debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: | 4605 | debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: |
4816 | version "4.3.4" | 4606 | version "4.3.4" |
4817 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" | 4607 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" |
4818 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== | 4608 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== |
@@ -4999,11 +4789,6 @@ devtools@8.10.5: | |||
4999 | uuid "^9.0.0" | 4789 | uuid "^9.0.0" |
5000 | which "^3.0.0" | 4790 | which "^3.0.0" |
5001 | 4791 | ||
5002 | dexie@^3.2.2: | ||
5003 | version "3.2.3" | ||
5004 | resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.3.tgz#f35c91ca797599df8e771b998e9ae9669c877f8c" | ||
5005 | integrity sha512-iHayBd4UYryDCVUNa3PMsJMEnd8yjyh5p7a+RFeC8i8n476BC9wMhVvqiImq5zJZJf5Tuer+s4SSj+AA3x+ZbQ== | ||
5006 | |||
5007 | diff-sequences@^29.4.3: | 4792 | diff-sequences@^29.4.3: |
5008 | version "29.4.3" | 4793 | version "29.4.3" |
5009 | resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" | 4794 | resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" |
@@ -5220,7 +5005,7 @@ encoding@^0.1.13: | |||
5220 | dependencies: | 5005 | dependencies: |
5221 | iconv-lite "^0.6.2" | 5006 | iconv-lite "^0.6.2" |
5222 | 5007 | ||
5223 | end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: | 5008 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: |
5224 | version "1.4.4" | 5009 | version "1.4.4" |
5225 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" | 5010 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" |
5226 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== | 5011 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== |
@@ -5458,7 +5243,7 @@ escalade@^3.1.1: | |||
5458 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" | 5243 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" |
5459 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== | 5244 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== |
5460 | 5245 | ||
5461 | escape-html@^1.0.3, escape-html@~1.0.3: | 5246 | escape-html@~1.0.3: |
5462 | version "1.0.3" | 5247 | version "1.0.3" |
5463 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" | 5248 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" |
5464 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== | 5249 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== |
@@ -5818,11 +5603,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: | |||
5818 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" | 5603 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" |
5819 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== | 5604 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== |
5820 | 5605 | ||
5821 | fast-fifo@^1.1.0: | ||
5822 | version "1.2.0" | ||
5823 | resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.2.0.tgz#2ee038da2468e8623066dee96958b0c1763aa55a" | ||
5824 | integrity sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg== | ||
5825 | |||
5826 | fast-glob@3.2.7: | 5606 | fast-glob@3.2.7: |
5827 | version "3.2.7" | 5607 | version "3.2.7" |
5828 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" | 5608 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" |
@@ -5855,11 +5635,6 @@ fast-levenshtein@^2.0.6: | |||
5855 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" | 5635 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" |
5856 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== | 5636 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== |
5857 | 5637 | ||
5858 | fast-readable-async-iterator@^1.1.1: | ||
5859 | version "1.1.1" | ||
5860 | resolved "https://registry.yarnpkg.com/fast-readable-async-iterator/-/fast-readable-async-iterator-1.1.1.tgz#77dfbb5262b278bb123c4d8d3219b1bb881b857c" | ||
5861 | integrity sha512-xEHkLUEmStETI+15zhglJLO9TjXxNkkp2ldEfYVZdcqxFhM172EfGl1irI6mVlTxXspYKH1/kjevnt/XSsPeFA== | ||
5862 | |||
5863 | fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: | 5638 | fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: |
5864 | version "1.0.16" | 5639 | version "1.0.16" |
5865 | resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" | 5640 | resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" |
@@ -6065,11 +5840,6 @@ fraction.js@^4.2.0: | |||
6065 | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" | 5840 | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" |
6066 | integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== | 5841 | integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== |
6067 | 5842 | ||
6068 | freelist@^1.0.3: | ||
6069 | version "1.0.3" | ||
6070 | resolved "https://registry.yarnpkg.com/freelist/-/freelist-1.0.3.tgz#006775509f3935701784d3ed2fc9f12c9df1bab2" | ||
6071 | integrity sha512-Ji7fEnMdZDGbS5oXElpRJsn9jPvBR8h/037D3bzreNmS8809cISq/2D9//JbA/TaZmkkN8cmecXwmQHmM+NHhg== | ||
6072 | |||
6073 | fresh@0.5.2: | 5843 | fresh@0.5.2: |
6074 | version "0.5.2" | 5844 | version "0.5.2" |
6075 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" | 5845 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" |
@@ -6080,18 +5850,6 @@ from@~0: | |||
6080 | resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" | 5850 | resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" |
6081 | integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== | 5851 | integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== |
6082 | 5852 | ||
6083 | fs-chunk-store@^2.0.5: | ||
6084 | version "2.0.5" | ||
6085 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.5.tgz#1dd4bdbb371239ac6f7234af6cd4386c72315059" | ||
6086 | integrity sha512-z3c2BmyaHdQTtIVXJDQOvwZVWN2gNU//0IYKK2LuPr+cZyGoIrgDwI4iDASaTUyQbOBtyg/k6GuDZepB6jQIPw== | ||
6087 | dependencies: | ||
6088 | queue-microtask "^1.2.2" | ||
6089 | random-access-file "^2.0.1" | ||
6090 | randombytes "^2.0.3" | ||
6091 | rimraf "^3.0.0" | ||
6092 | run-parallel "^1.1.2" | ||
6093 | thunky "^1.0.1" | ||
6094 | |||
6095 | fs-constants@^1.0.0: | 5853 | fs-constants@^1.0.0: |
6096 | version "1.0.0" | 5854 | version "1.0.0" |
6097 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" | 5855 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" |
@@ -6240,11 +5998,6 @@ get-port@^6.1.2: | |||
6240 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" | 5998 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" |
6241 | integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== | 5999 | integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== |
6242 | 6000 | ||
6243 | get-stdin@^8.0.0: | ||
6244 | version "8.0.0" | ||
6245 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" | ||
6246 | integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== | ||
6247 | |||
6248 | get-stream@^3.0.0: | 6001 | get-stream@^3.0.0: |
6249 | version "3.0.0" | 6002 | version "3.0.0" |
6250 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" | 6003 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" |
@@ -6734,11 +6487,6 @@ http-parser-js@>=0.5.1: | |||
6734 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" | 6487 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" |
6735 | integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== | 6488 | integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== |
6736 | 6489 | ||
6737 | http-parser-js@^0.4.3: | ||
6738 | version "0.4.13" | ||
6739 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.13.tgz#3bd6d6fde6e3172c9334c3b33b6c193d80fe1137" | ||
6740 | integrity sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA== | ||
6741 | |||
6742 | http-proxy-agent@5.0.0, http-proxy-agent@^5.0.0: | 6490 | http-proxy-agent@5.0.0, http-proxy-agent@^5.0.0: |
6743 | version "5.0.0" | 6491 | version "5.0.0" |
6744 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" | 6492 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" |
@@ -6784,11 +6532,6 @@ http2-wrapper@^2.1.10: | |||
6784 | quick-lru "^5.1.1" | 6532 | quick-lru "^5.1.1" |
6785 | resolve-alpn "^1.2.0" | 6533 | resolve-alpn "^1.2.0" |
6786 | 6534 | ||
6787 | https-browserify@^1.0.0: | ||
6788 | version "1.0.0" | ||
6789 | resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" | ||
6790 | integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== | ||
6791 | |||
6792 | https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: | 6535 | https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: |
6793 | version "5.0.1" | 6536 | version "5.0.1" |
6794 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" | 6537 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" |
@@ -6863,13 +6606,6 @@ image-size@~0.5.0: | |||
6863 | resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" | 6606 | resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" |
6864 | integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== | 6607 | integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== |
6865 | 6608 | ||
6866 | immediate-chunk-store@^2.2.0: | ||
6867 | version "2.2.0" | ||
6868 | resolved "https://registry.yarnpkg.com/immediate-chunk-store/-/immediate-chunk-store-2.2.0.tgz#f56d30ecc7171f6cfcf632b0eb8395a89f92c03c" | ||
6869 | integrity sha512-1bHBna0hCa6arRXicu91IiL9RvvkbNYLVq+mzWdaLGZC3hXvX4doh8e1dLhMKez5siu63CYgO5NrGJbRX5lbPA== | ||
6870 | dependencies: | ||
6871 | queue-microtask "^1.2.3" | ||
6872 | |||
6873 | immutable@^4.0.0: | 6609 | immutable@^4.0.0: |
6874 | version "4.3.0" | 6610 | version "4.3.0" |
6875 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" | 6611 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" |
@@ -6934,7 +6670,7 @@ inflight@^1.0.4: | |||
6934 | once "^1.3.0" | 6670 | once "^1.3.0" |
6935 | wrappy "1" | 6671 | wrappy "1" |
6936 | 6672 | ||
6937 | inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4: | 6673 | inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: |
6938 | version "2.0.4" | 6674 | version "2.0.4" |
6939 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" | 6675 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" |
6940 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== | 6676 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== |
@@ -7030,13 +6766,6 @@ ip-regex@^4.1.0: | |||
7030 | resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" | 6766 | resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" |
7031 | integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== | 6767 | integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== |
7032 | 6768 | ||
7033 | ip-set@^2.1.0: | ||
7034 | version "2.1.0" | ||
7035 | resolved "https://registry.yarnpkg.com/ip-set/-/ip-set-2.1.0.tgz#9a47b9f5d220c38bc7fe5db8efc4baa45b0a0a35" | ||
7036 | integrity sha512-JdHz4tSMx1IeFj8yEcQU0i58qiSkOlmZXkZ8+HJ0ROV5KcgLRDO9F703oJ1GeZCvqggrcCbmagD/V7hghY62wA== | ||
7037 | dependencies: | ||
7038 | ip "^1.1.5" | ||
7039 | |||
7040 | ip@^1.1.5: | 6769 | ip@^1.1.5: |
7041 | version "1.1.8" | 6770 | version "1.1.8" |
7042 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" | 6771 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" |
@@ -7084,11 +6813,6 @@ is-arrayish@^0.2.1: | |||
7084 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | 6813 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" |
7085 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== | 6814 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== |
7086 | 6815 | ||
7087 | is-ascii@^1.0.0: | ||
7088 | version "1.0.0" | ||
7089 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" | ||
7090 | integrity sha512-CXMaB/+EWCSGlLPs7ZlXRBpaPRRSRnrOfq0N3+RGeCZfqQaHQtiDLlkPCn63+LCkRUc1iRE0AXiI+sm2/Hi3qQ== | ||
7091 | |||
7092 | is-bigint@^1.0.1: | 6816 | is-bigint@^1.0.1: |
7093 | version "1.0.4" | 6817 | version "1.0.4" |
7094 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" | 6818 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" |
@@ -7140,11 +6864,6 @@ is-extglob@^2.1.1: | |||
7140 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" | 6864 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" |
7141 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== | 6865 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== |
7142 | 6866 | ||
7143 | is-file@^1.0.0: | ||
7144 | version "1.0.0" | ||
7145 | resolved "https://registry.yarnpkg.com/is-file/-/is-file-1.0.0.tgz#28a44cfbd9d3db193045f22b65fce8edf9620596" | ||
7146 | integrity sha512-ZGMuc+xA8mRnrXtmtf2l/EkIW2zaD2LSBWlaOVEF6yH4RTndHob65V4SwWWdtGKVthQfXPVKsXqw4TDUjbVxVQ== | ||
7147 | |||
7148 | is-fullwidth-code-point@^1.0.0: | 6867 | is-fullwidth-code-point@^1.0.0: |
7149 | version "1.0.0" | 6868 | version "1.0.0" |
7150 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" | 6869 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" |
@@ -7485,11 +7204,6 @@ jest-worker@^27.4.5: | |||
7485 | merge-stream "^2.0.0" | 7204 | merge-stream "^2.0.0" |
7486 | supports-color "^8.0.0" | 7205 | supports-color "^8.0.0" |
7487 | 7206 | ||
7488 | join-async-iterator@^1.1.1: | ||
7489 | version "1.1.1" | ||
7490 | resolved "https://registry.yarnpkg.com/join-async-iterator/-/join-async-iterator-1.1.1.tgz#7d2857d7f4066267861888d264769e842110d07e" | ||
7491 | integrity sha512-ATse+nuNeKZ9K1y27LKdvPe/GCe9R/u9dw9vI248e+vILeRK3IcJP4JUPAlSmKRCDK0cKhEwfmiw4Skqx7UnGQ== | ||
7492 | |||
7493 | js-tokens@^4.0.0: | 7207 | js-tokens@^4.0.0: |
7494 | version "4.0.0" | 7208 | version "4.0.0" |
7495 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | 7209 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" |
@@ -7591,37 +7305,6 @@ jsonparse@^1.3.1: | |||
7591 | resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" | 7305 | resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" |
7592 | integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== | 7306 | integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== |
7593 | 7307 | ||
7594 | junk@^3.1.0: | ||
7595 | version "3.1.0" | ||
7596 | resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" | ||
7597 | integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== | ||
7598 | |||
7599 | k-bucket@^5.0.0, k-bucket@^5.1.0: | ||
7600 | version "5.1.0" | ||
7601 | resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.1.0.tgz#db2c9e72bd168b432e3f3e8fc092e2ccb61bff89" | ||
7602 | integrity sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg== | ||
7603 | dependencies: | ||
7604 | randombytes "^2.1.0" | ||
7605 | |||
7606 | k-rpc-socket@^1.7.2: | ||
7607 | version "1.11.1" | ||
7608 | resolved "https://registry.yarnpkg.com/k-rpc-socket/-/k-rpc-socket-1.11.1.tgz#f14b4b240a716c6cad7b6434b21716dbd7c7b0e8" | ||
7609 | integrity sha512-8xtA8oqbZ6v1Niryp2/g4GxW16EQh5MvrUylQoOG+zcrDff5CKttON2XUXvMwlIHq4/2zfPVFiinAccJ+WhxoA== | ||
7610 | dependencies: | ||
7611 | bencode "^2.0.0" | ||
7612 | chrome-dgram "^3.0.2" | ||
7613 | chrome-dns "^1.0.0" | ||
7614 | chrome-net "^3.3.2" | ||
7615 | |||
7616 | k-rpc@^5.1.0: | ||
7617 | version "5.1.0" | ||
7618 | resolved "https://registry.yarnpkg.com/k-rpc/-/k-rpc-5.1.0.tgz#af2052de2e84994d55da3032175da5dad8640174" | ||
7619 | integrity sha512-FGc+n70Hcjoa/X2JTwP+jMIOpBz+pkRffHnSl9yrYiwUxg3FIgD50+u1ePfJUOnRCnx6pbjmVk5aAeB1wIijuQ== | ||
7620 | dependencies: | ||
7621 | k-bucket "^5.0.0" | ||
7622 | k-rpc-socket "^1.7.2" | ||
7623 | randombytes "^2.0.5" | ||
7624 | |||
7625 | karma-source-map-support@1.4.0: | 7308 | karma-source-map-support@1.4.0: |
7626 | version "1.4.0" | 7309 | version "1.4.0" |
7627 | resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" | 7310 | resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" |
@@ -7661,11 +7344,6 @@ ky@^0.33.0: | |||
7661 | resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" | 7344 | resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" |
7662 | integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== | 7345 | integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== |
7663 | 7346 | ||
7664 | last-one-wins@^1.0.4: | ||
7665 | version "1.0.4" | ||
7666 | resolved "https://registry.yarnpkg.com/last-one-wins/-/last-one-wins-1.0.4.tgz#c1bfd0cbcb46790ec9156b8d1aee8fcb86cda22a" | ||
7667 | integrity sha512-t+KLJFkHPQk8lfN6WBOiGkiUXoub+gnb2XTYI2P3aiISL+94xgZ1vgz1SXN/N4hthuOoLXarXfBZPUruyjQtfA== | ||
7668 | |||
7669 | launch-editor@^2.6.0: | 7347 | launch-editor@^2.6.0: |
7670 | version "2.6.0" | 7348 | version "2.6.0" |
7671 | resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" | 7349 | resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" |
@@ -7735,11 +7413,6 @@ lighthouse-logger@^1.0.0: | |||
7735 | debug "^2.6.9" | 7413 | debug "^2.6.9" |
7736 | marky "^1.2.2" | 7414 | marky "^1.2.2" |
7737 | 7415 | ||
7738 | limiter@^1.1.5: | ||
7739 | version "1.1.5" | ||
7740 | resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" | ||
7741 | integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== | ||
7742 | |||
7743 | lines-and-columns@^1.1.6: | 7416 | lines-and-columns@^1.1.6: |
7744 | version "1.2.4" | 7417 | version "1.2.4" |
7745 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" | 7418 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" |
@@ -7772,17 +7445,6 @@ listenercount@~1.0.1: | |||
7772 | resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" | 7445 | resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" |
7773 | integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== | 7446 | integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== |
7774 | 7447 | ||
7775 | load-ip-set@^2.2.1: | ||
7776 | version "2.2.1" | ||
7777 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.2.1.tgz#9496ab8aa14ebf81aeb7c8bb38e7abdf50af3563" | ||
7778 | integrity sha512-G3hQXehU2LTOp52e+lPffpK4EvidfjwbvHaGqmFcp4ptiZagR4xFdL+D08kMX906dxeqZyWhfonEjdUxrWcldg== | ||
7779 | dependencies: | ||
7780 | ip-set "^2.1.0" | ||
7781 | netmask "^2.0.1" | ||
7782 | once "^1.4.0" | ||
7783 | simple-get "^4.0.0" | ||
7784 | split "^1.0.1" | ||
7785 | |||
7786 | load-json-file@^1.0.0: | 7448 | load-json-file@^1.0.0: |
7787 | version "1.1.0" | 7449 | version "1.1.0" |
7788 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" | 7450 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" |
@@ -7991,14 +7653,6 @@ lru@^3.1.0: | |||
7991 | dependencies: | 7653 | dependencies: |
7992 | inherits "^2.0.1" | 7654 | inherits "^2.0.1" |
7993 | 7655 | ||
7994 | lt_donthave@^1.0.1: | ||
7995 | version "1.0.1" | ||
7996 | resolved "https://registry.yarnpkg.com/lt_donthave/-/lt_donthave-1.0.1.tgz#a160e08bdf15b9e092172063688855a6c031d8b3" | ||
7997 | integrity sha512-PfOXfDN9GnUjlNHjjxKQuMxPC8s12iSrnmg+Ff1BU1uLn7S1BFAKzpZCu6Gwg3WsCUvTZrZoDSHvy6B/j+N4/Q== | ||
7998 | dependencies: | ||
7999 | debug "^4.2.0" | ||
8000 | unordered-array-remove "^1.0.2" | ||
8001 | |||
8002 | m3u8-parser@4.8.0, m3u8-parser@^4.7.1: | 7656 | m3u8-parser@4.8.0, m3u8-parser@^4.7.1: |
8003 | version "4.8.0" | 7657 | version "4.8.0" |
8004 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" | 7658 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" |
@@ -8015,14 +7669,6 @@ magic-string@0.30.0: | |||
8015 | dependencies: | 7669 | dependencies: |
8016 | "@jridgewell/sourcemap-codec" "^1.4.13" | 7670 | "@jridgewell/sourcemap-codec" "^1.4.13" |
8017 | 7671 | ||
8018 | magnet-uri@^6.2.0: | ||
8019 | version "6.2.0" | ||
8020 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.2.0.tgz#10f7be050bf23452df210838239b118463c3eeff" | ||
8021 | integrity sha512-O9AgdDwT771fnUj0giPYu/rACpz8173y8UXCSOdLITjOVfBenZ9H9q3FqQmveK+ORUMuD+BkKNSZP8C3+IMAKQ== | ||
8022 | dependencies: | ||
8023 | bep53-range "^1.1.0" | ||
8024 | thirty-two "^1.0.2" | ||
8025 | |||
8026 | mailparser-mit@^1.0.0: | 7672 | mailparser-mit@^1.0.0: |
8027 | version "1.0.0" | 7673 | version "1.0.0" |
8028 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" | 7674 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" |
@@ -8149,15 +7795,6 @@ media-typer@0.3.0: | |||
8149 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" | 7795 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" |
8150 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== | 7796 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== |
8151 | 7797 | ||
8152 | mediasource@^2.2.2, mediasource@^2.4.0: | ||
8153 | version "2.4.0" | ||
8154 | resolved "https://registry.yarnpkg.com/mediasource/-/mediasource-2.4.0.tgz#7b03378054c41400374e9bade50aa0d7a758c39b" | ||
8155 | integrity sha512-SKUMrbFMHgiCUZFOWZcL0aiF/KgHx9SPIKzxrl6+7nMUMDK/ZnOmJdY/9wKzYeM0g3mybt3ueg+W+/mrYfmeFQ== | ||
8156 | dependencies: | ||
8157 | inherits "^2.0.4" | ||
8158 | readable-stream "^3.6.0" | ||
8159 | to-arraybuffer "^1.0.1" | ||
8160 | |||
8161 | mem@^1.1.0: | 7798 | mem@^1.1.0: |
8162 | version "1.1.0" | 7799 | version "1.1.0" |
8163 | resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" | 7800 | resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" |
@@ -8172,13 +7809,6 @@ memfs@^3.4.12, memfs@^3.4.3: | |||
8172 | dependencies: | 7809 | dependencies: |
8173 | fs-monkey "^1.0.3" | 7810 | fs-monkey "^1.0.3" |
8174 | 7811 | ||
8175 | memory-chunk-store@^1.3.5: | ||
8176 | version "1.3.5" | ||
8177 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.5.tgz#700f712415895600bc5466007333efa19f1de07c" | ||
8178 | integrity sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g== | ||
8179 | dependencies: | ||
8180 | queue-microtask "^1.2.3" | ||
8181 | |||
8182 | meow@^9.0.0: | 7812 | meow@^9.0.0: |
8183 | version "9.0.0" | 7813 | version "9.0.0" |
8184 | resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" | 7814 | resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" |
@@ -8242,11 +7872,6 @@ mime@1.6.0, mime@^1.4.1, mime@^1.6.0: | |||
8242 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 7872 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
8243 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 7873 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
8244 | 7874 | ||
8245 | mime@^3.0.0: | ||
8246 | version "3.0.0" | ||
8247 | resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" | ||
8248 | integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== | ||
8249 | |||
8250 | mimic-fn@^1.0.0: | 7875 | mimic-fn@^1.0.0: |
8251 | version "1.2.0" | 7876 | version "1.2.0" |
8252 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" | 7877 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" |
@@ -8354,7 +7979,7 @@ minimist-options@4.1.0: | |||
8354 | is-plain-obj "^1.1.0" | 7979 | is-plain-obj "^1.1.0" |
8355 | kind-of "^6.0.3" | 7980 | kind-of "^6.0.3" |
8356 | 7981 | ||
8357 | minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.7: | 7982 | minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: |
8358 | version "1.2.8" | 7983 | version "1.2.8" |
8359 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" | 7984 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" |
8360 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== | 7985 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== |
@@ -8501,23 +8126,6 @@ mousetrap@^1.6.5: | |||
8501 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" | 8126 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" |
8502 | integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== | 8127 | integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== |
8503 | 8128 | ||
8504 | mp4-box-encoding@^1.3.0: | ||
8505 | version "1.4.1" | ||
8506 | resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.4.1.tgz#19b31804c896bc1adf1c21b497bcf951aa3b9098" | ||
8507 | integrity sha512-2/PRtGGiqPc/VEhbm7xAQ+gbb7yzHjjMAv6MpAifr5pCpbh3fQUdj93uNgwPiTppAGu8HFKe3PeU+OdRyAxStA== | ||
8508 | dependencies: | ||
8509 | uint64be "^2.0.2" | ||
8510 | |||
8511 | mp4-stream@^3.0.0: | ||
8512 | version "3.1.3" | ||
8513 | resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-3.1.3.tgz#79b8a19900337203a9bd607a02eccc64419a379c" | ||
8514 | integrity sha512-DUT8f0x2jHbZjNMdqe9h6lZdt6RENWTTdGn8z3TXa4uEsoltuNY9lCCij84mdm0q7xcV0E2W25WRxlKBMo4hSw== | ||
8515 | dependencies: | ||
8516 | mp4-box-encoding "^1.3.0" | ||
8517 | next-event "^1.0.0" | ||
8518 | queue-microtask "^1.2.2" | ||
8519 | readable-stream "^3.0.6" | ||
8520 | |||
8521 | mpd-parser@0.22.1, mpd-parser@^0.22.1: | 8129 | mpd-parser@0.22.1, mpd-parser@^0.22.1: |
8522 | version "0.22.1" | 8130 | version "0.22.1" |
8523 | resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" | 8131 | resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" |
@@ -8556,14 +8164,6 @@ multicast-dns@^7.2.5: | |||
8556 | dns-packet "^5.2.2" | 8164 | dns-packet "^5.2.2" |
8557 | thunky "^1.0.2" | 8165 | thunky "^1.0.2" |
8558 | 8166 | ||
8559 | multistream@^4.1.0: | ||
8560 | version "4.1.0" | ||
8561 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" | ||
8562 | integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== | ||
8563 | dependencies: | ||
8564 | once "^1.4.0" | ||
8565 | readable-stream "^3.6.0" | ||
8566 | |||
8567 | mute-stream@0.0.8: | 8167 | mute-stream@0.0.8: |
8568 | version "0.0.8" | 8168 | version "0.0.8" |
8569 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" | 8169 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" |
@@ -8592,11 +8192,6 @@ nanoid@^3.3.6: | |||
8592 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" | 8192 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" |
8593 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== | 8193 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== |
8594 | 8194 | ||
8595 | napi-macros@^2.0.0: | ||
8596 | version "2.2.2" | ||
8597 | resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.2.2.tgz#817fef20c3e0e40a963fbf7b37d1600bd0201044" | ||
8598 | integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== | ||
8599 | |||
8600 | natural-compare-lite@^1.4.0: | 8195 | natural-compare-lite@^1.4.0: |
8601 | version "1.4.0" | 8196 | version "1.4.0" |
8602 | resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" | 8197 | resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" |
@@ -8626,16 +8221,6 @@ neo-async@^2.6.2: | |||
8626 | resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" | 8221 | resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" |
8627 | integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== | 8222 | integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== |
8628 | 8223 | ||
8629 | netmask@^2.0.1: | ||
8630 | version "2.0.2" | ||
8631 | resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" | ||
8632 | integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== | ||
8633 | |||
8634 | next-event@^1.0.0: | ||
8635 | version "1.0.0" | ||
8636 | resolved "https://registry.yarnpkg.com/next-event/-/next-event-1.0.0.tgz#e7778acde2e55802e0ad1879c39cf6f75eda61d8" | ||
8637 | integrity sha512-IXGPhl/yAiUU597gz+k5OYxYZkmLSWTcPPcpQjWABud9OK6m/ZNLrVdcEu4e7NgmOObFIhgZVg1jecPYT/6AoA== | ||
8638 | |||
8639 | ngx-uploadx@^6.1.0: | 8224 | ngx-uploadx@^6.1.0: |
8640 | version "6.1.0" | 8225 | version "6.1.0" |
8641 | resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-6.1.0.tgz#40f00c352ba5a1af5b4bbe78a6a7572518314f8c" | 8226 | resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-6.1.0.tgz#40f00c352ba5a1af5b4bbe78a6a7572518314f8c" |
@@ -8690,7 +8275,7 @@ node-forge@^1: | |||
8690 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" | 8275 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" |
8691 | integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== | 8276 | integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== |
8692 | 8277 | ||
8693 | node-gyp-build@^4.2.0, node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: | 8278 | node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: |
8694 | version "4.6.0" | 8279 | version "4.6.0" |
8695 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" | 8280 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" |
8696 | integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== | 8281 | integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== |
@@ -9151,13 +8736,6 @@ p-try@^2.0.0: | |||
9151 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" | 8736 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" |
9152 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== | 8737 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== |
9153 | 8738 | ||
9154 | package-json-versionify@^1.0.4: | ||
9155 | version "1.0.4" | ||
9156 | resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17" | ||
9157 | integrity sha512-mtKKtCeSZMtWcc5hHJS6OlEGP7J9g7WN6vWCCZi2hCXFag/Zmjokh6WFFTQb9TuMnBcZpRjhhMQyOyglPCAahw== | ||
9158 | dependencies: | ||
9159 | browserify-package-json "^1.0.0" | ||
9160 | |||
9161 | pacote@15.1.3: | 8739 | pacote@15.1.3: |
9162 | version "15.1.3" | 8740 | version "15.1.3" |
9163 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.3.tgz#4c0e7fb5e7ab3b27fb3f86514b451ad4c4f64e9d" | 8741 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.3.tgz#4c0e7fb5e7ab3b27fb3f86514b451ad4c4f64e9d" |
@@ -9234,19 +8812,6 @@ parse-srcset@^1.0.2: | |||
9234 | resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" | 8812 | resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" |
9235 | integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== | 8813 | integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== |
9236 | 8814 | ||
9237 | parse-torrent@^9.1.5: | ||
9238 | version "9.1.5" | ||
9239 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.5.tgz#fcae5f360d9baf617d9a2de68e74d5de4c8099fd" | ||
9240 | integrity sha512-K8FXRwTOaZMI0/xuv0dpng1MVHZRtMJ0jRWBJ3qZWVNTrC1MzWUxm9QwaXDz/2qPhV2XC4UIHI92IGHwseAwaA== | ||
9241 | dependencies: | ||
9242 | bencode "^2.0.2" | ||
9243 | blob-to-buffer "^1.2.9" | ||
9244 | get-stdin "^8.0.0" | ||
9245 | magnet-uri "^6.2.0" | ||
9246 | queue-microtask "^1.2.3" | ||
9247 | simple-get "^4.0.1" | ||
9248 | simple-sha1 "^3.1.0" | ||
9249 | |||
9250 | parse5-html-rewriting-stream@7.0.0: | 8815 | parse5-html-rewriting-stream@7.0.0: |
9251 | version "7.0.0" | 8816 | version "7.0.0" |
9252 | resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" | 8817 | resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" |
@@ -9403,11 +8968,6 @@ picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch | |||
9403 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" | 8968 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" |
9404 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== | 8969 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== |
9405 | 8970 | ||
9406 | piece-length@^2.0.1: | ||
9407 | version "2.0.1" | ||
9408 | resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-2.0.1.tgz#dbed4e78976955f34466d0a65304d0cb21914ac9" | ||
9409 | integrity sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug== | ||
9410 | |||
9411 | pify@^2.0.0: | 8971 | pify@^2.0.0: |
9412 | version "2.3.0" | 8972 | version "2.3.0" |
9413 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" | 8973 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" |
@@ -9660,11 +9220,6 @@ pump@^3.0.0: | |||
9660 | end-of-stream "^1.1.0" | 9220 | end-of-stream "^1.1.0" |
9661 | once "^1.3.1" | 9221 | once "^1.3.1" |
9662 | 9222 | ||
9663 | punycode@1.3.2: | ||
9664 | version "1.3.2" | ||
9665 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" | ||
9666 | integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== | ||
9667 | |||
9668 | punycode@^2.1.0: | 9223 | punycode@^2.1.0: |
9669 | version "2.3.0" | 9224 | version "2.3.0" |
9670 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" | 9225 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" |
@@ -9715,26 +9270,11 @@ query-selector-shadow-dom@^1.0.0: | |||
9715 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" | 9270 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" |
9716 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== | 9271 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== |
9717 | 9272 | ||
9718 | querystring@0.2.0: | ||
9719 | version "0.2.0" | ||
9720 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" | ||
9721 | integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== | ||
9722 | |||
9723 | querystring@^0.2.1: | ||
9724 | version "0.2.1" | ||
9725 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" | ||
9726 | integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== | ||
9727 | |||
9728 | queue-microtask@^1.2.2, queue-microtask@^1.2.3: | 9273 | queue-microtask@^1.2.2, queue-microtask@^1.2.3: |
9729 | version "1.2.3" | 9274 | version "1.2.3" |
9730 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" | 9275 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
9731 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== | 9276 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
9732 | 9277 | ||
9733 | queue-tick@^1.0.0, queue-tick@^1.0.1: | ||
9734 | version "1.0.1" | ||
9735 | resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" | ||
9736 | integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== | ||
9737 | |||
9738 | quick-lru@^4.0.1: | 9278 | quick-lru@^4.0.1: |
9739 | version "4.0.1" | 9279 | version "4.0.1" |
9740 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" | 9280 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" |
@@ -9745,29 +9285,12 @@ quick-lru@^5.1.1: | |||
9745 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" | 9285 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" |
9746 | integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== | 9286 | integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== |
9747 | 9287 | ||
9748 | random-access-file@^2.0.1: | ||
9749 | version "2.2.1" | ||
9750 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.1.tgz#071d086d8a92cc65abbd32b42aeba6d1d845d68d" | ||
9751 | integrity sha512-RGU0xmDqdOyEiynob1KYSeh8+9c9Td1MJ74GT1viMEYAn8SJ9oBtWCXLsYZukCF46yududHOdM449uRYbzBrZQ== | ||
9752 | dependencies: | ||
9753 | mkdirp-classic "^0.5.2" | ||
9754 | random-access-storage "^1.1.1" | ||
9755 | |||
9756 | random-access-storage@^1.1.1: | ||
9757 | version "1.4.3" | ||
9758 | resolved "https://registry.yarnpkg.com/random-access-storage/-/random-access-storage-1.4.3.tgz#277d07005107562dfea84798eb9a6acd47d64b7f" | ||
9759 | integrity sha512-D5e2iIC5dNENWyBxsjhEnNOMCwZZ64TARK6dyMN+3g4OTC4MJxyjh9hKLjTGoNhDOPrgjI+YlFEHFnrp/cSnzQ== | ||
9760 | dependencies: | ||
9761 | events "^3.3.0" | ||
9762 | inherits "^2.0.3" | ||
9763 | queue-tick "^1.0.0" | ||
9764 | |||
9765 | random-iterate@^1.0.1: | 9288 | random-iterate@^1.0.1: |
9766 | version "1.0.1" | 9289 | version "1.0.1" |
9767 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" | 9290 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" |
9768 | integrity sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA== | 9291 | integrity sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA== |
9769 | 9292 | ||
9770 | randombytes@^2.0.3, randombytes@^2.0.5, randombytes@^2.1.0: | 9293 | randombytes@^2.1.0: |
9771 | version "2.1.0" | 9294 | version "2.1.0" |
9772 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" | 9295 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" |
9773 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== | 9296 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== |
@@ -9779,13 +9302,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: | |||
9779 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" | 9302 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" |
9780 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== | 9303 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== |
9781 | 9304 | ||
9782 | range-slice-stream@^2.0.0: | ||
9783 | version "2.0.0" | ||
9784 | resolved "https://registry.yarnpkg.com/range-slice-stream/-/range-slice-stream-2.0.0.tgz#1f25fc7a2cacf9ccd140c46f9cf670a1a7fe3ce6" | ||
9785 | integrity sha512-PPYLwZ63lXi6Tv2EZ8w3M4FzC0rVqvxivaOVS8pXSp5FMIHFnvi4MWHL3UdFLhwSy50aNtJsgjY0mBC6oFL26Q== | ||
9786 | dependencies: | ||
9787 | readable-stream "^3.0.2" | ||
9788 | |||
9789 | raw-body@2.5.1: | 9305 | raw-body@2.5.1: |
9790 | version "2.5.1" | 9306 | version "2.5.1" |
9791 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" | 9307 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" |
@@ -9804,11 +9320,6 @@ raw-loader@^4.0.2: | |||
9804 | loader-utils "^2.0.0" | 9320 | loader-utils "^2.0.0" |
9805 | schema-utils "^3.0.0" | 9321 | schema-utils "^3.0.0" |
9806 | 9322 | ||
9807 | rc4@^0.1.5: | ||
9808 | version "0.1.5" | ||
9809 | resolved "https://registry.yarnpkg.com/rc4/-/rc4-0.1.5.tgz#08c6e04a0168f6eb621c22ab6cb1151bd9f4a64d" | ||
9810 | integrity sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA== | ||
9811 | |||
9812 | react-is@^18.0.0: | 9323 | react-is@^18.0.0: |
9813 | version "18.2.0" | 9324 | version "18.2.0" |
9814 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" | 9325 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" |
@@ -9917,7 +9428,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable | |||
9917 | string_decoder "~1.1.1" | 9428 | string_decoder "~1.1.1" |
9918 | util-deprecate "~1.0.1" | 9429 | util-deprecate "~1.0.1" |
9919 | 9430 | ||
9920 | readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: | 9431 | readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: |
9921 | version "3.6.2" | 9432 | version "3.6.2" |
9922 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" | 9433 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" |
9923 | integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== | 9434 | integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== |
@@ -9947,13 +9458,6 @@ rechoir@^0.8.0: | |||
9947 | dependencies: | 9458 | dependencies: |
9948 | resolve "^1.20.0" | 9459 | resolve "^1.20.0" |
9949 | 9460 | ||
9950 | record-cache@^1.2.0: | ||
9951 | version "1.2.0" | ||
9952 | resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.2.0.tgz#e601bc4f164d58330cc00055e27aa4682291c882" | ||
9953 | integrity sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw== | ||
9954 | dependencies: | ||
9955 | b4a "^1.3.1" | ||
9956 | |||
9957 | recursive-readdir@^2.2.3: | 9461 | recursive-readdir@^2.2.3: |
9958 | version "2.2.3" | 9462 | version "2.2.3" |
9959 | resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" | 9463 | resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" |
@@ -10036,17 +9540,6 @@ relateurl@^0.2.7: | |||
10036 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" | 9540 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" |
10037 | integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== | 9541 | integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== |
10038 | 9542 | ||
10039 | render-media@^4.1.0: | ||
10040 | version "4.1.0" | ||
10041 | resolved "https://registry.yarnpkg.com/render-media/-/render-media-4.1.0.tgz#9188376822653d7e56c2d789d157c81e74fee0cb" | ||
10042 | integrity sha512-F5BMWDmgATEoyPCtKjmGNTGN1ghoZlfRQ3MJh8dS/MrvIUIxupiof/Y9uahChipXcqQ57twVbgMmyQmuO1vokw== | ||
10043 | dependencies: | ||
10044 | debug "^4.2.0" | ||
10045 | is-ascii "^1.0.0" | ||
10046 | mediasource "^2.4.0" | ||
10047 | stream-to-blob-url "^3.0.2" | ||
10048 | videostream "^3.2.2" | ||
10049 | |||
10050 | renderkid@^3.0.0: | 9543 | renderkid@^3.0.0: |
10051 | version "3.0.0" | 9544 | version "3.0.0" |
10052 | resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" | 9545 | resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" |
@@ -10218,14 +9711,7 @@ run-async@^3.0.0: | |||
10218 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" | 9711 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" |
10219 | integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== | 9712 | integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== |
10220 | 9713 | ||
10221 | run-parallel-limit@^1.1.0: | 9714 | run-parallel@^1.1.9, run-parallel@^1.2.0: |
10222 | version "1.1.0" | ||
10223 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" | ||
10224 | integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== | ||
10225 | dependencies: | ||
10226 | queue-microtask "^1.2.2" | ||
10227 | |||
10228 | run-parallel@^1.1.2, run-parallel@^1.1.9, run-parallel@^1.2.0: | ||
10229 | version "1.2.0" | 9715 | version "1.2.0" |
10230 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" | 9716 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" |
10231 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== | 9717 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== |
@@ -10237,11 +9723,6 @@ run-series@^1.1.9: | |||
10237 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" | 9723 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" |
10238 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== | 9724 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== |
10239 | 9725 | ||
10240 | rusha@^0.8.13: | ||
10241 | version "0.8.14" | ||
10242 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68" | ||
10243 | integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA== | ||
10244 | |||
10245 | rust-result@^1.0.0: | 9726 | rust-result@^1.0.0: |
10246 | version "1.0.0" | 9727 | version "1.0.0" |
10247 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" | 9728 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" |
@@ -10546,12 +10027,12 @@ sigstore@^1.3.0: | |||
10546 | make-fetch-happen "^11.0.1" | 10027 | make-fetch-happen "^11.0.1" |
10547 | tuf-js "^1.1.3" | 10028 | tuf-js "^1.1.3" |
10548 | 10029 | ||
10549 | simple-concat@^1.0.0, simple-concat@^1.0.1: | 10030 | simple-concat@^1.0.0: |
10550 | version "1.0.1" | 10031 | version "1.0.1" |
10551 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" | 10032 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" |
10552 | integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== | 10033 | integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== |
10553 | 10034 | ||
10554 | simple-get@^4.0.0, simple-get@^4.0.1: | 10035 | simple-get@^4.0.0: |
10555 | version "4.0.1" | 10036 | version "4.0.1" |
10556 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" | 10037 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" |
10557 | integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== | 10038 | integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== |
@@ -10573,14 +10054,6 @@ simple-peer@^9.11.0, simple-peer@^9.11.1: | |||
10573 | randombytes "^2.1.0" | 10054 | randombytes "^2.1.0" |
10574 | readable-stream "^3.6.0" | 10055 | readable-stream "^3.6.0" |
10575 | 10056 | ||
10576 | simple-sha1@^3.0.1, simple-sha1@^3.1.0: | ||
10577 | version "3.1.0" | ||
10578 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" | ||
10579 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== | ||
10580 | dependencies: | ||
10581 | queue-microtask "^1.2.2" | ||
10582 | rusha "^0.8.13" | ||
10583 | |||
10584 | simple-websocket@^9.1.0: | 10057 | simple-websocket@^9.1.0: |
10585 | version "9.1.0" | 10058 | version "9.1.0" |
10586 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" | 10059 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" |
@@ -10775,19 +10248,6 @@ spdy@^4.0.2: | |||
10775 | select-hose "^2.0.0" | 10248 | select-hose "^2.0.0" |
10776 | spdy-transport "^3.0.0" | 10249 | spdy-transport "^3.0.0" |
10777 | 10250 | ||
10778 | speed-limiter@^1.0.2: | ||
10779 | version "1.0.2" | ||
10780 | resolved "https://registry.yarnpkg.com/speed-limiter/-/speed-limiter-1.0.2.tgz#e4632f476a1d25d32557aad7bd089b3a0d948116" | ||
10781 | integrity sha512-Ax+TbUOho84bWUc3AKqWtkIvAIVws7d6QI4oJkgH4yQ5Yil+lR3vjd/7qd51dHKGzS5bFxg0++QwyNRN7s6rZA== | ||
10782 | dependencies: | ||
10783 | limiter "^1.1.5" | ||
10784 | streamx "^2.10.3" | ||
10785 | |||
10786 | speedometer@^1.1.0: | ||
10787 | version "1.1.0" | ||
10788 | resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.1.0.tgz#a30b13abda45687a1a76977012c060f2ac8a7934" | ||
10789 | integrity sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ== | ||
10790 | |||
10791 | split2@^4.1.0: | 10251 | split2@^4.1.0: |
10792 | version "4.2.0" | 10252 | version "4.2.0" |
10793 | resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" | 10253 | resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" |
@@ -10800,13 +10260,6 @@ split@0.3: | |||
10800 | dependencies: | 10260 | dependencies: |
10801 | through "2" | 10261 | through "2" |
10802 | 10262 | ||
10803 | split@^1.0.1: | ||
10804 | version "1.0.1" | ||
10805 | resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" | ||
10806 | integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== | ||
10807 | dependencies: | ||
10808 | through "2" | ||
10809 | |||
10810 | sprintf-js@~1.0.2: | 10263 | sprintf-js@~1.0.2: |
10811 | version "1.0.3" | 10264 | version "1.0.3" |
10812 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" | 10265 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" |
@@ -10850,14 +10303,6 @@ stop-iteration-iterator@^1.0.0: | |||
10850 | dependencies: | 10303 | dependencies: |
10851 | internal-slot "^1.0.4" | 10304 | internal-slot "^1.0.4" |
10852 | 10305 | ||
10853 | stream-browserify@^3.0.0: | ||
10854 | version "3.0.0" | ||
10855 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" | ||
10856 | integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== | ||
10857 | dependencies: | ||
10858 | inherits "~2.0.4" | ||
10859 | readable-stream "^3.5.0" | ||
10860 | |||
10861 | stream-buffers@^3.0.2: | 10306 | stream-buffers@^3.0.2: |
10862 | version "3.0.2" | 10307 | version "3.0.2" |
10863 | resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" | 10308 | resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" |
@@ -10870,43 +10315,6 @@ stream-combiner@~0.0.4: | |||
10870 | dependencies: | 10315 | dependencies: |
10871 | duplexer "~0.1.1" | 10316 | duplexer "~0.1.1" |
10872 | 10317 | ||
10873 | stream-http@^3.0.0: | ||
10874 | version "3.2.0" | ||
10875 | resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" | ||
10876 | integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== | ||
10877 | dependencies: | ||
10878 | builtin-status-codes "^3.0.0" | ||
10879 | inherits "^2.0.4" | ||
10880 | readable-stream "^3.6.0" | ||
10881 | xtend "^4.0.2" | ||
10882 | |||
10883 | stream-to-blob-url@^3.0.2: | ||
10884 | version "3.0.2" | ||
10885 | resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-3.0.2.tgz#5574d139e2a6d1435945476f0a9469947f2da4fb" | ||
10886 | integrity sha512-PS6wT2ZyyR38Cy+lE6PBEI1ZmO2HdzZoLeDGG0zZbYikCZd0dh8FUoSeFzgWLItpBYw1WJmPVRLpykRV+lAWLQ== | ||
10887 | dependencies: | ||
10888 | stream-to-blob "^2.0.0" | ||
10889 | |||
10890 | stream-to-blob@^2.0.0, stream-to-blob@^2.0.1: | ||
10891 | version "2.0.1" | ||
10892 | resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-2.0.1.tgz#59ab71d7a7f0bfb899570e886e44d39f4ac4381a" | ||
10893 | integrity sha512-GXlqXt3svqwIVWoICenix5Poxi4KbCF0BdXXUbpU1X4vq1V8wmjiEIU3aFJzCGNFpKxfbnG0uoowS3nKUgSPYg== | ||
10894 | |||
10895 | stream-with-known-length-to-buffer@^1.0.4: | ||
10896 | version "1.0.4" | ||
10897 | resolved "https://registry.yarnpkg.com/stream-with-known-length-to-buffer/-/stream-with-known-length-to-buffer-1.0.4.tgz#6a8aec53f27b8f481f962337c951aa3916fb60d1" | ||
10898 | integrity sha512-ztP79ug6S+I7td0Nd2GBeIKCm+vA54c+e60FY87metz5n/l6ydPELd2lxsljz8OpIhsRM9HkIiAwz85+S5G5/A== | ||
10899 | dependencies: | ||
10900 | once "^1.4.0" | ||
10901 | |||
10902 | streamx@^2.10.3: | ||
10903 | version "2.13.2" | ||
10904 | resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.13.2.tgz#9de43569a1cd54980d128673b3c1429b79afff1c" | ||
10905 | integrity sha512-+TWqixPhGDXEG9L/XczSbhfkmwAtGs3BJX5QNU6cvno+pOLKeszByWcnaTu6dg8efsTYqR8ZZuXWHhZfgrxMvA== | ||
10906 | dependencies: | ||
10907 | fast-fifo "^1.1.0" | ||
10908 | queue-tick "^1.0.1" | ||
10909 | |||
10910 | "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | 10318 | "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: |
10911 | version "4.2.3" | 10319 | version "4.2.3" |
10912 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | 10320 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" |
@@ -10969,7 +10377,7 @@ string.prototype.trimstart@^1.0.6: | |||
10969 | define-properties "^1.1.4" | 10377 | define-properties "^1.1.4" |
10970 | es-abstract "^1.20.4" | 10378 | es-abstract "^1.20.4" |
10971 | 10379 | ||
10972 | string2compact@^1.3.0, string2compact@^1.3.2: | 10380 | string2compact@^1.3.0: |
10973 | version "1.3.2" | 10381 | version "1.3.2" |
10974 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.2.tgz#c9d11a13f368404b8025425cc53f9916de1d0b8b" | 10382 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.2.tgz#c9d11a13f368404b8025425cc53f9916de1d0b8b" |
10975 | integrity sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw== | 10383 | integrity sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw== |
@@ -11306,31 +10714,16 @@ text-table@0.2.0, text-table@^0.2.0: | |||
11306 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" | 10714 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" |
11307 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== | 10715 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== |
11308 | 10716 | ||
11309 | thirty-two@^1.0.2: | ||
11310 | version "1.0.2" | ||
11311 | resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" | ||
11312 | integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA== | ||
11313 | |||
11314 | through@2, through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: | 10717 | through@2, through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: |
11315 | version "2.3.8" | 10718 | version "2.3.8" |
11316 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" | 10719 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" |
11317 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== | 10720 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== |
11318 | 10721 | ||
11319 | throughput@^1.0.1: | 10722 | thunky@^1.0.2: |
11320 | version "1.0.1" | ||
11321 | resolved "https://registry.yarnpkg.com/throughput/-/throughput-1.0.1.tgz#f8474cfc8f2f0eb740410bc23fa920b0bdba6d53" | ||
11322 | integrity sha512-4Mvv5P4xyVz6RM07wS3tGyZ/kPAiKtLeqznq3hK4pxDiTUSyQ5xeFlBiWxflCWexvSnxo2aAfedzKajJqihz4Q== | ||
11323 | |||
11324 | thunky@^1.0.1, thunky@^1.0.2: | ||
11325 | version "1.1.0" | 10723 | version "1.1.0" |
11326 | resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" | 10724 | resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" |
11327 | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== | 10725 | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== |
11328 | 10726 | ||
11329 | timeout-refresh@^1.0.0: | ||
11330 | version "1.0.3" | ||
11331 | resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-1.0.3.tgz#7024a8ce0a09a57acc2ea86002048e6c0bff7375" | ||
11332 | integrity sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA== | ||
11333 | |||
11334 | tmp@0.2.1, tmp@~0.2.1: | 10727 | tmp@0.2.1, tmp@~0.2.1: |
11335 | version "0.2.1" | 10728 | version "0.2.1" |
11336 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" | 10729 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" |
@@ -11345,11 +10738,6 @@ tmp@^0.0.33: | |||
11345 | dependencies: | 10738 | dependencies: |
11346 | os-tmpdir "~1.0.2" | 10739 | os-tmpdir "~1.0.2" |
11347 | 10740 | ||
11348 | to-arraybuffer@^1.0.1: | ||
11349 | version "1.0.1" | ||
11350 | resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" | ||
11351 | integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== | ||
11352 | |||
11353 | to-fast-properties@^2.0.0: | 10741 | to-fast-properties@^2.0.0: |
11354 | version "2.0.0" | 10742 | version "2.0.0" |
11355 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" | 10743 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" |
@@ -11372,22 +10760,6 @@ tokenizr@^1.6.4: | |||
11372 | resolved "https://registry.yarnpkg.com/tokenizr/-/tokenizr-1.6.9.tgz#67e7fc575fb73ae1145afe166e5e21a27d725b0d" | 10760 | resolved "https://registry.yarnpkg.com/tokenizr/-/tokenizr-1.6.9.tgz#67e7fc575fb73ae1145afe166e5e21a27d725b0d" |
11373 | integrity sha512-JeEey5bD1S0hsUEANaEKqqa4HDRfZRy8I1Enx+Rpb7wD1nDMqN53g6I9nhAOejPCfG5m0gOE73H4jT3NuK29Cw== | 10761 | integrity sha512-JeEey5bD1S0hsUEANaEKqqa4HDRfZRy8I1Enx+Rpb7wD1nDMqN53g6I9nhAOejPCfG5m0gOE73H4jT3NuK29Cw== |
11374 | 10762 | ||
11375 | torrent-discovery@^9.4.13: | ||
11376 | version "9.4.15" | ||
11377 | resolved "https://registry.yarnpkg.com/torrent-discovery/-/torrent-discovery-9.4.15.tgz#95f983543d3e5259857116532cecca4aa979e494" | ||
11378 | integrity sha512-71nx+TpLaF27mbsSj/tZTr588Dfk7XVzx+Rf1+nrxfXqe8qn5dIlRhgA+yY4cg8Ib69vWwkKFhAzbRqg8z42aw== | ||
11379 | dependencies: | ||
11380 | bittorrent-dht "^10.0.7" | ||
11381 | bittorrent-lsd "^1.1.1" | ||
11382 | bittorrent-tracker "^9.19.0" | ||
11383 | debug "^4.3.4" | ||
11384 | run-parallel "^1.2.0" | ||
11385 | |||
11386 | torrent-piece@^2.0.1: | ||
11387 | version "2.0.1" | ||
11388 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.1.tgz#a1a50fffa589d9bf9560e38837230708bc3afdc6" | ||
11389 | integrity sha512-JLSOyvQVLI6JTWqioY4vFL0JkEUKQcaHQsU3loxkCvPTSttw8ePs2tFwsP4XIjw99Fz8EdOzt/4faykcbnPbCQ== | ||
11390 | |||
11391 | totalist@^1.0.0: | 10763 | totalist@^1.0.0: |
11392 | version "1.1.0" | 10764 | version "1.1.0" |
11393 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" | 10765 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" |
@@ -11559,13 +10931,6 @@ uglify-js@^3.0.6: | |||
11559 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" | 10931 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" |
11560 | integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== | 10932 | integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== |
11561 | 10933 | ||
11562 | uint64be@^2.0.2: | ||
11563 | version "2.0.2" | ||
11564 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" | ||
11565 | integrity sha512-9QqdvpGQTXgxthP+lY4e/gIBy+RuqcBaC6JVwT5I3bDLgT/btL6twZMR0pI3/Fgah9G/pdwzIprE5gL6v9UvyQ== | ||
11566 | dependencies: | ||
11567 | buffer-alloc "^1.1.0" | ||
11568 | |||
11569 | unbox-primitive@^1.0.2: | 10934 | unbox-primitive@^1.0.2: |
11570 | version "1.0.2" | 10935 | version "1.0.2" |
11571 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" | 10936 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" |
@@ -11645,11 +11010,6 @@ unordered-array-remove@^1.0.2: | |||
11645 | resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef" | 11010 | resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef" |
11646 | integrity sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw== | 11011 | integrity sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw== |
11647 | 11012 | ||
11648 | unordered-set@^2.0.1: | ||
11649 | version "2.0.1" | ||
11650 | resolved "https://registry.yarnpkg.com/unordered-set/-/unordered-set-2.0.1.tgz#4cd0fe27b8814bcf5d6073e5f0966ec7a50841e6" | ||
11651 | integrity sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg== | ||
11652 | |||
11653 | unpipe@1.0.0, unpipe@~1.0.0: | 11013 | unpipe@1.0.0, unpipe@~1.0.0: |
11654 | version "1.0.0" | 11014 | version "1.0.0" |
11655 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" | 11015 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" |
@@ -11696,33 +11056,6 @@ url-toolkit@^2.2.1: | |||
11696 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" | 11056 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" |
11697 | integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== | 11057 | integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== |
11698 | 11058 | ||
11699 | url@^0.11.0: | ||
11700 | version "0.11.0" | ||
11701 | resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" | ||
11702 | integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ== | ||
11703 | dependencies: | ||
11704 | punycode "1.3.2" | ||
11705 | querystring "0.2.0" | ||
11706 | |||
11707 | ut_metadata@^3.5.2: | ||
11708 | version "3.5.2" | ||
11709 | resolved "https://registry.yarnpkg.com/ut_metadata/-/ut_metadata-3.5.2.tgz#2351c9348759e929978fa6a08d56ef6f584749e7" | ||
11710 | integrity sha512-3XZZuJSeoIUyMYSuDbTbVtP4KAVGHPfU8nmHFkr8LJc+THCaUXwnu/2AV+LCSLarET/hL9IlbNfYTGrt6fOVuQ== | ||
11711 | dependencies: | ||
11712 | bencode "^2.0.1" | ||
11713 | bitfield "^4.0.0" | ||
11714 | debug "^4.2.0" | ||
11715 | simple-sha1 "^3.0.1" | ||
11716 | |||
11717 | ut_pex@^3.0.2: | ||
11718 | version "3.0.2" | ||
11719 | resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-3.0.2.tgz#cd794d4fe02ebfa82704d41854c76c8d8187eea0" | ||
11720 | integrity sha512-3xM88t+AVU5GR0sIY3tmRMLUS+YKiwStc7U7+ZFQ+UHQpX7BjVJOomhmtm0Bs+8R2n812Dt2ymXm01EqDrOOpQ== | ||
11721 | dependencies: | ||
11722 | bencode "^2.0.2" | ||
11723 | compact2string "^1.4.1" | ||
11724 | string2compact "^1.3.2" | ||
11725 | |||
11726 | utf-8-validate@^5.0.5: | 11059 | utf-8-validate@^5.0.5: |
11727 | version "5.0.10" | 11060 | version "5.0.10" |
11728 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" | 11061 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" |
@@ -11745,17 +11078,6 @@ utils-merge@1.0.1: | |||
11745 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" | 11078 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" |
11746 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== | 11079 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== |
11747 | 11080 | ||
11748 | utp-native@^2.5.3: | ||
11749 | version "2.5.3" | ||
11750 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.3.tgz#7c04c2a8c2858716555a77d10adb9819e3119b25" | ||
11751 | integrity sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w== | ||
11752 | dependencies: | ||
11753 | napi-macros "^2.0.0" | ||
11754 | node-gyp-build "^4.2.0" | ||
11755 | readable-stream "^3.0.2" | ||
11756 | timeout-refresh "^1.0.0" | ||
11757 | unordered-set "^2.0.1" | ||
11758 | |||
11759 | uue@^3.1.0: | 11081 | uue@^3.1.0: |
11760 | version "3.1.2" | 11082 | version "3.1.2" |
11761 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" | 11083 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" |
@@ -11830,18 +11152,6 @@ videojs-vtt.js@^0.15.4: | |||
11830 | dependencies: | 11152 | dependencies: |
11831 | global "^4.3.1" | 11153 | global "^4.3.1" |
11832 | 11154 | ||
11833 | videostream@^3.2.2, videostream@~3.2.1: | ||
11834 | version "3.2.2" | ||
11835 | resolved "https://registry.yarnpkg.com/videostream/-/videostream-3.2.2.tgz#e3e8d44f5159892f8f31ad35cbf9302d7a6e6afc" | ||
11836 | integrity sha512-4tz23yGGeATmbzj/ZnUm6wgQ4E1lzmMXu2mUA/c0G6adtWKxm1Di5YejdZdRsK6SdkLjKjhplFFYT7r+UUDKvA== | ||
11837 | dependencies: | ||
11838 | binary-search "^1.3.4" | ||
11839 | mediasource "^2.2.2" | ||
11840 | mp4-box-encoding "^1.3.0" | ||
11841 | mp4-stream "^3.0.0" | ||
11842 | pump "^3.0.0" | ||
11843 | range-slice-stream "^2.0.0" | ||
11844 | |||
11845 | vite@4.3.1: | 11155 | vite@4.3.1: |
11846 | version "4.3.1" | 11156 | version "4.3.1" |
11847 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a" | 11157 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a" |
@@ -12158,63 +11468,6 @@ websocket-extensions@>=0.1.1: | |||
12158 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" | 11468 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" |
12159 | integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== | 11469 | integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== |
12160 | 11470 | ||
12161 | webtorrent@1.8.26: | ||
12162 | version "1.8.26" | ||
12163 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-1.8.26.tgz#c40313f3329d2bdfe8ae23365c17dd77825a829d" | ||
12164 | integrity sha512-1bbCIDtbk4OA7xXmT87t6jDhnng6RNC9d7HNpRyvxF0GQTrIz1fB3oDnNcbOim9Upjy1GDqxAOe0Mejmc86TUg== | ||
12165 | dependencies: | ||
12166 | "@webtorrent/http-node" "^1.3.0" | ||
12167 | addr-to-ip-port "^1.5.4" | ||
12168 | bitfield "^4.1.0" | ||
12169 | bittorrent-dht "^10.0.4" | ||
12170 | bittorrent-protocol "^3.5.5" | ||
12171 | cache-chunk-store "^3.2.2" | ||
12172 | chrome-net "^3.3.4" | ||
12173 | chunk-store-stream "^4.3.0" | ||
12174 | cpus "^1.0.3" | ||
12175 | create-torrent "^5.0.4" | ||
12176 | debug "^4.3.4" | ||
12177 | end-of-stream "^1.4.4" | ||
12178 | escape-html "^1.0.3" | ||
12179 | fs-chunk-store "^2.0.5" | ||
12180 | immediate-chunk-store "^2.2.0" | ||
12181 | load-ip-set "^2.2.1" | ||
12182 | lt_donthave "^1.0.1" | ||
12183 | memory-chunk-store "^1.3.5" | ||
12184 | mime "^3.0.0" | ||
12185 | multistream "^4.1.0" | ||
12186 | package-json-versionify "^1.0.4" | ||
12187 | parse-torrent "^9.1.5" | ||
12188 | pump "^3.0.0" | ||
12189 | queue-microtask "^1.2.3" | ||
12190 | random-iterate "^1.0.1" | ||
12191 | randombytes "^2.1.0" | ||
12192 | range-parser "^1.2.1" | ||
12193 | render-media "^4.1.0" | ||
12194 | run-parallel "^1.2.0" | ||
12195 | run-parallel-limit "^1.1.0" | ||
12196 | simple-concat "^1.0.1" | ||
12197 | simple-get "^4.0.1" | ||
12198 | simple-peer "^9.11.1" | ||
12199 | simple-sha1 "^3.1.0" | ||
12200 | speed-limiter "^1.0.2" | ||
12201 | stream-to-blob "^2.0.1" | ||
12202 | stream-to-blob-url "^3.0.2" | ||
12203 | stream-with-known-length-to-buffer "^1.0.4" | ||
12204 | throughput "^1.0.1" | ||
12205 | torrent-discovery "^9.4.13" | ||
12206 | torrent-piece "^2.0.1" | ||
12207 | unordered-array-remove "^1.0.2" | ||
12208 | ut_metadata "^3.5.2" | ||
12209 | ut_pex "^3.0.2" | ||
12210 | optionalDependencies: | ||
12211 | utp-native "^2.5.3" | ||
12212 | |||
12213 | whatwg-fetch@^3.0.0: | ||
12214 | version "3.6.2" | ||
12215 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" | ||
12216 | integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== | ||
12217 | |||
12218 | whatwg-url@^5.0.0: | 11471 | whatwg-url@^5.0.0: |
12219 | version "5.0.0" | 11472 | version "5.0.0" |
12220 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" | 11473 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" |
@@ -12377,11 +11630,6 @@ xmlhttprequest-ssl@~2.0.0: | |||
12377 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" | 11630 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" |
12378 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== | 11631 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== |
12379 | 11632 | ||
12380 | xtend@^4.0.2: | ||
12381 | version "4.0.2" | ||
12382 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" | ||
12383 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== | ||
12384 | |||
12385 | y18n@^3.2.1: | 11633 | y18n@^3.2.1: |
12386 | version "3.2.2" | 11634 | version "3.2.2" |
12387 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" | 11635 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" |
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index d03d0fe83..696a097b1 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts | |||
@@ -72,7 +72,10 @@ const playerKeys = { | |||
72 | 'Next video': 'Next video', | 72 | 'Next video': 'Next video', |
73 | 'This video is password protected': 'This video is password protected', | 73 | 'This video is password protected': 'This video is password protected', |
74 | 'You need a password to watch this video.': 'You need a password to watch this video.', | 74 | 'You need a password to watch this video.': 'You need a password to watch this video.', |
75 | 'Incorrect password, please enter a correct password': 'Incorrect password, please enter a correct password' | 75 | 'Incorrect password, please enter a correct password': 'Incorrect password, please enter a correct password', |
76 | 'Cancel': 'Cancel', | ||
77 | 'Up Next': 'Up Next', | ||
78 | 'Autoplay is suspended': 'Autoplay is suspended' | ||
76 | } | 79 | } |
77 | Object.assign(playerKeys, videojs) | 80 | Object.assign(playerKeys, videojs) |
78 | 81 | ||
diff --git a/server/tests/api/check-params/video-storyboards.ts b/server/tests/api/check-params/video-storyboards.ts index a43d8fc48..c038e7370 100644 --- a/server/tests/api/check-params/video-storyboards.ts +++ b/server/tests/api/check-params/video-storyboards.ts | |||
@@ -12,7 +12,7 @@ describe('Test video storyboards API validator', function () { | |||
12 | // --------------------------------------------------------------- | 12 | // --------------------------------------------------------------- |
13 | 13 | ||
14 | before(async function () { | 14 | before(async function () { |
15 | this.timeout(30000) | 15 | this.timeout(120000) |
16 | 16 | ||
17 | server = await createSingleServer(1) | 17 | server = await createSingleServer(1) |
18 | await setAccessTokensToServers([ server ]) | 18 | await setAccessTokensToServers([ server ]) |
diff --git a/shared/models/metrics/playback-metric-create.model.ts b/shared/models/metrics/playback-metric-create.model.ts index d669ab690..3a8f328c8 100644 --- a/shared/models/metrics/playback-metric-create.model.ts +++ b/shared/models/metrics/playback-metric-create.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { VideoResolution } from '../videos' | 1 | import { VideoResolution } from '../videos' |
2 | 2 | ||
3 | export interface PlaybackMetricCreate { | 3 | export interface PlaybackMetricCreate { |
4 | playerMode: 'p2p-media-loader' | 'webtorrent' | 4 | playerMode: 'p2p-media-loader' | 'webtorrent' | 'web-video' // FIXME: remove webtorrent player mode not used anymore in PeerTube v6 |
5 | 5 | ||
6 | resolution?: VideoResolution | 6 | resolution?: VideoResolution |
7 | fps?: number | 7 | fps?: number |
diff --git a/shared/models/plugins/client/client-hook.model.ts b/shared/models/plugins/client/client-hook.model.ts index bc3f5dd9f..4a0818c99 100644 --- a/shared/models/plugins/client/client-hook.model.ts +++ b/shared/models/plugins/client/client-hook.model.ts | |||
@@ -59,6 +59,10 @@ export const clientFilterHookObject = { | |||
59 | 'filter:internal.video-watch.player.build-options.params': true, | 59 | 'filter:internal.video-watch.player.build-options.params': true, |
60 | 'filter:internal.video-watch.player.build-options.result': true, | 60 | 'filter:internal.video-watch.player.build-options.result': true, |
61 | 61 | ||
62 | // Filter the options to load a new video in our player | ||
63 | 'filter:internal.video-watch.player.load-options.params': true, | ||
64 | 'filter:internal.video-watch.player.load-options.result': true, | ||
65 | |||
62 | // Filter our SVG icons content | 66 | // Filter our SVG icons content |
63 | 'filter:internal.common.svg-icons.get-content.params': true, | 67 | 'filter:internal.common.svg-icons.get-content.params': true, |
64 | 'filter:internal.common.svg-icons.get-content.result': true, | 68 | 'filter:internal.common.svg-icons.get-content.result': true, |