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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -328,7 +328,7 @@ class LiveManager {
allResolutions: number[]
hasAudio: boolean
}) {
const { sessionId, videoLive, user } = options
const { sessionId, videoLive, user, ratio } = options
const videoUUID = videoLive.Video.uuid
const localLTags = lTags(sessionId, videoUUID)
@ -345,7 +345,7 @@ class LiveManager {
...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 }) => {
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
try {
@ -415,6 +421,7 @@ class LiveManager {
video.state = VideoState.PUBLISHED
video.publishedAt = new Date()
video.aspectRatio = ratio
await video.save()
live.Video = video

View File

@ -33,6 +33,7 @@ import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video
import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
import { buildAspectRatio } from '@peertube/peertube-core-utils'
type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
duration: number
@ -116,6 +117,8 @@ export class LocalVideoCreator {
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
await move(this.videoFilePath, destination)
this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
}
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 { onTranscodingEnded } from '@server/lib/transcoding/ended-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 { MVideoFullLight } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
import { lTags } from '@server/lib/object-storage/shared/logger.js'
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
video: MVideoFullLight
videoFilePath: string
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' })
videoFile.videoId = video.id
const deleteWebInputVideoFile = privatePayload.deleteInputFileId
? video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
: undefined
const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename)
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 onWebVideoFileTranscoding({ video, videoOutputPath: videoFilePath, deleteWebInputVideoFile, wasAudioFile })
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 { pick } from '@peertube/peertube-core-utils'
import { buildUUID } from '@peertube/peertube-node-utils'
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
import {
RunnerJobUpdatePayload,
RunnerJobVODAudioMergeTranscodingPayload,
@ -77,12 +76,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
const videoFilePath = resultPayload.videoFile as string
// ffmpeg generated a new video file, so update the video duration
// See https://trac.ffmpeg.org/ticket/5456
video.duration = await getVideoStreamDuration(videoFilePath)
await video.save()
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: true })
logger.info(
'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 { renameVideoFileInPlaylist } from '@server/lib/hls.js'
import { getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
import { onTranscodingEnded } from '@server/lib/transcoding/ended-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 { MVideo } from '@server/types/models/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 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({
video,
videoFile,
m3u8OutputPath: newResolutionPlaylistFilePath,
videoOutputPath: newVideoFilePath
m3u8OutputPath: resolutionPlaylistFilePath,
videoOutputPath: videoFilePath
})
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
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: false })
logger.info(
'Runner VOD web video transcoding job %s for %s ended.',

View File

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

View File

@ -1,22 +1,22 @@
import { Job } from 'bullmq'
import { move, remove } from 'fs-extra/esm'
import { copyFile, stat } from 'fs/promises'
import { copyFile } from 'fs/promises'
import { basename, join } from 'path'
import { FileStorage } from '@peertube/peertube-models'
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
import { VideoModel } from '@server/models/video/video.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 { VideoFileModel } from '../../models/video/video-file.js'
import { JobQueue } from '../job-queue/index.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 { buildFFmpegVOD } from './shared/index.js'
import { buildOriginalFileResolution } from './transcoding-resolutions.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.
export async function optimizeOriginalVideofile (options: {
@ -62,19 +62,7 @@ export async function optimizeOriginalVideofile (options: {
fps
})
// Important to do this before getVideoFilename() to take in account the new filename
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)
const { videoFile } = await onWebVideoFileTranscoding({ video, videoOutputPath, deleteWebInputVideoFile: inputVideoFile })
return { transcodeType, videoFile }
})
@ -104,15 +92,8 @@ export async function transcodeNewWebVideoResolution (options: {
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
const newVideoFile = new VideoFileModel({
resolution,
extname: newExtname,
filename: generateWebVideoFilename(resolution, newExtname),
size: 0,
videoId: video.id
})
const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
const filename = generateWebVideoFilename(resolution, newExtname)
const videoOutputPath = join(transcodeDirectory, filename)
const transcodeOptions = {
type: 'video' as 'video',
@ -128,7 +109,7 @@ export async function transcodeNewWebVideoResolution (options: {
await buildFFmpegVOD(job).transcode(transcodeOptions)
return onWebVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath })
return onWebVideoFileTranscoding({ video, videoOutputPath })
})
return result
@ -188,20 +169,10 @@ export async function mergeAudioVideofile (options: {
throw err
}
// Important to do this before getVideoFilename() to take in account the new file extension
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({
await onWebVideoFileTranscoding({
video,
videoFile: inputVideoFile,
videoOutputPath,
deleteWebInputVideoFile: inputVideoFile,
wasAudioFile: true
})
})
@ -214,36 +185,42 @@ export async function mergeAudioVideofile (options: {
export async function onWebVideoFileTranscoding (options: {
video: MVideoFullLight
videoFile: MVideoFile
videoOutputPath: string
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 videoFile = await buildNewFile({ mode: 'web-video', path: videoOutputPath })
videoFile.videoId = video.id
try {
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 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 })
videoFile.size = stats.size
videoFile.fps = fps
videoFile.metadata = metadata
await createTorrentAndSetInfoHash(video, videoFile)
const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
if (oldFile) await video.removeWebVideoFile(oldFile)
if (deleteWebInputVideoFile) {
await video.removeWebVideoFile(deleteWebInputVideoFile)
await deleteWebInputVideoFile.destroy()
}
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
video.VideoFiles = await video.$get('VideoFiles')

View File

@ -29,8 +29,11 @@ async function buildNewFile (options: {
if (await isAudioFile(path, probe)) {
videoFile.resolution = VideoResolution.H_NOVIDEO
} else {
const dimensions = await getVideoStreamDimensionsInfo(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'

View File

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

View File

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

View File

@ -88,6 +88,8 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
preview: buildPreviewAPAttribute(video),
aspectRatio: video.aspectRatio,
url,
likes: getLocalVideoLikesActivityPubUrl(video),
@ -185,7 +187,8 @@ function buildVideoFileUrls (options: {
rel: [ 'metadata', fileAP.mediaType ],
mediaType: 'application/json' as 'application/json',
href: getLocalVideoFileMetadataUrl(video, file),
height: file.resolution,
height: file.height || file.resolution,
width: file.width,
fps: file.fps
})
@ -194,14 +197,18 @@ function buildVideoFileUrls (options: {
type: 'Link',
mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
href: file.getTorrentUrl(),
height: file.resolution
height: file.height || file.resolution,
width: file.width,
fps: file.fps
})
urls.push({
type: 'Link',
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
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(),
duration: video.duration,
aspectRatio: video.aspectRatio,
views: video.views,
viewers: VideoViewsManager.Instance.getTotalViewersOf(video),
@ -214,6 +216,9 @@ export function videoFilesModelToFormattedJSON (
: `${videoFile.resolution}p`
},
width: videoFile.width,
height: videoFile.height,
magnetUri: includeMagnet && videoFile.hasTorrent()
? generateMagnetUri(video, videoFile, trackerUrls)
: undefined,

View File

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

View File

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

View File

@ -565,6 +565,10 @@ export class VideoModel extends SequelizeModel<VideoModel> {
@Column
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
// And also to store the info from remote instances
@AllowNull(true)

View File

@ -2086,7 +2086,7 @@ paths:
/api/v1/users/me/videos:
get:
summary: Get videos of my user
summary: List videos of my user
security:
- OAuth2:
- user
@ -7560,6 +7560,12 @@ components:
fps:
type: number
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:
type: string
format: url
@ -7676,6 +7682,11 @@ components:
example: 1419
format: 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:
type: boolean
name: