1
0
Fork 0

Add logic to handle playlist in embed

This commit is contained in:
Chocobozzz 2020-08-04 11:42:06 +02:00 committed by Chocobozzz
parent a4ff3100d3
commit 5abc96fca2
10 changed files with 244 additions and 51 deletions

View File

@ -163,6 +163,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// Unsubscribe subscriptions // Unsubscribe subscriptions
if (this.paramsSub) this.paramsSub.unsubscribe() if (this.paramsSub) this.paramsSub.unsubscribe()
if (this.queryParamsSub) this.queryParamsSub.unsubscribe() if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
if (this.configSub) this.configSub.unsubscribe()
// Unbind hotkeys // Unbind hotkeys
this.hotkeysService.remove(this.hotkeys) this.hotkeysService.remove(this.hotkeys)

View File

@ -1,11 +1,12 @@
import { PeerTubePlugin } from './peertube-plugin'
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
import { PlayerMode } from './peertube-player-manager'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { VideoFile } from '@shared/models'
import videojs from 'video.js'
import { Config, Level } from 'hls.js' import { Config, Level } from 'hls.js'
import videojs from 'video.js'
import { VideoFile } from '@shared/models'
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { PlayerMode } from './peertube-player-manager'
import { PeerTubePlugin } from './peertube-plugin'
import { EndCardOptions } from './upnext/end-card'
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
declare module 'video.js' { declare module 'video.js' {
@ -42,6 +43,8 @@ declare module 'video.js' {
} }
dock (options: { title: string, description: string }): void dock (options: { title: string, description: string }): void
upnext (options: Partial<EndCardOptions>): void
} }
} }

View File

