Compare commits

...

8 Commits

23 changed files with 881 additions and 42 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## Differences from upstream
### vtopia1
+ Refactor wording involving "live" to "stream" where appropriate (f10a5b0)
+ Utilize VHS and [Owncast latency compensation](https://github.com/owncast/vhs-latency-compensator) (9bf55ea)
## v4.3.0
### Maintenance

View File

@ -1,16 +1,16 @@
{
"name": "peertube-client",
"version": "4.3.0",
"version": "4.3.0+vtopia1",
"private": true,
"license": "AGPL-3.0",
"author": {
"name": "Chocobozzz",
"email": "chocobozzz@framasoft.org",
"url": "http://github.com/Chocobozzz"
"name": "VtopiaLIVE",
"email": "admin@votpia.live",
"url": "https://social.vtopia.live/@staff"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Chocobozzz/PeerTube.git"
"url": "git+https://code.vtopia.live/Vtopia/VeeTube.git"
},
"scripts": {
"lint": "npm run lint-ts && npm run lint-scss",

View File

@ -31,6 +31,7 @@ import {
HTMLServerConfig,
HttpStatusCode,
LiveVideo,
LiveVideoLatencyMode,
PeerTubeProblemDocument,
ServerErrorCode,
VideoCaption,
@ -671,8 +672,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader'
else mode = 'webtorrent'
} else {
if (video.hasHlsPlaylist()) mode = 'p2p-media-loader'
else mode = 'webtorrent'
if (video.hasHlsPlaylist()) {
if (liveOptions?.latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) {
mode = 'lowlatency'
} else {
mode = 'p2p-media-loader'
}
}
else {
mode = 'webtorrent'
}
}
// p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available
@ -694,6 +703,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
Object.assign(options, { p2pMediaLoader })
}
if (mode === 'lowlatency') {
const hlsPlaylist = video.getHlsPlaylist()
const vhsMediaLoader = {
playlistUrl: hlsPlaylist.playlistUrl,
}
Object.assign(options, { vhsMediaLoader })
}
return { playerMode: mode, playerOptions: options }
}

View File

@ -70,6 +70,13 @@ export class PeertubePlayerManager {
this.p2pMediaLoaderModule = p2pMediaLoaderModule
}
if (mode === 'lowlatency') {
await Promise.all([
import('./shared/vhs-loader/vhs-loader-plugin'),
import('./shared/vhs-loader/latencyCompensator')
])
Object.assign(options.common, { p2pEnabled: false })
}
await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)

View File

