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/player | |
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/player')
-rw-r--r-- | client/src/standalone/player/definitions.ts | 18 | ||||
-rw-r--r-- | client/src/standalone/player/events.ts | 48 | ||||
-rw-r--r-- | client/src/standalone/player/player.ts | 190 |
3 files changed, 256 insertions, 0 deletions
diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/player/definitions.ts new file mode 100644 index 000000000..6920672a7 --- /dev/null +++ b/client/src/standalone/player/definitions.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | |||
2 | export interface EventHandler<T> { | ||
3 | (ev : T) : void | ||
4 | } | ||
5 | |||
6 | export type PlayerEventType = | ||
7 | 'pause' | 'play' | | ||
8 | 'playbackStatusUpdate' | | ||
9 | 'playbackStatusChange' | | ||
10 | 'resolutionUpdate' | ||
11 | ; | ||
12 | |||
13 | export interface PeerTubeResolution { | ||
14 | id : any | ||
15 | label : string | ||
16 | src : string | ||
17 | active : boolean | ||
18 | } \ No newline at end of file | ||
diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/player/events.ts new file mode 100644 index 000000000..c01328352 --- /dev/null +++ b/client/src/standalone/player/events.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { EventHandler } from "./definitions" | ||
2 | |||
3 | interface PlayerEventRegistrar { | ||
4 | registrations : Function[] | ||
5 | } | ||
6 | |||
7 | interface PlayerEventRegistrationMap { | ||
8 | [name : string] : PlayerEventRegistrar | ||
9 | } | ||
10 | |||
11 | export class EventRegistrar { | ||
12 | |||
13 | private eventRegistrations : PlayerEventRegistrationMap = {} | ||
14 | |||
15 | public bindToChannel(channel : Channel.MessagingChannel) { | ||
16 | for (let name of Object.keys(this.eventRegistrations)) | ||
17 | channel.bind(name, (txn, params) => this.fire(name, params)) | ||
18 | } | ||
19 | |||
20 | public registerTypes(names : string[]) { | ||
21 | for (let name of names) | ||
22 | this.eventRegistrations[name] = { registrations: [] } | ||
23 | } | ||
24 | |||
25 | public fire<T>(name : string, event : T) { | ||
26 | this.eventRegistrations[name].registrations.forEach(x => x(event)) | ||
27 | } | ||
28 | |||
29 | public addListener<T>(name : string, handler : EventHandler<T>) { | ||
30 | if (!this.eventRegistrations[name]) { | ||
31 | console.warn(`PeerTube: addEventListener(): The event '${name}' is not supported`) | ||
32 | return false | ||
33 | } | ||
34 | |||
35 | this.eventRegistrations[name].registrations.push(handler) | ||
36 | return true | ||
37 | } | ||
38 | |||
39 | public removeListener<T>(name : string, handler : EventHandler<T>) { | ||
40 | if (!this.eventRegistrations[name]) | ||
41 | return false | ||
42 | |||
43 | this.eventRegistrations[name].registrations = | ||
44 | this.eventRegistrations[name].registrations.filter(x => x === handler) | ||
45 | |||
46 | return true | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/player/player.ts new file mode 100644 index 000000000..9fc648d25 --- /dev/null +++ b/client/src/standalone/player/player.ts | |||
@@ -0,0 +1,190 @@ | |||
1 | import * as Channel from 'jschannel' | ||
2 | import { EventRegistrar } from './events' | ||
3 | import { EventHandler, PlayerEventType, PeerTubeResolution } from './definitions' | ||
4 | |||
5 | const PASSTHROUGH_EVENTS = [ | ||
6 | 'pause', 'play', | ||
7 | 'playbackStatusUpdate', | ||
8 | 'playbackStatusChange', | ||
9 | 'resolutionUpdate' | ||
10 | ] | ||
11 | |||
12 | /** | ||
13 | * Allows for programmatic control of a PeerTube embed running in an <iframe> | ||
14 | * within a web page. | ||
15 | */ | ||
16 | export class PeerTubePlayer { | ||
17 | /** | ||
18 | * Construct a new PeerTubePlayer for the given PeerTube embed iframe. | ||
19 | * Optionally provide a `scope` to ensure that messages are not crossed | ||
20 | * between multiple PeerTube embeds. The string passed here must match the | ||
21 | * `scope=` query parameter on the embed URL. | ||
22 | * | ||
23 | * @param embedElement | ||
24 | * @param scope | ||
25 | */ | ||
26 | constructor( | ||
27 | private embedElement : HTMLIFrameElement, | ||
28 | private scope? : string | ||
29 | ) { | ||
30 | this.eventRegistrar.registerTypes(PASSTHROUGH_EVENTS) | ||
31 | |||
32 | this.constructChannel() | ||
33 | this.prepareToBeReady() | ||
34 | } | ||
35 | |||
36 | private eventRegistrar : EventRegistrar = new EventRegistrar() | ||
37 | private channel : Channel.MessagingChannel | ||
38 | private readyPromise : Promise<void> | ||
39 | |||
40 | /** | ||
41 | * Destroy the player object and remove the associated player from the DOM. | ||
42 | */ | ||
43 | destroy() { | ||
44 | this.embedElement.remove() | ||
45 | } | ||
46 | |||
47 | /** | ||
48 | * Listen to an event emitted by this player. | ||
49 | * | ||
50 | * @param event One of the supported event types | ||
51 | * @param handler A handler which will be passed an event object (or undefined if no event object is included) | ||
52 | */ | ||
53 | addEventListener(event : PlayerEventType, handler : EventHandler<any>): boolean { | ||
54 | return this.eventRegistrar.addListener(event, handler) | ||
55 | } | ||
56 | |||
57 | /** | ||
58 | * Remove an event listener previously added with addEventListener(). | ||
59 | * | ||
60 | * @param event The name of the event previously listened to | ||
61 | * @param handler | ||
62 | */ | ||
63 | removeEventListener(event : PlayerEventType, handler : EventHandler<any>): boolean { | ||
64 | return this.eventRegistrar.removeListener(event, handler) | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * Promise resolves when the player is ready. | ||
69 | */ | ||
70 | get ready(): Promise<void> { | ||
71 | return this.readyPromise | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * Tell the embed to start/resume playback | ||
76 | */ | ||
77 | async play() { | ||
78 | await this.sendMessage('play') | ||
79 | } | ||
80 | |||
81 | /** | ||
82 | * Tell the embed to pause playback. | ||
83 | */ | ||
84 | async pause() { | ||
85 | await this.sendMessage('pause') | ||
86 | } | ||
87 | |||
88 | /** | ||
89 | * Tell the embed to change the audio volume | ||
90 | * @param value A number from 0 to 1 | ||
91 | */ | ||
92 | async setVolume(value : number) { | ||
93 | await this.sendMessage('setVolume', value) | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Get the current volume level in the embed. | ||
98 | * @param value A number from 0 to 1 | ||
99 | */ | ||
100 | async getVolume(): Promise<number> { | ||
101 | return await this.sendMessage<void, number>('setVolume') | ||
102 | } | ||
103 | |||
104 | /** | ||
105 | * Tell the embed to seek to a specific position (in seconds) | ||
106 | * @param seconds | ||
107 | */ | ||
108 | async seek(seconds : number) { | ||
109 | await this.sendMessage('seek', seconds) | ||
110 | } | ||
111 | |||
112 | /** | ||
113 | * Tell the embed to switch resolutions to the resolution identified | ||
114 | * by the given ID. | ||
115 | * | ||
116 | * @param resolutionId The ID of the resolution as found with getResolutions() | ||
117 | */ | ||
118 | async setResolution(resolutionId : any) { | ||
119 | await this.sendMessage('setResolution', resolutionId) | ||
120 | } | ||
121 | |||
122 | /** | ||
123 | * Retrieve a list of the available resolutions. This may change later, listen to the | ||
124 | * `resolutionUpdate` event with `addEventListener` in order to be updated as the available | ||
125 | * resolutions change. | ||
126 | */ | ||
127 | async getResolutions(): Promise<PeerTubeResolution[]> { | ||
128 | return await this.sendMessage<void, PeerTubeResolution[]>('getResolutions') | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * Retrieve a list of available playback rates. | ||
133 | */ | ||
134 | async getPlaybackRates() : Promise<number[]> { | ||
135 | return await this.sendMessage<void, number[]>('getPlaybackRates') | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Get the current playback rate. Defaults to 1 (1x playback rate). | ||
140 | */ | ||
141 | async getPlaybackRate() : Promise<number> { | ||
142 | return await this.sendMessage<void, number>('getPlaybackRate') | ||
143 | } | ||
144 | |||
145 | /** | ||
146 | * Set the playback rate. Should be one of the options returned by getPlaybackRates(). | ||
147 | * Passing 0.5 means half speed, 1 means normal, 2 means 2x speed, etc. | ||
148 | * | ||
149 | * @param rate | ||
150 | */ | ||
151 | async setPlaybackRate(rate : number) { | ||
152 | await this.sendMessage('setPlaybackRate', rate) | ||
153 | } | ||
154 | |||
155 | private constructChannel() { | ||
156 | this.channel = Channel.build({ | ||
157 | window: this.embedElement.contentWindow, | ||
158 | origin: '*', | ||
159 | scope: this.scope || 'peertube' | ||
160 | }) | ||
161 | this.eventRegistrar.bindToChannel(this.channel) | ||
162 | } | ||
163 | |||
164 | private prepareToBeReady() { | ||
165 | let readyResolve, readyReject | ||
166 | this.readyPromise = new Promise<void>((res, rej) => { | ||
167 | readyResolve = res | ||
168 | readyReject = rej | ||
169 | }) | ||
170 | |||
171 | this.channel.bind('ready', success => success ? readyResolve() : readyReject()) | ||
172 | this.channel.call({ | ||
173 | method: 'isReady', | ||
174 | success: isReady => isReady ? readyResolve() : null | ||
175 | }) | ||
176 | } | ||
177 | |||
178 | private sendMessage<TIn, TOut>(method : string, params? : TIn): Promise<TOut> { | ||
179 | return new Promise<TOut>((resolve, reject) => { | ||
180 | this.channel.call({ | ||
181 | method, params, | ||
182 | success: result => resolve(result), | ||
183 | error: error => reject(error) | ||
184 | }) | ||
185 | }) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | // put it on the window as well as the export | ||
190 | window['PeerTubePlayer'] = PeerTubePlayer \ No newline at end of file | ||