Fast forward on HLS decode error

This commit is contained in:
Chocobozzz 2022-02-02 11:16:23 +01:00
parent b25fdc73fd
commit c4207f978e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 610 additions and 549 deletions

View File

@ -33,7 +33,6 @@ import {
VideoPrivacy,
VideoState
} from '@shared/models'
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
import {
CustomizationOptions,
P2PMediaLoaderOptions,
@ -41,7 +40,8 @@ import {
PeertubePlayerManagerOptions,
PlayerMode,
videojs
} from '../../../assets/player/peertube-player-manager'
} from '../../../assets/player'
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
import { environment } from '../../../environments/environment'
import { VideoWatchPlaylistComponent } from './shared'
@ -612,7 +612,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoCaptions: playerCaptions,
videoShortUUID: video.shortUUID,
videoUUID: video.uuid
videoUUID: video.uuid,
errorNotifier: (message: string) => this.notifier.error(message)
},
webtorrent: {

View File

@ -0,0 +1,2 @@
export * from './peertube-player-manager'
export * from './peertube-player-options-builder'

View File

@ -174,6 +174,12 @@ class Html5Hlsjs {
dispose () {
this.videoElement.removeEventListener('play', this.handlers.play)
// FIXME: https://github.com/video-dev/hls.js/issues/4092
const untypedHLS = this.hls as any
untypedHLS.log = untypedHLS.warn = () => {
// empty
}
this.hls.destroy()
}

View File

@ -24,28 +24,12 @@ import './mobile/peertube-mobile-plugin'
import './mobile/peertube-mobile-buttons'
import './hotkeys/peertube-hotkeys-plugin'
import videojs from 'video.js'
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
import { isDefaultLocale } from '@shared/core-utils/i18n'
import { VideoFile } from '@shared/models'
import { copyToClipboard } from '../../root-helpers/utils'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
import { getAverageBandwidthInStore, saveAverageBandwidth } from './peertube-player-local-storage'
import {
NextPreviousVideoButtonOptions,
P2PMediaLoaderPluginOptions,
PeerTubeLinkButtonOptions,
PlayerNetworkInfo,
PlaylistPluginOptions,
UserWatching,
VideoJSCaption,
VideoJSPluginOptions
} from './peertube-videojs-typings'
import { saveAverageBandwidth } from './peertube-player-local-storage'
import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder'
import { PlayerNetworkInfo } from './peertube-videojs-typings'
import { TranslationsManager } from './translations-manager'
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isMobile, isSafari } from './utils'
import { isMobile } from './utils'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
@ -56,112 +40,49 @@ 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)
CaptionsButton.prototype.label_ = ' '
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
export type WebtorrentOptions = {
videoFiles: VideoFile[]
}
export type P2PMediaLoaderOptions = {
playlistUrl: string
segmentsSha256Url: string
trackerAnnounce: string[]
redundancyBaseUrls: string[]
videoFiles: VideoFile[]
}
export interface CustomizationOptions {
startTime: number | string
stopTime: number | string
controls?: boolean
muted?: boolean
loop?: boolean
subtitle?: string
resume?: string
peertubeLink: boolean
}
export interface CommonOptions extends CustomizationOptions {
playerElement: HTMLVideoElement
onPlayerElementChange: (element: HTMLVideoElement) => void
autoplay: boolean
p2pEnabled: boolean
nextVideo?: () => void
hasNextVideo?: () => boolean
previousVideo?: () => void
hasPreviousVideo?: () => boolean
playlist?: PlaylistPluginOptions
videoDuration: number
enableHotkeys: boolean
inactivityTimeout: number
poster: string
theaterButton: boolean
captions: boolean
videoViewUrl: string
embedUrl: string
embedTitle: string
isLive: boolean
language?: string
videoCaptions: VideoJSCaption[]
videoUUID: string
videoShortUUID: string
userWatching?: UserWatching
serverUrl: string
}
export type PeertubePlayerManagerOptions = {
common: CommonOptions
webtorrent: WebtorrentOptions
p2pMediaLoader?: P2PMediaLoaderOptions
pluginsManager: PluginsManager
}
export class PeertubePlayerManager {
private static playerElementClassName: string
private static onPlayerChange: (player: videojs.Player) => void
private static alreadyPlayed = false
private static pluginsManager: PluginsManager
private static videojsDecodeErrors = 0
private static p2pMediaLoaderModule: any
static initState () {
PeertubePlayerManager.alreadyPlayed = false
this.alreadyPlayed = false
}
static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
this.pluginsManager = options.pluginsManager
let p2pMediaLoader: any
this.onPlayerChange = onPlayerChange
this.playerElementClassName = options.common.playerElement.className
if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
if (mode === 'p2p-media-loader') {
[ p2pMediaLoader ] = await Promise.all([
const [ p2pMediaLoaderModule ] = await Promise.all([
import('@peertube/p2p-media-loader-hlsjs'),
import('./p2p-media-loader/p2p-media-loader-plugin')
])
this.p2pMediaLoaderModule = p2pMediaLoaderModule
}
const videojsOptions = await this.getVideojsOptions(mode, options, p2pMediaLoader)
await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
return this.buildPlayer(mode, options)
}
private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> {
const videojsOptionsBuilder = new PeertubePlayerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
const videojsOptions = await this.pluginsManager.runHook(
'filter:internal.player.videojs.options.result',
videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed)
)
const self = this
return new Promise(res => {
videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
@ -169,27 +90,24 @@ export class PeertubePlayerManager {
let alreadyFallback = false
player.tech(true).one('error', () => {
if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
const handleError = () => {
if (alreadyFallback) return
alreadyFallback = true
})
player.one('error', () => {
if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
alreadyFallback = true
})
if (mode === 'p2p-media-loader') {
self.tryToRecoverHLSError(player.error(), player, options)
} else {
self.maybeFallbackToWebTorrent(mode, player, options)
}
}
player.one('error', () => handleError())
player.one('play', () => {
PeertubePlayerManager.alreadyPlayed = true
self.alreadyPlayed = true
})
self.addContextMenu({
mode,
player,
videoShortUUID: options.common.videoShortUUID,
videoEmbedUrl: options.common.embedUrl,
videoEmbedTitle: options.common.embedTitle
})
self.addContextMenu(videojsOptionsBuilder, player, options.common)
if (isMobile()) player.peertubeMobile()
if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin()
@ -214,437 +132,77 @@ export class PeertubePlayerManager {
})
}
private static async maybeFallbackToWebTorrent (currentMode: PlayerMode, player: any, options: PeertubePlayerManagerOptions) {
if (currentMode === 'webtorrent') return
private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) {
if (err.code === 3) { // Decode error
// Display a notification to user
if (this.videojsDecodeErrors === 0) {
options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.'))
}
if (this.videojsDecodeErrors === 20) {
this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
return
}
console.log('Fast forwarding HLS to recover from an error.')
this.videojsDecodeErrors++
options.common.startTime = currentPlayer.currentTime() + 2
options.common.autoplay = true
this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
const newPlayer = await this.buildPlayer('p2p-media-loader', options)
this.onPlayerChange(newPlayer)
} else {
this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
}
}
private static async maybeFallbackToWebTorrent (
currentMode: PlayerMode,
currentPlayer: videojs.Player,
options: PeertubePlayerManagerOptions
) {
if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') {
currentPlayer.peertube().displayFatalError()
return
}
console.log('Fallback to webtorrent.')
this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
await import('./webtorrent/webtorrent-plugin')
const newPlayer = await this.buildPlayer('webtorrent', options)
this.onPlayerChange(newPlayer)
}
private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) {
const newVideoElement = document.createElement('video')
newVideoElement.className = this.playerElementClassName
// VideoJS wraps our video element inside a div
let currentParentPlayerElement = options.common.playerElement.parentNode
let currentParentPlayerElement = commonOptions.playerElement.parentNode
// Fix on IOS, don't ask me why
if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(options.common.playerElement.id).parentNode
if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode
currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
options.common.playerElement = newVideoElement
options.common.onPlayerElementChange(newVideoElement)
commonOptions.playerElement = newVideoElement
commonOptions.onPlayerElementChange(newVideoElement)
player.dispose()
await import('./webtorrent/webtorrent-plugin')
const mode = 'webtorrent'
const videojsOptions = await this.getVideojsOptions(mode, options)
const self = this
videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
const player = this
self.addContextMenu({
mode,
player,
videoShortUUID: options.common.videoShortUUID,
videoEmbedUrl: options.common.embedUrl,
videoEmbedTitle: options.common.embedTitle
})
PeertubePlayerManager.onPlayerChange(player)
})
return newVideoElement
}
private static async getVideojsOptions (
mode: PlayerMode,
options: PeertubePlayerManagerOptions,
p2pMediaLoaderModule?: any
): Promise<videojs.PlayerOptions> {
const commonOptions = options.common
const isHLS = mode === 'p2p-media-loader'
private static addContextMenu (optionsBuilder: PeertubePlayerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
let autoplay = this.getAutoPlayValue(commonOptions.autoplay)
const html5 = {
preloadTextTracks: false
}
const plugins: VideoJSPluginOptions = {
peertube: {
mode,
autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
videoViewUrl: commonOptions.videoViewUrl,
videoDuration: commonOptions.videoDuration,
userWatching: commonOptions.userWatching,
subtitle: commonOptions.subtitle,
videoCaptions: commonOptions.videoCaptions,
stopTime: commonOptions.stopTime,
isLive: commonOptions.isLive,
videoUUID: commonOptions.videoUUID
}
}
if (commonOptions.playlist) {
plugins.playlist = commonOptions.playlist
}
if (isHLS) {
const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
Object.assign(html5, hlsjs.html5)
}
if (mode === 'webtorrent') {
PeertubePlayerManager.addWebTorrentOptions(plugins, options)
// WebTorrent plugin handles autoplay, because we do some hackish stuff in there
autoplay = false
}
const videojsOptions = {
html5,
// We don't use text track settings for now
textTrackSettings: false as any, // FIXME: typings
controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
muted: commonOptions.muted !== undefined
? commonOptions.muted
: undefined, // Undefined so the player knows it has to check the local storage
autoplay: this.getAutoPlayValue(autoplay),
poster: commonOptions.poster,
inactivityTimeout: commonOptions.inactivityTimeout,
playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
plugins,
controlBar: {
children: this.getControlBarChildren(mode, {
videoShortUUID: commonOptions.videoShortUUID,
p2pEnabled: commonOptions.p2pEnabled,
captions: commonOptions.captions,
peertubeLink: commonOptions.peertubeLink,
theaterButton: commonOptions.theaterButton,
nextVideo: commonOptions.nextVideo,
hasNextVideo: commonOptions.hasNextVideo,
previousVideo: commonOptions.previousVideo,
hasPreviousVideo: commonOptions.hasPreviousVideo
}) as any // FIXME: typings
}
}
if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
Object.assign(videojsOptions, { language: commonOptions.language })
}
return this.pluginsManager.runHook('filter:internal.player.videojs.options.result', videojsOptions)
}
private static addP2PMediaLoaderOptions (
plugins: VideoJSPluginOptions,
options: PeertubePlayerManagerOptions,
p2pMediaLoaderModule: any
) {
const p2pMediaLoaderOptions = options.p2pMediaLoader
const commonOptions = options.common
const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
.filter(t => t.startsWith('ws'))
const redundancyUrlManager = new RedundancyUrlManager(options.p2pMediaLoader.redundancyBaseUrls)
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
redundancyUrlManager,
type: 'application/x-mpegURL',
startTime: commonOptions.startTime,
src: p2pMediaLoaderOptions.playlistUrl
}
let consumeOnly = false
if ((navigator as any)?.connection?.type === 'cellular') {
console.log('We are on a cellular connection: disabling seeding.')
consumeOnly = true
}
const p2pMediaLoaderConfig: HlsJsEngineSettings = {
loader: {
trackerAnnounce,
segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url, options.common.isLive),
rtcConfig: getRtcConfig(),
requiredSegmentsPriority: 1,
simultaneousHttpDownloads: 1,
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
useP2P: commonOptions.p2pEnabled,
consumeOnly
},
segments: {
swarmId: p2pMediaLoaderOptions.playlistUrl
}
}
const hlsjs = {
levelLabelHandler: (level: { height: number, width: number }) => {
const resolution = Math.min(level.height || 0, level.width || 0)
const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
// We don't have files for live videos
if (!file) return level.height
let label = file.resolution.label
if (file.fps >= 50) label += file.fps
return label
},
html5: {
hlsjsConfig: this.getHLSOptions(p2pMediaLoaderModule, p2pMediaLoaderConfig)
}
}
const toAssign = { p2pMediaLoader, hlsjs }
Object.assign(plugins, toAssign)
return toAssign
}
private static getHLSOptions (p2pMediaLoaderModule: any, p2pMediaLoaderConfig: HlsJsEngineSettings) {
const base = {
capLevelToPlayerSize: true,
autoStartLoad: false,
liveSyncDurationCount: 5,
loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
}
const averageBandwidth = getAverageBandwidthInStore()
if (!averageBandwidth) return base
return {
...base,
abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
startLevel: -1,
testBandwidth: false,
debug: false
}
}
private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) {
const commonOptions = options.common
const webtorrentOptions = options.webtorrent
const p2pMediaLoaderOptions = options.p2pMediaLoader
const autoplay = this.getAutoPlayValue(commonOptions.autoplay) === 'play'
const webtorrent = {
autoplay,
playerRefusedP2P: commonOptions.p2pEnabled === false,
videoDuration: commonOptions.videoDuration,
playerElement: commonOptions.playerElement,
videoFiles: webtorrentOptions.videoFiles.length !== 0
? webtorrentOptions.videoFiles
// The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
: p2pMediaLoaderOptions?.videoFiles || [],
startTime: commonOptions.startTime
}
Object.assign(plugins, { webtorrent })
}
private static getControlBarChildren (mode: PlayerMode, options: {
p2pEnabled: boolean
videoShortUUID: string
peertubeLink: boolean
theaterButton: boolean
captions: boolean
nextVideo?: () => void
hasNextVideo?: () => boolean
previousVideo?: () => void
hasPreviousVideo?: () => boolean
}) {
const settingEntries = []
const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
// Keep an order
settingEntries.push('playbackRateMenuButton')
if (options.captions === true) settingEntries.push('captionsButton')
settingEntries.push('resolutionMenuButton')
const children = {}
if (options.previousVideo) {
const buttonOptions: NextPreviousVideoButtonOptions = {
type: 'previous',
handler: options.previousVideo,
isDisabled: () => {
if (!options.hasPreviousVideo) return false
return !options.hasPreviousVideo()
}
}
Object.assign(children, {
previousVideoButton: buttonOptions
})
}
Object.assign(children, { playToggle: {} })
if (options.nextVideo) {
const buttonOptions: NextPreviousVideoButtonOptions = {
type: 'next',
handler: options.nextVideo,
isDisabled: () => {
if (!options.hasNextVideo) return false
return !options.hasNextVideo()
}
}
Object.assign(children, {
nextVideoButton: buttonOptions
})
}
Object.assign(children, {
currentTimeDisplay: {},
timeDivider: {},
durationDisplay: {},
liveDisplay: {},
flexibleWidthSpacer: {},
progressControl: {
children: {
seekBar: {
children: {
[loadProgressBar]: {},
mouseTimeDisplay: {},
playProgressBar: {}
}
}
}
},
p2PInfoButton: {
p2pEnabled: options.p2pEnabled
},
muteToggle: {},
volumeControl: {},
settingsButton: {
setup: {
maxHeightOffset: 40
},
entries: settingEntries
}
})
if (options.peertubeLink === true) {
Object.assign(children, {
peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
})
}
if (options.theaterButton === true) {
Object.assign(children, {
theaterButton: {}
})
}
Object.assign(children, {
fullscreenToggle: {}
})
return children
}
private static addContextMenu (options: {
mode: PlayerMode
player: videojs.Player
videoShortUUID: string
videoEmbedUrl: string
videoEmbedTitle: string
}) {
const { mode, player, videoEmbedTitle, videoEmbedUrl, videoShortUUID } = options
const content = () => {
const isLoopEnabled = player.options_['loop']
const items = [
{
icon: 'repeat',
label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
listener: function () {
player.options_['loop'] = !isLoopEnabled
}
},
{
label: player.localize('Copy the video URL'),
listener: function () {
copyToClipboard(buildVideoLink({ shortUUID: videoShortUUID }))
}
},
{
label: player.localize('Copy the video URL at the current time'),
listener: function (this: videojs.Player) {
const url = buildVideoLink({ shortUUID: videoShortUUID })
copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
}
},
{
icon: 'code',
label: player.localize('Copy embed code'),
listener: () => {
copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle))
}
}
]
if (mode === 'webtorrent') {
items.push({
label: player.localize('Copy magnet URI'),
listener: function (this: videojs.Player) {
copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
}
})
}
items.push({
icon: 'info',
label: player.localize('Stats for nerds'),
listener: () => {
player.stats().show()
}
})
return items.map(i => ({
...i,
label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
}))
}
// adding the menu
player.contextmenuUI({ content })
}
private static getAutoPlayValue (autoplay: any) {
if (autoplay !== true) return autoplay
// On first play, disable autoplay to avoid issues
// But if the player already played videos, we can safely autoplay next ones
if (isIOS() || isSafari()) {
return PeertubePlayerManager.alreadyPlayed ? 'play' : false
}
return 'play'
player.contextmenuUI(options)
}
}

