servicesTwitterUsername: this.customConfig.services.twitter.username,
servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
cachePreviewsSize: this.customConfig.cache.previews.size,
+ cacheCaptionsSize: this.customConfig.cache.captions.size,
signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit,
adminEmail: this.customConfig.admin.email,
import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
import { MetaService } from '@ngx-meta/core'
import { NotificationsService } from 'angular2-notifications'
-import { Subscription } from 'rxjs'
+import { forkJoin, Subscription } from 'rxjs'
import * as videojs from 'video.js'
import 'videojs-hotkeys'
import * as WebTorrent from 'webtorrent'
-import { UserVideoRateType, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared'
+import { ResultList, UserVideoRateType, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared'
import '../../../assets/player/peertube-videojs-plugin'
import { AuthService, ConfirmService } from '../../core'
import { RestExtractor, VideoBlacklistService } from '../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { environment } from '../../../environments/environment'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
+import { VideoCaptionService } from '@app/shared/video-caption'
+import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model'
+import { VideoJSCaption } from '../../../assets/player/peertube-videojs-typings'
@Component({
selector: 'my-video-watch',
private markdownService: MarkdownService,
private zone: NgZone,
private redirectService: RedirectService,
+ private videoCaptionService: VideoCaptionService,
private i18n: I18n,
@Inject(LOCALE_ID) private localeId: string
) {}
if (this.player) this.player.pause()
// Video did change
- this.videoService
- .getVideo(uuid)
- .pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
- .subscribe(video => {
- const startTime = this.route.snapshot.queryParams.start
- this.onVideoFetched(video, startTime)
- .catch(err => this.handleError(err))
- })
+ forkJoin(
+ this.videoService.getVideo(uuid),
+ this.videoCaptionService.listCaptions(uuid)
+ )
+ .pipe(
+ catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
+ )
+ .subscribe(([ video, captionsResult ]) => {
+ const startTime = this.route.snapshot.queryParams.start
+ this.onVideoFetched(video, captionsResult.data, startTime)
+ .catch(err => this.handleError(err))
+ })
})
}
)
}
- private async onVideoFetched (video: VideoDetails, startTime = 0) {
+ private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTime = 0) {
this.video = video
// Re init attributes
this.playerElement.setAttribute('playsinline', 'true')
playerElementWrapper.appendChild(this.playerElement)
+ const playerCaptions = videoCaptions.map(c => ({
+ label: c.language.label,
+ language: c.language.id,
+ src: environment.apiUrl + c.captionPath
+ }))
+
const videojsOptions = getVideojsOptions({
autoplay: this.isAutoplay(),
inactivityTimeout: 2500,
videoFiles: this.video.files,
+ videoCaptions: playerCaptions,
playerElement: this.playerElement,
videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
videoDuration: this.video.duration,
import './peertube-videojs-plugin'
import './peertube-load-progress-bar'
import './theater-button'
-import { videojsUntyped } from './peertube-videojs-typings'
+import { VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
+// Change Captions to Subtitles/CC
+videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
+// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
+videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
function getVideojsOptions (options: {
autoplay: boolean,
poster: string,
startTime: number
theaterMode: boolean,
+ videoCaptions: VideoJSCaption[],
controls?: boolean,
muted?: boolean,
loop?: boolean
}) {
const videojsOptions = {
+ // We don't use text track settings for now
+ textTrackSettings: false,
controls: options.controls !== undefined ? options.controls : true,
muted: options.controls !== undefined ? options.muted : false,
loop: options.loop !== undefined ? options.loop : false,
plugins: {
peertube: {
autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
+ videoCaptions: options.videoCaptions,
videoFiles: options.videoFiles,
playerElement: options.playerElement,
videoViewUrl: options.videoViewUrl,
function getControlBarChildren (options: {
peertubeLink: boolean
- theaterMode: boolean
+ theaterMode: boolean,
+ videoCaptions: VideoJSCaption[]
}) {
+ const settingEntries = []
+
+ // Keep an order
+ settingEntries.push('playbackRateMenuButton')
+ if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton')
+ settingEntries.push('resolutionMenuButton')
+
const children = {
'playToggle': {},
'currentTimeDisplay': {},
setup: {
maxHeightOffset: 40
},
- entries: [
- 'resolutionMenuButton',
- 'playbackRateMenuButton'
- ]
+ entries: settingEntries
}
}
import { VideoFile } from '../../../../shared/models/videos/video.model'
import { renderVideo } from './video-renderer'
import './settings-menu-button'
-import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { PeertubePluginOptions, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
import { isMobile, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
import * as CacheChunkStore from 'cache-chunk-store'
import { PeertubeChunkStore } from './peertube-chunk-store'
private player: any
private currentVideoFile: VideoFile
private torrent: WebTorrent.Torrent
+ private videoCaptions: VideoJSCaption[]
private renderer
private fakeRenderer
private autoResolution = true
this.videoFiles = options.videoFiles
this.videoViewUrl = options.videoViewUrl
this.videoDuration = options.videoDuration
+ this.videoCaptions = options.videoCaptions
this.savePlayerSrcFunction = this.player.src
// Hack to "simulate" src link in video.js >= 6
this.initSmoothProgressBar()
+ this.initCaptions()
+
this.alterInactivity()
if (this.autoplay === true) {
this.player.options_.inactivityTimeout = 0
}
const enableInactivity = () => {
- this.player.options_.inactivityTimeout = saveInactivityTimeout
+ // this.player.options_.inactivityTimeout = saveInactivityTimeout
}
const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog')
}
}
+ private initCaptions () {
+ for (const caption of this.videoCaptions) {
+ this.player.addRemoteTextTrack({
+ kind: 'captions',
+ label: caption.label,
+ language: caption.language,
+ id: caption.language,
+ src: caption.src
+ }, false)
+ }
+ }
+
// Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
private initSmoothProgressBar () {
const SeekBar = videojsUntyped.getComponent('SeekBar')
registerComponent (name: string, obj: any)
}
+type VideoJSCaption = {
+ label: string
+ language: string
+ src: string
+}
+
type PeertubePluginOptions = {
videoFiles: VideoFile[]
playerElement: HTMLVideoElement
videoViewUrl: string
videoDuration: number
startTime: number
- autoplay: boolean
+ autoplay: boolean,
+ videoCaptions: VideoJSCaption[]
}
// videojs typings don't have some method we need
export {
VideoJSComponentInterface,
PeertubePluginOptions,
- videojsUntyped
+ videojsUntyped,
+ VideoJSCaption
}
throw new Error(`Component ${subMenuName} does not exist`)
}
this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this)
+ const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
+ this.settingsSubMenuEl_.className += ' ' + subMenuClass
this.eventHandlers()
.vjs-settings-sub-menu-title {
display: table-cell;
padding: 0 5px;
+ text-transform: capitalize;
}
.vjs-settings-sub-menu-title {
.vjs-menu-item {
outline: 0;
font-weight: $font-semibold;
-
- padding: 5px 8px;
text-align: right;
+ padding: 5px 8px;
&.vjs-back-button {
background-color: inherit;
- padding: 8px 8px 13px 8px;
+ padding: 8px 8px 13px 12px;
margin-bottom: 5px;
border-bottom: 1px solid grey;
+ text-align: left;
&::before {
@include chevron-left(9px, 2px);
}
}
}
+
+ // Special captions case
+ // Bigger caption button
+ &.vjs-captions-button {
+ width: 200px;
+
+ .vjs-menu-item {
+ text-align: left;
+
+ .vjs-menu-item-text {
+ margin-left: 25px;
+ text-transform: capitalize;
+ }
+ }
+ }
+
+ .vjs-menu {
+ width: inherit;
+ }
}
}
}
import * as vjs from 'video.js'
import * as Channel from 'jschannel'
-import { VideoDetails } from '../../../../shared'
+import { ResultList, VideoDetails } from '../../../../shared'
import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player'
import { PeerTubeResolution } from '../player/definitions'
+import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
+import { VideoCaption } from '../../../../shared/models/videos/video-caption.model'
/**
* Embed API exposes control of the embed player to the outside world via
return fetch(this.getVideoUrl(videoId))
}
+ loadVideoCaptions (videoId: string): Promise<Response> {
+ return fetch(this.getVideoUrl(videoId) + '/captions')
+ }
+
removeElement (element: HTMLElement) {
element.parentElement.removeChild(element)
}
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[ 0 ]
await loadLocale(window.location.origin, vjs, navigator.language)
- let response = await this.loadVideoInfo(videoId)
+ const [ videoResponse, captionsResponse ] = await Promise.all([
+ this.loadVideoInfo(videoId),
+ this.loadVideoCaptions(videoId)
+ ])
- if (!response.ok) {
- if (response.status === 404) return this.videoNotFound(this.videoElement)
+ if (!videoResponse.ok) {
+ if (videoResponse.status === 404) return this.videoNotFound(this.videoElement)
return this.videoFetchError(this.videoElement)
}
- const videoInfo: VideoDetails = await response.json()
+ const videoInfo: VideoDetails = await videoResponse.json()
+ let videoCaptions: VideoJSCaption[] = []
+ if (captionsResponse.ok) {
+ const { data } = (await captionsResponse.json()) as ResultList<VideoCaption>
+ videoCaptions = data.map(c => ({
+ label: c.language.label,
+ language: c.language.id,
+ src: window.location.origin + c.captionPath
+ }))
+ }
this.loadParams()
loop: this.loop,
startTime: this.startTime,
+ videoCaptions,
inactivityTimeout: 1500,
videoViewUrl: this.getVideoUrl(videoId) + '/views',
playerElement: this.videoElement,
}
addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
+
this.initializeApi()
})
}
'Quality': 'Quality',
'Auto': 'Auto',
'Speed': 'Speed',
+ 'Subtitles/CC': 'Subtitles/CC',
'peers': 'peers',
'Go to the video page': 'Go to the video page',
'Settings': 'Settings',