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
|
thumbnailPath: string
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
|
|
||||||
|
aspectRatio: number
|
||||||
|
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
|
|
||||||
previewPath: string
|
previewPath: string
|
||||||
|
@ -197,6 +199,8 @@ export class Video implements VideoServerModel {
|
||||||
this.originInstanceUrl = 'https://' + this.originInstanceHost
|
this.originInstanceUrl = 'https://' + this.originInstanceHost
|
||||||
|
|
||||||
this.pluginData = hash.pluginData
|
this.pluginData = hash.pluginData
|
||||||
|
|
||||||
|
this.aspectRatio = hash.aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
|
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
|
||||||
|
|
|
@ -103,9 +103,14 @@ function calculateBitrate (options: {
|
||||||
VideoResolution.H_NOVIDEO
|
VideoResolution.H_NOVIDEO
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const size1 = resolution
|
||||||
|
const size2 = ratio < 1 && ratio > 0
|
||||||
|
? resolution / ratio // Portrait mode
|
||||||
|
: resolution * ratio
|
||||||
|
|
||||||
for (const toTestResolution of resolutionsOrder) {
|
for (const toTestResolution of resolutionsOrder) {
|
||||||
if (toTestResolution <= resolution) {
|
if (toTestResolution <= resolution) {
|
||||||
return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution])
|
return Math.floor(size1 * size2 * fps * bitPerPixel[toTestResolution])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||||
|
|
||||||
function getAllPrivacies () {
|
export function getAllPrivacies () {
|
||||||
return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
|
return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
|
export function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
|
||||||
const files = video.files
|
const files = video.files
|
||||||
|
|
||||||
const hls = getHLS(video)
|
const hls = getHLS(video)
|
||||||
|
@ -13,12 +13,13 @@ function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlay
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
|
export function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
|
||||||
return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export function buildAspectRatio (options: { width: number, height: number }) {
|
||||||
getAllPrivacies,
|
const { width, height } = options
|
||||||
getAllFiles,
|
if (!width || !height) return null
|
||||||
getHLS
|
|
||||||
|
return Math.round((width / height) * 10000) / 10000 // 4 decimals precision
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'
|
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'
|
||||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
import { buildAspectRatio, forceNumber } from '@peertube/peertube-core-utils'
|
||||||
import { VideoResolution } from '@peertube/peertube-models'
|
import { VideoResolution } from '@peertube/peertube-models'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,7 +123,7 @@ async function getVideoStreamDimensionsInfo (path: string, existingProbe?: Ffpro
|
||||||
return {
|
return {
|
||||||
width: videoStream.width,
|
width: videoStream.width,
|
||||||
height: videoStream.height,
|
height: videoStream.height,
|
||||||
ratio: Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width),
|
ratio: buildAspectRatio({ width: videoStream.width, height: videoStream.height }),
|
||||||
resolution: Math.min(videoStream.height, videoStream.width),
|
resolution: Math.min(videoStream.height, videoStream.width),
|
||||||
isPortraitMode: videoStream.height > videoStream.width
|
isPortraitMode: videoStream.height > videoStream.width
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ export interface ActivityIconObject {
|
||||||
type: 'Image'
|
type: 'Image'
|
||||||
url: string
|
url: string
|
||||||
mediaType: string
|
mediaType: string
|
||||||
width?: number
|
width: number
|
||||||
height?: number
|
height: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActivityVideoUrlObject = {
|
export type ActivityVideoUrlObject = {
|
||||||
|
@ -19,6 +19,7 @@ export type ActivityVideoUrlObject = {
|
||||||
mediaType: 'video/mp4' | 'video/webm' | 'video/ogg' | 'audio/mp4'
|
mediaType: 'video/mp4' | 'video/webm' | 'video/ogg' | 'audio/mp4'
|
||||||
href: string
|
href: string
|
||||||
height: number
|
height: number
|
||||||
|
width: number | null
|
||||||
size: number
|
size: number
|
||||||
fps: number
|
fps: number
|
||||||
}
|
}
|
||||||
|
@ -35,6 +36,7 @@ export type ActivityVideoFileMetadataUrlObject = {
|
||||||
rel: [ 'metadata', any ]
|
rel: [ 'metadata', any ]
|
||||||
mediaType: 'application/json'
|
mediaType: 'application/json'
|
||||||
height: number
|
height: number
|
||||||
|
width: number | null
|
||||||
href: string
|
href: string
|
||||||
fps: number
|
fps: number
|
||||||
}
|
}
|
||||||
|
@ -63,6 +65,8 @@ export type ActivityBitTorrentUrlObject = {
|
||||||
mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
|
mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
|
||||||
href: string
|
href: string
|
||||||
height: number
|
height: number
|
||||||
|
width: number | null
|
||||||
|
fps: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActivityMagnetUrlObject = {
|
export type ActivityMagnetUrlObject = {
|
||||||
|
@ -70,6 +74,8 @@ export type ActivityMagnetUrlObject = {
|
||||||
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet'
|
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet'
|
||||||
href: string
|
href: string
|
||||||
height: number
|
height: number
|
||||||
|
width: number | null
|
||||||
|
fps: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActivityHtmlUrlObject = {
|
export type ActivityHtmlUrlObject = {
|
||||||
|
|
|
@ -44,6 +44,8 @@ export interface VideoObject {
|
||||||
|
|
||||||
support: string
|
support: string
|
||||||
|
|
||||||
|
aspectRatio: number
|
||||||
|
|
||||||
icon: ActivityIconObject[]
|
icon: ActivityIconObject[]
|
||||||
|
|
||||||
url: ActivityUrlObject[]
|
url: ActivityUrlObject[]
|
||||||
|
|
|
@ -7,6 +7,9 @@ export interface VideoFile {
|
||||||
resolution: VideoConstant<number>
|
resolution: VideoConstant<number>
|
||||||
size: number // Bytes
|
size: number // Bytes
|
||||||
|
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
|
||||||
torrentUrl: string
|
torrentUrl: string
|
||||||
torrentDownloadUrl: string
|
torrentDownloadUrl: string
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
|
||||||
isLocal: boolean
|
isLocal: boolean
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
aspectRatio: number | null
|
||||||
|
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
|
|
||||||
thumbnailPath: string
|
thumbnailPath: string
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 KiB |
|
@ -115,6 +115,8 @@ describe('Test live', function () {
|
||||||
|
|
||||||
expect(video.isLive).to.be.true
|
expect(video.isLive).to.be.true
|
||||||
|
|
||||||
|
expect(video.aspectRatio).to.not.exist
|
||||||
|
|
||||||
expect(video.nsfw).to.be.false
|
expect(video.nsfw).to.be.false
|
||||||
expect(video.waitTranscoding).to.be.false
|
expect(video.waitTranscoding).to.be.false
|
||||||
expect(video.name).to.equal('my super live')
|
expect(video.name).to.equal('my super live')
|
||||||
|
@ -552,6 +554,7 @@ describe('Test live', function () {
|
||||||
|
|
||||||
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||||
expect(video.duration).to.be.greaterThan(1)
|
expect(video.duration).to.be.greaterThan(1)
|
||||||
|
expect(video.aspectRatio).to.equal(1.7778)
|
||||||
expect(video.files).to.have.lengthOf(0)
|
expect(video.files).to.have.lengthOf(0)
|
||||||
|
|
||||||
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
import { decode as magnetUriDecode } from 'magnet-uri'
|
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import {
|
import {
|
||||||
|
@ -25,12 +24,13 @@ import {
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
import { checkSegmentHash } from '@tests/shared/streaming-playlists.js'
|
import { checkSegmentHash } from '@tests/shared/streaming-playlists.js'
|
||||||
import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js'
|
import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js'
|
||||||
|
import { magnetUriDecode } from '@tests/shared/webtorrent.js'
|
||||||
|
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
let video1Server2: VideoDetails
|
let video1Server2: VideoDetails
|
||||||
|
|
||||||
async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
|
async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
|
||||||
const parsed = magnetUriDecode(file.magnetUri)
|
const parsed = await magnetUriDecode(file.magnetUri)
|
||||||
|
|
||||||
for (const ws of baseWebseeds) {
|
for (const ws of baseWebseeds) {
|
||||||
const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)
|
const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)
|
||||||
|
|
|
@ -479,6 +479,8 @@ describe('Test follows', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
size: 218910
|
size: 218910
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -69,6 +69,8 @@ describe('Test handle downs', function () {
|
||||||
fixture: 'video_short1.webm',
|
fixture: 'video_short1.webm',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
size: 572456
|
size: 572456
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
|
||||||
|
|
||||||
import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri'
|
|
||||||
import WebTorrent from 'webtorrent'
|
import WebTorrent from 'webtorrent'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
|
@ -9,6 +8,7 @@ import {
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
setAccessTokensToServers
|
setAccessTokensToServers
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { magnetUriDecode, magnetUriEncode } from '@tests/shared/webtorrent.js'
|
||||||
|
|
||||||
describe('Test tracker', function () {
|
describe('Test tracker', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -25,10 +25,10 @@ describe('Test tracker', function () {
|
||||||
const video = await server.videos.get({ id: uuid })
|
const video = await server.videos.get({ id: uuid })
|
||||||
goodMagnet = video.files[0].magnetUri
|
goodMagnet = video.files[0].magnetUri
|
||||||
|
|
||||||
const parsed = magnetUriDecode(goodMagnet)
|
const parsed = await magnetUriDecode(goodMagnet)
|
||||||
parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9'
|
parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9'
|
||||||
|
|
||||||
badMagnet = magnetUriEncode(parsed)
|
badMagnet = await magnetUriEncode(parsed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -401,10 +401,14 @@ function runTest (withObjectStorage: boolean) {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 61000
|
size: 61000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 240,
|
resolution: 240,
|
||||||
|
height: 240,
|
||||||
|
width: 426,
|
||||||
size: 23000
|
size: 23000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -118,6 +118,8 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 572456
|
size: 572456
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -205,18 +207,26 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 240,
|
resolution: 240,
|
||||||
|
height: 240,
|
||||||
|
width: 426,
|
||||||
size: 270000
|
size: 270000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 360,
|
resolution: 360,
|
||||||
|
height: 360,
|
||||||
|
width: 640,
|
||||||
size: 359000
|
size: 359000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 480,
|
resolution: 480,
|
||||||
|
height: 480,
|
||||||
|
width: 854,
|
||||||
size: 465000
|
size: 465000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 750000
|
size: 750000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -312,6 +322,8 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 292677
|
size: 292677
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -344,6 +356,8 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 218910
|
size: 218910
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -654,6 +668,8 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 292677
|
size: 292677
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1061,18 +1077,26 @@ describe('Test multiple servers', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 61000
|
size: 61000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 480,
|
resolution: 480,
|
||||||
|
height: 480,
|
||||||
|
width: 854,
|
||||||
size: 40000
|
size: 40000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 360,
|
resolution: 360,
|
||||||
|
height: 360,
|
||||||
|
width: 640,
|
||||||
size: 32000
|
size: 32000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolution: 240,
|
resolution: 240,
|
||||||
|
height: 240,
|
||||||
|
width: 426,
|
||||||
size: 23000
|
size: 23000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -50,6 +50,8 @@ describe('Test a single server', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 218910
|
size: 218910
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -81,6 +83,8 @@ describe('Test a single server', function () {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
resolution: 720,
|
resolution: 720,
|
||||||
|
height: 720,
|
||||||
|
width: 1280,
|
||||||
size: 292677
|
size: 292677
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -105,7 +105,8 @@ describe('Test videos files', function () {
|
||||||
const video = await servers[0].videos.get({ id: webVideoId })
|
const video = await servers[0].videos.get({ id: webVideoId })
|
||||||
const files = video.files
|
const files = video.files
|
||||||
|
|
||||||
await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id })
|
const toDelete = files[0]
|
||||||
|
await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: toDelete.id })
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ describe('Test videos files', function () {
|
||||||
const video = await server.videos.get({ id: webVideoId })
|
const video = await server.videos.get({ id: webVideoId })
|
||||||
|
|
||||||
expect(video.files).to.have.lengthOf(files.length - 1)
|
expect(video.files).to.have.lengthOf(files.length - 1)
|
||||||
expect(video.files.find(f => f.id === files[0].id)).to.not.exist
|
expect(video.files.find(f => f.resolution.id === toDelete.resolution.id)).to.not.exist
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -151,7 +152,7 @@ describe('Test videos files', function () {
|
||||||
const video = await server.videos.get({ id: hlsId })
|
const video = await server.videos.get({ id: hlsId })
|
||||||
|
|
||||||
expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
|
expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
|
||||||
expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist
|
expect(video.streamingPlaylists[0].files.find(f => f.resolution.id === toDelete.resolution.id)).to.not.exist
|
||||||
|
|
||||||
const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
|
const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { decode } from 'magnet-uri'
|
|
||||||
import { getAllFiles, wait } from '@peertube/peertube-core-utils'
|
import { getAllFiles, wait } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
|
import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
|
@ -18,7 +17,7 @@ import {
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
import { expectStartWith } from '@tests/shared/checks.js'
|
import { expectStartWith } from '@tests/shared/checks.js'
|
||||||
import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
|
import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
|
||||||
import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
|
import { magnetUriDecode, parseTorrentVideo } from '@tests/shared/webtorrent.js'
|
||||||
|
|
||||||
describe('Test video static file privacy', function () {
|
describe('Test video static file privacy', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -48,7 +47,7 @@ describe('Test video static file privacy', function () {
|
||||||
const torrent = await parseTorrentVideo(server, file)
|
const torrent = await parseTorrentVideo(server, file)
|
||||||
expect(torrent.urlList).to.have.lengthOf(0)
|
expect(torrent.urlList).to.have.lengthOf(0)
|
||||||
|
|
||||||
const magnet = decode(file.magnetUri)
|
const magnet = await magnetUriDecode(file.magnetUri)
|
||||||
expect(magnet.urlList).to.have.lengthOf(0)
|
expect(magnet.urlList).to.have.lengthOf(0)
|
||||||
|
|
||||||
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
@ -74,7 +73,7 @@ describe('Test video static file privacy', function () {
|
||||||
const torrent = await parseTorrentVideo(server, file)
|
const torrent = await parseTorrentVideo(server, file)
|
||||||
expect(torrent.urlList[0]).to.not.include('private')
|
expect(torrent.urlList[0]).to.not.include('private')
|
||||||
|
|
||||||
const magnet = decode(file.magnetUri)
|
const magnet = await magnetUriDecode(file.magnetUri)
|
||||||
expect(magnet.urlList[0]).to.not.include('private')
|
expect(magnet.urlList[0]).to.not.include('private')
|
||||||
|
|
||||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import snakeCase from 'lodash-es/snakeCase.js'
|
import snakeCase from 'lodash-es/snakeCase.js'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { getAverageTheoreticalBitrate, getMaxTheoreticalBitrate, parseChapters, timeToInt } from '@peertube/peertube-core-utils'
|
import {
|
||||||
|
buildAspectRatio,
|
||||||
|
getAverageTheoreticalBitrate,
|
||||||
|
getMaxTheoreticalBitrate,
|
||||||
|
parseChapters,
|
||||||
|
timeToInt
|
||||||
|
} from '@peertube/peertube-core-utils'
|
||||||
import { VideoResolution } from '@peertube/peertube-models'
|
import { VideoResolution } from '@peertube/peertube-models'
|
||||||
import { objectConverter, parseBytes, parseDurationToMs, parseSemVersion } from '@peertube/peertube-server/core/helpers/core-utils.js'
|
import { objectConverter, parseBytes, parseDurationToMs, parseSemVersion } from '@peertube/peertube-server/core/helpers/core-utils.js'
|
||||||
|
|
||||||
|
@ -169,6 +175,18 @@ describe('Bitrate', function () {
|
||||||
expect(getAverageTheoreticalBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000)
|
expect(getAverageTheoreticalBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Ratio', function () {
|
||||||
|
|
||||||
|
it('Should have the correct aspect ratio in landscape', function () {
|
||||||
|
expect(buildAspectRatio({ width: 1920, height: 1080 })).to.equal(1.7778)
|
||||||
|
expect(buildAspectRatio({ width: 1000, height: 1000 })).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have the correct aspect ratio in portrait', function () {
|
||||||
|
expect(buildAspectRatio({ width: 1080, height: 1920 })).to.equal(0.5625)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Parse semantic version string', function () {
|
describe('Parse semantic version string', function () {
|
||||||
|
|
|
@ -103,9 +103,15 @@ async function testImage (url: string, imageName: string, imageHTTPPath: string,
|
||||||
? PNG.sync.read(data)
|
? PNG.sync.read(data)
|
||||||
: JPEG.decode(data)
|
: JPEG.decode(data)
|
||||||
|
|
||||||
const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
|
const errorMsg = `${imageHTTPPath} image is not the same as ${imageName}${extension}`
|
||||||
|
|
||||||
expect(result).to.equal(0, `${imageHTTPPath} image is not the same as ${imageName}${extension}`)
|
try {
|
||||||
|
const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
|
||||||
|
|
||||||
|
expect(result).to.equal(0, errorMsg)
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`${errorMsg}: ${err.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
|
async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
|
||||||
|
|
|
@ -66,6 +66,8 @@ async function testLiveVideoResolutions (options: {
|
||||||
expect(data.find(v => v.uuid === liveVideoId)).to.exist
|
expect(data.find(v => v.uuid === liveVideoId)).to.exist
|
||||||
|
|
||||||
const video = await server.videos.get({ id: liveVideoId })
|
const video = await server.videos.get({ id: liveVideoId })
|
||||||
|
|
||||||
|
expect(video.aspectRatio).to.equal(1.7778)
|
||||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||||
|
|
||||||
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
||||||
|
|
|
@ -145,6 +145,9 @@ async function completeCheckHlsPlaylist (options: {
|
||||||
expect(file.resolution.label).to.equal(resolution + 'p')
|
expect(file.resolution.label).to.equal(resolution + 'p')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(Math.min(file.height, file.width)).to.equal(resolution)
|
||||||
|
expect(Math.max(file.height, file.width)).to.be.greaterThan(resolution)
|
||||||
|
|
||||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||||
await checkWebTorrentWorks(file.magnetUri)
|
await checkWebTorrentWorks(file.magnetUri)
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ export async function completeWebVideoFilesCheck (options: {
|
||||||
fixture: string
|
fixture: string
|
||||||
files: {
|
files: {
|
||||||
resolution: number
|
resolution: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
size?: number
|
size?: number
|
||||||
}[]
|
}[]
|
||||||
objectStorageBaseUrl?: string
|
objectStorageBaseUrl?: string
|
||||||
|
@ -84,7 +86,9 @@ export async function completeWebVideoFilesCheck (options: {
|
||||||
makeRawRequest({
|
makeRawRequest({
|
||||||
url: file.fileDownloadUrl,
|
url: file.fileDownloadUrl,
|
||||||
token,
|
token,
|
||||||
expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200
|
expectedStatus: objectStorageBaseUrl
|
||||||
|
? HttpStatusCode.FOUND_302
|
||||||
|
: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -97,6 +101,12 @@ export async function completeWebVideoFilesCheck (options: {
|
||||||
expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
|
expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attributeFile.width !== undefined) expect(file.width).to.equal(attributeFile.width)
|
||||||
|
if (attributeFile.height !== undefined) expect(file.height).to.equal(attributeFile.height)
|
||||||
|
|
||||||
|
expect(Math.min(file.height, file.width)).to.equal(file.resolution.id)
|
||||||
|
expect(Math.max(file.height, file.width)).to.be.greaterThan(file.resolution.id)
|
||||||
|
|
||||||
if (attributeFile.size) {
|
if (attributeFile.size) {
|
||||||
const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
|
const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
|
||||||
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
|
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
|
||||||
|
@ -156,6 +166,8 @@ export async function completeVideoCheck (options: {
|
||||||
files?: {
|
files?: {
|
||||||
resolution: number
|
resolution: number
|
||||||
size: number
|
size: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
hls?: {
|
hls?: {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { basename, join } from 'path'
|
||||||
import type { Instance, Torrent } from 'webtorrent'
|
import type { Instance, Torrent } from 'webtorrent'
|
||||||
import { VideoFile } from '@peertube/peertube-models'
|
import { VideoFile } from '@peertube/peertube-models'
|
||||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
|
import type { Instance as MagnetUriInstance } from 'magnet-uri'
|
||||||
|
|
||||||
let webtorrent: Instance
|
let webtorrent: Instance
|
||||||
|
|
||||||
|
@ -28,6 +29,14 @@ export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile
|
||||||
return (await import('parse-torrent')).default(data)
|
return (await import('parse-torrent')).default(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function magnetUriDecode (data: string) {
|
||||||
|
return (await import('magnet-uri')).decode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function magnetUriEncode (data: MagnetUriInstance) {
|
||||||
|
return (await import('magnet-uri')).encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Private
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
replaceVideoSourceResumableValidator,
|
replaceVideoSourceResumableValidator,
|
||||||
videoSourceGetLatestValidator
|
videoSourceGetLatestValidator
|
||||||
} from '../../../middlewares/index.js'
|
} from '../../../middlewares/index.js'
|
||||||
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'video')
|
const lTags = loggerTagsFactory('api', 'video')
|
||||||
|
|
||||||
|
@ -96,6 +97,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||||
video.state = buildNextVideoState()
|
video.state = buildNextVideoState()
|
||||||
video.duration = videoPhysicalFile.duration
|
video.duration = videoPhysicalFile.duration
|
||||||
video.inputFileUpdatedAt = inputFileUpdatedAt
|
video.inputFileUpdatedAt = inputFileUpdatedAt
|
||||||
|
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
|
||||||
await video.save({ transaction })
|
await video.save({ transaction })
|
||||||
|
|
||||||
await autoBlacklistVideoIfNeeded({
|
await autoBlacklistVideoIfNeeded({
|
||||||
|
|
|
@ -94,6 +94,10 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
|
||||||
'@type': 'sc:Number',
|
'@type': 'sc:Number',
|
||||||
'@id': 'pt:tileDuration'
|
'@id': 'pt:tileDuration'
|
||||||
},
|
},
|
||||||
|
aspectRatio: {
|
||||||
|
'@type': 'sc:Float',
|
||||||
|
'@id': 'pt:aspectRatio'
|
||||||
|
},
|
||||||
|
|
||||||
originallyPublishedAt: 'sc:datePublished',
|
originallyPublishedAt: 'sc:datePublished',
|
||||||
|
|
||||||
|
|
|
@ -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)[]
|
urls: (ActivityTagObject | ActivityUrlObject)[]
|
||||||
) {
|
) {
|
||||||
const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
|
const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
|
||||||
|
|
||||||
if (fileUrls.length === 0) return []
|
if (fileUrls.length === 0) return []
|
||||||
|
|
||||||
const attributes: FilteredModelAttributes<VideoFileModel>[] = []
|
const attributes: FilteredModelAttributes<VideoFileModel>[] = []
|
||||||
|
@ -96,6 +95,9 @@ function getFileAttributesFromUrl (
|
||||||
fps: fileUrl.fps || -1,
|
fps: fileUrl.fps || -1,
|
||||||
metadataUrl: metadata?.href,
|
metadataUrl: metadata?.href,
|
||||||
|
|
||||||
|
width: fileUrl.width,
|
||||||
|
height: fileUrl.height,
|
||||||
|
|
||||||
// Use the name of the remote file because we don't proxify video file requests
|
// Use the name of the remote file because we don't proxify video file requests
|
||||||
filename: basename(fileUrl.href),
|
filename: basename(fileUrl.href),
|
||||||
fileUrl: fileUrl.href,
|
fileUrl: fileUrl.href,
|
||||||
|
@ -223,6 +225,7 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi
|
||||||
waitTranscoding: videoObject.waitTranscoding,
|
waitTranscoding: videoObject.waitTranscoding,
|
||||||
isLive: videoObject.isLiveBroadcast,
|
isLive: videoObject.isLiveBroadcast,
|
||||||
state: videoObject.state,
|
state: videoObject.state,
|
||||||
|
aspectRatio: videoObject.aspectRatio,
|
||||||
channelId: videoChannel.id,
|
channelId: videoChannel.id,
|
||||||
duration: getDurationFromActivityStream(videoObject.duration),
|
duration: getDurationFromActivityStream(videoObject.duration),
|
||||||
createdAt: new Date(videoObject.published),
|
createdAt: new Date(videoObject.published),
|
||||||
|
|
|
@ -143,6 +143,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
|
||||||
this.video.channelId = videoData.channelId
|
this.video.channelId = videoData.channelId
|
||||||
this.video.views = videoData.views
|
this.video.views = videoData.views
|
||||||
this.video.isLive = videoData.isLive
|
this.video.isLive = videoData.isLive
|
||||||
|
this.video.aspectRatio = videoData.aspectRatio
|
||||||
|
|
||||||
// Ensures we update the updatedAt attribute, even if main attributes did not change
|
// Ensures we update the updatedAt attribute, even if main attributes did not change
|
||||||
this.video.changed('updatedAt', true)
|
this.video.changed('updatedAt', true)
|
||||||
|
|
|
@ -51,10 +51,10 @@ async function processGenerateStoryboard (job: Job): Promise<void> {
|
||||||
|
|
||||||
if (videoStreamInfo.isPortraitMode) {
|
if (videoStreamInfo.isPortraitMode) {
|
||||||
spriteHeight = STORYBOARD.SPRITE_MAX_SIZE
|
spriteHeight = STORYBOARD.SPRITE_MAX_SIZE
|
||||||
spriteWidth = Math.round(STORYBOARD.SPRITE_MAX_SIZE / videoStreamInfo.ratio)
|
spriteWidth = Math.round(spriteHeight * videoStreamInfo.ratio)
|
||||||
} else {
|
} else {
|
||||||
spriteHeight = Math.round(STORYBOARD.SPRITE_MAX_SIZE / videoStreamInfo.ratio)
|
|
||||||
spriteWidth = STORYBOARD.SPRITE_MAX_SIZE
|
spriteWidth = STORYBOARD.SPRITE_MAX_SIZE
|
||||||
|
spriteHeight = Math.round(spriteWidth / videoStreamInfo.ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail'))
|
const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail'))
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import { Job } from 'bullmq'
|
import { Job } from 'bullmq'
|
||||||
import { copy } from 'fs-extra/esm'
|
import { copy } from 'fs-extra/esm'
|
||||||
import { stat } from 'fs/promises'
|
import { VideoFileImportPayload } from '@peertube/peertube-models'
|
||||||
import { VideoFileImportPayload, FileStorage } from '@peertube/peertube-models'
|
|
||||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||||
import { generateWebVideoFilename } from '@server/lib/paths.js'
|
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
import { getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg'
|
||||||
import { getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
|
||||||
import { logger } from '../../../helpers/logger.js'
|
import { logger } from '../../../helpers/logger.js'
|
||||||
import { JobQueue } from '../job-queue.js'
|
import { JobQueue } from '../job-queue.js'
|
||||||
import { buildMoveJob } from '@server/lib/video-jobs.js'
|
import { buildMoveJob } from '@server/lib/video-jobs.js'
|
||||||
|
import { buildNewFile } from '@server/lib/video-file.js'
|
||||||
|
|
||||||
async function processVideoFileImport (job: Job) {
|
async function processVideoFileImport (job: Job) {
|
||||||
const payload = job.data as VideoFileImportPayload
|
const payload = job.data as VideoFileImportPayload
|
||||||
|
@ -48,11 +45,6 @@ export {
|
||||||
|
|
||||||
async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
||||||
const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath)
|
const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath)
|
||||||
const { size } = await stat(inputFilePath)
|
|
||||||
const fps = await getVideoStreamFPS(inputFilePath)
|
|
||||||
|
|
||||||
const fileExt = getLowercaseExtension(inputFilePath)
|
|
||||||
|
|
||||||
const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === resolution)
|
const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === resolution)
|
||||||
|
|
||||||
if (currentVideoFile) {
|
if (currentVideoFile) {
|
||||||
|
@ -64,15 +56,8 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
||||||
await currentVideoFile.destroy()
|
await currentVideoFile.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVideoFile = new VideoFileModel({
|
const newVideoFile = await buildNewFile({ mode: 'web-video', path: inputFilePath })
|
||||||
resolution,
|
newVideoFile.videoId = video.id
|
||||||
extname: fileExt,
|
|
||||||
filename: generateWebVideoFilename(resolution, fileExt),
|
|
||||||
storage: FileStorage.FILE_SYSTEM,
|
|
||||||
size,
|
|
||||||
fps,
|
|
||||||
videoId: video.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
|
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
|
||||||
await copy(inputFilePath, outputPath)
|
await copy(inputFilePath, outputPath)
|
||||||
|
|
|
@ -10,15 +10,12 @@ import {
|
||||||
VideoImportTorrentPayload,
|
VideoImportTorrentPayload,
|
||||||
VideoImportTorrentPayloadType,
|
VideoImportTorrentPayloadType,
|
||||||
VideoImportYoutubeDLPayload,
|
VideoImportYoutubeDLPayload,
|
||||||
VideoImportYoutubeDLPayloadType,
|
VideoImportYoutubeDLPayloadType, VideoState
|
||||||
VideoResolution,
|
|
||||||
VideoState
|
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||||
import { YoutubeDLWrapper } from '@server/helpers/youtube-dl/index.js'
|
import { YoutubeDLWrapper } from '@server/helpers/youtube-dl/index.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { isPostImportVideoAccepted } from '@server/lib/moderation.js'
|
import { isPostImportVideoAccepted } from '@server/lib/moderation.js'
|
||||||
import { generateWebVideoFilename } from '@server/lib/paths.js'
|
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||||
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js'
|
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js'
|
||||||
|
@ -28,14 +25,9 @@ import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js'
|
import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js'
|
||||||
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
|
||||||
import {
|
import {
|
||||||
ffprobePromise,
|
ffprobePromise,
|
||||||
getChaptersFromContainer,
|
getChaptersFromContainer, getVideoStreamDuration
|
||||||
getVideoStreamDimensionsInfo,
|
|
||||||
getVideoStreamDuration,
|
|
||||||
getVideoStreamFPS,
|
|
||||||
isAudioFile
|
|
||||||
} from '@peertube/peertube-ffmpeg'
|
} from '@peertube/peertube-ffmpeg'
|
||||||
import { logger } from '../../../helpers/logger.js'
|
import { logger } from '../../../helpers/logger.js'
|
||||||
import { getSecureTorrentName } from '../../../helpers/utils.js'
|
import { getSecureTorrentName } from '../../../helpers/utils.js'
|
||||||
|
@ -51,6 +43,8 @@ import { generateLocalVideoMiniature } from '../../thumbnail.js'
|
||||||
import { JobQueue } from '../job-queue.js'
|
import { JobQueue } from '../job-queue.js'
|
||||||
import { replaceChaptersIfNotExist } from '@server/lib/video-chapters.js'
|
import { replaceChaptersIfNotExist } from '@server/lib/video-chapters.js'
|
||||||
import { FfprobeData } from 'fluent-ffmpeg'
|
import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
|
import { buildNewFile } from '@server/lib/video-file.js'
|
||||||
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> {
|
async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> {
|
||||||
const payload = job.data as VideoImportPayload
|
const payload = job.data as VideoImportPayload
|
||||||
|
@ -129,46 +123,31 @@ type ProcessFileOptions = {
|
||||||
videoImportId: number
|
videoImportId: number
|
||||||
}
|
}
|
||||||
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
|
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
|
||||||
let tempVideoPath: string
|
let tmpVideoPath: string
|
||||||
let videoFile: VideoFileModel
|
let videoFile: VideoFileModel
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download video from youtubeDL
|
// Download video from youtubeDL
|
||||||
tempVideoPath = await downloader()
|
tmpVideoPath = await downloader()
|
||||||
|
|
||||||
// Get information about this video
|
// Get information about this video
|
||||||
const stats = await stat(tempVideoPath)
|
const stats = await stat(tmpVideoPath)
|
||||||
const isAble = await isUserQuotaValid({ userId: videoImport.User.id, uploadSize: stats.size })
|
const isAble = await isUserQuotaValid({ userId: videoImport.User.id, uploadSize: stats.size })
|
||||||
if (isAble === false) {
|
if (isAble === false) {
|
||||||
throw new Error('The user video quota is exceeded with this video to import.')
|
throw new Error('The user video quota is exceeded with this video to import.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffprobe = await ffprobePromise(tempVideoPath)
|
const ffprobe = await ffprobePromise(tmpVideoPath)
|
||||||
|
const duration = await getVideoStreamDuration(tmpVideoPath, ffprobe)
|
||||||
const { resolution } = await isAudioFile(tempVideoPath, ffprobe)
|
|
||||||
? { resolution: VideoResolution.H_NOVIDEO }
|
|
||||||
: await getVideoStreamDimensionsInfo(tempVideoPath, ffprobe)
|
|
||||||
|
|
||||||
const fps = await getVideoStreamFPS(tempVideoPath, ffprobe)
|
|
||||||
const duration = await getVideoStreamDuration(tempVideoPath, ffprobe)
|
|
||||||
|
|
||||||
const containerChapters = await getChaptersFromContainer({
|
const containerChapters = await getChaptersFromContainer({
|
||||||
path: tempVideoPath,
|
path: tmpVideoPath,
|
||||||
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
|
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
|
||||||
ffprobe
|
ffprobe
|
||||||
})
|
})
|
||||||
|
|
||||||
// Prepare video file object for creation in database
|
videoFile = await buildNewFile({ mode: 'web-video', ffprobe, path: tmpVideoPath })
|
||||||
const fileExt = getLowercaseExtension(tempVideoPath)
|
videoFile.videoId = videoImport.videoId
|
||||||
const videoFileData = {
|
|
||||||
extname: fileExt,
|
|
||||||
resolution,
|
|
||||||
size: stats.size,
|
|
||||||
filename: generateWebVideoFilename(resolution, fileExt),
|
|
||||||
fps,
|
|
||||||
videoId: videoImport.videoId
|
|
||||||
}
|
|
||||||
videoFile = new VideoFileModel(videoFileData)
|
|
||||||
|
|
||||||
const hookName = options.type === 'youtube-dl'
|
const hookName = options.type === 'youtube-dl'
|
||||||
? 'filter:api.video.post-import-url.accept.result'
|
? 'filter:api.video.post-import-url.accept.result'
|
||||||
|
@ -178,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||||
const acceptParameters = {
|
const acceptParameters = {
|
||||||
videoImport,
|
videoImport,
|
||||||
video: videoImport.Video,
|
video: videoImport.Video,
|
||||||
videoFilePath: tempVideoPath,
|
videoFilePath: tmpVideoPath,
|
||||||
videoFile,
|
videoFile,
|
||||||
user: videoImport.User
|
user: videoImport.User
|
||||||
}
|
}
|
||||||
|
@ -201,9 +180,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||||
|
|
||||||
// Move file
|
// Move file
|
||||||
const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile)
|
const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile)
|
||||||
await move(tempVideoPath, videoDestFile)
|
await move(tmpVideoPath, videoDestFile)
|
||||||
|
|
||||||
tempVideoPath = null // This path is not used anymore
|
tmpVideoPath = null // This path is not used anymore
|
||||||
|
|
||||||
const thumbnails = await generateMiniature({ videoImportWithFiles, videoFile, ffprobe })
|
const thumbnails = await generateMiniature({ videoImportWithFiles, videoFile, ffprobe })
|
||||||
|
|
||||||
|
@ -221,6 +200,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||||
// Update video DB object
|
// Update video DB object
|
||||||
video.duration = duration
|
video.duration = duration
|
||||||
video.state = buildNextVideoState(video.state)
|
video.state = buildNextVideoState(video.state)
|
||||||
|
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
|
||||||
await video.save({ transaction: t })
|
await video.save({ transaction: t })
|
||||||
|
|
||||||
for (const thumbnail of thumbnails) {
|
for (const thumbnail of thumbnails) {
|
||||||
|
@ -248,7 +228,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||||
videoFileLockReleaser()
|
videoFileLockReleaser()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await onImportError(err, tempVideoPath, videoImport)
|
await onImportError(err, tmpVideoPath, videoImport)
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@ async function saveReplayToExternalVideo (options: {
|
||||||
waitTranscoding: true,
|
waitTranscoding: true,
|
||||||
nsfw: liveVideo.nsfw,
|
nsfw: liveVideo.nsfw,
|
||||||
description: liveVideo.description,
|
description: liveVideo.description,
|
||||||
|
aspectRatio: liveVideo.aspectRatio,
|
||||||
support: liveVideo.support,
|
support: liveVideo.support,
|
||||||
privacy: replaySettings.privacy,
|
privacy: replaySettings.privacy,
|
||||||
channelId: liveVideo.channelId
|
channelId: liveVideo.channelId
|
||||||
|
|
|
@ -328,7 +328,7 @@ class LiveManager {
|
||||||
allResolutions: number[]
|
allResolutions: number[]
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
}) {
|
}) {
|
||||||
const { sessionId, videoLive, user } = options
|
const { sessionId, videoLive, user, ratio } = options
|
||||||
const videoUUID = videoLive.Video.uuid
|
const videoUUID = videoLive.Video.uuid
|
||||||
const localLTags = lTags(sessionId, videoUUID)
|
const localLTags = lTags(sessionId, videoUUID)
|
||||||
|
|
||||||
|
@ -345,7 +345,7 @@ class LiveManager {
|
||||||
...pick(options, [ 'inputLocalUrl', 'inputPublicUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio' ])
|
...pick(options, [ 'inputLocalUrl', 'inputPublicUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio' ])
|
||||||
})
|
})
|
||||||
|
|
||||||
muxingSession.on('live-ready', () => this.publishAndFederateLive(videoLive, localLTags))
|
muxingSession.on('live-ready', () => this.publishAndFederateLive({ live: videoLive, ratio, localLTags }))
|
||||||
|
|
||||||
muxingSession.on('bad-socket-health', ({ videoUUID }) => {
|
muxingSession.on('bad-socket-health', ({ videoUUID }) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@ -405,7 +405,13 @@ class LiveManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async publishAndFederateLive (live: MVideoLiveVideo, localLTags: { tags: (string | number)[] }) {
|
private async publishAndFederateLive (options: {
|
||||||
|
live: MVideoLiveVideo
|
||||||
|
ratio: number
|
||||||
|
localLTags: { tags: (string | number)[] }
|
||||||
|
}) {
|
||||||
|
const { live, ratio, localLTags } = options
|
||||||
|
|
||||||
const videoId = live.videoId
|
const videoId = live.videoId
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -415,6 +421,7 @@ class LiveManager {
|
||||||
|
|
||||||
video.state = VideoState.PUBLISHED
|
video.state = VideoState.PUBLISHED
|
||||||
video.publishedAt = new Date()
|
video.publishedAt = new Date()
|
||||||
|
video.aspectRatio = ratio
|
||||||
await video.save()
|
await video.save()
|
||||||
|
|
||||||
live.Video = video
|
live.Video = video
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video
|
||||||
import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
|
import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
|
||||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||||
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
|
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
|
||||||
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
|
type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
|
||||||
duration: number
|
duration: number
|
||||||
|
@ -116,6 +117,8 @@ export class LocalVideoCreator {
|
||||||
|
|
||||||
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
|
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
|
||||||
await move(this.videoFilePath, destination)
|
await move(this.videoFilePath, destination)
|
||||||
|
|
||||||
|
this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnails = await this.createThumbnails()
|
const thumbnails = await this.createThumbnails()
|
||||||
|
|
|
@ -1,50 +1,24 @@
|
||||||
import { move } from 'fs-extra/esm'
|
|
||||||
import { dirname, join } from 'path'
|
|
||||||
import { logger, LoggerTagsFn } from '@server/helpers/logger.js'
|
import { logger, LoggerTagsFn } from '@server/helpers/logger.js'
|
||||||
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
|
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
|
||||||
import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding.js'
|
import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding.js'
|
||||||
import { buildNewFile } from '@server/lib/video-file.js'
|
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
|
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
|
||||||
import { lTags } from '@server/lib/object-storage/shared/logger.js'
|
|
||||||
|
|
||||||
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
||||||
video: MVideoFullLight
|
video: MVideoFullLight
|
||||||
videoFilePath: string
|
videoFilePath: string
|
||||||
privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload | RunnerJobVODAudioMergeTranscodingPrivatePayload
|
privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload | RunnerJobVODAudioMergeTranscodingPrivatePayload
|
||||||
|
wasAudioFile: boolean
|
||||||
}) {
|
}) {
|
||||||
const { video, videoFilePath, privatePayload } = options
|
const { video, videoFilePath, privatePayload, wasAudioFile } = options
|
||||||
|
|
||||||
const videoFile = await buildNewFile({ path: videoFilePath, mode: 'web-video' })
|
const deleteWebInputVideoFile = privatePayload.deleteInputFileId
|
||||||
videoFile.videoId = video.id
|
? video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename)
|
await onWebVideoFileTranscoding({ video, videoOutputPath: videoFilePath, deleteWebInputVideoFile, wasAudioFile })
|
||||||
await move(videoFilePath, newVideoFilePath)
|
|
||||||
|
|
||||||
await onWebVideoFileTranscoding({
|
|
||||||
video,
|
|
||||||
videoFile,
|
|
||||||
videoOutputPath: newVideoFilePath
|
|
||||||
})
|
|
||||||
|
|
||||||
if (privatePayload.deleteInputFileId) {
|
|
||||||
const inputFile = video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
|
|
||||||
|
|
||||||
if (inputFile) {
|
|
||||||
await video.removeWebVideoFile(inputFile)
|
|
||||||
await inputFile.destroy()
|
|
||||||
|
|
||||||
video.VideoFiles = video.VideoFiles.filter(f => f.id !== inputFile.id)
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
'Cannot delete input file %d of video %s: does not exist anymore',
|
|
||||||
privatePayload.deleteInputFileId, video.uuid,
|
|
||||||
{ ...lTags(video.uuid), privatePayload }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { MVideo } from '@server/types/models/index.js'
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
import { pick } from '@peertube/peertube-core-utils'
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
|
||||||
import {
|
import {
|
||||||
RunnerJobUpdatePayload,
|
RunnerJobUpdatePayload,
|
||||||
RunnerJobVODAudioMergeTranscodingPayload,
|
RunnerJobVODAudioMergeTranscodingPayload,
|
||||||
|
@ -77,12 +76,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
|
||||||
|
|
||||||
const videoFilePath = resultPayload.videoFile as string
|
const videoFilePath = resultPayload.videoFile as string
|
||||||
|
|
||||||
// ffmpeg generated a new video file, so update the video duration
|
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: true })
|
||||||
// See https://trac.ffmpeg.org/ticket/5456
|
|
||||||
video.duration = await getVideoStreamDuration(videoFilePath)
|
|
||||||
await video.save()
|
|
||||||
|
|
||||||
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Runner VOD audio merge transcoding job %s for %s ended.',
|
'Runner VOD audio merge transcoding job %s for %s ended.',
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { move } from 'fs-extra/esm'
|
|
||||||
import { dirname, join } from 'path'
|
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { renameVideoFileInPlaylist } from '@server/lib/hls.js'
|
|
||||||
import { getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
|
|
||||||
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
|
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
|
||||||
import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js'
|
import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js'
|
||||||
import { buildNewFile, removeAllWebVideoFiles } from '@server/lib/video-file.js'
|
import { removeAllWebVideoFiles } from '@server/lib/video-file.js'
|
||||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||||
import { MVideo } from '@server/types/models/index.js'
|
import { MVideo } from '@server/types/models/index.js'
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
|
@ -84,21 +80,10 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle
|
||||||
const videoFilePath = resultPayload.videoFile as string
|
const videoFilePath = resultPayload.videoFile as string
|
||||||
const resolutionPlaylistFilePath = resultPayload.resolutionPlaylistFile as string
|
const resolutionPlaylistFilePath = resultPayload.resolutionPlaylistFile as string
|
||||||
|
|
||||||
const videoFile = await buildNewFile({ path: videoFilePath, mode: 'hls' })
|
|
||||||
const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename)
|
|
||||||
await move(videoFilePath, newVideoFilePath)
|
|
||||||
|
|
||||||
const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
|
|
||||||
const newResolutionPlaylistFilePath = join(dirname(resolutionPlaylistFilePath), resolutionPlaylistFilename)
|
|
||||||
await move(resolutionPlaylistFilePath, newResolutionPlaylistFilePath)
|
|
||||||
|
|
||||||
await renameVideoFileInPlaylist(newResolutionPlaylistFilePath, videoFile.filename)
|
|
||||||
|
|
||||||
await onHLSVideoFileTranscoding({
|
await onHLSVideoFileTranscoding({
|
||||||
video,
|
video,
|
||||||
videoFile,
|
m3u8OutputPath: resolutionPlaylistFilePath,
|
||||||
m3u8OutputPath: newResolutionPlaylistFilePath,
|
videoOutputPath: videoFilePath
|
||||||
videoOutputPath: newVideoFilePath
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobH
|
||||||
|
|
||||||
const videoFilePath = resultPayload.videoFile as string
|
const videoFilePath = resultPayload.videoFile as string
|
||||||
|
|
||||||
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
|
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: false })
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Runner VOD web video transcoding job %s for %s ended.',
|
'Runner VOD web video transcoding job %s for %s ended.',
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import { MutexInterface } from 'async-mutex'
|
import { MutexInterface } from 'async-mutex'
|
||||||
import { Job } from 'bullmq'
|
import { Job } from 'bullmq'
|
||||||
import { ensureDir, move } from 'fs-extra/esm'
|
import { ensureDir, move } from 'fs-extra/esm'
|
||||||
import { stat } from 'fs/promises'
|
import { join } from 'path'
|
||||||
import { basename, extname as extnameUtil, join } from 'path'
|
|
||||||
import { pick } from '@peertube/peertube-core-utils'
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { MVideo, MVideoFile } from '@server/types/models/index.js'
|
import { MVideo } from '@server/types/models/index.js'
|
||||||
import { getVideoStreamDuration, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||||
import { CONFIG } from '../../initializers/config.js'
|
import { CONFIG } from '../../initializers/config.js'
|
||||||
import { VideoFileModel } from '../../models/video/video-file.js'
|
import { VideoFileModel } from '../../models/video/video-file.js'
|
||||||
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
|
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
|
||||||
import { updatePlaylistAfterFileChange } from '../hls.js'
|
import { renameVideoFileInPlaylist, updatePlaylistAfterFileChange } from '../hls.js'
|
||||||
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
|
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
|
||||||
import { buildFileMetadata } from '../video-file.js'
|
import { buildNewFile } from '../video-file.js'
|
||||||
import { VideoPathManager } from '../video-path-manager.js'
|
import { VideoPathManager } from '../video-path-manager.js'
|
||||||
import { buildFFmpegVOD } from './shared/index.js'
|
import { buildFFmpegVOD } from './shared/index.js'
|
||||||
|
|
||||||
|
@ -55,12 +54,11 @@ export function generateHlsPlaylistResolution (options: {
|
||||||
|
|
||||||
export async function onHLSVideoFileTranscoding (options: {
|
export async function onHLSVideoFileTranscoding (options: {
|
||||||
video: MVideo
|
video: MVideo
|
||||||
videoFile: MVideoFile
|
|
||||||
videoOutputPath: string
|
videoOutputPath: string
|
||||||
m3u8OutputPath: string
|
m3u8OutputPath: string
|
||||||
filesLockedInParent?: boolean // default false
|
filesLockedInParent?: boolean // default false
|
||||||
}) {
|
}) {
|
||||||
const { video, videoFile, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
|
const { video, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
|
||||||
|
|
||||||
// Create or update the playlist
|
// Create or update the playlist
|
||||||
const playlist = await retryTransactionWrapper(() => {
|
const playlist = await retryTransactionWrapper(() => {
|
||||||
|
@ -68,7 +66,9 @@ export async function onHLSVideoFileTranscoding (options: {
|
||||||
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
videoFile.videoStreamingPlaylistId = playlist.id
|
|
||||||
|
const newVideoFile = await buildNewFile({ mode: 'hls', path: videoOutputPath })
|
||||||
|
newVideoFile.videoStreamingPlaylistId = playlist.id
|
||||||
|
|
||||||
const mutexReleaser = !filesLockedInParent
|
const mutexReleaser = !filesLockedInParent
|
||||||
? await VideoPathManager.Instance.lockFiles(video.uuid)
|
? await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||||
|
@ -77,33 +77,33 @@ export async function onHLSVideoFileTranscoding (options: {
|
||||||
try {
|
try {
|
||||||
await video.reload()
|
await video.reload()
|
||||||
|
|
||||||
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile)
|
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
|
||||||
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
|
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
|
||||||
|
|
||||||
// Move playlist file
|
// Move playlist file
|
||||||
const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath))
|
const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(
|
||||||
|
video,
|
||||||
|
getHlsResolutionPlaylistFilename(newVideoFile.filename)
|
||||||
|
)
|
||||||
await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
|
await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
|
||||||
|
|
||||||
// Move video file
|
// Move video file
|
||||||
await move(videoOutputPath, videoFilePath, { overwrite: true })
|
await move(videoOutputPath, videoFilePath, { overwrite: true })
|
||||||
|
|
||||||
|
await renameVideoFileInPlaylist(resolutionPlaylistPath, newVideoFile.filename)
|
||||||
|
|
||||||
// Update video duration if it was not set (in case of a live for example)
|
// Update video duration if it was not set (in case of a live for example)
|
||||||
if (!video.duration) {
|
if (!video.duration) {
|
||||||
video.duration = await getVideoStreamDuration(videoFilePath)
|
video.duration = await getVideoStreamDuration(videoFilePath)
|
||||||
await video.save()
|
await video.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = await stat(videoFilePath)
|
await createTorrentAndSetInfoHash(playlist, newVideoFile)
|
||||||
|
|
||||||
videoFile.size = stats.size
|
|
||||||
videoFile.fps = await getVideoStreamFPS(videoFilePath)
|
|
||||||
videoFile.metadata = await buildFileMetadata(videoFilePath)
|
|
||||||
|
|
||||||
await createTorrentAndSetInfoHash(playlist, videoFile)
|
|
||||||
|
|
||||||
const oldFile = await VideoFileModel.loadHLSFile({
|
const oldFile = await VideoFileModel.loadHLSFile({
|
||||||
playlistId: playlist.id,
|
playlistId: playlist.id,
|
||||||
fps: videoFile.fps,
|
fps: newVideoFile.fps,
|
||||||
resolution: videoFile.resolution
|
resolution: newVideoFile.resolution
|
||||||
})
|
})
|
||||||
|
|
||||||
if (oldFile) {
|
if (oldFile) {
|
||||||
|
@ -111,7 +111,7 @@ export async function onHLSVideoFileTranscoding (options: {
|
||||||
await oldFile.destroy()
|
await oldFile.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined)
|
const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
|
||||||
|
|
||||||
await updatePlaylistAfterFileChange(video, playlist)
|
await updatePlaylistAfterFileChange(video, playlist)
|
||||||
|
|
||||||
|
@ -171,17 +171,8 @@ async function generateHlsPlaylistCommon (options: {
|
||||||
|
|
||||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||||
|
|
||||||
const newVideoFile = new VideoFileModel({
|
|
||||||
resolution,
|
|
||||||
extname: extnameUtil(videoFilename),
|
|
||||||
size: 0,
|
|
||||||
filename: videoFilename,
|
|
||||||
fps: -1
|
|
||||||
})
|
|
||||||
|
|
||||||
await onHLSVideoFileTranscoding({
|
await onHLSVideoFileTranscoding({
|
||||||
video,
|
video,
|
||||||
videoFile: newVideoFile,
|
|
||||||
videoOutputPath,
|
videoOutputPath,
|
||||||
m3u8OutputPath,
|
m3u8OutputPath,
|
||||||
filesLockedInParent: !inputFileMutexReleaser
|
filesLockedInParent: !inputFileMutexReleaser
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { Job } from 'bullmq'
|
import { Job } from 'bullmq'
|
||||||
import { move, remove } from 'fs-extra/esm'
|
import { move, remove } from 'fs-extra/esm'
|
||||||
import { copyFile, stat } from 'fs/promises'
|
import { copyFile } from 'fs/promises'
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { FileStorage } from '@peertube/peertube-models'
|
|
||||||
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
||||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@peertube/peertube-ffmpeg'
|
import { getVideoStreamDuration, TranscodeVODOptionsType } from '@peertube/peertube-ffmpeg'
|
||||||
import { CONFIG } from '../../initializers/config.js'
|
import { CONFIG } from '../../initializers/config.js'
|
||||||
import { VideoFileModel } from '../../models/video/video-file.js'
|
import { VideoFileModel } from '../../models/video/video-file.js'
|
||||||
import { JobQueue } from '../job-queue/index.js'
|
import { JobQueue } from '../job-queue/index.js'
|
||||||
import { generateWebVideoFilename } from '../paths.js'
|
import { generateWebVideoFilename } from '../paths.js'
|
||||||
import { buildFileMetadata } from '../video-file.js'
|
import { buildNewFile } from '../video-file.js'
|
||||||
import { VideoPathManager } from '../video-path-manager.js'
|
import { VideoPathManager } from '../video-path-manager.js'
|
||||||
import { buildFFmpegVOD } from './shared/index.js'
|
import { buildFFmpegVOD } from './shared/index.js'
|
||||||
import { buildOriginalFileResolution } from './transcoding-resolutions.js'
|
import { buildOriginalFileResolution } from './transcoding-resolutions.js'
|
||||||
import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
|
import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
|
||||||
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
// Optimize the original video file and replace it. The resolution is not changed.
|
// Optimize the original video file and replace it. The resolution is not changed.
|
||||||
export async function optimizeOriginalVideofile (options: {
|
export async function optimizeOriginalVideofile (options: {
|
||||||
|
@ -62,19 +62,7 @@ export async function optimizeOriginalVideofile (options: {
|
||||||
fps
|
fps
|
||||||
})
|
})
|
||||||
|
|
||||||
// Important to do this before getVideoFilename() to take in account the new filename
|
const { videoFile } = await onWebVideoFileTranscoding({ video, videoOutputPath, deleteWebInputVideoFile: inputVideoFile })
|
||||||
inputVideoFile.resolution = resolution
|
|
||||||
inputVideoFile.extname = newExtname
|
|
||||||
inputVideoFile.filename = generateWebVideoFilename(resolution, newExtname)
|
|
||||||
inputVideoFile.storage = FileStorage.FILE_SYSTEM
|
|
||||||
|
|
||||||
const { videoFile } = await onWebVideoFileTranscoding({
|
|
||||||
video,
|
|
||||||
videoFile: inputVideoFile,
|
|
||||||
videoOutputPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await remove(videoInputPath)
|
|
||||||
|
|
||||||
return { transcodeType, videoFile }
|
return { transcodeType, videoFile }
|
||||||
})
|
})
|
||||||
|
@ -104,15 +92,8 @@ export async function transcodeNewWebVideoResolution (options: {
|
||||||
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
||||||
|
|
||||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
||||||
const newVideoFile = new VideoFileModel({
|
const filename = generateWebVideoFilename(resolution, newExtname)
|
||||||
resolution,
|
const videoOutputPath = join(transcodeDirectory, filename)
|
||||||
extname: newExtname,
|
|
||||||
filename: generateWebVideoFilename(resolution, newExtname),
|
|
||||||
size: 0,
|
|
||||||
videoId: video.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
|
|
||||||
|
|
||||||
const transcodeOptions = {
|
const transcodeOptions = {
|
||||||
type: 'video' as 'video',
|
type: 'video' as 'video',
|
||||||
|
@ -128,7 +109,7 @@ export async function transcodeNewWebVideoResolution (options: {
|
||||||
|
|
||||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||||
|
|
||||||
return onWebVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath })
|
return onWebVideoFileTranscoding({ video, videoOutputPath })
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -188,20 +169,10 @@ export async function mergeAudioVideofile (options: {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
await onWebVideoFileTranscoding({
|
||||||
inputVideoFile.extname = newExtname
|
|
||||||
inputVideoFile.resolution = resolution
|
|
||||||
inputVideoFile.filename = generateWebVideoFilename(inputVideoFile.resolution, newExtname)
|
|
||||||
|
|
||||||
// ffmpeg generated a new video file, so update the video duration
|
|
||||||
// See https://trac.ffmpeg.org/ticket/5456
|
|
||||||
video.duration = await getVideoStreamDuration(videoOutputPath)
|
|
||||||
await video.save()
|
|
||||||
|
|
||||||
return onWebVideoFileTranscoding({
|
|
||||||
video,
|
video,
|
||||||
videoFile: inputVideoFile,
|
|
||||||
videoOutputPath,
|
videoOutputPath,
|
||||||
|
deleteWebInputVideoFile: inputVideoFile,
|
||||||
wasAudioFile: true
|
wasAudioFile: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -214,36 +185,42 @@ export async function mergeAudioVideofile (options: {
|
||||||
|
|
||||||
export async function onWebVideoFileTranscoding (options: {
|
export async function onWebVideoFileTranscoding (options: {
|
||||||
video: MVideoFullLight
|
video: MVideoFullLight
|
||||||
videoFile: MVideoFile
|
|
||||||
videoOutputPath: string
|
videoOutputPath: string
|
||||||
wasAudioFile?: boolean // default false
|
wasAudioFile?: boolean // default false
|
||||||
|
deleteWebInputVideoFile?: MVideoFile
|
||||||
}) {
|
}) {
|
||||||
const { video, videoFile, videoOutputPath, wasAudioFile } = options
|
const { video, videoOutputPath, wasAudioFile, deleteWebInputVideoFile } = options
|
||||||
|
|
||||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||||
|
|
||||||
|
const videoFile = await buildNewFile({ mode: 'web-video', path: videoOutputPath })
|
||||||
|
videoFile.videoId = video.id
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await video.reload()
|
await video.reload()
|
||||||
|
|
||||||
|
// ffmpeg generated a new video file, so update the video duration
|
||||||
|
// See https://trac.ffmpeg.org/ticket/5456
|
||||||
|
if (wasAudioFile) {
|
||||||
|
video.duration = await getVideoStreamDuration(videoOutputPath)
|
||||||
|
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
|
||||||
|
await video.save()
|
||||||
|
}
|
||||||
|
|
||||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
||||||
|
|
||||||
const stats = await stat(videoOutputPath)
|
|
||||||
|
|
||||||
const probe = await ffprobePromise(videoOutputPath)
|
|
||||||
const fps = await getVideoStreamFPS(videoOutputPath, probe)
|
|
||||||
const metadata = await buildFileMetadata(videoOutputPath, probe)
|
|
||||||
|
|
||||||
await move(videoOutputPath, outputPath, { overwrite: true })
|
await move(videoOutputPath, outputPath, { overwrite: true })
|
||||||
|
|
||||||
videoFile.size = stats.size
|
|
||||||
videoFile.fps = fps
|
|
||||||
videoFile.metadata = metadata
|
|
||||||
|
|
||||||
await createTorrentAndSetInfoHash(video, videoFile)
|
await createTorrentAndSetInfoHash(video, videoFile)
|
||||||
|
|
||||||
const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
||||||
if (oldFile) await video.removeWebVideoFile(oldFile)
|
if (oldFile) await video.removeWebVideoFile(oldFile)
|
||||||
|
|
||||||
|
if (deleteWebInputVideoFile) {
|
||||||
|
await video.removeWebVideoFile(deleteWebInputVideoFile)
|
||||||
|
await deleteWebInputVideoFile.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||||
video.VideoFiles = await video.$get('VideoFiles')
|
video.VideoFiles = await video.$get('VideoFiles')
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,11 @@ async function buildNewFile (options: {
|
||||||
if (await isAudioFile(path, probe)) {
|
if (await isAudioFile(path, probe)) {
|
||||||
videoFile.resolution = VideoResolution.H_NOVIDEO
|
videoFile.resolution = VideoResolution.H_NOVIDEO
|
||||||
} else {
|
} else {
|
||||||
|
const dimensions = await getVideoStreamDimensionsInfo(path, probe)
|
||||||
videoFile.fps = await getVideoStreamFPS(path, probe)
|
videoFile.fps = await getVideoStreamFPS(path, probe)
|
||||||
videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution
|
videoFile.resolution = dimensions.resolution
|
||||||
|
videoFile.width = dimensions.width
|
||||||
|
videoFile.height = dimensions.height
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFile.filename = mode === 'web-video'
|
videoFile.filename = mode === 'web-video'
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { getTranscodingJobPriority } from './transcoding/transcoding-priority.js
|
||||||
import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file.js'
|
import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file.js'
|
||||||
import { VideoPathManager } from './video-path-manager.js'
|
import { VideoPathManager } from './video-path-manager.js'
|
||||||
import { buildStoryboardJobIfNeeded } from './video-jobs.js'
|
import { buildStoryboardJobIfNeeded } from './video-jobs.js'
|
||||||
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('video-studio')
|
const lTags = loggerTagsFactory('video-studio')
|
||||||
|
|
||||||
|
@ -104,6 +105,7 @@ export async function onVideoStudioEnded (options: {
|
||||||
await newFile.save()
|
await newFile.save()
|
||||||
|
|
||||||
video.duration = await getVideoStreamDuration(outputPath)
|
video.duration = await getVideoStreamDuration(outputPath)
|
||||||
|
video.aspectRatio = buildAspectRatio({ width: newFile.width, height: newFile.height })
|
||||||
await video.save()
|
await video.save()
|
||||||
|
|
||||||
return JobQueue.Instance.createSequentialJobFlow(
|
return JobQueue.Instance.createSequentialJobFlow(
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
isPluginTypeValid
|
isPluginTypeValid
|
||||||
} from '../../helpers/custom-validators/plugins.js'
|
} from '../../helpers/custom-validators/plugins.js'
|
||||||
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
||||||
|
import { logger } from '@server/helpers/logger.js'
|
||||||
|
|
||||||
@DefaultScope(() => ({
|
@DefaultScope(() => ({
|
||||||
attributes: {
|
attributes: {
|
||||||
|
@ -173,6 +174,7 @@ export class PluginModel extends SequelizeModel<PluginModel> {
|
||||||
result[name] = p.settings[name]
|
result[name] = p.settings[name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.error('internal', { result })
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
|
@ -88,6 +88,8 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
|
|
||||||
preview: buildPreviewAPAttribute(video),
|
preview: buildPreviewAPAttribute(video),
|
||||||
|
|
||||||
|
aspectRatio: video.aspectRatio,
|
||||||
|
|
||||||
url,
|
url,
|
||||||
|
|
||||||
likes: getLocalVideoLikesActivityPubUrl(video),
|
likes: getLocalVideoLikesActivityPubUrl(video),
|
||||||
|
@ -185,7 +187,8 @@ function buildVideoFileUrls (options: {
|
||||||
rel: [ 'metadata', fileAP.mediaType ],
|
rel: [ 'metadata', fileAP.mediaType ],
|
||||||
mediaType: 'application/json' as 'application/json',
|
mediaType: 'application/json' as 'application/json',
|
||||||
href: getLocalVideoFileMetadataUrl(video, file),
|
href: getLocalVideoFileMetadataUrl(video, file),
|
||||||
height: file.resolution,
|
height: file.height || file.resolution,
|
||||||
|
width: file.width,
|
||||||
fps: file.fps
|
fps: file.fps
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -194,14 +197,18 @@ function buildVideoFileUrls (options: {
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
||||||
href: file.getTorrentUrl(),
|
href: file.getTorrentUrl(),
|
||||||
height: file.resolution
|
height: file.height || file.resolution,
|
||||||
|
width: file.width,
|
||||||
|
fps: file.fps
|
||||||
})
|
})
|
||||||
|
|
||||||
urls.push({
|
urls.push({
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
||||||
href: generateMagnetUri(video, file, trackerUrls),
|
href: generateMagnetUri(video, file, trackerUrls),
|
||||||
height: file.resolution
|
height: file.height || file.resolution,
|
||||||
|
width: file.width,
|
||||||
|
fps: file.fps
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,8 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
|
||||||
isLocal: video.isOwned(),
|
isLocal: video.isOwned(),
|
||||||
duration: video.duration,
|
duration: video.duration,
|
||||||
|
|
||||||
|
aspectRatio: video.aspectRatio,
|
||||||
|
|
||||||
views: video.views,
|
views: video.views,
|
||||||
viewers: VideoViewsManager.Instance.getTotalViewersOf(video),
|
viewers: VideoViewsManager.Instance.getTotalViewersOf(video),
|
||||||
|
|
||||||
|
@ -214,6 +216,9 @@ export function videoFilesModelToFormattedJSON (
|
||||||
: `${videoFile.resolution}p`
|
: `${videoFile.resolution}p`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
width: videoFile.width,
|
||||||
|
height: videoFile.height,
|
||||||
|
|
||||||
magnetUri: includeMagnet && videoFile.hasTorrent()
|
magnetUri: includeMagnet && videoFile.hasTorrent()
|
||||||
? generateMagnetUri(video, videoFile, trackerUrls)
|
? generateMagnetUri(video, videoFile, trackerUrls)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
@ -88,6 +88,8 @@ export class VideoTableAttributes {
|
||||||
'metadataUrl',
|
'metadataUrl',
|
||||||
'videoStreamingPlaylistId',
|
'videoStreamingPlaylistId',
|
||||||
'videoId',
|
'videoId',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
'storage'
|
'storage'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -255,6 +257,7 @@ export class VideoTableAttributes {
|
||||||
'dislikes',
|
'dislikes',
|
||||||
'remote',
|
'remote',
|
||||||
'isLive',
|
'isLive',
|
||||||
|
'aspectRatio',
|
||||||
'url',
|
'url',
|
||||||
'commentsEnabled',
|
'commentsEnabled',
|
||||||
'downloadEnabled',
|
'downloadEnabled',
|
||||||
|
|
|
@ -167,6 +167,14 @@ export class VideoFileModel extends SequelizeModel<VideoFileModel> {
|
||||||
@Column
|
@Column
|
||||||
resolution: number
|
resolution: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
width: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
height: number
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
|
@Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
|
||||||
@Column(DataType.BIGINT)
|
@Column(DataType.BIGINT)
|
||||||
|
@ -640,7 +648,8 @@ export class VideoFileModel extends SequelizeModel<VideoFileModel> {
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mediaType: mimeType as ActivityVideoUrlObject['mediaType'],
|
mediaType: mimeType as ActivityVideoUrlObject['mediaType'],
|
||||||
href: this.getFileUrl(video),
|
href: this.getFileUrl(video),
|
||||||
height: this.resolution,
|
height: this.height || this.resolution,
|
||||||
|
width: this.width,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
fps: this.fps
|
fps: this.fps
|
||||||
}
|
}
|
||||||
|
|
|
@ -565,6 +565,10 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
||||||
@Column
|
@Column
|
||||||
state: VideoStateType
|
state: VideoStateType
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column(DataType.FLOAT)
|
||||||
|
aspectRatio: number
|
||||||
|
|
||||||
// We already have the information in videoSource table for local videos, but we prefer to normalize it for performance
|
// We already have the information in videoSource table for local videos, but we prefer to normalize it for performance
|
||||||
// And also to store the info from remote instances
|
// And also to store the info from remote instances
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
|
|
|
@ -2086,7 +2086,7 @@ paths:
|
||||||
|
|
||||||
/api/v1/users/me/videos:
|
/api/v1/users/me/videos:
|
||||||
get:
|
get:
|
||||||
summary: Get videos of my user
|
summary: List videos of my user
|
||||||
security:
|
security:
|
||||||
- OAuth2:
|
- OAuth2:
|
||||||
- user
|
- user
|
||||||
|
@ -7560,6 +7560,12 @@ components:
|
||||||
fps:
|
fps:
|
||||||
type: number
|
type: number
|
||||||
description: Frames per second of the video file
|
description: Frames per second of the video file
|
||||||
|
width:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Video stream width"
|
||||||
|
height:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Video stream height"
|
||||||
metadataUrl:
|
metadataUrl:
|
||||||
type: string
|
type: string
|
||||||
format: url
|
format: url
|
||||||
|
@ -7676,6 +7682,11 @@ components:
|
||||||
example: 1419
|
example: 1419
|
||||||
format: seconds
|
format: seconds
|
||||||
description: duration of the video in seconds
|
description: duration of the video in seconds
|
||||||
|
aspectRatio:
|
||||||
|
type: number
|
||||||
|
format: float
|
||||||
|
example: 1.778
|
||||||
|
description: "**PeerTube >= 6.1** Aspect ratio of the video stream"
|
||||||
isLocal:
|
isLocal:
|
||||||
type: boolean
|
type: boolean
|
||||||
name:
|
name:
|
||||||
|
|
Loading…
Reference in New Issue