Add video aspect ratio in server
This commit is contained in:
parent
c75381208f
commit
b6b1aaa56f
|
@ -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) {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -44,6 +44,8 @@ export interface VideoObject {
|
|||
|
||||
support: string
|
||||
|
||||
aspectRatio: number
|
||||
|
||||
icon: ActivityIconObject[]
|
||||
|
||||
url: ActivityUrlObject[]
|
||||
|
|
|
@ -7,6 +7,9 @@ export interface VideoFile {
|
|||
resolution: VideoConstant<number>
|
||||
size: number // Bytes
|
||||
|
||||
width?: number
|
||||
height?: number
|
||||
|
||||
torrentUrl: string
|
||||
torrentDownloadUrl: string
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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)
|
||||
|
|
|
@ -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)}`)
|
||||
|
|
|
@ -479,6 +479,8 @@ describe('Test follows', function () {
|
|||
files: [
|
||||
{
|
||||
resolution: 720,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
size: 218910
|
||||
}
|
||||
]
|
||||
|
|
|
@ -69,6 +69,8 @@ describe('Test handle downs', function () {
|
|||
fixture: 'video_short1.webm',
|
||||
files: [
|
||||
{
|
||||
height: 720,
|
||||
width: 1280,
|
||||
resolution: 720,
|
||||
size: 572456
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ import { cpus } from 'os'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 820
|
||||
const LAST_MIGRATION_VERSION = 825
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue