From 999417328bde0e60cd59318fc1c18672356254ce Mon Sep 17 00:00:00 2001 From: William Lahti Date: Tue, 10 Jul 2018 08:47:56 -0700 Subject: 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 --- client/src/standalone/player/definitions.ts | 18 ++ client/src/standalone/player/events.ts | 48 +++++ client/src/standalone/player/player.ts | 190 +++++++++++++++++ client/src/standalone/videos/embed.ts | 306 ++++++++++++++++++++++----- client/src/standalone/videos/test-embed.html | 51 +++++ client/src/standalone/videos/test-embed.scss | 149 +++++++++++++ client/src/standalone/videos/test-embed.ts | 98 +++++++++ 7 files changed, 805 insertions(+), 55 deletions(-) create mode 100644 client/src/standalone/player/definitions.ts create mode 100644 client/src/standalone/player/events.ts create mode 100644 client/src/standalone/player/player.ts create mode 100644 client/src/standalone/videos/test-embed.html create mode 100644 client/src/standalone/videos/test-embed.scss create mode 100644 client/src/standalone/videos/test-embed.ts (limited to 'client/src/standalone') diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/player/definitions.ts new file mode 100644 index 000000000..6920672a7 --- /dev/null +++ b/client/src/standalone/player/definitions.ts @@ -0,0 +1,18 @@ + +export interface EventHandler { + (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 diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/player/events.ts new file mode 100644 index 000000000..c01328352 --- /dev/null +++ b/client/src/standalone/player/events.ts @@ -0,0 +1,48 @@ +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(name : string, event : T) { + this.eventRegistrations[name].registrations.forEach(x => x(event)) + } + + public addListener(name : string, handler : EventHandler) { + 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(name : string, handler : EventHandler) { + if (!this.eventRegistrations[name]) + return false + + this.eventRegistrations[name].registrations = + this.eventRegistrations[name].registrations.filter(x => x === handler) + + return true + } +} diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/player/player.ts new file mode 100644 index 000000000..9fc648d25 --- /dev/null +++ b/client/src/standalone/player/player.ts @@ -0,0 +1,190 @@ +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 + + + diff --git a/client/src/standalone/videos/test-embed.scss b/client/src/standalone/videos/test-embed.scss new file mode 100644 index 000000000..df3d69f21 --- /dev/null +++ b/client/src/standalone/videos/test-embed.scss @@ -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); + } +} \ No newline at end of file diff --git a/client/src/standalone/videos/test-embed.ts b/client/src/standalone/videos/test-embed.ts new file mode 100644 index 000000000..721514488 --- /dev/null +++ b/client/src/standalone/videos/test-embed.ts @@ -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(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 -- cgit v1.2.3