diff options
Diffstat (limited to 'client/src/assets')
12 files changed, 128 insertions, 207 deletions
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts index 53969a5a5..3050110cd 100644 --- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts | |||
@@ -264,20 +264,16 @@ class Html5Hlsjs { | |||
264 | if (this.errorCounts[ data.type ]) this.errorCounts[ data.type ] += 1 | 264 | if (this.errorCounts[ data.type ]) this.errorCounts[ data.type ] += 1 |
265 | else this.errorCounts[ data.type ] = 1 | 265 | else this.errorCounts[ data.type ] = 1 |
266 | 266 | ||
267 | if (!data.fatal) { | 267 | if (data.fatal) console.warn(error.message) |
268 | console.warn(error.message) | 268 | else console.error(error.message, data) |
269 | return | ||
270 | } | ||
271 | |||
272 | console.error(error.message) | ||
273 | 269 | ||
274 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { | 270 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { |
275 | error.code = 2 | 271 | error.code = 2 |
276 | this._handleNetworkError(error) | 272 | this._handleNetworkError(error) |
277 | } else if (data.type === Hlsjs.ErrorTypes.MEDIA_ERROR && data.details !== 'manifestIncompatibleCodecsError') { | 273 | } else if (data.fatal && data.type === Hlsjs.ErrorTypes.MEDIA_ERROR && data.details !== 'manifestIncompatibleCodecsError') { |
278 | error.code = 3 | 274 | error.code = 3 |
279 | this._handleMediaError(error) | 275 | this._handleMediaError(error) |
280 | } else { | 276 | } else if (data.fatal) { |
281 | this.hls.destroy() | 277 | this.hls.destroy() |
282 | console.info('bubbling error up to VIDEOJS') | 278 | console.info('bubbling error up to VIDEOJS') |
283 | this.tech.error = () => error as any | 279 | this.tech.error = () => error as any |
@@ -286,12 +282,12 @@ class Html5Hlsjs { | |||
286 | } | 282 | } |
287 | 283 | ||
288 | private switchQuality (qualityId: number) { | 284 | private switchQuality (qualityId: number) { |
289 | this.hls.nextLevel = qualityId | 285 | this.hls.currentLevel = qualityId |
290 | } | 286 | } |
291 | 287 | ||
292 | private _levelLabel (level: Hlsjs.Level) { | 288 | private _levelLabel (level: Hlsjs.Level) { |
293 | if (this.player.srOptions_.levelLabelHandler) { | 289 | if (this.player.srOptions_.levelLabelHandler) { |
294 | return this.player.srOptions_.levelLabelHandler(level) | 290 | return this.player.srOptions_.levelLabelHandler(level as any) |
295 | } | 291 | } |
296 | 292 | ||
297 | if (level.height) return level.height + 'p' | 293 | if (level.height) return level.height + 'p' |
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index f1bd9f0c4..093795e48 100644 --- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import * as Hlsjs from 'hls.js/dist/hls.light.js' | 1 | import * as Hlsjs from 'hls.js/dist/hls.light.js' |
2 | import { Events, Segment } from 'p2p-media-loader-core' | ||
3 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
4 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { Events, Segment } from '@peertube/p2p-media-loader-core' | ||
4 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' | ||
5 | import { timeToInt } from '@shared/core-utils' | ||
5 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' | 6 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' |
6 | import { timeToInt } from '../utils' | ||
7 | import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' | 7 | import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' |
8 | 8 | ||
9 | registerConfigPlugin(videojs) | 9 | registerConfigPlugin(videojs) |
@@ -36,9 +36,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
36 | 36 | ||
37 | private networkInfoInterval: any | 37 | private networkInfoInterval: any |
38 | 38 | ||
39 | private hlsjsCurrentLevel: number | ||
40 | private hlsjsLevels: Hlsjs.Level[] | ||
41 | |||
42 | constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) { | 39 | constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) { |
43 | super(player) | 40 | super(player) |
44 | 41 | ||
@@ -88,13 +85,12 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
88 | } | 85 | } |
89 | 86 | ||
90 | getCurrentLevel () { | 87 | getCurrentLevel () { |
91 | return this.hlsjsLevels.find(l => l.level === this.hlsjsCurrentLevel) | 88 | return this.hlsjs.levels[this.hlsjs.currentLevel] |
92 | } | 89 | } |
93 | 90 | ||
94 | getLiveLatency () { | 91 | getLiveLatency () { |
95 | return undefined as number | 92 | // FIXME: typings |
96 | // FIXME: Use latency when hls >= V1 | 93 | return Math.round((this.hlsjs as any).latency) |
97 | // return this.hlsjs.latency | ||
98 | } | 94 | } |
99 | 95 | ||
100 | getHLSJS () { | 96 | getHLSJS () { |
@@ -140,31 +136,23 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
140 | } | 136 | } |
141 | 137 | ||
142 | private runStats () { | 138 | private runStats () { |
143 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | 139 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, _segment, bytes: number) => { |
144 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | 140 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes |
145 | 141 | ||
146 | elem.pendingDownload.push(size) | 142 | elem.pendingDownload.push(bytes) |
147 | elem.totalDownload += size | 143 | elem.totalDownload += bytes |
148 | }) | 144 | }) |
149 | 145 | ||
150 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | 146 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => { |
151 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | 147 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes |
152 | 148 | ||
153 | elem.pendingUpload.push(size) | 149 | elem.pendingUpload.push(bytes) |
154 | elem.totalUpload += size | 150 | elem.totalUpload += bytes |
155 | }) | 151 | }) |
156 | 152 | ||
157 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | 153 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) |
158 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | 154 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) |
159 | 155 | ||
160 | this.hlsjs.on(Hlsjs.Events.MANIFEST_PARSED, (_e, manifest) => { | ||
161 | this.hlsjsCurrentLevel = manifest.firstLevel | ||
162 | this.hlsjsLevels = manifest.levels | ||
163 | }) | ||
164 | this.hlsjs.on(Hlsjs.Events.LEVEL_LOADED, (_e, level) => { | ||
165 | this.hlsjsCurrentLevel = level.levelId || (level as any).id | ||
166 | }) | ||
167 | |||
168 | this.networkInfoInterval = setInterval(() => { | 156 | this.networkInfoInterval = setInterval(() => { |
169 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | 157 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) |
170 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) | 158 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) |
diff --git a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts index 039777cea..ad0e460ae 100644 --- a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts +++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | 1 | import { Segment } from '@peertube/p2p-media-loader-core' |
2 | import { RedundancyUrlManager } from './redundancy-url-manager' | 2 | import { RedundancyUrlManager } from './redundancy-url-manager' |
3 | 3 | ||
4 | function segmentUrlBuilderFactory (redundancyUrlManager: RedundancyUrlManager) { | 4 | function segmentUrlBuilderFactory (redundancyUrlManager: RedundancyUrlManager) { |
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts index 59245ceb4..d0a4c4a3f 100644 --- a/client/src/assets/player/p2p-media-loader/segment-validator.ts +++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { wait } from '@root-helpers/utils' | 1 | import { wait } from '@root-helpers/utils' |
2 | import { Segment } from 'p2p-media-loader-core' | 2 | import { Segment } from '@peertube/p2p-media-loader-core' |
3 | import { basename } from 'path' | 3 | import { basename } from 'path' |
4 | 4 | ||
5 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } | 5 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index b071a0938..c45e8f53e 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -22,24 +22,28 @@ import './videojs-components/settings-panel-child' | |||
22 | import './videojs-components/theater-button' | 22 | import './videojs-components/theater-button' |
23 | import './playlist/playlist-plugin' | 23 | import './playlist/playlist-plugin' |
24 | import videojs from 'video.js' | 24 | import videojs from 'video.js' |
25 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | ||
25 | import { PluginsManager } from '@root-helpers/plugins-manager' | 26 | import { PluginsManager } from '@root-helpers/plugins-manager' |
27 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | ||
26 | import { isDefaultLocale } from '@shared/core-utils/i18n' | 28 | import { isDefaultLocale } from '@shared/core-utils/i18n' |
27 | import { VideoFile } from '@shared/models' | 29 | import { VideoFile } from '@shared/models' |
28 | import { copyToClipboard } from '../../root-helpers/utils' | 30 | import { copyToClipboard } from '../../root-helpers/utils' |
29 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 31 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
30 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | 32 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' |
31 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | 33 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' |
32 | import { getStoredP2PEnabled } from './peertube-player-local-storage' | 34 | import { getAverageBandwidthInStore, getStoredP2PEnabled, saveAverageBandwidth } from './peertube-player-local-storage' |
33 | import { | 35 | import { |
34 | NextPreviousVideoButtonOptions, | 36 | NextPreviousVideoButtonOptions, |
35 | P2PMediaLoaderPluginOptions, | 37 | P2PMediaLoaderPluginOptions, |
38 | PeerTubeLinkButtonOptions, | ||
39 | PlayerNetworkInfo, | ||
36 | PlaylistPluginOptions, | 40 | PlaylistPluginOptions, |
37 | UserWatching, | 41 | UserWatching, |
38 | VideoJSCaption, | 42 | VideoJSCaption, |
39 | VideoJSPluginOptions | 43 | VideoJSPluginOptions |
40 | } from './peertube-videojs-typings' | 44 | } from './peertube-videojs-typings' |
41 | import { TranslationsManager } from './translations-manager' | 45 | import { TranslationsManager } from './translations-manager' |
42 | import { buildVideoLink, buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' | 46 | import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' |
43 | 47 | ||
44 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 48 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
45 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | 49 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' |
@@ -110,6 +114,7 @@ export interface CommonOptions extends CustomizationOptions { | |||
110 | videoCaptions: VideoJSCaption[] | 114 | videoCaptions: VideoJSCaption[] |
111 | 115 | ||
112 | videoUUID: string | 116 | videoUUID: string |
117 | videoShortUUID: string | ||
113 | 118 | ||
114 | userWatching?: UserWatching | 119 | userWatching?: UserWatching |
115 | 120 | ||
@@ -145,7 +150,7 @@ export class PeertubePlayerManager { | |||
145 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') | 150 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') |
146 | if (mode === 'p2p-media-loader') { | 151 | if (mode === 'p2p-media-loader') { |
147 | [ p2pMediaLoader ] = await Promise.all([ | 152 | [ p2pMediaLoader ] = await Promise.all([ |
148 | import('p2p-media-loader-hlsjs'), | 153 | import('@peertube/p2p-media-loader-hlsjs'), |
149 | import('./p2p-media-loader/p2p-media-loader-plugin') | 154 | import('./p2p-media-loader/p2p-media-loader-plugin') |
150 | ]) | 155 | ]) |
151 | } | 156 | } |
@@ -175,7 +180,13 @@ export class PeertubePlayerManager { | |||
175 | PeertubePlayerManager.alreadyPlayed = true | 180 | PeertubePlayerManager.alreadyPlayed = true |
176 | }) | 181 | }) |
177 | 182 | ||
178 | self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) | 183 | self.addContextMenu({ |
184 | mode, | ||
185 | player, | ||
186 | videoShortUUID: options.common.videoShortUUID, | ||
187 | videoEmbedUrl: options.common.embedUrl, | ||
188 | videoEmbedTitle: options.common.embedTitle | ||
189 | }) | ||
179 | 190 | ||
180 | player.bezels() | 191 | player.bezels() |
181 | player.stats({ | 192 | player.stats({ |
@@ -184,6 +195,12 @@ export class PeertubePlayerManager { | |||
184 | mode | 195 | mode |
185 | }) | 196 | }) |
186 | 197 | ||
198 | player.on('p2pInfo', (_, data: PlayerNetworkInfo) => { | ||
199 | if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return | ||
200 | |||
201 | saveAverageBandwidth(data.bandwidthEstimate) | ||
202 | }) | ||
203 | |||
187 | return res(player) | 204 | return res(player) |
188 | }) | 205 | }) |
189 | }) | 206 | }) |
@@ -218,7 +235,13 @@ export class PeertubePlayerManager { | |||
218 | videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { | 235 | videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { |
219 | const player = this | 236 | const player = this |
220 | 237 | ||
221 | self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) | 238 | self.addContextMenu({ |
239 | mode, | ||
240 | player, | ||
241 | videoShortUUID: options.common.videoShortUUID, | ||
242 | videoEmbedUrl: options.common.embedUrl, | ||
243 | videoEmbedTitle: options.common.embedTitle | ||
244 | }) | ||
222 | 245 | ||
223 | PeertubePlayerManager.onPlayerChange(player) | 246 | PeertubePlayerManager.onPlayerChange(player) |
224 | }) | 247 | }) |
@@ -295,6 +318,8 @@ export class PeertubePlayerManager { | |||
295 | 318 | ||
296 | controlBar: { | 319 | controlBar: { |
297 | children: this.getControlBarChildren(mode, { | 320 | children: this.getControlBarChildren(mode, { |
321 | videoShortUUID: commonOptions.videoShortUUID, | ||
322 | |||
298 | captions: commonOptions.captions, | 323 | captions: commonOptions.captions, |
299 | peertubeLink: commonOptions.peertubeLink, | 324 | peertubeLink: commonOptions.peertubeLink, |
300 | theaterButton: commonOptions.theaterButton, | 325 | theaterButton: commonOptions.theaterButton, |
@@ -342,12 +367,13 @@ export class PeertubePlayerManager { | |||
342 | consumeOnly = true | 367 | consumeOnly = true |
343 | } | 368 | } |
344 | 369 | ||
345 | const p2pMediaLoaderConfig = { | 370 | const p2pMediaLoaderConfig: HlsJsEngineSettings = { |
346 | loader: { | 371 | loader: { |
347 | trackerAnnounce, | 372 | trackerAnnounce, |
348 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url, options.common.isLive), | 373 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url, options.common.isLive), |
349 | rtcConfig: getRtcConfig(), | 374 | rtcConfig: getRtcConfig(), |
350 | requiredSegmentsPriority: 1, | 375 | requiredSegmentsPriority: 1, |
376 | simultaneousHttpDownloads: 1, | ||
351 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), | 377 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), |
352 | useP2P: getStoredP2PEnabled(), | 378 | useP2P: getStoredP2PEnabled(), |
353 | consumeOnly | 379 | consumeOnly |
@@ -356,6 +382,7 @@ export class PeertubePlayerManager { | |||
356 | swarmId: p2pMediaLoaderOptions.playlistUrl | 382 | swarmId: p2pMediaLoaderOptions.playlistUrl |
357 | } | 383 | } |
358 | } | 384 | } |
385 | |||
359 | const hlsjs = { | 386 | const hlsjs = { |
360 | levelLabelHandler: (level: { height: number, width: number }) => { | 387 | levelLabelHandler: (level: { height: number, width: number }) => { |
361 | const resolution = Math.min(level.height || 0, level.width || 0) | 388 | const resolution = Math.min(level.height || 0, level.width || 0) |
@@ -370,12 +397,7 @@ export class PeertubePlayerManager { | |||
370 | return label | 397 | return label |
371 | }, | 398 | }, |
372 | html5: { | 399 | html5: { |
373 | hlsjsConfig: { | 400 | hlsjsConfig: this.getHLSOptions(p2pMediaLoaderModule, p2pMediaLoaderConfig) |
374 | capLevelToPlayerSize: true, | ||
375 | autoStartLoad: false, | ||
376 | liveSyncDurationCount: 5, | ||
377 | loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | ||
378 | } | ||
379 | } | 401 | } |
380 | } | 402 | } |
381 | 403 | ||
@@ -385,6 +407,28 @@ export class PeertubePlayerManager { | |||
385 | return toAssign | 407 | return toAssign |
386 | } | 408 | } |
387 | 409 | ||
410 | private static getHLSOptions (p2pMediaLoaderModule: any, p2pMediaLoaderConfig: HlsJsEngineSettings) { | ||
411 | const base = { | ||
412 | capLevelToPlayerSize: true, | ||
413 | autoStartLoad: false, | ||
414 | liveSyncDurationCount: 5, | ||
415 | |||
416 | loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | ||
417 | } | ||
418 | |||
419 | const averageBandwidth = getAverageBandwidthInStore() | ||
420 | if (!averageBandwidth) return base | ||
421 | |||
422 | return { | ||
423 | ...base, | ||
424 | |||
425 | abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s | ||
426 | startLevel: -1, | ||
427 | testBandwidth: false, | ||
428 | debug: false | ||
429 | } | ||
430 | } | ||
431 | |||
388 | private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) { | 432 | private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) { |
389 | const commonOptions = options.common | 433 | const commonOptions = options.common |
390 | const webtorrentOptions = options.webtorrent | 434 | const webtorrentOptions = options.webtorrent |
@@ -409,6 +453,8 @@ export class PeertubePlayerManager { | |||
409 | } | 453 | } |
410 | 454 | ||
411 | private static getControlBarChildren (mode: PlayerMode, options: { | 455 | private static getControlBarChildren (mode: PlayerMode, options: { |
456 | videoShortUUID: string | ||
457 | |||
412 | peertubeLink: boolean | 458 | peertubeLink: boolean |
413 | theaterButton: boolean | 459 | theaterButton: boolean |
414 | captions: boolean | 460 | captions: boolean |
@@ -497,7 +543,7 @@ export class PeertubePlayerManager { | |||
497 | 543 | ||
498 | if (options.peertubeLink === true) { | 544 | if (options.peertubeLink === true) { |
499 | Object.assign(children, { | 545 | Object.assign(children, { |
500 | 'peerTubeLinkButton': {} | 546 | 'peerTubeLinkButton': { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions |
501 | }) | 547 | }) |
502 | } | 548 | } |
503 | 549 | ||
@@ -514,7 +560,15 @@ export class PeertubePlayerManager { | |||
514 | return children | 560 | return children |
515 | } | 561 | } |
516 | 562 | ||
517 | private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string, videoEmbedTitle: string) { | 563 | private static addContextMenu (options: { |
564 | mode: PlayerMode | ||
565 | player: videojs.Player | ||
566 | videoShortUUID: string | ||
567 | videoEmbedUrl: string | ||
568 | videoEmbedTitle: string | ||
569 | }) { | ||
570 | const { mode, player, videoEmbedTitle, videoEmbedUrl, videoShortUUID } = options | ||
571 | |||
518 | const content = () => { | 572 | const content = () => { |
519 | const isLoopEnabled = player.options_['loop'] | 573 | const isLoopEnabled = player.options_['loop'] |
520 | const items = [ | 574 | const items = [ |
@@ -528,13 +582,15 @@ export class PeertubePlayerManager { | |||
528 | { | 582 | { |
529 | label: player.localize('Copy the video URL'), | 583 | label: player.localize('Copy the video URL'), |
530 | listener: function () { | 584 | listener: function () { |
531 | copyToClipboard(buildVideoLink()) | 585 | copyToClipboard(buildVideoLink({ shortUUID: videoShortUUID })) |
532 | } | 586 | } |
533 | }, | 587 | }, |
534 | { | 588 | { |
535 | label: player.localize('Copy the video URL at the current time'), | 589 | label: player.localize('Copy the video URL at the current time'), |
536 | listener: function (this: videojs.Player) { | 590 | listener: function (this: videojs.Player) { |
537 | copyToClipboard(buildVideoLink({ startTime: this.currentTime() })) | 591 | const url = buildVideoLink({ shortUUID: videoShortUUID }) |
592 | |||
593 | copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() })) | ||
538 | } | 594 | } |
539 | }, | 595 | }, |
540 | { | 596 | { |
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index 07c7e33f6..ade8e2ee4 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -1,12 +1,6 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import './videojs-components/settings-menu-button' | 1 | import './videojs-components/settings-menu-button' |
3 | import { | 2 | import videojs from 'video.js' |
4 | PeerTubePluginOptions, | 3 | import { timeToInt } from '@shared/core-utils' |
5 | ResolutionUpdateData, | ||
6 | UserWatching, | ||
7 | VideoJSCaption | ||
8 | } from './peertube-videojs-typings' | ||
9 | import { isMobile, timeToInt } from './utils' | ||
10 | import { | 4 | import { |
11 | getStoredLastSubtitle, | 5 | getStoredLastSubtitle, |
12 | getStoredMute, | 6 | getStoredMute, |
@@ -16,6 +10,8 @@ import { | |||
16 | saveVideoWatchHistory, | 10 | saveVideoWatchHistory, |
17 | saveVolumeInStore | 11 | saveVolumeInStore |
18 | } from './peertube-player-local-storage' | 12 | } from './peertube-player-local-storage' |
13 | import { PeerTubePluginOptions, ResolutionUpdateData, UserWatching, VideoJSCaption } from './peertube-videojs-typings' | ||
14 | import { isMobile } from './utils' | ||
19 | 15 | ||
20 | const Plugin = videojs.getPlugin('plugin') | 16 | const Plugin = videojs.getPlugin('plugin') |
21 | 17 | ||
@@ -232,6 +228,7 @@ class PeerTubePlugin extends Plugin { | |||
232 | } | 228 | } |
233 | } | 229 | } |
234 | 230 | ||
231 | console.log('Resolution changed.', data) | ||
235 | this.trigger('resolutionChange', data) | 232 | this.trigger('resolutionChange', data) |
236 | } | 233 | } |
237 | 234 | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 8afb424a7..f0eb129d4 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Config, Level } from 'hls.js' | 1 | import { HlsConfig, Level } from 'hls.js' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 3 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
4 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | 4 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' |
@@ -60,7 +60,7 @@ export interface VideoJSTechHLS extends videojs.Tech { | |||
60 | } | 60 | } |
61 | 61 | ||
62 | export interface HlsjsConfigHandlerOptions { | 62 | export interface HlsjsConfigHandlerOptions { |
63 | hlsjsConfig?: Config & { cueHandler: any }// FIXME: typings | 63 | hlsjsConfig?: HlsConfig & { cueHandler: any }// FIXME: typings |
64 | captionConfig?: any // FIXME: typings | 64 | captionConfig?: any // FIXME: typings |
65 | 65 | ||
66 | levelLabelHandler?: (level: Level) => string | 66 | levelLabelHandler?: (level: Level) => string |
@@ -132,6 +132,10 @@ type NextPreviousVideoButtonOptions = { | |||
132 | isDisabled: () => boolean | 132 | isDisabled: () => boolean |
133 | } | 133 | } |
134 | 134 | ||
135 | type PeerTubeLinkButtonOptions = { | ||
136 | shortUUID: string | ||
137 | } | ||
138 | |||
135 | type WebtorrentPluginOptions = { | 139 | type WebtorrentPluginOptions = { |
136 | playerElement: HTMLVideoElement | 140 | playerElement: HTMLVideoElement |
137 | 141 | ||
@@ -225,5 +229,6 @@ export { | |||
225 | VideoJSPluginOptions, | 229 | VideoJSPluginOptions, |
226 | LoadedQualityData, | 230 | LoadedQualityData, |
227 | QualityLevelRepresentation, | 231 | QualityLevelRepresentation, |
232 | PeerTubeLinkButtonOptions, | ||
228 | QualityLevels | 233 | QualityLevels |
229 | } | 234 | } |
diff --git a/client/src/assets/player/playlist/playlist-menu-item.ts b/client/src/assets/player/playlist/playlist-menu-item.ts index 87a72b6a3..2519a34c7 100644 --- a/client/src/assets/player/playlist/playlist-menu-item.ts +++ b/client/src/assets/player/playlist/playlist-menu-item.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { secondsToTime } from '@shared/core-utils' | ||
2 | import { VideoPlaylistElement } from '@shared/models' | 3 | import { VideoPlaylistElement } from '@shared/models' |
3 | import { PlaylistItemOptions } from '../peertube-videojs-typings' | 4 | import { PlaylistItemOptions } from '../peertube-videojs-typings' |
4 | import { secondsToTime } from '../utils' | ||
5 | 5 | ||
6 | const Component = videojs.getComponent('Component') | 6 | const Component = videojs.getComponent('Component') |
7 | 7 | ||
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts index a93f59506..b271d0526 100644 --- a/client/src/assets/player/stats/stats-card.ts +++ b/client/src/assets/player/stats/stats-card.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { secondsToTime } from '@shared/core-utils' | ||
2 | import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings' | 3 | import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings' |
3 | import { bytes, secondsToTime } from '../utils' | 4 | import { bytes } from '../utils' |
4 | 5 | ||
5 | interface StatsCardOptions extends videojs.ComponentOptions { | 6 | interface StatsCardOptions extends videojs.ComponentOptions { |
6 | videoUUID: string | 7 | videoUUID: string |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index f26176acc..f0a1b1aee 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoFile } from '@shared/models' | ||
2 | import { escapeHTML } from '@shared/core-utils/renderer' | 1 | import { escapeHTML } from '@shared/core-utils/renderer' |
2 | import { VideoFile } from '@shared/models' | ||
3 | 3 | ||
4 | function toTitleCase (str: string) { | 4 | function toTitleCase (str: string) { |
5 | return str.charAt(0).toUpperCase() + str.slice(1) | 5 | return str.charAt(0).toUpperCase() + str.slice(1) |
@@ -43,136 +43,9 @@ function isMobile () { | |||
43 | return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) | 43 | return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) |
44 | } | 44 | } |
45 | 45 | ||
46 | function buildVideoLink (options: { | ||
47 | baseUrl?: string | ||
48 | |||
49 | startTime?: number | ||
50 | stopTime?: number | ||
51 | |||
52 | subtitle?: string | ||
53 | |||
54 | loop?: boolean | ||
55 | autoplay?: boolean | ||
56 | muted?: boolean | ||
57 | |||
58 | // Embed options | ||
59 | title?: boolean | ||
60 | warningTitle?: boolean | ||
61 | controls?: boolean | ||
62 | peertubeLink?: boolean | ||
63 | } = {}) { | ||
64 | const { baseUrl } = options | ||
65 | |||
66 | const url = baseUrl | ||
67 | ? baseUrl | ||
68 | : window.location.origin + window.location.pathname.replace('/videos/embed/', '/w/') | ||
69 | |||
70 | const params = generateParams(window.location.search) | ||
71 | |||
72 | if (options.startTime !== undefined && options.startTime !== null) { | ||
73 | const startTimeInt = Math.floor(options.startTime) | ||
74 | params.set('start', secondsToTime(startTimeInt)) | ||
75 | } | ||
76 | |||
77 | if (options.stopTime) { | ||
78 | const stopTimeInt = Math.floor(options.stopTime) | ||
79 | params.set('stop', secondsToTime(stopTimeInt)) | ||
80 | } | ||
81 | |||
82 | if (options.subtitle) params.set('subtitle', options.subtitle) | ||
83 | |||
84 | if (options.loop === true) params.set('loop', '1') | ||
85 | if (options.autoplay === true) params.set('autoplay', '1') | ||
86 | if (options.muted === true) params.set('muted', '1') | ||
87 | if (options.title === false) params.set('title', '0') | ||
88 | if (options.warningTitle === false) params.set('warningTitle', '0') | ||
89 | if (options.controls === false) params.set('controls', '0') | ||
90 | if (options.peertubeLink === false) params.set('peertubeLink', '0') | ||
91 | |||
92 | return buildUrl(url, params) | ||
93 | } | ||
94 | |||
95 | function buildPlaylistLink (options: { | ||
96 | baseUrl?: string | ||
97 | |||
98 | playlistPosition?: number | ||
99 | }) { | ||
100 | const { baseUrl } = options | ||
101 | |||
102 | const url = baseUrl | ||
103 | ? baseUrl | ||
104 | : window.location.origin + window.location.pathname.replace('/video-playlists/embed/', '/w/p/') | ||
105 | |||
106 | const params = generateParams(window.location.search) | ||
107 | |||
108 | if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition) | ||
109 | |||
110 | return buildUrl(url, params) | ||
111 | } | ||
112 | |||
113 | function buildUrl (url: string, params: URLSearchParams) { | ||
114 | let hasParams = false | ||
115 | params.forEach(() => hasParams = true) | ||
116 | |||
117 | if (hasParams) return url + '?' + params.toString() | ||
118 | |||
119 | return url | ||
120 | } | ||
121 | |||
122 | function generateParams (url: string) { | ||
123 | const params = new URLSearchParams(window.location.search) | ||
124 | // Unused parameters in embed | ||
125 | params.delete('videoId') | ||
126 | params.delete('resume') | ||
127 | |||
128 | return params | ||
129 | } | ||
130 | |||
131 | function timeToInt (time: number | string) { | ||
132 | if (!time) return 0 | ||
133 | if (typeof time === 'number') return time | ||
134 | |||
135 | const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/ | ||
136 | const matches = time.match(reg) | ||
137 | |||
138 | if (!matches) return 0 | ||
139 | |||
140 | const hours = parseInt(matches[2] || '0', 10) | ||
141 | const minutes = parseInt(matches[4] || '0', 10) | ||
142 | const seconds = parseInt(matches[6] || '0', 10) | ||
143 | |||
144 | return hours * 3600 + minutes * 60 + seconds | ||
145 | } | ||
146 | |||
147 | function secondsToTime (seconds: number, full = false, symbol?: string) { | ||
148 | let time = '' | ||
149 | |||
150 | if (seconds === 0 && !full) return '0s' | ||
151 | |||
152 | const hourSymbol = (symbol || 'h') | ||
153 | const minuteSymbol = (symbol || 'm') | ||
154 | const secondsSymbol = full ? '' : 's' | ||
155 | |||
156 | const hours = Math.floor(seconds / 3600) | ||
157 | if (hours >= 1) time = hours + hourSymbol | ||
158 | else if (full) time = '0' + hourSymbol | ||
159 | |||
160 | seconds %= 3600 | ||
161 | const minutes = Math.floor(seconds / 60) | ||
162 | if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol | ||
163 | else if (minutes >= 1) time += minutes + minuteSymbol | ||
164 | else if (full) time += '00' + minuteSymbol | ||
165 | |||
166 | seconds %= 60 | ||
167 | if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol | ||
168 | else if (seconds >= 1) time += seconds + secondsSymbol | ||
169 | else if (full) time += '00' | ||
170 | |||
171 | return time | ||
172 | } | ||
173 | |||
174 | function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) { | 46 | function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) { |
175 | const title = escapeHTML(embedTitle) | 47 | const title = escapeHTML(embedTitle) |
48 | |||
176 | return '<iframe width="560" height="315" ' + | 49 | return '<iframe width="560" height="315" ' + |
177 | 'sandbox="allow-same-origin allow-scripts allow-popups" ' + | 50 | 'sandbox="allow-same-origin allow-scripts allow-popups" ' + |
178 | 'title="' + title + '" ' + | 51 | 'title="' + title + '" ' + |
@@ -221,11 +94,8 @@ function getRtcConfig () { | |||
221 | export { | 94 | export { |
222 | getRtcConfig, | 95 | getRtcConfig, |
223 | toTitleCase, | 96 | toTitleCase, |
224 | timeToInt, | ||
225 | secondsToTime, | ||
226 | isWebRTCDisabled, | 97 | isWebRTCDisabled, |
227 | buildPlaylistLink, | 98 | |
228 | buildVideoLink, | ||
229 | buildVideoOrPlaylistEmbed, | 99 | buildVideoOrPlaylistEmbed, |
230 | videoFileMaxByResolution, | 100 | videoFileMaxByResolution, |
231 | videoFileMinByResolution, | 101 | videoFileMinByResolution, |
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index e73c95900..c49cee566 100644 --- a/client/src/assets/player/videojs-components/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { buildVideoLink } from '../utils' | ||
2 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | ||
3 | import { PeerTubeLinkButtonOptions } from '../peertube-videojs-typings' | ||
3 | 4 | ||
4 | const Button = videojs.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
5 | class PeerTubeLinkButton extends Button { | 6 | class PeerTubeLinkButton extends Button { |
6 | 7 | ||
7 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { | 8 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { |
8 | super(player, options) | 9 | super(player, options as any) |
9 | } | 10 | } |
10 | 11 | ||
11 | createEl () { | 12 | createEl () { |
@@ -13,7 +14,7 @@ class PeerTubeLinkButton extends Button { | |||
13 | } | 14 | } |
14 | 15 | ||
15 | updateHref () { | 16 | updateHref () { |
16 | this.el().setAttribute('href', buildVideoLink({ startTime: this.player().currentTime() })) | 17 | this.el().setAttribute('href', this.buildLink()) |
17 | } | 18 | } |
18 | 19 | ||
19 | handleClick () { | 20 | handleClick () { |
@@ -22,7 +23,7 @@ class PeerTubeLinkButton extends Button { | |||
22 | 23 | ||
23 | private buildElement () { | 24 | private buildElement () { |
24 | const el = videojs.dom.createEl('a', { | 25 | const el = videojs.dom.createEl('a', { |
25 | href: buildVideoLink(), | 26 | href: this.buildLink(), |
26 | innerHTML: 'PeerTube', | 27 | innerHTML: 'PeerTube', |
27 | title: this.player().localize('Video page (new window)'), | 28 | title: this.player().localize('Video page (new window)'), |
28 | className: 'vjs-peertube-link', | 29 | className: 'vjs-peertube-link', |
@@ -33,6 +34,12 @@ class PeerTubeLinkButton extends Button { | |||
33 | 34 | ||
34 | return el as HTMLButtonElement | 35 | return el as HTMLButtonElement |
35 | } | 36 | } |
37 | |||
38 | private buildLink () { | ||
39 | const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID }) | ||
40 | |||
41 | return decorateVideoLink({ url, startTime: this.player().currentTime() }) | ||
42 | } | ||
36 | } | 43 | } |
37 | 44 | ||
38 | videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | 45 | videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) |
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index b648b29e8..17d369c10 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import * as WebTorrent from 'webtorrent' | 2 | import * as WebTorrent from 'webtorrent' |
3 | import { renderVideo } from './video-renderer' | 3 | import { timeToInt } from '@shared/core-utils' |
4 | import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' | 4 | import { VideoFile } from '@shared/models' |
5 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution, isIOS, isSafari } from '../utils' | ||
6 | import { PeertubeChunkStore } from './peertube-chunk-store' | ||
7 | import { | 5 | import { |
8 | getAverageBandwidthInStore, | 6 | getAverageBandwidthInStore, |
9 | getStoredMute, | 7 | getStoredMute, |
@@ -11,7 +9,10 @@ import { | |||
11 | getStoredVolume, | 9 | getStoredVolume, |
12 | saveAverageBandwidth | 10 | saveAverageBandwidth |
13 | } from '../peertube-player-local-storage' | 11 | } from '../peertube-player-local-storage' |
14 | import { VideoFile } from '@shared/models' | 12 | import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
13 | import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' | ||
14 | import { PeertubeChunkStore } from './peertube-chunk-store' | ||
15 | import { renderVideo } from './video-renderer' | ||
15 | 16 | ||
16 | const CacheChunkStore = require('cache-chunk-store') | 17 | const CacheChunkStore = require('cache-chunk-store') |
17 | 18 | ||