1
0
Fork 0

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:
William Lahti 2018-07-10 08:47:56 -07:00 committed by Chocobozzz
parent 0b755f3b27
commit 999417328b
12 changed files with 975 additions and 74 deletions

View file

@ -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",

View file

@ -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,

View 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
}

View 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
}
}

View 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

View file

@ -17,100 +17,296 @@ 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) { /**
return window.location.origin + '/api/v1/videos/' + id * 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
})
}
} }
function loadVideoInfo (videoId: string): Promise<Response> { class PeerTubeEmbed {
return fetch(getVideoUrl(videoId)) constructor(
} private videoContainerId : string
) {
this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
}
function removeElement (element: HTMLElement) { videoElement : HTMLVideoElement
element.parentElement.removeChild(element) 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'
function displayError (videoElement: HTMLVideoElement, text: string) { static async main() {
// Remove video element
removeElement(videoElement)
document.title = 'Sorry - ' + text
const errorBlock = document.getElementById('error-block')
errorBlock.style.display = 'flex'
const errorText = document.getElementById('error-content')
errorText.innerHTML = text
}
function videoNotFound (videoElement: HTMLVideoElement) {
const text = 'This video does not exist.'
displayError(videoElement, text)
}
function videoFetchError (videoElement: HTMLVideoElement) {
const text = 'We cannot fetch the video. Please try again later.'
displayError(videoElement, text)
}
const urlParts = window.location.href.split('/')
const lastPart = urlParts[urlParts.length - 1]
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
loadLocale(window.location.origin, videojs, navigator.language)
.then(() => loadVideoInfo(videoId))
.then(async response => {
const videoContainerId = 'video-container' const videoContainerId = 'video-container'
const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement const embed = new PeerTubeEmbed(videoContainerId)
await embed.init()
}
getVideoUrl (id: string) {
return window.location.origin + '/api/v1/videos/' + id
}
loadVideoInfo (videoId: string): Promise<Response> {
return fetch(this.getVideoUrl(videoId))
}
removeElement (element: HTMLElement) {
element.parentElement.removeChild(element)
}
displayError (videoElement: HTMLVideoElement, text: string) {
// Remove video element
this.removeElement(videoElement)
document.title = 'Sorry - ' + text
const errorBlock = document.getElementById('error-block')
errorBlock.style.display = 'flex'
const errorText = document.getElementById('error-content')
errorText.innerHTML = text
}
videoNotFound (videoElement: HTMLVideoElement) {
const text = 'This video does not exist.'
this.displayError(videoElement, text)
}
videoFetchError (videoElement: HTMLVideoElement) {
const text = 'We cannot fetch the video. Please try again later.'
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 lastPart = urlParts[urlParts.length - 1]
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
await loadLocale(window.location.origin, vjs, navigator.language)
let response = await this.loadVideoInfo(videoId)
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
title: videoInfo.name, this.player = vjs(this.videoContainerId, videojsOptions, () => {
description: player.localize('Uses P2P, others may know your IP is downloading this video.')
})
addContextMenu(player, window.location.origin + videoInfo.embedPath) window['videojsPlayer'] = this.player
if (this.controls) {
(this.player as any).dock({
title: videoInfo.name,
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()
}) })
}) }
.catch(err => console.error(err)) }
PeerTubeEmbed.main()

View 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>

View 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);
}
}

View 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))
})

View file

@ -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']
}), }),
/** /**

View file

@ -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
View 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.