]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add logic to handle playlist in embed
authorChocobozzz <me@florianbigard.com>
Tue, 4 Aug 2020 09:42:06 +0000 (11:42 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 7 Aug 2020 06:58:29 +0000 (08:58 +0200)
client/src/app/+videos/+video-watch/video-watch.component.ts
client/src/assets/player/peertube-videojs-typings.ts
client/src/assets/player/translations-manager.ts
client/src/sass/player/peertube-skin.scss
client/src/standalone/videos/embed-api.ts
client/src/standalone/videos/embed.html
client/src/standalone/videos/embed.scss
client/src/standalone/videos/embed.ts
scripts/i18n/create-custom-files.ts
server/controllers/client.ts

index bb0830d992b2c9879eddecb3aaca8e810e7c2023..dfe73d14d7d0729b19b97cbb762a40dc1b7f01fa 100644 (file)
@@ -163,6 +163,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     // Unsubscribe subscriptions
     if (this.paramsSub) this.paramsSub.unsubscribe()
     if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
     // Unsubscribe subscriptions
     if (this.paramsSub) this.paramsSub.unsubscribe()
     if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
+    if (this.configSub) this.configSub.unsubscribe()
 
     // Unbind hotkeys
     this.hotkeysService.remove(this.hotkeys)
 
     // Unbind hotkeys
     this.hotkeysService.remove(this.hotkeys)
index 9c81fd5bc4d458600a1c8662f1107f4c6e2cec3b..1506a04ac305cec3f620b549008ab96e058f8d84 100644 (file)
@@ -1,11 +1,12 @@
-import { PeerTubePlugin } from './peertube-plugin'
-import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
+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 { 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 { 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 { 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
   }
 }
 
   }
 }
 
index 631e3feba1a2ae614c6b818695cc488f03042e69..d5a09a31a71bfc71ee8c4088533a0d66a27e7220 100644 (file)
@@ -3,7 +3,7 @@ import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from
 export class TranslationsManager {
   private static videojsLocaleCache: { [ path: string ]: any } = {}
 
 export class TranslationsManager {
   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)
     // It is the default locale, nothing to translate
     if (!path) return Promise.resolve(undefined)
     const path = TranslationsManager.getLocalePath(serverUrl, locale)
     // It is the default locale, nothing to translate
     if (!path) return Promise.resolve(undefined)
index bdeff8f9a45f2ceeb5a4f3ca8a1f0358f6606f7d..2c22239a092a2c8917ed9b95530a3429c095ee1a 100644 (file)
@@ -308,8 +308,10 @@ body {
       .icon {
         &.icon-next {
           mask-image: url('#{$assets-path}/player/images/next.svg');
       .icon {
         &.icon-next {
           mask-image: url('#{$assets-path}/player/images/next.svg');
+          -webkit-mask-image: url('#{$assets-path}/player/images/next.svg');
           background-color: white;
           mask-size: cover;
           background-color: white;
           mask-size: cover;
+          -webkit-mask-size: cover;
           transform: scale(2.2);
         }
       }
           transform: scale(2.2);
         }
       }
index a9263555d839e400b6e12292a3e000eff8eafff9..efc23a1fc786c6b0f0a0d49255b3385978d3e1eb 100644 (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(() => {
       const position = this.element.currentTime
       const volume = this.element.volume
     setInterval(() => {
       const position = this.element.currentTime
       const volume = this.element.volume
-      const duration = this.element.duration
 
       this.channel.notify({
         method: 'playbackStatusUpdate',
 
       this.channel.notify({
         method: 'playbackStatusUpdate',
index 6edf71f48680f73eaaabfbdf31651abb2b5f0490..908aad940ba3d1fc604b2432bc2842d096fc0b4a 100644 (file)
       <div id="error-content"></div>
     </div>
 
       <div id="error-content"></div>
     </div>
 
-    <video playsinline="true" id="video-container" class="video-js vjs-peertube-skin">
-    </video>
+    <div id="video-wrapper"></div>
 
 
-    <div id="placeholder-preview" />
+    <div id="placeholder-preview"></div>
 
   </body>
 </html>
 
   </body>
 </html>
index 95573dabeb06b403b0fc41e805e2b67b962f0ba6..cbe6bdd012896d4e3b7204a740d35f488d4b5b93 100644 (file)
@@ -27,6 +27,11 @@ html, body {
   background-color: #000;
 }
 
   background-color: #000;
 }
 
+#video-wrapper {
+  width: 100%;
+  height: 100%;
+}
+
 .video-js.vjs-peertube-skin {
   width: 100%;
   height: 100%;
 .video-js.vjs-peertube-skin {
   width: 100%;
   height: 100%;
index 8b00be7906fa5ff02ba0f78491f82c22d30f52af..71bd04e764378dca8cc525deadf1fb2f14598562 100644 (file)
@@ -9,6 +9,8 @@ import {
   UserRefreshToken,
   VideoCaption,
   VideoDetails,
   UserRefreshToken,
   VideoCaption,
   VideoDetails,
+  VideoPlaylist,
+  VideoPlaylistElement,
   VideoStreamingPlaylistType
 } from '../../../../shared/models'
 import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
   VideoStreamingPlaylistType
 } from '../../../../shared/models'
 import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
@@ -19,9 +21,10 @@ import { PeerTubeEmbedApi } from './embed-api'
 type Translations = { [ id: string ]: string }
 
 export class PeerTubeEmbed {
 type Translations = { [ id: string ]: string }
 
 export class PeerTubeEmbed {
-  videoElement: HTMLVideoElement
+  playerElement: HTMLVideoElement
   player: videojs.Player
   api: PeerTubeEmbedApi = null
   player: videojs.Player
   api: PeerTubeEmbedApi = null
+
   autoplay: boolean
   controls: boolean
   muted: boolean
   autoplay: boolean
   controls: 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)
     await embed.init()
   }
 
     const embed = new PeerTubeEmbed(videoContainerId)
     await embed.init()
   }
 
-  constructor (private videoContainerId: string) {
-    this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
+  constructor (private videoWrapperId: string) {
+    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> {
     return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers })
   }
   loadVideoInfo (videoId: string): Promise<Response> {
     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) {
     // Remove video element
 
   displayError (text: string, translations?: Translations) {
     // 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 translatedSorry = peertubeTranslate('Sorry', translations)
 
     const translatedText = peertubeTranslate(text, 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) {
     return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
   }
   getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
     return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
   }
@@ -218,34 +257,129 @@ export class PeerTubeEmbed {
     }
   }
 
     }
   }
 
-  private async initCore () {
-    const urlParts = window.location.pathname.split('/')
-    const videoId = urlParts[ urlParts.length - 1 ]
+  private async loadPlaylist (playlistId: string) {
+    const playlistPromise = this.loadPlaylistInfo(playlistId)
+    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
 
     if (!videoResponse.ok) {
     const videoResponse = await videoPromise
 
     if (!videoResponse.ok) {
-      const serverTranslations = await translationsPromise
+      const serverTranslations = await this.translationsPromise
+
+      if (videoResponse.status === 404) {
+        this.videoNotFound(serverTranslations)
+        return undefined
+      }
+
+      this.videoFetchError(serverTranslations)
+      return undefined
+    }
+
+    const captionsPromise = this.loadVideoCaptions(videoId)
+
+    return { captionsPromise, videoResponse }
+  }
 
 
-      if (videoResponse.status === 404) return this.videoNotFound(serverTranslations)
+  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
+    })
+  }
 
 
-      return this.videoFetchError(serverTranslations)
+  private async autoplayNext () {
+    const next = this.getNextPlaylistElement()
+    if (!next) {
+      console.log('Next element not found in playlist.')
+      return
     }
 
     }
 
-    const videoInfo: VideoDetails = await videoResponse.json()
-    this.loadPlaceholder(videoInfo)
+    this.currentPlaylistElement = next
 
 
-    const PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
+    const res = await this.loadVideo(this.currentPlaylistElement.video.uuid)
+    if (res === undefined) return
+
+    return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
+  }
 
 
-    const promises = [ translationsPromise, captionsPromise, configPromise, PeertubePlayerManagerModulePromise ]
-    const [ serverTranslations, captionsResponse, configResponse, PeertubePlayerManagerModule ] = await Promise.all(promises)
+  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 videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
 
     const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
     const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
@@ -254,7 +388,8 @@ export class PeerTubeEmbed {
 
     const options: PeertubePlayerManagerOptions = {
       common: {
 
     const options: PeertubePlayerManagerOptions = {
       common: {
-        autoplay: this.autoplay,
+        // Autoplay in playlist mode
+        autoplay: alreadyHadPlayer ? true : this.autoplay,
         controls: this.controls,
         muted: this.muted,
         loop: this.loop,
         controls: this.controls,
         muted: this.muted,
         loop: this.loop,
@@ -263,12 +398,14 @@ export class PeerTubeEmbed {
         stopTime: this.stopTime,
         subtitle: this.subtitle,
 
         stopTime: this.stopTime,
         subtitle: this.subtitle,
 
+        nextVideo: () => this.autoplayNext(),
+
         videoCaptions,
         inactivityTimeout: 2500,
         videoCaptions,
         inactivityTimeout: 2500,
-        videoViewUrl: this.getVideoUrl(videoId) + '/views',
+        videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
 
 
-        playerElement: this.videoElement,
-        onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element,
+        playerElement: this.playerElement,
+        onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
 
         videoDuration: videoInfo.duration,
         enableHotkeys: true,
 
         videoDuration: videoInfo.duration,
         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.removePlaceholder()
 
     this.initializeApi()
 
     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 }) {
     if (err.message.indexOf('from xs param') !== -1) {
       this.player.dispose()
   }
 
   private handleError (err: Error, translations?: { [ id: string ]: string }) {
     if (err.message.indexOf('from xs param') !== -1) {
       this.player.dispose()
-      this.videoElement = null
+      this.playerElement = null
       this.displayError('This video is not available because the remote instance is not responding.', translations)
       return
     }
   }
 
       this.displayError('This video is not available because the remote instance is not responding.', translations)
       return
     }
   }
 
-  private async buildDock (videoInfo: VideoDetails, configResponse: Response) {
+  private async buildDock (videoInfo: VideoDetails, config: ServerConfig) {
     if (!this.controls) return
 
     // On webtorrent fallback, player may have been disposed
     if (!this.controls) return
 
     // 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
       ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
       : undefined
     const description = config.tracker.enabled && this.warningTitle
       ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
       : undefined
@@ -373,11 +544,12 @@ export class PeerTubeEmbed {
 
     const url = window.location.origin + video.previewPath
     placeholder.style.backgroundImage = `url("${url}")`
 
     const url = window.location.origin + video.previewPath
     placeholder.style.backgroundImage = `url("${url}")`
+    placeholder.style.display = 'block'
   }
 
   private removePlaceholder () {
     const placeholder = this.getPlaceholderElement()
   }
 
   private removePlaceholder () {
     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 () {
     this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`)
   }
   private setHeadersFromTokens () {
     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()
index 2862b230e269445ca6c0f0f1650c2d1efbc9a9c2..298eda71b4fd8c53f6196530ca0d072d296a3e3d 100755 (executable)
@@ -48,7 +48,9 @@ values(VIDEO_CATEGORIES)
     'This video does not exist.',
     'We cannot fetch the video. Please try again later.',
     'Sorry',
     'This video does not exist.',
     'We cannot fetch the video. Please try again later.',
     '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 })
 
index 7c80820f45d439a1029c382fd9dd6c5630b933c0..b97c935a54a401ac26b50cf3dcf9a59e2e58a438 100644 (file)
@@ -22,19 +22,20 @@ clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
 clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
 clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
 
 clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
 clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
 
-const embedCSPMiddleware = CONFIG.CSP.ENABLED
-  ? embedCSP
-  : (req: express.Request, res: express.Response, next: express.NextFunction) => next()
+const embedMiddlewares = [
+  CONFIG.CSP.ENABLED
+    ? embedCSP
+    : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
 
 
-clientsRouter.use(
-  '/videos/embed',
-  embedCSPMiddleware,
   (req: express.Request, res: express.Response) => {
     res.removeHeader('X-Frame-Options')
     // Don't cache HTML file since it's an index to the immutable JS/CSS files
     res.sendFile(embedPath, { maxAge: 0 })
   }
   (req: express.Request, res: express.Response) => {
     res.removeHeader('X-Frame-Options')
     // Don't cache HTML file since it's an index to the immutable JS/CSS files
     res.sendFile(embedPath, { maxAge: 0 })
   }
-)
+]
+
+clientsRouter.use('/videos/embed', ...embedMiddlewares)
+clientsRouter.use('/video-playlists/embed', ...embedMiddlewares)
 clientsRouter.use(
   '/videos/test-embed',
   (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)
 clientsRouter.use(
   '/videos/test-embed',
   (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)