aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/assets/player
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/images/tick-white.svg (renamed from client/src/assets/player/images/tick.svg)0
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts143
-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.ts63
-rw-r--r--client/src/assets/player/peertube-player-local-storage.ts21
-rw-r--r--client/src/assets/player/peertube-player-manager.ts466
-rw-r--r--client/src/assets/player/peertube-player.ts284
-rw-r--r--client/src/assets/player/peertube-plugin.ts262
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts98
-rw-r--r--client/src/assets/player/resolution-menu-button.ts85
-rw-r--r--client/src/assets/player/resolution-menu-item.ts64
-rw-r--r--client/src/assets/player/utils.ts17
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts (renamed from client/src/assets/player/webtorrent-info-button.ts)25
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts (renamed from client/src/assets/player/peertube-link-button.ts)10
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts (renamed from client/src/assets/player/peertube-load-progress-bar.ts)9
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts109
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts83
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts (renamed from client/src/assets/player/settings-menu-button.ts)25
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts (renamed from client/src/assets/player/settings-menu-item.ts)49
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts (renamed from client/src/assets/player/theater-button.ts)10
-rw-r--r--client/src/assets/player/webtorrent/peertube-chunk-store.ts (renamed from client/src/assets/player/peertube-chunk-store.ts)20
-rw-r--r--client/src/assets/player/webtorrent/video-renderer.ts (renamed from client/src/assets/player/video-renderer.ts)18
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts (renamed from client/src/assets/player/peertube-videojs-plugin.ts)379
23 files changed, 1536 insertions, 732 deletions
diff --git a/client/src/assets/player/images/tick.svg b/client/src/assets/player/images/tick-white.svg
index d329e6bfb..d329e6bfb 100644
--- a/client/src/assets/player/images/tick.svg
+++ b/client/src/assets/player/images/tick-white.svg
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
new file mode 100644
index 000000000..022a9c16f
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -0,0 +1,143 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events } from 'p2p-media-loader-core'
7
8// videojs-hlsjs-plugin needs videojs in window
9window['videojs'] = videojs
10require('@streamroot/videojs-hlsjs-plugin')
11
12const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
13class P2pMediaLoaderPlugin extends Plugin {
14
15 private readonly CONSTANTS = {
16 INFO_SCHEDULER: 1000 // Don't change this
17 }
18 private readonly options: P2PMediaLoaderPluginOptions
19
20 private hlsjs: any // Don't type hlsjs to not bundle the module
21 private p2pEngine: Engine
22 private statsP2PBytes = {
23 pendingDownload: [] as number[],
24 pendingUpload: [] as number[],
25 numPeers: 0,
26 totalDownload: 0,
27 totalUpload: 0
28 }
29 private statsHTTPBytes = {
30 pendingDownload: [] as number[],
31 pendingUpload: [] as number[],
32 totalDownload: 0,
33 totalUpload: 0
34 }
35
36 private networkInfoInterval: any
37
38 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) {
39 super(player, options)
40
41 this.options = options
42
43 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
44 this.hlsjs = hlsjs
45 })
46
47 initVideoJsContribHlsJsPlayer(player)
48
49 player.src({
50 type: options.type,
51 src: options.src
52 })
53
54 player.on('play', () => {
55 player.addClass('vjs-has-big-play-button-clicked')
56 })
57
58 player.ready(() => this.initialize())
59 }
60
61 dispose () {
62 if (this.hlsjs) this.hlsjs.destroy()
63 if (this.p2pEngine) this.p2pEngine.destroy()
64
65 clearInterval(this.networkInfoInterval)
66 }
67
68 private initialize () {
69 initHlsJsPlayer(this.hlsjs)
70
71 const tech = this.player.tech_
72 this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine()
73
74 // Avoid using constants to not import hls.hs
75 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37
76 this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => {
77 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
78 })
79
80 this.p2pEngine.on(Events.SegmentError, (segment, err) => {
81 console.error('Segment error.', segment, err)
82 })
83
84 this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length
85
86 this.runStats()
87 }
88
89 private runStats () {
90 this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => {
91 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
92
93 elem.pendingDownload.push(size)
94 elem.totalDownload += size
95 })
96
97 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => {
98 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
99
100 elem.pendingUpload.push(size)
101 elem.totalUpload += size
102 })
103
104 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
105 this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
106
107 this.networkInfoInterval = setInterval(() => {
108 const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
109 const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
110
111 const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
112 const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload)
113
114 this.statsP2PBytes.pendingDownload = []
115 this.statsP2PBytes.pendingUpload = []
116 this.statsHTTPBytes.pendingDownload = []
117 this.statsHTTPBytes.pendingUpload = []
118
119 return this.player.trigger('p2pInfo', {
120 http: {
121 downloadSpeed: httpDownloadSpeed,
122 uploadSpeed: httpUploadSpeed,
123 downloaded: this.statsHTTPBytes.totalDownload,
124 uploaded: this.statsHTTPBytes.totalUpload
125 },
126 p2p: {
127 downloadSpeed: p2pDownloadSpeed,
128 uploadSpeed: p2pUploadSpeed,
129 numPeers: this.statsP2PBytes.numPeers,
130 downloaded: this.statsP2PBytes.totalDownload,
131 uploaded: this.statsP2PBytes.totalUpload
132 }
133 } as PlayerNetworkInfo)
134 }, this.CONSTANTS.INFO_SCHEDULER)
135 }
136
137 private arraySum (data: number[]) {
138 return data.reduce((a: number, b: number) => a + b, 0)
139 }
140}
141
142videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
143export { 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..72c32f9e0
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts
@@ -0,0 +1,63 @@
1import { Segment } from 'p2p-media-loader-core'
2import { basename } from 'path'
3
4function segmentValidatorFactory (segmentsSha256Url: string) {
5 const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
6 const regex = /bytes=(\d+)-(\d+)/
7
8 return async function segmentValidator (segment: Segment) {
9 const filename = basename(segment.url)
10 const captured = regex.exec(segment.range)
11
12 const range = captured[1] + '-' + captured[2]
13
14 const hashShouldBe = (await segmentsJSON)[filename][range]
15 if (hashShouldBe === undefined) {
16 throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
17 }
18
19 const calculatedSha = bufferToEx(await sha256(segment.data))
20 if (calculatedSha !== hashShouldBe) {
21 throw new Error(
22 `Hashes does not correspond for segment ${filename}/${range}` +
23 `(expected: ${hashShouldBe} instead of ${calculatedSha})`
24 )
25 }
26 }
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 segmentValidatorFactory
33}
34
35// ---------------------------------------------------------------------------
36
37function fetchSha256Segments (url: string) {
38 return fetch(url)
39 .then(res => res.json())
40 .catch(err => {
41 console.error('Cannot get sha256 segments', err)
42 return {}
43 })
44}
45
46function sha256 (data?: ArrayBuffer) {
47 if (!data) return undefined
48
49 return window.crypto.subtle.digest('SHA-256', data)
50}
51
52// Thanks: https://stackoverflow.com/a/53307879
53function bufferToEx (buffer?: ArrayBuffer) {
54 if (!buffer) return ''
55
56 let s = ''
57 const h = '0123456789abcdef'
58 const o = new Uint8Array(buffer)
59
60 o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ])
61
62 return s
63}
diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts
index dac54c5a4..059fca308 100644
--- a/client/src/assets/player/peertube-player-local-storage.ts
+++ b/client/src/assets/player/peertube-player-local-storage.ts
@@ -10,6 +10,14 @@ function getStoredVolume () {
10 return undefined 10 return undefined
11} 11}
12 12
13function getStoredWebTorrentEnabled (): boolean {
14 const value = getLocalStorage('webtorrent_enabled')
15 if (value !== null && value !== undefined) return value === 'true'
16
17 // By default webtorrent is enabled
18 return true
19}
20
13function getStoredMute () { 21function getStoredMute () {
14 const value = getLocalStorage('mute') 22 const value = getLocalStorage('mute')
15 if (value !== null && value !== undefined) return value === 'true' 23 if (value !== null && value !== undefined) return value === 'true'
@@ -52,17 +60,28 @@ function getAverageBandwidthInStore () {
52 return undefined 60 return undefined
53} 61}
54 62
63function saveLastSubtitle (language: string) {
64 return setLocalStorage('last-subtitle', language)
65}
66
67function getStoredLastSubtitle () {
68 return getLocalStorage('last-subtitle')
69}
70
55// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
56 72
57export { 73export {
58 getStoredVolume, 74 getStoredVolume,
75 getStoredWebTorrentEnabled,
59 getStoredMute, 76 getStoredMute,
60 getStoredTheater, 77 getStoredTheater,
61 saveVolumeInStore, 78 saveVolumeInStore,
62 saveMuteInStore, 79 saveMuteInStore,
63 saveTheaterInStore, 80 saveTheaterInStore,
64 saveAverageBandwidth, 81 saveAverageBandwidth,
65 getAverageBandwidthInStore 82 getAverageBandwidthInStore,
83 saveLastSubtitle,
84 getStoredLastSubtitle
66} 85}
67 86
68// --------------------------------------------------------------------------- 87// ---------------------------------------------------------------------------
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
new file mode 100644
index 000000000..0ba9bcb11
--- /dev/null
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -0,0 +1,466 @@
1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore
3import * as videojs from 'video.js'
4import 'videojs-hotkeys'
5import 'videojs-dock'
6import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels'
8import './peertube-plugin'
9import './videojs-components/peertube-link-button'
10import './videojs-components/resolution-menu-button'
11import './videojs-components/settings-menu-button'
12import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-load-progress-bar'
14import './videojs-components/theater-button'
15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
16import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
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'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
25// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
29
30export type WebtorrentOptions = {
31 videoFiles: VideoFile[]
32}
33
34export type P2PMediaLoaderOptions = {
35 playlistUrl: string
36 segmentsSha256Url: string
37 trackerAnnounce: string[]
38 redundancyBaseUrls: string[]
39 videoFiles: VideoFile[]
40}
41
42export type CommonOptions = {
43 playerElement: HTMLVideoElement
44 onPlayerElementChange: (element: HTMLVideoElement) => void
45
46 autoplay: boolean
47 videoDuration: number
48 enableHotkeys: boolean
49 inactivityTimeout: number
50 poster: string
51 startTime: number | string
52
53 theaterMode: boolean
54 captions: boolean
55 peertubeLink: boolean
56
57 videoViewUrl: string
58 embedUrl: string
59
60 language?: string
61 controls?: boolean
62 muted?: boolean
63 loop?: boolean
64 subtitle?: string
65
66 videoCaptions: VideoJSCaption[]
67
68 userWatching?: UserWatching
69
70 serverUrl: string
71}
72
73export type PeertubePlayerManagerOptions = {
74 common: CommonOptions,
75 webtorrent: WebtorrentOptions,
76 p2pMediaLoader?: P2PMediaLoaderOptions
77}
78
79export class PeertubePlayerManager {
80
81 private static videojsLocaleCache: { [ path: string ]: any } = {}
82 private static playerElementClassName: string
83
84 static getServerTranslations (serverUrl: string, locale: string) {
85 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
86 // It is the default locale, nothing to translate
87 if (!path) return Promise.resolve(undefined)
88
89 return fetch(path + '/server.json')
90 .then(res => res.json())
91 .catch(err => {
92 console.error('Cannot get server translations', err)
93 return undefined
94 })
95 }
96
97 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
98 let p2pMediaLoader: any
99
100 this.playerElementClassName = options.common.playerElement.className
101
102 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
103 if (mode === 'p2p-media-loader') {
104 [ p2pMediaLoader ] = await Promise.all([
105 import('p2p-media-loader-hlsjs'),
106 import('./p2p-media-loader/p2p-media-loader-plugin')
107 ])
108 }
109
110 const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader)
111
112 await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language)
113
114 const self = this
115 return new Promise(res => {
116 videojs(options.common.playerElement, videojsOptions, function (this: any) {
117 const player = this
118
119 player.tech_.on('error', () => {
120 // Fallback to webtorrent?
121 if (mode === 'p2p-media-loader') {
122 self.fallbackToWebTorrent(player, options)
123 }
124 })
125
126 self.addContextMenu(mode, player, options.common.embedUrl)
127
128 return res(player)
129 })
130 })
131 }
132
133 private static async fallbackToWebTorrent (player: any, options: PeertubePlayerManagerOptions) {
134 const newVideoElement = document.createElement('video')
135 newVideoElement.className = this.playerElementClassName
136
137 // VideoJS wraps our video element inside a div
138 const currentParentPlayerElement = options.common.playerElement.parentNode
139 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
140
141 options.common.playerElement = newVideoElement
142 options.common.onPlayerElementChange(newVideoElement)
143
144 player.dispose()
145
146 await import('./webtorrent/webtorrent-plugin')
147
148 const mode = 'webtorrent'
149 const videojsOptions = this.getVideojsOptions(mode, options)
150
151 const self = this
152 videojs(newVideoElement, videojsOptions, function (this: any) {
153 const player = this
154
155 self.addContextMenu(mode, player, options.common.embedUrl)
156 })
157 }
158
159 private static loadLocaleInVideoJS (serverUrl: string, locale: string) {
160 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
161 // It is the default locale, nothing to translate
162 if (!path) return Promise.resolve(undefined)
163
164 let p: Promise<any>
165
166 if (PeertubePlayerManager.videojsLocaleCache[path]) {
167 p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path])
168 } else {
169 p = fetch(path + '/player.json')
170 .then(res => res.json())
171 .then(json => {
172 PeertubePlayerManager.videojsLocaleCache[path] = json
173 return json
174 })
175 .catch(err => {
176 console.error('Cannot get player translations', err)
177 return undefined
178 })
179 }
180
181 const completeLocale = getCompleteLocale(locale)
182 return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
183 }
184
185 private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) {
186 const commonOptions = options.common
187 const webtorrentOptions = options.webtorrent
188 const p2pMediaLoaderOptions = options.p2pMediaLoader
189
190 let autoplay = options.common.autoplay
191 let html5 = {}
192
193 const plugins: VideoJSPluginOptions = {
194 peertube: {
195 mode,
196 autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
197 videoViewUrl: commonOptions.videoViewUrl,
198 videoDuration: commonOptions.videoDuration,
199 startTime: commonOptions.startTime,
200 userWatching: commonOptions.userWatching,
201 subtitle: commonOptions.subtitle,
202 videoCaptions: commonOptions.videoCaptions
203 }
204 }
205
206 if (mode === 'p2p-media-loader') {
207 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
208 redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls,
209 type: 'application/x-mpegURL',
210 src: p2pMediaLoaderOptions.playlistUrl
211 }
212
213 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
214 .filter(t => t.startsWith('ws'))
215
216 const p2pMediaLoaderConfig = {
217 loader: {
218 trackerAnnounce,
219 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url),
220 rtcConfig: getRtcConfig(),
221 requiredSegmentsPriority: 5,
222 segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls)
223 },
224 segments: {
225 swarmId: p2pMediaLoaderOptions.playlistUrl
226 }
227 }
228 const streamrootHls = {
229 levelLabelHandler: (level: { height: number, width: number }) => {
230 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height)
231
232 let label = file.resolution.label
233 if (file.fps >= 50) label += file.fps
234
235 return label
236 },
237 html5: {
238 hlsjsConfig: {
239 liveSyncDurationCount: 7,
240 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
241 }
242 }
243 }
244
245 Object.assign(plugins, { p2pMediaLoader, streamrootHls })
246 html5 = streamrootHls.html5
247 }
248
249 if (mode === 'webtorrent') {
250 const webtorrent = {
251 autoplay,
252 videoDuration: commonOptions.videoDuration,
253 playerElement: commonOptions.playerElement,
254 videoFiles: webtorrentOptions.videoFiles
255 }
256 Object.assign(plugins, { webtorrent })
257
258 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
259 autoplay = false
260 }
261
262 const videojsOptions = {
263 html5,
264
265 // We don't use text track settings for now
266 textTrackSettings: false,
267 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
268 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
269
270 muted: commonOptions.muted !== undefined
271 ? commonOptions.muted
272 : undefined, // Undefined so the player knows it has to check the local storage
273
274 poster: commonOptions.poster,
275 autoplay: autoplay === true ? 'any' : autoplay, // Use 'any' instead of true to get notifier by videojs if autoplay fails
276 inactivityTimeout: commonOptions.inactivityTimeout,
277 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
278 plugins,
279 controlBar: {
280 children: this.getControlBarChildren(mode, {
281 captions: commonOptions.captions,
282 peertubeLink: commonOptions.peertubeLink,
283 theaterMode: commonOptions.theaterMode
284 })
285 }
286 }
287
288 if (commonOptions.enableHotkeys === true) {
289 Object.assign(videojsOptions.plugins, {
290 hotkeys: {
291 enableVolumeScroll: false,
292 enableModifiersForNumbers: false,
293
294 fullscreenKey: function (event: KeyboardEvent) {
295 // fullscreen with the f key or Ctrl+Enter
296 return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
297 },
298
299 seekStep: function (event: KeyboardEvent) {
300 // mimic VLC seek behavior, and default to 5 (original value is 5).
301 if (event.ctrlKey && event.altKey) {
302 return 5 * 60
303 } else if (event.ctrlKey) {
304 return 60
305 } else if (event.altKey) {
306 return 10
307 } else {
308 return 5
309 }
310 },
311
312 customKeys: {
313 increasePlaybackRateKey: {
314 key: function (event: KeyboardEvent) {
315 return event.key === '>'
316 },
317 handler: function (player: videojs.Player) {
318 player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
319 }
320 },
321 decreasePlaybackRateKey: {
322 key: function (event: KeyboardEvent) {
323 return event.key === '<'
324 },
325 handler: function (player: videojs.Player) {
326 player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
327 }
328 },
329 frameByFrame: {
330 key: function (event: KeyboardEvent) {
331 return event.key === '.'
332 },
333 handler: function (player: videojs.Player) {
334 player.pause()
335 // Calculate movement distance (assuming 30 fps)
336 const dist = 1 / 30
337 player.currentTime(player.currentTime() + dist)
338 }
339 }
340 }
341 }
342 })
343 }
344
345 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
346 Object.assign(videojsOptions, { language: commonOptions.language })
347 }
348
349 return videojsOptions
350 }
351
352 private static getControlBarChildren (mode: PlayerMode, options: {
353 peertubeLink: boolean
354 theaterMode: boolean,
355 captions: boolean
356 }) {
357 const settingEntries = []
358 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
359
360 // Keep an order
361 settingEntries.push('playbackRateMenuButton')
362 if (options.captions === true) settingEntries.push('captionsButton')
363 settingEntries.push('resolutionMenuButton')
364
365 const children = {
366 'playToggle': {},
367 'currentTimeDisplay': {},
368 'timeDivider': {},
369 'durationDisplay': {},
370 'liveDisplay': {},
371
372 'flexibleWidthSpacer': {},
373 'progressControl': {
374 children: {
375 'seekBar': {
376 children: {
377 [loadProgressBar]: {},
378 'mouseTimeDisplay': {},
379 'playProgressBar': {}
380 }
381 }
382 }
383 },
384
385 'p2PInfoButton': {},
386
387 'muteToggle': {},
388 'volumeControl': {},
389
390 'settingsButton': {
391 setup: {
392 maxHeightOffset: 40
393 },
394 entries: settingEntries
395 }
396 }
397
398 if (options.peertubeLink === true) {
399 Object.assign(children, {
400 'peerTubeLinkButton': {}
401 })
402 }
403
404 if (options.theaterMode === true) {
405 Object.assign(children, {
406 'theaterButton': {}
407 })
408 }
409
410 Object.assign(children, {
411 'fullscreenToggle': {}
412 })
413
414 return children
415 }
416
417 private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) {
418 const content = [
419 {
420 label: player.localize('Copy the video URL'),
421 listener: function () {
422 copyToClipboard(buildVideoLink())
423 }
424 },
425 {
426 label: player.localize('Copy the video URL at the current time'),
427 listener: function () {
428 const player = this as videojs.Player
429 copyToClipboard(buildVideoLink(player.currentTime()))
430 }
431 },
432 {
433 label: player.localize('Copy embed code'),
434 listener: () => {
435 copyToClipboard(buildVideoEmbed(videoEmbedUrl))
436 }
437 }
438 ]
439
440 if (mode === 'webtorrent') {
441 content.push({
442 label: player.localize('Copy magnet URI'),
443 listener: function () {
444 const player = this as videojs.Player
445 copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri)
446 }
447 })
448 }
449
450 player.contextmenuUI({ content })
451 }
452
453 private static getLocalePath (serverUrl: string, locale: string) {
454 const completeLocale = getCompleteLocale(locale)
455
456 if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
457
458 return serverUrl + '/client/locales/' + completeLocale
459 }
460}
461
462// ############################################################################
463
464export {
465 videojs
466}
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts
deleted file mode 100644
index 792662b6c..000000000
--- a/client/src/assets/player/peertube-player.ts
+++ /dev/null
@@ -1,284 +0,0 @@
1import { VideoFile } from '../../../../shared/models/videos'
2
3import 'videojs-hotkeys'
4import 'videojs-dock'
5import 'videojs-contextmenu-ui'
6import './peertube-link-button'
7import './resolution-menu-button'
8import './settings-menu-button'
9import './webtorrent-info-button'
10import './peertube-videojs-plugin'
11import './peertube-load-progress-bar'
12import './theater-button'
13import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
14import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
15import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
16
17// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
18videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
19// Change Captions to Subtitles/CC
20videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
21// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
22videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
23
24function getVideojsOptions (options: {
25 autoplay: boolean,
26 playerElement: HTMLVideoElement,
27 videoViewUrl: string,
28 videoDuration: number,
29 videoFiles: VideoFile[],
30 enableHotkeys: boolean,
31 inactivityTimeout: number,
32 peertubeLink: boolean,
33 poster: string,
34 startTime: number | string
35 theaterMode: boolean,
36 videoCaptions: VideoJSCaption[],
37
38 language?: string,
39 controls?: boolean,
40 muted?: boolean,
41 loop?: boolean
42
43 userWatching?: UserWatching
44}) {
45 const videojsOptions = {
46 // We don't use text track settings for now
47 textTrackSettings: false,
48 controls: options.controls !== undefined ? options.controls : true,
49 muted: options.controls !== undefined ? options.muted : false,
50 loop: options.loop !== undefined ? options.loop : false,
51 poster: options.poster,
52 autoplay: false,
53 inactivityTimeout: options.inactivityTimeout,
54 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
55 plugins: {
56 peertube: {
57 autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
58 videoCaptions: options.videoCaptions,
59 videoFiles: options.videoFiles,
60 playerElement: options.playerElement,
61 videoViewUrl: options.videoViewUrl,
62 videoDuration: options.videoDuration,
63 startTime: options.startTime,
64 userWatching: options.userWatching
65 }
66 },
67 controlBar: {
68 children: getControlBarChildren(options)
69 }
70 }
71
72 if (options.enableHotkeys === true) {
73 Object.assign(videojsOptions.plugins, {
74 hotkeys: {
75 enableVolumeScroll: false,
76 enableModifiersForNumbers: false,
77
78 fullscreenKey: function (event) {
79 // fullscreen with the f key or Ctrl+Enter
80 return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
81 },
82
83 seekStep: function (event) {
84 // mimic VLC seek behavior, and default to 5 (original value is 5).
85 if (event.ctrlKey && event.altKey) {
86 return 5 * 60
87 } else if (event.ctrlKey) {
88 return 60
89 } else if (event.altKey) {
90 return 10
91 } else {
92 return 5
93 }
94 },
95
96 customKeys: {
97 increasePlaybackRateKey: {
98 key: function (event) {
99 return event.key === '>'
100 },
101 handler: function (player) {
102 player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
103 }
104 },
105 decreasePlaybackRateKey: {
106 key: function (event) {
107 return event.key === '<'
108 },
109 handler: function (player) {
110 player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
111 }
112 },
113 frameByFrame: {
114 key: function (event) {
115 return event.key === '.'
116 },
117 handler: function (player, options, event) {
118 player.pause()
119 // Calculate movement distance (assuming 30 fps)
120 const dist = 1 / 30
121 player.currentTime(player.currentTime() + dist)
122 }
123 }
124 }
125 }
126 })
127 }
128
129 if (options.language && !isDefaultLocale(options.language)) {
130 Object.assign(videojsOptions, { language: options.language })
131 }
132
133 return videojsOptions
134}
135
136function getControlBarChildren (options: {
137 peertubeLink: boolean
138 theaterMode: boolean,
139 videoCaptions: VideoJSCaption[]
140}) {
141 const settingEntries = []
142
143 // Keep an order
144 settingEntries.push('playbackRateMenuButton')
145 if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton')
146 settingEntries.push('resolutionMenuButton')
147
148 const children = {
149 'playToggle': {},
150 'currentTimeDisplay': {},
151 'timeDivider': {},
152 'durationDisplay': {},
153 'liveDisplay': {},
154
155 'flexibleWidthSpacer': {},
156 'progressControl': {
157 children: {
158 'seekBar': {
159 children: {
160 'peerTubeLoadProgressBar': {},
161 'mouseTimeDisplay': {},
162 'playProgressBar': {}
163 }
164 }
165 }
166 },
167
168 'webTorrentButton': {},
169
170 'muteToggle': {},
171 'volumeControl': {},
172
173 'settingsButton': {
174 setup: {
175 maxHeightOffset: 40
176 },
177 entries: settingEntries
178 }
179 }
180
181 if (options.peertubeLink === true) {
182 Object.assign(children, {
183 'peerTubeLinkButton': {}
184 })
185 }
186
187 if (options.theaterMode === true) {
188 Object.assign(children, {
189 'theaterButton': {}
190 })
191 }
192
193 Object.assign(children, {
194 'fullscreenToggle': {}
195 })
196
197 return children
198}
199
200function addContextMenu (player: any, videoEmbedUrl: string) {
201 player.contextmenuUI({
202 content: [
203 {
204 label: player.localize('Copy the video URL'),
205 listener: function () {
206 copyToClipboard(buildVideoLink())
207 }
208 },
209 {
210 label: player.localize('Copy the video URL at the current time'),
211 listener: function () {
212 const player = this
213 copyToClipboard(buildVideoLink(player.currentTime()))
214 }
215 },
216 {
217 label: player.localize('Copy embed code'),
218 listener: () => {
219 copyToClipboard(buildVideoEmbed(videoEmbedUrl))
220 }
221 },
222 {
223 label: player.localize('Copy magnet URI'),
224 listener: function () {
225 const player = this
226 copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri)
227 }
228 }
229 ]
230 })
231}
232
233function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) {
234 const path = getLocalePath(serverUrl, locale)
235 // It is the default locale, nothing to translate
236 if (!path) return Promise.resolve(undefined)
237
238 let p: Promise<any>
239
240 if (loadLocaleInVideoJS.cache[path]) {
241 p = Promise.resolve(loadLocaleInVideoJS.cache[path])
242 } else {
243 p = fetch(path + '/player.json')
244 .then(res => res.json())
245 .then(json => {
246 loadLocaleInVideoJS.cache[path] = json
247 return json
248 })
249 }
250
251 const completeLocale = getCompleteLocale(locale)
252 return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
253}
254namespace loadLocaleInVideoJS {
255 export const cache: { [ path: string ]: any } = {}
256}
257
258function getServerTranslations (serverUrl: string, locale: string) {
259 const path = getLocalePath(serverUrl, locale)
260 // It is the default locale, nothing to translate
261 if (!path) return Promise.resolve(undefined)
262
263 return fetch(path + '/server.json')
264 .then(res => res.json())
265}
266
267// ############################################################################
268
269export {
270 getServerTranslations,
271 loadLocaleInVideoJS,
272 getVideojsOptions,
273 addContextMenu
274}
275
276// ############################################################################
277
278function getLocalePath (serverUrl: string, locale: string) {
279 const completeLocale = getCompleteLocale(locale)
280
281 if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
282
283 return serverUrl + '/client/locales/' + completeLocale
284}
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
new file mode 100644
index 000000000..7ea4a06d4
--- /dev/null
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -0,0 +1,262 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button'
5import {
6 PeerTubePluginOptions,
7 ResolutionUpdateData,
8 UserWatching,
9 VideoJSCaption,
10 VideoJSComponentInterface,
11 videojsUntyped
12} from './peertube-videojs-typings'
13import { isMobile, timeToInt } from './utils'
14import {
15 getStoredLastSubtitle,
16 getStoredMute,
17 getStoredVolume,
18 saveLastSubtitle,
19 saveMuteInStore,
20 saveVolumeInStore
21} from './peertube-player-local-storage'
22
23const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
24class PeerTubePlugin extends Plugin {
25 private readonly autoplay: boolean = false
26 private readonly startTime: number = 0
27 private readonly videoViewUrl: string
28 private readonly videoDuration: number
29 private readonly CONSTANTS = {
30 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
31 }
32
33 private player: any
34 private videoCaptions: VideoJSCaption[]
35 private defaultSubtitle: string
36
37 private videoViewInterval: any
38 private userWatchingVideoInterval: any
39 private qualityObservationTimer: any
40 private lastResolutionChange: ResolutionUpdateData
41
42 constructor (player: videojs.Player, options: PeerTubePluginOptions) {
43 super(player, options)
44
45 this.startTime = timeToInt(options.startTime)
46 this.videoViewUrl = options.videoViewUrl
47 this.videoDuration = options.videoDuration
48 this.videoCaptions = options.videoCaptions
49
50 if (options.autoplay === true) this.player.addClass('vjs-has-autoplay')
51
52 this.player.on('autoplay-failure', () => {
53 this.player.removeClass('vjs-has-autoplay')
54 })
55
56 this.player.ready(() => {
57 const playerOptions = this.player.options_
58
59 if (options.mode === 'webtorrent') {
60 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
61 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
62 }
63
64 if (options.mode === 'p2p-media-loader') {
65 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
66 }
67
68 this.player.tech_.on('loadedqualitydata', () => {
69 setTimeout(() => {
70 // Replay a resolution change, now we loaded all quality data
71 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
72 }, 0)
73 })
74
75 const volume = getStoredVolume()
76 if (volume !== undefined) this.player.volume(volume)
77
78 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
79 if (muted !== undefined) this.player.muted(muted)
80
81 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
82
83 this.player.on('volumechange', () => {
84 saveVolumeInStore(this.player.volume())
85 saveMuteInStore(this.player.muted())
86 })
87
88 this.player.textTracks().on('change', () => {
89 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
90 return t.kind === 'captions' && t.mode === 'showing'
91 })
92
93 if (!showing) {
94 saveLastSubtitle('off')
95 return
96 }
97
98 saveLastSubtitle(showing.language)
99 })
100
101 this.player.on('sourcechange', () => this.initCaptions())
102
103 this.player.duration(options.videoDuration)
104
105 this.initializePlayer()
106 this.runViewAdd()
107
108 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
109 })
110 }
111
112 dispose () {
113 clearTimeout(this.qualityObservationTimer)
114
115 clearInterval(this.videoViewInterval)
116
117 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
118 }
119
120 private initializePlayer () {
121 if (isMobile()) this.player.addClass('vjs-is-mobile')
122
123 this.initSmoothProgressBar()
124
125 this.initCaptions()
126
127 this.alterInactivity()
128 }
129
130 private runViewAdd () {
131 this.clearVideoViewInterval()
132
133 // After 30 seconds (or 3/4 of the video), add a view to the video
134 let minSecondsToView = 30
135
136 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
137
138 let secondsViewed = 0
139 this.videoViewInterval = setInterval(() => {
140 if (this.player && !this.player.paused()) {
141 secondsViewed += 1
142
143 if (secondsViewed > minSecondsToView) {
144 this.clearVideoViewInterval()
145
146 this.addViewToVideo().catch(err => console.error(err))
147 }
148 }
149 }, 1000)
150 }
151
152 private runUserWatchVideo (options: UserWatching) {
153 let lastCurrentTime = 0
154
155 this.userWatchingVideoInterval = setInterval(() => {
156 const currentTime = Math.floor(this.player.currentTime())
157
158 if (currentTime - lastCurrentTime >= 1) {
159 lastCurrentTime = currentTime
160
161 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
162 .catch(err => console.error('Cannot notify user is watching.', err))
163 }
164 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
165 }
166
167 private clearVideoViewInterval () {
168 if (this.videoViewInterval !== undefined) {
169 clearInterval(this.videoViewInterval)
170 this.videoViewInterval = undefined
171 }
172 }
173
174 private addViewToVideo () {
175 if (!this.videoViewUrl) return Promise.resolve(undefined)
176
177 return fetch(this.videoViewUrl, { method: 'POST' })
178 }
179
180 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
181 const body = new URLSearchParams()
182 body.append('currentTime', currentTime.toString())
183
184 const headers = new Headers({ 'Authorization': authorizationHeader })
185
186 return fetch(url, { method: 'PUT', body, headers })
187 }
188
189 private handleResolutionChange (data: ResolutionUpdateData) {
190 this.lastResolutionChange = data
191
192 const qualityLevels = this.player.qualityLevels()
193
194 for (let i = 0; i < qualityLevels.length; i++) {
195 if (qualityLevels[i].height === data.resolutionId) {
196 data.id = qualityLevels[i].id
197 break
198 }
199 }
200
201 this.trigger('resolutionChange', data)
202 }
203
204 private alterInactivity () {
205 let saveInactivityTimeout: number
206
207 const disableInactivity = () => {
208 saveInactivityTimeout = this.player.options_.inactivityTimeout
209 this.player.options_.inactivityTimeout = 0
210 }
211 const enableInactivity = () => {
212 this.player.options_.inactivityTimeout = saveInactivityTimeout
213 }
214
215 const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
216
217 this.player.controlBar.on('mouseenter', () => disableInactivity())
218 settingsDialog.on('mouseenter', () => disableInactivity())
219 this.player.controlBar.on('mouseleave', () => enableInactivity())
220 settingsDialog.on('mouseleave', () => enableInactivity())
221 }
222
223 private initCaptions () {
224 for (const caption of this.videoCaptions) {
225 this.player.addRemoteTextTrack({
226 kind: 'captions',
227 label: caption.label,
228 language: caption.language,
229 id: caption.language,
230 src: caption.src,
231 default: this.defaultSubtitle === caption.language
232 }, false)
233 }
234
235 this.player.trigger('captionsChanged')
236 }
237
238 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
239 private initSmoothProgressBar () {
240 const SeekBar = videojsUntyped.getComponent('SeekBar')
241 SeekBar.prototype.getPercent = function getPercent () {
242 // Allows for smooth scrubbing, when player can't keep up.
243 // const time = (this.player_.scrubbing()) ?
244 // this.player_.getCache().currentTime :
245 // this.player_.currentTime()
246 const time = this.player_.currentTime()
247 const percent = time / this.player_.duration()
248 return percent >= 1 ? 1 : percent
249 }
250 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
251 let newTime = this.calculateDistance(event) * this.player_.duration()
252 if (newTime === this.player_.duration()) {
253 newTime = newTime - 0.1
254 }
255 this.player_.currentTime(newTime)
256 this.update()
257 }
258 }
259}
260
261videojs.registerPlugin('peertube', PeerTubePlugin)
262export { PeerTubePlugin }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index b117007af..79a5a6c4d 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,19 +1,27 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
1import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4
2import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
3import { PeerTubePlugin } from './peertube-videojs-plugin' 6import { PeerTubePlugin } from './peertube-plugin'
7import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
8import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
9import { PlayerMode } from './peertube-player-manager'
4 10
5declare namespace videojs { 11declare namespace videojs {
6 interface Player { 12 interface Player {
7 peertube (): PeerTubePlugin 13 peertube (): PeerTubePlugin
14 webtorrent (): WebTorrentPlugin
15 p2pMediaLoader (): P2pMediaLoaderPlugin
8 } 16 }
9} 17}
10 18
11interface VideoJSComponentInterface { 19interface VideoJSComponentInterface {
12 _player: videojs.Player 20 _player: videojs.Player
13 21
14 new (player: videojs.Player, options?: any) 22 new (player: videojs.Player, options?: any): any
15 23
16 registerComponent (name: string, obj: any) 24 registerComponent (name: string, obj: any): any
17} 25}
18 26
19type VideoJSCaption = { 27type VideoJSCaption = {
@@ -27,25 +35,95 @@ type UserWatching = {
27 authorizationHeader: string 35 authorizationHeader: string
28} 36}
29 37
30type PeertubePluginOptions = { 38type PeerTubePluginOptions = {
31 videoFiles: VideoFile[] 39 mode: PlayerMode
32 playerElement: HTMLVideoElement 40
41 autoplay: boolean
33 videoViewUrl: string 42 videoViewUrl: string
34 videoDuration: number 43 videoDuration: number
35 startTime: number | string 44 startTime: number | string
36 autoplay: boolean,
37 videoCaptions: VideoJSCaption[]
38 45
39 userWatching?: UserWatching 46 userWatching?: UserWatching
47 subtitle?: string
48
49 videoCaptions: VideoJSCaption[]
50}
51
52type WebtorrentPluginOptions = {
53 playerElement: HTMLVideoElement
54
55 autoplay: boolean
56 videoDuration: number
57
58 videoFiles: VideoFile[]
59}
60
61type P2PMediaLoaderPluginOptions = {
62 redundancyBaseUrls: string[]
63 type: string
64 src: string
65}
66
67type VideoJSPluginOptions = {
68 peertube: PeerTubePluginOptions
69
70 webtorrent?: WebtorrentPluginOptions
71
72 p2pMediaLoader?: P2PMediaLoaderPluginOptions
40} 73}
41 74
42// videojs typings don't have some method we need 75// videojs typings don't have some method we need
43const videojsUntyped = videojs as any 76const videojsUntyped = videojs as any
44 77
78type LoadedQualityData = {
79 qualitySwitchCallback: Function,
80 qualityData: {
81 video: {
82 id: number
83 label: string
84 selected: boolean
85 }[]
86 }
87}
88
89type ResolutionUpdateData = {
90 auto: boolean,
91 resolutionId: number
92 id?: number
93}
94
95type AutoResolutionUpdateData = {
96 possible: boolean
97}
98
99type PlayerNetworkInfo = {
100 http: {
101 downloadSpeed: number
102 uploadSpeed: number
103 downloaded: number
104 uploaded: number
105 }
106
107 p2p: {
108 downloadSpeed: number
109 uploadSpeed: number
110 downloaded: number
111 uploaded: number
112 numPeers: number
113 }
114}
115
45export { 116export {
117 PlayerNetworkInfo,
118 ResolutionUpdateData,
119 AutoResolutionUpdateData,
46 VideoJSComponentInterface, 120 VideoJSComponentInterface,
47 PeertubePluginOptions,
48 videojsUntyped, 121 videojsUntyped,
49 VideoJSCaption, 122 VideoJSCaption,
50 UserWatching 123 UserWatching,
124 PeerTubePluginOptions,
125 WebtorrentPluginOptions,
126 P2PMediaLoaderPluginOptions,
127 VideoJSPluginOptions,
128 LoadedQualityData
51} 129}
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts
deleted file mode 100644
index d53a24151..000000000
--- a/client/src/assets/player/resolution-menu-button.ts
+++ /dev/null
@@ -1,85 +0,0 @@
1import * as videojs from 'video.js'
2import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
3import { ResolutionMenuItem } from './resolution-menu-item'
4
5const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
6const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
7class ResolutionMenuButton extends MenuButton {
8 label: HTMLElement
9
10 constructor (player: videojs.Player, options) {
11 super(player, options)
12 this.player = player
13
14 player.peertube().on('videoFileUpdate', () => this.updateLabel())
15 player.peertube().on('autoResolutionUpdate', () => this.updateLabel())
16 }
17
18 createEl () {
19 const el = super.createEl()
20
21 this.labelEl_ = videojsUntyped.dom.createEl('div', {
22 className: 'vjs-resolution-value',
23 innerHTML: this.buildLabelHTML()
24 })
25
26 el.appendChild(this.labelEl_)
27
28 return el
29 }
30
31 updateARIAAttributes () {
32 this.el().setAttribute('aria-label', 'Quality')
33 }
34
35 createMenu () {
36 const menu = new Menu(this.player_)
37 for (const videoFile of this.player_.peertube().videoFiles) {
38 let label = videoFile.resolution.label
39 if (videoFile.fps && videoFile.fps >= 50) {
40 label += videoFile.fps
41 }
42
43 menu.addChild(new ResolutionMenuItem(
44 this.player_,
45 {
46 id: videoFile.resolution.id,
47 label,
48 src: videoFile.magnetUri
49 })
50 )
51 }
52
53 menu.addChild(new ResolutionMenuItem(
54 this.player_,
55 {
56 id: -1,
57 label: this.player_.localize('Auto'),
58 src: null
59 }
60 ))
61
62 return menu
63 }
64
65 updateLabel () {
66 if (!this.labelEl_) return
67
68 this.labelEl_.innerHTML = this.buildLabelHTML()
69 }
70
71 buildCSSClass () {
72 return super.buildCSSClass() + ' vjs-resolution-button'
73 }
74
75 buildWrapperCSSClass () {
76 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
77 }
78
79 private buildLabelHTML () {
80 return this.player_.peertube().getCurrentResolutionLabel()
81 }
82}
83ResolutionMenuButton.prototype.controlText_ = 'Quality'
84
85MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts
deleted file mode 100644
index 0ab0f53b5..000000000
--- a/client/src/assets/player/resolution-menu-item.ts
+++ /dev/null
@@ -1,64 +0,0 @@
1import * as videojs from 'video.js'
2import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
3
4const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
5class ResolutionMenuItem extends MenuItem {
6
7 constructor (player: videojs.Player, options) {
8 const currentResolutionId = player.peertube().getCurrentResolutionId()
9 options.selectable = true
10 options.selected = options.id === currentResolutionId
11
12 super(player, options)
13
14 this.label = options.label
15 this.id = options.id
16
17 player.peertube().on('videoFileUpdate', () => this.updateSelection())
18 player.peertube().on('autoResolutionUpdate', () => this.updateSelection())
19 }
20
21 handleClick (event) {
22 if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return
23
24 super.handleClick(event)
25
26 // Auto resolution
27 if (this.id === -1) {
28 this.player_.peertube().enableAutoResolution()
29 return
30 }
31
32 this.player_.peertube().disableAutoResolution()
33 this.player_.peertube().updateResolution(this.id)
34 }
35
36 updateSelection () {
37 // Check if auto resolution is forbidden or not
38 if (this.id === -1) {
39 if (this.player_.peertube().isAutoResolutionForbidden()) {
40 this.addClass('disabled')
41 } else {
42 this.removeClass('disabled')
43 }
44 }
45
46 if (this.player_.peertube().isAutoResolutionOn()) {
47 this.selected(this.id === -1)
48 return
49 }
50
51 this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
52 }
53
54 getLabel () {
55 if (this.id === -1) {
56 return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
57 }
58
59 return this.label
60 }
61}
62MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
63
64export { ResolutionMenuItem }
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index cf4f60f55..8d87567c2 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -12,7 +12,7 @@ const dictionaryBytes: Array<{max: number, type: string}> = [
12 { max: 1073741824, type: 'MB' }, 12 { max: 1073741824, type: 'MB' },
13 { max: 1.0995116e12, type: 'GB' } 13 { max: 1.0995116e12, type: 'GB' }
14] 14]
15function bytes (value) { 15function bytes (value: number) {
16 const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1] 16 const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
17 const calc = Math.floor(value / (format.max / 1024)).toString() 17 const calc = Math.floor(value / (format.max / 1024)).toString()
18 18
@@ -39,6 +39,7 @@ function buildVideoLink (time?: number, url?: string) {
39} 39}
40 40
41function timeToInt (time: number | string) { 41function timeToInt (time: number | string) {
42 if (!time) return 0
42 if (typeof time === 'number') return time 43 if (typeof time === 'number') return time
43 44
44 const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/ 45 const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/
@@ -111,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) {
111 return min 112 return min
112} 113}
113 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
114// --------------------------------------------------------------------------- 128// ---------------------------------------------------------------------------
115 129
116export { 130export {
131 getRtcConfig,
117 toTitleCase, 132 toTitleCase,
118 timeToInt, 133 timeToInt,
119 buildVideoLink, 134 buildVideoLink,
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
index deef253ce..6424787b2 100644
--- a/client/src/assets/player/webtorrent-info-button.ts
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -1,8 +1,8 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { bytes } from './utils' 2import { bytes } from '../utils'
3 3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class WebtorrentInfoButton extends Button { 5class P2pInfoButton extends Button {
6 6
7 createEl () { 7 createEl () {
8 const div = videojsUntyped.dom.createEl('div', { 8 const div = videojsUntyped.dom.createEl('div', {
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button {
65 subDivHttp.appendChild(subDivHttpText) 65 subDivHttp.appendChild(subDivHttpText)
66 div.appendChild(subDivHttp) 66 div.appendChild(subDivHttp)
67 67
68 this.player_.peertube().on('torrentInfo', (event, data) => { 68 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
69 // We are in HTTP fallback 69 // We are in HTTP fallback
70 if (!data) { 70 if (!data) {
71 subDivHttp.className = 'vjs-peertube-displayed' 71 subDivHttp.className = 'vjs-peertube-displayed'
@@ -74,11 +74,14 @@ class WebtorrentInfoButton extends Button {
74 return 74 return
75 } 75 }
76 76
77 const downloadSpeed = bytes(data.downloadSpeed) 77 const p2pStats = data.p2p
78 const uploadSpeed = bytes(data.uploadSpeed) 78 const httpStats = data.http
79 const totalDownloaded = bytes(data.downloaded) 79
80 const totalUploaded = bytes(data.uploaded) 80 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
81 const numPeers = data.numPeers 81 const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed)
82 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
84 const numPeers = p2pStats.numPeers
82 85
83 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + 86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
84 this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) 87 this.player_.localize('Total uploaded: ' + totalUploaded.join(' '))
@@ -90,7 +93,7 @@ class WebtorrentInfoButton extends Button {
90 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] 93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
91 94
92 peersNumber.textContent = numPeers 95 peersNumber.textContent = numPeers
93 peersText.textContent = ' ' + this.player_.localize('peers') 96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer'))
94 97
95 subDivHttp.className = 'vjs-peertube-hidden' 98 subDivHttp.className = 'vjs-peertube-hidden'
96 subDivWebtorrent.className = 'vjs-peertube-displayed' 99 subDivWebtorrent.className = 'vjs-peertube-displayed'
@@ -99,4 +102,4 @@ class WebtorrentInfoButton extends Button {
99 return div 102 return div
100 } 103 }
101} 104}
102Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) 105Button.registerComponent('P2PInfoButton', P2pInfoButton)
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts
index 715207bc0..fed8ea33e 100644
--- a/client/src/assets/player/peertube-link-button.ts
+++ b/client/src/assets/player/videojs-components/peertube-link-button.ts
@@ -1,11 +1,13 @@
1import * as videojs from 'video.js' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 2import { buildVideoLink } from '../utils'
3import { buildVideoLink } from './utils' 3// FIXME: something weird with our path definition in tsconfig and typings
4// @ts-ignore
5import { Player } from 'video.js'
4 6
5const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 7const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
6class PeerTubeLinkButton extends Button { 8class PeerTubeLinkButton extends Button {
7 9
8 constructor (player: videojs.Player, options) { 10 constructor (player: Player, options: any) {
9 super(player, options) 11 super(player, options)
10 } 12 }
11 13
diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
index aedc641e4..9a0e3b550 100644
--- a/client/src/assets/player/peertube-load-progress-bar.ts
+++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
@@ -1,10 +1,13 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
2 5
3const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 6const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
4 7
5class PeerTubeLoadProgressBar extends Component { 8class PeerTubeLoadProgressBar extends Component {
6 9
7 constructor (player, options) { 10 constructor (player: Player, options: any) {
8 super(player, options) 11 super(player, options)
9 this.partEls_ = [] 12 this.partEls_ = []
10 this.on(player, 'progress', this.update) 13 this.on(player, 'progress', this.update)
@@ -24,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component {
24 } 27 }
25 28
26 update () { 29 update () {
27 const torrent = this.player().peertube().getTorrent() 30 const torrent = this.player().webtorrent().getTorrent()
28 if (!torrent) return 31 if (!torrent) return
29 32
30 this.el_.style.width = (torrent.progress * 100) + '%' 33 this.el_.style.width = (torrent.progress * 100) + '%'
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
new file mode 100644
index 000000000..abcc16411
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -0,0 +1,109 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement
12
13 constructor (player: Player, options: any) {
14 super(player, options)
15 this.player = player
16
17 player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
18
19 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
20 }
21
22 createEl () {
23 const el = super.createEl()
24
25 this.labelEl_ = videojsUntyped.dom.createEl('div', {
26 className: 'vjs-resolution-value'
27 })
28
29 el.appendChild(this.labelEl_)
30
31 return el
32 }
33
34 updateARIAAttributes () {
35 this.el().setAttribute('aria-label', 'Quality')
36 }
37
38 createMenu () {
39 return new Menu(this.player_)
40 }
41
42 buildCSSClass () {
43 return super.buildCSSClass() + ' vjs-resolution-button'
44 }
45
46 buildWrapperCSSClass () {
47 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
48 }
49
50 private addClickListener (component: any) {
51 component.on('click', () => {
52 let children = this.menu.children()
53
54 for (const child of children) {
55 if (component !== child) {
56 child.selected(false)
57 }
58 }
59 })
60 }
61
62 private buildQualities (data: LoadedQualityData) {
63 // The automatic resolution item will need other labels
64 const labels: { [ id: number ]: string } = {}
65
66 data.qualityData.video.sort((a, b) => {
67 if (a.id > b.id) return -1
68 if (a.id === b.id) return 0
69 return 1
70 })
71
72 for (const d of data.qualityData.video) {
73 // Skip auto resolution, we'll add it ourselves
74 if (d.id === -1) continue
75
76 this.menu.addChild(new ResolutionMenuItem(
77 this.player_,
78 {
79 id: d.id,
80 label: d.label,
81 selected: d.selected,
82 callback: data.qualitySwitchCallback
83 })
84 )
85
86 labels[d.id] = d.label
87 }
88
89 this.menu.addChild(new ResolutionMenuItem(
90 this.player_,
91 {
92 id: -1,
93 label: this.player_.localize('Auto'),
94 labels,
95 callback: data.qualitySwitchCallback,
96 selected: true // By default, in auto mode
97 }
98 ))
99
100 for (const m of this.menu.children()) {
101 this.addClickListener(m)
102 }
103
104 this.trigger('menuChanged')
105 }
106}
107ResolutionMenuButton.prototype.controlText_ = 'Quality'
108
109MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
new file mode 100644
index 000000000..6c42fefd2
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -0,0 +1,83 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem {
9 private readonly id: number
10 private readonly label: string
11 // Only used for the automatic item
12 private readonly labels: { [id: number]: string }
13 private readonly callback: Function
14
15 private autoResolutionPossible: boolean
16 private currentResolutionLabel: string
17
18 constructor (player: Player, options: any) {
19 options.selectable = true
20
21 super(player, options)
22
23 this.autoResolutionPossible = true
24 this.currentResolutionLabel = ''
25
26 this.label = options.label
27 this.labels = options.labels
28 this.id = options.id
29 this.callback = options.callback
30
31 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
32
33 // We only want to disable the "Auto" item
34 if (this.id === -1) {
35 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
36 }
37 }
38
39 handleClick (event: any) {
40 // Auto button disabled?
41 if (this.autoResolutionPossible === false && this.id === -1) return
42
43 super.handleClick(event)
44
45 this.callback(this.id, 'video')
46 }
47
48 updateSelection (data: ResolutionUpdateData) {
49 if (this.id === -1) {
50 this.currentResolutionLabel = this.labels[data.id]
51 }
52
53 // Automatic resolution only
54 if (data.auto === true) {
55 this.selected(this.id === -1)
56 return
57 }
58
59 this.selected(this.id === data.id)
60 }
61
62 updateAutoResolution (data: AutoResolutionUpdateData) {
63 // Check if the auto resolution is enabled or not
64 if (data.possible === false) {
65 this.addClass('disabled')
66 } else {
67 this.removeClass('disabled')
68 }
69
70 this.autoResolutionPossible = data.possible
71 }
72
73 getLabel () {
74 if (this.id === -1) {
75 return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
76 }
77
78 return this.label
79 }
80}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82
83export { ResolutionMenuItem }
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index b51c52506..14cb8ba43 100644
--- a/client/src/assets/player/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -1,17 +1,20 @@
1// Author: Yanko Shterev 1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu 2// Thanks https://github.com/yshterev/videojs-settings-menu
3 3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
4import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7
5import { SettingsMenuItem } from './settings-menu-item' 8import { SettingsMenuItem } from './settings-menu-item'
6import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
7import { toTitleCase } from './utils' 10import { toTitleCase } from '../utils'
8 11
9const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
10const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
11const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 14const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
12 15
13class SettingsButton extends Button { 16class SettingsButton extends Button {
14 constructor (player: videojs.Player, options) { 17 constructor (player: videojs.Player, options: any) {
15 super(player, options) 18 super(player, options)
16 19
17 this.playerComponent = player 20 this.playerComponent = player
@@ -48,7 +51,7 @@ class SettingsButton extends Button {
48 } 51 }
49 } 52 }
50 53
51 onDisposeSettingsItem (event, name: string) { 54 onDisposeSettingsItem (event: any, name: string) {
52 if (name === undefined) { 55 if (name === undefined) {
53 let children = this.menu.children() 56 let children = this.menu.children()
54 57
@@ -74,7 +77,7 @@ class SettingsButton extends Button {
74 } 77 }
75 } 78 }
76 79
77 onAddSettingsItem (event, data) { 80 onAddSettingsItem (event: any, data: any) {
78 const [ entry, options ] = data 81 const [ entry, options ] = data
79 82
80 this.addMenuItem(entry, options) 83 this.addMenuItem(entry, options)
@@ -120,7 +123,7 @@ class SettingsButton extends Button {
120 this.resetChildren() 123 this.resetChildren()
121 } 124 }
122 125
123 getComponentSize (element) { 126 getComponentSize (element: any) {
124 let width: number = null 127 let width: number = null
125 let height: number = null 128 let height: number = null
126 129
@@ -178,8 +181,8 @@ class SettingsButton extends Button {
178 this.panelChild.addChild(this.menu) 181 this.panelChild.addChild(this.menu)
179 } 182 }
180 183
181 addMenuItem (entry, options) { 184 addMenuItem (entry: any, options: any) {
182 const openSubMenu = function () { 185 const openSubMenu = function (this: any) {
183 if (videojsUntyped.dom.hasClass(this.el_, 'open')) { 186 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
184 videojsUntyped.dom.removeClass(this.el_, 'open') 187 videojsUntyped.dom.removeClass(this.el_, 'open')
185 } else { 188 } else {
@@ -218,7 +221,7 @@ class SettingsButton extends Button {
218} 221}
219 222
220class SettingsPanel extends Component { 223class SettingsPanel extends Component {
221 constructor (player: videojs.Player, options) { 224 constructor (player: videojs.Player, options: any) {
222 super(player, options) 225 super(player, options)
223 } 226 }
224 227
@@ -232,7 +235,7 @@ class SettingsPanel extends Component {
232} 235}
233 236
234class SettingsPanelChild extends Component { 237class SettingsPanelChild extends Component {
235 constructor (player: videojs.Player, options) { 238 constructor (player: videojs.Player, options: any) {
236 super(player, options) 239 super(player, options)
237 } 240 }
238 241
@@ -246,7 +249,7 @@ class SettingsPanelChild extends Component {
246} 249}
247 250
248class SettingsDialog extends Component { 251class SettingsDialog extends Component {
249 constructor (player: videojs.Player, options) { 252 constructor (player: videojs.Player, options: any) {
250 super(player, options) 253 super(player, options)
251 this.hide() 254 this.hide()
252 } 255 }
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 665ce6fc2..f14959f9c 100644
--- a/client/src/assets/player/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -1,16 +1,19 @@
1// Author: Yanko Shterev 1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu 2// Thanks https://github.com/yshterev/videojs-settings-menu
3 3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
4import * as videojs from 'video.js' 6import * as videojs from 'video.js'
5import { toTitleCase } from './utils' 7
6import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 8import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
7 10
8const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
9const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
10 13
11class SettingsMenuItem extends MenuItem { 14class SettingsMenuItem extends MenuItem {
12 15
13 constructor (player: videojs.Player, options, entry: string, menuButton: VideoJSComponentInterface) { 16 constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) {
14 super(player, options) 17 super(player, options)
15 18
16 this.settingsButton = menuButton 19 this.settingsButton = menuButton
@@ -45,6 +48,19 @@ class SettingsMenuItem extends MenuItem {
45 // Update on rate change 48 // Update on rate change
46 player.on('ratechange', this.submenuClickHandler) 49 player.on('ratechange', this.submenuClickHandler)
47 50
51 if (subMenuName === 'CaptionsButton') {
52 // Hack to regenerate captions on HTTP fallback
53 player.on('captionsChanged', () => {
54 setTimeout(() => {
55 this.settingsSubMenuEl_.innerHTML = ''
56 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
57 this.update()
58 this.bindClickEvents()
59
60 }, 0)
61 })
62 }
63
48 this.reset() 64 this.reset()
49 }, 0) 65 }, 0)
50 }) 66 })
@@ -55,7 +71,7 @@ class SettingsMenuItem extends MenuItem {
55 this.transitionEndHandler = this.onTransitionEnd.bind(this) 71 this.transitionEndHandler = this.onTransitionEnd.bind(this)
56 } 72 }
57 73
58 onSubmenuClick (event) { 74 onSubmenuClick (event: any) {
59 let target = null 75 let target = null
60 76
61 if (event.type === 'tap') { 77 if (event.type === 'tap') {
@@ -150,7 +166,7 @@ class SettingsMenuItem extends MenuItem {
150 * 166 *
151 * @method PrefixedEvent 167 * @method PrefixedEvent
152 */ 168 */
153 PrefixedEvent (element, type, callback, action = 'addEvent') { 169 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
154 let prefix = ['webkit', 'moz', 'MS', 'o', ''] 170 let prefix = ['webkit', 'moz', 'MS', 'o', '']
155 171
156 for (let p = 0; p < prefix.length; p++) { 172 for (let p = 0; p < prefix.length; p++) {
@@ -166,7 +182,7 @@ class SettingsMenuItem extends MenuItem {
166 } 182 }
167 } 183 }
168 184
169 onTransitionEnd (event) { 185 onTransitionEnd (event: any) {
170 if (event.propertyName !== 'margin-right') { 186 if (event.propertyName !== 'margin-right') {
171 return 187 return
172 } 188 }
@@ -204,12 +220,14 @@ class SettingsMenuItem extends MenuItem {
204 } 220 }
205 221
206 build () { 222 build () {
207 const saveUpdateLabel = this.subMenu.updateLabel 223 this.subMenu.on('updateLabel', () => {
208 this.subMenu.updateLabel = () => {
209 this.update() 224 this.update()
210 225 })
211 saveUpdateLabel.call(this.subMenu) 226 this.subMenu.on('menuChanged', () => {
212 } 227 this.bindClickEvents()
228 this.setSize()
229 this.update()
230 })
213 231
214 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) 232 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
215 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 233 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
@@ -217,7 +235,7 @@ class SettingsMenuItem extends MenuItem {
217 this.update() 235 this.update()
218 236
219 this.createBackButton() 237 this.createBackButton()
220 this.getSize() 238 this.setSize()
221 this.bindClickEvents() 239 this.bindClickEvents()
222 240
223 // prefixed event listeners for CSS TransitionEnd 241 // prefixed event listeners for CSS TransitionEnd
@@ -229,8 +247,8 @@ class SettingsMenuItem extends MenuItem {
229 ) 247 )
230 } 248 }
231 249
232 update (event?: Event) { 250 update (event?: any) {
233 let target = null 251 let target: HTMLElement = null
234 let subMenu = this.subMenu.name() 252 let subMenu = this.subMenu.name()
235 253
236 if (event && event.type === 'tap') { 254 if (event && event.type === 'tap') {
@@ -279,8 +297,9 @@ class SettingsMenuItem extends MenuItem {
279 297
280 // save size of submenus on first init 298 // save size of submenus on first init
281 // if number of submenu items change dynamically more logic will be needed 299 // if number of submenu items change dynamically more logic will be needed
282 getSize () { 300 setSize () {
283 this.dialog.removeClass('vjs-hidden') 301 this.dialog.removeClass('vjs-hidden')
302 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
284 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) 303 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
285 this.setMargin() 304 this.setMargin()
286 this.dialog.addClass('vjs-hidden') 305 this.dialog.addClass('vjs-hidden')
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts
index 5cf0b6425..1e11a9546 100644
--- a/client/src/assets/player/theater-button.ts
+++ b/client/src/assets/player/videojs-components/theater-button.ts
@@ -1,12 +1,16 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1// FIXME: something weird with our path definition in tsconfig and typings
2import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' 2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
3 7
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class TheaterButton extends Button { 9class TheaterButton extends Button {
6 10
7 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' 11 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled'
8 12
9 constructor (player, options) { 13 constructor (player: videojs.Player, options: any) {
10 super(player, options) 14 super(player, options)
11 15
12 const enabled = getStoredTheater() 16 const enabled = getStoredTheater()
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts
index 767e46821..54cc0ea64 100644
--- a/client/src/assets/player/peertube-chunk-store.ts
+++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts
@@ -40,15 +40,15 @@ export class PeertubeChunkStore extends EventEmitter {
40 // If the store is full 40 // If the store is full
41 private memoryChunks: { [ id: number ]: Buffer | true } = {} 41 private memoryChunks: { [ id: number ]: Buffer | true } = {}
42 private databaseName: string 42 private databaseName: string
43 private putBulkTimeout 43 private putBulkTimeout: any
44 private cleanerInterval 44 private cleanerInterval: any
45 private db: ChunkDatabase 45 private db: ChunkDatabase
46 private expirationDB: ExpirationDatabase 46 private expirationDB: ExpirationDatabase
47 private readonly length: number 47 private readonly length: number
48 private readonly lastChunkLength: number 48 private readonly lastChunkLength: number
49 private readonly lastChunkIndex: number 49 private readonly lastChunkIndex: number
50 50
51 constructor (chunkLength: number, opts) { 51 constructor (chunkLength: number, opts: any) {
52 super() 52 super()
53 53
54 this.databaseName = 'webtorrent-chunks-' 54 this.databaseName = 'webtorrent-chunks-'
@@ -76,7 +76,7 @@ export class PeertubeChunkStore extends EventEmitter {
76 this.runCleaner() 76 this.runCleaner()
77 } 77 }
78 78
79 put (index: number, buf: Buffer, cb: Function) { 79 put (index: number, buf: Buffer, cb: (err?: Error) => void) {
80 const isLastChunk = (index === this.lastChunkIndex) 80 const isLastChunk = (index === this.lastChunkIndex)
81 if (isLastChunk && buf.length !== this.lastChunkLength) { 81 if (isLastChunk && buf.length !== this.lastChunkLength) {
82 return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength)) 82 return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength))
@@ -113,13 +113,13 @@ export class PeertubeChunkStore extends EventEmitter {
113 }, PeertubeChunkStore.BUFFERING_PUT_MS) 113 }, PeertubeChunkStore.BUFFERING_PUT_MS)
114 } 114 }
115 115
116 get (index: number, opts, cb) { 116 get (index: number, opts: any, cb: (err?: Error, buf?: Buffer) => void): void {
117 if (typeof opts === 'function') return this.get(index, null, opts) 117 if (typeof opts === 'function') return this.get(index, null, opts)
118 118
119 // IndexDB could be slow, use our memory index first 119 // IndexDB could be slow, use our memory index first
120 const memoryChunk = this.memoryChunks[index] 120 const memoryChunk = this.memoryChunks[index]
121 if (memoryChunk === undefined) { 121 if (memoryChunk === undefined) {
122 const err = new Error('Chunk not found') 122 const err = new Error('Chunk not found') as any
123 err['notFound'] = true 123 err['notFound'] = true
124 124
125 return process.nextTick(() => cb(err)) 125 return process.nextTick(() => cb(err))
@@ -146,11 +146,11 @@ export class PeertubeChunkStore extends EventEmitter {
146 }) 146 })
147 } 147 }
148 148
149 close (db) { 149 close (cb: (err?: Error) => void) {
150 return this.destroy(db) 150 return this.destroy(cb)
151 } 151 }
152 152
153 async destroy (cb) { 153 async destroy (cb: (err?: Error) => void) {
154 try { 154 try {
155 if (this.pendingPut) { 155 if (this.pendingPut) {
156 clearTimeout(this.putBulkTimeout) 156 clearTimeout(this.putBulkTimeout)
@@ -225,7 +225,7 @@ export class PeertubeChunkStore extends EventEmitter {
225 } 225 }
226 } 226 }
227 227
228 private nextTick (cb, err, val?) { 228 private nextTick <T> (cb: (err?: Error, val?: T) => void, err: Error, val?: T) {
229 process.nextTick(() => cb(err, val), undefined) 229 process.nextTick(() => cb(err, val), undefined)
230 } 230 }
231} 231}
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts
index 2cb05a448..a3415937b 100644
--- a/client/src/assets/player/video-renderer.ts
+++ b/client/src/assets/player/webtorrent/video-renderer.ts
@@ -1,9 +1,9 @@
1// Thanks: https://github.com/feross/render-media 1// Thanks: https://github.com/feross/render-media
2// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed 2// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed
3 3
4import * as MediaElementWrapper from 'mediasource' 4const MediaElementWrapper = require('mediasource')
5import { extname } from 'path' 5import { extname } from 'path'
6import * as videostream from 'videostream' 6const videostream = require('videostream')
7 7
8const VIDEOSTREAM_EXTS = [ 8const VIDEOSTREAM_EXTS = [
9 '.m4a', 9 '.m4a',
@@ -17,7 +17,7 @@ type RenderMediaOptions = {
17} 17}
18 18
19function renderVideo ( 19function renderVideo (
20 file, 20 file: any,
21 elem: HTMLVideoElement, 21 elem: HTMLVideoElement,
22 opts: RenderMediaOptions, 22 opts: RenderMediaOptions,
23 callback: (err: Error, renderer: any) => void 23 callback: (err: Error, renderer: any) => void
@@ -27,11 +27,11 @@ function renderVideo (
27 return renderMedia(file, elem, opts, callback) 27 return renderMedia(file, elem, opts, callback)
28} 28}
29 29
30function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { 30function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) {
31 const extension = extname(file.name).toLowerCase() 31 const extension = extname(file.name).toLowerCase()
32 let preparedElem = undefined 32 let preparedElem: any = undefined
33 let currentTime = 0 33 let currentTime = 0
34 let renderer 34 let renderer: any
35 35
36 try { 36 try {
37 if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) { 37 if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
@@ -45,7 +45,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
45 45
46 function useVideostream () { 46 function useVideostream () {
47 prepareElem() 47 prepareElem()
48 preparedElem.addEventListener('error', function onError (err) { 48 preparedElem.addEventListener('error', function onError (err: Error) {
49 preparedElem.removeEventListener('error', onError) 49 preparedElem.removeEventListener('error', onError)
50 50
51 return callback(err) 51 return callback(err)
@@ -58,7 +58,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
58 const codecs = getCodec(file.name, useVP9) 58 const codecs = getCodec(file.name, useVP9)
59 59
60 prepareElem() 60 prepareElem()
61 preparedElem.addEventListener('error', function onError (err) { 61 preparedElem.addEventListener('error', function onError (err: Error) {
62 preparedElem.removeEventListener('error', onError) 62 preparedElem.removeEventListener('error', onError)
63 63
64 // Try with vp9 before returning an error 64 // Try with vp9 before returning an error
@@ -102,7 +102,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
102 } 102 }
103} 103}
104 104
105function validateFile (file) { 105function validateFile (file: any) {
106 if (file == null) { 106 if (file == null) {
107 throw new Error('file cannot be null or undefined') 107 throw new Error('file cannot be null or undefined')
108 } 108 }
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 2330f476f..c69bf31fa 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -1,30 +1,37 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
1import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4
2import * as WebTorrent from 'webtorrent' 5import * as WebTorrent from 'webtorrent'
3import { VideoFile } from '../../../../shared/models/videos/video.model' 6import { VideoFile } from '../../../../../shared/models/videos/video.model'
4import { renderVideo } from './video-renderer' 7import { renderVideo } from './video-renderer'
5import './settings-menu-button' 8import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
6import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
7import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
8import * as CacheChunkStore from 'cache-chunk-store'
9import { PeertubeChunkStore } from './peertube-chunk-store' 10import { PeertubeChunkStore } from './peertube-chunk-store'
10import { 11import {
11 getAverageBandwidthInStore, 12 getAverageBandwidthInStore,
12 getStoredMute, 13 getStoredMute,
13 getStoredVolume, 14 getStoredVolume,
14 saveAverageBandwidth, 15 getStoredWebTorrentEnabled,
15 saveMuteInStore, 16 saveAverageBandwidth
16 saveVolumeInStore 17} from '../peertube-player-local-storage'
17} from './peertube-player-local-storage' 18
19const CacheChunkStore = require('cache-chunk-store')
20
21type PlayOptions = {
22 forcePlay?: boolean,
23 seek?: number,
24 delay?: number
25}
18 26
19const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
20class PeerTubePlugin extends Plugin { 28class WebTorrentPlugin extends Plugin {
21 private readonly playerElement: HTMLVideoElement 29 private readonly playerElement: HTMLVideoElement
22 30
23 private readonly autoplay: boolean = false 31 private readonly autoplay: boolean = false
24 private readonly startTime: number = 0 32 private readonly startTime: number = 0
25 private readonly savePlayerSrcFunction: Function 33 private readonly savePlayerSrcFunction: Function
26 private readonly videoFiles: VideoFile[] 34 private readonly videoFiles: VideoFile[]
27 private readonly videoViewUrl: string
28 private readonly videoDuration: number 35 private readonly videoDuration: number
29 private readonly CONSTANTS = { 36 private readonly CONSTANTS = {
30 INFO_SCHEDULER: 1000, // Don't change this 37 INFO_SCHEDULER: 1000, // Don't change this
@@ -32,22 +39,12 @@ class PeerTubePlugin extends Plugin {
32 AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it 39 AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
33 AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check 40 AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
34 AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds 41 AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
35 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth 42 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
36 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
37 } 43 }
38 44
39 private readonly webtorrent = new WebTorrent({ 45 private readonly webtorrent = new WebTorrent({
40 tracker: { 46 tracker: {
41 rtcConfig: { 47 rtcConfig: getRtcConfig()
42 iceServers: [
43 {
44 urls: 'stun:stun.stunprotocol.org'
45 },
46 {
47 urls: 'stun:stun.framasoft.org'
48 }
49 ]
50 }
51 }, 48 },
52 dht: false 49 dht: false
53 }) 50 })
@@ -55,65 +52,56 @@ class PeerTubePlugin extends Plugin {
55 private player: any 52 private player: any
56 private currentVideoFile: VideoFile 53 private currentVideoFile: VideoFile
57 private torrent: WebTorrent.Torrent 54 private torrent: WebTorrent.Torrent
58 private videoCaptions: VideoJSCaption[]
59 55
60 private renderer 56 private renderer: any
61 private fakeRenderer 57 private fakeRenderer: any
62 private destoyingFakeRenderer = false 58 private destroyingFakeRenderer = false
63 59
64 private autoResolution = true 60 private autoResolution = true
65 private forbidAutoResolution = false 61 private autoResolutionPossible = true
66 private isAutoResolutionObservation = false 62 private isAutoResolutionObservation = false
63 private playerRefusedP2P = false
67 64
68 private videoViewInterval 65 private torrentInfoInterval: any
69 private torrentInfoInterval 66 private autoQualityInterval: any
70 private autoQualityInterval 67 private addTorrentDelay: any
71 private userWatchingVideoInterval 68 private qualityObservationTimer: any
72 private addTorrentDelay 69 private runAutoQualitySchedulerTimer: any
73 private qualityObservationTimer
74 private runAutoQualitySchedulerTimer
75 70
76 private downloadSpeeds: number[] = [] 71 private downloadSpeeds: number[] = []
77 72
78 constructor (player: videojs.Player, options: PeertubePluginOptions) { 73 constructor (player: videojs.Player, options: WebtorrentPluginOptions) {
79 super(player, options) 74 super(player, options)
80 75
81 // Disable auto play on iOS 76 // Disable auto play on iOS
82 this.autoplay = options.autoplay && this.isIOS() === false 77 this.autoplay = options.autoplay && this.isIOS() === false
78 this.playerRefusedP2P = !getStoredWebTorrentEnabled()
83 79
84 this.startTime = timeToInt(options.startTime)
85 this.videoFiles = options.videoFiles 80 this.videoFiles = options.videoFiles
86 this.videoViewUrl = options.videoViewUrl
87 this.videoDuration = options.videoDuration 81 this.videoDuration = options.videoDuration
88 this.videoCaptions = options.videoCaptions
89 82
90 this.savePlayerSrcFunction = this.player.src 83 this.savePlayerSrcFunction = this.player.src
91 this.playerElement = options.playerElement 84 this.playerElement = options.playerElement
92 85
93 if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
94
95 this.player.ready(() => { 86 this.player.ready(() => {
87 const playerOptions = this.player.options_
88
96 const volume = getStoredVolume() 89 const volume = getStoredVolume()
97 if (volume !== undefined) this.player.volume(volume) 90 if (volume !== undefined) this.player.volume(volume)
98 const muted = getStoredMute() 91
92 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
99 if (muted !== undefined) this.player.muted(muted) 93 if (muted !== undefined) this.player.muted(muted)
100 94
95 this.player.duration(options.videoDuration)
96
101 this.initializePlayer() 97 this.initializePlayer()
102 this.runTorrentInfoScheduler() 98 this.runTorrentInfoScheduler()
103 this.runViewAdd()
104
105 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
106 99
107 this.player.one('play', () => { 100 this.player.one('play', () => {
108 // Don't run immediately scheduler, wait some seconds the TCP connections are made 101 // Don't run immediately scheduler, wait some seconds the TCP connections are made
109 this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER) 102 this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
110 }) 103 })
111 }) 104 })
112
113 this.player.on('volumechange', () => {
114 saveVolumeInStore(this.player.volume())
115 saveMuteInStore(this.player.muted())
116 })
117 } 105 }
118 106
119 dispose () { 107 dispose () {
@@ -121,12 +109,9 @@ class PeerTubePlugin extends Plugin {
121 clearTimeout(this.qualityObservationTimer) 109 clearTimeout(this.qualityObservationTimer)
122 clearTimeout(this.runAutoQualitySchedulerTimer) 110 clearTimeout(this.runAutoQualitySchedulerTimer)
123 111
124 clearInterval(this.videoViewInterval)
125 clearInterval(this.torrentInfoInterval) 112 clearInterval(this.torrentInfoInterval)
126 clearInterval(this.autoQualityInterval) 113 clearInterval(this.autoQualityInterval)
127 114
128 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
129
130 // Don't need to destroy renderer, video player will be destroyed 115 // Don't need to destroy renderer, video player will be destroyed
131 this.flushVideoFile(this.currentVideoFile, false) 116 this.flushVideoFile(this.currentVideoFile, false)
132 117
@@ -137,13 +122,6 @@ class PeerTubePlugin extends Plugin {
137 return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 122 return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1
138 } 123 }
139 124
140 getCurrentResolutionLabel () {
141 if (!this.currentVideoFile) return ''
142
143 const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : ''
144 return this.currentVideoFile.resolution.label + fps
145 }
146
147 updateVideoFile ( 125 updateVideoFile (
148 videoFile?: VideoFile, 126 videoFile?: VideoFile,
149 options: { 127 options: {
@@ -178,12 +156,22 @@ class PeerTubePlugin extends Plugin {
178 const previousVideoFile = this.currentVideoFile 156 const previousVideoFile = this.currentVideoFile
179 this.currentVideoFile = videoFile 157 this.currentVideoFile = videoFile
180 158
159 // Don't try on iOS that does not support MediaSource
160 // Or don't use P2P if webtorrent is disabled
161 if (this.isIOS() || this.playerRefusedP2P) {
162 return this.fallbackToHttp(options, () => {
163 this.player.playbackRate(oldPlaybackRate)
164 return done()
165 })
166 }
167
181 this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => { 168 this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => {
182 this.player.playbackRate(oldPlaybackRate) 169 this.player.playbackRate(oldPlaybackRate)
183 return done() 170 return done()
184 }) 171 })
185 172
186 this.trigger('videoFileUpdate') 173 this.changeQuality()
174 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
187 } 175 }
188 176
189 updateResolution (resolutionId: number, delay = 0) { 177 updateResolution (resolutionId: number, delay = 0) {
@@ -217,28 +205,17 @@ class PeerTubePlugin extends Plugin {
217 } 205 }
218 } 206 }
219 207
220 isAutoResolutionOn () {
221 return this.autoResolution
222 }
223
224 enableAutoResolution () { 208 enableAutoResolution () {
225 this.autoResolution = true 209 this.autoResolution = true
226 this.trigger('autoResolutionUpdate') 210 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
227 } 211 }
228 212
229 disableAutoResolution (forbid = false) { 213 disableAutoResolution (forbid = false) {
230 if (forbid === true) this.forbidAutoResolution = true 214 if (forbid === true) this.autoResolutionPossible = false
231 215
232 this.autoResolution = false 216 this.autoResolution = false
233 this.trigger('autoResolutionUpdate') 217 this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible })
234 } 218 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
235
236 isAutoResolutionForbidden () {
237 return this.forbidAutoResolution === true
238 }
239
240 getCurrentVideoFile () {
241 return this.currentVideoFile
242 } 219 }
243 220
244 getTorrent () { 221 getTorrent () {
@@ -248,18 +225,14 @@ class PeerTubePlugin extends Plugin {
248 private addTorrent ( 225 private addTorrent (
249 magnetOrTorrentUrl: string, 226 magnetOrTorrentUrl: string,
250 previousVideoFile: VideoFile, 227 previousVideoFile: VideoFile,
251 options: { 228 options: PlayOptions,
252 forcePlay?: boolean,
253 seek?: number,
254 delay?: number
255 },
256 done: Function 229 done: Function
257 ) { 230 ) {
258 console.log('Adding ' + magnetOrTorrentUrl + '.') 231 console.log('Adding ' + magnetOrTorrentUrl + '.')
259 232
260 const oldTorrent = this.torrent 233 const oldTorrent = this.torrent
261 const torrentOptions = { 234 const torrentOptions = {
262 store: (chunkLength, storeOpts) => new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { 235 store: (chunkLength: number, storeOpts: any) => new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), {
263 max: 100 236 max: 100
264 }) 237 })
265 } 238 }
@@ -284,11 +257,14 @@ class PeerTubePlugin extends Plugin {
284 257
285 this.flushVideoFile(previousVideoFile) 258 this.flushVideoFile(previousVideoFile)
286 259
260 // Update progress bar (just for the UI), do not wait rendering
261 if (options.seek) this.player.currentTime(options.seek)
262
287 const renderVideoOptions = { autoplay: false, controls: true } 263 const renderVideoOptions = { autoplay: false, controls: true }
288 renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => { 264 renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
289 this.renderer = renderer 265 this.renderer = renderer
290 266
291 if (err) return this.fallbackToHttp(done) 267 if (err) return this.fallbackToHttp(options, done)
292 268
293 return this.tryToPlay(err => { 269 return this.tryToPlay(err => {
294 if (err) return done(err) 270 if (err) return done(err)
@@ -296,13 +272,13 @@ class PeerTubePlugin extends Plugin {
296 if (options.seek) this.seek(options.seek) 272 if (options.seek) this.seek(options.seek)
297 if (options.forcePlay === false && paused === true) this.player.pause() 273 if (options.forcePlay === false && paused === true) this.player.pause()
298 274
299 return done(err) 275 return done()
300 }) 276 })
301 }) 277 })
302 }, options.delay || 0) 278 }, options.delay || 0)
303 }) 279 })
304 280
305 this.torrent.on('error', err => console.error(err)) 281 this.torrent.on('error', (err: any) => console.error(err))
306 282
307 this.torrent.on('warning', (err: any) => { 283 this.torrent.on('warning', (err: any) => {
308 // We don't support HTTP tracker but we don't care -> we use the web socket tracker 284 // We don't support HTTP tracker but we don't care -> we use the web socket tracker
@@ -330,13 +306,13 @@ class PeerTubePlugin extends Plugin {
330 }) 306 })
331 } 307 }
332 308
333 private tryToPlay (done?: Function) { 309 private tryToPlay (done?: (err?: Error) => void) {
334 if (!done) done = function () { /* empty */ } 310 if (!done) done = function () { /* empty */ }
335 311
336 const playPromise = this.player.play() 312 const playPromise = this.player.play()
337 if (playPromise !== undefined) { 313 if (playPromise !== undefined) {
338 return playPromise.then(done) 314 return playPromise.then(done)
339 .catch(err => { 315 .catch((err: Error) => {
340 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { 316 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) {
341 return 317 return
342 } 318 }
@@ -418,13 +394,7 @@ class PeerTubePlugin extends Plugin {
418 } 394 }
419 395
420 private initializePlayer () { 396 private initializePlayer () {
421 if (isMobile()) this.player.addClass('vjs-is-mobile') 397 this.buildQualities()
422
423 this.initSmoothProgressBar()
424
425 this.initCaptions()
426
427 this.alterInactivity()
428 398
429 if (this.autoplay === true) { 399 if (this.autoplay === true) {
430 this.player.posterImage.hide() 400 this.player.posterImage.hide()
@@ -432,12 +402,6 @@ class PeerTubePlugin extends Plugin {
432 return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) 402 return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
433 } 403 }
434 404
435 // Don't try on iOS that does not support MediaSource
436 if (this.isIOS()) {
437 this.currentVideoFile = this.pickAverageVideoFile()
438 return this.fallbackToHttp(undefined, false)
439 }
440
441 // Proxy first play 405 // Proxy first play
442 const oldPlay = this.player.play.bind(this.player) 406 const oldPlay = this.player.play.bind(this.player)
443 this.player.play = () => { 407 this.player.play = () => {
@@ -453,7 +417,7 @@ class PeerTubePlugin extends Plugin {
453 417
454 // Not initialized or in HTTP fallback 418 // Not initialized or in HTTP fallback
455 if (this.torrent === undefined || this.torrent === null) return 419 if (this.torrent === undefined || this.torrent === null) return
456 if (this.isAutoResolutionOn() === false) return 420 if (this.autoResolution === false) return
457 if (this.isAutoResolutionObservation === true) return 421 if (this.isAutoResolutionObservation === true) return
458 422
459 const file = this.getAppropriateFile() 423 const file = this.getAppropriateFile()
@@ -493,81 +457,32 @@ class PeerTubePlugin extends Plugin {
493 if (this.torrent === undefined) return 457 if (this.torrent === undefined) return
494 458
495 // Http fallback 459 // Http fallback
496 if (this.torrent === null) return this.trigger('torrentInfo', false) 460 if (this.torrent === null) return this.player.trigger('p2pInfo', false)
497 461
498 // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too 462 // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too
499 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) 463 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
500 464
501 return this.trigger('torrentInfo', { 465 return this.player.trigger('p2pInfo', {
502 downloadSpeed: this.torrent.downloadSpeed, 466 http: {
503 numPeers: this.torrent.numPeers, 467 downloadSpeed: 0,
504 uploadSpeed: this.torrent.uploadSpeed, 468 uploadSpeed: 0,
505 downloaded: this.torrent.downloaded, 469 downloaded: 0,
506 uploaded: this.torrent.uploaded 470 uploaded: 0
507 }) 471 },
508 }, this.CONSTANTS.INFO_SCHEDULER) 472 p2p: {
509 } 473 downloadSpeed: this.torrent.downloadSpeed,
510 474 numPeers: this.torrent.numPeers,
511 private runViewAdd () { 475 uploadSpeed: this.torrent.uploadSpeed,
512 this.clearVideoViewInterval() 476 downloaded: this.torrent.downloaded,
513 477 uploaded: this.torrent.uploaded
514 // After 30 seconds (or 3/4 of the video), add a view to the video
515 let minSecondsToView = 30
516
517 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
518
519 let secondsViewed = 0
520 this.videoViewInterval = setInterval(() => {
521 if (this.player && !this.player.paused()) {
522 secondsViewed += 1
523
524 if (secondsViewed > minSecondsToView) {
525 this.clearVideoViewInterval()
526
527 this.addViewToVideo().catch(err => console.error(err))
528 } 478 }
529 } 479 } as PlayerNetworkInfo)
530 }, 1000) 480 }, this.CONSTANTS.INFO_SCHEDULER)
531 }
532
533 private runUserWatchVideo (options: UserWatching) {
534 let lastCurrentTime = 0
535
536 this.userWatchingVideoInterval = setInterval(() => {
537 const currentTime = Math.floor(this.player.currentTime())
538
539 if (currentTime - lastCurrentTime >= 1) {
540 lastCurrentTime = currentTime
541
542 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
543 .catch(err => console.error('Cannot notify user is watching.', err))
544 }
545 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
546 }
547
548 private clearVideoViewInterval () {
549 if (this.videoViewInterval !== undefined) {
550 clearInterval(this.videoViewInterval)
551 this.videoViewInterval = undefined
552 }
553 }
554
555 private addViewToVideo () {
556 if (!this.videoViewUrl) return Promise.resolve(undefined)
557
558 return fetch(this.videoViewUrl, { method: 'POST' })
559 } 481 }
560 482
561 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { 483 private fallbackToHttp (options: PlayOptions, done?: Function) {
562 const body = new URLSearchParams() 484 const paused = this.player.paused()
563 body.append('currentTime', currentTime.toString())
564
565 const headers = new Headers({ 'Authorization': authorizationHeader })
566 485
567 return fetch(url, { method: 'PUT', body, headers })
568 }
569
570 private fallbackToHttp (done?: Function, play = true) {
571 this.disableAutoResolution(true) 486 this.disableAutoResolution(true)
572 487
573 this.flushVideoFile(this.currentVideoFile, true) 488 this.flushVideoFile(this.currentVideoFile, true)
@@ -579,9 +494,20 @@ class PeerTubePlugin extends Plugin {
579 const httpUrl = this.currentVideoFile.fileUrl 494 const httpUrl = this.currentVideoFile.fileUrl
580 this.player.src = this.savePlayerSrcFunction 495 this.player.src = this.savePlayerSrcFunction
581 this.player.src(httpUrl) 496 this.player.src(httpUrl)
582 if (play) this.tryToPlay()
583 497
584 if (done) return done() 498 this.changeQuality()
499
500 // We changed the source, so reinit captions
501 this.player.trigger('sourcechange')
502
503 return this.tryToPlay(err => {
504 if (err && done) return done(err)
505
506 if (options.seek) this.seek(options.seek)
507 if (options.forcePlay === false && paused === true) this.player.pause()
508
509 if (done) return done()
510 })
585 } 511 }
586 512
587 private handleError (err: Error | string) { 513 private handleError (err: Error | string) {
@@ -600,25 +526,6 @@ class PeerTubePlugin extends Plugin {
600 return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) 526 return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
601 } 527 }
602 528
603 private alterInactivity () {
604 let saveInactivityTimeout: number
605
606 const disableInactivity = () => {
607 saveInactivityTimeout = this.player.options_.inactivityTimeout
608 this.player.options_.inactivityTimeout = 0
609 }
610 const enableInactivity = () => {
611 this.player.options_.inactivityTimeout = saveInactivityTimeout
612 }
613
614 const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog')
615
616 this.player.controlBar.on('mouseenter', () => disableInactivity())
617 settingsDialog.on('mouseenter', () => disableInactivity())
618 this.player.controlBar.on('mouseleave', () => enableInactivity())
619 settingsDialog.on('mouseleave', () => enableInactivity())
620 }
621
622 private pickAverageVideoFile () { 529 private pickAverageVideoFile () {
623 if (this.videoFiles.length === 1) return this.videoFiles[0] 530 if (this.videoFiles.length === 1) return this.videoFiles[0]
624 531
@@ -632,14 +539,14 @@ class PeerTubePlugin extends Plugin {
632 } 539 }
633 540
634 private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) { 541 private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) {
635 this.destoyingFakeRenderer = false 542 this.destroyingFakeRenderer = false
636 543
637 const fakeVideoElem = document.createElement('video') 544 const fakeVideoElem = document.createElement('video')
638 renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => { 545 renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => {
639 this.fakeRenderer = renderer 546 this.fakeRenderer = renderer
640 547
641 // The renderer returns an error when we destroy it, so skip them 548 // The renderer returns an error when we destroy it, so skip them
642 if (this.destoyingFakeRenderer === false && err) { 549 if (this.destroyingFakeRenderer === false && err) {
643 console.error('Cannot render new torrent in fake video element.', err) 550 console.error('Cannot render new torrent in fake video element.', err)
644 } 551 }
645 552
@@ -650,7 +557,7 @@ class PeerTubePlugin extends Plugin {
650 557
651 private destroyFakeRenderer () { 558 private destroyFakeRenderer () {
652 if (this.fakeRenderer) { 559 if (this.fakeRenderer) {
653 this.destoyingFakeRenderer = true 560 this.destroyingFakeRenderer = true
654 561
655 if (this.fakeRenderer.destroy) { 562 if (this.fakeRenderer.destroy) {
656 try { 563 try {
@@ -663,40 +570,70 @@ class PeerTubePlugin extends Plugin {
663 } 570 }
664 } 571 }
665 572
666 private initCaptions () { 573 private buildQualities () {
667 for (const caption of this.videoCaptions) { 574 const qualityLevelsPayload = []
668 this.player.addRemoteTextTrack({ 575
669 kind: 'captions', 576 for (const file of this.videoFiles) {
670 label: caption.label, 577 const representation = {
671 language: caption.language, 578 id: file.resolution.id,
672 id: caption.language, 579 label: this.buildQualityLabel(file),
673 src: caption.src 580 height: file.resolution.id,
674 }, false) 581 _enabled: true
582 }
583
584 this.player.qualityLevels().addQualityLevel(representation)
585
586 qualityLevelsPayload.push({
587 id: representation.id,
588 label: representation.label,
589 selected: false
590 })
591 }
592
593 const payload: LoadedQualityData = {
594 qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d),
595 qualityData: {
596 video: qualityLevelsPayload
597 }
675 } 598 }
599 this.player.tech_.trigger('loadedqualitydata', payload)
676 } 600 }
677 601
678 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 602 private buildQualityLabel (file: VideoFile) {
679 private initSmoothProgressBar () { 603 let label = file.resolution.label
680 const SeekBar = videojsUntyped.getComponent('SeekBar') 604
681 SeekBar.prototype.getPercent = function getPercent () { 605 if (file.fps && file.fps >= 50) {
682 // Allows for smooth scrubbing, when player can't keep up. 606 label += file.fps
683 // const time = (this.player_.scrubbing()) ?
684 // this.player_.getCache().currentTime :
685 // this.player_.currentTime()
686 const time = this.player_.currentTime()
687 const percent = time / this.player_.duration()
688 return percent >= 1 ? 1 : percent
689 } 607 }
690 SeekBar.prototype.handleMouseMove = function handleMouseMove (event) { 608
691 let newTime = this.calculateDistance(event) * this.player_.duration() 609 return label
692 if (newTime === this.player_.duration()) { 610 }
693 newTime = newTime - 0.1 611
694 } 612 private qualitySwitchCallback (id: number) {
695 this.player_.currentTime(newTime) 613 if (id === -1) {
696 this.update() 614 if (this.autoResolutionPossible === true) this.enableAutoResolution()
615 return
616 }
617
618 this.disableAutoResolution()
619 this.updateResolution(id)
620 }
621
622 private changeQuality () {
623 const resolutionId = this.currentVideoFile.resolution.id
624 const qualityLevels = this.player.qualityLevels()
625
626 if (resolutionId === -1) {
627 qualityLevels.selectedIndex = -1
628 return
629 }
630
631 for (let i = 0; i < qualityLevels; i++) {
632 const q = this.player.qualityLevels[i]
633 if (q.height === resolutionId) qualityLevels.selectedIndex = i
697 } 634 }
698 } 635 }
699} 636}
700 637
701videojs.registerPlugin('peertube', PeerTubePlugin) 638videojs.registerPlugin('webtorrent', WebTorrentPlugin)
702export { PeerTubePlugin } 639export { WebTorrentPlugin }