Fix captions in HTTP fallback
[oweals/peertube.git] / client / src / assets / player / peertube-player.ts
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 // FIXME: something weird with our path definition in tsconfig and typings
18 // @ts-ignore
19 import { Player } from 'video.js'
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 function getVideojsOptions (options: {
29   autoplay: boolean
30   playerElement: HTMLVideoElement
31   videoViewUrl: string
32   videoDuration: number
33   videoFiles: VideoFile[]
34   enableHotkeys: boolean
35   inactivityTimeout: number
36   peertubeLink: boolean
37   poster: string
38   startTime: number | string
39   theaterMode: boolean
40   videoCaptions: VideoJSCaption[]
41
42   language?: string
43   controls?: boolean
44   muted?: boolean
45   loop?: boolean
46   subtitle?: string
47
48   userWatching?: UserWatching
49 }) {
50   const videojsOptions = {
51     // We don't use text track settings for now
52     textTrackSettings: false,
53     controls: options.controls !== undefined ? options.controls : true,
54     loop: options.loop !== undefined ? options.loop : false,
55
56     muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage
57
58     poster: options.poster,
59     autoplay: false,
60     inactivityTimeout: options.inactivityTimeout,
61     playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
62     plugins: {
63       peertube: {
64         autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
65         videoCaptions: options.videoCaptions,
66         videoFiles: options.videoFiles,
67         playerElement: options.playerElement,
68         videoViewUrl: options.videoViewUrl,
69         videoDuration: options.videoDuration,
70         startTime: options.startTime,
71         userWatching: options.userWatching,
72         subtitle: options.subtitle
73       }
74     },
75     controlBar: {
76       children: getControlBarChildren(options)
77     }
78   }
79
80   if (options.enableHotkeys === true) {
81     Object.assign(videojsOptions.plugins, {
82       hotkeys: {
83         enableVolumeScroll: false,
84         enableModifiersForNumbers: false,
85
86         fullscreenKey: function (event: KeyboardEvent) {
87           // fullscreen with the f key or Ctrl+Enter
88           return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
89         },
90
91         seekStep: function (event: KeyboardEvent) {
92           // mimic VLC seek behavior, and default to 5 (original value is 5).
93           if (event.ctrlKey && event.altKey) {
94             return 5 * 60
95           } else if (event.ctrlKey) {
96             return 60
97           } else if (event.altKey) {
98             return 10
99           } else {
100             return 5
101           }
102         },
103
104         customKeys: {
105           increasePlaybackRateKey: {
106             key: function (event: KeyboardEvent) {
107               return event.key === '>'
108             },
109             handler: function (player: Player) {
110               player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
111             }
112           },
113           decreasePlaybackRateKey: {
114             key: function (event: KeyboardEvent) {
115               return event.key === '<'
116             },
117             handler: function (player: Player) {
118               player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
119             }
120           },
121           frameByFrame: {
122             key: function (event: KeyboardEvent) {
123               return event.key === '.'
124             },
125             handler: function (player: Player) {
126               player.pause()
127               // Calculate movement distance (assuming 30 fps)
128               const dist = 1 / 30
129               player.currentTime(player.currentTime() + dist)
130             }
131           }
132         }
133       }
134     })
135   }
136
137   if (options.language && !isDefaultLocale(options.language)) {
138     Object.assign(videojsOptions, { language: options.language })
139   }
140
141   return videojsOptions
142 }
143
144 function getControlBarChildren (options: {
145   peertubeLink: boolean
146   theaterMode: boolean,
147   videoCaptions: VideoJSCaption[]
148 }) {
149   const settingEntries = []
150
151   // Keep an order
152   settingEntries.push('playbackRateMenuButton')
153   if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton')
154   settingEntries.push('resolutionMenuButton')
155
156   const children = {
157     'playToggle': {},
158     'currentTimeDisplay': {},
159     'timeDivider': {},
160     'durationDisplay': {},
161     'liveDisplay': {},
162
163     'flexibleWidthSpacer': {},
164     'progressControl': {
165       children: {
166         'seekBar': {
167           children: {
168             'peerTubeLoadProgressBar': {},
169             'mouseTimeDisplay': {},
170             'playProgressBar': {}
171           }
172         }
173       }
174     },
175
176     'webTorrentButton': {},
177
178     'muteToggle': {},
179     'volumeControl': {},
180
181     'settingsButton': {
182       setup: {
183         maxHeightOffset: 40
184       },
185       entries: settingEntries
186     }
187   }
188
189   if (options.peertubeLink === true) {
190     Object.assign(children, {
191       'peerTubeLinkButton': {}
192     })
193   }
194
195   if (options.theaterMode === true) {
196     Object.assign(children, {
197       'theaterButton': {}
198     })
199   }
200
201   Object.assign(children, {
202     'fullscreenToggle': {}
203   })
204
205   return children
206 }
207
208 function addContextMenu (player: any, videoEmbedUrl: string) {
209   player.contextmenuUI({
210     content: [
211       {
212         label: player.localize('Copy the video URL'),
213         listener: function () {
214           copyToClipboard(buildVideoLink())
215         }
216       },
217       {
218         label: player.localize('Copy the video URL at the current time'),
219         listener: function () {
220           const player = this as Player
221           copyToClipboard(buildVideoLink(player.currentTime()))
222         }
223       },
224       {
225         label: player.localize('Copy embed code'),
226         listener: () => {
227           copyToClipboard(buildVideoEmbed(videoEmbedUrl))
228         }
229       },
230       {
231         label: player.localize('Copy magnet URI'),
232         listener: function () {
233           const player = this as Player
234           copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri)
235         }
236       }
237     ]
238   })
239 }
240
241 function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) {
242   const path = getLocalePath(serverUrl, locale)
243   // It is the default locale, nothing to translate
244   if (!path) return Promise.resolve(undefined)
245
246   let p: Promise<any>
247
248   if (loadLocaleInVideoJS.cache[path]) {
249     p = Promise.resolve(loadLocaleInVideoJS.cache[path])
250   } else {
251     p = fetch(path + '/player.json')
252       .then(res => res.json())
253       .then(json => {
254         loadLocaleInVideoJS.cache[path] = json
255         return json
256       })
257       .catch(err => {
258         console.error('Cannot get player translations', err)
259         return undefined
260       })
261   }
262
263   const completeLocale = getCompleteLocale(locale)
264   return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
265 }
266 namespace loadLocaleInVideoJS {
267   export const cache: { [ path: string ]: any } = {}
268 }
269
270 function getServerTranslations (serverUrl: string, locale: string) {
271   const path = getLocalePath(serverUrl, locale)
272   // It is the default locale, nothing to translate
273   if (!path) return Promise.resolve(undefined)
274
275   return fetch(path + '/server.json')
276     .then(res => res.json())
277     .catch(err => {
278       console.error('Cannot get server translations', err)
279       return undefined
280     })
281 }
282
283 // ############################################################################
284
285 export {
286   getServerTranslations,
287   loadLocaleInVideoJS,
288   getVideojsOptions,
289   addContextMenu
290 }
291
292 // ############################################################################
293
294 function getLocalePath (serverUrl: string, locale: string) {
295   const completeLocale = getCompleteLocale(locale)
296
297   if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
298
299   return serverUrl + '/client/locales/' + completeLocale
300 }