@ -74,7 +74,7 @@ export class ControlBarOptionsBuilder {
private getSettingsButton () {
const settingEntries: string[] = []
settingEntries.push('playbackRateMenuButton')
if (!this.options.isLive) settingEntries.push('playbackRateMenuButton')
if (this.options.captions === true) settingEntries.push('captionsButton')

View File

@ -9,6 +9,7 @@ import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../t
import { ControlBarOptionsBuilder } from './control-bar-options-builder'
import { HLSOptionsBuilder } from './hls-options-builder'
import { WebTorrentOptionsBuilder } from './webtorrent-options-builder'
import { VHSOptionsBuilder } from './vhs-options-builder'
export class ManagerOptionsBuilder {
@ -72,6 +73,12 @@ export class ManagerOptionsBuilder {
// WebTorrent plugin handles autoplay, because we do some hackish stuff in there
autoplay = false
} else if (this.mode === 'lowlatency') {
const vhsOptionsBuilder = new VHSOptionsBuilder(this.options)
const options = vhsOptionsBuilder.getPluginOptions()
Object.assign(plugins, pick(options, ['vhsLoader', 'latencyCompensator']))
Object.assign(html5, options.html5)
}
const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode)
@ -92,7 +99,7 @@ export class ManagerOptionsBuilder {
poster: commonOptions.poster,
inactivityTimeout: commonOptions.inactivityTimeout,
playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
playbackRates: commonOptions.isLive !== true ? [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ] : null,
plugins,

View File

@ -0,0 +1,44 @@
import { PeertubePlayerManagerOptions, LatencyCompensatorOptions } from '../../types/manager-options'
export class VHSOptionsBuilder {
constructor(
private options: PeertubePlayerManagerOptions,
) {
}
getPluginOptions() {
const vhsLoader = {
type: 'application/x-mpegURL',
src: this.options.vhsMediaLoader.playlistUrl,
}
const latencyCompensator: LatencyCompensatorOptions = {
rebuffer_event_limit: 3,
min_buffer_duration: 100,
max_speedup_rate: 1.03,
max_speedup_ramp: 0.01,
timeout_duration: 30 * 1000,
check_timer_interval: 3 * 1000,
buffering_amnesty_duration: 3 * 1000 * 60,
required_bandwidth_ratio: 2.0,
highest_latency_segment_length_multiplier: 2,
lowest_latency_segment_length_multiplier: 1,
min_latency: 2 * 1000,
max_latency: 8 * 1000,
max_jump_latency: 10 * 1000,
max_jump_frequency: 30 * 1000,
startup_wait_time: 10 * 1000,
}
const html5 = {
vhs: {
allowSeeksWithinUnsafeLiveWindow: true,
}
}
return { vhsLoader, html5, latencyCompensator }
}
}

View File

@ -59,6 +59,11 @@ class MetricsPlugin extends Plugin {
fps = framerate
? parseInt(framerate, 10)
: undefined
} else if (this.mode == 'lowlatency') {
const level = this.player.vhsLoader().getCurrentLevel()
if (!level) return
resolution = Math.min(level.height || 0, level.width || 0)
} else { // webtorrent
const videoFile = this.player.webtorrent().getCurrentVideoFile()
if (!videoFile) return

View File

@ -31,12 +31,14 @@ const registerSourceHandler = function (vjs: typeof videojs) {
// FIXME: typings
(html5 as any).registerSourceHandler({
canHandleSource: function (source: videojs.Tech.SourceObject) {
const hlsTypeRE = /^application\/x-mpegURL|application\/vnd\.apple\.mpegurl$/i
const hlsExtRE = /\.m3u8/i
canHandleSource: function(source: videojs.Tech.SourceObject, options: HlsjsConfigHandlerOptions) {
if (options.hlsjsConfig) { // Only consider using hlsjs if it is configured
const hlsTypeRE = /^application\/x-mpegURL|application\/vnd\.apple\.mpegurl$/i
const hlsExtRE = /\.m3u8/i
if (hlsTypeRE.test(source.type)) return 'probably'
if (hlsExtRE.test(source.src)) return 'maybe'
if (hlsTypeRE.test(source.type)) return 'probably'
if (hlsExtRE.test(source.src)) return 'maybe'
}
return ''
},

View File

@ -7,7 +7,7 @@ import { bytes } from '../common'
interface StatsCardOptions extends videojs.ComponentOptions {
videoUUID: string
videoIsLive: boolean
mode: 'webtorrent' | 'p2p-media-loader'
mode: 'webtorrent' | 'p2p-media-loader' | 'lowlatency'
p2pEnabled: boolean
}
@ -34,7 +34,7 @@ class StatsCard extends Component {
updateInterval: any
mode: 'webtorrent' | 'p2p-media-loader'
mode: 'webtorrent' | 'p2p-media-loader' | 'lowlatency'
metadataStore: any = {}
@ -120,9 +120,10 @@ class StatsCard extends Component {
this.updateInterval = setInterval(async () => {
try {
const options = this.mode === 'p2p-media-loader'
? this.buildHLSOptions()
: await this.buildWebTorrentOptions() // Default
let options;
if (this.mode === 'p2p-media-loader') options = this.buildHLSOptions()
else if (this.mode === 'lowlatency') options = this.buildVHSOptions()
else options = await this.buildWebTorrentOptions()
this.populateInfoValues(options)
} catch (err) {
@ -170,6 +171,33 @@ class StatsCard extends Component {
}
}
private buildVHSOptions () {
const mediaLoader = this.player_.vhsLoader()
const level = mediaLoader.getCurrentLevel()
const codecs = level?.playlist.attributes.CODECS
const resolution = level ? `${level.height}p` : undefined
const buffer = this.timeRangesToString(this.player().buffered())
let progress: number
let latency: string
if (this.options_.videoIsLive) {
latency = secondsToTime(mediaLoader.getLiveLatency())
} else {
progress = this.player().bufferedPercent()
}
return {
playerNetworkInfo: this.playerNetworkInfo,
resolution,
codecs,
buffer,
latency,
progress
}
}
private async buildWebTorrentOptions () {
const videoFile = this.player_.webtorrent().getCurrentVideoFile()

View File

@ -0,0 +1,32 @@
import videojs from 'video.js'
import { VhsMetadataCue, VideoJSTechVHS } from '../../types'
export function getCurrentlyPlayingSegment (player: videojs.Player) {
const currentCue = getCurrentMetadataCue(player)
if (!currentCue) return undefined
const tech = player.tech(false) as VideoJSTechVHS
const currentRep = tech.vhs.representations().find(
(level) => level.playlist.id == currentCue.value.playlist
)
const currentSegment = currentRep.playlist.segments.find(
(segment) => currentCue.value.start == segment.start
)
return currentSegment
}
export function getCurrentMetadataCue (player: videojs.Player) {
const tracks = player.textTracks()
let currentCue: VhsMetadataCue
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].label === 'segment-metadata') {
currentCue = tracks[i].activeCues[0] as VhsMetadataCue;
}
}
return currentCue
}

View File

@ -0,0 +1,439 @@
/*
MIT License
Copyright (c) 2020 Gabe Kangas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
The Owncast Latency Compensator.
It will try to slowly adjust the playback rate to enable the player to get
further into the future, with the goal of being as close to the live edge as
possible, without causing any buffering events.
How does latency occur?
Two pieces are at play. The first being the server. The larger each segment is
that is being generated by Owncast, the larger gap you are going to be from
live when you begin playback.
Second is your media player.
The player tries to play every segment as it comes in.
However, your computer is not always 100% in playing things in real time, and
there are natural stutters in playback. So if one frame is delayed in playback
you may not see it visually, but now you're one frame behind. Eventually this
can compound and you can be many seconds behind.
How to help with this? The Owncast Latency Compensator will:
- Determine the start (max) and end (min) latency values.
- Keep an eye on download speed and stop compensating if it drops too low.
- Limit the playback speedup rate so it doesn't sound weird by jumping speeds.
- Force a large jump to into the future once compensation begins.
- Dynamically calculate the speedup rate based on network speed.
- Pause the compensation if buffering events occur.
- Completely give up on all compensation if too many buffering events occur.
*/
/*
Modifications from upstream:
+ Typescript (wont be upstreamed)
+ Constants => paramaters
+ Don't assume the live edge is the current time (better handle sub-1x transcodes)
*/
import videojs from 'video.js'
import { LatencyCompensatorOptions, VideoJSTechVHS } from '../../types';
import { getCurrentlyPlayingSegment } from './common'
const Plugin = videojs.getPlugin('plugin')
const logger = videojs.log.createLogger('LatencyCompensatorPlugin')
class LatencyCompensatorPlugin extends Plugin {
player: videojs.Player
enabled = false
running = false
inTimeout = false
jumpingToLiveIgnoreBuffer = false
timeoutTimer: number
checkTimer: number
bufferingCounter = 0
bufferingTimer: number
playbackRate = 1.0
lastJumpOccurred: number
startupTime: Date
performedInitialLiveJump = false
private readonly options: LatencyCompensatorOptions
constructor(player: videojs.Player, options?: LatencyCompensatorOptions) {
super(player)
this.player = player
this.options = options
this.lastJumpOccurred = null;
this.startupTime = new Date();
this.player.on('playing', this.handlePlaying.bind(this));
this.player.on('error', this.handleError.bind(this));
this.player.on('waiting', this.handleBuffering.bind(this));
this.player.on('stalled', this.handleBuffering.bind(this));
this.player.on('ended', this.handleEnded.bind(this));
this.player.on('canplaythrough', this.handlePlaying.bind(this));
this.player.on('canplay', this.handlePlaying.bind(this));
}
dispose() {
this.disable()
}
// This is run on a timer to check if we should be compensating for latency.
check() {
// We have an arbitrary delay at startup to allow the player to run
// normally and hopefully get a bit of a buffer of segments before we
// start messing with it.
if (new Date().getTime() - this.startupTime.getTime() < this.options.startup_wait_time) {
return;
}
// If we're paused then do nothing.
if (this.player.paused()) {
return;
}
if (this.player.seeking()) {
return;
}
if (this.inTimeout) {
return;
}
if (!this.enabled) {
return;
}
const tech = this.player.tech({ IWillNotUseThisInPlugins: true }) as VideoJSTechVHS;
// We need access to the internal tech of VHS to move forward.
// If running under an Apple browser that uses CoreMedia (Safari)
// we do not have access to this as the tech is internal to the OS.
if (!tech || !tech.vhs) {
logger.error('Native player!')
this.disable()
return;
}
// Network state 2 means we're actively using the network.
// We only want to attempt latency compensation if we're continuing to
// download new segments.
const networkState = this.player.networkState();
if (networkState !== 2) {
return;
}
let totalBuffered = 0;
try {
// Check the player buffers to make sure there's enough playable content
// that we can safely play.
if (tech.vhs.stats.buffered.length === 0) {
this.timeout();
return;
}
tech.vhs.stats.buffered.forEach((buffer: any) => {
totalBuffered += buffer.end - buffer.start;
});
} catch (e) { }
// Determine how much of the current playlist's bandwidth requirements
// we're utilizing. If it's too high then we can't afford to push
// further into the future because we're downloading too slowly.
const currentPlaylist = tech.vhs.playlists.media();
const currentPlaylistBandwidth = currentPlaylist.attributes.BANDWIDTH;
const playerBandwidth = tech.vhs.systemBandwidth;
const bandwidthRatio = playerBandwidth / currentPlaylistBandwidth;
try {
const segment = getCurrentlyPlayingSegment(this.player);
if (!segment) {
return;
}
// If we're downloading media fast enough or we feel like we have a large
// enough buffer then continue. Otherwise timeout for a bit.
if (
bandwidthRatio < this.options.required_bandwidth_ratio &&
totalBuffered < segment.duration * 6
) {
this.timeout();
return;
}
// How far away from live edge do we stop the compensator.
const minLatencyThreshold = Math.max(
this.options.min_latency,
segment.duration * 1000 * this.options.lowest_latency_segment_length_multiplier
);
// How far away from live edge do we start the compensator.
const maxLatencyThreshold = Math.max(
minLatencyThreshold * 1.4,
Math.min(
segment.duration * 1000 * this.options.highest_latency_segment_length_multiplier,
this.options.max_latency
)
);
const liveEdge = currentPlaylist.segments[currentPlaylist.segments.length - 1];
// Calculate latency as the distance between the playhead and the (idealized) live edge
const now = new Date().getTime();
const playheadOffset = (tech.currentTime() - segment.start) * 1000;
const liveEdgeOffset = now - currentPlaylist.lastRequest;
const segmentTime = segment.dateTimeObject.getTime();
const liveEdgeTime = liveEdge.dateTimeObject.getTime();
const latency = (liveEdgeTime + liveEdgeOffset) - (segmentTime + playheadOffset);
if (latency > maxLatencyThreshold) {
// If the current latency exceeds the max jump amount then
// force jump into the future, skipping all the video in between.
if (
this.shouldJumpToLive() &&
latency > maxLatencyThreshold + this.options.max_jump_latency
) {
const jumpAmount = latency / 1000 - segment.duration * 3;
logger.debug('jump amount', jumpAmount);
const seekPosition = this.player.currentTime() + jumpAmount;
logger.debug(
'latency',
latency / 1000,
'jumping to live from ',
this.player.currentTime(),
' to ',
seekPosition
);
// Verify we have the seek position buffered before jumping.
const availableBufferedTimeEnd = tech.vhs.stats.buffered[0].end;
const availableBufferedTimeStart = tech.vhs.stats.buffered[0].start;
if (
seekPosition > availableBufferedTimeStart
&& seekPosition < availableBufferedTimeEnd
) {
this.jump(seekPosition);
return;
}
}
// Using our bandwidth ratio determine a wide guess at how fast we can play.
var proposedPlaybackRate = bandwidthRatio * 0.33;
// But limit the playback rate to a max value.
proposedPlaybackRate = Math.max(
Math.min(proposedPlaybackRate, this.options.max_speedup_rate),
1.0
);
if (proposedPlaybackRate > this.playbackRate + this.options.max_speedup_ramp) {
// If this proposed speed is substantially faster than the current rate,
// then allow us to ramp up by using a slower value for now.
proposedPlaybackRate = this.playbackRate + this.options.max_speedup_ramp;
}
// Limit to 3 decimal places of precision.
proposedPlaybackRate =
Math.round(proposedPlaybackRate * Math.pow(10, 3)) / Math.pow(10, 3);
// Otherwise start the playback rate adjustment.
this.start(proposedPlaybackRate);
} else if (latency <= minLatencyThreshold) {
this.stop();
}
logger.debug(
'latency',
latency / 1000,
'min',
minLatencyThreshold / 1000,
'max',
maxLatencyThreshold / 1000,
'playback rate',
this.playbackRate,
'enabled:',
this.enabled,
'running: ',
this.running,
'timeout: ',
this.inTimeout,
'buffers: ',
this.bufferingCounter
);
} catch (err) {
logger.error(err);
}
}
shouldJumpToLive() {
const now = new Date().getTime();
const delta = now - this.lastJumpOccurred;
return delta > this.options.max_jump_frequency;
}
jump(seekPosition: any) {
this.jumpingToLiveIgnoreBuffer = true;
this.performedInitialLiveJump = true;
this.lastJumpOccurred = new Date().getTime();
logger.debug(
'current time',
this.player.currentTime(),
'seeking to',
seekPosition
);
this.player.currentTime(seekPosition);
window.setTimeout(() => {
this.jumpingToLiveIgnoreBuffer = false;
}, 5000);
}
setPlaybackRate(rate: number) {
this.playbackRate = rate;
this.player.playbackRate(rate);
}
start(rate = 1.0) {
if (this.inTimeout || !this.enabled || rate === this.playbackRate) {
return;
}
this.running = true;
this.setPlaybackRate(rate);
}
stop() {
if (this.running) {
logger.debug('stopping latency compensator...');
}
this.running = false;
this.setPlaybackRate(1.0);
}
enable() {
this.enabled = true;
clearInterval(this.checkTimer);
clearTimeout(this.bufferingTimer);
this.checkTimer = window.setInterval(() => {
this.check();
}, this.options.check_timer_interval);
}
// Disable means we're done for good and should no longer compensate for latency.
disable() {
clearInterval(this.checkTimer);
clearTimeout(this.timeoutTimer);
this.stop();
this.enabled = false;
}
timeout() {
if (this.inTimeout) {
return;
}
if (this.jumpingToLiveIgnoreBuffer) {
return;
}
this.inTimeout = true;
this.stop();
clearTimeout(this.timeoutTimer);
this.timeoutTimer = window.setTimeout(() => {
this.endTimeout();
}, this.options.timeout_duration);
}
endTimeout() {
clearTimeout(this.timeoutTimer);
this.inTimeout = false;
}
handlePlaying() {
clearTimeout(this.bufferingTimer);
this.enable()
}
handleEnded() {
if (!this.enabled) {
return;
}
this.disable();
}
handleError(e: any) {
if (!this.enabled) {
return;
}
logger.error('handle error', e);
this.timeout();
}
countBufferingEvent() {
this.bufferingCounter++;
if (this.bufferingCounter > this.options.rebuffer_event_limit) {
this.disable();
return;
}
logger.error('timeout due to buffering');
this.timeout();
// Allow us to forget about old buffering events if enough time goes by.
window.setTimeout(() => {
if (this.bufferingCounter > 0) {
this.bufferingCounter--;
}
}, this.options.buffering_amnesty_duration);
}
handleBuffering() {
if (!this.enabled) {
return;
}
if (this.jumpingToLiveIgnoreBuffer) {
this.jumpingToLiveIgnoreBuffer = false;
return;
}
this.bufferingTimer = window.setTimeout(() => {
this.countBufferingEvent();
}, this.options.min_buffer_duration);
}
}
videojs.registerPlugin('latencyCompensator', LatencyCompensatorPlugin)
export { LatencyCompensatorPlugin }

View File

@ -0,0 +1,132 @@
import videojs from 'video.js'
import {
PeerTubeResolution, PlayerNetworkInfo,
VideoJSTechVHS, VhsLevel
} from '../../types'
import { getCurrentlyPlayingSegment, getCurrentMetadataCue } from './common'
const Plugin = videojs.getPlugin('plugin')
class VhsMediaLoaderPlugin extends Plugin {
private networkInfoInterval: number
private readonly options: any
private tech: VideoJSTechVHS
private lastTotal = 0
private readonly CONSTANTS = {
INFO_SCHEDULER: 1000 // Don't change this
}
constructor (player: videojs.Player, options?: any) {
super(player)
this.options = options
player.src({
type: options.type,
src: options.src
})
player.ready(() => {
this.initializeCore()
player.play()
this.tech = player.tech(false) as VideoJSTechVHS
this.runStats()
})
}
dispose () {
clearInterval(this.networkInfoInterval)
}
getCurrentLevel () {
const currentCue = getCurrentMetadataCue(this.player)
if (!currentCue) return undefined
const currentLevel = this.tech.vhs.representations().find(
(level: VhsLevel) => level.playlist.id == currentCue.value.playlist
)
return currentLevel
}
getLiveLatency () {
if (this.player.paused()) return undefined
const segment = getCurrentlyPlayingSegment(this.player)
if (!segment) return undefined
const segmentTime = segment.dateTimeObject.getTime()
const now = new Date().getTime();
const playhead = (this.tech as any).currentTime()
const latency = ((now - segmentTime) / 1000) - (playhead - segment.start);
return latency
}
private runStats () {
this.networkInfoInterval = window.setInterval(() => {
const delta = this.tech.vhs.stats.mediaBytesTransferred - this.lastTotal
this.lastTotal = this.tech.vhs.stats.mediaBytesTransferred
this.player.trigger('p2pInfo', {
source: 'lowlatency',
http: {
downloadSpeed: delta,
uploadSpeed: 0,
downloaded: this.lastTotal,
uploaded: 0
},
p2p: {
downloadSpeed: 0,
uploadSpeed: 0,
numPeers: 0,
downloaded: 0,
uploaded: 0
},
bandwidthEstimate: this.tech.vhs.stats.bandwidth / 8
} as PlayerNetworkInfo)
}, this.CONSTANTS.INFO_SCHEDULER)
}
private initializeCore () {
this.player.one('play', () => {
this.player.addClass('vjs-has-big-play-button-clicked')
})
this.player.on('loadedmetadata', () => {
this.buildQualities()
})
this.player.on('usage', (ev) => console.log(ev))
}
private buildQualities () {
const resolutions: PeerTubeResolution[] = this.tech.vhs.representations().map((level, index) => ({
id: index,
label: `${level.height}p`,
height: level.height,
selected: false,
selectCallback: () => this.changeQuality(level.id)
}))
resolutions.push({
id: -1,
label: this.player.localize('Auto'),
selected: true,
selectCallback: () => this.changeQuality()
})
this.player.peertubeResolutions().add(resolutions)
}
changeQuality (levelId?: string) {
this.tech.vhs.representations().forEach(level => {
if (!levelId) level.enabled(true)
else level.enabled(level.id == levelId)
})
}
}
videojs.registerPlugin('vhsLoader', VhsMediaLoaderPlugin)
export { VhsMediaLoaderPlugin }

View File

@ -2,7 +2,7 @@ import { PluginsManager } from '@root-helpers/plugins-manager'
import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
import { PlaylistPluginOptions, VideoJSCaption } from './peertube-videojs-typings'
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 'lowlatency'
export type WebtorrentOptions = {
videoFiles: VideoFile[]
@ -16,6 +16,44 @@ export type P2PMediaLoaderOptions = {
videoFiles: VideoFile[]
}
export type VHSMediaLoaderOptions = {
playlistUrl: string
}
export type LatencyCompensatorOptions = {
/** Max number of buffering events before we stop compensating for latency. */
rebuffer_event_limit: number
/** Min duration a buffer event must last to be counted. */
min_buffer_duration: number
/** The playback rate when compensating for latency. */
max_speedup_rate: number
/** The max amount we will increase the playback rate at once. */
max_speedup_ramp: number
/** The amount of time we stop handling latency after certain events. */
timeout_duration: number
/** How often we check if we should be compensating for latency. */
check_timer_interval: number
/** How often until a buffering event expires. */
buffering_amnesty_duration: number
/** The player:bitrate ratio required to enable compensating for latency. */
required_bandwidth_ratio: number
/** Segment length * this value is when we start compensating. */
highest_latency_segment_length_multiplier: number
/** Segment length * this value is when we stop compensating. */
lowest_latency_segment_length_multiplier: number
/** The absolute lowest we'll continue compensation to be running at. */
min_latency: number
/** The absolute highest we'll allow a target latency to be before we start compensating. */
max_latency: number
/** How much behind the max latency we need to be behind before we allow a jump. */
max_jump_latency: number
/** How often we'll allow a time jump. */
max_jump_frequency: number
/** The amount of time after we start up that we'll allow monitoring to occur. */
startup_wait_time: number
}
export interface CustomizationOptions {
startTime: number | string
stopTime: number | string
@ -85,6 +123,6 @@ export type PeertubePlayerManagerOptions = {
common: CommonOptions
webtorrent: WebtorrentOptions
p2pMediaLoader?: P2PMediaLoaderOptions
vhsMediaLoader?: VHSMediaLoaderOptions,
pluginsManager: PluginsManager
}

View File

@ -5,6 +5,7 @@ import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin'
import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin'
import { VhsMediaLoaderPlugin } from '../shared/vhs-loader/vhs-loader-plugin'
import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager'
import { PeerTubePlugin } from '../shared/peertube/peertube-plugin'
import { PlaylistPlugin } from '../shared/playlist/playlist-plugin'
@ -35,8 +36,8 @@ declare module 'video.js' {
peertube (): PeerTubePlugin
webtorrent (): WebTorrentPlugin
p2pMediaLoader (): P2pMediaLoaderPlugin
vhsLoader (): VhsMediaLoaderPlugin
peertubeResolutions (): PeerTubeResolutionsPlugin
@ -64,6 +65,65 @@ export interface VideoJSTechHLS extends videojs.Tech {
hlsProvider: Html5Hlsjs
}
export type VhsSegment = {
start: number
end: number
uri: string
codecs: string
resolution: { width: number, height: number }
duration: number
dateTimeObject: Date
}
export interface VhsMetadataCue extends TextTrackCue {
value: {
playlist: string
start: number
}
}
export type VhsPlaylist = {
id: string
uri: string
resolvedUri: string
lastRequest: number
attributes: {
BANDWIDTH: number
CODECS: string
RESOLUTION: { width: number, height: number }
}
segments: VhsSegment[]
}
export type VhsLevel = {
id: string
height: number
width: number
bandwidth: number
playlist: VhsPlaylist
enabled: (enable?: boolean) => boolean
}
export interface VideoJSTechVHS extends videojs.Tech {
vhs: {
representations: () => VhsLevel[]
playlists: {
master: () => VhsPlaylist
media: () => VhsPlaylist
},
systemBandwidth: number
stats: {
mediaBytesTransferred: number
bandwidth: number
buffered: { start: number, end: number }[]
}
}
currentTime: () => number
}
export interface HlsjsConfigHandlerOptions {
hlsjsConfig?: HlsConfig
@ -200,7 +260,7 @@ type AutoResolutionUpdateData = {
}
type PlayerNetworkInfo = {
source: 'webtorrent' | 'p2p-media-loader'
source: 'webtorrent' | 'p2p-media-loader' | 'lowlatency'
http: {
downloadSpeed: number

View File

@ -30,7 +30,6 @@
"baseUrl": "./",
"paths": {
"hls.js": [ "node_modules/hls.js/dist/hls.light" ],
"video.js": [ "node_modules/video.js/core" ],
"@app/*": [ "src/app/*" ],
"@shared/models/*": [ "../shared/models/*" ],
"@shared/models": [ "../shared/models" ],

View File

@ -1,7 +1,7 @@
{
"name": "peertube",
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
"version": "4.3.0",
"version": "4.3.0+vtopia1",
"private": true,
"licence": "AGPL-3.0",
"engines": {
@ -12,13 +12,13 @@
"peertube": "dist/server/tools/peertube.js"
},
"author": {
"name": "Chocobozzz",
"email": "chocobozzz@framasoft.org",
"url": "http://github.com/Chocobozzz"
"name": "VtopiaLIVE",
"email": "admin@votpia.live",
"url": "https://social.vtopia.live/@staff"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Chocobozzz/PeerTube.git"
"url": "git+https://code.vtopia.live/Vtopia/VeeTube.git"
},
"typings": "*.d.ts",
"scripts": {

View File

@ -99,16 +99,25 @@ function buildStreamSuffix (base: string, streamNum?: number) {
}
function getScaleFilter (options: EncoderOptions): string {
if (options.scaleFilter) return options.scaleFilter.name
if (options.scaleFilter?.name) return options.scaleFilter.name
return 'scale'
}
function getScaleFilterArgs (options: EncoderOptions, resolution: number): string {
if (options.scaleFilter?.args) {
return options.scaleFilter.args.join(':')
}
return `w=-2:h=${resolution}`
}
export {
getFFmpeg,
getFFmpegVersion,
runCommand,
StreamType,
buildStreamSuffix,
getScaleFilter
getScaleFilter,
getScaleFilterArgs
}

View File

@ -3,7 +3,7 @@ import { join } from 'path'
import { VIDEO_LIVE } from '@server/initializers/constants'
import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
import { logger, loggerTagsFactory } from '../logger'
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
import { buildStreamSuffix, getFFmpeg, getScaleFilter, getScaleFilterArgs, StreamType } from './ffmpeg-commons'
import { getEncoderBuilderResult } from './ffmpeg-encoders'
import { addDefaultEncoderGlobalParams, addDefaultEncoderParams, applyEncoderOptions } from './ffmpeg-presets'
import { computeFPS } from './ffprobe-utils'
@ -46,6 +46,8 @@ async function getLiveTranscodingCommand (options: {
addDefaultEncoderGlobalParams(command)
const segmentLength = getLiveSegmentTime(latencyMode)
for (let i = 0; i < resolutions.length; i++) {
const resolution = resolutions[i]
const resolutionFPS = computeFPS(fps, resolution)
@ -64,6 +66,7 @@ async function getLiveTranscodingCommand (options: {
resolution,
fps: resolutionFPS,
segmentLength,
streamNum: i,
videoType: 'live' as 'live'
@ -78,7 +81,9 @@ async function getLiveTranscodingCommand (options: {
command.outputOption(`-map [vout${resolution}]`)
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
addDefaultEncoderParams({
command, segmentLength, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i
})
logger.debug(
'Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile,
@ -91,7 +96,7 @@ async function getLiveTranscodingCommand (options: {
complexFilter.push({
inputs: `vtemp${resolution}`,
filter: getScaleFilter(builderResult.result),
options: `w=-2:h=${resolution}`,
options: getScaleFilterArgs(builderResult.result, resolution),
outputs: `vout${resolution}`
})
}

View File

@ -25,18 +25,20 @@ function addDefaultEncoderParams (options: {
fps: number
streamNum?: number
segmentLength?: number
}) {
const { command, encoder, fps, streamNum } = options
// Keyframe interval defaults to 2 for faster seeking and resolution switching on VODs,
// and is the same as the segment length when live to ensure keyframes are put where needed
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
const { command, encoder, fps, streamNum, segmentLength = 2 } = options
if (encoder === 'libx264') {
// 3.1 is the minimal resource allocation for our highest supported resolution
command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1')
if (fps) {
// Keyframe interval of 2 seconds for faster seeking and resolution switching.
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
// https://superuser.com/a/908325
command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * segmentLength))
}
}
}

View File

@ -43,6 +43,10 @@ function isLastWeek (d: Date) {
return getDaysDifferences(now, d) <= 7
}
function round(n: number, precision: number) {
return parseFloat(n.toFixed(precision))
}
// ---------------------------------------------------------------------------
function timeToInt (time: number | string) {
@ -81,8 +85,8 @@ function secondsToTime (seconds: number, full = false, symbol?: string) {
else if (full) time += '00' + minuteSymbol
seconds %= 60
if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
else if (seconds >= 1) time += seconds + secondsSymbol
if (seconds >= 1 && seconds < 10 && full) time += '0' + round(seconds, 3) + secondsSymbol
else if (seconds >= 1) time += round(seconds, 3) + secondsSymbol
else if (full) time += '00'
return time

View File

@ -1,7 +1,7 @@
import { VideoResolution } from '../videos'
export interface PlaybackMetricCreate {
playerMode: 'p2p-media-loader' | 'webtorrent'
playerMode: 'p2p-media-loader' | 'webtorrent' | 'lowlatency'
resolution?: VideoResolution
fps?: number

View File

@ -27,7 +27,8 @@ export interface EncoderOptions {
copy?: boolean // Copy stream? Default to false
scaleFilter?: {
name: string
name?: string
args?: string[]
}
inputOptions?: string[]