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
This commit is contained in:
parent
0b755f3b27
commit
999417328b
12 changed files with 975 additions and 74 deletions
|
@ -52,6 +52,7 @@
|
||||||
"@types/core-js": "^0.9.28",
|
"@types/core-js": "^0.9.28",
|
||||||
"@types/jasmine": "^2.8.7",
|
"@types/jasmine": "^2.8.7",
|
||||||
"@types/jasminewd2": "^2.0.3",
|
"@types/jasminewd2": "^2.0.3",
|
||||||
|
"@types/jschannel": "^1.0.0",
|
||||||
"@types/lodash-es": "^4.17.0",
|
"@types/lodash-es": "^4.17.0",
|
||||||
"@types/markdown-it": "^0.0.4",
|
"@types/markdown-it": "^0.0.4",
|
||||||
"@types/node": "^9.3.0",
|
"@types/node": "^9.3.0",
|
||||||
|
@ -70,9 +71,11 @@
|
||||||
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
||||||
"file-loader": "^1.1.5",
|
"file-loader": "^1.1.5",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"html-loader": "^0.5.5",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"jasmine-core": "^3.1.0",
|
"jasmine-core": "^3.1.0",
|
||||||
"jasmine-spec-reporter": "^4.2.1",
|
"jasmine-spec-reporter": "^4.2.1",
|
||||||
|
"jschannel": "^1.0.2",
|
||||||
"karma": "^2.0.2",
|
"karma": "^2.0.2",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
"karma-coverage-istanbul-reporter": "^2.0.1",
|
"karma-coverage-istanbul-reporter": "^2.0.1",
|
||||||
|
|
|
@ -29,10 +29,15 @@ function getVideojsOptions (options: {
|
||||||
peertubeLink: boolean,
|
peertubeLink: boolean,
|
||||||
poster: string,
|
poster: string,
|
||||||
startTime: number
|
startTime: number
|
||||||
theaterMode: boolean
|
theaterMode: boolean,
|
||||||
|
controls?: boolean,
|
||||||
|
muted?: boolean,
|
||||||
|
loop?: boolean
|
||||||
}) {
|
}) {
|
||||||
const videojsOptions = {
|
const videojsOptions = {
|
||||||
controls: true,
|
controls: options.controls !== undefined ? options.controls : true,
|
||||||
|
muted: options.controls !== undefined ? options.muted : false,
|
||||||
|
loop: options.loop !== undefined ? options.loop : false,
|
||||||
poster: options.poster,
|
poster: options.poster,
|
||||||
autoplay: false,
|
autoplay: false,
|
||||||
inactivityTimeout: options.inactivityTimeout,
|
inactivityTimeout: options.inactivityTimeout,
|
||||||
|
|
18
client/src/standalone/player/definitions.ts
Normal file
18
client/src/standalone/player/definitions.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
export interface EventHandler<T> {
|
||||||
|
(ev : T) : void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayerEventType =
|
||||||
|
'pause' | 'play' |
|
||||||
|
'playbackStatusUpdate' |
|
||||||
|
'playbackStatusChange' |
|
||||||
|
'resolutionUpdate'
|
||||||
|
;
|
||||||
|
|
||||||
|
export interface PeerTubeResolution {
|
||||||
|
id : any
|
||||||
|
label : string
|
||||||
|
src : string
|
||||||
|
active : boolean
|
||||||
|
}
|
48
client/src/standalone/player/events.ts
Normal file
48
client/src/standalone/player/events.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { EventHandler } from "./definitions"
|
||||||
|
|
||||||
|
interface PlayerEventRegistrar {
|
||||||
|
registrations : Function[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayerEventRegistrationMap {
|
||||||
|
[name : string] : PlayerEventRegistrar
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventRegistrar {
|
||||||
|
|
||||||
|
private eventRegistrations : PlayerEventRegistrationMap = {}
|
||||||
|
|
||||||
|
public bindToChannel(channel : Channel.MessagingChannel) {
|
||||||
|
for (let name of Object.keys(this.eventRegistrations))
|
||||||
|
channel.bind(name, (txn, params) => this.fire(name, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerTypes(names : string[]) {
|
||||||
|
for (let name of names)
|
||||||
|
this.eventRegistrations[name] = { registrations: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public fire<T>(name : string, event : T) {
|
||||||
|
this.eventRegistrations[name].registrations.forEach(x => x(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener<T>(name : string, handler : EventHandler<T>) {
|
||||||
|
if (!this.eventRegistrations[name]) {
|
||||||
|
console.warn(`PeerTube: addEventListener(): The event '${name}' is not supported`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventRegistrations[name].registrations.push(handler)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener<T>(name : string, handler : EventHandler<T>) {
|
||||||
|
if (!this.eventRegistrations[name])
|
||||||
|
return false
|
||||||
|
|
||||||
|
this.eventRegistrations[name].registrations =
|
||||||
|
this.eventRegistrations[name].registrations.filter(x => x === handler)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
190
client/src/standalone/player/player.ts
Normal file
190
client/src/standalone/player/player.ts
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import * as Channel from 'jschannel'
|
||||||
|
import { EventRegistrar } from './events'
|
||||||
|
import { EventHandler, PlayerEventType, PeerTubeResolution } from './definitions'
|
||||||
|
|
||||||
|
const PASSTHROUGH_EVENTS = [
|
||||||
|
'pause', 'play',
|
||||||
|
'playbackStatusUpdate',
|
||||||
|
'playbackStatusChange',
|
||||||
|
'resolutionUpdate'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows for programmatic control of a PeerTube embed running in an <iframe>
|
||||||
|
* within a web page.
|
||||||
|
*/
|
||||||
|
export class PeerTubePlayer {
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private eventRegistrar : EventRegistrar = new EventRegistrar()
|
||||||
|
private channel : Channel.MessagingChannel
|
||||||
|
private readyPromise : Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 await this.sendMessage<void, number>('setVolume')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 await this.sendMessage<void, PeerTubeResolution[]>('getResolutions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a list of available playback rates.
|
||||||
|
*/
|
||||||
|
async getPlaybackRates() : Promise<number[]> {
|
||||||
|
return await this.sendMessage<void, number[]>('getPlaybackRates')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current playback rate. Defaults to 1 (1x playback rate).
|
||||||
|
*/
|
||||||
|
async getPlaybackRate() : Promise<number> {
|
||||||
|
return await 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, readyReject
|
||||||
|
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
|
|
@ -17,26 +17,180 @@ import 'core-js/es6/set'
|
||||||
// For google bot that uses Chrome 41 and does not understand fetch
|
// For google bot that uses Chrome 41 and does not understand fetch
|
||||||
import 'whatwg-fetch'
|
import 'whatwg-fetch'
|
||||||
|
|
||||||
import * as videojs from 'video.js'
|
import * as vjs from 'video.js'
|
||||||
|
import * as Channel from 'jschannel'
|
||||||
|
|
||||||
import { VideoDetails } from '../../../../shared'
|
import { VideoDetails } from '../../../../shared'
|
||||||
import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player'
|
import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player'
|
||||||
|
import { PeerTubeResolution } from '../player/definitions';
|
||||||
|
|
||||||
function getVideoUrl (id: string) {
|
/**
|
||||||
|
* Embed API exposes control of the embed player to the outside world via
|
||||||
|
* JSChannels and window.postMessage
|
||||||
|
*/
|
||||||
|
class PeerTubeEmbedApi {
|
||||||
|
constructor(
|
||||||
|
private embed : PeerTubeEmbed
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private channel : Channel.MessagingChannel
|
||||||
|
private isReady = false
|
||||||
|
private resolutions : PeerTubeResolution[] = null
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.constructChannel()
|
||||||
|
this.setupStateTracking()
|
||||||
|
|
||||||
|
// We're ready!
|
||||||
|
|
||||||
|
this.notifyReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
private get element() {
|
||||||
|
return this.embed.videoElement
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructChannel() {
|
||||||
|
let channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope })
|
||||||
|
|
||||||
|
channel.bind('play', (txn, params) => this.embed.player.play())
|
||||||
|
channel.bind('pause', (txn, params) => this.embed.player.pause())
|
||||||
|
channel.bind('seek', (txn, time) => this.embed.player.currentTime(time))
|
||||||
|
channel.bind('setVolume', (txn, value) => this.embed.player.volume(value))
|
||||||
|
channel.bind('getVolume', (txn, value) => this.embed.player.volume())
|
||||||
|
channel.bind('isReady', (txn, params) => this.isReady)
|
||||||
|
channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId))
|
||||||
|
channel.bind('getResolutions', (txn, params) => this.resolutions)
|
||||||
|
channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate))
|
||||||
|
channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate())
|
||||||
|
channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates)
|
||||||
|
|
||||||
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
private setResolution(resolutionId : number) {
|
||||||
|
if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden())
|
||||||
|
return
|
||||||
|
|
||||||
|
// Auto resolution
|
||||||
|
if (resolutionId === -1) {
|
||||||
|
this.embed.player.peertube().enableAutoResolution()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.embed.player.peertube().disableAutoResolution()
|
||||||
|
this.embed.player.peertube().updateResolution(resolutionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let the host know that we're ready to go!
|
||||||
|
*/
|
||||||
|
private notifyReady() {
|
||||||
|
this.isReady = true
|
||||||
|
this.channel.notify({ method: 'ready', params: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupStateTracking() {
|
||||||
|
|
||||||
|
let currentState : 'playing' | 'paused' | 'unstarted' = 'unstarted'
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
let position = this.element.currentTime
|
||||||
|
let volume = this.element.volume
|
||||||
|
|
||||||
|
this.channel.notify({
|
||||||
|
method: 'playbackStatusUpdate',
|
||||||
|
params: {
|
||||||
|
position,
|
||||||
|
volume,
|
||||||
|
playbackState: currentState,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
this.element.addEventListener('play', ev => {
|
||||||
|
currentState = 'playing'
|
||||||
|
this.channel.notify({ method: 'playbackStatusChange', params: 'playing' })
|
||||||
|
})
|
||||||
|
|
||||||
|
this.element.addEventListener('pause', ev => {
|
||||||
|
currentState = 'paused'
|
||||||
|
this.channel.notify({ method: 'playbackStatusChange', params: 'paused' })
|
||||||
|
})
|
||||||
|
|
||||||
|
// PeerTube specific capabilities
|
||||||
|
|
||||||
|
this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions())
|
||||||
|
this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions())
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadResolutions() {
|
||||||
|
let resolutions = []
|
||||||
|
let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId()
|
||||||
|
|
||||||
|
for (const videoFile of this.embed.player.peertube().videoFiles) {
|
||||||
|
let label = videoFile.resolution.label
|
||||||
|
if (videoFile.fps && videoFile.fps >= 50) {
|
||||||
|
label += videoFile.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutions.push({
|
||||||
|
id: videoFile.resolution.id,
|
||||||
|
label,
|
||||||
|
src: videoFile.magnetUri,
|
||||||
|
active: videoFile.resolution.id === currentResolutionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resolutions = resolutions
|
||||||
|
this.channel.notify({
|
||||||
|
method: 'resolutionUpdate',
|
||||||
|
params: this.resolutions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerTubeEmbed {
|
||||||
|
constructor(
|
||||||
|
private videoContainerId : string
|
||||||
|
) {
|
||||||
|
this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
|
||||||
|
}
|
||||||
|
|
||||||
|
videoElement : HTMLVideoElement
|
||||||
|
player : any
|
||||||
|
playerOptions : any
|
||||||
|
api : PeerTubeEmbedApi = null
|
||||||
|
autoplay : boolean = false
|
||||||
|
controls : boolean = true
|
||||||
|
muted : boolean = false
|
||||||
|
loop : boolean = false
|
||||||
|
enableApi : boolean = false
|
||||||
|
startTime : number = 0
|
||||||
|
scope : string = 'peertube'
|
||||||
|
|
||||||
|
static async main() {
|
||||||
|
const videoContainerId = 'video-container'
|
||||||
|
const embed = new PeerTubeEmbed(videoContainerId)
|
||||||
|
await embed.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoUrl (id: string) {
|
||||||
return window.location.origin + '/api/v1/videos/' + id
|
return window.location.origin + '/api/v1/videos/' + id
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadVideoInfo (videoId: string): Promise<Response> {
|
loadVideoInfo (videoId: string): Promise<Response> {
|
||||||
return fetch(getVideoUrl(videoId))
|
return fetch(this.getVideoUrl(videoId))
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeElement (element: HTMLElement) {
|
removeElement (element: HTMLElement) {
|
||||||
element.parentElement.removeChild(element)
|
element.parentElement.removeChild(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayError (videoElement: HTMLVideoElement, text: string) {
|
displayError (videoElement: HTMLVideoElement, text: string) {
|
||||||
// Remove video element
|
// Remove video element
|
||||||
removeElement(videoElement)
|
this.removeElement(videoElement)
|
||||||
|
|
||||||
document.title = 'Sorry - ' + text
|
document.title = 'Sorry - ' + text
|
||||||
|
|
||||||
|
@ -47,70 +201,112 @@ function displayError (videoElement: HTMLVideoElement, text: string) {
|
||||||
errorText.innerHTML = text
|
errorText.innerHTML = text
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoNotFound (videoElement: HTMLVideoElement) {
|
videoNotFound (videoElement: HTMLVideoElement) {
|
||||||
const text = 'This video does not exist.'
|
const text = 'This video does not exist.'
|
||||||
displayError(videoElement, text)
|
this.displayError(videoElement, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoFetchError (videoElement: HTMLVideoElement) {
|
videoFetchError (videoElement: HTMLVideoElement) {
|
||||||
const text = 'We cannot fetch the video. Please try again later.'
|
const text = 'We cannot fetch the video. Please try again later.'
|
||||||
displayError(videoElement, text)
|
this.displayError(videoElement, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParamToggle (params: URLSearchParams, name: string, defaultValue: boolean) {
|
||||||
|
return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
getParamString (params: URLSearchParams, name: string, defaultValue: string) {
|
||||||
|
return params.has(name) ? params.get(name) : defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeApi() {
|
||||||
|
if (!this.enableApi)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.api = new PeerTubeEmbedApi(this)
|
||||||
|
this.api.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
await this.initCore()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadParams() {
|
||||||
|
try {
|
||||||
|
let params = new URL(window.location.toString()).searchParams
|
||||||
|
|
||||||
|
this.autoplay = this.getParamToggle(params, 'autoplay', this.autoplay)
|
||||||
|
this.controls = this.getParamToggle(params, 'controls', this.controls)
|
||||||
|
this.muted = this.getParamToggle(params, 'muted', this.muted)
|
||||||
|
this.loop = this.getParamToggle(params, 'loop', this.loop)
|
||||||
|
this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
|
||||||
|
this.scope = this.getParamString(params, 'scope', this.scope)
|
||||||
|
|
||||||
|
const startTimeParamString = params.get('start')
|
||||||
|
const startTimeParamNumber = parseInt(startTimeParamString, 10)
|
||||||
|
if (isNaN(startTimeParamNumber) === false)
|
||||||
|
this.startTime = startTimeParamNumber
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Cannot get params from URL.', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initCore() {
|
||||||
const urlParts = window.location.href.split('/')
|
const urlParts = window.location.href.split('/')
|
||||||
const lastPart = urlParts[urlParts.length - 1]
|
const lastPart = urlParts[urlParts.length - 1]
|
||||||
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
|
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
|
||||||
|
|
||||||
loadLocale(window.location.origin, videojs, navigator.language)
|
await loadLocale(window.location.origin, vjs, navigator.language)
|
||||||
.then(() => loadVideoInfo(videoId))
|
let response = await this.loadVideoInfo(videoId)
|
||||||
.then(async response => {
|
|
||||||
const videoContainerId = 'video-container'
|
|
||||||
const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) return videoNotFound(videoElement)
|
if (response.status === 404)
|
||||||
|
return this.videoNotFound(this.videoElement)
|
||||||
|
|
||||||
return videoFetchError(videoElement)
|
return this.videoFetchError(this.videoElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoInfo: VideoDetails = await response.json()
|
const videoInfo: VideoDetails = await response.json()
|
||||||
|
|
||||||
let autoplay = false
|
this.loadParams()
|
||||||
let startTime = 0
|
|
||||||
|
|
||||||
try {
|
|
||||||
let params = new URL(window.location.toString()).searchParams
|
|
||||||
autoplay = params.has('autoplay') && (params.get('autoplay') === '1' || params.get('autoplay') === 'true')
|
|
||||||
|
|
||||||
const startTimeParamString = params.get('start')
|
|
||||||
const startTimeParamNumber = parseInt(startTimeParamString, 10)
|
|
||||||
if (isNaN(startTimeParamNumber) === false) startTime = startTimeParamNumber
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot get params from URL.', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const videojsOptions = getVideojsOptions({
|
const videojsOptions = getVideojsOptions({
|
||||||
autoplay,
|
autoplay: this.autoplay,
|
||||||
|
controls: this.controls,
|
||||||
|
muted: this.muted,
|
||||||
|
loop: this.loop,
|
||||||
|
startTime : this.startTime,
|
||||||
|
|
||||||
inactivityTimeout: 1500,
|
inactivityTimeout: 1500,
|
||||||
videoViewUrl: getVideoUrl(videoId) + '/views',
|
videoViewUrl: this.getVideoUrl(videoId) + '/views',
|
||||||
playerElement: videoElement,
|
playerElement: this.videoElement,
|
||||||
videoFiles: videoInfo.files,
|
videoFiles: videoInfo.files,
|
||||||
videoDuration: videoInfo.duration,
|
videoDuration: videoInfo.duration,
|
||||||
enableHotkeys: true,
|
enableHotkeys: true,
|
||||||
peertubeLink: true,
|
peertubeLink: true,
|
||||||
poster: window.location.origin + videoInfo.previewPath,
|
poster: window.location.origin + videoInfo.previewPath,
|
||||||
startTime,
|
|
||||||
theaterMode: false
|
theaterMode: false
|
||||||
})
|
})
|
||||||
videojs(videoContainerId, videojsOptions, function () {
|
|
||||||
const player = this
|
|
||||||
|
|
||||||
player.dock({
|
this.playerOptions = videojsOptions
|
||||||
|
this.player = vjs(this.videoContainerId, videojsOptions, () => {
|
||||||
|
|
||||||
|
window['videojsPlayer'] = this.player
|
||||||
|
|
||||||
|
if (this.controls) {
|
||||||
|
(this.player as any).dock({
|
||||||
title: videoInfo.name,
|
title: videoInfo.name,
|
||||||
description: player.localize('Uses P2P, others may know your IP is downloading this video.')
|
description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
|
||||||
|
this.initializeApi()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addContextMenu(player, window.location.origin + videoInfo.embedPath)
|
PeerTubeEmbed.main()
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
|
|
51
client/src/standalone/videos/test-embed.html
Normal file
51
client/src/standalone/videos/test-embed.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="logo">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="../../assets/images/logo.svg">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
PeerTube
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<h1>Embed Playground</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<aside>
|
||||||
|
<div id="host"></div>
|
||||||
|
</aside>
|
||||||
|
<div id="controls">
|
||||||
|
<div>
|
||||||
|
<button onclick="player.play()">Play</button>
|
||||||
|
<button onclick="player.pause()">Pause</button>
|
||||||
|
<button onclick="player.seek(parseInt(prompt('Enter position to seek to (in seconds)')))">Seek</button>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="options">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Resolutions:</legend>
|
||||||
|
<div id="resolution-list"></div>
|
||||||
|
<br/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Rates:</legend>
|
||||||
|
<div id="rate-list"></div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- iframes are used dynamically -->
|
||||||
|
<iframe hidden></iframe>
|
||||||
|
<a hidden></a>
|
||||||
|
</body>
|
||||||
|
</html>
|
149
client/src/standalone/videos/test-embed.scss
Normal file
149
client/src/standalone/videos/test-embed.scss
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 200px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
width: 33vw;
|
||||||
|
margin: 0 .5em .5em 0;
|
||||||
|
height: calc(33vw * 0.5625);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 150%;
|
||||||
|
height: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 18px 0 32px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 0 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
width: 100%;
|
||||||
|
height: 3.2em;
|
||||||
|
background-color: #F1680D;
|
||||||
|
color: white;
|
||||||
|
//background-image: url(../../assets/images/backdrop/network-o.png);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
box-shadow: 1px 0px 10px rgba(0,0,0,0.6);
|
||||||
|
background-size: 50%;
|
||||||
|
background-position: top left;
|
||||||
|
padding-right: 1em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em 0 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: 100;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: none;
|
||||||
|
min-width: 8em;
|
||||||
|
legend {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #F1680D;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em 1.25em;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&, &:hover, &:focus, &:visited, &:active {
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
aside {
|
||||||
|
width: 50vw;
|
||||||
|
height: calc(50vw * 0.5625);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
main {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
width: calc(100vw - 2em);
|
||||||
|
height: calc(56.25vw - 2em * 0.5625);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1800px) {
|
||||||
|
aside {
|
||||||
|
width: 50vw;
|
||||||
|
height: calc(50vw * 0.5625);
|
||||||
|
}
|
||||||
|
}
|
98
client/src/standalone/videos/test-embed.ts
Normal file
98
client/src/standalone/videos/test-embed.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import './test-embed.scss'
|
||||||
|
import { PeerTubePlayer } from '../player/player';
|
||||||
|
import { PlayerEventType } from '../player/definitions';
|
||||||
|
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
|
||||||
|
const urlParts = window.location.href.split('/')
|
||||||
|
const lastPart = urlParts[urlParts.length - 1]
|
||||||
|
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
|
||||||
|
|
||||||
|
let iframe = document.createElement('iframe')
|
||||||
|
iframe.src = `/videos/embed/${videoId}?autoplay=1&controls=0&api=1`
|
||||||
|
let mainElement = document.querySelector('#host')
|
||||||
|
mainElement.appendChild(iframe);
|
||||||
|
|
||||||
|
console.log(`Document finished loading.`)
|
||||||
|
let player = new PeerTubePlayer(document.querySelector('iframe'))
|
||||||
|
|
||||||
|
window['player'] = player
|
||||||
|
|
||||||
|
console.log(`Awaiting player ready...`)
|
||||||
|
await player.ready
|
||||||
|
console.log(`Player is ready.`)
|
||||||
|
|
||||||
|
let monitoredEvents = [
|
||||||
|
'pause', 'play',
|
||||||
|
'playbackStatusUpdate',
|
||||||
|
'playbackStatusChange'
|
||||||
|
]
|
||||||
|
|
||||||
|
monitoredEvents.forEach(e => {
|
||||||
|
player.addEventListener(<PlayerEventType>e, () => console.log(`PLAYER: event '${e}' received`))
|
||||||
|
console.log(`PLAYER: now listening for event '${e}'`)
|
||||||
|
})
|
||||||
|
|
||||||
|
let playbackRates = []
|
||||||
|
let activeRate = 1
|
||||||
|
let currentRate = await player.getPlaybackRate()
|
||||||
|
|
||||||
|
let updateRates = async () => {
|
||||||
|
|
||||||
|
let rateListEl = document.querySelector('#rate-list')
|
||||||
|
rateListEl.innerHTML = ''
|
||||||
|
|
||||||
|
playbackRates.forEach(rate => {
|
||||||
|
if (currentRate == rate) {
|
||||||
|
let itemEl = document.createElement('strong')
|
||||||
|
itemEl.innerText = `${rate} (active)`
|
||||||
|
itemEl.style.display = 'block'
|
||||||
|
rateListEl.appendChild(itemEl)
|
||||||
|
} else {
|
||||||
|
let itemEl = document.createElement('a')
|
||||||
|
itemEl.href = 'javascript:;'
|
||||||
|
itemEl.innerText = rate
|
||||||
|
itemEl.addEventListener('click', () => {
|
||||||
|
player.setPlaybackRate(rate)
|
||||||
|
currentRate = rate
|
||||||
|
updateRates()
|
||||||
|
})
|
||||||
|
itemEl.style.display = 'block'
|
||||||
|
rateListEl.appendChild(itemEl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
player.getPlaybackRates().then(rates => {
|
||||||
|
playbackRates = rates
|
||||||
|
updateRates()
|
||||||
|
})
|
||||||
|
|
||||||
|
let updateResolutions = resolutions => {
|
||||||
|
let resolutionListEl = document.querySelector('#resolution-list')
|
||||||
|
resolutionListEl.innerHTML = ''
|
||||||
|
|
||||||
|
resolutions.forEach(resolution => {
|
||||||
|
if (resolution.active) {
|
||||||
|
let itemEl = document.createElement('strong')
|
||||||
|
itemEl.innerText = `${resolution.label} (active)`
|
||||||
|
itemEl.style.display = 'block'
|
||||||
|
resolutionListEl.appendChild(itemEl)
|
||||||
|
} else {
|
||||||
|
let itemEl = document.createElement('a')
|
||||||
|
itemEl.href = 'javascript:;'
|
||||||
|
itemEl.innerText = resolution.label
|
||||||
|
itemEl.addEventListener('click', () => {
|
||||||
|
player.setResolution(resolution.id)
|
||||||
|
})
|
||||||
|
itemEl.style.display = 'block'
|
||||||
|
resolutionListEl.appendChild(itemEl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
player.getResolutions().then(
|
||||||
|
resolutions => updateResolutions(resolutions))
|
||||||
|
player.addEventListener('resolutionUpdate',
|
||||||
|
resolutions => updateResolutions(resolutions))
|
||||||
|
})
|
|
@ -14,7 +14,9 @@ module.exports = function () {
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
entry: {
|
entry: {
|
||||||
'video-embed': './src/standalone/videos/embed.ts'
|
'video-embed': './src/standalone/videos/embed.ts',
|
||||||
|
'player': './src/standalone/player/player.ts',
|
||||||
|
'test-embed': './src/standalone/videos/test-embed.ts'
|
||||||
},
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -89,7 +91,8 @@ module.exports = function () {
|
||||||
use: 'raw-loader',
|
use: 'raw-loader',
|
||||||
exclude: [
|
exclude: [
|
||||||
helpers.root('src/index.html'),
|
helpers.root('src/index.html'),
|
||||||
helpers.root('src/standalone/videos/embed.html')
|
helpers.root('src/standalone/videos/embed.html'),
|
||||||
|
helpers.root('src/standalone/videos/test-embed.html')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -110,7 +113,10 @@ module.exports = function () {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new PurifyCSSPlugin({
|
new PurifyCSSPlugin({
|
||||||
paths: [ helpers.root('src/standalone/videos/embed.ts') ],
|
paths: [
|
||||||
|
helpers.root('src/standalone/videos/embed.ts'),
|
||||||
|
helpers.root('src/standalone/videos/test-embed.html')
|
||||||
|
],
|
||||||
purifyOptions: {
|
purifyOptions: {
|
||||||
minify: true,
|
minify: true,
|
||||||
whitelist: [ '*vjs*', '*video-js*' ]
|
whitelist: [ '*vjs*', '*video-js*' ]
|
||||||
|
@ -124,7 +130,17 @@ module.exports = function () {
|
||||||
filename: 'embed.html',
|
filename: 'embed.html',
|
||||||
title: 'PeerTube',
|
title: 'PeerTube',
|
||||||
chunksSortMode: 'dependency',
|
chunksSortMode: 'dependency',
|
||||||
inject: 'body'
|
inject: 'body',
|
||||||
|
chunks: ['video-embed']
|
||||||
|
}),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: '!!html-loader!src/standalone/videos/test-embed.html',
|
||||||
|
filename: 'test-embed.html',
|
||||||
|
title: 'PeerTube',
|
||||||
|
chunksSortMode: 'dependency',
|
||||||
|
inject: 'body',
|
||||||
|
chunks: ['test-embed']
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,7 @@ const clientsRouter = express.Router()
|
||||||
const distPath = join(root(), 'client', 'dist')
|
const distPath = join(root(), 'client', 'dist')
|
||||||
const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
|
const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
|
||||||
const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
|
const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
|
||||||
|
const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
|
||||||
|
|
||||||
// Special route that add OpenGraph and oEmbed tags
|
// Special route that add OpenGraph and oEmbed tags
|
||||||
// Do not use a template engine for a so little thing
|
// Do not use a template engine for a so little thing
|
||||||
|
@ -32,6 +33,10 @@ clientsRouter.use('' +
|
||||||
'/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
'/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.sendFile(embedPath)
|
res.sendFile(embedPath)
|
||||||
})
|
})
|
||||||
|
clientsRouter.use('' +
|
||||||
|
'/videos/test-embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
res.sendFile(testEmbedPath)
|
||||||
|
})
|
||||||
|
|
||||||
// Static HTML/CSS/JS client files
|
// Static HTML/CSS/JS client files
|
||||||
|
|
||||||
|
|
122
support/doc/api/embeds.md
Normal file
122
support/doc/api/embeds.md
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
# PeerTube Embed API
|
||||||
|
|
||||||
|
PeerTube lets you embed videos and programmatically control their playback. This documentation covers how to interact with the PeerTube Embed API.
|
||||||
|
|
||||||
|
## Playground
|
||||||
|
|
||||||
|
Any PeerTube embed URL (ie `https://my-instance.example.com/videos/embed/52a10666-3a18-4e73-93da-e8d3c12c305a`) can be viewed as an embedding playground which
|
||||||
|
allows you to test various aspects of PeerTube embeds. Simply replace `/embed` with `/test-embed` and visit the URL in a browser.
|
||||||
|
For instance, the playground URL for the above embed URL is `https://my-instance.example.com/videos/test-embed/52a10666-3a18-4e73-93da-e8d3c12c305a`.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Given an existing PeerTube embed `<iframe>`, one can use the PeerTube Embed API to control it by first including the library. You can include it via Yarn with:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add @peertube/embed-api
|
||||||
|
```
|
||||||
|
|
||||||
|
Now just use the `PeerTubePlayer` class exported by the module:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PeerTubePlayer } from '@peertube/embed-api'
|
||||||
|
|
||||||
|
let player = new PeerTubePlayer(document.querySelector('iframe'))
|
||||||
|
await player.ready // wait for the player to be ready
|
||||||
|
|
||||||
|
// now you can use it!
|
||||||
|
player.play()
|
||||||
|
player.seek(32)
|
||||||
|
player.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Methods
|
||||||
|
|
||||||
|
## `play() : Promise<void>`
|
||||||
|
|
||||||
|
Starts playback, or resumes playback if it is paused.
|
||||||
|
|
||||||
|
## `pause() : Promise<void>`
|
||||||
|
|
||||||
|
Pauses playback.
|
||||||
|
|
||||||
|
## `seek(positionInSeconds : number)`
|
||||||
|
|
||||||
|
Seek to the given position, as specified in seconds into the video.
|
||||||
|
|
||||||
|
## `addEventListener(eventName : string, handler : Function)`
|
||||||
|
|
||||||
|
Add a listener for a specific event. See below for the available events.
|
||||||
|
|
||||||
|
## `getResolutions() : Promise<PeerTubeResolution[]>`
|
||||||
|
|
||||||
|
Get the available resolutions. A `PeerTubeResolution` looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"label": "720p",
|
||||||
|
"src": "//src-url-here",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`active` is true if the resolution is the currently selected resolution.
|
||||||
|
|
||||||
|
## `setResolution(resolutionId : number): Promise<void>`
|
||||||
|
|
||||||
|
Change the current resolution. Pass `-1` for automatic resolution (when available).
|
||||||
|
Otherwise, `resolutionId` should be the ID of an object returned by `getResolutions()`
|
||||||
|
|
||||||
|
## `getPlaybackRates() : Promise<number[]>`
|
||||||
|
|
||||||
|
Get the available playback rates, where `1` represents normal speed, `0.5` is half speed, `2` is double speed, etc.
|
||||||
|
|
||||||
|
## `getPlaybackRates() : Promise<number>`
|
||||||
|
|
||||||
|
Get the current playback rate. See `getPlaybackRates()` for more information.
|
||||||
|
|
||||||
|
## `setPlaybackRate(rate : number) : Promise<void>`
|
||||||
|
|
||||||
|
Set the current playback rate. The passed rate should be a value as returned by `getPlaybackRates()`.
|
||||||
|
|
||||||
|
## `setVolume(factor : number) : Promise<void>`
|
||||||
|
|
||||||
|
Set the playback volume. Value should be between `0` and `1`.
|
||||||
|
|
||||||
|
## `getVolume(): Promise<number>`
|
||||||
|
|
||||||
|
Get the playback volume. Returns a value between `0` and `1`.
|
||||||
|
# Events
|
||||||
|
|
||||||
|
You can subscribe to events by using `addEventListener()`. See above for details.
|
||||||
|
|
||||||
|
## Event `play`
|
||||||
|
|
||||||
|
Fired when playback begins or is resumed after pausing.
|
||||||
|
|
||||||
|
## Event `pause`
|
||||||
|
|
||||||
|
Fired when playback is paused.
|
||||||
|
|
||||||
|
## Event `playbackStatusUpdate`
|
||||||
|
|
||||||
|
Fired every half second to provide the current status of playback. The parameter of the callback will resemble:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"position": 22.3,
|
||||||
|
"volume": 0.9,
|
||||||
|
"playbackState": "playing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `volume` field contains the volume from `0` (silent) to `1` (full volume). The `playbackState` can be `playing` or `paused`. More states may be added later.
|
||||||
|
|
||||||
|
## Event `playbackStatusChange`
|
||||||
|
|
||||||
|
Fired when playback transitions between states, such as `pausing` and `playing`. More states may be added later.
|
||||||
|
|
||||||
|
## Event `resolutionUpdate`
|
||||||
|
|
||||||
|
Fired when the available resolutions have changed, or when the currently selected resolution has changed. Listener should call `getResolutions()` to get the updated information.
|
Loading…
Reference in a new issue