diff options
Diffstat (limited to 'client/src/standalone/videos')
-rw-r--r-- | client/src/standalone/videos/embed-api.ts | 130 | ||||
-rw-r--r-- | client/src/standalone/videos/embed.html | 2 | ||||
-rw-r--r-- | client/src/standalone/videos/embed.ts | 205 |
3 files changed, 188 insertions, 149 deletions
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts new file mode 100644 index 000000000..169e371da --- /dev/null +++ b/client/src/standalone/videos/embed-api.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | import './embed.scss' | ||
2 | |||
3 | import * as Channel from 'jschannel' | ||
4 | import { PeerTubeResolution } from '../player/definitions' | ||
5 | import { PeerTubeEmbed } from './embed' | ||
6 | |||
7 | /** | ||
8 | * Embed API exposes control of the embed player to the outside world via | ||
9 | * JSChannels and window.postMessage | ||
10 | */ | ||
11 | export class PeerTubeEmbedApi { | ||
12 | private channel: Channel.MessagingChannel | ||
13 | private isReady = false | ||
14 | private resolutions: PeerTubeResolution[] = null | ||
15 | |||
16 | constructor (private embed: PeerTubeEmbed) { | ||
17 | } | ||
18 | |||
19 | initialize () { | ||
20 | this.constructChannel() | ||
21 | this.setupStateTracking() | ||
22 | |||
23 | // We're ready! | ||
24 | |||
25 | this.notifyReady() | ||
26 | } | ||
27 | |||
28 | private get element () { | ||
29 | return this.embed.videoElement | ||
30 | } | ||
31 | |||
32 | private constructChannel () { | ||
33 | const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) | ||
34 | |||
35 | channel.bind('play', (txn, params) => this.embed.player.play()) | ||
36 | channel.bind('pause', (txn, params) => this.embed.player.pause()) | ||
37 | channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) | ||
38 | channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) | ||
39 | channel.bind('getVolume', (txn, value) => this.embed.player.volume()) | ||
40 | channel.bind('isReady', (txn, params) => this.isReady) | ||
41 | channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) | ||
42 | channel.bind('getResolutions', (txn, params) => this.resolutions) | ||
43 | channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) | ||
44 | channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) | ||
45 | channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) | ||
46 | |||
47 | this.channel = channel | ||
48 | } | ||
49 | |||
50 | private setResolution (resolutionId: number) { | ||
51 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return | ||
52 | |||
53 | // Auto resolution | ||
54 | if (resolutionId === -1) { | ||
55 | this.embed.player.webtorrent().enableAutoResolution() | ||
56 | return | ||
57 | } | ||
58 | |||
59 | this.embed.player.webtorrent().disableAutoResolution() | ||
60 | this.embed.player.webtorrent().updateResolution(resolutionId) | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * Let the host know that we're ready to go! | ||
65 | */ | ||
66 | private notifyReady () { | ||
67 | this.isReady = true | ||
68 | this.channel.notify({ method: 'ready', params: true }) | ||
69 | } | ||
70 | |||
71 | private setupStateTracking () { | ||
72 | let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' | ||
73 | |||
74 | setInterval(() => { | ||
75 | const position = this.element.currentTime | ||
76 | const volume = this.element.volume | ||
77 | |||
78 | this.channel.notify({ | ||
79 | method: 'playbackStatusUpdate', | ||
80 | params: { | ||
81 | position, | ||
82 | volume, | ||
83 | playbackState: currentState | ||
84 | } | ||
85 | }) | ||
86 | }, 500) | ||
87 | |||
88 | this.element.addEventListener('play', ev => { | ||
89 | currentState = 'playing' | ||
90 | this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) | ||
91 | }) | ||
92 | |||
93 | this.element.addEventListener('pause', ev => { | ||
94 | currentState = 'paused' | ||
95 | this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) | ||
96 | }) | ||
97 | |||
98 | // PeerTube specific capabilities | ||
99 | |||
100 | if (this.embed.player.webtorrent) { | ||
101 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) | ||
102 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private loadWebTorrentResolutions () { | ||
107 | const resolutions = [] | ||
108 | const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() | ||
109 | |||
110 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { | ||
111 | let label = videoFile.resolution.label | ||
112 | if (videoFile.fps && videoFile.fps >= 50) { | ||
113 | label += videoFile.fps | ||
114 | } | ||
115 | |||
116 | resolutions.push({ | ||
117 | id: videoFile.resolution.id, | ||
118 | label, | ||
119 | src: videoFile.magnetUri, | ||
120 | active: videoFile.resolution.id === currentResolutionId | ||
121 | }) | ||
122 | } | ||
123 | |||
124 | this.resolutions = resolutions | ||
125 | this.channel.notify({ | ||
126 | method: 'resolutionUpdate', | ||
127 | params: this.resolutions | ||
128 | }) | ||
129 | } | ||
130 | } | ||
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index c3b6e08ca..5a15bf552 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -11,7 +11,7 @@ | |||
11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
12 | </head> | 12 | </head> |
13 | 13 | ||
14 | <body> | 14 | <body id="custom-css"> |
15 | 15 | ||
16 | <div id="error-block"> | 16 | <div id="error-block"> |
17 | <h1 id="error-title"></h1> | 17 | <h1 id="error-title"></h1> |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 707f04253..cfe8e94b1 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -1,9 +1,6 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | 2 | ||
3 | import * as Channel from 'jschannel' | ||
4 | |||
5 | import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' | 3 | import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' |
6 | import { PeerTubeResolution } from '../player/definitions' | ||
7 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' | 4 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' |
8 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 5 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
9 | import { | 6 | import { |
@@ -13,133 +10,9 @@ import { | |||
13 | PlayerMode | 10 | PlayerMode |
14 | } from '../../assets/player/peertube-player-manager' | 11 | } from '../../assets/player/peertube-player-manager' |
15 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 12 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
13 | import { PeerTubeEmbedApi } from './embed-api' | ||
16 | 14 | ||
17 | /** | 15 | export class PeerTubeEmbed { |
18 | * Embed API exposes control of the embed player to the outside world via | ||
19 | * JSChannels and window.postMessage | ||
20 | */ | ||
21 | class PeerTubeEmbedApi { | ||
22 | private channel: Channel.MessagingChannel | ||
23 | private isReady = false | ||
24 | private resolutions: PeerTubeResolution[] = null | ||
25 | |||
26 | constructor (private embed: PeerTubeEmbed) { | ||
27 | } | ||
28 | |||
29 | initialize () { | ||
30 | this.constructChannel() | ||
31 | this.setupStateTracking() | ||
32 | |||
33 | // We're ready! | ||
34 | |||
35 | this.notifyReady() | ||
36 | } | ||
37 | |||
38 | private get element () { | ||
39 | return this.embed.videoElement | ||
40 | } | ||
41 | |||
42 | private constructChannel () { | ||
43 | const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) | ||
44 | |||
45 | channel.bind('play', (txn, params) => this.embed.player.play()) | ||
46 | channel.bind('pause', (txn, params) => this.embed.player.pause()) | ||
47 | channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) | ||
48 | channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) | ||
49 | channel.bind('getVolume', (txn, value) => this.embed.player.volume()) | ||
50 | channel.bind('isReady', (txn, params) => this.isReady) | ||
51 | channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) | ||
52 | channel.bind('getResolutions', (txn, params) => this.resolutions) | ||
53 | channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) | ||
54 | channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) | ||
55 | channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) | ||
56 | |||
57 | this.channel = channel | ||
58 | } | ||
59 | |||
60 | private setResolution (resolutionId: number) { | ||
61 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return | ||
62 | |||
63 | // Auto resolution | ||
64 | if (resolutionId === -1) { | ||
65 | this.embed.player.webtorrent().enableAutoResolution() | ||
66 | return | ||
67 | } | ||
68 | |||
69 | this.embed.player.webtorrent().disableAutoResolution() | ||
70 | this.embed.player.webtorrent().updateResolution(resolutionId) | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Let the host know that we're ready to go! | ||
75 | */ | ||
76 | private notifyReady () { | ||
77 | this.isReady = true | ||
78 | this.channel.notify({ method: 'ready', params: true }) | ||
79 | } | ||
80 | |||
81 | private setupStateTracking () { | ||
82 | let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' | ||
83 | |||
84 | setInterval(() => { | ||
85 | const position = this.element.currentTime | ||
86 | const volume = this.element.volume | ||
87 | |||
88 | this.channel.notify({ | ||
89 | method: 'playbackStatusUpdate', | ||
90 | params: { | ||
91 | position, | ||
92 | volume, | ||
93 | playbackState: currentState | ||
94 | } | ||
95 | }) | ||
96 | }, 500) | ||
97 | |||
98 | this.element.addEventListener('play', ev => { | ||
99 | currentState = 'playing' | ||
100 | this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) | ||
101 | }) | ||
102 | |||
103 | this.element.addEventListener('pause', ev => { | ||
104 | currentState = 'paused' | ||
105 | this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) | ||
106 | }) | ||
107 | |||
108 | // PeerTube specific capabilities | ||
109 | |||
110 | if (this.embed.player.webtorrent) { | ||
111 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) | ||
112 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | private loadWebTorrentResolutions () { | ||
117 | const resolutions = [] | ||
118 | const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() | ||
119 | |||
120 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { | ||
121 | let label = videoFile.resolution.label | ||
122 | if (videoFile.fps && videoFile.fps >= 50) { | ||
123 | label += videoFile.fps | ||
124 | } | ||
125 | |||
126 | resolutions.push({ | ||
127 | id: videoFile.resolution.id, | ||
128 | label, | ||
129 | src: videoFile.magnetUri, | ||
130 | active: videoFile.resolution.id === currentResolutionId | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | this.resolutions = resolutions | ||
135 | this.channel.notify({ | ||
136 | method: 'resolutionUpdate', | ||
137 | params: this.resolutions | ||
138 | }) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | class PeerTubeEmbed { | ||
143 | videoElement: HTMLVideoElement | 16 | videoElement: HTMLVideoElement |
144 | player: any | 17 | player: any |
145 | playerOptions: any | 18 | playerOptions: any |
@@ -152,6 +25,12 @@ class PeerTubeEmbed { | |||
152 | enableApi = false | 25 | enableApi = false |
153 | startTime: number | string = 0 | 26 | startTime: number | string = 0 |
154 | stopTime: number | string | 27 | stopTime: number | string |
28 | |||
29 | title: boolean | ||
30 | warningTitle: boolean | ||
31 | bigPlayBackgroundColor: string | ||
32 | foregroundColor: string | ||
33 | |||
155 | mode: PlayerMode | 34 | mode: PlayerMode |
156 | scope = 'peertube' | 35 | scope = 'peertube' |
157 | 36 | ||
@@ -245,13 +124,18 @@ class PeerTubeEmbed { | |||
245 | this.controls = this.getParamToggle(params, 'controls', true) | 124 | this.controls = this.getParamToggle(params, 'controls', true) |
246 | this.muted = this.getParamToggle(params, 'muted', false) | 125 | this.muted = this.getParamToggle(params, 'muted', false) |
247 | this.loop = this.getParamToggle(params, 'loop', false) | 126 | this.loop = this.getParamToggle(params, 'loop', false) |
127 | this.title = this.getParamToggle(params, 'title', true) | ||
248 | this.enableApi = this.getParamToggle(params, 'api', this.enableApi) | 128 | this.enableApi = this.getParamToggle(params, 'api', this.enableApi) |
129 | this.warningTitle = this.getParamToggle(params, 'warningTitle', true) | ||
249 | 130 | ||
250 | this.scope = this.getParamString(params, 'scope', this.scope) | 131 | this.scope = this.getParamString(params, 'scope', this.scope) |
251 | this.subtitle = this.getParamString(params, 'subtitle') | 132 | this.subtitle = this.getParamString(params, 'subtitle') |
252 | this.startTime = this.getParamString(params, 'start') | 133 | this.startTime = this.getParamString(params, 'start') |
253 | this.stopTime = this.getParamString(params, 'stop') | 134 | this.stopTime = this.getParamString(params, 'stop') |
254 | 135 | ||
136 | this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor') | ||
137 | this.foregroundColor = this.getParamString(params, 'foregroundColor') | ||
138 | |||
255 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' | 139 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' |
256 | } catch (err) { | 140 | } catch (err) { |
257 | console.error('Cannot get params from URL.', err) | 141 | console.error('Cannot get params from URL.', err) |
@@ -276,15 +160,7 @@ class PeerTubeEmbed { | |||
276 | } | 160 | } |
277 | 161 | ||
278 | const videoInfo: VideoDetails = await videoResponse.json() | 162 | const videoInfo: VideoDetails = await videoResponse.json() |
279 | let videoCaptions: VideoJSCaption[] = [] | 163 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) |
280 | if (captionsResponse.ok) { | ||
281 | const { data } = (await captionsResponse.json()) as ResultList<VideoCaption> | ||
282 | videoCaptions = data.map(c => ({ | ||
283 | label: peertubeTranslate(c.language.label, serverTranslations), | ||
284 | language: c.language.id, | ||
285 | src: window.location.origin + c.captionPath | ||
286 | })) | ||
287 | } | ||
288 | 164 | ||
289 | this.loadParams() | 165 | this.loadParams() |
290 | 166 | ||
@@ -337,33 +213,66 @@ class PeerTubeEmbed { | |||
337 | } | 213 | } |
338 | 214 | ||
339 | this.player = await PeertubePlayerManager.initialize(this.mode, options) | 215 | this.player = await PeertubePlayerManager.initialize(this.mode, options) |
340 | |||
341 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) | 216 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) |
342 | 217 | ||
343 | window[ 'videojsPlayer' ] = this.player | 218 | window[ 'videojsPlayer' ] = this.player |
344 | 219 | ||
220 | this.buildCSS() | ||
221 | |||
222 | await this.buildDock(videoInfo, configResponse) | ||
223 | |||
224 | this.initializeApi() | ||
225 | } | ||
226 | |||
227 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | ||
228 | if (err.message.indexOf('from xs param') !== -1) { | ||
229 | this.player.dispose() | ||
230 | this.videoElement = null | ||
231 | this.displayError('This video is not available because the remote instance is not responding.', translations) | ||
232 | return | ||
233 | } | ||
234 | } | ||
235 | |||
236 | private async buildDock (videoInfo: VideoDetails, configResponse: Response) { | ||
345 | if (this.controls) { | 237 | if (this.controls) { |
238 | const title = this.title ? videoInfo.name : undefined | ||
239 | |||
346 | const config: ServerConfig = await configResponse.json() | 240 | const config: ServerConfig = await configResponse.json() |
347 | const description = config.tracker.enabled | 241 | const description = config.tracker.enabled && this.warningTitle |
348 | ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' | 242 | ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' |
349 | : undefined | 243 | : undefined |
350 | 244 | ||
351 | this.player.dock({ | 245 | this.player.dock({ |
352 | title: videoInfo.name, | 246 | title, |
353 | description | 247 | description |
354 | }) | 248 | }) |
355 | } | 249 | } |
250 | } | ||
356 | 251 | ||
357 | this.initializeApi() | 252 | private buildCSS () { |
253 | const body = document.getElementById('custom-css') | ||
254 | |||
255 | if (this.bigPlayBackgroundColor) { | ||
256 | body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor) | ||
257 | } | ||
258 | |||
259 | if (this.foregroundColor) { | ||
260 | body.style.setProperty('--embedForegroundColor', this.foregroundColor) | ||
261 | } | ||
358 | } | 262 | } |
359 | 263 | ||
360 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | 264 | private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> { |
361 | if (err.message.indexOf('from xs param') !== -1) { | 265 | if (captionsResponse.ok) { |
362 | this.player.dispose() | 266 | const { data } = (await captionsResponse.json()) as ResultList<VideoCaption> |
363 | this.videoElement = null | 267 | |
364 | this.displayError('This video is not available because the remote instance is not responding.', translations) | 268 | return data.map(c => ({ |
365 | return | 269 | label: peertubeTranslate(c.language.label, serverTranslations), |
270 | language: c.language.id, | ||
271 | src: window.location.origin + c.captionPath | ||
272 | })) | ||
366 | } | 273 | } |
274 | |||
275 | return [] | ||
367 | } | 276 | } |
368 | } | 277 | } |
369 | 278 | ||