Add video aspect ratio in server

pull/6266/head
Chocobozzz 2024-02-27 11:18:56 +01:00
parent c75381208f
commit b6b1aaa56f
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
52 changed files with 345 additions and 237 deletions

View File

@ -50,6 +50,8 @@ export class Video implements VideoServerModel {
thumbnailPath: string thumbnailPath: string
thumbnailUrl: string thumbnailUrl: string
aspectRatio: number
isLive: boolean isLive: boolean
previewPath: string previewPath: string
@ -197,6 +199,8 @@ export class Video implements VideoServerModel {
this.originInstanceUrl = 'https://' + this.originInstanceHost this.originInstanceUrl = 'https://' + this.originInstanceHost
this.pluginData = hash.pluginData this.pluginData = hash.pluginData
this.aspectRatio = hash.aspectRatio
} }
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) { isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {

View File

@ -103,9 +103,14 @@ function calculateBitrate (options: {
VideoResolution.H_NOVIDEO VideoResolution.H_NOVIDEO
] ]
const size1 = resolution
const size2 = ratio < 1 && ratio > 0
? resolution / ratio // Portrait mode
: resolution * ratio
for (const toTestResolution of resolutionsOrder) { for (const toTestResolution of resolutionsOrder) {
if (toTestResolution <= resolution) { 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' 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 ] 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 files = video.files
const hls = getHLS(video) const hls = getHLS(video)
@ -13,12 +13,13 @@ function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlay
return files 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) return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
} }
export { export function buildAspectRatio (options: { width: number, height: number }) {
getAllPrivacies, const { width, height } = options
getAllFiles, if (!width || !height) return null
getHLS
return Math.round((width / height) * 10000) / 10000 // 4 decimals precision
} }

View File

@ -1,5 +1,5 @@
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg' 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' import { VideoResolution } from '@peertube/peertube-models'
/** /**
@ -123,7 +123,7 @@ async function getVideoStreamDimensionsInfo (path: string, existingProbe?: Ffpro
return { return {
width: videoStream.width, width: videoStream.width,
height: videoStream.height, 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), resolution: Math.min(videoStream.height, videoStream.width),
isPortraitMode: videoStream.height > videoStream.width isPortraitMode: videoStream.height > videoStream.width
} }

View File

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

View File

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

View File

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

View File

@ -29,6 +29,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
isLocal: boolean isLocal: boolean
name: string name: string
aspectRatio: number | null
isLive: boolean isLive: boolean
thumbnailPath: string 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.isLive).to.be.true
expect(video.aspectRatio).to.not.exist
expect(video.nsfw).to.be.false expect(video.nsfw).to.be.false
expect(video.waitTranscoding).to.be.false expect(video.waitTranscoding).to.be.false
expect(video.name).to.equal('my super live') 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.state.id).to.equal(VideoState.PUBLISHED)
expect(video.duration).to.be.greaterThan(1) expect(video.duration).to.be.greaterThan(1)
expect(video.aspectRatio).to.equal(1.7778)
expect(video.files).to.have.lengthOf(0) expect(video.files).to.have.lengthOf(0)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,8 @@ describe('Test videos files', function () {
const video = await servers[0].videos.get({ id: webVideoId }) const video = await servers[0].videos.get({ id: webVideoId })
const files = video.files 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) await waitJobs(servers)
@ -113,7 +114,7 @@ describe('Test videos files', function () {
const video = await server.videos.get({ id: webVideoId }) const video = await server.videos.get({ id: webVideoId })
expect(video.files).to.have.lengthOf(files.length - 1) 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 }) const video = await server.videos.get({ id: hlsId })
expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1) 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 }) 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 */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai' import { expect } from 'chai'
import { decode } from 'magnet-uri'
import { getAllFiles, wait } from '@peertube/peertube-core-utils' import { getAllFiles, wait } from '@peertube/peertube-core-utils'
import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
import { import {
@ -18,7 +17,7 @@ import {
} from '@peertube/peertube-server-commands' } from '@peertube/peertube-server-commands'
import { expectStartWith } from '@tests/shared/checks.js' import { expectStartWith } from '@tests/shared/checks.js'
import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.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 () { describe('Test video static file privacy', function () {
let server: PeerTubeServer let server: PeerTubeServer
@ -48,7 +47,7 @@ describe('Test video static file privacy', function () {
const torrent = await parseTorrentVideo(server, file) const torrent = await parseTorrentVideo(server, file)
expect(torrent.urlList).to.have.lengthOf(0) 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) expect(magnet.urlList).to.have.lengthOf(0)
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) 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) const torrent = await parseTorrentVideo(server, file)
expect(torrent.urlList[0]).to.not.include('private') 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') expect(magnet.urlList[0]).to.not.include('private')
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })

View File

@ -3,7 +3,13 @@
import { expect } from 'chai' import { expect } from 'chai'
import snakeCase from 'lodash-es/snakeCase.js' import snakeCase from 'lodash-es/snakeCase.js'
import validator from 'validator' 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 { VideoResolution } from '@peertube/peertube-models'
import { objectConverter, parseBytes, parseDurationToMs, parseSemVersion } from '@peertube/peertube-server/core/helpers/core-utils.js' 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) 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 () { 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) ? PNG.sync.read(data)
: JPEG.decode(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) { 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 expect(data.find(v => v.uuid === liveVideoId)).to.exist
const video = await server.videos.get({ id: liveVideoId }) const video = await server.videos.get({ id: liveVideoId })
expect(video.aspectRatio).to.equal(1.7778)
expect(video.streamingPlaylists).to.have.lengthOf(1) expect(video.streamingPlaylists).to.have.lengthOf(1)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) 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(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) expect(file.magnetUri).to.have.lengthOf.above(2)
await checkWebTorrentWorks(file.magnetUri) await checkWebTorrentWorks(file.magnetUri)

View File

@ -26,6 +26,8 @@ export async function completeWebVideoFilesCheck (options: {
fixture: string fixture: string
files: { files: {
resolution: number resolution: number
width?: number
height?: number
size?: number size?: number
}[] }[]
objectStorageBaseUrl?: string objectStorageBaseUrl?: string
@ -84,7 +86,9 @@ export async function completeWebVideoFilesCheck (options: {
makeRawRequest({ makeRawRequest({
url: file.fileDownloadUrl, url: file.fileDownloadUrl,
token, 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') 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) { if (attributeFile.size) {
const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
@ -156,6 +166,8 @@ export async function completeVideoCheck (options: {
files?: { files?: {
resolution: number resolution: number
size: number size: number
width: number
height: number
}[] }[]
hls?: { hls?: {

View File

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

View File

@ -23,6 +23,7 @@ import {
replaceVideoSourceResumableValidator, replaceVideoSourceResumableValidator,
videoSourceGetLatestValidator videoSourceGetLatestValidator
} from '../../../middlewares/index.js' } from '../../../middlewares/index.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('api', 'video') const lTags = loggerTagsFactory('api', 'video')
@ -96,6 +97,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
video.state = buildNextVideoState() video.state = buildNextVideoState()
video.duration = videoPhysicalFile.duration video.duration = videoPhysicalFile.duration
video.inputFileUpdatedAt = inputFileUpdatedAt video.inputFileUpdatedAt = inputFileUpdatedAt
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
await video.save({ transaction }) await video.save({ transaction })
await autoBlacklistVideoIfNeeded({ await autoBlacklistVideoIfNeeded({

View File

@ -94,6 +94,10 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@type': 'sc:Number', '@type': 'sc:Number',
'@id': 'pt:tileDuration' '@id': 'pt:tileDuration'
}, },
aspectRatio: {
'@type': 'sc:Float',
'@id': 'pt:aspectRatio'
},
originallyPublishedAt: 'sc:datePublished', originallyPublishedAt: 'sc:datePublished',

View File

@ -45,7 +45,7 @@ import { cpus } from 'os'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 820 const LAST_MIGRATION_VERSION = 825
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,43 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
{
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('videoFile', 'width', data)
}
{
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('videoFile', 'height', data)
}
{
const data = {
type: Sequelize.FLOAT,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('video', 'aspectRatio', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -55,7 +55,6 @@ function getFileAttributesFromUrl (
urls: (ActivityTagObject | ActivityUrlObject)[] urls: (ActivityTagObject | ActivityUrlObject)[]
) { ) {
const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
if (fileUrls.length === 0) return [] if (fileUrls.length === 0) return []
const attributes: FilteredModelAttributes<VideoFileModel>[] = [] const attributes: FilteredModelAttributes<VideoFileModel>[] = []
@ -96,6 +95,9 @@ function getFileAttributesFromUrl (
fps: fileUrl.fps || -1, fps: fileUrl.fps || -1,
metadataUrl: metadata?.href, metadataUrl: metadata?.href,
width: fileUrl.width,
height: fileUrl.height,
// Use the name of the remote file because we don't proxify video file requests // Use the name of the remote file because we don't proxify video file requests
filename: basename(fileUrl.href), filename: basename(fileUrl.href),
fileUrl: fileUrl.href, fileUrl: fileUrl.href,
@ -223,6 +225,7 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi
waitTranscoding: videoObject.waitTranscoding, waitTranscoding: videoObject.waitTranscoding,
isLive: videoObject.isLiveBroadcast, isLive: videoObject.isLiveBroadcast,
state: videoObject.state, state: videoObject.state,
aspectRatio: videoObject.aspectRatio,
channelId: videoChannel.id, channelId: videoChannel.id,
duration: getDurationFromActivityStream(videoObject.duration), duration: getDurationFromActivityStream(videoObject.duration),
createdAt: new Date(videoObject.published), createdAt: new Date(videoObject.published),

View File

@ -143,6 +143,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
this.video.channelId = videoData.channelId this.video.channelId = videoData.channelId
this.video.views = videoData.views this.video.views = videoData.views
this.video.isLive = videoData.isLive this.video.isLive = videoData.isLive
this.video.aspectRatio = videoData.aspectRatio
// Ensures we update the updatedAt attribute, even if main attributes did not change // Ensures we update the updatedAt attribute, even if main attributes did not change
this.video.changed('updatedAt', true) this.video.changed('updatedAt', true)

View File

@ -51,10 +51,10 @@ async function processGenerateStoryboard (job: Job): Promise<void> {
if (videoStreamInfo.isPortraitMode) { if (videoStreamInfo.isPortraitMode) {
spriteHeight = STORYBOARD.SPRITE_MAX_SIZE spriteHeight = STORYBOARD.SPRITE_MAX_SIZE
spriteWidth = Math.round(STORYBOARD.SPRITE_MAX_SIZE / videoStreamInfo.ratio) spriteWidth = Math.round(spriteHeight * videoStreamInfo.ratio)
} else { } else {
spriteHeight = Math.round(STORYBOARD.SPRITE_MAX_SIZE / videoStreamInfo.ratio)
spriteWidth = STORYBOARD.SPRITE_MAX_SIZE spriteWidth = STORYBOARD.SPRITE_MAX_SIZE
spriteHeight = Math.round(spriteWidth / videoStreamInfo.ratio)
} }
const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail')) const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail'))

View File

@ -1,20 +1,17 @@
import { Job } from 'bullmq' import { Job } from 'bullmq'
import { copy } from 'fs-extra/esm' import { copy } from 'fs-extra/esm'
import { stat } from 'fs/promises' import { VideoFileImportPayload } from '@peertube/peertube-models'
import { VideoFileImportPayload, FileStorage } from '@peertube/peertube-models'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
import { CONFIG } from '@server/initializers/config.js' import { CONFIG } from '@server/initializers/config.js'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js' import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
import { generateWebVideoFilename } from '@server/lib/paths.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js' import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { MVideoFullLight } from '@server/types/models/index.js' import { MVideoFullLight } from '@server/types/models/index.js'
import { getLowercaseExtension } from '@peertube/peertube-node-utils' import { getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg'
import { getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { JobQueue } from '../job-queue.js' import { JobQueue } from '../job-queue.js'
import { buildMoveJob } from '@server/lib/video-jobs.js' import { buildMoveJob } from '@server/lib/video-jobs.js'
import { buildNewFile } from '@server/lib/video-file.js'
async function processVideoFileImport (job: Job) { async function processVideoFileImport (job: Job) {
const payload = job.data as VideoFileImportPayload const payload = job.data as VideoFileImportPayload
@ -48,11 +45,6 @@ export {
async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath) const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath)
const { size } = await stat(inputFilePath)
const fps = await getVideoStreamFPS(inputFilePath)
const fileExt = getLowercaseExtension(inputFilePath)
const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === resolution) const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === resolution)
if (currentVideoFile) { if (currentVideoFile) {
@ -64,15 +56,8 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
await currentVideoFile.destroy() await currentVideoFile.destroy()
} }
const newVideoFile = new VideoFileModel({ const newVideoFile = await buildNewFile({ mode: 'web-video', path: inputFilePath })
resolution, newVideoFile.videoId = video.id
extname: fileExt,
filename: generateWebVideoFilename(resolution, fileExt),
storage: FileStorage.FILE_SYSTEM,
size,
fps,
videoId: video.id
})
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
await copy(inputFilePath, outputPath) await copy(inputFilePath, outputPath)

View File

@ -10,15 +10,12 @@ import {
VideoImportTorrentPayload, VideoImportTorrentPayload,
VideoImportTorrentPayloadType, VideoImportTorrentPayloadType,
VideoImportYoutubeDLPayload, VideoImportYoutubeDLPayload,
VideoImportYoutubeDLPayloadType, VideoImportYoutubeDLPayloadType, VideoState
VideoResolution,
VideoState
} from '@peertube/peertube-models' } from '@peertube/peertube-models'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js' import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { YoutubeDLWrapper } from '@server/helpers/youtube-dl/index.js' import { YoutubeDLWrapper } from '@server/helpers/youtube-dl/index.js'
import { CONFIG } from '@server/initializers/config.js' import { CONFIG } from '@server/initializers/config.js'
import { isPostImportVideoAccepted } from '@server/lib/moderation.js' import { isPostImportVideoAccepted } from '@server/lib/moderation.js'
import { generateWebVideoFilename } from '@server/lib/paths.js'
import { Hooks } from '@server/lib/plugins/hooks.js' import { Hooks } from '@server/lib/plugins/hooks.js'
import { ServerConfigManager } from '@server/lib/server-config-manager.js' import { ServerConfigManager } from '@server/lib/server-config-manager.js'
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js' import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js'
@ -28,14 +25,9 @@ import { buildNextVideoState } from '@server/lib/video-state.js'
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js' import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js' import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js' import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js'
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
import { import {
ffprobePromise, ffprobePromise,
getChaptersFromContainer, getChaptersFromContainer, getVideoStreamDuration
getVideoStreamDimensionsInfo,
getVideoStreamDuration,
getVideoStreamFPS,
isAudioFile
} from '@peertube/peertube-ffmpeg' } from '@peertube/peertube-ffmpeg'
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { getSecureTorrentName } from '../../../helpers/utils.js' import { getSecureTorrentName } from '../../../helpers/utils.js'
@ -51,6 +43,8 @@ import { generateLocalVideoMiniature } from '../../thumbnail.js'
import { JobQueue } from '../job-queue.js' import { JobQueue } from '../job-queue.js'
import { replaceChaptersIfNotExist } from '@server/lib/video-chapters.js' import { replaceChaptersIfNotExist } from '@server/lib/video-chapters.js'
import { FfprobeData } from 'fluent-ffmpeg' import { FfprobeData } from 'fluent-ffmpeg'
import { buildNewFile } from '@server/lib/video-file.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> { async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> {
const payload = job.data as VideoImportPayload const payload = job.data as VideoImportPayload
@ -129,46 +123,31 @@ type ProcessFileOptions = {
videoImportId: number videoImportId: number
} }
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) { async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
let tempVideoPath: string let tmpVideoPath: string
let videoFile: VideoFileModel let videoFile: VideoFileModel
try { try {
// Download video from youtubeDL // Download video from youtubeDL
tempVideoPath = await downloader() tmpVideoPath = await downloader()
// Get information about this video // Get information about this video
const stats = await stat(tempVideoPath) const stats = await stat(tmpVideoPath)
const isAble = await isUserQuotaValid({ userId: videoImport.User.id, uploadSize: stats.size }) const isAble = await isUserQuotaValid({ userId: videoImport.User.id, uploadSize: stats.size })
if (isAble === false) { if (isAble === false) {
throw new Error('The user video quota is exceeded with this video to import.') throw new Error('The user video quota is exceeded with this video to import.')
} }
const ffprobe = await ffprobePromise(tempVideoPath) const ffprobe = await ffprobePromise(tmpVideoPath)
const duration = await getVideoStreamDuration(tmpVideoPath, ffprobe)
const { resolution } = await isAudioFile(tempVideoPath, ffprobe)
? { resolution: VideoResolution.H_NOVIDEO }
: await getVideoStreamDimensionsInfo(tempVideoPath, ffprobe)
const fps = await getVideoStreamFPS(tempVideoPath, ffprobe)
const duration = await getVideoStreamDuration(tempVideoPath, ffprobe)
const containerChapters = await getChaptersFromContainer({ const containerChapters = await getChaptersFromContainer({
path: tempVideoPath, path: tmpVideoPath,
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max, maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
ffprobe ffprobe
}) })
// Prepare video file object for creation in database videoFile = await buildNewFile({ mode: 'web-video', ffprobe, path: tmpVideoPath })
const fileExt = getLowercaseExtension(tempVideoPath) videoFile.videoId = videoImport.videoId
const videoFileData = {
extname: fileExt,
resolution,
size: stats.size,
filename: generateWebVideoFilename(resolution, fileExt),
fps,
videoId: videoImport.videoId
}
videoFile = new VideoFileModel(videoFileData)
const hookName = options.type === 'youtube-dl' const hookName = options.type === 'youtube-dl'
? 'filter:api.video.post-import-url.accept.result' ? 'filter:api.video.post-import-url.accept.result'
@ -178,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
const acceptParameters = { const acceptParameters = {
videoImport, videoImport,
video: videoImport.Video, video: videoImport.Video,
videoFilePath: tempVideoPath, videoFilePath: tmpVideoPath,
videoFile, videoFile,
user: videoImport.User user: videoImport.User
} }
@ -201,9 +180,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
// Move file // Move file
const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile) const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile)
await move(tempVideoPath, videoDestFile) await move(tmpVideoPath, videoDestFile)
tempVideoPath = null // This path is not used anymore tmpVideoPath = null // This path is not used anymore
const thumbnails = await generateMiniature({ videoImportWithFiles, videoFile, ffprobe }) const thumbnails = await generateMiniature({ videoImportWithFiles, videoFile, ffprobe })
@ -221,6 +200,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
// Update video DB object // Update video DB object
video.duration = duration video.duration = duration
video.state = buildNextVideoState(video.state) video.state = buildNextVideoState(video.state)
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
await video.save({ transaction: t }) await video.save({ transaction: t })
for (const thumbnail of thumbnails) { for (const thumbnail of thumbnails) {
@ -248,7 +228,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
videoFileLockReleaser() videoFileLockReleaser()
} }
} catch (err) { } catch (err) {
await onImportError(err, tempVideoPath, videoImport) await onImportError(err, tmpVideoPath, videoImport)
throw err throw err
} }

View File

@ -125,6 +125,7 @@ async function saveReplayToExternalVideo (options: {
waitTranscoding: true, waitTranscoding: true,
nsfw: liveVideo.nsfw, nsfw: liveVideo.nsfw,
description: liveVideo.description, description: liveVideo.description,
aspectRatio: liveVideo.aspectRatio,
support: liveVideo.support, support: liveVideo.support,
privacy: replaySettings.privacy, privacy: replaySettings.privacy,
channelId: liveVideo.channelId channelId: liveVideo.channelId

View File

@ -328,7 +328,7 @@ class LiveManager {
allResolutions: number[] allResolutions: number[]
hasAudio: boolean hasAudio: boolean
}) { }) {
const { sessionId, videoLive, user } = options const { sessionId, videoLive, user, ratio } = options
const videoUUID = videoLive.Video.uuid const videoUUID = videoLive.Video.uuid
const localLTags = lTags(sessionId, videoUUID) const localLTags = lTags(sessionId, videoUUID)
@ -345,7 +345,7 @@ class LiveManager {
...pick(options, [ 'inputLocalUrl', 'inputPublicUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio' ]) ...pick(options, [ 'inputLocalUrl', 'inputPublicUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio' ])
}) })
muxingSession.on('live-ready', () => this.publishAndFederateLive(videoLive, localLTags)) muxingSession.on('live-ready', () => this.publishAndFederateLive({ live: videoLive, ratio, localLTags }))
muxingSession.on('bad-socket-health', ({ videoUUID }) => { muxingSession.on('bad-socket-health', ({ videoUUID }) => {
logger.error( logger.error(
@ -405,7 +405,13 @@ class LiveManager {
}) })
} }
private async publishAndFederateLive (live: MVideoLiveVideo, localLTags: { tags: (string | number)[] }) { private async publishAndFederateLive (options: {
live: MVideoLiveVideo
ratio: number
localLTags: { tags: (string | number)[] }
}) {
const { live, ratio, localLTags } = options
const videoId = live.videoId const videoId = live.videoId
try { try {
@ -415,6 +421,7 @@ class LiveManager {
video.state = VideoState.PUBLISHED video.state = VideoState.PUBLISHED
video.publishedAt = new Date() video.publishedAt = new Date()
video.aspectRatio = ratio
await video.save() await video.save()
live.Video = video live.Video = video

View File

@ -33,6 +33,7 @@ import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video
import { LoggerTagsFn, logger } from '@server/helpers/logger.js' import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js' import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { federateVideoIfNeeded } from './activitypub/videos/federate.js' import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
type VideoAttributes = Omit<VideoCreate, 'channelId'> & { type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
duration: number duration: number
@ -116,6 +117,8 @@ export class LocalVideoCreator {
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile) const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
await move(this.videoFilePath, destination) await move(this.videoFilePath, destination)
this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
} }
const thumbnails = await this.createThumbnails() const thumbnails = await this.createThumbnails()

View File

@ -1,50 +1,24 @@
import { move } from 'fs-extra/esm'
import { dirname, join } from 'path'
import { logger, LoggerTagsFn } from '@server/helpers/logger.js' import { logger, LoggerTagsFn } from '@server/helpers/logger.js'
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js' import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding.js' import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding.js'
import { buildNewFile } from '@server/lib/video-file.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { MVideoFullLight } from '@server/types/models/index.js' import { MVideoFullLight } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js' import { MRunnerJob } from '@server/types/models/runners/index.js'
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models' import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
import { lTags } from '@server/lib/object-storage/shared/logger.js'
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: { export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
video: MVideoFullLight video: MVideoFullLight
videoFilePath: string videoFilePath: string
privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload | RunnerJobVODAudioMergeTranscodingPrivatePayload privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload | RunnerJobVODAudioMergeTranscodingPrivatePayload
wasAudioFile: boolean
}) { }) {
const { video, videoFilePath, privatePayload } = options const { video, videoFilePath, privatePayload, wasAudioFile } = options
const videoFile = await buildNewFile({ path: videoFilePath, mode: 'web-video' }) const deleteWebInputVideoFile = privatePayload.deleteInputFileId
videoFile.videoId = video.id ? video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
: undefined
const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename) await onWebVideoFileTranscoding({ video, videoOutputPath: videoFilePath, deleteWebInputVideoFile, wasAudioFile })
await move(videoFilePath, newVideoFilePath)
await onWebVideoFileTranscoding({
video,
videoFile,
videoOutputPath: newVideoFilePath
})
if (privatePayload.deleteInputFileId) {
const inputFile = video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
if (inputFile) {
await video.removeWebVideoFile(inputFile)
await inputFile.destroy()
video.VideoFiles = video.VideoFiles.filter(f => f.id !== inputFile.id)
} else {
logger.error(
'Cannot delete input file %d of video %s: does not exist anymore',
privatePayload.deleteInputFileId, video.uuid,
{ ...lTags(video.uuid), privatePayload }
)
}
}
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video }) await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
} }

View File

@ -4,7 +4,6 @@ import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js' import { MRunnerJob } from '@server/types/models/runners/index.js'
import { pick } from '@peertube/peertube-core-utils' import { pick } from '@peertube/peertube-core-utils'
import { buildUUID } from '@peertube/peertube-node-utils' import { buildUUID } from '@peertube/peertube-node-utils'
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
import { import {
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
RunnerJobVODAudioMergeTranscodingPayload, RunnerJobVODAudioMergeTranscodingPayload,
@ -77,12 +76,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
const videoFilePath = resultPayload.videoFile as string const videoFilePath = resultPayload.videoFile as string
// ffmpeg generated a new video file, so update the video duration await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: true })
// See https://trac.ffmpeg.org/ticket/5456
video.duration = await getVideoStreamDuration(videoFilePath)
await video.save()
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
logger.info( logger.info(
'Runner VOD audio merge transcoding job %s for %s ended.', 'Runner VOD audio merge transcoding job %s for %s ended.',

View File

@ -1,11 +1,7 @@
import { move } from 'fs-extra/esm'
import { dirname, join } from 'path'
import { logger } from '@server/helpers/logger.js' import { logger } from '@server/helpers/logger.js'
import { renameVideoFileInPlaylist } from '@server/lib/hls.js'
import { getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js' import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js' import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js'
import { buildNewFile, removeAllWebVideoFiles } from '@server/lib/video-file.js' import { removeAllWebVideoFiles } from '@server/lib/video-file.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js' import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MVideo } from '@server/types/models/index.js' import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js' import { MRunnerJob } from '@server/types/models/runners/index.js'
@ -84,21 +80,10 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle
const videoFilePath = resultPayload.videoFile as string const videoFilePath = resultPayload.videoFile as string
const resolutionPlaylistFilePath = resultPayload.resolutionPlaylistFile as string const resolutionPlaylistFilePath = resultPayload.resolutionPlaylistFile as string
const videoFile = await buildNewFile({ path: videoFilePath, mode: 'hls' })
const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename)
await move(videoFilePath, newVideoFilePath)
const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
const newResolutionPlaylistFilePath = join(dirname(resolutionPlaylistFilePath), resolutionPlaylistFilename)
await move(resolutionPlaylistFilePath, newResolutionPlaylistFilePath)
await renameVideoFileInPlaylist(newResolutionPlaylistFilePath, videoFile.filename)
await onHLSVideoFileTranscoding({ await onHLSVideoFileTranscoding({
video, video,
videoFile, m3u8OutputPath: resolutionPlaylistFilePath,
m3u8OutputPath: newResolutionPlaylistFilePath, videoOutputPath: videoFilePath
videoOutputPath: newVideoFilePath
}) })
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video }) await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })

View File

@ -75,7 +75,7 @@ export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobH
const videoFilePath = resultPayload.videoFile as string const videoFilePath = resultPayload.videoFile as string
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload }) await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: false })
logger.info( logger.info(
'Runner VOD web video transcoding job %s for %s ended.', 'Runner VOD web video transcoding job %s for %s ended.',

View File

@ -1,20 +1,19 @@
import { MutexInterface } from 'async-mutex' import { MutexInterface } from 'async-mutex'
import { Job } from 'bullmq' import { Job } from 'bullmq'
import { ensureDir, move } from 'fs-extra/esm' import { ensureDir, move } from 'fs-extra/esm'
import { stat } from 'fs/promises' import { join } from 'path'
import { basename, extname as extnameUtil, join } from 'path'
import { pick } from '@peertube/peertube-core-utils' import { pick } from '@peertube/peertube-core-utils'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js' import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
import { sequelizeTypescript } from '@server/initializers/database.js' import { sequelizeTypescript } from '@server/initializers/database.js'
import { MVideo, MVideoFile } from '@server/types/models/index.js' import { MVideo } from '@server/types/models/index.js'
import { getVideoStreamDuration, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
import { CONFIG } from '../../initializers/config.js' import { CONFIG } from '../../initializers/config.js'
import { VideoFileModel } from '../../models/video/video-file.js' import { VideoFileModel } from '../../models/video/video-file.js'
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js' import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
import { updatePlaylistAfterFileChange } from '../hls.js' import { renameVideoFileInPlaylist, updatePlaylistAfterFileChange } from '../hls.js'
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js' import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
import { buildFileMetadata } from '../video-file.js' import { buildNewFile } from '../video-file.js'
import { VideoPathManager } from '../video-path-manager.js' import { VideoPathManager } from '../video-path-manager.js'
import { buildFFmpegVOD } from './shared/index.js' import { buildFFmpegVOD } from './shared/index.js'
@ -55,12 +54,11 @@ export function generateHlsPlaylistResolution (options: {
export async function onHLSVideoFileTranscoding (options: { export async function onHLSVideoFileTranscoding (options: {
video: MVideo video: MVideo
videoFile: MVideoFile
videoOutputPath: string videoOutputPath: string
m3u8OutputPath: string m3u8OutputPath: string
filesLockedInParent?: boolean // default false filesLockedInParent?: boolean // default false
}) { }) {
const { video, videoFile, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options const { video, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
// Create or update the playlist // Create or update the playlist
const playlist = await retryTransactionWrapper(() => { const playlist = await retryTransactionWrapper(() => {
@ -68,7 +66,9 @@ export async function onHLSVideoFileTranscoding (options: {
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
}) })
}) })
videoFile.videoStreamingPlaylistId = playlist.id
const newVideoFile = await buildNewFile({ mode: 'hls', path: videoOutputPath })
newVideoFile.videoStreamingPlaylistId = playlist.id
const mutexReleaser = !filesLockedInParent const mutexReleaser = !filesLockedInParent
? await VideoPathManager.Instance.lockFiles(video.uuid) ? await VideoPathManager.Instance.lockFiles(video.uuid)
@ -77,33 +77,33 @@ export async function onHLSVideoFileTranscoding (options: {
try { try {
await video.reload() await video.reload()
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile) const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
// Move playlist file // Move playlist file
const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath)) const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(
video,
getHlsResolutionPlaylistFilename(newVideoFile.filename)
)
await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true }) await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
// Move video file // Move video file
await move(videoOutputPath, videoFilePath, { overwrite: true }) await move(videoOutputPath, videoFilePath, { overwrite: true })
await renameVideoFileInPlaylist(resolutionPlaylistPath, newVideoFile.filename)
// Update video duration if it was not set (in case of a live for example) // Update video duration if it was not set (in case of a live for example)
if (!video.duration) { if (!video.duration) {
video.duration = await getVideoStreamDuration(videoFilePath) video.duration = await getVideoStreamDuration(videoFilePath)
await video.save() await video.save()
} }
const stats = await stat(videoFilePath) await createTorrentAndSetInfoHash(playlist, newVideoFile)
videoFile.size = stats.size
videoFile.fps = await getVideoStreamFPS(videoFilePath)
videoFile.metadata = await buildFileMetadata(videoFilePath)
await createTorrentAndSetInfoHash(playlist, videoFile)
const oldFile = await VideoFileModel.loadHLSFile({ const oldFile = await VideoFileModel.loadHLSFile({
playlistId: playlist.id, playlistId: playlist.id,
fps: videoFile.fps, fps: newVideoFile.fps,
resolution: videoFile.resolution resolution: newVideoFile.resolution
}) })
if (oldFile) { if (oldFile) {
@ -111,7 +111,7 @@ export async function onHLSVideoFileTranscoding (options: {
await oldFile.destroy() await oldFile.destroy()
} }
const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined) const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
await updatePlaylistAfterFileChange(video, playlist) await updatePlaylistAfterFileChange(video, playlist)
@ -171,17 +171,8 @@ async function generateHlsPlaylistCommon (options: {
await buildFFmpegVOD(job).transcode(transcodeOptions) await buildFFmpegVOD(job).transcode(transcodeOptions)
const newVideoFile = new VideoFileModel({
resolution,
extname: extnameUtil(videoFilename),
size: 0,
filename: videoFilename,
fps: -1
})
await onHLSVideoFileTranscoding({ await onHLSVideoFileTranscoding({
video, video,
videoFile: newVideoFile,
videoOutputPath, videoOutputPath,
m3u8OutputPath, m3u8OutputPath,
filesLockedInParent: !inputFileMutexReleaser filesLockedInParent: !inputFileMutexReleaser

View File

@ -1,22 +1,22 @@
import { Job } from 'bullmq' import { Job } from 'bullmq'
import { move, remove } from 'fs-extra/esm' import { move, remove } from 'fs-extra/esm'
import { copyFile, stat } from 'fs/promises' import { copyFile } from 'fs/promises'
import { basename, join } from 'path' import { basename, join } from 'path'
import { FileStorage } from '@peertube/peertube-models'
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js' import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js' import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@peertube/peertube-ffmpeg' import { getVideoStreamDuration, TranscodeVODOptionsType } from '@peertube/peertube-ffmpeg'
import { CONFIG } from '../../initializers/config.js' import { CONFIG } from '../../initializers/config.js'
import { VideoFileModel } from '../../models/video/video-file.js' import { VideoFileModel } from '../../models/video/video-file.js'
import { JobQueue } from '../job-queue/index.js' import { JobQueue } from '../job-queue/index.js'
import { generateWebVideoFilename } from '../paths.js' import { generateWebVideoFilename } from '../paths.js'
import { buildFileMetadata } from '../video-file.js' import { buildNewFile } from '../video-file.js'
import { VideoPathManager } from '../video-path-manager.js' import { VideoPathManager } from '../video-path-manager.js'
import { buildFFmpegVOD } from './shared/index.js' import { buildFFmpegVOD } from './shared/index.js'
import { buildOriginalFileResolution } from './transcoding-resolutions.js' import { buildOriginalFileResolution } from './transcoding-resolutions.js'
import { buildStoryboardJobIfNeeded } from '../video-jobs.js' import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
// Optimize the original video file and replace it. The resolution is not changed. // Optimize the original video file and replace it. The resolution is not changed.
export async function optimizeOriginalVideofile (options: { export async function optimizeOriginalVideofile (options: {
@ -62,19 +62,7 @@ export async function optimizeOriginalVideofile (options: {
fps fps
}) })
// Important to do this before getVideoFilename() to take in account the new filename const { videoFile } = await onWebVideoFileTranscoding({ video, videoOutputPath, deleteWebInputVideoFile: inputVideoFile })
inputVideoFile.resolution = resolution
inputVideoFile.extname = newExtname
inputVideoFile.filename = generateWebVideoFilename(resolution, newExtname)
inputVideoFile.storage = FileStorage.FILE_SYSTEM
const { videoFile } = await onWebVideoFileTranscoding({
video,
videoFile: inputVideoFile,
videoOutputPath
})
await remove(videoInputPath)
return { transcodeType, videoFile } return { transcodeType, videoFile }
}) })
@ -104,15 +92,8 @@ export async function transcodeNewWebVideoResolution (options: {
const file = video.getMaxQualityFile().withVideoOrPlaylist(video) const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => { const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
const newVideoFile = new VideoFileModel({ const filename = generateWebVideoFilename(resolution, newExtname)
resolution, const videoOutputPath = join(transcodeDirectory, filename)
extname: newExtname,
filename: generateWebVideoFilename(resolution, newExtname),
size: 0,
videoId: video.id
})
const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
const transcodeOptions = { const transcodeOptions = {
type: 'video' as 'video', type: 'video' as 'video',
@ -128,7 +109,7 @@ export async function transcodeNewWebVideoResolution (options: {
await buildFFmpegVOD(job).transcode(transcodeOptions) await buildFFmpegVOD(job).transcode(transcodeOptions)
return onWebVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) return onWebVideoFileTranscoding({ video, videoOutputPath })
}) })
return result return result
@ -188,20 +169,10 @@ export async function mergeAudioVideofile (options: {
throw err throw err
} }
// Important to do this before getVideoFilename() to take in account the new file extension await onWebVideoFileTranscoding({
inputVideoFile.extname = newExtname
inputVideoFile.resolution = resolution
inputVideoFile.filename = generateWebVideoFilename(inputVideoFile.resolution, newExtname)
// ffmpeg generated a new video file, so update the video duration
// See https://trac.ffmpeg.org/ticket/5456
video.duration = await getVideoStreamDuration(videoOutputPath)
await video.save()
return onWebVideoFileTranscoding({
video, video,
videoFile: inputVideoFile,
videoOutputPath, videoOutputPath,
deleteWebInputVideoFile: inputVideoFile,
wasAudioFile: true wasAudioFile: true
}) })
}) })
@ -214,36 +185,42 @@ export async function mergeAudioVideofile (options: {
export async function onWebVideoFileTranscoding (options: { export async function onWebVideoFileTranscoding (options: {
video: MVideoFullLight video: MVideoFullLight
videoFile: MVideoFile
videoOutputPath: string videoOutputPath: string
wasAudioFile?: boolean // default false wasAudioFile?: boolean // default false
deleteWebInputVideoFile?: MVideoFile
}) { }) {
const { video, videoFile, videoOutputPath, wasAudioFile } = options const { video, videoOutputPath, wasAudioFile, deleteWebInputVideoFile } = options
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
const videoFile = await buildNewFile({ mode: 'web-video', path: videoOutputPath })
videoFile.videoId = video.id
try { try {
await video.reload() await video.reload()
// ffmpeg generated a new video file, so update the video duration
// See https://trac.ffmpeg.org/ticket/5456
if (wasAudioFile) {
video.duration = await getVideoStreamDuration(videoOutputPath)
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
await video.save()
}
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
const stats = await stat(videoOutputPath)
const probe = await ffprobePromise(videoOutputPath)
const fps = await getVideoStreamFPS(videoOutputPath, probe)
const metadata = await buildFileMetadata(videoOutputPath, probe)
await move(videoOutputPath, outputPath, { overwrite: true }) await move(videoOutputPath, outputPath, { overwrite: true })
videoFile.size = stats.size
videoFile.fps = fps
videoFile.metadata = metadata
await createTorrentAndSetInfoHash(video, videoFile) await createTorrentAndSetInfoHash(video, videoFile)
const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
if (oldFile) await video.removeWebVideoFile(oldFile) if (oldFile) await video.removeWebVideoFile(oldFile)
if (deleteWebInputVideoFile) {
await video.removeWebVideoFile(deleteWebInputVideoFile)
await deleteWebInputVideoFile.destroy()
}
await VideoFileModel.customUpsert(videoFile, 'video', undefined) await VideoFileModel.customUpsert(videoFile, 'video', undefined)
video.VideoFiles = await video.$get('VideoFiles') video.VideoFiles = await video.$get('VideoFiles')

View File

@ -29,8 +29,11 @@ async function buildNewFile (options: {
if (await isAudioFile(path, probe)) { if (await isAudioFile(path, probe)) {
videoFile.resolution = VideoResolution.H_NOVIDEO videoFile.resolution = VideoResolution.H_NOVIDEO
} else { } else {
const dimensions = await getVideoStreamDimensionsInfo(path, probe)
videoFile.fps = await getVideoStreamFPS(path, probe) videoFile.fps = await getVideoStreamFPS(path, probe)
videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution videoFile.resolution = dimensions.resolution
videoFile.width = dimensions.width
videoFile.height = dimensions.height
} }
videoFile.filename = mode === 'web-video' videoFile.filename = mode === 'web-video'

View File

@ -12,6 +12,7 @@ import { getTranscodingJobPriority } from './transcoding/transcoding-priority.js
import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file.js' import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file.js'
import { VideoPathManager } from './video-path-manager.js' import { VideoPathManager } from './video-path-manager.js'
import { buildStoryboardJobIfNeeded } from './video-jobs.js' import { buildStoryboardJobIfNeeded } from './video-jobs.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('video-studio') const lTags = loggerTagsFactory('video-studio')
@ -104,6 +105,7 @@ export async function onVideoStudioEnded (options: {
await newFile.save() await newFile.save()
video.duration = await getVideoStreamDuration(outputPath) video.duration = await getVideoStreamDuration(outputPath)
video.aspectRatio = buildAspectRatio({ width: newFile.width, height: newFile.height })
await video.save() await video.save()
return JobQueue.Instance.createSequentialJobFlow( return JobQueue.Instance.createSequentialJobFlow(

View File

@ -18,6 +18,7 @@ import {
isPluginTypeValid isPluginTypeValid
} from '../../helpers/custom-validators/plugins.js' } from '../../helpers/custom-validators/plugins.js'
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js' import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
import { logger } from '@server/helpers/logger.js'
@DefaultScope(() => ({ @DefaultScope(() => ({
attributes: { attributes: {
@ -173,6 +174,7 @@ export class PluginModel extends SequelizeModel<PluginModel> {
result[name] = p.settings[name] result[name] = p.settings[name]
} }
} }
logger.error('internal', { result })
return result return result
}) })

View File

@ -88,6 +88,8 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
preview: buildPreviewAPAttribute(video), preview: buildPreviewAPAttribute(video),
aspectRatio: video.aspectRatio,
url, url,
likes: getLocalVideoLikesActivityPubUrl(video), likes: getLocalVideoLikesActivityPubUrl(video),
@ -185,7 +187,8 @@ function buildVideoFileUrls (options: {
rel: [ 'metadata', fileAP.mediaType ], rel: [ 'metadata', fileAP.mediaType ],
mediaType: 'application/json' as 'application/json', mediaType: 'application/json' as 'application/json',
href: getLocalVideoFileMetadataUrl(video, file), href: getLocalVideoFileMetadataUrl(video, file),
height: file.resolution, height: file.height || file.resolution,
width: file.width,
fps: file.fps fps: file.fps
}) })
@ -194,14 +197,18 @@ function buildVideoFileUrls (options: {
type: 'Link', type: 'Link',
mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
href: file.getTorrentUrl(), href: file.getTorrentUrl(),
height: file.resolution height: file.height || file.resolution,
width: file.width,
fps: file.fps
}) })
urls.push({ urls.push({
type: 'Link', type: 'Link',
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
href: generateMagnetUri(video, file, trackerUrls), href: generateMagnetUri(video, file, trackerUrls),
height: file.resolution height: file.height || file.resolution,
width: file.width,
fps: file.fps
}) })
} }
} }

View File

@ -89,6 +89,8 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
isLocal: video.isOwned(), isLocal: video.isOwned(),
duration: video.duration, duration: video.duration,
aspectRatio: video.aspectRatio,
views: video.views, views: video.views,
viewers: VideoViewsManager.Instance.getTotalViewersOf(video), viewers: VideoViewsManager.Instance.getTotalViewersOf(video),
@ -214,6 +216,9 @@ export function videoFilesModelToFormattedJSON (
: `${videoFile.resolution}p` : `${videoFile.resolution}p`
}, },
width: videoFile.width,
height: videoFile.height,
magnetUri: includeMagnet && videoFile.hasTorrent() magnetUri: includeMagnet && videoFile.hasTorrent()
? generateMagnetUri(video, videoFile, trackerUrls) ? generateMagnetUri(video, videoFile, trackerUrls)
: undefined, : undefined,

View File

@ -88,6 +88,8 @@ export class VideoTableAttributes {
'metadataUrl', 'metadataUrl',
'videoStreamingPlaylistId', 'videoStreamingPlaylistId',
'videoId', 'videoId',
'width',
'height',
'storage' 'storage'
] ]
} }
@ -255,6 +257,7 @@ export class VideoTableAttributes {
'dislikes', 'dislikes',
'remote', 'remote',
'isLive', 'isLive',
'aspectRatio',
'url', 'url',
'commentsEnabled', 'commentsEnabled',
'downloadEnabled', 'downloadEnabled',

View File

@ -167,6 +167,14 @@ export class VideoFileModel extends SequelizeModel<VideoFileModel> {
@Column @Column
resolution: number resolution: number
@AllowNull(true)
@Column
width: number
@AllowNull(true)
@Column
height: number
@AllowNull(false) @AllowNull(false)
@Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size')) @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
@Column(DataType.BIGINT) @Column(DataType.BIGINT)
@ -640,7 +648,8 @@ export class VideoFileModel extends SequelizeModel<VideoFileModel> {
type: 'Link', type: 'Link',
mediaType: mimeType as ActivityVideoUrlObject['mediaType'], mediaType: mimeType as ActivityVideoUrlObject['mediaType'],
href: this.getFileUrl(video), href: this.getFileUrl(video),
height: this.resolution, height: this.height || this.resolution,
width: this.width,
size: this.size, size: this.size,
fps: this.fps fps: this.fps
} }

View File

@ -565,6 +565,10 @@ export class VideoModel extends SequelizeModel<VideoModel> {
@Column @Column
state: VideoStateType state: VideoStateType
@AllowNull(true)
@Column(DataType.FLOAT)
aspectRatio: number
// We already have the information in videoSource table for local videos, but we prefer to normalize it for performance // We already have the information in videoSource table for local videos, but we prefer to normalize it for performance
// And also to store the info from remote instances // And also to store the info from remote instances
@AllowNull(true) @AllowNull(true)

View File

@ -2086,7 +2086,7 @@ paths:
/api/v1/users/me/videos: /api/v1/users/me/videos:
get: get:
summary: Get videos of my user summary: List videos of my user
security: security:
- OAuth2: - OAuth2:
- user - user
@ -7560,6 +7560,12 @@ components:
fps: fps:
type: number type: number
description: Frames per second of the video file description: Frames per second of the video file
width:
type: number
description: "**PeerTube >= 6.1** Video stream width"
height:
type: number
description: "**PeerTube >= 6.1** Video stream height"
metadataUrl: metadataUrl:
type: string type: string
format: url format: url
@ -7676,6 +7682,11 @@ components:
example: 1419 example: 1419
format: seconds format: seconds
description: duration of the video in seconds description: duration of the video in seconds
aspectRatio:
type: number
format: float
example: 1.778
description: "**PeerTube >= 6.1** Aspect ratio of the video stream"
isLocal: isLocal:
type: boolean type: boolean
name: name: