Handle subtitles in player

pull/777/merge
Chocobozzz 2018-07-13 18:21:19 +02:00
parent 40e87e9ecc
commit 16f7022b06
9 changed files with 125 additions and 29 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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
}
}

View File

@ -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')

View File

@ -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
}

View File

@ -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()

View File

@ -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;
}
}
}
}

View File

@ -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()
})
}

View File

@ -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',