View File

@ -0,0 +1,489 @@
import videojs from 'video.js'
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
import { isDefaultLocale } from '@shared/core-utils/i18n'
import { VideoFile } from '@shared/models'
import { copyToClipboard } from '../../root-helpers/utils'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
import { getAverageBandwidthInStore } from './peertube-player-local-storage'
import {
NextPreviousVideoButtonOptions,
P2PMediaLoaderPluginOptions,
PeerTubeLinkButtonOptions,
PlaylistPluginOptions,
UserWatching,
VideoJSCaption,
VideoJSPluginOptions
} from './peertube-videojs-typings'
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
export type WebtorrentOptions = {
videoFiles: VideoFile[]
}
export type P2PMediaLoaderOptions = {
playlistUrl: string
segmentsSha256Url: string
trackerAnnounce: string[]
redundancyBaseUrls: string[]
videoFiles: VideoFile[]
}
export interface CustomizationOptions {
startTime: number | string
stopTime: number | string
controls?: boolean
muted?: boolean
loop?: boolean
subtitle?: string
resume?: string
peertubeLink: boolean
}
export interface CommonOptions extends CustomizationOptions {
playerElement: HTMLVideoElement
onPlayerElementChange: (element: HTMLVideoElement) => void
autoplay: boolean
p2pEnabled: boolean
nextVideo?: () => void
hasNextVideo?: () => boolean
previousVideo?: () => void
hasPreviousVideo?: () => boolean
playlist?: PlaylistPluginOptions
videoDuration: number
enableHotkeys: boolean
inactivityTimeout: number
poster: string
theaterButton: boolean
captions: boolean
videoViewUrl: string
embedUrl: string
embedTitle: string
isLive: boolean
language?: string
videoCaptions: VideoJSCaption[]
videoUUID: string
videoShortUUID: string
userWatching?: UserWatching
serverUrl: string
errorNotifier: (message: string) => void
}
export type PeertubePlayerManagerOptions = {
common: CommonOptions
webtorrent: WebtorrentOptions
p2pMediaLoader?: P2PMediaLoaderOptions
pluginsManager: PluginsManager
}
export class PeertubePlayerOptionsBuilder {
constructor (
private mode: PlayerMode,
private options: PeertubePlayerManagerOptions,
private p2pMediaLoaderModule?: any
) {
}
getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
const commonOptions = this.options.common
const isHLS = this.mode === 'p2p-media-loader'
let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
const html5 = {
preloadTextTracks: false
}
const plugins: VideoJSPluginOptions = {
peertube: {
mode: this.mode,
autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
videoViewUrl: commonOptions.videoViewUrl,
videoDuration: commonOptions.videoDuration,
userWatching: commonOptions.userWatching,
subtitle: commonOptions.subtitle,
videoCaptions: commonOptions.videoCaptions,
stopTime: commonOptions.stopTime,
isLive: commonOptions.isLive,
videoUUID: commonOptions.videoUUID
}
}
if (commonOptions.playlist) {
plugins.playlist = commonOptions.playlist
}
if (isHLS) {
const { hlsjs } = this.addP2PMediaLoaderOptions(plugins)
Object.assign(html5, hlsjs.html5)
}
if (this.mode === 'webtorrent') {
this.addWebTorrentOptions(plugins, alreadyPlayed)
// WebTorrent plugin handles autoplay, because we do some hackish stuff in there
autoplay = false
}
const videojsOptions = {
html5,
// We don't use text track settings for now
textTrackSettings: false as any, // FIXME: typings
controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
muted: commonOptions.muted !== undefined
? commonOptions.muted
: undefined, // Undefined so the player knows it has to check the local storage
autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
poster: commonOptions.poster,
inactivityTimeout: commonOptions.inactivityTimeout,
playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
plugins,
controlBar: {
children: this.getControlBarChildren(this.mode, {
videoShortUUID: commonOptions.videoShortUUID,
p2pEnabled: commonOptions.p2pEnabled,
captions: commonOptions.captions,
peertubeLink: commonOptions.peertubeLink,
theaterButton: commonOptions.theaterButton,
nextVideo: commonOptions.nextVideo,
hasNextVideo: commonOptions.hasNextVideo,
previousVideo: commonOptions.previousVideo,
hasPreviousVideo: commonOptions.hasPreviousVideo
}) as any // FIXME: typings
}
}
if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
Object.assign(videojsOptions, { language: commonOptions.language })
}
return videojsOptions
}
private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) {
const p2pMediaLoaderOptions = this.options.p2pMediaLoader
const commonOptions = this.options.common
const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
.filter(t => t.startsWith('ws'))
const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
redundancyUrlManager,
type: 'application/x-mpegURL',
startTime: commonOptions.startTime,
src: p2pMediaLoaderOptions.playlistUrl
}
let consumeOnly = false
if ((navigator as any)?.connection?.type === 'cellular') {
console.log('We are on a cellular connection: disabling seeding.')
consumeOnly = true
}
const p2pMediaLoaderConfig: HlsJsEngineSettings = {
loader: {
trackerAnnounce,
segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
rtcConfig: getRtcConfig(),
requiredSegmentsPriority: 1,
simultaneousHttpDownloads: 1,
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
useP2P: commonOptions.p2pEnabled,
consumeOnly
},
segments: {
swarmId: p2pMediaLoaderOptions.playlistUrl
}
}
const hlsjs = {
levelLabelHandler: (level: { height: number, width: number }) => {
const resolution = Math.min(level.height || 0, level.width || 0)
const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
// We don't have files for live videos
if (!file) return level.height
let label = file.resolution.label
if (file.fps >= 50) label += file.fps
return label
},
html5: {
hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig)
}
}
const toAssign = { p2pMediaLoader, hlsjs }
Object.assign(plugins, toAssign)
return toAssign
}
private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
const base = {
capLevelToPlayerSize: true,
autoStartLoad: false,
liveSyncDurationCount: 5,
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
}
const averageBandwidth = getAverageBandwidthInStore()
if (!averageBandwidth) return base
return {
...base,
abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
startLevel: -1,
testBandwidth: false,
debug: false
}
}
private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
const commonOptions = this.options.common
const webtorrentOptions = this.options.webtorrent
const p2pMediaLoaderOptions = this.options.p2pMediaLoader
const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play'
const webtorrent = {
autoplay,
playerRefusedP2P: commonOptions.p2pEnabled === false,
videoDuration: commonOptions.videoDuration,
playerElement: commonOptions.playerElement,
videoFiles: webtorrentOptions.videoFiles.length !== 0
? webtorrentOptions.videoFiles
// The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
: p2pMediaLoaderOptions?.videoFiles || [],
startTime: commonOptions.startTime
}
Object.assign(plugins, { webtorrent })
}
private getControlBarChildren (mode: PlayerMode, options: {
p2pEnabled: boolean
videoShortUUID: string
peertubeLink: boolean
theaterButton: boolean
captions: boolean
nextVideo?: () => void
hasNextVideo?: () => boolean
previousVideo?: () => void
hasPreviousVideo?: () => boolean
}) {
const settingEntries = []
const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
// Keep an order
settingEntries.push('playbackRateMenuButton')
if (options.captions === true) settingEntries.push('captionsButton')
settingEntries.push('resolutionMenuButton')
const children = {}
if (options.previousVideo) {
const buttonOptions: NextPreviousVideoButtonOptions = {
type: 'previous',
handler: options.previousVideo,
isDisabled: () => {
if (!options.hasPreviousVideo) return false
return !options.hasPreviousVideo()
}
}
Object.assign(children, {
previousVideoButton: buttonOptions
})
}
Object.assign(children, { playToggle: {} })
if (options.nextVideo) {
const buttonOptions: NextPreviousVideoButtonOptions = {
type: 'next',
handler: options.nextVideo,
isDisabled: () => {
if (!options.hasNextVideo) return false
return !options.hasNextVideo()
}
}
Object.assign(children, {
nextVideoButton: buttonOptions
})
}
Object.assign(children, {
currentTimeDisplay: {},
timeDivider: {},
durationDisplay: {},
liveDisplay: {},
flexibleWidthSpacer: {},
progressControl: {
children: {
seekBar: {
children: {
[loadProgressBar]: {},
mouseTimeDisplay: {},
playProgressBar: {}
}
}
}
},
p2PInfoButton: {
p2pEnabled: options.p2pEnabled
},
muteToggle: {},
volumeControl: {},
settingsButton: {
setup: {
maxHeightOffset: 40
},
entries: settingEntries
}
})
if (options.peertubeLink === true) {
Object.assign(children, {
peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
})
}
if (options.theaterButton === true) {
Object.assign(children, {
theaterButton: {}
})
}
Object.assign(children, {
fullscreenToggle: {}
})
return children
}
private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
if (autoplay !== true) return autoplay
// On first play, disable autoplay to avoid issues
// But if the player already played videos, we can safely autoplay next ones
if (isIOS() || isSafari()) {
return alreadyPlayed ? 'play' : false
}
return 'play'
}
getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
const content = () => {
const isLoopEnabled = player.options_['loop']
const items = [
{
icon: 'repeat',
label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
listener: function () {
player.options_['loop'] = !isLoopEnabled
}
},
{
label: player.localize('Copy the video URL'),
listener: function () {
copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
}
},
{
label: player.localize('Copy the video URL at the current time'),
listener: function (this: videojs.Player) {
const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
}
},
{
icon: 'code',
label: player.localize('Copy embed code'),
listener: () => {
copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
}
}
]
if (this.mode === 'webtorrent') {
items.push({
label: player.localize('Copy magnet URI'),
listener: function (this: videojs.Player) {
copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
}
})
}
items.push({
icon: 'info',
label: player.localize('Stats for nerds'),
listener: () => {
player.stats().show()
}
})
return items.map(i => ({
...i,
label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
}))
}
return { content }
}
}

