diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/src/app/videos/+video-watch/video-watch.component.ts | 24 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 21 | ||||
-rw-r--r-- | client/src/sass/include/_variables.scss | 4 | ||||
-rw-r--r-- | client/src/sass/player/_player-variables.scss | 8 | ||||
-rw-r--r-- | client/src/sass/player/context-menu.scss | 4 | ||||
-rw-r--r-- | client/src/sass/player/peertube-skin.scss | 12 | ||||
-rw-r--r-- | client/src/sass/player/settings-menu.scss | 2 | ||||
-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 |
10 files changed, 237 insertions, 175 deletions
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 2d13f1b58..cf9dc8f9c 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -20,6 +20,7 @@ import { environment } from '../../../environments/environment' | |||
20 | import { VideoCaptionService } from '@app/shared/video-caption' | 20 | import { VideoCaptionService } from '@app/shared/video-caption' |
21 | import { MarkdownService } from '@app/shared/renderer' | 21 | import { MarkdownService } from '@app/shared/renderer' |
22 | import { | 22 | import { |
23 | CustomizationOptions, | ||
23 | P2PMediaLoaderOptions, | 24 | P2PMediaLoaderOptions, |
24 | PeertubePlayerManager, | 25 | PeertubePlayerManager, |
25 | PeertubePlayerManagerOptions, | 26 | PeertubePlayerManagerOptions, |
@@ -28,7 +29,7 @@ import { | |||
28 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 29 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
29 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 30 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
30 | import { Video } from '@app/shared/video/video.model' | 31 | import { Video } from '@app/shared/video/video.model' |
31 | import { isWebRTCDisabled } from '../../../assets/player/utils' | 32 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' |
32 | import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' | 33 | import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' |
33 | 34 | ||
34 | @Component({ | 35 | @Component({ |
@@ -249,8 +250,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
249 | const urlOptions = { | 250 | const urlOptions = { |
250 | startTime: queryParams.start, | 251 | startTime: queryParams.start, |
251 | stopTime: queryParams.stop, | 252 | stopTime: queryParams.stop, |
253 | |||
254 | muted: queryParams.muted, | ||
255 | loop: queryParams.loop, | ||
252 | subtitle: queryParams.subtitle, | 256 | subtitle: queryParams.subtitle, |
253 | playerMode: queryParams.mode | 257 | |
258 | playerMode: queryParams.mode, | ||
259 | peertubeLink: false | ||
254 | } | 260 | } |
255 | 261 | ||
256 | this.onVideoFetched(video, captionsResult.data, urlOptions) | 262 | this.onVideoFetched(video, captionsResult.data, urlOptions) |
@@ -327,7 +333,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
327 | private async onVideoFetched ( | 333 | private async onVideoFetched ( |
328 | video: VideoDetails, | 334 | video: VideoDetails, |
329 | videoCaptions: VideoCaption[], | 335 | videoCaptions: VideoCaption[], |
330 | urlOptions: { startTime?: number, stopTime?: number, subtitle?: string, playerMode?: string } | 336 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } |
331 | ) { | 337 | ) { |
332 | this.video = video | 338 | this.video = video |
333 | 339 | ||
@@ -339,7 +345,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
339 | 345 | ||
340 | this.videoWatchPlaylist.updatePlaylistIndex(video) | 346 | this.videoWatchPlaylist.updatePlaylistIndex(video) |
341 | 347 | ||
342 | let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) | 348 | let startTime = timeToInt(urlOptions.startTime) || (this.video.userHistory ? this.video.userHistory.currentTime : 0) |
343 | // If we are at the end of the video, reset the timer | 349 | // If we are at the end of the video, reset the timer |
344 | if (this.video.duration - startTime <= 1) startTime = 0 | 350 | if (this.video.duration - startTime <= 1) startTime = 0 |
345 | 351 | ||
@@ -378,12 +384,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
378 | enableHotkeys: true, | 384 | enableHotkeys: true, |
379 | inactivityTimeout: 2500, | 385 | inactivityTimeout: 2500, |
380 | poster: this.video.previewUrl, | 386 | poster: this.video.previewUrl, |
387 | |||
381 | startTime, | 388 | startTime, |
382 | stopTime: urlOptions.stopTime, | 389 | stopTime: urlOptions.stopTime, |
390 | controls: urlOptions.controls, | ||
391 | muted: urlOptions.muted, | ||
392 | loop: urlOptions.loop, | ||
393 | subtitle: urlOptions.subtitle, | ||
394 | |||
395 | peertubeLink: urlOptions.peertubeLink, | ||
383 | 396 | ||
384 | theaterMode: true, | 397 | theaterMode: true, |
385 | captions: videoCaptions.length !== 0, | 398 | captions: videoCaptions.length !== 0, |
386 | peertubeLink: false, | ||
387 | 399 | ||
388 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE | 400 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE |
389 | ? this.videoService.getVideoViewUrl(this.video.uuid) | 401 | ? this.videoService.getVideoViewUrl(this.video.uuid) |
@@ -392,8 +404,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
392 | 404 | ||
393 | language: this.localeId, | 405 | language: this.localeId, |
394 | 406 | ||
395 | subtitle: urlOptions.subtitle, | ||
396 | |||
397 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | 407 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { |
398 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | 408 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), |
399 | authorizationHeader: this.authService.getRequestHeaderValue() | 409 | authorizationHeader: this.authService.getRequestHeaderValue() |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 31cbc7dfd..8f6237326 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -39,7 +39,19 @@ export type P2PMediaLoaderOptions = { | |||
39 | videoFiles: VideoFile[] | 39 | videoFiles: VideoFile[] |
40 | } | 40 | } |
41 | 41 | ||
42 | export type CommonOptions = { | 42 | export interface CustomizationOptions { |
43 | startTime: number | string | ||
44 | stopTime: number | string | ||
45 | |||
46 | controls?: boolean | ||
47 | muted?: boolean | ||
48 | loop?: boolean | ||
49 | subtitle?: string | ||
50 | |||
51 | peertubeLink: boolean | ||
52 | } | ||
53 | |||
54 | export interface CommonOptions extends CustomizationOptions { | ||
43 | playerElement: HTMLVideoElement | 55 | playerElement: HTMLVideoElement |
44 | onPlayerElementChange: (element: HTMLVideoElement) => void | 56 | onPlayerElementChange: (element: HTMLVideoElement) => void |
45 | 57 | ||
@@ -48,21 +60,14 @@ export type CommonOptions = { | |||
48 | enableHotkeys: boolean | 60 | enableHotkeys: boolean |
49 | inactivityTimeout: number | 61 | inactivityTimeout: number |
50 | poster: string | 62 | poster: string |
51 | startTime: number | string | ||
52 | stopTime: number | string | ||
53 | 63 | ||
54 | theaterMode: boolean | 64 | theaterMode: boolean |
55 | captions: boolean | 65 | captions: boolean |
56 | peertubeLink: boolean | ||
57 | 66 | ||
58 | videoViewUrl: string | 67 | videoViewUrl: string |
59 | embedUrl: string | 68 | embedUrl: string |
60 | 69 | ||
61 | language?: string | 70 | language?: string |
62 | controls?: boolean | ||
63 | muted?: boolean | ||
64 | loop?: boolean | ||
65 | subtitle?: string | ||
66 | 71 | ||
67 | videoCaptions: VideoJSCaption[] | 72 | videoCaptions: VideoJSCaption[] |
68 | 73 | ||
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c7b205b11..aafeda257 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -71,7 +71,9 @@ $variables: ( | |||
71 | --menuForegroundColor: var(--menuForegroundColor), | 71 | --menuForegroundColor: var(--menuForegroundColor), |
72 | --submenuColor: var(--submenuColor), | 72 | --submenuColor: var(--submenuColor), |
73 | --inputColor: var(--inputColor), | 73 | --inputColor: var(--inputColor), |
74 | --inputPlaceholderColor: var(--inputPlaceholderColor) | 74 | --inputPlaceholderColor: var(--inputPlaceholderColor), |
75 | --embedForegroundColor: var(--embedForegroundColor), | ||
76 | --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) | ||
75 | ); | 77 | ); |
76 | 78 | ||
77 | /*** theme helper ***/ | 79 | /*** theme helper ***/ |
diff --git a/client/src/sass/player/_player-variables.scss b/client/src/sass/player/_player-variables.scss index 110129790..4e9e8736c 100644 --- a/client/src/sass/player/_player-variables.scss +++ b/client/src/sass/player/_player-variables.scss | |||
@@ -10,4 +10,10 @@ $slider-bg-color: lighten($primary-background-color, 33%); | |||
10 | 10 | ||
11 | $progress-margin: 10px; | 11 | $progress-margin: 10px; |
12 | 12 | ||
13 | $assets-path: '../../assets/' !default; \ No newline at end of file | 13 | $assets-path: '../../assets/' !default; |
14 | |||
15 | body { | ||
16 | --embedForegroundColor: #{$primary-foreground-color}; | ||
17 | |||
18 | --embedBigPlayBackgroundColor: #{$primary-background-color}; | ||
19 | } | ||
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index 71d6d1b1d..eeab0ccdf 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss | |||
@@ -14,7 +14,7 @@ $context-menu-width: 350px; | |||
14 | 14 | ||
15 | .vjs-menu-content { | 15 | .vjs-menu-content { |
16 | opacity: $primary-foreground-opacity; | 16 | opacity: $primary-foreground-opacity; |
17 | color: $primary-foreground-color; | 17 | color: var(--embedForegroundCsolor); |
18 | font-size: $font-size !important; | 18 | font-size: $font-size !important; |
19 | font-weight: $font-semibold; | 19 | font-weight: $font-semibold; |
20 | } | 20 | } |
@@ -30,4 +30,4 @@ $context-menu-width: 350px; | |||
30 | background-color: rgba(255, 255, 255, 0.2); | 30 | background-color: rgba(255, 255, 255, 0.2); |
31 | } | 31 | } |
32 | } | 32 | } |
33 | } \ No newline at end of file | 33 | } |
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index e63a2875c..996024ade 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -10,9 +10,8 @@ | |||
10 | } | 10 | } |
11 | 11 | ||
12 | .video-js.vjs-peertube-skin { | 12 | .video-js.vjs-peertube-skin { |
13 | |||
14 | font-size: $font-size; | 13 | font-size: $font-size; |
15 | color: $primary-foreground-color; | 14 | color: var(--embedForegroundColor); |
16 | 15 | ||
17 | .vjs-dock-text { | 16 | .vjs-dock-text { |
18 | padding-right: 10px; | 17 | padding-right: 10px; |
@@ -114,7 +113,7 @@ | |||
114 | .vjs-control-bar, | 113 | .vjs-control-bar, |
115 | .vjs-big-play-button, | 114 | .vjs-big-play-button, |
116 | .vjs-settings-dialog { | 115 | .vjs-settings-dialog { |
117 | background-color: rgba($primary-background-color, 0.5); | 116 | background-color: var(--embedBigPlayBackgroundColor); |
118 | } | 117 | } |
119 | 118 | ||
120 | .vjs-poster { | 119 | .vjs-poster { |
@@ -139,7 +138,8 @@ | |||
139 | .vjs-theater-control, | 138 | .vjs-theater-control, |
140 | .vjs-settings | 139 | .vjs-settings |
141 | { | 140 | { |
142 | color: $primary-foreground-color !important; | 141 | color: var(--embedForegroundColor) !important; |
142 | |||
143 | opacity: $primary-foreground-opacity; | 143 | opacity: $primary-foreground-opacity; |
144 | transition: opacity .1s; | 144 | transition: opacity .1s; |
145 | 145 | ||
@@ -151,7 +151,7 @@ | |||
151 | .vjs-current-time, | 151 | .vjs-current-time, |
152 | .vjs-duration, | 152 | .vjs-duration, |
153 | .vjs-peertube { | 153 | .vjs-peertube { |
154 | color: $primary-foreground-color; | 154 | color: var(--embedForegroundColor); |
155 | opacity: $primary-foreground-opacity; | 155 | opacity: $primary-foreground-opacity; |
156 | } | 156 | } |
157 | 157 | ||
@@ -171,7 +171,7 @@ | |||
171 | transition: none; | 171 | transition: none; |
172 | 172 | ||
173 | .vjs-play-progress { | 173 | .vjs-play-progress { |
174 | background: $primary-foreground-color; | 174 | background: var(--embedForegroundColor); |
175 | 175 | ||
176 | // Not display the circle if the progress is not hovered | 176 | // Not display the circle if the progress is not hovered |
177 | &::before { | 177 | &::before { |
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss index 61965c85e..83407b445 100644 --- a/client/src/sass/player/settings-menu.scss +++ b/client/src/sass/player/settings-menu.scss | |||
@@ -38,7 +38,7 @@ $setting-transition-easing: ease-out; | |||
38 | position: absolute; | 38 | position: absolute; |
39 | right: .5em; | 39 | right: .5em; |
40 | bottom: 3.5em; | 40 | bottom: 3.5em; |
41 | color: $primary-foreground-color; | 41 | color: var(--embedForegroundColor); |
42 | opacity: $primary-foreground-opacity; | 42 | opacity: $primary-foreground-opacity; |
43 | margin: 0 auto; | 43 | margin: 0 auto; |
44 | font-size: $font-size !important; | 44 | font-size: $font-size !important; |
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 | ||