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
This commit is contained in:
Chocobozzz 2025-02-05 06:36:05 +01:00
parent 2fe6ce79f1
commit 50b067f9cd
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
151 changed files with 868 additions and 578 deletions

View File

@ -1,4 +1,4 @@
last 1 Chrome version last 1 Chrome version
last 2 Edge major versions last 2 Edge major versions
Firefox ESR Firefox ESR
ios_saf >= 13.1 ios_saf >= 14

View File

@ -163,7 +163,8 @@
"@typescript-eslint/unbound-method": [ "@typescript-eslint/unbound-method": [
"error", "error",
{ "ignoreStatic": true } { "ignoreStatic": true }
] ],
"import/no-named-default": "off"
} }
}, },
{ {

2
client/.gitignore vendored
View File

@ -12,6 +12,8 @@
/e2e/local.log /e2e/local.log
/e2e/browserstack.err /e2e/browserstack.err
/e2e/screenshots /e2e/screenshots
/src/standalone/player/build
/src/standalone/player/dist
/src/standalone/embed-player-api/build /src/standalone/embed-player-api/build
/src/standalone/embed-player-api/dist /src/standalone/embed-player-api/dist
/e2e/logs /e2e/logs

View File

@ -183,7 +183,10 @@
"includePaths": [ "includePaths": [
"src/sass/include", "src/sass/include",
"." "."
] ],
"sass": {
"silenceDeprecations": [ "import", "mixed-decls", "color-functions", "global-builtin" ]
}
}, },
"assets": [ "assets": [
"src/assets/images", "src/assets/images",
@ -212,7 +215,9 @@
"is-plain-object", "is-plain-object",
"parse-srcset", "parse-srcset",
"deepmerge", "deepmerge",
"core-js/features/reflect" "core-js/features/reflect",
"hammerjs",
"jschannel"
], ],
"scripts": [], "scripts": [],
"extractLicenses": false, "extractLicenses": false,
@ -241,7 +246,7 @@
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "6kb", "maximumWarning": "6kb",
"maximumError": "100kb" "maximumError": "120kb"
} }
], ],
"fileReplacements": [ "fileReplacements": [

View File

@ -5,7 +5,7 @@
"noImplicitAny": false, "noImplicitAny": false,
"esModuleInterop": true, "esModuleInterop": true,
"module": "commonjs", "module": "commonjs",
"target": "ES2015", "target": "ES2018",
"typeRoots": [ "typeRoots": [
"../node_modules/@types", "../node_modules/@types",
"../node_modules" "../node_modules"

View File

@ -24,10 +24,13 @@
"net": false, "net": false,
"stream": false, "stream": false,
"os": false, "os": false,
"http": false,
"dgram": false,
"util": false "util": false
}, },
"workspaces": [ "workspaces": [
"../packages/*" "../packages/*",
"./src/standalone/player"
], ],
"typings": "*.d.ts", "typings": "*.d.ts",
"devDependencies": { "devDependencies": {
@ -55,8 +58,6 @@
"@ngx-loading-bar/http-client": "^7.0.0", "@ngx-loading-bar/http-client": "^7.0.0",
"@ngx-loading-bar/router": "^7.0.0", "@ngx-loading-bar/router": "^7.0.0",
"@peertube/maildev": "^1.2.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", "@peertube/xliffmerge": "^2.0.3",
"@plussub/srt-vtt-parser": "^2.0.5", "@plussub/srt-vtt-parser": "^2.0.5",
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.5",
@ -103,8 +104,11 @@
"markdown-it": "14.1.0", "markdown-it": "14.1.0",
"markdown-it-emoji": "^3.0.0", "markdown-it-emoji": "^3.0.0",
"ngx-uploadx": "^7.0.0", "ngx-uploadx": "^7.0.0",
"p2p-media-loader-core": "^2.1.2",
"p2p-media-loader-hlsjs": "^2.1.2",
"primeng": "^17", "primeng": "^17",
"rxjs": "^7.3.0", "rxjs": "^7.3.0",
"sass-embedded": "^1.83.4",
"sha.js": "^2.4.11", "sha.js": "^2.4.11",
"socket.io-client": "^4.5.4", "socket.io-client": "^4.5.4",
"stylelint": "^16.2.1", "stylelint": "^16.2.1",
@ -115,9 +119,9 @@
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"video.js": "^7.19.2", "video.js": "^7.19.2",
"vite": "^5.3.1", "vite": "^6.0.11",
"vite-plugin-checker": "^0.7.2", "vite-plugin-checker": "^0.8.0",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.23.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"dependencies": {} "dependencies": {}

View File

@ -1,5 +1 @@
@use 'node_modules/video.js/dist/video-js'; @use '../../../standalone/player/build/peertube-player.css';
$assets-path: '../../assets/';
@use '../../../sass/player/index';

View File

@ -47,16 +47,18 @@ import { logger } from '@root-helpers/logger'
import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
import debug from 'debug' import debug from 'debug'
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
import { environment } from '../../../environments/environment'
import { import {
cleanupVideoWatch,
getStoredTheater,
getStoredVideoWatchHistory,
HLSOptions, HLSOptions,
PeerTubePlayer, PeerTubePlayer,
PeerTubePlayerConstructorOptions, PeerTubePlayerConstructorOptions,
PeerTubePlayerLoadOptions, PeerTubePlayerLoadOptions,
PlayerMode, PlayerMode,
videojs videojs
} from '../../../assets/player' } from '@peertube/player'
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
import { environment } from '../../../environments/environment'
import { DateToggleComponent } from '../../shared/shared-main/date/date-toggle.component' import { DateToggleComponent } from '../../shared/shared-main/date/date-toggle.component'
import { PluginPlaceholderComponent } from '../../shared/shared-main/plugins/plugin-placeholder.component' import { PluginPlaceholderComponent } from '../../shared/shared-main/plugins/plugin-placeholder.component'
import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component' import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component'

View File

@ -1,2 +0,0 @@
export * from './peertube-player'
export * from './types'

View File

@ -2,7 +2,7 @@
import debug from 'debug' import debug from 'debug'
import { firstValueFrom, ReplaySubject } from 'rxjs' import { firstValueFrom, ReplaySubject } from 'rxjs'
import { first, shareReplay } from 'rxjs/operators' 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 { getExternalAuthHref, getHookType, internalRunHook } from '@peertube/peertube-core-utils'
import { import {
ClientDoAction, ClientDoAction,
@ -83,6 +83,7 @@ class PluginsManager {
private readonly onFormFields: OnFormFields private readonly onFormFields: OnFormFields
private readonly onSettingsScripts: OnSettingsScripts private readonly onSettingsScripts: OnSettingsScripts
private readonly onClientRoute: OnClientRoute private readonly onClientRoute: OnClientRoute
private readonly backendUrl: string
constructor (options: { constructor (options: {
doAction?: ClientDoAction doAction?: ClientDoAction
@ -90,12 +91,14 @@ class PluginsManager {
onFormFields?: OnFormFields onFormFields?: OnFormFields
onSettingsScripts?: OnSettingsScripts onSettingsScripts?: OnSettingsScripts
onClientRoute?: OnClientRoute onClientRoute?: OnClientRoute
backendUrl?: string
}) { }) {
this.doAction = options.doAction this.doAction = options.doAction
this.peertubeHelpersFactory = options.peertubeHelpersFactory this.peertubeHelpersFactory = options.peertubeHelpersFactory
this.onFormFields = options.onFormFields this.onFormFields = options.onFormFields
this.onSettingsScripts = options.onSettingsScripts this.onSettingsScripts = options.onSettingsScripts
this.onClientRoute = options.onClientRoute this.onClientRoute = options.onClientRoute
this.backendUrl = options.backendUrl
} }
static getPluginPathPrefix (isTheme: boolean) { static getPluginPathPrefix (isTheme: boolean) {
@ -281,7 +284,7 @@ class PluginsManager {
logger.info(`Loading script ${clientScript.script} of plugin ${plugin.name}`) 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) return dynamicImport(absURL)
.then((script: ClientScript) => { .then((script: ClientScript) => {
return script.register({ return script.register({

View File

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

View File

@ -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": {}
}

View File

@ -0,0 +1,5 @@
export * from './peertube-player'
export * from './peertube-player-local-storage'
export * from './types'
import './sass/player.scss'

View File

@ -31,6 +31,8 @@ import './shared/metrics/metrics-plugin'
import './shared/p2p-media-loader/hls-plugin' import './shared/p2p-media-loader/hls-plugin'
import './shared/p2p-media-loader/p2p-media-loader-plugin' import './shared/p2p-media-loader/p2p-media-loader-plugin'
import './shared/web-video/web-video-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 videojs, { VideoJsPlayer } from 'video.js'
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
import { PluginsManager } from '@root-helpers/plugins-manager' 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 { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
import { saveAverageBandwidth } from './peertube-player-local-storage' import { saveAverageBandwidth } from './peertube-player-local-storage'
import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder' 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' import { PeerTubePlayerConstructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)

View File

@ -0,0 +1,2 @@
@use '../../../node_modules/video.js/dist/video-js';
@use './shared/index.scss';

View File

@ -22,5 +22,3 @@ $control-bar-total-height: $control-bar-height - $control-bar-slider-top;
$progress-margin: 10px; $progress-margin: 10px;
$dock-padding: 20px; $dock-padding: 20px;
$assets-path: '../../assets/' !default;

View File

@ -50,8 +50,8 @@ $context-menu-width: 350px;
@each $icon in $icons { @each $icon in $icons {
&[class$="-#{$icon}"] { &[class$="-#{$icon}"] {
mask-image: url('#{$assets-path}/player/images/#{$icon}.svg'); mask-image: url('./svg/#{$icon}.svg');
-webkit-mask-image: url('#{$assets-path}/player/images/#{$icon}.svg'); -webkit-mask-image: url('./svg/#{$icon}.svg');
} }
} }

View File

@ -270,11 +270,11 @@ $chapter-marker-size: 9px;
@include margin-right(2px); @include margin-right(2px);
&.icon-download { &.icon-download {
background-image: url('#{$assets-path}/player/images/arrow-down.svg'); background-image: url('./svg/arrow-down.svg');
} }
&.icon-upload { &.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 {
&.icon-next, &.icon-next,
&.icon-previous { &.icon-previous {
mask-image: url('#{$assets-path}/player/images/next.svg'); mask-image: url('./svg/next.svg');
-webkit-mask-image: url('#{$assets-path}/player/images/next.svg'); -webkit-mask-image: url('./svg/next.svg');
mask-size: cover; mask-size: cover;
-webkit-mask-size: cover; -webkit-mask-size: cover;
@ -319,7 +319,7 @@ $chapter-marker-size: 9px;
width: $control-bar-icon-size; width: $control-bar-icon-size;
height: $control-bar-icon-size; height: $control-bar-icon-size;
vertical-align: middle; vertical-align: middle;
background: url('#{$assets-path}/player/images/volume.svg') no-repeat; background: url('./svg/volume.svg') no-repeat;
background-size: contain; background-size: contain;
&::before { &::before {
@ -328,7 +328,7 @@ $chapter-marker-size: 9px;
} }
&.vjs-vol-0 .vjs-icon-placeholder { &.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; background-size: contain;
} }
} }
@ -415,7 +415,7 @@ $chapter-marker-size: 9px;
height: $control-bar-icon-size - 4px; height: $control-bar-icon-size - 4px;
width: $control-bar-icon-size - 4px; width: $control-bar-icon-size - 4px;
vertical-align: middle; vertical-align: middle;
background: url('#{$assets-path}/player/images/settings.svg') no-repeat; background: url('./svg/settings.svg') no-repeat;
background-size: contain; background-size: contain;
&::before { &::before {
@ -448,7 +448,7 @@ $chapter-marker-size: 9px;
width: $control-bar-icon-size - 4px; width: $control-bar-icon-size - 4px;
height: $control-bar-icon-size - 4px; height: $control-bar-icon-size - 4px;
vertical-align: middle; vertical-align: middle;
background: url('#{$assets-path}/player/images/theater.svg') no-repeat; background: url('./svg/theater.svg') no-repeat;
background-size: contain; background-size: contain;
&::before { &::before {
@ -493,7 +493,7 @@ $chapter-marker-size: 9px;
width: $control-bar-icon-size; width: $control-bar-icon-size;
height: $control-bar-icon-size; height: $control-bar-icon-size;
vertical-align: middle; vertical-align: middle;
background: url('#{$assets-path}/player/images/fullscreen.svg') no-repeat; background: url('./svg/fullscreen.svg') no-repeat;
background-size: contain; background-size: contain;
&::before { &::before {
@ -515,14 +515,6 @@ $chapter-marker-size: 9px;
.vjs-theater-control { .vjs-theater-control {
display: none; 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 { .video-js.vjs-peertube-skin.vjs-size-570 .vjs-control-bar {
@ -540,6 +532,12 @@ $chapter-marker-size: 9px;
.vjs-peertube-displayed { .vjs-peertube-displayed {
display: none !important; display: none !important;
} }
.icon,
.download-speed-text,
.upload-speed-text {
display: none !important;
}
} }
.vjs-peertube-link { .vjs-peertube-link {

View File

@ -60,7 +60,7 @@ body {
.vjs-icon-placeholder::before { .vjs-icon-placeholder::before {
content: ''; 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); @include big-play-button-triangle-size(45px);
} }

View File

@ -44,8 +44,8 @@ $playlist-menu-width: 350px;
} }
.cross { .cross {
mask-image: url('#{$assets-path}/images/feather/x.svg'); mask-image: url('./svg/x.svg');
-webkit-mask-image: url('#{$assets-path}/images/feather/x.svg'); -webkit-mask-image: url('./svg/x.svg');
mask-size: cover; mask-size: cover;
-webkit-mask-size: cover; -webkit-mask-size: cover;
@ -92,8 +92,8 @@ $playlist-menu-width: 350px;
} }
.vjs-playlist-icon { .vjs-playlist-icon {
mask-image: url('#{$assets-path}/images/feather/playlists.svg'); mask-image: url('./svg/playlists.svg');
-webkit-mask-image: url('#{$assets-path}/images/feather/playlists.svg'); -webkit-mask-image: url('./svg/playlists.svg');
mask-size: cover; mask-size: cover;
-webkit-mask-size: cover; -webkit-mask-size: cover;

View File

@ -145,7 +145,7 @@ $setting-transition-easing: ease-out;
position: absolute; position: absolute;
content: ' '; content: ' ';
margin-top: 1px; margin-top: 1px;
background-image: url('#{$assets-path}/player/images/tick-white.svg'); background-image: url('./svg/tick-white.svg');
@include icon(15px); @include icon(15px);
@include left(15px); @include left(15px);

View File

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 893 B

View File

Before

Width:  |  Height:  |  Size: 887 B

After

Width:  |  Height:  |  Size: 887 B

View File

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View File

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 347 B

View File

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 355 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 702 B

View File

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 692 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

View File

@ -5,6 +5,10 @@ import { logger } from '@root-helpers/logger'
import Hlsjs, { ErrorData, Level, LevelSwitchingData, ManifestParsedData } from 'hls.js' import Hlsjs, { ErrorData, Level, LevelSwitchingData, ManifestParsedData } from 'hls.js'
import videojs from 'video.js' import videojs from 'video.js'
import { HLSPluginOptions, HlsjsConfigHandlerOptions, PeerTubeResolution, VideoJSTechHLS } from '../../types' 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 ErrorCounts = {
[ type: string ]: number [ type: string ]: number
@ -14,8 +18,6 @@ type ErrorCounts = {
// Source handler registration // Source handler registration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type HookFn = (player: videojs.Player, hljs: Hlsjs) => void
let alreadyRegistered = false let alreadyRegistered = false
const registerSourceHandler = function (vjs: typeof videojs) { const registerSourceHandler = function (vjs: typeof videojs) {
@ -110,8 +112,6 @@ videojs.registerPlugin('hlsjs', HLSJSConfigHandler)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class Html5Hlsjs { export class Html5Hlsjs {
private static hooks: { [id: string]: HookFn[] } = {}
private readonly videoElement: HTMLVideoElement private readonly videoElement: HTMLVideoElement
private readonly errorCounts: ErrorCounts = {} private readonly errorCounts: ErrorCounts = {}
private readonly player: videojs.Player private readonly player: videojs.Player
@ -121,7 +121,7 @@ export class Html5Hlsjs {
private maxNetworkErrorRecovery = 5 private maxNetworkErrorRecovery = 5
private hls: Hlsjs private hls: HlsWithP2PInstance<Hlsjs>
private hlsjsConfig: HLSPluginOptions = null private hlsjsConfig: HLSPluginOptions = null
private _duration: number = null private _duration: number = null
@ -410,10 +410,15 @@ export class Html5Hlsjs {
this.videoElement.addEventListener('play', this.handlers.play) this.videoElement.addEventListener('play', this.handlers.play)
} }
const loader = this.hlsjsConfig.loaderBuilder() this.hls = new HlsWithP2P({
this.hls = new Hlsjs({ ...this.hlsjsConfig, loader }) ...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.ERROR, (event, data) => this._onError(event, data))
this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(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.hlsjsConfig.autoStartLoad = true
this.player.autoplay('play') this.player.autoplay('play')
const loader = this.hlsjsConfig.loaderBuilder() this.hls = new HlsWithP2P({
this.hls = new Hlsjs({ ...omit(this.hlsjsConfig, [ 'p2pMediaLoaderOptions' ]),
...this.hlsjsConfig,
loader, p2p: {
core: this.hlsjsConfig.p2pMediaLoaderOptions
},
startPosition: this.duration() === Infinity startPosition: this.duration() === Infinity
? undefined ? undefined
: currentTime, : currentTime,
startLevel 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.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
this.registerLevelEventSwitch() this.registerLevelEventSwitch()

View File

@ -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 { addQueryParams } from '@peertube/peertube-core-utils'
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
import debug from 'debug' 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 videojs from 'video.js'
import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
import { SettingsButton } from '../settings/settings-menu-button' import { SettingsButton } from '../settings/settings-menu-button'
@ -14,8 +14,7 @@ const Plugin = videojs.getPlugin('plugin')
class P2pMediaLoaderPlugin extends Plugin { class P2pMediaLoaderPlugin extends Plugin {
declare private readonly options: P2PMediaLoaderPluginOptions declare private readonly options: P2PMediaLoaderPluginOptions
declare private hlsjs: Hlsjs declare private hlsjs: HlsWithP2PInstance<Hlsjs>
declare private p2pEngine: Engine
declare private statsP2PBytes: { declare private statsP2PBytes: {
pendingDownload: number[] pendingDownload: number[]
pendingUpload: number[] pendingUpload: number[]
@ -33,6 +32,9 @@ class P2pMediaLoaderPlugin extends Plugin {
declare private liveEnded: boolean declare private liveEnded: boolean
declare private connectedPeers: Set<string>
declare private totalHTTPPeers: number
constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) { constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
super(player) super(player)
@ -76,15 +78,13 @@ class P2pMediaLoaderPlugin extends Plugin {
} }
{ {
const onHLSJSInitialized = (_: any, { hlsjs, engine }: { hlsjs: Hlsjs, engine: Engine }) => { const onHLSJSInitialized = (_: any, { hlsjs }: { hlsjs: HlsWithP2PInstance<Hlsjs> }) => {
this.p2pEngine?.removeAllListeners() this.hlsjs?.p2pEngine?.destroy()
this.p2pEngine?.destroy()
clearInterval(this.networkInfoInterval) clearInterval(this.networkInfoInterval)
this.hlsjs = hlsjs 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()) player.ready(() => this.initializePlugin())
} }
@ -116,8 +116,7 @@ class P2pMediaLoaderPlugin extends Plugin {
} }
dispose () { dispose () {
this.p2pEngine?.removeAllListeners() this.hlsjs?.p2pEngine?.destroy()
this.p2pEngine?.destroy()
this.hlsjs?.destroy() this.hlsjs?.destroy()
this.options.segmentValidator?.destroy() this.options.segmentValidator?.destroy()
@ -152,15 +151,22 @@ class P2pMediaLoaderPlugin extends Plugin {
} }
private initializePlugin () { 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) => { const segment = details.segment
if (navigator.onLine === false || this.liveEnded) return logger.clientError(`Segment ${segment.runtimeId} error.`, details)
logger.clientError(`Segment ${segment.id} error.`, err)
if (this.options.redundancyUrlManager) { 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() ? this.options.redundancyUrlManager.countBaseUrls()
: 0 : 0
this.statsP2PBytes.peersWithWebSeed = 1 + redundancyUrlsCount this.totalHTTPPeers = 1 + redundancyUrlsCount
this.statsP2PBytes.peersWithWebSeed = this.totalHTTPPeers
this.runStats() this.runStats()
@ -200,30 +207,44 @@ class P2pMediaLoaderPlugin extends Plugin {
} }
private runStats () { 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 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
elem.pendingDownload.push(bytes) elem.pendingDownload.push(bytes)
elem.totalDownload += bytes elem.totalDownload += bytes
}) })
this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => { this.hlsjs.p2pEngine.addEventListener('onChunkUploaded', (bytes: number) => {
if (method !== 'p2p') {
logger.error(`Received upload from unknown method ${method}`)
return
}
this.statsP2PBytes.pendingUpload.push(bytes) this.statsP2PBytes.pendingUpload.push(bytes)
this.statsP2PBytes.totalUpload += bytes this.statsP2PBytes.totalUpload += bytes
}) })
this.p2pEngine.on(Events.PeerConnect, () => { this.hlsjs.p2pEngine.addEventListener('onPeerConnect', peer => {
this.statsP2PBytes.peersWithWebSeed++ if (peer.streamType !== 'main') return
this.statsP2PBytes.peersP2POnly++
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.hlsjs.p2pEngine.addEventListener('onPeerClose', peer => {
this.statsP2PBytes.peersWithWebSeed-- if (peer.streamType !== 'main') return
this.statsP2PBytes.peersP2POnly--
this.connectedPeers.delete(peer.peerId)
this.statsP2PBytes.peersP2POnly = this.connectedPeers.size
this.statsP2PBytes.peersWithWebSeed = this.totalHTTPPeers + this.statsP2PBytes.peersP2POnly
}) })
this.networkInfoInterval = setInterval(() => { this.networkInfoInterval = setInterval(() => {

View File

@ -1,22 +1,32 @@
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
class RedundancyUrlManager { class RedundancyUrlManager {
private map = new Map<string, string>()
constructor (private baseUrls: string[] = []) { constructor (private baseUrls: string[] = []) {
// empty // empty
} }
removeBySegmentUrl (segmentUrl: string) { onSegmentError (segmentUrl: string) {
const baseUrl = getBaseUrl(segmentUrl) if (!this.map.has(segmentUrl)) return
const customSegmentUrl = this.map.get(segmentUrl)
this.map.delete(segmentUrl)
const baseUrl = getBaseUrl(customSegmentUrl)
const oldLength = baseUrl.length const oldLength = baseUrl.length
this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/') this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/')
if (oldLength !== this.baseUrls.length) { 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) { buildUrl (url: string) {
const max = this.baseUrls.length + 1 const max = this.baseUrls.length + 1
const i = this.getRandomInt(max) const i = this.getRandomInt(max)
@ -26,7 +36,11 @@ class RedundancyUrlManager {
const newBaseUrl = this.baseUrls[i] const newBaseUrl = this.baseUrls[i]
const slashPart = newBaseUrl.endsWith('/') ? '' : '/' const slashPart = newBaseUrl.endsWith('/') ? '' : '/'
return newBaseUrl + slashPart + getFilename(url) const newUrl = newBaseUrl + slashPart + getFilename(url)
this.map.set(url, newUrl)
return newUrl
} }
countBaseUrls () { countBaseUrls () {

View File

@ -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' import { RedundancyUrlManager } from './redundancy-url-manager'
export function segmentUrlBuilderFactory (redundancyUrlManager: RedundancyUrlManager | null) { export function segmentUrlBuilderFactory (redundancyUrlManager: RedundancyUrlManager | null) {

View File

@ -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 { logger } from '@root-helpers/logger'
import { wait } from '@root-helpers/utils' import { wait } from '@root-helpers/utils'
import { removeQueryParams } from '@peertube/peertube-core-utils'
import { isSameOrigin } from '../common'
import debug from 'debug' import debug from 'debug'
import { isSameOrigin } from '../common'
const debugLogger = debug('peertube:player:segment-validator') const debugLogger = debug('peertube:player:segment-validator')
@ -12,9 +12,6 @@ type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string
const maxRetries = 10 const maxRetries = 10
export class SegmentValidator { export class SegmentValidator {
private readonly bytesRangeRegex = /bytes=(\d+)-(\d+)/
private destroyed = false private destroyed = false
private segmentJSONPromise: Promise<SegmentsJSON> private segmentJSONPromise: Promise<SegmentsJSON>
@ -29,17 +26,18 @@ export class SegmentValidator {
}) { }) {
} }
async validate (segment: Segment, _method: string, _peerId: string, retry = 1) { async validate (url: string, byteRange: ByteRange | undefined, data: ArrayBuffer, retry = 1): Promise<boolean> {
if (this.destroyed) return if (this.destroyed) return false
this.loadSha256SegmentsPromiseIfNeeded() this.loadSha256SegmentsPromiseIfNeeded()
const filename = removeQueryParams(segment.url).split('/').pop() const filename = removeQueryParams(url).split('/').pop()
const segmentValue = (await this.segmentJSONPromise)[filename] const segmentValue = (await this.segmentJSONPromise)[filename]
if (!segmentValue && retry > maxRetries) { 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) { if (!segmentValue) {
@ -49,9 +47,7 @@ export class SegmentValidator {
this.loadSha256SegmentsPromise() this.loadSha256SegmentsPromise()
await this.validate(segment, _method, _peerId, retry + 1) return this.validate(url, byteRange, data, retry + 1)
return
} }
let hashShouldBe: string let hashShouldBe: string
@ -60,24 +56,25 @@ export class SegmentValidator {
if (typeof segmentValue === 'string') { if (typeof segmentValue === 'string') {
hashShouldBe = segmentValue hashShouldBe = segmentValue
} else { } else {
const captured = this.bytesRangeRegex.exec(segment.range) range = byteRange.start + '-' + byteRange.end
range = captured[1] + '-' + captured[2]
hashShouldBe = segmentValue[range] hashShouldBe = segmentValue[range]
} }
if (hashShouldBe === undefined) { 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) { if (calculatedSha !== hashShouldBe) {
throw new Error( logger.clientError(
`Hashes does not correspond for segment ${filename}/${range}` + `Hashes does not correspond for segment ${filename}/${range} (expected: ${hashShouldBe} instead of ${calculatedSha})`
`(expected: ${hashShouldBe} instead of ${calculatedSha})`
) )
return true
} }
} }
@ -106,7 +103,7 @@ export class SegmentValidator {
return fetch(this.options.segmentsSha256Url, { headers }) return fetch(this.options.segmentsSha256Url, { headers })
.then(res => res.json() as Promise<SegmentsJSON>) .then(res => res.json() as Promise<SegmentsJSON>)
.catch(err => { .catch(err => {
logger.error('Cannot get sha256 segments', err) logger.clientError('Cannot get sha256 segments', err)
return {} return {}
}) })
} }

View File

@ -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 { getResolutionAndFPSLabel, getResolutionLabel } from '@peertube/peertube-core-utils'
import { LiveVideoLatencyMode } from '@peertube/peertube-models' import { LiveVideoLatencyMode } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import debug from 'debug'
import { Level } from 'hls.js' import { Level } from 'hls.js'
import type { CoreConfig, StreamConfig } from 'p2p-media-loader-core'
import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' import { getAverageBandwidthInStore } from '../../peertube-player-local-storage'
import { import {
HLSLoaderClass,
HLSPluginOptions, HLSPluginOptions,
P2PMediaLoaderPluginOptions, P2PMediaLoaderPluginOptions,
PeerTubePlayerConstructorOptions, PeerTubePlayerConstructorOptions,
@ -15,9 +14,7 @@ import {
} from '../../types' } from '../../types'
import { getRtcConfig, isSameOrigin } from '../common' import { getRtcConfig, isSameOrigin } from '../common'
import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' 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 { SegmentValidator } from '../p2p-media-loader/segment-validator'
import debug from 'debug'
const debugLogger = debug('peertube:player:hls') const debugLogger = debug('peertube:player:hls')
@ -58,7 +55,6 @@ export class HLSOptionsBuilder {
'filter:internal.player.p2p-media-loader.options.result', 'filter:internal.player.p2p-media-loader.options.result',
this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator }) this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator })
) )
const loaderBuilder = () => new Engine(p2pMediaLoaderConfig).createLoaderClass() as unknown as HLSLoaderClass
const p2pMediaLoader: P2PMediaLoaderPluginOptions = { const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
requiresUserAuth: this.options.requiresUserAuth, requiresUserAuth: this.options.requiresUserAuth,
@ -73,7 +69,7 @@ export class HLSOptionsBuilder {
} }
const hlsjs = { const hlsjs = {
hlsjsConfig: this.getHLSJSOptions(loaderBuilder), hlsjsConfig: this.getHLSJSOptions(p2pMediaLoaderConfig),
levelLabelHandler: (level: Level, player: videojs.VideoJsPlayer) => { levelLabelHandler: (level: Level, player: videojs.VideoJsPlayer) => {
const resolution = Math.min(level.height || 0, level.width || 0) const resolution = Math.min(level.height || 0, level.width || 0)
@ -99,65 +95,81 @@ export class HLSOptionsBuilder {
private getP2PMediaLoaderOptions (options: { private getP2PMediaLoaderOptions (options: {
redundancyUrlManager: RedundancyUrlManager | null redundancyUrlManager: RedundancyUrlManager | null
segmentValidator: SegmentValidator | null segmentValidator: SegmentValidator | null
}): HlsJsEngineSettings { }) {
const { redundancyUrlManager, segmentValidator } = options const { redundancyUrlManager, segmentValidator } = options
let consumeOnly = false let isP2PUploadDisabled = false
if ( if (
(navigator as any)?.connection?.type === 'cellular' || (navigator as any)?.connection?.type === 'cellular' ||
peertubeLocalStorage.getItem('peertube-videojs-p2p-consume-only') === 'true' // Use for E2E testing peertubeLocalStorage.getItem('peertube-videojs-p2p-consume-only') === 'true' // Use for E2E testing
) { ) {
logger.info('We are on a cellular connection: disabling seeding.') 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')) .filter(t => t.startsWith('ws'))
const specificLiveOrVODOptions = this.options.isLive const specificLiveOrVODOptions = this.options.isLive
? this.getP2PMediaLoaderLiveOptions() ? this.getP2PMediaLoaderLiveOptions()
: this.getP2PMediaLoaderVODOptions() : this.getP2PMediaLoaderVODOptions()
return { // TODO: remove validateHTTPSegment typing when p2p-media-loader-core is updated
loader: { const loaderOptions: Partial<StreamConfig> & { validateHTTPSegment: any } = {
trackerAnnounce, announceTrackers,
rtcConfig: getRtcConfig(this.options.stunServers), rtcConfig: getRtcConfig(this.options.stunServers),
simultaneousHttpDownloads: 1, httpRequestSetup: (segmentUrlArg, segmentByteRange, requestAbortSignal, requestByteRange) => {
httpFailedSegmentTimeout: 1000,
xhrSetup: (xhr, url) => {
const { requiresUserAuth, requiresPassword } = this.options const { requiresUserAuth, requiresPassword } = this.options
if (!(requiresUserAuth || requiresPassword)) return const segmentUrl = redundancyUrlManager
? redundancyUrlManager.buildUrl(segmentUrlArg)
: segmentUrlArg
if (!isSameOrigin(this.options.serverUrl, url)) return const headers = new Headers()
if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword()) if (requestByteRange) {
else xhr.setRequestHeader('Authorization', this.options.authorizationHeader()) headers.set('Range', `bytes=${requestByteRange.start}-${requestByteRange.end ?? ''}`)
}
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())
}
}
return Promise.resolve(
new Request(segmentUrl, {
headers,
signal: requestAbortSignal
})
)
}, },
segmentValidator: segmentValidator validateP2PSegment: segmentValidator
? segmentValidator.validate.bind(segmentValidator) ? segmentValidator.validate.bind(segmentValidator)
: null, : null,
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), validateHTTPSegment: segmentValidator
? segmentValidator.validate.bind(segmentValidator)
: null,
useP2P: this.options.p2pEnabled, isP2PDisabled: !this.options.p2pEnabled,
consumeOnly, isP2PUploadDisabled,
swarmId: this.options.hls.playlistUrl,
...specificLiveOrVODOptions ...specificLiveOrVODOptions
},
segments: {
swarmId: this.options.hls.playlistUrl,
forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20
}
}
} }
private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> { return { loader: loaderOptions }
}
private getP2PMediaLoaderLiveOptions (): Partial<CoreConfig> {
const base = { const base = {
requiredSegmentsPriority: 1 highDemandTimeWindow: 4
} }
const latencyMode = this.options.liveOptions.latencyMode const latencyMode = this.options.liveOptions.latencyMode
@ -167,8 +179,7 @@ export class HLSOptionsBuilder {
return { return {
...base, ...base,
useP2P: false, isP2PDisabled: true
requiredSegmentsPriority: 10
} }
case LiveVideoLatencyMode.HIGH_LATENCY: case LiveVideoLatencyMode.HIGH_LATENCY:
@ -179,34 +190,39 @@ export class HLSOptionsBuilder {
} }
} }
private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> { private getP2PMediaLoaderVODOptions (): Partial<CoreConfig> {
return { return {
requiredSegmentsPriority: 3, highDemandTimeWindow: 15,
skipSegmentBuilderPriority: 1,
cachedSegmentExpiration: 86400000, segmentMemoryStorageLimit: 1024
cachedSegmentsCount: 100,
httpDownloadMaxPriority: 9,
httpDownloadProbability: 0.06,
httpDownloadProbabilitySkipIfNoPeers: true,
p2pDownloadMaxPriority: 50
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
private getHLSJSOptions (loaderBuilder: () => HLSLoaderClass): HLSPluginOptions { private getHLSJSOptions (p2pMediaLoaderConfig: { loader: CoreConfig }): HLSPluginOptions {
const specificLiveOrVODOptions = this.options.isLive const specificLiveOrVODOptions = this.options.isLive
? this.getHLSLiveOptions() ? this.getHLSLiveOptions()
: this.getHLSVODOptions() : this.getHLSVODOptions()
const base = { const base: HLSPluginOptions = {
capLevelToPlayerSize: true, capLevelToPlayerSize: true,
autoStartLoad: false, 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 ...specificLiveOrVODOptions
} }

Some files were not shown because too many files have changed in this diff Show More