diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
commit | 88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch) | |
tree | b242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/assets/player | |
parent | 53a94c7cfa8368da4cd248d65df8346905938f0c (diff) | |
parent | 9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff) | |
download | PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip |
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/assets/player')
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 | ||
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
6 | import { Events } from 'p2p-media-loader-core' | ||
7 | |||
8 | // videojs-hlsjs-plugin needs videojs in window | ||
9 | window['videojs'] = videojs | ||
10 | require('@streamroot/videojs-hlsjs-plugin') | ||
11 | |||
12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
13 | class 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 | |||
142 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
143 | export { 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 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { Segment } from 'p2p-media-loader-core' | ||
3 | |||
4 | function 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 | |||
20 | export { | ||
21 | segmentUrlBuilderFactory | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function 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 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | ||
2 | import { basename } from 'path' | ||
3 | |||
4 | function 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 | |||
31 | export { | ||
32 | segmentValidatorFactory | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function 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 | |||
46 | function 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 | ||
53 | function 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 | ||
13 | function 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 | |||
13 | function getStoredMute () { | 21 | function 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 | ||
63 | function saveLastSubtitle (language: string) { | ||
64 | return setLocalStorage('last-subtitle', language) | ||
65 | } | ||
66 | |||
67 | function getStoredLastSubtitle () { | ||
68 | return getLocalStorage('last-subtitle') | ||
69 | } | ||
70 | |||
55 | // --------------------------------------------------------------------------- | 71 | // --------------------------------------------------------------------------- |
56 | 72 | ||
57 | export { | 73 | export { |
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 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | ||
5 | import 'videojs-dock' | ||
6 | import 'videojs-contextmenu-ui' | ||
7 | import 'videojs-contrib-quality-levels' | ||
8 | import './peertube-plugin' | ||
9 | import './videojs-components/peertube-link-button' | ||
10 | import './videojs-components/resolution-menu-button' | ||
11 | import './videojs-components/settings-menu-button' | ||
12 | import './videojs-components/p2p-info-button' | ||
13 | import './videojs-components/peertube-load-progress-bar' | ||
14 | import './videojs-components/theater-button' | ||
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | ||
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | ||
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
18 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
19 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.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) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
29 | |||
30 | export type WebtorrentOptions = { | ||
31 | videoFiles: VideoFile[] | ||
32 | } | ||
33 | |||
34 | export type P2PMediaLoaderOptions = { | ||
35 | playlistUrl: string | ||
36 | segmentsSha256Url: string | ||
37 | trackerAnnounce: string[] | ||
38 | redundancyBaseUrls: string[] | ||
39 | videoFiles: VideoFile[] | ||
40 | } | ||
41 | |||
42 | export 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 | |||
73 | export type PeertubePlayerManagerOptions = { | ||
74 | common: CommonOptions, | ||
75 | webtorrent: WebtorrentOptions, | ||
76 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
77 | } | ||
78 | |||
79 | export 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 | |||
464 | export { | ||
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 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | |||
3 | import 'videojs-hotkeys' | ||
4 | import 'videojs-dock' | ||
5 | import 'videojs-contextmenu-ui' | ||
6 | import './peertube-link-button' | ||
7 | import './resolution-menu-button' | ||
8 | import './settings-menu-button' | ||
9 | import './webtorrent-info-button' | ||
10 | import './peertube-videojs-plugin' | ||
11 | import './peertube-load-progress-bar' | ||
12 | import './theater-button' | ||
13 | import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' | ||
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
15 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
16 | |||
17 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
18 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
19 | // Change Captions to Subtitles/CC | ||
20 | videojsUntyped.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) | ||
22 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
23 | |||
24 | function 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 | |||
136 | function 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 | |||
200 | function 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 | |||
233 | function 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 | } | ||
254 | namespace loadLocaleInVideoJS { | ||
255 | export const cache: { [ path: string ]: any } = {} | ||
256 | } | ||
257 | |||
258 | function 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 | |||
269 | export { | ||
270 | getServerTranslations, | ||
271 | loadLocaleInVideoJS, | ||
272 | getVideojsOptions, | ||
273 | addContextMenu | ||
274 | } | ||
275 | |||
276 | // ############################################################################ | ||
277 | |||
278 | function 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 | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | ||
5 | import { | ||
6 | PeerTubePluginOptions, | ||
7 | ResolutionUpdateData, | ||
8 | UserWatching, | ||
9 | VideoJSCaption, | ||
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | ||
13 | import { isMobile, timeToInt } from './utils' | ||
14 | import { | ||
15 | getStoredLastSubtitle, | ||
16 | getStoredMute, | ||
17 | getStoredVolume, | ||
18 | saveLastSubtitle, | ||
19 | saveMuteInStore, | ||
20 | saveVolumeInStore | ||
21 | } from './peertube-player-local-storage' | ||
22 | |||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
24 | class 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 | |||
261 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
262 | export { 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 | ||
1 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | |||
2 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
3 | import { PeerTubePlugin } from './peertube-videojs-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | ||
9 | import { PlayerMode } from './peertube-player-manager' | ||
4 | 10 | ||
5 | declare namespace videojs { | 11 | declare 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 | ||
11 | interface VideoJSComponentInterface { | 19 | interface 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 | ||
19 | type VideoJSCaption = { | 27 | type VideoJSCaption = { |
@@ -27,25 +35,95 @@ type UserWatching = { | |||
27 | authorizationHeader: string | 35 | authorizationHeader: string |
28 | } | 36 | } |
29 | 37 | ||
30 | type PeertubePluginOptions = { | 38 | type 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 | |||
52 | type WebtorrentPluginOptions = { | ||
53 | playerElement: HTMLVideoElement | ||
54 | |||
55 | autoplay: boolean | ||
56 | videoDuration: number | ||
57 | |||
58 | videoFiles: VideoFile[] | ||
59 | } | ||
60 | |||
61 | type P2PMediaLoaderPluginOptions = { | ||
62 | redundancyBaseUrls: string[] | ||
63 | type: string | ||
64 | src: string | ||
65 | } | ||
66 | |||
67 | type 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 |
43 | const videojsUntyped = videojs as any | 76 | const videojsUntyped = videojs as any |
44 | 77 | ||
78 | type LoadedQualityData = { | ||
79 | qualitySwitchCallback: Function, | ||
80 | qualityData: { | ||
81 | video: { | ||
82 | id: number | ||
83 | label: string | ||
84 | selected: boolean | ||
85 | }[] | ||
86 | } | ||
87 | } | ||
88 | |||
89 | type ResolutionUpdateData = { | ||
90 | auto: boolean, | ||
91 | resolutionId: number | ||
92 | id?: number | ||
93 | } | ||
94 | |||
95 | type AutoResolutionUpdateData = { | ||
96 | possible: boolean | ||
97 | } | ||
98 | |||
99 | type 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 | |||
45 | export { | 116 | export { |
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 @@ | |||
1 | import * as videojs from 'video.js' | ||
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
3 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
4 | |||
5 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
6 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
7 | class 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 | } | ||
83 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
84 | |||
85 | MenuButton.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 @@ | |||
1 | import * as videojs from 'video.js' | ||
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
3 | |||
4 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
5 | class 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 | } | ||
62 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
63 | |||
64 | export { 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 | ] |
15 | function bytes (value) { | 15 | function 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 | ||
41 | function timeToInt (time: number | string) { | 41 | function 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 | ||
115 | function 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 | ||
116 | export { | 130 | export { |
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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { bytes } from './utils' | 2 | import { bytes } from '../utils' |
3 | 3 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class WebtorrentInfoButton extends Button { | 5 | class 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 | } |
102 | Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) | 105 | Button.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 @@ | |||
1 | import * as videojs from 'video.js' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 2 | import { buildVideoLink } from '../utils' |
3 | import { buildVideoLink } from './utils' | 3 | // FIXME: something weird with our path definition in tsconfig and typings |
4 | // @ts-ignore | ||
5 | import { Player } from 'video.js' | ||
4 | 6 | ||
5 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 7 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
6 | class PeerTubeLinkButton extends Button { | 8 | class 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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
2 | 5 | ||
3 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 6 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
4 | 7 | ||
5 | class PeerTubeLoadProgressBar extends Component { | 8 | class 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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class 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 | } | ||
107 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
108 | |||
109 | MenuButton.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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class 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 | } | ||
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
82 | |||
83 | export { 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 | ||
4 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | |||
5 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
6 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
7 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
8 | 11 | ||
9 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
10 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
11 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 14 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
12 | 15 | ||
13 | class SettingsButton extends Button { | 16 | class 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 | ||
220 | class SettingsPanel extends Component { | 223 | class 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 | ||
234 | class SettingsPanelChild extends Component { | 237 | class 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 | ||
248 | class SettingsDialog extends Component { | 251 | class 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 | ||
4 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
5 | import { toTitleCase } from './utils' | 7 | |
6 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
7 | 10 | ||
8 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
9 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
10 | 13 | ||
11 | class SettingsMenuItem extends MenuItem { | 14 | class 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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | // FIXME: something weird with our path definition in tsconfig and typings |
2 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' | ||
3 | 7 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class TheaterButton extends Button { | 9 | class 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 | ||
4 | import * as MediaElementWrapper from 'mediasource' | 4 | const MediaElementWrapper = require('mediasource') |
5 | import { extname } from 'path' | 5 | import { extname } from 'path' |
6 | import * as videostream from 'videostream' | 6 | const videostream = require('videostream') |
7 | 7 | ||
8 | const VIDEOSTREAM_EXTS = [ | 8 | const VIDEOSTREAM_EXTS = [ |
9 | '.m4a', | 9 | '.m4a', |
@@ -17,7 +17,7 @@ type RenderMediaOptions = { | |||
17 | } | 17 | } |
18 | 18 | ||
19 | function renderVideo ( | 19 | function 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 | ||
30 | function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { | 30 | function 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 | ||
105 | function validateFile (file) { | 105 | function 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 | ||
1 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | |||
2 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
3 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
4 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
5 | import './settings-menu-button' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
6 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
7 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | ||
8 | import * as CacheChunkStore from 'cache-chunk-store' | ||
9 | import { PeertubeChunkStore } from './peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
10 | import { | 11 | import { |
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 | |
19 | const CacheChunkStore = require('cache-chunk-store') | ||
20 | |||
21 | type PlayOptions = { | ||
22 | forcePlay?: boolean, | ||
23 | seek?: number, | ||
24 | delay?: number | ||
25 | } | ||
18 | 26 | ||
19 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
20 | class PeerTubePlugin extends Plugin { | 28 | class 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 | ||
701 | videojs.registerPlugin('peertube', PeerTubePlugin) | 638 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
702 | export { PeerTubePlugin } | 639 | export { WebTorrentPlugin } |