aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-01-29 08:37:25 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-02-11 09:13:02 +0100
commit092092969633bbcf6d4891a083ea497a7d5c3154 (patch)
tree69e82fe4f60c444cca216830e96afe143a9dac71 /client/src
parent4348a27d252a3349bafa7ef4859c0e2cf060c255 (diff)
downloadPeerTube-092092969633bbcf6d4891a083ea497a7d5c3154.tar.gz
PeerTube-092092969633bbcf6d4891a083ea497a7d5c3154.tar.zst
PeerTube-092092969633bbcf6d4891a083ea497a7d5c3154.zip
Add hls support on server
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts6
-rw-r--r--client/src/app/core/server/server.service.ts5
-rw-r--r--client/src/app/shared/video/video-details.model.ts13
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts28
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts (renamed from client/src/assets/player/p2p-media-loader-plugin.ts)67
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-url-builder.ts28
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-validator.ts56
-rw-r--r--client/src/assets/player/peertube-player-manager.ts45
-rw-r--r--client/src/assets/player/peertube-plugin.ts4
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts15
-rw-r--r--client/src/assets/player/utils.ts14
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts11
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts (renamed from client/src/assets/player/webtorrent-plugin.ts)29
-rw-r--r--client/src/standalone/videos/embed.ts21
14 files changed, 278 insertions, 64 deletions
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 0b3511e8e..021b1feb4 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -22,7 +22,9 @@ export abstract class UserEdit extends FormReactive {
22 } 22 }
23 23
24 computeQuotaWithTranscoding () { 24 computeQuotaWithTranscoding () {
25 const resolutions = this.serverService.getConfig().transcoding.enabledResolutions 25 const transcodingConfig = this.serverService.getConfig().transcoding
26
27 const resolutions = transcodingConfig.enabledResolutions
26 const higherResolution = VideoResolution.H_1080P 28 const higherResolution = VideoResolution.H_1080P
27 let multiplier = 0 29 let multiplier = 0
28 30
@@ -30,6 +32,8 @@ export abstract class UserEdit extends FormReactive {
30 multiplier += resolution / higherResolution 32 multiplier += resolution / higherResolution
31 } 33 }
32 34
35 if (transcodingConfig.hls.enabled) multiplier *= 2
36
33 return multiplier * parseInt(this.form.value['videoQuota'], 10) 37 return multiplier * parseInt(this.form.value['videoQuota'], 10)
34 } 38 }
35 39
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 4ae72427b..c868ccdcc 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -51,7 +51,10 @@ export class ServerService {
51 requiresEmailVerification: false 51 requiresEmailVerification: false
52 }, 52 },
53 transcoding: { 53 transcoding: {
54 enabledResolutions: [] 54 enabledResolutions: [],
55 hls: {
56 enabled: false
57 }
55 }, 58 },
56 avatar: { 59 avatar: {
57 file: { 60 file: {
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index fa4ca7f93..f44b4138b 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 3import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model'
7import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type'
6 8
7export class VideoDetails extends Video implements VideoDetailsServerModel { 9export class VideoDetails extends Video implements VideoDetailsServerModel {
8 descriptionPath: string 10 descriptionPath: string
@@ -19,6 +21,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
19 likesPercent: number 21 likesPercent: number
20 dislikesPercent: number 22 dislikesPercent: number
21 23
24 trackerUrls: string[]
25
26 streamingPlaylists: VideoStreamingPlaylist[]
27
22 constructor (hash: VideoDetailsServerModel, translations = {}) { 28 constructor (hash: VideoDetailsServerModel, translations = {}) {
23 super(hash, translations) 29 super(hash, translations)
24 30
@@ -30,6 +36,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
30 this.support = hash.support 36 this.support = hash.support
31 this.commentsEnabled = hash.commentsEnabled 37 this.commentsEnabled = hash.commentsEnabled
32 38
39 this.trackerUrls = hash.trackerUrls
40 this.streamingPlaylists = hash.streamingPlaylists
41
33 this.buildLikeAndDislikePercents() 42 this.buildLikeAndDislikePercents()
34 } 43 }
35 44
@@ -53,4 +62,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
53 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 62 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
54 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 63 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
55 } 64 }
65
66 getHlsPlaylist () {
67 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
68 }
56} 69}
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 6e38af195..f77316712 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -23,7 +23,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
23import { environment } from '../../../environments/environment' 23import { environment } from '../../../environments/environment'
24import { VideoCaptionService } from '@app/shared/video-caption' 24import { VideoCaptionService } from '@app/shared/video-caption'
25import { MarkdownService } from '@app/shared/renderer' 25import { MarkdownService } from '@app/shared/renderer'
26import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager' 26import { P2PMediaLoaderOptions, PeertubePlayerManager, PlayerMode, WebtorrentOptions } from '../../../assets/player/peertube-player-manager'
27 27
28@Component({ 28@Component({
29 selector: 'my-video-watch', 29 selector: 'my-video-watch',
@@ -424,15 +424,33 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
424 serverUrl: environment.apiUrl, 424 serverUrl: environment.apiUrl,
425 425
426 videoCaptions: playerCaptions 426 videoCaptions: playerCaptions
427 }, 427 }
428 }
428 429
429 webtorrent: { 430 let mode: PlayerMode
431 const hlsPlaylist = this.video.getHlsPlaylist()
432 if (hlsPlaylist) {
433 mode = 'p2p-media-loader'
434 const p2pMediaLoader = {
435 playlistUrl: hlsPlaylist.playlistUrl,
436 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
437 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
438 trackerAnnounce: this.video.trackerUrls,
430 videoFiles: this.video.files 439 videoFiles: this.video.files
431 } 440 } as P2PMediaLoaderOptions
441
442 Object.assign(options, { p2pMediaLoader })
443 } else {
444 mode = 'webtorrent'
445 const webtorrent = {
446 videoFiles: this.video.files
447 } as WebtorrentOptions
448
449 Object.assign(options, { webtorrent })
432 } 450 }
433 451
434 this.zone.runOutsideAngular(async () => { 452 this.zone.runOutsideAngular(async () => {
435 this.player = await PeertubePlayerManager.initialize('webtorrent', options) 453 this.player = await PeertubePlayerManager.initialize(mode, options)
436 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) 454 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
437 }) 455 })
438 456
diff --git a/client/src/assets/player/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index a5b20219f..f9a2707fb 100644
--- a/client/src/assets/player/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -1,21 +1,21 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore 2// @ts-ignore
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from './peertube-videojs-typings' 4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events } from 'p2p-media-loader-core'
5 7
6// videojs-hlsjs-plugin needs videojs in window 8// videojs-hlsjs-plugin needs videojs in window
7window['videojs'] = videojs 9window['videojs'] = videojs
8require('@streamroot/videojs-hlsjs-plugin') 10require('@streamroot/videojs-hlsjs-plugin')
9 11
10import { Engine, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
11import { Events } from 'p2p-media-loader-core'
12
13const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 12const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
14class P2pMediaLoaderPlugin extends Plugin { 13class P2pMediaLoaderPlugin extends Plugin {
15 14
16 private readonly CONSTANTS = { 15 private readonly CONSTANTS = {
17 INFO_SCHEDULER: 1000 // Don't change this 16 INFO_SCHEDULER: 1000 // Don't change this
18 } 17 }
18 private readonly options: P2PMediaLoaderPluginOptions
19 19
20 private hlsjs: any // Don't type hlsjs to not bundle the module 20 private hlsjs: any // Don't type hlsjs to not bundle the module
21 private p2pEngine: Engine 21 private p2pEngine: Engine
@@ -26,16 +26,22 @@ class P2pMediaLoaderPlugin extends Plugin {
26 totalDownload: 0, 26 totalDownload: 0,
27 totalUpload: 0 27 totalUpload: 0
28 } 28 }
29 private statsHTTPBytes = {
30 pendingDownload: [] as number[],
31 pendingUpload: [] as number[],
32 totalDownload: 0,
33 totalUpload: 0
34 }
29 35
30 private networkInfoInterval: any 36 private networkInfoInterval: any
31 37
32 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { 38 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) {
33 super(player, options) 39 super(player, options)
34 40
41 this.options = options
42
35 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { 43 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
36 this.hlsjs = hlsjs 44 this.hlsjs = hlsjs
37
38 this.initialize()
39 }) 45 })
40 46
41 initVideoJsContribHlsJsPlayer(player) 47 initVideoJsContribHlsJsPlayer(player)
@@ -44,6 +50,8 @@ class P2pMediaLoaderPlugin extends Plugin {
44 type: options.type, 50 type: options.type,
45 src: options.src 51 src: options.src
46 }) 52 })
53
54 player.ready(() => this.initialize())
47 } 55 }
48 56
49 dispose () { 57 dispose () {
@@ -51,6 +59,8 @@ class P2pMediaLoaderPlugin extends Plugin {
51 } 59 }
52 60
53 private initialize () { 61 private initialize () {
62 initHlsJsPlayer(this.hlsjs)
63
54 this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine() 64 this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine()
55 65
56 // Avoid using constants to not import hls.hs 66 // Avoid using constants to not import hls.hs
@@ -59,38 +69,55 @@ class P2pMediaLoaderPlugin extends Plugin {
59 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) 69 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
60 }) 70 })
61 71
72 this.p2pEngine.on(Events.SegmentError, (segment, err) => {
73 console.error('Segment error.', segment, err)
74 })
75
76 this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length
77
62 this.runStats() 78 this.runStats()
63 } 79 }
64 80
65 private runStats () { 81 private runStats () {
66 this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { 82 this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => {
67 if (method === 'p2p') { 83 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
68 this.statsP2PBytes.pendingDownload.push(size) 84
69 this.statsP2PBytes.totalDownload += size 85 elem.pendingDownload.push(size)
70 } 86 elem.totalDownload += size
71 }) 87 })
72 88
73 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { 89 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => {
74 if (method === 'p2p') { 90 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
75 this.statsP2PBytes.pendingUpload.push(size) 91
76 this.statsP2PBytes.totalUpload += size 92 elem.pendingUpload.push(size)
77 } 93 elem.totalUpload += size
78 }) 94 })
79 95
80 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) 96 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
81 this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) 97 this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
82 98
83 this.networkInfoInterval = setInterval(() => { 99 this.networkInfoInterval = setInterval(() => {
84 let downloadSpeed = this.statsP2PBytes.pendingDownload.reduce((a: number, b: number) => a + b, 0) 100 const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
85 let uploadSpeed = this.statsP2PBytes.pendingUpload.reduce((a: number, b: number) => a + b, 0) 101 const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
102
103 const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
104 const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload)
86 105
87 this.statsP2PBytes.pendingDownload = [] 106 this.statsP2PBytes.pendingDownload = []
88 this.statsP2PBytes.pendingUpload = [] 107 this.statsP2PBytes.pendingUpload = []
108 this.statsHTTPBytes.pendingDownload = []
109 this.statsHTTPBytes.pendingUpload = []
89 110
90 return this.player.trigger('p2pInfo', { 111 return this.player.trigger('p2pInfo', {
112 http: {
113 downloadSpeed: httpDownloadSpeed,
114 uploadSpeed: httpUploadSpeed,
115 downloaded: this.statsHTTPBytes.totalDownload,
116 uploaded: this.statsHTTPBytes.totalUpload
117 },
91 p2p: { 118 p2p: {
92 downloadSpeed, 119 downloadSpeed: p2pDownloadSpeed,
93 uploadSpeed, 120 uploadSpeed: p2pUploadSpeed,
94 numPeers: this.statsP2PBytes.numPeers, 121 numPeers: this.statsP2PBytes.numPeers,
95 downloaded: this.statsP2PBytes.totalDownload, 122 downloaded: this.statsP2PBytes.totalDownload,
96 uploaded: this.statsP2PBytes.totalUpload 123 uploaded: this.statsP2PBytes.totalUpload
@@ -98,6 +125,10 @@ class P2pMediaLoaderPlugin extends Plugin {
98 } as PlayerNetworkInfo) 125 } as PlayerNetworkInfo)
99 }, this.CONSTANTS.INFO_SCHEDULER) 126 }, this.CONSTANTS.INFO_SCHEDULER)
100 } 127 }
128
129 private arraySum (data: number[]) {
130 return data.reduce((a: number, b: number) => a + b, 0)
131 }
101} 132}
102 133
103videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) 134videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
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
new file mode 100644
index 000000000..32e7ce4f2
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts
@@ -0,0 +1,28 @@
1import { basename } from 'path'
2import { Segment } from 'p2p-media-loader-core'
3
4function segmentUrlBuilderFactory (baseUrls: string[]) {
5 return function segmentBuilder (segment: Segment) {
6 const max = baseUrls.length + 1
7 const i = getRandomInt(max)
8
9 if (i === max - 1) return segment.url
10
11 let newBaseUrl = baseUrls[i]
12 let middlePart = newBaseUrl.endsWith('/') ? '' : '/'
13
14 return newBaseUrl + middlePart + basename(segment.url)
15 }
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 segmentUrlBuilderFactory
22}
23
24// ---------------------------------------------------------------------------
25
26function getRandomInt (max: number) {
27 return Math.floor(Math.random() * Math.floor(max))
28}
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts
new file mode 100644
index 000000000..8f4922daa
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts
@@ -0,0 +1,56 @@
1import { Segment } from 'p2p-media-loader-core'
2import { basename } from 'path'
3
4function segmentValidatorFactory (segmentsSha256Url: string) {
5 const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
6
7 return async function segmentValidator (segment: Segment) {
8 const segmentName = basename(segment.url)
9
10 const hashShouldBe = (await segmentsJSON)[segmentName]
11 if (hashShouldBe === undefined) {
12 throw new Error(`Unknown segment name ${segmentName} in segment validator`)
13 }
14
15 const calculatedSha = bufferToEx(await sha256(segment.data))
16 if (calculatedSha !== hashShouldBe) {
17 throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`)
18 }
19 }
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 segmentValidatorFactory
26}
27
28// ---------------------------------------------------------------------------
29
30function fetchSha256Segments (url: string) {
31 return fetch(url)
32 .then(res => res.json())
33 .catch(err => {
34 console.error('Cannot get sha256 segments', err)
35 return {}
36 })
37}
38
39function sha256 (data?: ArrayBuffer) {
40 if (!data) return undefined
41
42 return window.crypto.subtle.digest('SHA-256', data)
43}
44
45// Thanks: https://stackoverflow.com/a/53307879
46function bufferToEx (buffer?: ArrayBuffer) {
47 if (!buffer) return ''
48
49 let s = ''
50 const h = '0123456789abcdef'
51 const o = new Uint8Array(buffer)
52
53 o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ])
54
55 return s
56}
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 91ca6a2aa..3fdba6fdf 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -13,8 +13,10 @@ import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-load-progress-bar' 13import './videojs-components/peertube-load-progress-bar'
14import './videojs-components/theater-button' 14import './videojs-components/theater-button'
15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' 15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
16import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' 16import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
17import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' 17import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
18import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
19import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
18 20
19// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) 21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
20videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' 22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
@@ -31,7 +33,10 @@ export type WebtorrentOptions = {
31 33
32export type P2PMediaLoaderOptions = { 34export type P2PMediaLoaderOptions = {
33 playlistUrl: string 35 playlistUrl: string
36 segmentsSha256Url: string
34 trackerAnnounce: string[] 37 trackerAnnounce: string[]
38 redundancyBaseUrls: string[]
39 videoFiles: VideoFile[]
35} 40}
36 41
37export type CommonOptions = { 42export type CommonOptions = {
@@ -90,11 +95,11 @@ export class PeertubePlayerManager {
90 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { 95 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
91 let p2pMediaLoader: any 96 let p2pMediaLoader: any
92 97
93 if (mode === 'webtorrent') await import('./webtorrent-plugin') 98 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
94 if (mode === 'p2p-media-loader') { 99 if (mode === 'p2p-media-loader') {
95 [ p2pMediaLoader ] = await Promise.all([ 100 [ p2pMediaLoader ] = await Promise.all([
96 import('p2p-media-loader-hlsjs'), 101 import('p2p-media-loader-hlsjs'),
97 import('./p2p-media-loader-plugin') 102 import('./p2p-media-loader/p2p-media-loader-plugin')
98 ]) 103 ])
99 } 104 }
100 105
@@ -144,11 +149,14 @@ export class PeertubePlayerManager {
144 const commonOptions = options.common 149 const commonOptions = options.common
145 const webtorrentOptions = options.webtorrent 150 const webtorrentOptions = options.webtorrent
146 const p2pMediaLoaderOptions = options.p2pMediaLoader 151 const p2pMediaLoaderOptions = options.p2pMediaLoader
152
153 let autoplay = options.common.autoplay
147 let html5 = {} 154 let html5 = {}
148 155
149 const plugins: VideoJSPluginOptions = { 156 const plugins: VideoJSPluginOptions = {
150 peertube: { 157 peertube: {
151 autoplay: commonOptions.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent 158 mode,
159 autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
152 videoViewUrl: commonOptions.videoViewUrl, 160 videoViewUrl: commonOptions.videoViewUrl,
153 videoDuration: commonOptions.videoDuration, 161 videoDuration: commonOptions.videoDuration,
154 startTime: commonOptions.startTime, 162 startTime: commonOptions.startTime,
@@ -160,19 +168,35 @@ export class PeertubePlayerManager {
160 168
161 if (p2pMediaLoaderOptions) { 169 if (p2pMediaLoaderOptions) {
162 const p2pMediaLoader: P2PMediaLoaderPluginOptions = { 170 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
171 redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls,
163 type: 'application/x-mpegURL', 172 type: 'application/x-mpegURL',
164 src: p2pMediaLoaderOptions.playlistUrl 173 src: p2pMediaLoaderOptions.playlistUrl
165 } 174 }
166 175
176 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
177 .filter(t => t.startsWith('ws'))
178
167 const p2pMediaLoaderConfig = { 179 const p2pMediaLoaderConfig = {
168 // loader: { 180 loader: {
169 // trackerAnnounce: p2pMediaLoaderOptions.trackerAnnounce 181 trackerAnnounce,
170 // }, 182 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url),
183 rtcConfig: getRtcConfig(),
184 requiredSegmentsPriority: 5,
185 segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls)
186 },
171 segments: { 187 segments: {
172 swarmId: p2pMediaLoaderOptions.playlistUrl 188 swarmId: p2pMediaLoaderOptions.playlistUrl
173 } 189 }
174 } 190 }
175 const streamrootHls = { 191 const streamrootHls = {
192 levelLabelHandler: (level: { height: number, width: number }) => {
193 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height)
194
195 let label = file.resolution.label
196 if (file.fps >= 50) label += file.fps
197
198 return label
199 },
176 html5: { 200 html5: {
177 hlsjsConfig: { 201 hlsjsConfig: {
178 liveSyncDurationCount: 7, 202 liveSyncDurationCount: 7,
@@ -187,12 +211,15 @@ export class PeertubePlayerManager {
187 211
188 if (webtorrentOptions) { 212 if (webtorrentOptions) {
189 const webtorrent = { 213 const webtorrent = {
190 autoplay: commonOptions.autoplay, 214 autoplay,
191 videoDuration: commonOptions.videoDuration, 215 videoDuration: commonOptions.videoDuration,
192 playerElement: commonOptions.playerElement, 216 playerElement: commonOptions.playerElement,
193 videoFiles: webtorrentOptions.videoFiles 217 videoFiles: webtorrentOptions.videoFiles
194 } 218 }
195 Object.assign(plugins, { webtorrent }) 219 Object.assign(plugins, { webtorrent })
220
221 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
222 autoplay = false
196 } 223 }
197 224
198 const videojsOptions = { 225 const videojsOptions = {
@@ -208,7 +235,7 @@ export class PeertubePlayerManager {
208 : undefined, // Undefined so the player knows it has to check the local storage 235 : undefined, // Undefined so the player knows it has to check the local storage
209 236
210 poster: commonOptions.poster, 237 poster: commonOptions.poster,
211 autoplay: false, 238 autoplay,
212 inactivityTimeout: commonOptions.inactivityTimeout, 239 inactivityTimeout: commonOptions.inactivityTimeout,
213 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], 240 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
214 plugins, 241 plugins,
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index f83d9094a..aacbf5f6e 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -52,12 +52,12 @@ class PeerTubePlugin extends Plugin {
52 this.player.ready(() => { 52 this.player.ready(() => {
53 const playerOptions = this.player.options_ 53 const playerOptions = this.player.options_
54 54
55 if (this.player.webtorrent) { 55 if (options.mode === 'webtorrent') {
56 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) 56 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
57 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) 57 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
58 } 58 }
59 59
60 if (this.player.p2pMediaLoader) { 60 if (options.mode === 'p2p-media-loader') {
61 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) 61 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
62 } 62 }
63 63
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index fff992a6f..79a5a6c4d 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -4,12 +4,15 @@ import * as videojs from 'video.js'
4 4
5import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
6import { PeerTubePlugin } from './peertube-plugin' 6import { PeerTubePlugin } from './peertube-plugin'
7import { WebTorrentPlugin } from './webtorrent-plugin' 7import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
8import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
9import { PlayerMode } from './peertube-player-manager'
8 10
9declare namespace videojs { 11declare namespace videojs {
10 interface Player { 12 interface Player {
11 peertube (): PeerTubePlugin 13 peertube (): PeerTubePlugin
12 webtorrent (): WebTorrentPlugin 14 webtorrent (): WebTorrentPlugin
15 p2pMediaLoader (): P2pMediaLoaderPlugin
13 } 16 }
14} 17}
15 18
@@ -33,6 +36,8 @@ type UserWatching = {
33} 36}
34 37
35type PeerTubePluginOptions = { 38type PeerTubePluginOptions = {
39 mode: PlayerMode
40
36 autoplay: boolean 41 autoplay: boolean
37 videoViewUrl: string 42 videoViewUrl: string
38 videoDuration: number 43 videoDuration: number
@@ -54,6 +59,7 @@ type WebtorrentPluginOptions = {
54} 59}
55 60
56type P2PMediaLoaderPluginOptions = { 61type P2PMediaLoaderPluginOptions = {
62 redundancyBaseUrls: string[]
57 type: string 63 type: string
58 src: string 64 src: string
59} 65}
@@ -91,6 +97,13 @@ type AutoResolutionUpdateData = {
91} 97}
92 98
93type PlayerNetworkInfo = { 99type PlayerNetworkInfo = {
100 http: {
101 downloadSpeed: number
102 uploadSpeed: number
103 downloaded: number
104 uploaded: number
105 }
106
94 p2p: { 107 p2p: {
95 downloadSpeed: number 108 downloadSpeed: number
96 uploadSpeed: number 109 uploadSpeed: number
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 8b9f34b99..8d87567c2 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -112,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) {
112 return min 112 return min
113} 113}
114 114
115function getRtcConfig () {
116 return {
117 iceServers: [
118 {
119 urls: 'stun:stun.stunprotocol.org'
120 },
121 {
122 urls: 'stun:stun.framasoft.org'
123 }
124 ]
125 }
126}
127
115// --------------------------------------------------------------------------- 128// ---------------------------------------------------------------------------
116 129
117export { 130export {
131 getRtcConfig,
118 toTitleCase, 132 toTitleCase,
119 timeToInt, 133 timeToInt,
120 buildVideoLink, 134 buildVideoLink,
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
index 2fc4c4562..6424787b2 100644
--- a/client/src/assets/player/videojs-components/p2p-info-button.ts
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -75,11 +75,12 @@ class P2pInfoButton extends Button {
75 } 75 }
76 76
77 const p2pStats = data.p2p 77 const p2pStats = data.p2p
78 const httpStats = data.http
78 79
79 const downloadSpeed = bytes(p2pStats.downloadSpeed) 80 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
80 const uploadSpeed = bytes(p2pStats.uploadSpeed) 81 const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed)
81 const totalDownloaded = bytes(p2pStats.downloaded) 82 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
82 const totalUploaded = bytes(p2pStats.uploaded) 83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
83 const numPeers = p2pStats.numPeers 84 const numPeers = p2pStats.numPeers
84 85
85 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + 86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
@@ -92,7 +93,7 @@ class P2pInfoButton extends Button {
92 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] 93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
93 94
94 peersNumber.textContent = numPeers 95 peersNumber.textContent = numPeers
95 peersText.textContent = ' ' + this.player_.localize('peers') 96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer'))
96 97
97 subDivHttp.className = 'vjs-peertube-hidden' 98 subDivHttp.className = 'vjs-peertube-hidden'
98 subDivWebtorrent.className = 'vjs-peertube-displayed' 99 subDivWebtorrent.className = 'vjs-peertube-displayed'
diff --git a/client/src/assets/player/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 47f169e24..c69bf31fa 100644
--- a/client/src/assets/player/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -3,18 +3,18 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import * as WebTorrent from 'webtorrent' 5import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 6import { VideoFile } from '../../../../../shared/models/videos/video.model'
7import { renderVideo } from './webtorrent/video-renderer' 7import { renderVideo } from './video-renderer'
8import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings' 8import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
9import { videoFileMaxByResolution, videoFileMinByResolution } from './utils' 9import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
10import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store' 10import { PeertubeChunkStore } from './peertube-chunk-store'
11import { 11import {
12 getAverageBandwidthInStore, 12 getAverageBandwidthInStore,
13 getStoredMute, 13 getStoredMute,
14 getStoredVolume, 14 getStoredVolume,
15 getStoredWebTorrentEnabled, 15 getStoredWebTorrentEnabled,
16 saveAverageBandwidth 16 saveAverageBandwidth
17} from './peertube-player-local-storage' 17} from '../peertube-player-local-storage'
18 18
19const CacheChunkStore = require('cache-chunk-store') 19const CacheChunkStore = require('cache-chunk-store')
20 20
@@ -44,16 +44,7 @@ class WebTorrentPlugin extends Plugin {
44 44
45 private readonly webtorrent = new WebTorrent({ 45 private readonly webtorrent = new WebTorrent({
46 tracker: { 46 tracker: {
47 rtcConfig: { 47 rtcConfig: getRtcConfig()
48 iceServers: [
49 {
50 urls: 'stun:stun.stunprotocol.org'
51 },
52 {
53 urls: 'stun:stun.framasoft.org'
54 }
55 ]
56 }
57 }, 48 },
58 dht: false 49 dht: false
59 }) 50 })
@@ -472,6 +463,12 @@ class WebTorrentPlugin extends Plugin {
472 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) 463 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
473 464
474 return this.player.trigger('p2pInfo', { 465 return this.player.trigger('p2pInfo', {
466 http: {
467 downloadSpeed: 0,
468 uploadSpeed: 0,
469 downloaded: 0,
470 uploaded: 0
471 },
475 p2p: { 472 p2p: {
476 downloadSpeed: this.torrent.downloadSpeed, 473 downloadSpeed: this.torrent.downloadSpeed,
477 numPeers: this.torrent.numPeers, 474 numPeers: this.torrent.numPeers,
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 6dd9a3d76..1e58d42d9 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -23,7 +23,13 @@ import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
23import { PeerTubeResolution } from '../player/definitions' 23import { PeerTubeResolution } from '../player/definitions'
24import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 24import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
25import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 25import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
26import { PeertubePlayerManager, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' 26import {
27 P2PMediaLoaderOptions,
28 PeertubePlayerManager,
29 PeertubePlayerManagerOptions,
30 PlayerMode
31} from '../../assets/player/peertube-player-manager'
32import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
27 33
28/** 34/**
29 * Embed API exposes control of the embed player to the outside world via 35 * Embed API exposes control of the embed player to the outside world via
@@ -319,13 +325,16 @@ class PeerTubeEmbed {
319 } 325 }
320 326
321 if (this.mode === 'p2p-media-loader') { 327 if (this.mode === 'p2p-media-loader') {
328 const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
329
322 Object.assign(options, { 330 Object.assign(options, {
323 p2pMediaLoader: { 331 p2pMediaLoader: {
324 // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8' 332 playlistUrl: hlsPlaylist.playlistUrl,
325 // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8' 333 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
326 // trackerAnnounce: [ window.location.origin.replace(/^http/, 'ws') + '/tracker/socket' ], 334 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
327 playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8' 335 trackerAnnounce: videoInfo.trackerUrls,
328 } 336 videoFiles: videoInfo.files
337 } as P2PMediaLoaderOptions
329 }) 338 })
330 } else { 339 } else {
331 Object.assign(options, { 340 Object.assign(options, {