diff options
author | William Lahti <wilahti@gmail.com> | 2018-07-10 08:47:56 -0700 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-07-10 17:47:56 +0200 |
commit | 999417328bde0e60cd59318fc1c18672356254ce (patch) | |
tree | 22673fcbd4dc333e3362912b2c813e97a41c765f /client/src/standalone/videos/embed.ts | |
parent | 0b755f3b27190ea4d9c301ede0955b2736605f4c (diff) | |
download | PeerTube-999417328bde0e60cd59318fc1c18672356254ce.tar.gz PeerTube-999417328bde0e60cd59318fc1c18672356254ce.tar.zst PeerTube-999417328bde0e60cd59318fc1c18672356254ce.zip |
Ability to programmatically control embeds (#776)
* first stab at jschannel based player api
* semicolon purge
* more method-level docs; consolidate definitions
* missing definitions
* better match peertube's class conventions
* styling for embed tester
* basic docs
* add `getVolume`
* document the test-embed feature
Diffstat (limited to 'client/src/standalone/videos/embed.ts')
-rw-r--r-- | client/src/standalone/videos/embed.ts | 306 |
1 files changed, 251 insertions, 55 deletions
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index d8db2a119..e9baf64d0 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -17,100 +17,296 @@ import 'core-js/es6/set' | |||
17 | // For google bot that uses Chrome 41 and does not understand fetch | 17 | // For google bot that uses Chrome 41 and does not understand fetch |
18 | import 'whatwg-fetch' | 18 | import 'whatwg-fetch' |
19 | 19 | ||
20 | import * as videojs from 'video.js' | 20 | import * as vjs from 'video.js' |
21 | import * as Channel from 'jschannel' | ||
21 | 22 | ||
22 | import { VideoDetails } from '../../../../shared' | 23 | import { VideoDetails } from '../../../../shared' |
23 | import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player' | 24 | import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player' |
25 | import { PeerTubeResolution } from '../player/definitions'; | ||
24 | 26 | ||
25 | function getVideoUrl (id: string) { | 27 | /** |
26 | return window.location.origin + '/api/v1/videos/' + id | 28 | * Embed API exposes control of the embed player to the outside world via |
27 | } | 29 | * JSChannels and window.postMessage |
30 | */ | ||
31 | class PeerTubeEmbedApi { | ||
32 | constructor( | ||
33 | private embed : PeerTubeEmbed | ||
34 | ) { | ||
35 | } | ||
28 | 36 | ||
29 | function loadVideoInfo (videoId: string): Promise<Response> { | 37 | private channel : Channel.MessagingChannel |
30 | return fetch(getVideoUrl(videoId)) | 38 | private isReady = false |
31 | } | 39 | private resolutions : PeerTubeResolution[] = null |
32 | 40 | ||
33 | function removeElement (element: HTMLElement) { | 41 | initialize() { |
34 | element.parentElement.removeChild(element) | 42 | this.constructChannel() |
35 | } | 43 | this.setupStateTracking() |
36 | 44 | ||
37 | function displayError (videoElement: HTMLVideoElement, text: string) { | 45 | // We're ready! |
38 | // Remove video element | ||
39 | removeElement(videoElement) | ||
40 | 46 | ||
41 | document.title = 'Sorry - ' + text | 47 | this.notifyReady() |
48 | } | ||
49 | |||
50 | private get element() { | ||
51 | return this.embed.videoElement | ||
52 | } | ||
42 | 53 | ||
43 | const errorBlock = document.getElementById('error-block') | 54 | private constructChannel() { |
44 | errorBlock.style.display = 'flex' | 55 | let channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) |
56 | |||
57 | channel.bind('play', (txn, params) => this.embed.player.play()) | ||
58 | channel.bind('pause', (txn, params) => this.embed.player.pause()) | ||
59 | channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) | ||
60 | channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) | ||
61 | channel.bind('getVolume', (txn, value) => this.embed.player.volume()) | ||
62 | channel.bind('isReady', (txn, params) => this.isReady) | ||
63 | channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) | ||
64 | channel.bind('getResolutions', (txn, params) => this.resolutions) | ||
65 | channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) | ||
66 | channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) | ||
67 | channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) | ||
45 | 68 | ||
46 | const errorText = document.getElementById('error-content') | 69 | this.channel = channel |
47 | errorText.innerHTML = text | 70 | } |
48 | } | ||
49 | 71 | ||
50 | function videoNotFound (videoElement: HTMLVideoElement) { | 72 | private setResolution(resolutionId : number) { |
51 | const text = 'This video does not exist.' | 73 | if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) |
52 | displayError(videoElement, text) | 74 | return |
53 | } | 75 | |
76 | // Auto resolution | ||
77 | if (resolutionId === -1) { | ||
78 | this.embed.player.peertube().enableAutoResolution() | ||
79 | return | ||
80 | } | ||
81 | |||
82 | this.embed.player.peertube().disableAutoResolution() | ||
83 | this.embed.player.peertube().updateResolution(resolutionId) | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Let the host know that we're ready to go! | ||
88 | */ | ||
89 | private notifyReady() { | ||
90 | this.isReady = true | ||
91 | this.channel.notify({ method: 'ready', params: true }) | ||
92 | } | ||
93 | |||
94 | private setupStateTracking() { | ||
95 | |||
96 | let currentState : 'playing' | 'paused' | 'unstarted' = 'unstarted' | ||
97 | |||
98 | setInterval(() => { | ||
99 | let position = this.element.currentTime | ||
100 | let volume = this.element.volume | ||
101 | |||
102 | this.channel.notify({ | ||
103 | method: 'playbackStatusUpdate', | ||
104 | params: { | ||
105 | position, | ||
106 | volume, | ||
107 | playbackState: currentState, | ||
108 | } | ||
109 | }) | ||
110 | }, 500) | ||
111 | |||
112 | this.element.addEventListener('play', ev => { | ||
113 | currentState = 'playing' | ||
114 | this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) | ||
115 | }) | ||
116 | |||
117 | this.element.addEventListener('pause', ev => { | ||
118 | currentState = 'paused' | ||
119 | this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) | ||
120 | }) | ||
121 | |||
122 | // PeerTube specific capabilities | ||
123 | |||
124 | this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions()) | ||
125 | this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions()) | ||
126 | } | ||
127 | |||
128 | private loadResolutions() { | ||
129 | let resolutions = [] | ||
130 | let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId() | ||
131 | |||
132 | for (const videoFile of this.embed.player.peertube().videoFiles) { | ||
133 | let label = videoFile.resolution.label | ||
134 | if (videoFile.fps && videoFile.fps >= 50) { | ||
135 | label += videoFile.fps | ||
136 | } | ||
54 | 137 | ||
55 | function videoFetchError (videoElement: HTMLVideoElement) { | 138 | resolutions.push({ |
56 | const text = 'We cannot fetch the video. Please try again later.' | 139 | id: videoFile.resolution.id, |
57 | displayError(videoElement, text) | 140 | label, |
141 | src: videoFile.magnetUri, | ||
142 | active: videoFile.resolution.id === currentResolutionId | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | this.resolutions = resolutions | ||
147 | this.channel.notify({ | ||
148 | method: 'resolutionUpdate', | ||
149 | params: this.resolutions | ||
150 | }) | ||
151 | } | ||
58 | } | 152 | } |
59 | 153 | ||
60 | const urlParts = window.location.href.split('/') | 154 | class PeerTubeEmbed { |
61 | const lastPart = urlParts[urlParts.length - 1] | 155 | constructor( |
62 | const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0] | 156 | private videoContainerId : string |
157 | ) { | ||
158 | this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement | ||
159 | } | ||
63 | 160 | ||
64 | loadLocale(window.location.origin, videojs, navigator.language) | 161 | videoElement : HTMLVideoElement |
65 | .then(() => loadVideoInfo(videoId)) | 162 | player : any |
66 | .then(async response => { | 163 | playerOptions : any |
164 | api : PeerTubeEmbedApi = null | ||
165 | autoplay : boolean = false | ||
166 | controls : boolean = true | ||
167 | muted : boolean = false | ||
168 | loop : boolean = false | ||
169 | enableApi : boolean = false | ||
170 | startTime : number = 0 | ||
171 | scope : string = 'peertube' | ||
172 | |||
173 | static async main() { | ||
67 | const videoContainerId = 'video-container' | 174 | const videoContainerId = 'video-container' |
68 | const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement | 175 | const embed = new PeerTubeEmbed(videoContainerId) |
176 | await embed.init() | ||
177 | } | ||
178 | |||
179 | getVideoUrl (id: string) { | ||
180 | return window.location.origin + '/api/v1/videos/' + id | ||
181 | } | ||
69 | 182 | ||
70 | if (!response.ok) { | 183 | loadVideoInfo (videoId: string): Promise<Response> { |
71 | if (response.status === 404) return videoNotFound(videoElement) | 184 | return fetch(this.getVideoUrl(videoId)) |
185 | } | ||
72 | 186 | ||
73 | return videoFetchError(videoElement) | 187 | removeElement (element: HTMLElement) { |
74 | } | 188 | element.parentElement.removeChild(element) |
189 | } | ||
75 | 190 | ||
76 | const videoInfo: VideoDetails = await response.json() | 191 | displayError (videoElement: HTMLVideoElement, text: string) { |
192 | // Remove video element | ||
193 | this.removeElement(videoElement) | ||
194 | |||
195 | document.title = 'Sorry - ' + text | ||
196 | |||
197 | const errorBlock = document.getElementById('error-block') | ||
198 | errorBlock.style.display = 'flex' | ||
199 | |||
200 | const errorText = document.getElementById('error-content') | ||
201 | errorText.innerHTML = text | ||
202 | } | ||
203 | |||
204 | videoNotFound (videoElement: HTMLVideoElement) { | ||
205 | const text = 'This video does not exist.' | ||
206 | this.displayError(videoElement, text) | ||
207 | } | ||
208 | |||
209 | videoFetchError (videoElement: HTMLVideoElement) { | ||
210 | const text = 'We cannot fetch the video. Please try again later.' | ||
211 | this.displayError(videoElement, text) | ||
212 | } | ||
213 | |||
214 | getParamToggle (params: URLSearchParams, name: string, defaultValue: boolean) { | ||
215 | return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue | ||
216 | } | ||
77 | 217 | ||
78 | let autoplay = false | 218 | getParamString (params: URLSearchParams, name: string, defaultValue: string) { |
79 | let startTime = 0 | 219 | return params.has(name) ? params.get(name) : defaultValue |
220 | } | ||
80 | 221 | ||
222 | private initializeApi() { | ||
223 | if (!this.enableApi) | ||
224 | return | ||
225 | |||
226 | this.api = new PeerTubeEmbedApi(this) | ||
227 | this.api.initialize() | ||
228 | } | ||
229 | |||
230 | async init() { | ||
231 | try { | ||
232 | await this.initCore() | ||
233 | } catch (e) { | ||
234 | console.error(e) | ||
235 | } | ||
236 | } | ||
237 | |||
238 | private loadParams() { | ||
81 | try { | 239 | try { |
82 | let params = new URL(window.location.toString()).searchParams | 240 | let params = new URL(window.location.toString()).searchParams |
83 | autoplay = params.has('autoplay') && (params.get('autoplay') === '1' || params.get('autoplay') === 'true') | 241 | |
242 | this.autoplay = this.getParamToggle(params, 'autoplay', this.autoplay) | ||
243 | this.controls = this.getParamToggle(params, 'controls', this.controls) | ||
244 | this.muted = this.getParamToggle(params, 'muted', this.muted) | ||
245 | this.loop = this.getParamToggle(params, 'loop', this.loop) | ||
246 | this.enableApi = this.getParamToggle(params, 'api', this.enableApi) | ||
247 | this.scope = this.getParamString(params, 'scope', this.scope) | ||
84 | 248 | ||
85 | const startTimeParamString = params.get('start') | 249 | const startTimeParamString = params.get('start') |
86 | const startTimeParamNumber = parseInt(startTimeParamString, 10) | 250 | const startTimeParamNumber = parseInt(startTimeParamString, 10) |
87 | if (isNaN(startTimeParamNumber) === false) startTime = startTimeParamNumber | 251 | if (isNaN(startTimeParamNumber) === false) |
252 | this.startTime = startTimeParamNumber | ||
88 | } catch (err) { | 253 | } catch (err) { |
89 | console.error('Cannot get params from URL.', err) | 254 | console.error('Cannot get params from URL.', err) |
90 | } | 255 | } |
256 | } | ||
257 | |||
258 | private async initCore() { | ||
259 | const urlParts = window.location.href.split('/') | ||
260 | const lastPart = urlParts[urlParts.length - 1] | ||
261 | const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0] | ||
262 | |||
263 | await loadLocale(window.location.origin, vjs, navigator.language) | ||
264 | let response = await this.loadVideoInfo(videoId) | ||
265 | |||
266 | if (!response.ok) { | ||
267 | if (response.status === 404) | ||
268 | return this.videoNotFound(this.videoElement) | ||
269 | |||
270 | return this.videoFetchError(this.videoElement) | ||
271 | } | ||
272 | |||
273 | const videoInfo: VideoDetails = await response.json() | ||
274 | |||
275 | this.loadParams() | ||
91 | 276 | ||
92 | const videojsOptions = getVideojsOptions({ | 277 | const videojsOptions = getVideojsOptions({ |
93 | autoplay, | 278 | autoplay: this.autoplay, |
279 | controls: this.controls, | ||
280 | muted: this.muted, | ||
281 | loop: this.loop, | ||
282 | startTime : this.startTime, | ||
283 | |||
94 | inactivityTimeout: 1500, | 284 | inactivityTimeout: 1500, |
95 | videoViewUrl: getVideoUrl(videoId) + '/views', | 285 | videoViewUrl: this.getVideoUrl(videoId) + '/views', |
96 | playerElement: videoElement, | 286 | playerElement: this.videoElement, |
97 | videoFiles: videoInfo.files, | 287 | videoFiles: videoInfo.files, |
98 | videoDuration: videoInfo.duration, | 288 | videoDuration: videoInfo.duration, |
99 | enableHotkeys: true, | 289 | enableHotkeys: true, |
100 | peertubeLink: true, | 290 | peertubeLink: true, |
101 | poster: window.location.origin + videoInfo.previewPath, | 291 | poster: window.location.origin + videoInfo.previewPath, |
102 | startTime, | ||
103 | theaterMode: false | 292 | theaterMode: false |
104 | }) | 293 | }) |
105 | videojs(videoContainerId, videojsOptions, function () { | ||
106 | const player = this | ||
107 | 294 | ||
108 | player.dock({ | 295 | this.playerOptions = videojsOptions |
109 | title: videoInfo.name, | 296 | this.player = vjs(this.videoContainerId, videojsOptions, () => { |
110 | description: player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
111 | }) | ||
112 | 297 | ||
113 | addContextMenu(player, window.location.origin + videoInfo.embedPath) | 298 | window['videojsPlayer'] = this.player |
299 | |||
300 | if (this.controls) { | ||
301 | (this.player as any).dock({ | ||
302 | title: videoInfo.name, | ||
303 | description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
304 | }) | ||
305 | } | ||
306 | addContextMenu(this.player, window.location.origin + videoInfo.embedPath) | ||
307 | this.initializeApi() | ||
114 | }) | 308 | }) |
115 | }) | 309 | } |
116 | .catch(err => console.error(err)) | 310 | } |
311 | |||
312 | PeerTubeEmbed.main() | ||