mirror of https://github.com/Chocobozzz/PeerTube
Handle subtitles in player
parent
40e87e9ecc
commit
16f7022b06
|
@ -213,6 +213,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
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,
|
||||
|
|
|
@ -6,11 +6,11 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
|
|||
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'
|
||||
|
@ -26,6 +26,9 @@ import { ServerService } from '@app/core'
|
|||
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',
|
||||
|
@ -74,6 +77,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
private markdownService: MarkdownService,
|
||||
private zone: NgZone,
|
||||
private redirectService: RedirectService,
|
||||
private videoCaptionService: VideoCaptionService,
|
||||
private i18n: I18n,
|
||||
@Inject(LOCALE_ID) private localeId: string
|
||||
) {}
|
||||
|
@ -109,14 +113,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
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))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -331,7 +339,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
)
|
||||
}
|
||||
|
||||
private async onVideoFetched (video: VideoDetails, startTime = 0) {
|
||||
private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTime = 0) {
|
||||
this.video = video
|
||||
|
||||
// Re init attributes
|
||||
|
@ -358,10 +366,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
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,
|
||||
|
|
|
@ -11,12 +11,16 @@ import './webtorrent-info-button'
|
|||
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,
|
||||
|
@ -30,11 +34,14 @@ function getVideojsOptions (options: {
|
|||
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,
|
||||
|
@ -45,6 +52,7 @@ function getVideojsOptions (options: {
|
|||
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,
|
||||
|
@ -71,8 +79,16 @@ function getVideojsOptions (options: {
|
|||
|
||||
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': {},
|
||||
|
@ -102,10 +118,7 @@ function getControlBarChildren (options: {
|
|||
setup: {
|
||||
maxHeightOffset: 40
|
||||
},
|
||||
entries: [
|
||||
'resolutionMenuButton',
|
||||
'playbackRateMenuButton'
|
||||
]
|
||||
entries: settingEntries
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as WebTorrent from 'webtorrent'
|
|||
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'
|
||||
|
@ -54,6 +54,7 @@ class PeerTubePlugin extends Plugin {
|
|||
private player: any
|
||||
private currentVideoFile: VideoFile
|
||||
private torrent: WebTorrent.Torrent
|
||||
private videoCaptions: VideoJSCaption[]
|
||||
private renderer
|
||||
private fakeRenderer
|
||||
private autoResolution = true
|
||||
|
@ -79,6 +80,7 @@ class PeerTubePlugin extends Plugin {
|
|||
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
|
||||
|
@ -421,6 +423,8 @@ class PeerTubePlugin extends Plugin {
|
|||
|
||||
this.initSmoothProgressBar()
|
||||
|
||||
this.initCaptions()
|
||||
|
||||
this.alterInactivity()
|
||||
|
||||
if (this.autoplay === true) {
|
||||
|
@ -581,7 +585,7 @@ class PeerTubePlugin extends Plugin {
|
|||
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')
|
||||
|
@ -611,6 +615,18 @@ class PeerTubePlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
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')
|
||||
|
|
|
@ -16,13 +16,20 @@ interface VideoJSComponentInterface {
|
|||
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
|
||||
|
@ -31,5 +38,6 @@ const videojsUntyped = videojs as any
|
|||
export {
|
||||
VideoJSComponentInterface,
|
||||
PeertubePluginOptions,
|
||||
videojsUntyped
|
||||
videojsUntyped,
|
||||
VideoJSCaption
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ class SettingsMenuItem extends MenuItem {
|
|||
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()
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ $setting-transition-easing: ease-out;
|
|||
.vjs-settings-sub-menu-title {
|
||||
display: table-cell;
|
||||
padding: 0 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.vjs-settings-sub-menu-title {
|
||||
|
@ -141,15 +142,15 @@ $setting-transition-easing: ease-out;
|
|||
.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);
|
||||
|
@ -174,6 +175,25 @@ $setting-transition-easing: ease-out;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,11 @@ import 'whatwg-fetch'
|
|||
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
|
||||
|
@ -178,6 +180,10 @@ class PeerTubeEmbed {
|
|||
return fetch(this.getVideoUrl(videoId))
|
||||
}
|
||||
|
||||
loadVideoCaptions (videoId: string): Promise<Response> {
|
||||
return fetch(this.getVideoUrl(videoId) + '/captions')
|
||||
}
|
||||
|
||||
removeElement (element: HTMLElement) {
|
||||
element.parentElement.removeChild(element)
|
||||
}
|
||||
|
@ -254,15 +260,27 @@ class PeerTubeEmbed {
|
|||
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()
|
||||
|
||||
|
@ -273,6 +291,7 @@ class PeerTubeEmbed {
|
|||
loop: this.loop,
|
||||
startTime: this.startTime,
|
||||
|
||||
videoCaptions,
|
||||
inactivityTimeout: 1500,
|
||||
videoViewUrl: this.getVideoUrl(videoId) + '/views',
|
||||
playerElement: this.videoElement,
|
||||
|
@ -297,6 +316,7 @@ class PeerTubeEmbed {
|
|||
}
|
||||
|
||||
addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
|
||||
|
||||
this.initializeApi()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ const playerKeys = {
|
|||
'Quality': 'Quality',
|
||||
'Auto': 'Auto',
|
||||
'Speed': 'Speed',
|
||||
'Subtitles/CC': 'Subtitles/CC',
|
||||
'peers': 'peers',
|
||||
'Go to the video page': 'Go to the video page',
|
||||
'Settings': 'Settings',
|
||||
|
|
Loading…
Reference in New Issue