Separate player in dedicated build
* Separate player in a dedicated build, that we can control using vite. We had too many issues with Angular build system and we can now have the same build between the embed and the client. We can also embed SVG directly in the CSS * Upgrade p2p-media-loader to v2 * Update internal infohashes to reflect this p2p-media-loader protocol change (they are updated at PeerTube startup) * Minimum required iOS version is now v14
|
@ -1,4 +1,4 @@
|
|||
last 1 Chrome version
|
||||
last 2 Edge major versions
|
||||
Firefox ESR
|
||||
ios_saf >= 13.1
|
||||
ios_saf >= 14
|
||||
|
|
|
@ -163,7 +163,8 @@
|
|||
"@typescript-eslint/unbound-method": [
|
||||
"error",
|
||||
{ "ignoreStatic": true }
|
||||
]
|
||||
],
|
||||
"import/no-named-default": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
/e2e/local.log
|
||||
/e2e/browserstack.err
|
||||
/e2e/screenshots
|
||||
/src/standalone/player/build
|
||||
/src/standalone/player/dist
|
||||
/src/standalone/embed-player-api/build
|
||||
/src/standalone/embed-player-api/dist
|
||||
/e2e/logs
|
||||
|
|
|
@ -183,7 +183,10 @@
|
|||
"includePaths": [
|
||||
"src/sass/include",
|
||||
"."
|
||||
]
|
||||
],
|
||||
"sass": {
|
||||
"silenceDeprecations": [ "import", "mixed-decls", "color-functions", "global-builtin" ]
|
||||
}
|
||||
},
|
||||
"assets": [
|
||||
"src/assets/images",
|
||||
|
@ -212,7 +215,9 @@
|
|||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"deepmerge",
|
||||
"core-js/features/reflect"
|
||||
"core-js/features/reflect",
|
||||
"hammerjs",
|
||||
"jschannel"
|
||||
],
|
||||
"scripts": [],
|
||||
"extractLicenses": false,
|
||||
|
@ -241,7 +246,7 @@
|
|||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "100kb"
|
||||
"maximumError": "120kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"target": "ES2015",
|
||||
"target": "ES2018",
|
||||
"typeRoots": [
|
||||
"../node_modules/@types",
|
||||
"../node_modules"
|
||||
|
|
|
@ -24,10 +24,13 @@
|
|||
"net": false,
|
||||
"stream": false,
|
||||
"os": false,
|
||||
"http": false,
|
||||
"dgram": false,
|
||||
"util": false
|
||||
},
|
||||
"workspaces": [
|
||||
"../packages/*"
|
||||
"../packages/*",
|
||||
"./src/standalone/player"
|
||||
],
|
||||
"typings": "*.d.ts",
|
||||
"devDependencies": {
|
||||
|
@ -55,8 +58,6 @@
|
|||
"@ngx-loading-bar/http-client": "^7.0.0",
|
||||
"@ngx-loading-bar/router": "^7.0.0",
|
||||
"@peertube/maildev": "^1.2.0",
|
||||
"@peertube/p2p-media-loader-core": "^1.0.20",
|
||||
"@peertube/p2p-media-loader-hlsjs": "^1.0.20",
|
||||
"@peertube/xliffmerge": "^2.0.3",
|
||||
"@plussub/srt-vtt-parser": "^2.0.5",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
|
@ -103,8 +104,11 @@
|
|||
"markdown-it": "14.1.0",
|
||||
"markdown-it-emoji": "^3.0.0",
|
||||
"ngx-uploadx": "^7.0.0",
|
||||
"p2p-media-loader-core": "^2.1.2",
|
||||
"p2p-media-loader-hlsjs": "^2.1.2",
|
||||
"primeng": "^17",
|
||||
"rxjs": "^7.3.0",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"sha.js": "^2.4.11",
|
||||
"socket.io-client": "^4.5.4",
|
||||
"stylelint": "^16.2.1",
|
||||
|
@ -115,9 +119,9 @@
|
|||
"tslib": "^2.4.0",
|
||||
"typescript": "~5.7.3",
|
||||
"video.js": "^7.19.2",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-checker": "^0.7.2",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
|
|
|
@ -1,5 +1 @@
|
|||
@use 'node_modules/video.js/dist/video-js';
|
||||
|
||||
$assets-path: '../../assets/';
|
||||
|
||||
@use '../../../sass/player/index';
|
||||
@use '../../../standalone/player/build/peertube-player.css';
|
||||
|
|
|
@ -47,16 +47,18 @@ import { logger } from '@root-helpers/logger'
|
|||
import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
|
||||
import debug from 'debug'
|
||||
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import {
|
||||
cleanupVideoWatch,
|
||||
getStoredTheater,
|
||||
getStoredVideoWatchHistory,
|
||||
HLSOptions,
|
||||
PeerTubePlayer,
|
||||
PeerTubePlayerConstructorOptions,
|
||||
PeerTubePlayerLoadOptions,
|
||||
PlayerMode,
|
||||
videojs
|
||||
} from '../../../assets/player'
|
||||
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
|
||||
import { environment } from '../../../environments/environment'
|
||||
} from '@peertube/player'
|
||||
import { DateToggleComponent } from '../../shared/shared-main/date/date-toggle.component'
|
||||
import { PluginPlaceholderComponent } from '../../shared/shared-main/plugins/plugin-placeholder.component'
|
||||
import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component'
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export * from './peertube-player'
|
||||
export * from './types'
|
|
@ -2,7 +2,7 @@
|
|||
import debug from 'debug'
|
||||
import { firstValueFrom, ReplaySubject } from 'rxjs'
|
||||
import { first, shareReplay } from 'rxjs/operators'
|
||||
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
|
||||
import { RegisterClientHelpers } from '../types/register-client-option.model'
|
||||
import { getExternalAuthHref, getHookType, internalRunHook } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
ClientDoAction,
|
||||
|
@ -83,6 +83,7 @@ class PluginsManager {
|
|||
private readonly onFormFields: OnFormFields
|
||||
private readonly onSettingsScripts: OnSettingsScripts
|
||||
private readonly onClientRoute: OnClientRoute
|
||||
private readonly backendUrl: string
|
||||
|
||||
constructor (options: {
|
||||
doAction?: ClientDoAction
|
||||
|
@ -90,12 +91,14 @@ class PluginsManager {
|
|||
onFormFields?: OnFormFields
|
||||
onSettingsScripts?: OnSettingsScripts
|
||||
onClientRoute?: OnClientRoute
|
||||
backendUrl?: string
|
||||
}) {
|
||||
this.doAction = options.doAction
|
||||
this.peertubeHelpersFactory = options.peertubeHelpersFactory
|
||||
this.onFormFields = options.onFormFields
|
||||
this.onSettingsScripts = options.onSettingsScripts
|
||||
this.onClientRoute = options.onClientRoute
|
||||
this.backendUrl = options.backendUrl
|
||||
}
|
||||
|
||||
static getPluginPathPrefix (isTheme: boolean) {
|
||||
|
@ -281,7 +284,7 @@ class PluginsManager {
|
|||
|
||||
logger.info(`Loading script ${clientScript.script} of plugin ${plugin.name}`)
|
||||
|
||||
const absURL = (environment.apiUrl || window.location.origin) + clientScript.script
|
||||
const absURL = (this.backendUrl || environment.apiUrl || window.location.origin) + clientScript.script
|
||||
return dynamicImport(absURL)
|
||||
.then((script: ClientScript) => {
|
||||
return script.register({
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { resolve } from 'path'
|
||||
|
||||
export function getCSSConfig (root: string) {
|
||||
return {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
loadPaths: [ resolve(root, './src/sass/include') ],
|
||||
// FIXME: Wait for bootstrap upgrade that fixes deprecated sass utils
|
||||
silenceDeprecations: [ 'import', 'mixed-decls', 'color-functions', 'global-builtin' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getAliasConfig (root: string) {
|
||||
return [
|
||||
{ find: /^video.js$/, replacement: resolve(root, './node_modules/video.js/core.js') },
|
||||
{ find: '@root-helpers', replacement: resolve(root, './src/root-helpers') }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@peertube/player",
|
||||
"private": false,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"build": "rm -rf ./build && ../../../node_modules/.bin/vite build --mode production --config ./vite.config.mjs",
|
||||
"dev": "../../../node_modules/.bin/vite build --mode dev --watch --config ./vite.config.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Chocobozzz/PeerTube.git"
|
||||
},
|
||||
"keywords": [
|
||||
"peertube",
|
||||
"embed"
|
||||
],
|
||||
"main": "./build/peertube-player.js",
|
||||
"exports": {
|
||||
".": "./build/peertube-player.js"
|
||||
},
|
||||
"types": "./src/index.ts",
|
||||
"author": "Chocobozzz",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"sideEffects": true,
|
||||
"bugs": {
|
||||
"url": "https://github.com/Chocobozzz/PeerTube/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Chocobozzz/PeerTube#readme",
|
||||
"dependencies": {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './peertube-player'
|
||||
export * from './peertube-player-local-storage'
|
||||
export * from './types'
|
||||
|
||||
import './sass/player.scss'
|
|
@ -31,6 +31,8 @@ import './shared/metrics/metrics-plugin'
|
|||
import './shared/p2p-media-loader/hls-plugin'
|
||||
import './shared/p2p-media-loader/p2p-media-loader-plugin'
|
||||
import './shared/web-video/web-video-plugin'
|
||||
import './shared/dock/peertube-dock-component'
|
||||
import './shared/dock/peertube-dock-plugin'
|
||||
import videojs, { VideoJsPlayer } from 'video.js'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||
|
@ -40,7 +42,7 @@ import { isMobile } from '@root-helpers/web-browser'
|
|||
import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
|
||||
import { saveAverageBandwidth } from './peertube-player-local-storage'
|
||||
import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder'
|
||||
import { TranslationsManager } from './translations-manager'
|
||||
import { TranslationsManager } from '@root-helpers/translations-manager'
|
||||
import { PeerTubePlayerConstructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types'
|
||||
|
||||
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
|
|
@ -0,0 +1,2 @@
|
|||
@use '../../../node_modules/video.js/dist/video-js';
|
||||
@use './shared/index.scss';
|
|
@ -22,5 +22,3 @@ $control-bar-total-height: $control-bar-height - $control-bar-slider-top;
|
|||
$progress-margin: 10px;
|
||||
|
||||
$dock-padding: 20px;
|
||||
|
||||
$assets-path: '../../assets/' !default;
|
|
@ -50,8 +50,8 @@ $context-menu-width: 350px;
|
|||
|
||||
@each $icon in $icons {
|
||||
&[class$="-#{$icon}"] {
|
||||
mask-image: url('#{$assets-path}/player/images/#{$icon}.svg');
|
||||
-webkit-mask-image: url('#{$assets-path}/player/images/#{$icon}.svg');
|
||||
mask-image: url('./svg/#{$icon}.svg');
|
||||
-webkit-mask-image: url('./svg/#{$icon}.svg');
|
||||
}
|
||||
}
|
||||
|
|
@ -270,11 +270,11 @@ $chapter-marker-size: 9px;
|
|||
@include margin-right(2px);
|
||||
|
||||
&.icon-download {
|
||||
background-image: url('#{$assets-path}/player/images/arrow-down.svg');
|
||||
background-image: url('./svg/arrow-down.svg');
|
||||
}
|
||||
|
||||
&.icon-upload {
|
||||
background-image: url('#{$assets-path}/player/images/arrow-up.svg');
|
||||
background-image: url('./svg/arrow-up.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,8 +291,8 @@ $chapter-marker-size: 9px;
|
|||
.icon {
|
||||
&.icon-next,
|
||||
&.icon-previous {
|
||||
mask-image: url('#{$assets-path}/player/images/next.svg');
|
||||
-webkit-mask-image: url('#{$assets-path}/player/images/next.svg');
|
||||
mask-image: url('./svg/next.svg');
|
||||
-webkit-mask-image: url('./svg/next.svg');
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
|
||||
|
@ -319,7 +319,7 @@ $chapter-marker-size: 9px;
|
|||
width: $control-bar-icon-size;
|
||||
height: $control-bar-icon-size;
|
||||
vertical-align: middle;
|
||||
background: url('#{$assets-path}/player/images/volume.svg') no-repeat;
|
||||
background: url('./svg/volume.svg') no-repeat;
|
||||
background-size: contain;
|
||||
|
||||
&::before {
|
||||
|
@ -328,7 +328,7 @@ $chapter-marker-size: 9px;
|
|||
}
|
||||
|
||||
&.vjs-vol-0 .vjs-icon-placeholder {
|
||||
background: url('#{$assets-path}/player/images/volume-mute.svg') no-repeat;
|
||||
background: url('./svg/volume-mute.svg') no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
@ -415,7 +415,7 @@ $chapter-marker-size: 9px;
|
|||
height: $control-bar-icon-size - 4px;
|
||||
width: $control-bar-icon-size - 4px;
|
||||
vertical-align: middle;
|
||||
background: url('#{$assets-path}/player/images/settings.svg') no-repeat;
|
||||
background: url('./svg/settings.svg') no-repeat;
|
||||
background-size: contain;
|
||||
|
||||
&::before {
|
||||
|
@ -448,7 +448,7 @@ $chapter-marker-size: 9px;
|
|||
width: $control-bar-icon-size - 4px;
|
||||
height: $control-bar-icon-size - 4px;
|
||||
vertical-align: middle;
|
||||
background: url('#{$assets-path}/player/images/theater.svg') no-repeat;
|
||||
background: url('./svg/theater.svg') no-repeat;
|
||||
background-size: contain;
|
||||
|
||||
&::before {
|
||||
|
@ -493,7 +493,7 @@ $chapter-marker-size: 9px;
|
|||
width: $control-bar-icon-size;
|
||||
height: $control-bar-icon-size;
|
||||
vertical-align: middle;
|
||||
background: url('#{$assets-path}/player/images/fullscreen.svg') no-repeat;
|
||||
background: url('./svg/fullscreen.svg') no-repeat;
|
||||
background-size: contain;
|
||||
|
||||
&::before {
|
||||
|
@ -515,14 +515,6 @@ $chapter-marker-size: 9px;
|
|||
.vjs-theater-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-peertube {
|
||||
.icon,
|
||||
.download-speed-text,
|
||||
.upload-speed-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-peertube-skin.vjs-size-570 .vjs-control-bar {
|
||||
|
@ -540,6 +532,12 @@ $chapter-marker-size: 9px;
|
|||
.vjs-peertube-displayed {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.download-speed-text,
|
||||
.upload-speed-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-peertube-link {
|
|
@ -60,7 +60,7 @@ body {
|
|||
|
||||
.vjs-icon-placeholder::before {
|
||||
content: '';
|
||||
background-image: url('#{$assets-path}/player/images/big-play-button.svg');
|
||||
background-image: url('./svg/big-play-button.svg');
|
||||
|
||||
@include big-play-button-triangle-size(45px);
|
||||
}
|
|
@ -44,8 +44,8 @@ $playlist-menu-width: 350px;
|
|||
}
|
||||
|
||||
.cross {
|
||||
mask-image: url('#{$assets-path}/images/feather/x.svg');
|
||||
-webkit-mask-image: url('#{$assets-path}/images/feather/x.svg');
|
||||
mask-image: url('./svg/x.svg');
|
||||
-webkit-mask-image: url('./svg/x.svg');
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
|
||||
|
@ -92,8 +92,8 @@ $playlist-menu-width: 350px;
|
|||
}
|
||||
|
||||
.vjs-playlist-icon {
|
||||
mask-image: url('#{$assets-path}/images/feather/playlists.svg');
|
||||
-webkit-mask-image: url('#{$assets-path}/images/feather/playlists.svg');
|
||||
mask-image: url('./svg/playlists.svg');
|
||||
-webkit-mask-image: url('./svg/playlists.svg');
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
|
|
@ -145,7 +145,7 @@ $setting-transition-easing: ease-out;
|
|||
position: absolute;
|
||||
content: ' ';
|
||||
margin-top: 1px;
|
||||
background-image: url('#{$assets-path}/player/images/tick-white.svg');
|
||||
background-image: url('./svg/tick-white.svg');
|
||||
|
||||
@include icon(15px);
|
||||
@include left(15px);
|
Before Width: | Height: | Size: 893 B After Width: | Height: | Size: 893 B |
Before Width: | Height: | Size: 887 B After Width: | Height: | Size: 887 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="playlists.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs6" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="30.708333"
|
||||
inkscape:cx="12.016282"
|
||||
inkscape:cy="15.989145"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1036"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M 8.70016,4.8 H 21.90019"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 2.10001,4.8 H 2.650009"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M 8.70016,19.2 H 21.90019"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path3" />
|
||||
<path
|
||||
d="M 2.10001,19.2 H 2.650009"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M 8.699719,12 H 21.89974"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path5" />
|
||||
<path
|
||||
d="M 2.10001,12 H 2.650009"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path6" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 392 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 702 B |
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 692 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
After Width: | Height: | Size: 299 B |
|
@ -5,6 +5,10 @@ import { logger } from '@root-helpers/logger'
|
|||
import Hlsjs, { ErrorData, Level, LevelSwitchingData, ManifestParsedData } from 'hls.js'
|
||||
import videojs from 'video.js'
|
||||
import { HLSPluginOptions, HlsjsConfigHandlerOptions, PeerTubeResolution, VideoJSTechHLS } from '../../types'
|
||||
import { HlsJsP2PEngine, HlsWithP2PInstance } from 'p2p-media-loader-hlsjs'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
|
||||
const HlsWithP2P = HlsJsP2PEngine.injectMixin(Hlsjs)
|
||||
|
||||
type ErrorCounts = {
|
||||
[ type: string ]: number
|
||||
|
@ -14,8 +18,6 @@ type ErrorCounts = {
|
|||
// Source handler registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type HookFn = (player: videojs.Player, hljs: Hlsjs) => void
|
||||
|
||||
let alreadyRegistered = false
|
||||
|
||||
const registerSourceHandler = function (vjs: typeof videojs) {
|
||||
|
@ -110,8 +112,6 @@ videojs.registerPlugin('hlsjs', HLSJSConfigHandler)
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class Html5Hlsjs {
|
||||
private static hooks: { [id: string]: HookFn[] } = {}
|
||||
|
||||
private readonly videoElement: HTMLVideoElement
|
||||
private readonly errorCounts: ErrorCounts = {}
|
||||
private readonly player: videojs.Player
|
||||
|
@ -121,7 +121,7 @@ export class Html5Hlsjs {
|
|||
|
||||
private maxNetworkErrorRecovery = 5
|
||||
|
||||
private hls: Hlsjs
|
||||
private hls: HlsWithP2PInstance<Hlsjs>
|
||||
private hlsjsConfig: HLSPluginOptions = null
|
||||
|
||||
private _duration: number = null
|
||||
|
@ -410,10 +410,15 @@ export class Html5Hlsjs {
|
|||
this.videoElement.addEventListener('play', this.handlers.play)
|
||||
}
|
||||
|
||||
const loader = this.hlsjsConfig.loaderBuilder()
|
||||
this.hls = new Hlsjs({ ...this.hlsjsConfig, loader })
|
||||
this.hls = new HlsWithP2P({
|
||||
...omit(this.hlsjsConfig, [ 'p2pMediaLoaderOptions' ]),
|
||||
|
||||
this.player.trigger('hlsjs-initialized', { hlsjs: this.hls, engine: loader.getEngine() })
|
||||
p2p: {
|
||||
core: this.hlsjsConfig.p2pMediaLoaderOptions
|
||||
}
|
||||
})
|
||||
|
||||
this.player.trigger('hlsjs-initialized', { hlsjs: this.hls })
|
||||
|
||||
this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
|
||||
this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data))
|
||||
|
@ -462,17 +467,21 @@ export class Html5Hlsjs {
|
|||
this.hlsjsConfig.autoStartLoad = true
|
||||
this.player.autoplay('play')
|
||||
|
||||
const loader = this.hlsjsConfig.loaderBuilder()
|
||||
this.hls = new Hlsjs({
|
||||
...this.hlsjsConfig,
|
||||
loader,
|
||||
this.hls = new HlsWithP2P({
|
||||
...omit(this.hlsjsConfig, [ 'p2pMediaLoaderOptions' ]),
|
||||
|
||||
p2p: {
|
||||
core: this.hlsjsConfig.p2pMediaLoaderOptions
|
||||
},
|
||||
|
||||
startPosition: this.duration() === Infinity
|
||||
? undefined
|
||||
: currentTime,
|
||||
|
||||
startLevel
|
||||
})
|
||||
|
||||
this.player.trigger('hlsjs-initialized', { hlsjs: this.hls, engine: loader.getEngine() })
|
||||
this.player.trigger('hlsjs-initialized', { hlsjs: this.hls })
|
||||
|
||||
this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
|
||||
this.registerLevelEventSwitch()
|
|
@ -1,9 +1,9 @@
|
|||
import { Events, Segment } from '@peertube/p2p-media-loader-core'
|
||||
import { Engine, initHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
|
||||
import { addQueryParams } from '@peertube/peertube-core-utils'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import debug from 'debug'
|
||||
import Hlsjs from 'hls.js'
|
||||
import { FragLoadedData, default as Hlsjs } from 'hls.js'
|
||||
import type { DownloadSource, SegmentErrorDetails, SegmentLoadDetails } from 'p2p-media-loader-core'
|
||||
import type { HlsWithP2PInstance } from 'p2p-media-loader-hlsjs'
|
||||
import videojs from 'video.js'
|
||||
import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
|
||||
import { SettingsButton } from '../settings/settings-menu-button'
|
||||
|
@ -14,8 +14,7 @@ const Plugin = videojs.getPlugin('plugin')
|
|||
class P2pMediaLoaderPlugin extends Plugin {
|
||||
declare private readonly options: P2PMediaLoaderPluginOptions
|
||||
|
||||
declare private hlsjs: Hlsjs
|
||||
declare private p2pEngine: Engine
|
||||
declare private hlsjs: HlsWithP2PInstance<Hlsjs>
|
||||
declare private statsP2PBytes: {
|
||||
pendingDownload: number[]
|
||||
pendingUpload: number[]
|
||||
|
@ -33,6 +32,9 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
|
||||
declare private liveEnded: boolean
|
||||
|
||||
declare private connectedPeers: Set<string>
|
||||
declare private totalHTTPPeers: number
|
||||
|
||||
constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
|
||||
super(player)
|
||||
|
||||
|
@ -76,15 +78,13 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
}
|
||||
|
||||
{
|
||||
const onHLSJSInitialized = (_: any, { hlsjs, engine }: { hlsjs: Hlsjs, engine: Engine }) => {
|
||||
this.p2pEngine?.removeAllListeners()
|
||||
this.p2pEngine?.destroy()
|
||||
const onHLSJSInitialized = (_: any, { hlsjs }: { hlsjs: HlsWithP2PInstance<Hlsjs> }) => {
|
||||
this.hlsjs?.p2pEngine?.destroy()
|
||||
clearInterval(this.networkInfoInterval)
|
||||
|
||||
this.hlsjs = hlsjs
|
||||
this.p2pEngine = engine
|
||||
|
||||
debugLogger('hls.js initialized, initializing p2p-media-loader plugin', { hlsjs, engine })
|
||||
debugLogger('hls.js initialized, initializing p2p-media-loader plugin', { hlsjs })
|
||||
|
||||
player.ready(() => this.initializePlugin())
|
||||
}
|
||||
|
@ -116,8 +116,7 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
}
|
||||
|
||||
dispose () {
|
||||
this.p2pEngine?.removeAllListeners()
|
||||
this.p2pEngine?.destroy()
|
||||
this.hlsjs?.p2pEngine?.destroy()
|
||||
|
||||
this.hlsjs?.destroy()
|
||||
this.options.segmentValidator?.destroy()
|
||||
|
@ -152,15 +151,22 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private initializePlugin () {
|
||||
initHlsJsPlayer(this.player, this.hlsjs)
|
||||
this.hlsjs.p2pEngine.addEventListener('onSegmentError', (details: SegmentErrorDetails) => {
|
||||
if (navigator.onLine === false || this.liveEnded || details.downloadSource !== 'http') return
|
||||
|
||||
this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
|
||||
if (navigator.onLine === false || this.liveEnded) return
|
||||
|
||||
logger.clientError(`Segment ${segment.id} error.`, err)
|
||||
const segment = details.segment
|
||||
logger.clientError(`Segment ${segment.runtimeId} error.`, details)
|
||||
|
||||
if (this.options.redundancyUrlManager) {
|
||||
this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
|
||||
this.options.redundancyUrlManager.onSegmentError(segment.url)
|
||||
}
|
||||
})
|
||||
|
||||
this.hlsjs.p2pEngine.addEventListener('onSegmentLoaded', (details: SegmentLoadDetails) => {
|
||||
if (details.downloadSource !== 'http') return
|
||||
|
||||
if (this.options.redundancyUrlManager) {
|
||||
this.options.redundancyUrlManager.onSegmentSuccess(details.segmentUrl)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -168,7 +174,8 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
? this.options.redundancyUrlManager.countBaseUrls()
|
||||
: 0
|
||||
|
||||
this.statsP2PBytes.peersWithWebSeed = 1 + redundancyUrlsCount
|
||||
this.totalHTTPPeers = 1 + redundancyUrlsCount
|
||||
this.statsP2PBytes.peersWithWebSeed = this.totalHTTPPeers
|
||||
|
||||
this.runStats()
|
||||
|
||||
|
@ -200,30 +207,44 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private runStats () {
|
||||
this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, _segment, bytes: number) => {
|
||||
this.connectedPeers = new Set()
|
||||
|
||||
if (this.hlsjs.p2pEngine.getConfig().core.mainStream.isP2PDisabled) {
|
||||
this.hlsjs.on(Hlsjs.Events.FRAG_LOADED, (e, data: FragLoadedData) => {
|
||||
const bytes = data.frag.stats.loaded
|
||||
|
||||
this.statsHTTPBytes.pendingDownload.push(bytes)
|
||||
this.statsHTTPBytes.totalDownload += bytes
|
||||
})
|
||||
}
|
||||
|
||||
this.hlsjs.p2pEngine.addEventListener('onChunkDownloaded', (bytes: number, method: DownloadSource) => {
|
||||
const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
|
||||
|
||||
elem.pendingDownload.push(bytes)
|
||||
elem.totalDownload += bytes
|
||||
})
|
||||
|
||||
this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => {
|
||||
if (method !== 'p2p') {
|
||||
logger.error(`Received upload from unknown method ${method}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.hlsjs.p2pEngine.addEventListener('onChunkUploaded', (bytes: number) => {
|
||||
this.statsP2PBytes.pendingUpload.push(bytes)
|
||||
this.statsP2PBytes.totalUpload += bytes
|
||||
})
|
||||
|
||||
this.p2pEngine.on(Events.PeerConnect, () => {
|
||||
this.statsP2PBytes.peersWithWebSeed++
|
||||
this.statsP2PBytes.peersP2POnly++
|
||||
this.hlsjs.p2pEngine.addEventListener('onPeerConnect', peer => {
|
||||
if (peer.streamType !== 'main') return
|
||||
|
||||
this.connectedPeers.add(peer.peerId)
|
||||
this.statsP2PBytes.peersP2POnly = this.connectedPeers.size
|
||||
|
||||
this.statsP2PBytes.peersWithWebSeed = this.totalHTTPPeers + this.statsP2PBytes.peersP2POnly
|
||||
})
|
||||
this.p2pEngine.on(Events.PeerClose, () => {
|
||||
this.statsP2PBytes.peersWithWebSeed--
|
||||
this.statsP2PBytes.peersP2POnly--
|
||||
this.hlsjs.p2pEngine.addEventListener('onPeerClose', peer => {
|
||||
if (peer.streamType !== 'main') return
|
||||
|
||||
this.connectedPeers.delete(peer.peerId)
|
||||
this.statsP2PBytes.peersP2POnly = this.connectedPeers.size
|
||||
|
||||
this.statsP2PBytes.peersWithWebSeed = this.totalHTTPPeers + this.statsP2PBytes.peersP2POnly
|
||||
})
|
||||
|
||||
this.networkInfoInterval = setInterval(() => {
|
|
@ -1,22 +1,32 @@
|
|||
import { logger } from '@root-helpers/logger'
|
||||
|
||||
class RedundancyUrlManager {
|
||||
private map = new Map<string, string>()
|
||||
|
||||
constructor (private baseUrls: string[] = []) {
|
||||
// empty
|
||||
}
|
||||
|
||||
removeBySegmentUrl (segmentUrl: string) {
|
||||
const baseUrl = getBaseUrl(segmentUrl)
|
||||
onSegmentError (segmentUrl: string) {
|
||||
if (!this.map.has(segmentUrl)) return
|
||||
|
||||
const customSegmentUrl = this.map.get(segmentUrl)
|
||||
this.map.delete(segmentUrl)
|
||||
|
||||
const baseUrl = getBaseUrl(customSegmentUrl)
|
||||
const oldLength = baseUrl.length
|
||||
|
||||
this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/')
|
||||
|
||||
if (oldLength !== this.baseUrls.length) {
|
||||
logger.info(`Removed redundancy of segment URL ${segmentUrl}.`)
|
||||
logger.info(`Removed redundancy of segment URL ${customSegmentUrl}.`)
|
||||
}
|
||||
}
|
||||
|
||||
onSegmentSuccess (segmentUrl: string) {
|
||||
this.map.delete(segmentUrl)
|
||||
}
|
||||
|
||||
buildUrl (url: string) {
|
||||
const max = this.baseUrls.length + 1
|
||||
const i = this.getRandomInt(max)
|
||||
|
@ -26,7 +36,11 @@ class RedundancyUrlManager {
|
|||
const newBaseUrl = this.baseUrls[i]
|
||||
const slashPart = newBaseUrl.endsWith('/') ? '' : '/'
|
||||
|
||||
return newBaseUrl + slashPart + getFilename(url)
|
||||
const newUrl = newBaseUrl + slashPart + getFilename(url)
|
||||
|
||||
this.map.set(url, newUrl)
|
||||
|
||||
return newUrl
|
||||
}
|
||||
|
||||
countBaseUrls () {
|
|
@ -1,4 +1,4 @@
|
|||
import { Segment } from '@peertube/p2p-media-loader-core'
|
||||
import type { Segment } from 'p2p-media-loader-core'
|
||||
import { RedundancyUrlManager } from './redundancy-url-manager'
|
||||
|
||||
export function segmentUrlBuilderFactory (redundancyUrlManager: RedundancyUrlManager | null) {
|
|
@ -1,9 +1,9 @@
|
|||
import { Segment } from '@peertube/p2p-media-loader-core'
|
||||
import type { ByteRange } from 'p2p-media-loader-core'
|
||||
import { removeQueryParams } from '@peertube/peertube-core-utils'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { wait } from '@root-helpers/utils'
|
||||
import { removeQueryParams } from '@peertube/peertube-core-utils'
|
||||
import { isSameOrigin } from '../common'
|
||||
import debug from 'debug'
|
||||
import { isSameOrigin } from '../common'
|
||||
|
||||
const debugLogger = debug('peertube:player:segment-validator')
|
||||
|
||||
|
@ -12,9 +12,6 @@ type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string
|
|||
const maxRetries = 10
|
||||
|
||||
export class SegmentValidator {
|
||||
|
||||
private readonly bytesRangeRegex = /bytes=(\d+)-(\d+)/
|
||||
|
||||
private destroyed = false
|
||||
|
||||
private segmentJSONPromise: Promise<SegmentsJSON>
|
||||
|
@ -29,17 +26,18 @@ export class SegmentValidator {
|
|||
}) {
|
||||
}
|
||||
|
||||
async validate (segment: Segment, _method: string, _peerId: string, retry = 1) {
|
||||
if (this.destroyed) return
|
||||
async validate (url: string, byteRange: ByteRange | undefined, data: ArrayBuffer, retry = 1): Promise<boolean> {
|
||||
if (this.destroyed) return false
|
||||
|
||||
this.loadSha256SegmentsPromiseIfNeeded()
|
||||
|
||||
const filename = removeQueryParams(segment.url).split('/').pop()
|
||||
const filename = removeQueryParams(url).split('/').pop()
|
||||
|
||||
const segmentValue = (await this.segmentJSONPromise)[filename]
|
||||
|
||||
if (!segmentValue && retry > maxRetries) {
|
||||
throw new Error(`Unknown segment name ${filename} in segment validator`)
|
||||
logger.clientError(`Unknown segment name ${filename} in segment validator`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!segmentValue) {
|
||||
|
@ -49,9 +47,7 @@ export class SegmentValidator {
|
|||
|
||||
this.loadSha256SegmentsPromise()
|
||||
|
||||
await this.validate(segment, _method, _peerId, retry + 1)
|
||||
|
||||
return
|
||||
return this.validate(url, byteRange, data, retry + 1)
|
||||
}
|
||||
|
||||
let hashShouldBe: string
|
||||
|
@ -60,24 +56,25 @@ export class SegmentValidator {
|
|||
if (typeof segmentValue === 'string') {
|
||||
hashShouldBe = segmentValue
|
||||
} else {
|
||||
const captured = this.bytesRangeRegex.exec(segment.range)
|
||||
range = captured[1] + '-' + captured[2]
|
||||
range = byteRange.start + '-' + byteRange.end
|
||||
|
||||
hashShouldBe = segmentValue[range]
|
||||
}
|
||||
|
||||
if (hashShouldBe === undefined) {
|
||||
throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
|
||||
logger.clientError(`Unknown segment name ${filename}/${range} in segment validator`)
|
||||
return false
|
||||
}
|
||||
|
||||
debugLogger(`Validating ${filename}` + (segment.range ? ` range ${segment.range}` : ''))
|
||||
debugLogger(`Validating ${filename}` + (range ? ` range ${range}` : ''))
|
||||
|
||||
const calculatedSha = await this.sha256Hex(segment.data)
|
||||
const calculatedSha = await this.sha256Hex(data)
|
||||
if (calculatedSha !== hashShouldBe) {
|
||||
throw new Error(
|
||||
`Hashes does not correspond for segment ${filename}/${range}` +
|
||||
`(expected: ${hashShouldBe} instead of ${calculatedSha})`
|
||||
logger.clientError(
|
||||
`Hashes does not correspond for segment ${filename}/${range} (expected: ${hashShouldBe} instead of ${calculatedSha})`
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +103,7 @@ export class SegmentValidator {
|
|||
return fetch(this.options.segmentsSha256Url, { headers })
|
||||
.then(res => res.json() as Promise<SegmentsJSON>)
|
||||
.catch(err => {
|
||||
logger.error('Cannot get sha256 segments', err)
|
||||
logger.clientError('Cannot get sha256 segments', err)
|
||||
return {}
|
||||
})
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
|
||||
import { Engine, HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
|
||||
import { getResolutionAndFPSLabel, getResolutionLabel } from '@peertube/peertube-core-utils'
|
||||
import { LiveVideoLatencyMode } from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import debug from 'debug'
|
||||
import { Level } from 'hls.js'
|
||||
import type { CoreConfig, StreamConfig } from 'p2p-media-loader-core'
|
||||
import { getAverageBandwidthInStore } from '../../peertube-player-local-storage'
|
||||
import {
|
||||
HLSLoaderClass,
|
||||
HLSPluginOptions,
|
||||
P2PMediaLoaderPluginOptions,
|
||||
PeerTubePlayerConstructorOptions,
|
||||
|
@ -15,9 +14,7 @@ import {
|
|||
} from '../../types'
|
||||
import { getRtcConfig, isSameOrigin } from '../common'
|
||||
import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
|
||||
import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
|
||||
import { SegmentValidator } from '../p2p-media-loader/segment-validator'
|
||||
import debug from 'debug'
|
||||
|
||||
const debugLogger = debug('peertube:player:hls')
|
||||
|
||||
|
@ -58,7 +55,6 @@ export class HLSOptionsBuilder {
|
|||
'filter:internal.player.p2p-media-loader.options.result',
|
||||
this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator })
|
||||
)
|
||||
const loaderBuilder = () => new Engine(p2pMediaLoaderConfig).createLoaderClass() as unknown as HLSLoaderClass
|
||||
|
||||
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
|
||||
requiresUserAuth: this.options.requiresUserAuth,
|
||||
|
@ -73,7 +69,7 @@ export class HLSOptionsBuilder {
|
|||
}
|
||||
|
||||
const hlsjs = {
|
||||
hlsjsConfig: this.getHLSJSOptions(loaderBuilder),
|
||||
hlsjsConfig: this.getHLSJSOptions(p2pMediaLoaderConfig),
|
||||
|
||||
levelLabelHandler: (level: Level, player: videojs.VideoJsPlayer) => {
|
||||
const resolution = Math.min(level.height || 0, level.width || 0)
|
||||
|
@ -99,65 +95,81 @@ export class HLSOptionsBuilder {
|
|||
private getP2PMediaLoaderOptions (options: {
|
||||
redundancyUrlManager: RedundancyUrlManager | null
|
||||
segmentValidator: SegmentValidator | null
|
||||
}): HlsJsEngineSettings {
|
||||
}) {
|
||||
const { redundancyUrlManager, segmentValidator } = options
|
||||
|
||||
let consumeOnly = false
|
||||
let isP2PUploadDisabled = false
|
||||
if (
|
||||
(navigator as any)?.connection?.type === 'cellular' ||
|
||||
peertubeLocalStorage.getItem('peertube-videojs-p2p-consume-only') === 'true' // Use for E2E testing
|
||||
) {
|
||||
logger.info('We are on a cellular connection: disabling seeding.')
|
||||
consumeOnly = true
|
||||
isP2PUploadDisabled = true
|
||||
}
|
||||
|
||||
const trackerAnnounce = this.options.hls.trackerAnnounce
|
||||
const announceTrackers = this.options.hls.trackerAnnounce
|
||||
.filter(t => t.startsWith('ws'))
|
||||
|
||||
const specificLiveOrVODOptions = this.options.isLive
|
||||
? this.getP2PMediaLoaderLiveOptions()
|
||||
: this.getP2PMediaLoaderVODOptions()
|
||||
|
||||
return {
|
||||
loader: {
|
||||
trackerAnnounce,
|
||||
rtcConfig: getRtcConfig(this.options.stunServers),
|
||||
// TODO: remove validateHTTPSegment typing when p2p-media-loader-core is updated
|
||||
const loaderOptions: Partial<StreamConfig> & { validateHTTPSegment: any } = {
|
||||
announceTrackers,
|
||||
rtcConfig: getRtcConfig(this.options.stunServers),
|
||||
|
||||
simultaneousHttpDownloads: 1,
|
||||
httpFailedSegmentTimeout: 1000,
|
||||
httpRequestSetup: (segmentUrlArg, segmentByteRange, requestAbortSignal, requestByteRange) => {
|
||||
const { requiresUserAuth, requiresPassword } = this.options
|
||||
|
||||
xhrSetup: (xhr, url) => {
|
||||
const { requiresUserAuth, requiresPassword } = this.options
|
||||
const segmentUrl = redundancyUrlManager
|
||||
? redundancyUrlManager.buildUrl(segmentUrlArg)
|
||||
: segmentUrlArg
|
||||
|
||||
if (!(requiresUserAuth || requiresPassword)) return
|
||||
const headers = new Headers()
|
||||
|
||||
if (!isSameOrigin(this.options.serverUrl, url)) return
|
||||
if (requestByteRange) {
|
||||
headers.set('Range', `bytes=${requestByteRange.start}-${requestByteRange.end ?? ''}`)
|
||||
}
|
||||
|
||||
if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword())
|
||||
else xhr.setRequestHeader('Authorization', this.options.authorizationHeader())
|
||||
},
|
||||
if (isSameOrigin(this.options.serverUrl, segmentUrl)) {
|
||||
if (requiresPassword) {
|
||||
headers.set('x-peertube-video-password', this.options.videoPassword())
|
||||
} else if (requiresUserAuth) {
|
||||
headers.set('Authorization', this.options.authorizationHeader())
|
||||
}
|
||||
}
|
||||
|
||||
segmentValidator: segmentValidator
|
||||
? segmentValidator.validate.bind(segmentValidator)
|
||||
: null,
|
||||
|
||||
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
|
||||
|
||||
useP2P: this.options.p2pEnabled,
|
||||
consumeOnly,
|
||||
|
||||
...specificLiveOrVODOptions
|
||||
return Promise.resolve(
|
||||
new Request(segmentUrl, {
|
||||
headers,
|
||||
signal: requestAbortSignal
|
||||
})
|
||||
)
|
||||
},
|
||||
segments: {
|
||||
swarmId: this.options.hls.playlistUrl,
|
||||
forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20
|
||||
}
|
||||
|
||||
validateP2PSegment: segmentValidator
|
||||
? segmentValidator.validate.bind(segmentValidator)
|
||||
: null,
|
||||
|
||||
validateHTTPSegment: segmentValidator
|
||||
? segmentValidator.validate.bind(segmentValidator)
|
||||
: null,
|
||||
|
||||
isP2PDisabled: !this.options.p2pEnabled,
|
||||
isP2PUploadDisabled,
|
||||
|
||||
swarmId: this.options.hls.playlistUrl,
|
||||
|
||||
...specificLiveOrVODOptions
|
||||
}
|
||||
|
||||
return { loader: loaderOptions }
|
||||
}
|
||||
|
||||
private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
|
||||
private getP2PMediaLoaderLiveOptions (): Partial<CoreConfig> {
|
||||
const base = {
|
||||
requiredSegmentsPriority: 1
|
||||
highDemandTimeWindow: 4
|
||||
}
|
||||
|
||||
const latencyMode = this.options.liveOptions.latencyMode
|
||||
|
@ -167,8 +179,7 @@ export class HLSOptionsBuilder {
|
|||
return {
|
||||
...base,
|
||||
|
||||
useP2P: false,
|
||||
requiredSegmentsPriority: 10
|
||||
isP2PDisabled: true
|
||||
}
|
||||
|
||||
case LiveVideoLatencyMode.HIGH_LATENCY:
|
||||
|
@ -179,34 +190,39 @@ export class HLSOptionsBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
|
||||
private getP2PMediaLoaderVODOptions (): Partial<CoreConfig> {
|
||||
return {
|
||||
requiredSegmentsPriority: 3,
|
||||
skipSegmentBuilderPriority: 1,
|
||||
highDemandTimeWindow: 15,
|
||||
|
||||
cachedSegmentExpiration: 86400000,
|
||||
cachedSegmentsCount: 100,
|
||||
|
||||
httpDownloadMaxPriority: 9,
|
||||
httpDownloadProbability: 0.06,
|
||||
httpDownloadProbabilitySkipIfNoPeers: true,
|
||||
|
||||
p2pDownloadMaxPriority: 50
|
||||
segmentMemoryStorageLimit: 1024
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getHLSJSOptions (loaderBuilder: () => HLSLoaderClass): HLSPluginOptions {
|
||||
private getHLSJSOptions (p2pMediaLoaderConfig: { loader: CoreConfig }): HLSPluginOptions {
|
||||
const specificLiveOrVODOptions = this.options.isLive
|
||||
? this.getHLSLiveOptions()
|
||||
: this.getHLSVODOptions()
|
||||
|
||||
const base = {
|
||||
const base: HLSPluginOptions = {
|
||||
capLevelToPlayerSize: true,
|
||||
autoStartLoad: false,
|
||||
|
||||
loaderBuilder,
|
||||
p2pMediaLoaderOptions: p2pMediaLoaderConfig.loader,
|
||||
|
||||
// p2p-media-loader uses hls.js loader to fetch m3u8 playlists
|
||||
xhrSetup: (xhr, url) => {
|
||||
const { requiresUserAuth, requiresPassword } = this.options
|
||||
|
||||
if (isSameOrigin(this.options.serverUrl, url)) {
|
||||
if (requiresPassword) {
|
||||
xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword())
|
||||
} else if (requiresUserAuth) {
|
||||
xhr.setRequestHeader('Authorization', this.options.authorizationHeader())
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...specificLiveOrVODOptions
|
||||
}
|