"@types/core-js": "^0.9.28",
"@types/jasmine": "^2.8.7",
"@types/jasminewd2": "^2.0.3",
+ "@types/jschannel": "^1.0.0",
"@types/lodash-es": "^4.17.0",
"@types/markdown-it": "^0.0.4",
"@types/node": "^9.3.0",
"extract-text-webpack-plugin": "4.0.0-beta.0",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^3.2.0",
+ "html-loader": "^0.5.5",
"https-browserify": "^1.0.0",
"jasmine-core": "^3.1.0",
"jasmine-spec-reporter": "^4.2.1",
+ "jschannel": "^1.0.2",
"karma": "^2.0.2",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.1",
peertubeLink: boolean,
poster: string,
startTime: number
- theaterMode: boolean
+ theaterMode: boolean,
+ controls?: boolean,
+ muted?: boolean,
+ loop?: boolean
}) {
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,
autoplay: false,
inactivityTimeout: options.inactivityTimeout,
--- /dev/null
+
+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
+}
\ No newline at end of file
--- /dev/null
+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
+ }
+}
--- /dev/null
+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
\ No newline at end of file
// For google bot that uses Chrome 41 and does not understand 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 { 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
+ ) {
+ }
-function loadVideoInfo (videoId: string): Promise<Response> {
- return fetch(getVideoUrl(videoId))
-}
+ private channel : Channel.MessagingChannel
+ private isReady = false
+ private resolutions : PeerTubeResolution[] = null
-function removeElement (element: HTMLElement) {
- element.parentElement.removeChild(element)
-}
+ initialize() {
+ this.constructChannel()
+ this.setupStateTracking()
-function displayError (videoElement: HTMLVideoElement, text: string) {
- // Remove video element
- removeElement(videoElement)
+ // We're ready!
- document.title = 'Sorry - ' + text
+ this.notifyReady()
+ }
+
+ private get element() {
+ return this.embed.videoElement
+ }
- const errorBlock = document.getElementById('error-block')
- errorBlock.style.display = 'flex'
+ 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)
- const errorText = document.getElementById('error-content')
- errorText.innerHTML = text
-}
+ this.channel = channel
+ }
-function videoNotFound (videoElement: HTMLVideoElement) {
- const text = 'This video does not exist.'
- displayError(videoElement, text)
-}
+ 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
+ }
-function videoFetchError (videoElement: HTMLVideoElement) {
- const text = 'We cannot fetch the video. Please try again later.'
- displayError(videoElement, text)
+ 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
+ })
+ }
}
-const urlParts = window.location.href.split('/')
-const lastPart = urlParts[urlParts.length - 1]
-const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[0]
+class PeerTubeEmbed {
+ constructor(
+ private videoContainerId : string
+ ) {
+ this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
+ }
-loadLocale(window.location.origin, videojs, navigator.language)
- .then(() => loadVideoInfo(videoId))
- .then(async response => {
+ 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 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
+ }
- if (!response.ok) {
- if (response.status === 404) return videoNotFound(videoElement)
+ loadVideoInfo (videoId: string): Promise<Response> {
+ return fetch(this.getVideoUrl(videoId))
+ }
- return videoFetchError(videoElement)
- }
+ removeElement (element: HTMLElement) {
+ element.parentElement.removeChild(element)
+ }
- const videoInfo: VideoDetails = await response.json()
+ 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
+ }
- let autoplay = false
- let startTime = 0
+ 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
- autoplay = params.has('autoplay') && (params.get('autoplay') === '1' || params.get('autoplay') === 'true')
+
+ 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) startTime = startTimeParamNumber
+ 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.status === 404)
+ return this.videoNotFound(this.videoElement)
+
+ return this.videoFetchError(this.videoElement)
+ }
+
+ const videoInfo: VideoDetails = await response.json()
+
+ this.loadParams()
const videojsOptions = getVideojsOptions({
- autoplay,
+ autoplay: this.autoplay,
+ controls: this.controls,
+ muted: this.muted,
+ loop: this.loop,
+ startTime : this.startTime,
+
inactivityTimeout: 1500,
- videoViewUrl: getVideoUrl(videoId) + '/views',
- playerElement: videoElement,
+ videoViewUrl: this.getVideoUrl(videoId) + '/views',
+ playerElement: this.videoElement,
videoFiles: videoInfo.files,
videoDuration: videoInfo.duration,
enableHotkeys: true,
peertubeLink: true,
poster: window.location.origin + videoInfo.previewPath,
- startTime,
theaterMode: false
})
- videojs(videoContainerId, videojsOptions, function () {
- const player = this
- player.dock({
- title: videoInfo.name,
- description: player.localize('Uses P2P, others may know your IP is downloading this video.')
- })
+ this.playerOptions = videojsOptions
+ this.player = vjs(this.videoContainerId, videojsOptions, () => {
- 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()
--- /dev/null
+<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>
--- /dev/null
+
+* {
+ 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);
+ }
+}
\ No newline at end of file
--- /dev/null
+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))
+})
\ No newline at end of file
const configuration = {
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: {
use: 'raw-loader',
exclude: [
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')
]
},
}),
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: {
minify: true,
whitelist: [ '*vjs*', '*video-js*' ]
filename: 'embed.html',
title: 'PeerTube',
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']
}),
/**
const distPath = join(root(), 'client', 'dist')
const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
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
// Do not use a template engine for a so little thing
'/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
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
--- /dev/null
+# 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.
\ No newline at end of file