aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/player/player.ts
blob: f33539134c892f301cc4341e93511134737dc79c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import * as Channel from 'jschannel'
import { EventRegistrar } from './events'
import { EventHandler, PlayerEventType, PeerTubeResolution } from './definitions'

const PASSTHROUGH_EVENTS = [
  'pause',
  'play',
  'playbackStatusUpdate',
  'playbackStatusChange',
  'resolutionUpdate',
  'volumeChange'
]

/**
 * Allows for programmatic control of a PeerTube embed running in an <iframe>
 * within a web page.
 */
export class PeerTubePlayer {

  private eventRegistrar: EventRegistrar = new EventRegistrar()
  private channel: Channel.MessagingChannel
  private readyPromise: Promise<void>

  /**
   * Construct a new PeerTubePlayer for the given PeerTube embed iframe.
   * Optionally provide a `scope` to ensure that messages are not crossed
   * between multiple PeerTube embeds. The string passed here must match the
   * `scope=` query parameter on the embed URL.
   *
   * @param embedElement
   * @param scope
   */
  constructor (
    private embedElement: HTMLIFrameElement,
    private scope?: string
  ) {
    this.eventRegistrar.registerTypes(PASSTHROUGH_EVENTS)

    this.constructChannel()
    this.prepareToBeReady()
  }

  /**
   * Destroy the player object and remove the associated player from the DOM.
   */
  destroy () {
    this.embedElement.remove()
  }

  /**
   * Listen to an event emitted by this player.
   *
   * @param event One of the supported event types
   * @param handler A handler which will be passed an event object (or undefined if no event object is included)
   */
  addEventListener (event: PlayerEventType, handler: EventHandler<any>): boolean {
    return this.eventRegistrar.addListener(event, handler)
  }

  /**
   * Remove an event listener previously added with addEventListener().
   *
   * @param event The name of the event previously listened to
   * @param handler
   */
  removeEventListener (event: PlayerEventType, handler: EventHandler<any>): boolean {
    return this.eventRegistrar.removeListener(event, handler)
  }

  /**
   * Promise resolves when the player is ready.
   */
  get ready (): Promise<void> {
    return this.readyPromise
  }

  /**
   * Tell the embed to start/resume playback
   */
  async play () {
    await this.sendMessage('play')
  }

  /**
   * Tell the embed to pause playback.
   */
  async pause () {
    await this.sendMessage('pause')
  }

  /**
   * Tell the embed to change the audio volume
   * @param value A number from 0 to 1
   */
  async setVolume (value: number) {
    await this.sendMessage('setVolume', value)
  }

  /**
   * Get the current volume level in the embed.
   * @param value A number from 0 to 1
   */
  async getVolume (): Promise<number> {
    return this.sendMessage<void, number>('getVolume')
  }

  /**
   * Tell the embed to seek to a specific position (in seconds)
   * @param seconds
   */
  async seek (seconds: number) {
    await this.sendMessage('seek', seconds)
  }

  /**
   * Tell the embed to switch resolutions to the resolution identified
   * by the given ID.
   *
   * @param resolutionId The ID of the resolution as found with getResolutions()
   */
  async setResolution (resolutionId: any) {
    await this.sendMessage('setResolution', resolutionId)
  }

  /**
   * Retrieve a list of the available resolutions. This may change later, listen to the
   * `resolutionUpdate` event with `addEventListener` in order to be updated as the available
   * resolutions change.
   */
  async getResolutions (): Promise<PeerTubeResolution[]> {
    return this.sendMessage<void, PeerTubeResolution[]>('getResolutions')
  }

  /**
   * Retrieve a list of available playback rates.
   */
  async getPlaybackRates (): Promise<number[]> {
    return this.sendMessage<void, number[]>('getPlaybackRates')
  }

  /**
   * Get the current playback rate. Defaults to 1 (1x playback rate).
   */
  async getPlaybackRate (): Promise<number> {
    return this.sendMessage<void, number>('getPlaybackRate')
  }

  /**
   * Set the playback rate. Should be one of the options returned by getPlaybackRates().
   * Passing 0.5 means half speed, 1 means normal, 2 means 2x speed, etc.
   *
   * @param rate
   */
  async setPlaybackRate (rate: number) {
    await this.sendMessage('setPlaybackRate', rate)
  }

  private constructChannel () {
    this.channel = Channel.build({
      window: this.embedElement.contentWindow,
      origin: '*',
      scope: this.scope || 'peertube'
    })
    this.eventRegistrar.bindToChannel(this.channel)
  }

  private prepareToBeReady () {
    let readyResolve: Function
    let readyReject: Function

    this.readyPromise = new Promise<void>((res, rej) => {
      readyResolve = res
      readyReject = rej
    })

    this.channel.bind('ready', success => success ? readyResolve() : readyReject())
    this.channel.call({
      method: 'isReady',
      success: isReady => isReady ? readyResolve() : null
    })
  }

  private sendMessage<TIn, TOut> (method: string, params?: TIn): Promise<TOut> {
    return new Promise<TOut>((resolve, reject) => {
      this.channel.call({
        method, params,
        success: result => resolve(result),
        error: error => reject(error)
      })
    })
  }
}

// put it on the window as well as the export
window[ 'PeerTubePlayer' ] = PeerTubePlayer