diff --git a/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.html b/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.html index fe8dcc12f..f00bba8be 100644 --- a/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.html +++ b/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.html @@ -17,9 +17,15 @@ } diff --git a/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.ts b/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.ts index 9d89b0a77..52bb04552 100644 --- a/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/download/video-generate-download.component.ts @@ -6,7 +6,7 @@ import { VideoService } from '@app/shared/shared-main/video/video.service' import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap' -import { maxBy } from '@peertube/peertube-core-utils' +import { getResolutionAndFPSLabel, maxBy } from '@peertube/peertube-core-utils' import { VideoFile, VideoResolution, VideoSource } from '@peertube/peertube-models' import { videoRequiresFileToken } from '@root-helpers/video' import { GlobalIconComponent } from '../../shared-icons/global-icon.component' @@ -52,6 +52,10 @@ export class VideoGenerateDownloadComponent implements OnInit { this.videoFileChosen = 'file-' + maxBy(this.videoFiles, 'resolution').id } + getLabel (file: VideoFile) { + return getResolutionAndFPSLabel(file.resolution.label, file.fps) + } + getFileSize (file: VideoFile) { if (file.hasAudio && file.hasVideo) return file.size if (file.hasAudio) return file.size diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index a93fff7fb..ba9a8d627 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts @@ -303,15 +303,7 @@ export class Html5Hlsjs { // --------------------------------------------------------------------------- private buildLevelLabel (level: Level) { - if (this.player.srOptions_.levelLabelHandler) { - return this.player.srOptions_.levelLabelHandler(level, this.player) - } - - if (level.height) return level.height + 'p' - if (level.width) return Math.round(level.width * 9 / 16) + 'p' - if (level.bitrate) return (level.bitrate / 1000) + 'kbps' - - return this.player.localize('Audio only') + return this.player.srOptions_.levelLabelHandler(level, this.player) } private _removeQuality (index: number) { diff --git a/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts index b4a3a4d6e..7ba4faaee 100644 --- a/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts +++ b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts @@ -1,8 +1,10 @@ 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 { Level } from 'hls.js' import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' import { HLSLoaderClass, @@ -70,21 +72,17 @@ export class HLSOptionsBuilder { const hlsjs = { hlsjsConfig: this.getHLSJSOptions(loaderBuilder), - levelLabelHandler: (level: { height: number, width: number }, player: videojs.VideoJsPlayer) => { + levelLabelHandler: (level: Level, player: videojs.VideoJsPlayer) => { const resolution = Math.min(level.height || 0, level.width || 0) - const file = this.options.hls.videoFiles.find(f => f.resolution.id === resolution) - // We don't have files for live videos - if (!file) { - if (resolution === 0) return player.localize('Audio only') - return level.height + 'p' - } + const resolutionLabel = getResolutionLabel({ + resolution, + height: file?.height ?? level.height, + width: file?.width ?? level.width + }) - let label = file.resolution.label - if (file.fps >= 50) label += file.fps - - return label + return player.localize(getResolutionAndFPSLabel(resolutionLabel, file?.fps ?? level.frameRate)) } } diff --git a/client/src/assets/player/shared/web-video/web-video-plugin.ts b/client/src/assets/player/shared/web-video/web-video-plugin.ts index a214fd6ae..e4544f87e 100644 --- a/client/src/assets/player/shared/web-video/web-video-plugin.ts +++ b/client/src/assets/player/shared/web-video/web-video-plugin.ts @@ -1,8 +1,8 @@ +import { addQueryParams, getResolutionAndFPSLabel } from '@peertube/peertube-core-utils' +import { VideoFile } from '@peertube/peertube-models' +import { logger } from '@root-helpers/logger' import debug from 'debug' import videojs from 'video.js' -import { logger } from '@root-helpers/logger' -import { addQueryParams } from '@peertube/peertube-core-utils' -import { VideoFile } from '@peertube/peertube-models' import { PeerTubeResolution, PlayerNetworkInfo, WebVideoPluginOptions } from '../../types' const debugLogger = debug('peertube:player:web-video-plugin') @@ -155,7 +155,7 @@ class WebVideoPlugin extends Plugin { private buildQualities () { const resolutions: PeerTubeResolution[] = this.videoFiles.map(videoFile => ({ id: videoFile.resolution.id, - label: this.buildQualityLabel(videoFile), + label: this.player.localize(getResolutionAndFPSLabel(videoFile.resolution.label, videoFile.fps)), height: videoFile.resolution.id, selected: videoFile.id === this.currentVideoFile?.id, selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true }) @@ -164,16 +164,6 @@ class WebVideoPlugin extends Plugin { this.player.peertubeResolutions().add(resolutions) } - private buildQualityLabel (file: VideoFile) { - let label = file.resolution.label - - if (file.fps && file.fps >= 50) { - label += file.fps - } - - return label - } - private setupNetworkInfoInterval () { this.networkInfoInterval = setInterval(() => { return this.player.trigger('network-info', { diff --git a/packages/core-utils/src/videos/common.ts b/packages/core-utils/src/videos/common.ts index 64e66094c..60c1c1eac 100644 --- a/packages/core-utils/src/videos/common.ts +++ b/packages/core-utils/src/videos/common.ts @@ -1,4 +1,4 @@ -import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models' +import { VideoDetails, VideoPrivacy, VideoResolution, VideoStreamingPlaylistType } from '@peertube/peertube-models' export function getAllPrivacies () { return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ] @@ -23,3 +23,54 @@ export function buildAspectRatio (options: { width: number, height: number }) { return Math.round((width / height) * 10000) / 10000 // 4 decimals precision } + +const classicResolutions = new Set([ + VideoResolution.H_NOVIDEO, + VideoResolution.H_144P, + VideoResolution.H_240P, + VideoResolution.H_360P, + VideoResolution.H_480P, + VideoResolution.H_720P, + VideoResolution.H_1080P, + VideoResolution.H_1440P, + VideoResolution.H_4K +]) + +const resolutionConverter = { + 3840: VideoResolution.H_4K, + 1920: VideoResolution.H_1080P, + 1280: VideoResolution.H_720P, + 854: VideoResolution.H_480P, + 640: VideoResolution.H_360P, + 426: VideoResolution.H_240P, + 256: VideoResolution.H_144P +} + +export function getResolutionLabel (options: { + resolution: number + height: number + width: number +}) { + const { height, width } = options + + if (options.resolution === 0) return 'Audio only' + + let resolution = options.resolution + + // Try to find a better resolution label + // For example with a video 1920x816 we prefer to display "1080p" + if (!classicResolutions.has(resolution) && typeof height === 'number' && typeof width === 'number') { + const max = Math.max(height, width) + + const alternativeLabel = resolutionConverter[max] + if (alternativeLabel) resolution = resolutionConverter[max] + } + + return `${resolution}p` +} + +export function getResolutionAndFPSLabel (resolutionLabel: string, fps: number) { + if (fps && fps >= 50) return resolutionLabel + fps + + return resolutionLabel +} diff --git a/server/core/models/video/formatter/video-api-format.ts b/server/core/models/video/formatter/video-api-format.ts index 11d15fabc..0c627a13e 100644 --- a/server/core/models/video/formatter/video-api-format.ts +++ b/server/core/models/video/formatter/video-api-format.ts @@ -25,6 +25,7 @@ import { import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models/index.js' import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file.js' import { sortByResolutionDesc } from './shared/index.js' +import { getResolutionLabel } from '@peertube/peertube-core-utils' export type VideoFormattingJSONOptions = { completeDescription?: boolean @@ -230,7 +231,12 @@ export function videoFilesModelToFormattedJSON ( resolution: { id: videoFile.resolution, - label: getResolutionLabel(videoFile.resolution) + + label: getResolutionLabel({ + resolution: videoFile.resolution, + height: videoFile.height, + width: videoFile.width + }) }, width: videoFile.width, @@ -279,12 +285,6 @@ export function getStateLabel (id: number) { return VIDEO_STATES[id] || 'Unknown' } -export function getResolutionLabel (resolution: number) { - if (resolution === 0) return 'Audio' - - return `${resolution}p` -} - // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- diff --git a/server/core/models/video/video-source.ts b/server/core/models/video/video-source.ts index 9c80ae148..de809322f 100644 --- a/server/core/models/video/video-source.ts +++ b/server/core/models/video/video-source.ts @@ -1,3 +1,4 @@ +import { getResolutionLabel } from '@peertube/peertube-core-utils' import { ActivityVideoUrlObject, type FileStorageType, type VideoSource } from '@peertube/peertube-models' import { DOWNLOAD_PATHS, WEBSERVER } from '@server/initializers/constants.js' import { getVideoFileMimeType } from '@server/lib/video-file.js' @@ -6,7 +7,6 @@ import { extname, join } from 'path' import { Transaction } from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript' import { SequelizeModel, doesExist, getSort } from '../shared/index.js' -import { getResolutionLabel } from './formatter/video-api-format.js' import { VideoModel } from './video.js' @Table({ @@ -148,7 +148,7 @@ export class VideoSourceModel extends SequelizeModel { resolution: { id: this.resolution, label: this.resolution !== null - ? getResolutionLabel(this.resolution) + ? getResolutionLabel({ resolution: this.resolution, height: this.height, width: this.width }) : null }, size: this.size,