View File

@ -122,6 +122,14 @@ class PeerTubePlugin extends Plugin {
this.alterInactivity()
}
displayFatalError () {
this.player.addClass('vjs-error-display-enabled')
}
hideFatalError () {
this.player.removeClass('vjs-error-display-enabled')
}
private initializePlayer () {
if (isMobile()) this.player.addClass('vjs-is-mobile')

View File

@ -4,7 +4,7 @@ import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
import { Html5Hlsjs } from './p2p-media-loader/hls-plugin'
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 { PlayerMode } from './peertube-player-options-builder'
import { PeerTubePlugin } from './peertube-plugin'
import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin'
import { PlaylistPlugin } from './playlist/playlist-plugin'

View File

@ -145,7 +145,7 @@ class WebTorrentPlugin extends Plugin {
}
// Do not display error to user because we will have multiple fallback
this.disableErrorDisplay();
this.player.peertube().hideFatalError();
// Hack to "simulate" src link in video.js >= 6
// Without this, we can't play the video after pausing it
@ -524,7 +524,7 @@ class WebTorrentPlugin extends Plugin {
this.torrent = null
// Enable error display now this is our last fallback
this.player.one('error', () => this.enableErrorDisplay())
this.player.one('error', () => this.player.peertube().displayFatalError())
const httpUrl = this.currentVideoFile.fileUrl
this.player.src = this.savePlayerSrcFunction
@ -549,14 +549,6 @@ class WebTorrentPlugin extends Plugin {
return this.player.trigger('customError', { err })
}
private enableErrorDisplay () {
this.player.addClass('vjs-error-display-enabled')
}
private disableErrorDisplay () {
this.player.removeClass('vjs-error-display-enabled')
}
private pickAverageVideoFile () {
if (this.videoFiles.length === 1) return this.videoFiles[0]

View File

@ -14,7 +14,7 @@ import {
VideoPlaylistElement,
VideoStreamingPlaylistType
} from '../../../../shared/models'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player'
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { TranslationsManager } from '../../assets/player/translations-manager'
import { isP2PEnabled } from '../../assets/player/utils'
@ -558,7 +558,11 @@ export class PeerTubeEmbed {
serverUrl: window.location.origin,
language: navigator.language,
embedUrl: window.location.origin + videoInfo.embedPath,
embedTitle: videoInfo.name
embedTitle: videoInfo.name,
errorNotifier: () => {
// Empty, we don't have a notifier in the embed
}
},
webtorrent: {
@ -664,7 +668,6 @@ export class PeerTubeEmbed {
this.player.dispose()
this.playerElement = null
this.displayError('This video is not available because the remote instance is not responding.', translations)
}
}

View File

@ -55,7 +55,8 @@ const playerKeys = {
'Playlist: {1}': 'Playlist: {1}',
'disabled': 'disabled',
' off': ' off',
'Player mode': 'Player mode'
'Player mode': 'Player mode',
'The video failed to play, will try to fast forward.': 'The video failed to play, will try to fast forward.'
}
Object.assign(playerKeys, videojs)