Add video aspect ratio in server

This commit is contained in:
Chocobozzz
2024-02-27 11:18:56 +01:00
parent c75381208f
commit b6b1aaa56f
52 changed files with 345 additions and 237 deletions

View File

@@ -103,9 +103,14 @@ function calculateBitrate (options: {
VideoResolution.H_NOVIDEO
]
const size1 = resolution
const size2 = ratio < 1 && ratio > 0
? resolution / ratio // Portrait mode
: resolution * ratio
for (const toTestResolution of resolutionsOrder) {
if (toTestResolution <= resolution) {
return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution])
return Math.floor(size1 * size2 * fps * bitPerPixel[toTestResolution])
}
}

View File

@@ -1,10 +1,10 @@
import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models'
function getAllPrivacies () {
export function getAllPrivacies () {
return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
}
function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
export function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
const files = video.files
const hls = getHLS(video)
@@ -13,12 +13,13 @@ function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlay
return files
}
function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
export function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
}
export {
getAllPrivacies,
getAllFiles,
getHLS
export function buildAspectRatio (options: { width: number, height: number }) {
const { width, height } = options
if (!width || !height) return null
return Math.round((width / height) * 10000) / 10000 // 4 decimals precision
}

View File

@@ -1,5 +1,5 @@
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'
import { forceNumber } from '@peertube/peertube-core-utils'
import { buildAspectRatio, forceNumber } from '@peertube/peertube-core-utils'
import { VideoResolution } from '@peertube/peertube-models'
/**
@@ -123,7 +123,7 @@ async function getVideoStreamDimensionsInfo (path: string, existingProbe?: Ffpro
return {
width: videoStream.width,
height: videoStream.height,
ratio: Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width),
ratio: buildAspectRatio({ width: videoStream.width, height: videoStream.height }),
resolution: Math.min(videoStream.height, videoStream.width),
isPortraitMode: videoStream.height > videoStream.width
}

View File

@@ -10,8 +10,8 @@ export interface ActivityIconObject {
type: 'Image'
url: string
mediaType: string
width?: number
height?: number
width: number
height: number | null
}
export type ActivityVideoUrlObject = {
@@ -19,6 +19,7 @@ export type ActivityVideoUrlObject = {
mediaType: 'video/mp4' | 'video/webm' | 'video/ogg' | 'audio/mp4'
href: string
height: number
width: number | null
size: number
fps: number
}
@@ -35,6 +36,7 @@ export type ActivityVideoFileMetadataUrlObject = {
rel: [ 'metadata', any ]
mediaType: 'application/json'
height: number
width: number | null
href: string
fps: number
}
@@ -63,6 +65,8 @@ export type ActivityBitTorrentUrlObject = {
mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
href: string
height: number
width: number | null
fps: number | null
}
export type ActivityMagnetUrlObject = {
@@ -70,6 +74,8 @@ export type ActivityMagnetUrlObject = {
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet'
href: string
height: number
width: number | null
fps: number | null
}
export type ActivityHtmlUrlObject = {

View File

@@ -44,6 +44,8 @@ export interface VideoObject {
support: string
aspectRatio: number
icon: ActivityIconObject[]
url: ActivityUrlObject[]

View File

@@ -7,6 +7,9 @@ export interface VideoFile {
resolution: VideoConstant<number>
size: number // Bytes
width?: number
height?: number
torrentUrl: string
torrentDownloadUrl: string

View File

@@ -29,6 +29,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
isLocal: boolean
name: string
aspectRatio: number | null
isLive: boolean
thumbnailPath: string

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -115,6 +115,8 @@ describe('Test live', function () {
expect(video.isLive).to.be.true
expect(video.aspectRatio).to.not.exist
expect(video.nsfw).to.be.false
expect(video.waitTranscoding).to.be.false
expect(video.name).to.equal('my super live')
@@ -552,6 +554,7 @@ describe('Test live', function () {
expect(video.state.id).to.equal(VideoState.PUBLISHED)
expect(video.duration).to.be.greaterThan(1)
expect(video.aspectRatio).to.equal(1.7778)
expect(video.files).to.have.lengthOf(0)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)

View File

@@ -2,7 +2,6 @@
import { expect } from 'chai'
import { readdir } from 'fs/promises'
import { decode as magnetUriDecode } from 'magnet-uri'
import { basename, join } from 'path'
import { wait } from '@peertube/peertube-core-utils'
import {
@@ -25,12 +24,13 @@ import {
} from '@peertube/peertube-server-commands'
import { checkSegmentHash } from '@tests/shared/streaming-playlists.js'
import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js'
import { magnetUriDecode } from '@tests/shared/webtorrent.js'
let servers: PeerTubeServer[] = []
let video1Server2: VideoDetails
async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
const parsed = magnetUriDecode(file.magnetUri)
const parsed = await magnetUriDecode(file.magnetUri)
for (const ws of baseWebseeds) {
const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)

View File

@@ -479,6 +479,8 @@ describe('Test follows', function () {
files: [
{
resolution: 720,
width: 1280,
height: 720,
size: 218910
}
]

View File

@@ -69,6 +69,8 @@ describe('Test handle downs', function () {
fixture: 'video_short1.webm',
files: [
{
height: 720,
width: 1280,
resolution: 720,
size: 572456
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri'
import WebTorrent from 'webtorrent'
import {
cleanupTests,
@@ -9,6 +8,7 @@ import {
PeerTubeServer,
setAccessTokensToServers
} from '@peertube/peertube-server-commands'
import { magnetUriDecode, magnetUriEncode } from '@tests/shared/webtorrent.js'
describe('Test tracker', function () {
let server: PeerTubeServer
@@ -25,10 +25,10 @@ describe('Test tracker', function () {
const video = await server.videos.get({ id: uuid })
goodMagnet = video.files[0].magnetUri
const parsed = magnetUriDecode(goodMagnet)
const parsed = await magnetUriDecode(goodMagnet)
parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9'
badMagnet = magnetUriEncode(parsed)
badMagnet = await magnetUriEncode(parsed)
}
})

View File

@@ -401,10 +401,14 @@ function runTest (withObjectStorage: boolean) {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 61000
},
{
resolution: 240,
height: 240,
width: 426,
size: 23000
}
],

View File

@@ -118,6 +118,8 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 572456
}
]
@@ -205,18 +207,26 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 240,
height: 240,
width: 426,
size: 270000
},
{
resolution: 360,
height: 360,
width: 640,
size: 359000
},
{
resolution: 480,
height: 480,
width: 854,
size: 465000
},
{
resolution: 720,
height: 720,
width: 1280,
size: 750000
}
],
@@ -312,6 +322,8 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 292677
}
]
@@ -344,6 +356,8 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 218910
}
]
@@ -654,6 +668,8 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 292677
}
],
@@ -1061,18 +1077,26 @@ describe('Test multiple servers', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 61000
},
{
resolution: 480,
height: 480,
width: 854,
size: 40000
},
{
resolution: 360,
height: 360,
width: 640,
size: 32000
},
{
resolution: 240,
height: 240,
width: 426,
size: 23000
}
]

View File

@@ -50,6 +50,8 @@ describe('Test a single server', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 218910
}
]
@@ -81,6 +83,8 @@ describe('Test a single server', function () {
files: [
{
resolution: 720,
height: 720,
width: 1280,
size: 292677
}
]

View File

@@ -105,7 +105,8 @@ describe('Test videos files', function () {
const video = await servers[0].videos.get({ id: webVideoId })
const files = video.files
await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id })
const toDelete = files[0]
await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: toDelete.id })
await waitJobs(servers)
@@ -113,7 +114,7 @@ describe('Test videos files', function () {
const video = await server.videos.get({ id: webVideoId })
expect(video.files).to.have.lengthOf(files.length - 1)
expect(video.files.find(f => f.id === files[0].id)).to.not.exist
expect(video.files.find(f => f.resolution.id === toDelete.resolution.id)).to.not.exist
}
})
@@ -151,7 +152,7 @@ describe('Test videos files', function () {
const video = await server.videos.get({ id: hlsId })
expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist
expect(video.streamingPlaylists[0].files.find(f => f.resolution.id === toDelete.resolution.id)).to.not.exist
const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 })

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { decode } from 'magnet-uri'
import { getAllFiles, wait } from '@peertube/peertube-core-utils'
import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
import {
@@ -18,7 +17,7 @@ import {
} from '@peertube/peertube-server-commands'
import { expectStartWith } from '@tests/shared/checks.js'
import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
import { magnetUriDecode, parseTorrentVideo } from '@tests/shared/webtorrent.js'
describe('Test video static file privacy', function () {
let server: PeerTubeServer
@@ -48,7 +47,7 @@ describe('Test video static file privacy', function () {
const torrent = await parseTorrentVideo(server, file)
expect(torrent.urlList).to.have.lengthOf(0)
const magnet = decode(file.magnetUri)
const magnet = await magnetUriDecode(file.magnetUri)
expect(magnet.urlList).to.have.lengthOf(0)
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
@@ -74,7 +73,7 @@ describe('Test video static file privacy', function () {
const torrent = await parseTorrentVideo(server, file)
expect(torrent.urlList[0]).to.not.include('private')
const magnet = decode(file.magnetUri)
const magnet = await magnetUriDecode(file.magnetUri)
expect(magnet.urlList[0]).to.not.include('private')
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })

View File

@@ -3,7 +3,13 @@
import { expect } from 'chai'
import snakeCase from 'lodash-es/snakeCase.js'
import validator from 'validator'
import { getAverageTheoreticalBitrate, getMaxTheoreticalBitrate, parseChapters, timeToInt } from '@peertube/peertube-core-utils'
import {
buildAspectRatio,
getAverageTheoreticalBitrate,
getMaxTheoreticalBitrate,
parseChapters,
timeToInt
} from '@peertube/peertube-core-utils'
import { VideoResolution } from '@peertube/peertube-models'
import { objectConverter, parseBytes, parseDurationToMs, parseSemVersion } from '@peertube/peertube-server/core/helpers/core-utils.js'
@@ -169,6 +175,18 @@ describe('Bitrate', function () {
expect(getAverageTheoreticalBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000)
}
})
describe('Ratio', function () {
it('Should have the correct aspect ratio in landscape', function () {
expect(buildAspectRatio({ width: 1920, height: 1080 })).to.equal(1.7778)
expect(buildAspectRatio({ width: 1000, height: 1000 })).to.equal(1)
})
it('Should have the correct aspect ratio in portrait', function () {
expect(buildAspectRatio({ width: 1080, height: 1920 })).to.equal(0.5625)
})
})
})
describe('Parse semantic version string', function () {

View File

@@ -103,9 +103,15 @@ async function testImage (url: string, imageName: string, imageHTTPPath: string,
? PNG.sync.read(data)
: JPEG.decode(data)
const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
const errorMsg = `${imageHTTPPath} image is not the same as ${imageName}${extension}`
expect(result).to.equal(0, `${imageHTTPPath} image is not the same as ${imageName}${extension}`)
try {
const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
expect(result).to.equal(0, errorMsg)
} catch (err) {
throw new Error(`${errorMsg}: ${err.message}`)
}
}
async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {

View File

@@ -66,6 +66,8 @@ async function testLiveVideoResolutions (options: {
expect(data.find(v => v.uuid === liveVideoId)).to.exist
const video = await server.videos.get({ id: liveVideoId })
expect(video.aspectRatio).to.equal(1.7778)
expect(video.streamingPlaylists).to.have.lengthOf(1)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)

View File

@@ -145,6 +145,9 @@ async function completeCheckHlsPlaylist (options: {
expect(file.resolution.label).to.equal(resolution + 'p')
}
expect(Math.min(file.height, file.width)).to.equal(resolution)
expect(Math.max(file.height, file.width)).to.be.greaterThan(resolution)
expect(file.magnetUri).to.have.lengthOf.above(2)
await checkWebTorrentWorks(file.magnetUri)

View File

@@ -26,6 +26,8 @@ export async function completeWebVideoFilesCheck (options: {
fixture: string
files: {
resolution: number
width?: number
height?: number
size?: number
}[]
objectStorageBaseUrl?: string
@@ -84,7 +86,9 @@ export async function completeWebVideoFilesCheck (options: {
makeRawRequest({
url: file.fileDownloadUrl,
token,
expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200
expectedStatus: objectStorageBaseUrl
? HttpStatusCode.FOUND_302
: HttpStatusCode.OK_200
})
])
}
@@ -97,6 +101,12 @@ export async function completeWebVideoFilesCheck (options: {
expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
}
if (attributeFile.width !== undefined) expect(file.width).to.equal(attributeFile.width)
if (attributeFile.height !== undefined) expect(file.height).to.equal(attributeFile.height)
expect(Math.min(file.height, file.width)).to.equal(file.resolution.id)
expect(Math.max(file.height, file.width)).to.be.greaterThan(file.resolution.id)
if (attributeFile.size) {
const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
@@ -156,6 +166,8 @@ export async function completeVideoCheck (options: {
files?: {
resolution: number
size: number
width: number
height: number
}[]
hls?: {

View File

@@ -4,6 +4,7 @@ import { basename, join } from 'path'
import type { Instance, Torrent } from 'webtorrent'
import { VideoFile } from '@peertube/peertube-models'
import { PeerTubeServer } from '@peertube/peertube-server-commands'
import type { Instance as MagnetUriInstance } from 'magnet-uri'
let webtorrent: Instance
@@ -28,6 +29,14 @@ export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile
return (await import('parse-torrent')).default(data)
}
export async function magnetUriDecode (data: string) {
return (await import('magnet-uri')).decode(data)
}
export async function magnetUriEncode (data: MagnetUriInstance) {
return (await import('magnet-uri')).encode(data)
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------