@ -3,7 +3,7 @@ import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from
export class TranslationsManager { export class TranslationsManager {
private static videojsLocaleCache: { [ path: string ]: any } = {} private static videojsLocaleCache: { [ path: string ]: any } = {}
static getServerTranslations (serverUrl: string, locale: string) { static getServerTranslations (serverUrl: string, locale: string): Promise<{ [id: string]: string }> {
const path = TranslationsManager.getLocalePath(serverUrl, locale) const path = TranslationsManager.getLocalePath(serverUrl, locale)
// It is the default locale, nothing to translate // It is the default locale, nothing to translate
if (!path) return Promise.resolve(undefined) if (!path) return Promise.resolve(undefined)

View File

@ -308,8 +308,10 @@ body {
.icon { .icon {
&.icon-next { &.icon-next {
mask-image: url('#{$assets-path}/player/images/next.svg'); mask-image: url('#{$assets-path}/player/images/next.svg');
-webkit-mask-image: url('#{$assets-path}/player/images/next.svg');
background-color: white; background-color: white;
mask-size: cover; mask-size: cover;
-webkit-mask-size: cover;
transform: scale(2.2); transform: scale(2.2);
} }
} }

View File

@ -26,7 +26,7 @@ export class PeerTubeEmbedApi {
} }
private get element () { private get element () {
return this.embed.videoElement return this.embed.playerElement
} }
private constructChannel () { private constructChannel () {
@ -108,7 +108,6 @@ export class PeerTubeEmbedApi {
setInterval(() => { setInterval(() => {
const position = this.element.currentTime const position = this.element.currentTime
const volume = this.element.volume const volume = this.element.volume
const duration = this.element.duration
this.channel.notify({ this.channel.notify({
method: 'playbackStatusUpdate', method: 'playbackStatusUpdate',

View File

@ -19,10 +19,9 @@
<div id="error-content"></div> <div id="error-content"></div>
</div> </div>
<video playsinline="true" id="video-container" class="video-js vjs-peertube-skin"> <div id="video-wrapper"></div>
</video>
<div id="placeholder-preview" /> <div id="placeholder-preview"></div>
</body> </body>
</html> </html>

View File

@ -27,6 +27,11 @@ html, body {
background-color: #000; background-color: #000;
} }
#video-wrapper {
width: 100%;
height: 100%;
}
.video-js.vjs-peertube-skin { .video-js.vjs-peertube-skin {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -9,6 +9,8 @@ import {
UserRefreshToken, UserRefreshToken,
VideoCaption, VideoCaption,
VideoDetails, VideoDetails,
VideoPlaylist,
VideoPlaylistElement,
VideoStreamingPlaylistType VideoStreamingPlaylistType
} from '../../../../shared/models' } from '../../../../shared/models'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
@ -19,9 +21,10 @@ import { PeerTubeEmbedApi } from './embed-api'
type Translations = { [ id: string ]: string } type Translations = { [ id: string ]: string }
export class PeerTubeEmbed { export class PeerTubeEmbed {
videoElement: HTMLVideoElement playerElement: HTMLVideoElement
player: videojs.Player player: videojs.Player
api: PeerTubeEmbedApi = null api: PeerTubeEmbedApi = null
autoplay: boolean autoplay: boolean
controls: boolean controls: boolean
muted: boolean muted: boolean
@ -47,14 +50,24 @@ export class PeerTubeEmbed {
CLIENT_SECRET: 'client_secret' CLIENT_SECRET: 'client_secret'
} }
private translationsPromise: Promise<{ [id: string]: string }>
private configPromise: Promise<ServerConfig>
private PeertubePlayerManagerModulePromise: Promise<any>
private playlist: VideoPlaylist
private playlistElements: VideoPlaylistElement[]
private currentPlaylistElement: VideoPlaylistElement
private wrapperElement: HTMLElement
static async main () { static async main () {
const videoContainerId = 'video-container' const videoContainerId = 'video-wrapper'
const embed = new PeerTubeEmbed(videoContainerId) const embed = new PeerTubeEmbed(videoContainerId)
await embed.init() await embed.init()
} }
constructor (private videoContainerId: string) { constructor (private videoWrapperId: string) {
this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement this.wrapperElement = document.getElementById(this.videoWrapperId)
} }
getVideoUrl (id: string) { getVideoUrl (id: string) {
@ -114,6 +127,10 @@ export class PeerTubeEmbed {
}) })
} }
getPlaylistUrl (id: string) {
return window.location.origin + '/api/v1/video-playlists/' + id
}
loadVideoInfo (videoId: string): Promise<Response> { loadVideoInfo (videoId: string): Promise<Response> {
return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers }) return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers })
} }
@ -122,8 +139,17 @@ export class PeerTubeEmbed {
return fetch(this.getVideoUrl(videoId) + '/captions') return fetch(this.getVideoUrl(videoId) + '/captions')
} }
loadConfig (): Promise<Response> { loadPlaylistInfo (playlistId: string): Promise<Response> {
return fetch(this.getPlaylistUrl(playlistId))
}
loadPlaylistElements (playlistId: string): Promise<Response> {
return fetch(this.getPlaylistUrl(playlistId) + '/videos')
}
loadConfig (): Promise<ServerConfig> {
return fetch('/api/v1/config') return fetch('/api/v1/config')
.then(res => res.json())
} }
removeElement (element: HTMLElement) { removeElement (element: HTMLElement) {
@ -132,7 +158,10 @@ export class PeerTubeEmbed {
displayError (text: string, translations?: Translations) { displayError (text: string, translations?: Translations) {
// Remove video element // Remove video element
if (this.videoElement) this.removeElement(this.videoElement) if (this.playerElement) {
this.removeElement(this.playerElement)
this.playerElement = undefined
}
const translatedText = peertubeTranslate(text, translations) const translatedText = peertubeTranslate(text, translations)
const translatedSorry = peertubeTranslate('Sorry', translations) const translatedSorry = peertubeTranslate('Sorry', translations)
@ -159,6 +188,16 @@ export class PeerTubeEmbed {
this.displayError(text, translations) this.displayError(text, translations)
} }
playlistNotFound (translations?: Translations) {
const text = 'This playlist does not exist.'
this.displayError(text, translations)
}
playlistFetchError (translations?: Translations) {
const text = 'We cannot fetch the playlist. Please try again later.'
this.displayError(text, translations)
}
getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) { getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
} }
@ -218,34 +257,129 @@ export class PeerTubeEmbed {
} }
} }
private async initCore () { private async loadPlaylist (playlistId: string) {
const urlParts = window.location.pathname.split('/') const playlistPromise = this.loadPlaylistInfo(playlistId)
const videoId = urlParts[ urlParts.length - 1 ] const playlistElementsPromise = this.loadPlaylistElements(playlistId)
if (this.userTokens) this.setHeadersFromTokens() const playlistResponse = await playlistPromise
if (!playlistResponse.ok) {
const serverTranslations = await this.translationsPromise
if (playlistResponse.status === 404) {
this.playlistNotFound(serverTranslations)
return undefined
}
this.playlistFetchError(serverTranslations)
return undefined
}
return { playlistResponse, videosResponse: await playlistElementsPromise }
}
private async loadVideo (videoId: string) {
const videoPromise = this.loadVideoInfo(videoId) const videoPromise = this.loadVideoInfo(videoId)
const captionsPromise = this.loadVideoCaptions(videoId)
const configPromise = this.loadConfig()
const translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
const videoResponse = await videoPromise const videoResponse = await videoPromise
if (!videoResponse.ok) { if (!videoResponse.ok) {
const serverTranslations = await translationsPromise const serverTranslations = await this.translationsPromise
if (videoResponse.status === 404) return this.videoNotFound(serverTranslations) if (videoResponse.status === 404) {
this.videoNotFound(serverTranslations)
return this.videoFetchError(serverTranslations) return undefined
} }
const videoInfo: VideoDetails = await videoResponse.json() this.videoFetchError(serverTranslations)
this.loadPlaceholder(videoInfo) return undefined
}
const PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') const captionsPromise = this.loadVideoCaptions(videoId)
const promises = [ translationsPromise, captionsPromise, configPromise, PeertubePlayerManagerModulePromise ] return { captionsPromise, videoResponse }
const [ serverTranslations, captionsResponse, configResponse, PeertubePlayerManagerModule ] = await Promise.all(promises) }
private async buildPlaylistManager () {
const translations = await this.translationsPromise
this.player.upnext({
timeout: 10000, // 10s
headText: peertubeTranslate('Up Next', translations),
cancelText: peertubeTranslate('Cancel', translations),
suspendedText: peertubeTranslate('Autoplay is suspended', translations),
getTitle: () => this.nextVideoTitle(),
next: () => this.autoplayNext(),
condition: () => !!this.getNextPlaylistElement(),
suspended: () => false
})
}
private async autoplayNext () {
const next = this.getNextPlaylistElement()
if (!next) {
console.log('Next element not found in playlist.')
return
}
this.currentPlaylistElement = next
const res = await this.loadVideo(this.currentPlaylistElement.video.uuid)
if (res === undefined) return
return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
}
private nextVideoTitle () {
const next = this.getNextPlaylistElement()
if (!next) return ''
return next.video.name
}
private getNextPlaylistElement (position?: number): VideoPlaylistElement {
if (!position) position = this.currentPlaylistElement.position + 1
if (position > this.playlist.videosLength) {
return undefined
}
const next = this.playlistElements.find(e => e.position === position)
if (!next || !next.video) {
return this.getNextPlaylistElement(position + 1)
}
return next
}
private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
let alreadyHadPlayer = false
if (this.player) {
this.player.dispose()
alreadyHadPlayer = true
}
this.playerElement = document.createElement('video')
this.playerElement.className = 'video-js vjs-peertube-skin'
this.playerElement.setAttribute('playsinline', 'true')
this.wrapperElement.appendChild(this.playerElement)
const videoInfoPromise = videoResponse.json()
.then((videoInfo: VideoDetails) => {
if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo)
return videoInfo
})
const [ videoInfo, serverTranslations, captionsResponse, config, PeertubePlayerManagerModule ] = await Promise.all([
videoInfoPromise,
this.translationsPromise,
captionsPromise,
this.configPromise,
this.PeertubePlayerManagerModulePromise
])
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
@ -254,7 +388,8 @@ export class PeerTubeEmbed {
const options: PeertubePlayerManagerOptions = { const options: PeertubePlayerManagerOptions = {
common: { common: {
autoplay: this.autoplay, // Autoplay in playlist mode
autoplay: alreadyHadPlayer ? true : this.autoplay,
controls: this.controls, controls: this.controls,
muted: this.muted, muted: this.muted,
loop: this.loop, loop: this.loop,
@ -263,12 +398,14 @@ export class PeerTubeEmbed {
stopTime: this.stopTime, stopTime: this.stopTime,
subtitle: this.subtitle, subtitle: this.subtitle,
nextVideo: () => this.autoplayNext(),
videoCaptions, videoCaptions,
inactivityTimeout: 2500, inactivityTimeout: 2500,
videoViewUrl: this.getVideoUrl(videoId) + '/views', videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
playerElement: this.videoElement, playerElement: this.playerElement,
onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element, onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
videoDuration: videoInfo.duration, videoDuration: videoInfo.duration,
enableHotkeys: true, enableHotkeys: true,
@ -307,23 +444,58 @@ export class PeerTubeEmbed {
this.buildCSS() this.buildCSS()
await this.buildDock(videoInfo, configResponse) await this.buildDock(videoInfo, config)
this.initializeApi() this.initializeApi()
this.removePlaceholder() this.removePlaceholder()
if (this.isPlaylistEmbed()) {
await this.buildPlaylistManager()
}
}
private async initCore () {
if (this.userTokens) this.setHeadersFromTokens()
this.configPromise = this.loadConfig()
this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
let videoId: string
if (this.isPlaylistEmbed()) {
const playlistId = this.getResourceId()
const res = await this.loadPlaylist(playlistId)
if (!res) return undefined
this.playlist = await res.playlistResponse.json()
const playlistElementResult = await res.videosResponse.json()
this.playlistElements = playlistElementResult.data
this.currentPlaylistElement = this.playlistElements[0]
videoId = this.currentPlaylistElement.video.uuid
} else {
videoId = this.getResourceId()
}
const res = await this.loadVideo(videoId)
if (res === undefined) return
return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
} }
private handleError (err: Error, translations?: { [ id: string ]: string }) { private handleError (err: Error, translations?: { [ id: string ]: string }) {
if (err.message.indexOf('from xs param') !== -1) { if (err.message.indexOf('from xs param') !== -1) {
this.player.dispose() this.player.dispose()
this.videoElement = null this.playerElement = null
this.displayError('This video is not available because the remote instance is not responding.', translations) this.displayError('This video is not available because the remote instance is not responding.', translations)
return return
} }
} }
private async buildDock (videoInfo: VideoDetails, configResponse: Response) { private async buildDock (videoInfo: VideoDetails, config: ServerConfig) {
if (!this.controls) return if (!this.controls) return
// On webtorrent fallback, player may have been disposed // On webtorrent fallback, player may have been disposed
@ -331,7 +503,6 @@ export class PeerTubeEmbed {
const title = this.title ? videoInfo.name : undefined const title = this.title ? videoInfo.name : undefined
const config: ServerConfig = await configResponse.json()
const description = config.tracker.enabled && this.warningTitle const description = config.tracker.enabled && this.warningTitle
? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
: undefined : undefined
@ -373,11 +544,12 @@ export class PeerTubeEmbed {
const url = window.location.origin + video.previewPath const url = window.location.origin + video.previewPath
placeholder.style.backgroundImage = `url("${url}")` placeholder.style.backgroundImage = `url("${url}")`
placeholder.style.display = 'block'
} }
private removePlaceholder () { private removePlaceholder () {
const placeholder = this.getPlaceholderElement() const placeholder = this.getPlaceholderElement()
placeholder.parentElement.removeChild(placeholder) placeholder.style.display = 'none'
} }
private getPlaceholderElement () { private getPlaceholderElement () {
@ -387,6 +559,15 @@ export class PeerTubeEmbed {
private setHeadersFromTokens () { private setHeadersFromTokens () {
this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`) this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`)
} }
private getResourceId () {
const urlParts = window.location.pathname.split('/')
return urlParts[ urlParts.length - 1 ]
}
private isPlaylistEmbed () {
return window.location.pathname.split('/')[1] === 'video-playlists'
}
} }
PeerTubeEmbed.main() PeerTubeEmbed.main()

View File

@ -48,7 +48,9 @@ values(VIDEO_CATEGORIES)
'This video does not exist.', 'This video does not exist.',
'We cannot fetch the video. Please try again later.', 'We cannot fetch the video. Please try again later.',
'Sorry', 'Sorry',
'This video is not available because the remote instance is not responding.' 'This video is not available because the remote instance is not responding.',
'This playlist does not exist',
'We cannot fetch the playlist. Please try again later.'
]) ])
.forEach(v => { serverKeys[v] = v }) .forEach(v => { serverKeys[v] = v })

View File

@ -22,19 +22,20 @@ clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage)) clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage)) clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
const embedCSPMiddleware = CONFIG.CSP.ENABLED const embedMiddlewares = [
CONFIG.CSP.ENABLED
? embedCSP ? embedCSP
: (req: express.Request, res: express.Response, next: express.NextFunction) => next() : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
clientsRouter.use(
'/videos/embed',
embedCSPMiddleware,
(req: express.Request, res: express.Response) => { (req: express.Request, res: express.Response) => {
res.removeHeader('X-Frame-Options') res.removeHeader('X-Frame-Options')
// Don't cache HTML file since it's an index to the immutable JS/CSS files // Don't cache HTML file since it's an index to the immutable JS/CSS files
res.sendFile(embedPath, { maxAge: 0 }) res.sendFile(embedPath, { maxAge: 0 })
} }
) ]
clientsRouter.use('/videos/embed', ...embedMiddlewares)
clientsRouter.use('/video-playlists/embed', ...embedMiddlewares)
clientsRouter.use( clientsRouter.use(
'/videos/test-embed', '/videos/test-embed',
(req: express.Request, res: express.Response) => res.sendFile(testEmbedPath) (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)