1
0
Fork 0

Add option to not transcode original resolution

This commit is contained in:
Chocobozzz 2022-08-05 10:36:19 +02:00
parent 7e0f50d6e0
commit 84cae54e7a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
26 changed files with 303 additions and 87 deletions

View File

@ -175,6 +175,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
profile: null,
concurrency: CONCURRENCY_VALIDATOR,
resolutions: {},
alwaysTranscodeOriginalResolution: null,
hls: {
enabled: null
},
@ -197,7 +198,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
enabled: null,
threads: TRANSCODING_THREADS_VALIDATOR,
profile: null,
resolutions: {}
resolutions: {},
alwaysTranscodeOriginalResolution: null
}
},
videoStudio: {

View File

@ -41,7 +41,6 @@
<ng-container ngProjectAs="description" i18n>
Small latency disables P2P and high latency can increase P2P ratio
</ng-container>
</my-peertube-checkbox>
</div>
@ -115,8 +114,8 @@
<label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
<div class="ms-2 mt-2 d-flex flex-column">
<ng-container formGroupName="resolutions">
<ng-container formGroupName="resolutions">
<div class="form-group" *ngFor="let resolution of liveResolutions">
<my-peertube-checkbox
[inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
@ -127,8 +126,18 @@
</ng-template>
</my-peertube-checkbox>
</div>
</ng-container>
<div class="form-group">
<my-peertube-checkbox
inputName="transcodingAlwaysTranscodeOriginalResolution" formControlName="alwaysTranscodeOriginalResolution"
i18n-labelText labelText="Also transcode original resolution"
>
<ng-container i18n ngProjectAs="description">
Even if it's above your maximum enabled resolution
</ng-container>
</my-peertube-checkbox>
</div>
</div>
</div>

View File

@ -111,7 +111,13 @@
<label i18n>Resolutions to generate per enabled format</label>
<div class="ms-2 d-flex flex-column">
<span class="mb-3 small muted" i18n>
<my-peertube-checkbox
inputName="transcodingAlwaysTranscodeOriginalResolution" formControlName="alwaysTranscodeOriginalResolution"
i18n-labelText labelText="Always transcode original resolution"
>
</my-peertube-checkbox>
<span class="mt-3 mb-2 small muted" i18n>
The original file resolution will be the default target if no option is selected.
</span>

View File

@ -403,6 +403,9 @@ transcoding:
1440p: false
2160p: false
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
# Generate videos in a WebTorrent format (what we do since the first PeerTube release)
# If you also enabled the hls format, it will multiply videos storage by 2
# If disabled, breaks federation with PeerTube instances < 2.1
@ -496,6 +499,9 @@ live:
1440p: false
2160p: false
# Also transcode original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
video_studio:
# Enable video edition by users (cut, add intro/outro, add watermark etc)
# If enabled, users can create transcoding tasks as they wish

View File

@ -413,6 +413,9 @@ transcoding:
1440p: false
2160p: false
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
# Generate videos in a WebTorrent format (what we do since the first PeerTube release)
# If you also enabled the hls format, it will multiply videos storage by 2
# If disabled, breaks federation with PeerTube instances < 2.1
@ -506,6 +509,9 @@ live:
1440p: false
2160p: false
# Also transcode original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
video_studio:
# Enable video edition by users (cut, add intro/outro, add watermark etc)
# If enabled, users can create transcoding tasks as they wish

View File

@ -1,6 +1,6 @@
import { program } from 'commander'
import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg'
import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg'
import { CONFIG } from '@server/initializers/config'
import { addTranscodingJob } from '@server/lib/video'
import { VideoState, VideoTranscodingPayload } from '@shared/models'
@ -53,7 +53,7 @@ async function run () {
if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
const resolutionsEnabled = options.resolution
? [ parseInt(options.resolution) ]
: computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ])
: computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true })
for (const resolution of resolutionsEnabled) {
dataInput.push({
@ -61,8 +61,6 @@ async function run () {
videoUUID: video.uuid,
resolution,
// FIXME: check the file has audio and is not in portrait mode
isPortraitMode: false,
hasAudio: true,
copyCodecs: false,

View File

@ -31,8 +31,7 @@ async function run (path: string, cmd: any) {
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
resolution: +cmd.resolution,
isPortraitMode: false
resolution: +cmd.resolution
} as TranscodeVODOptions
let command = ffmpeg(options.inputPath)

View File

@ -227,6 +227,7 @@ function customConfig (): CustomConfig {
'1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'],
'2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
},
alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
webtorrent: {
enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
},
@ -256,7 +257,8 @@ function customConfig (): CustomConfig {
'1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'],
'1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'],
'2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
}
},
alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
}
},
videoStudio: {

View File

@ -1,5 +1,5 @@
import express from 'express'
import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg'
import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { addTranscodingJob } from '@server/lib/video'
import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
@ -30,9 +30,9 @@ async function createTranscoding (req: express.Request, res: express.Response) {
const body: VideoTranscodingCreate = req.body
const { resolution: maxResolution, isPortraitMode, audioStream } = await video.probeMaxQualityFile()
const { resolution: maxResolution, audioStream } = await video.probeMaxQualityFile()
const resolutions = await Hooks.wrapObject(
computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]),
computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true }),
'filter:transcoding.manual.lower-resolutions-to-transcode.result',
body
)
@ -50,7 +50,6 @@ async function createTranscoding (req: express.Request, res: express.Response) {
type: 'new-resolution-to-hls',
videoUUID: video.uuid,
resolution,
isPortraitMode,
hasAudio: !!audioStream,
copyCodecs: false,
isNewVideo: false,
@ -64,8 +63,7 @@ async function createTranscoding (req: express.Request, res: express.Response) {
isNewVideo: false,
resolution,
hasAudio: !!audioStream,
createHLSIfNeeded: false,
isPortraitMode
createHLSIfNeeded: false
})
}
}

View File

@ -7,7 +7,7 @@ import { AvailableEncoders, VideoResolution } from '@shared/models'
import { logger, loggerTagsFactory } from '../logger'
import { getFFmpeg, runCommand } from './ffmpeg-commons'
import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
import { computeFPS, getVideoStreamFPS } from './ffprobe-utils'
import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
const lTags = loggerTagsFactory('ffmpeg')
@ -27,8 +27,6 @@ interface BaseTranscodeVODOptions {
resolution: number
isPortraitMode?: boolean
job?: Job
}
@ -115,13 +113,17 @@ export {
// ---------------------------------------------------------------------------
async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
let fps = await getVideoStreamFPS(options.inputPath)
const probe = await ffprobePromise(options.inputPath)
let fps = await getVideoStreamFPS(options.inputPath, probe)
fps = computeFPS(fps, options.resolution)
let scaleFilterValue: string
if (options.resolution !== undefined) {
scaleFilterValue = options.isPortraitMode === true
const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
scaleFilterValue = videoStreamInfo?.isPortraitMode === true
? `w=${options.resolution}:h=-2`
: `w=-2:h=${options.resolution}`
}

View File

@ -90,15 +90,21 @@ async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) {
// Resolutions
// ---------------------------------------------------------------------------
function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
function computeResolutionsToTranscode (options: {
inputResolution: number
type: 'vod' | 'live'
includeInputResolution: boolean
}) {
const { inputResolution, type, includeInputResolution } = options
const configResolutions = type === 'vod'
? CONFIG.TRANSCODING.RESOLUTIONS
: CONFIG.LIVE.TRANSCODING.RESOLUTIONS
const resolutionsEnabled: number[] = []
const resolutionsEnabled = new Set<number>()
// Put in the order we want to proceed jobs
const resolutions: VideoResolution[] = [
const availableResolutions: VideoResolution[] = [
VideoResolution.H_NOVIDEO,
VideoResolution.H_480P,
VideoResolution.H_360P,
@ -110,13 +116,17 @@ function computeLowerResolutionsToTranscode (videoFileResolution: number, type:
VideoResolution.H_4K
]
for (const resolution of resolutions) {
if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
resolutionsEnabled.push(resolution)
for (const resolution of availableResolutions) {
if (configResolutions[resolution + 'p'] === true && inputResolution > resolution) {
resolutionsEnabled.add(resolution)
}
}
return resolutionsEnabled
if (includeInputResolution) {
resolutionsEnabled.add(inputResolution)
}
return Array.from(resolutionsEnabled)
}
// ---------------------------------------------------------------------------
@ -224,7 +234,7 @@ export {
computeFPS,
getClosestFramerateStandard,
computeLowerResolutionsToTranscode,
computeResolutionsToTranscode,
canDoQuickTranscode,
canDoQuickVideoTranscode,

View File

@ -30,7 +30,7 @@ function checkMissedConfig () {
'transcoding.profile', 'transcoding.concurrency',
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
'transcoding.resolutions.2160p', 'video_studio.enabled',
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'video_studio.enabled',
'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days',
'client.videos.miniature.display_author_avatar',
@ -59,7 +59,7 @@ function checkMissedConfig () {
'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p'
'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution'
]
const requiredAlternatives = [

View File

@ -309,6 +309,7 @@ const CONFIG = {
get THREADS () { return config.get<number>('transcoding.threads') },
get CONCURRENCY () { return config.get<number>('transcoding.concurrency') },
get PROFILE () { return config.get<string>('transcoding.profile') },
get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('transcoding.always_transcode_original_resolution') },
RESOLUTIONS: {
get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
get '144p' () { return config.get<boolean>('transcoding.resolutions.144p') },
@ -361,6 +362,8 @@ const CONFIG = {
get THREADS () { return config.get<number>('live.transcoding.threads') },
get PROFILE () { return config.get<string>('live.transcoding.profile') },
get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('live.transcoding.always_transcode_original_resolution') },
RESOLUTIONS: {
get '144p' () { return config.get<boolean>('live.transcoding.resolutions.144p') },
get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },

View File

@ -213,13 +213,12 @@ async function assignReplayFilesToVideo (options: {
const probe = await ffprobePromise(concatenatedTsFilePath)
const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe)
const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
video,
concatenatedTsFilePath,
resolution,
isPortraitMode,
isAAC: audioStream?.codec_name === 'aac'
})

View File

@ -1,5 +1,6 @@
import { Job } from 'bull'
import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg'
import { Hooks } from '@server/lib/plugins/hooks'
import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video'
import { VideoPathManager } from '@server/lib/video-path-manager'
import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
@ -16,7 +17,7 @@ import {
VideoTranscodingPayload
} from '@shared/models'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg'
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg'
import { logger, loggerTagsFactory } from '../../../helpers/logger'
import { CONFIG } from '../../../initializers/config'
import { VideoModel } from '../../../models/video/video'
@ -26,7 +27,6 @@ import {
optimizeOriginalVideofile,
transcodeNewWebTorrentResolution
} from '../../transcoding/transcoding'
import { Hooks } from '@server/lib/plugins/hooks'
type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void>
@ -99,7 +99,6 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV
videoInputPath,
resolution: payload.resolution,
copyCodecs: payload.copyCodecs,
isPortraitMode: payload.isPortraitMode || false,
job
})
})
@ -117,7 +116,7 @@ async function handleNewWebTorrentResolutionJob (
) {
logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid))
await transcodeNewWebTorrentResolution(video, payload.resolution, payload.isPortraitMode || false, job)
await transcodeNewWebTorrentResolution({ video, resolution: payload.resolution, job })
logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid))
@ -127,7 +126,7 @@ async function handleNewWebTorrentResolutionJob (
async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) {
logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid))
await mergeAudioVideofile(video, payload.resolution, job)
await mergeAudioVideofile({ video, resolution: payload.resolution, job })
logger.info('Merge audio transcoding job for %s ended.', video.uuid, lTags(video.uuid))
@ -137,7 +136,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans
async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) {
logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid))
const { transcodeType } = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job)
const { transcodeType } = await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), job })
logger.info('Optimize transcoding job for %s ended.', video.uuid, lTags(video.uuid))
@ -161,7 +160,6 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
video,
user,
videoFileResolution: payload.resolution,
isPortraitMode: payload.isPortraitMode,
hasAudio: payload.hasAudio,
isNewVideo: payload.isNewVideo ?? true,
type: 'hls'
@ -178,7 +176,7 @@ async function onVideoFirstWebTorrentTranscoding (
transcodeType: TranscodeVODOptionsType,
user: MUserId
) {
const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile()
const { resolution, audioStream } = await videoArg.probeMaxQualityFile()
// Maybe the video changed in database, refresh it
const videoDatabase = await VideoModel.loadFull(videoArg.uuid)
@ -189,7 +187,6 @@ async function onVideoFirstWebTorrentTranscoding (
const originalFileHLSPayload = {
...payload,
isPortraitMode,
hasAudio: !!audioStream,
resolution: videoDatabase.getMaxQualityFile().resolution,
// If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues
@ -202,7 +199,6 @@ async function onVideoFirstWebTorrentTranscoding (
user,
videoFileResolution: resolution,
hasAudio: !!audioStream,
isPortraitMode,
type: 'webtorrent',
isNewVideo: payload.isNewVideo ?? true
})
@ -235,7 +231,6 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
videoUUID: string
resolution: number
hasAudio: boolean
isPortraitMode?: boolean
copyCodecs: boolean
isMaxQuality: boolean
isNewVideo?: boolean
@ -250,7 +245,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
type: 'new-resolution-to-hls',
autoDeleteWebTorrentIfNeeded: true,
...pick(payload, [ 'videoUUID', 'resolution', 'isPortraitMode', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ])
...pick(payload, [ 'videoUUID', 'resolution', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ])
}
await addTranscodingJob(hlsTranscodingPayload, jobOptions)
@ -262,16 +257,15 @@ async function createLowerResolutionsJobs (options: {
video: MVideoFullLight
user: MUserId
videoFileResolution: number
isPortraitMode: boolean
hasAudio: boolean
isNewVideo: boolean
type: 'hls' | 'webtorrent'
}) {
const { video, user, videoFileResolution, isPortraitMode, isNewVideo, hasAudio, type } = options
const { video, user, videoFileResolution, isNewVideo, hasAudio, type } = options
// Create transcoding jobs if there are enabled resolutions
const resolutionsEnabled = await Hooks.wrapObject(
computeLowerResolutionsToTranscode(videoFileResolution, 'vod'),
computeResolutionsToTranscode({ inputResolution: videoFileResolution, type: 'vod', includeInputResolution: false }),
'filter:transcoding.auto.lower-resolutions-to-transcode.result',
options
)
@ -289,7 +283,6 @@ async function createLowerResolutionsJobs (options: {
type: 'new-resolution-to-webtorrent',
videoUUID: video.uuid,
resolution,
isPortraitMode,
hasAudio,
createHLSIfNeeded: true,
isNewVideo
@ -303,7 +296,6 @@ async function createLowerResolutionsJobs (options: {
type: 'new-resolution-to-hls',
videoUUID: video.uuid,
resolution,
isPortraitMode,
hasAudio,
copyCodecs: false,
isMaxQuality: false,

View File

@ -4,7 +4,7 @@ import { createServer, Server } from 'net'
import { join } from 'path'
import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
import {
computeLowerResolutionsToTranscode,
computeResolutionsToTranscode,
ffprobePromise,
getLiveSegmentTime,
getVideoStreamBitrate,
@ -26,10 +26,10 @@ import { federateVideoIfNeeded } from '../activitypub/videos'
import { JobQueue } from '../job-queue'
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths'
import { PeerTubeSocket } from '../peertube-socket'
import { Hooks } from '../plugins/hooks'
import { LiveQuotaStore } from './live-quota-store'
import { cleanupPermanentLive } from './live-utils'
import { MuxingSession } from './shared'
import { Hooks } from '../plugins/hooks'
const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
const context = require('node-media-server/src/node_core_ctx')
@ -456,11 +456,17 @@ class LiveManager {
}
private buildAllResolutionsToTranscode (originResolution: number) {
const includeInputResolution = CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED
? computeLowerResolutionsToTranscode(originResolution, 'live')
? computeResolutionsToTranscode({ inputResolution: originResolution, type: 'live', includeInputResolution })
: []
return resolutionsEnabled.concat([ originResolution ])
if (resolutionsEnabled.length === 0) {
return [ originResolution ]
}
return resolutionsEnabled
}
private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> {

View File

@ -10,6 +10,7 @@ import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
import {
buildFileMetadata,
canDoQuickTranscode,
computeResolutionsToTranscode,
getVideoStreamDuration,
getVideoStreamFPS,
transcodeVOD,
@ -32,7 +33,13 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
*/
// Optimize the original video file and replace it. The resolution is not changed.
function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
function optimizeOriginalVideofile (options: {
video: MVideoFullLight
inputVideoFile: MVideoFile
job: Job
}) {
const { video, inputVideoFile, job } = options
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
@ -43,7 +50,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
? 'quick-transcode'
: 'video'
const resolution = toEven(inputVideoFile.resolution)
const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
const transcodeOptions: TranscodeVODOptions = {
type: transcodeType,
@ -63,6 +70,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
await transcodeVOD(transcodeOptions)
// Important to do this before getVideoFilename() to take in account the new filename
inputVideoFile.resolution = resolution
inputVideoFile.extname = newExtname
inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
inputVideoFile.storage = VideoStorage.FILE_SYSTEM
@ -76,17 +84,22 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
})
}
// Transcode the original video file to a lower resolution
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
// Transcode the original video file to a lower resolution compatible with WebTorrent
function transcodeNewWebTorrentResolution (options: {
video: MVideoFullLight
resolution: VideoResolution
job: Job
}) {
const { video, resolution, job } = options
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const extname = '.mp4'
const newExtname = '.mp4'
return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
const newVideoFile = new VideoFileModel({
resolution,
extname,
filename: generateWebTorrentVideoFilename(resolution, extname),
extname: newExtname,
filename: generateWebTorrentVideoFilename(resolution, newExtname),
size: 0,
videoId: video.id
})
@ -117,7 +130,6 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V
profile: CONFIG.TRANSCODING.PROFILE,
resolution,
isPortraitMode: isPortrait,
job
}
@ -129,7 +141,13 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V
}
// Merge an image with an audio file to create a video
function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
function mergeAudioVideofile (options: {
video: MVideoFullLight
resolution: VideoResolution
job: Job
}) {
const { video, resolution, job } = options
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
@ -188,13 +206,11 @@ async function generateHlsPlaylistResolutionFromTS (options: {
video: MVideo
concatenatedTsFilePath: string
resolution: VideoResolution
isPortraitMode: boolean
isAAC: boolean
}) {
return generateHlsPlaylistCommon({
video: options.video,
resolution: options.resolution,
isPortraitMode: options.isPortraitMode,
inputPath: options.concatenatedTsFilePath,
type: 'hls-from-ts' as 'hls-from-ts',
isAAC: options.isAAC
@ -207,14 +223,12 @@ function generateHlsPlaylistResolution (options: {
videoInputPath: string
resolution: VideoResolution
copyCodecs: boolean
isPortraitMode: boolean
job?: Job
}) {
return generateHlsPlaylistCommon({
video: options.video,
resolution: options.resolution,
copyCodecs: options.copyCodecs,
isPortraitMode: options.isPortraitMode,
inputPath: options.videoInputPath,
type: 'hls' as 'hls',
job: options.job
@ -267,11 +281,10 @@ async function generateHlsPlaylistCommon (options: {
resolution: VideoResolution
copyCodecs?: boolean
isAAC?: boolean
isPortraitMode: boolean
job?: Job
}) {
const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const videoTranscodedBasePath = join(transcodeDirectory, type)
@ -292,7 +305,6 @@ async function generateHlsPlaylistCommon (options: {
resolution,
copyCodecs,
isPortraitMode,
isAAC,
@ -350,3 +362,12 @@ async function generateHlsPlaylistCommon (options: {
return { resolutionPlaylistPath, videoFile: savedVideoFile }
}
function buildOriginalFileResolution (inputResolution: number) {
if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution)
const resolutions = computeResolutionsToTranscode({ inputResolution, type: 'vod', includeInputResolution: false })
if (resolutions.length === 0) return toEven(inputResolution)
return Math.max(...resolutions)
}

View File

@ -54,6 +54,9 @@ const customConfigUpdateValidator = [
body('transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'),
body('transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'),
body('transcoding.alwaysTranscodeOriginalResolution').isBoolean()
.withMessage('Should have a valid always transcode original resolution boolean'),
body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'),
@ -91,6 +94,8 @@ const customConfigUpdateValidator = [
body('live.transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
body('live.transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'),
body('live.transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'),
body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean()
.withMessage('Should have a valid always transcode live original resolution boolean'),
body('search.remoteUri.users').isBoolean().withMessage('Should have a remote URI search for users boolean'),
body('search.remoteUri.anonymous').isBoolean().withMessage('Should have a valid remote URI search for anonymous boolean'),

View File

@ -114,6 +114,7 @@ describe('Test config API validators', function () {
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false,
webtorrent: {
enabled: true
},
@ -145,7 +146,8 @@ describe('Test config API validators', function () {
'1080p': true,
'1440p': true,
'2160p': true
}
},
alwaysTranscodeOriginalResolution: false
}
},
videoStudio: {

View File

@ -4,7 +4,7 @@ import 'mocha'
import * as chai from 'chai'
import { basename, join } from 'path'
import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg'
import { checkLiveCleanup, checkLiveSegmentHash, checkResolutionsInMasterPlaylist, testImage } from '@server/tests/shared'
import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist, getAllFiles, testImage } from '@server/tests/shared'
import { wait } from '@shared/core-utils'
import {
HttpStatusCode,
@ -468,7 +468,7 @@ describe('Test live', function () {
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, resolutions)
await testVideoResolutions(liveVideoId, resolutions.concat([ 720 ]))
await stopFfmpeg(ffmpegCommand)
})
@ -580,10 +580,73 @@ describe('Test live', function () {
}
})
it('Should correctly have cleaned up the live files', async function () {
this.timeout(30000)
it('Should not generate an upper resolution than original file', async function () {
this.timeout(400_000)
await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ])
const resolutions = [ 240, 480 ]
await updateConf(resolutions)
await servers[0].config.updateExistingSubConfig({
newConfig: {
live: {
transcoding: {
alwaysTranscodeOriginalResolution: false
}
}
}
})
liveVideoId = await createLiveWrapper(true)
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, resolutions)
await stopFfmpeg(ffmpegCommand)
await commands[0].waitUntilEnded({ videoId: liveVideoId })
await waitJobs(servers)
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
const video = await servers[0].videos.get({ id: liveVideoId })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(0)
expect(hlsFiles).to.have.lengthOf(resolutions.length)
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions)
})
it('Should only keep the original resolution if all resolutions are disabled', async function () {
this.timeout(400_000)
await updateConf([])
liveVideoId = await createLiveWrapper(true)
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, [ 720 ])
await stopFfmpeg(ffmpegCommand)
await commands[0].waitUntilEnded({ videoId: liveVideoId })
await waitJobs(servers)
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
const video = await servers[0].videos.get({ id: liveVideoId })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(0)
expect(hlsFiles).to.have.lengthOf(1)
expect(hlsFiles[0].resolution.id).to.equal(720)
})
})

View File

@ -77,6 +77,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.transcoding.resolutions['1080p']).to.be.true
expect(data.transcoding.resolutions['1440p']).to.be.true
expect(data.transcoding.resolutions['2160p']).to.be.true
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
expect(data.transcoding.webtorrent.enabled).to.be.true
expect(data.transcoding.hls.enabled).to.be.true
@ -97,6 +98,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.live.transcoding.resolutions['1080p']).to.be.false
expect(data.live.transcoding.resolutions['1440p']).to.be.false
expect(data.live.transcoding.resolutions['2160p']).to.be.false
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
expect(data.videoStudio.enabled).to.be.false
@ -181,6 +183,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.transcoding.resolutions['720p']).to.be.false
expect(data.transcoding.resolutions['1080p']).to.be.false
expect(data.transcoding.resolutions['2160p']).to.be.false
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
expect(data.transcoding.hls.enabled).to.be.false
expect(data.transcoding.webtorrent.enabled).to.be.true
@ -200,6 +203,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.live.transcoding.resolutions['720p']).to.be.true
expect(data.live.transcoding.resolutions['1080p']).to.be.true
expect(data.live.transcoding.resolutions['2160p']).to.be.true
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
expect(data.videoStudio.enabled).to.be.true
@ -318,6 +322,7 @@ const newCustomConfig: CustomConfig = {
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false,
webtorrent: {
enabled: true
},
@ -347,7 +352,8 @@ const newCustomConfig: CustomConfig = {
'1080p': true,
'1440p': true,
'2160p': true
}
},
alwaysTranscodeOriginalResolution: false
}
},
videoStudio: {

View File

@ -7,11 +7,11 @@ import { canDoQuickTranscode } from '@server/helpers/ffmpeg'
import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared'
import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils'
import {
getAudioStream,
buildFileMetadata,
getAudioStream,
getVideoStreamBitrate,
getVideoStreamFPS,
getVideoStreamDimensionsInfo,
getVideoStreamFPS,
hasAudioStream
} from '@shared/extra-utils'
import { HttpStatusCode, VideoState } from '@shared/models'
@ -727,6 +727,82 @@ describe('Test video transcoding', function () {
})
})
describe('Bounded transcoding', function () {
it('Should not generate an upper resolution than original file', async function () {
this.timeout(120_000)
await servers[0].config.updateExistingSubConfig({
newConfig: {
transcoding: {
enabled: true,
hls: { enabled: true },
webtorrent: { enabled: true },
resolutions: {
'0p': false,
'144p': false,
'240p': true,
'360p': false,
'480p': true,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(2)
expect(hlsFiles).to.have.lengthOf(2)
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
})
it('Should only keep the original resolution if all resolutions are disabled', async function () {
this.timeout(120_000)
await servers[0].config.updateExistingSubConfig({
newConfig: {
transcoding: {
resolutions: {
'0p': false,
'144p': false,
'240p': false,
'360p': false,
'480p': false,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
}
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(1)
expect(hlsFiles).to.have.lengthOf(1)
expect(video.files[0].resolution.id).to.equal(720)
expect(hlsFiles[0].resolution.id).to.equal(720)
})
})
after(async function () {
await cleanupTests(servers)
})

View File

@ -68,6 +68,9 @@ async function checkResolutionsInMasterPlaylist (options: {
expect(masterPlaylist).to.match(reg)
}
const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
expect(playlistsLength).to.have.lengthOf(resolutions.length)
}
export {

View File

@ -117,6 +117,8 @@ export interface CustomConfig {
resolutions: ConfigResolutions & { '0p': boolean }
alwaysTranscodeOriginalResolution: boolean
webtorrent: {
enabled: boolean
}
@ -144,6 +146,7 @@ export interface CustomConfig {
threads: number
profile: string
resolutions: ConfigResolutions
alwaysTranscodeOriginalResolution: boolean
}
}

View File

@ -1,7 +1,7 @@
import { ContextType } from '../activitypub/context'
import { VideoState } from '../videos'
import { VideoStudioTaskCut } from '../videos/studio'
import { VideoResolution } from '../videos/file/video-resolution.enum'
import { VideoStudioTaskCut } from '../videos/studio'
import { SendEmailOptions } from './emailer.model'
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused'
@ -126,7 +126,6 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload {
copyCodecs: boolean
hasAudio: boolean
isPortraitMode?: boolean
autoDeleteWebTorrentIfNeeded: boolean
isMaxQuality: boolean
@ -138,8 +137,6 @@ export interface NewWebTorrentResolutionTranscodingPayload extends BaseTranscodi
hasAudio: boolean
createHLSIfNeeded: boolean
isPortraitMode?: boolean
}
export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {

View File

@ -310,6 +310,7 @@ export class ConfigCommand extends AbstractCommand {
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: true,
webtorrent: {
enabled: true
},
@ -339,7 +340,8 @@ export class ConfigCommand extends AbstractCommand {
'1080p': true,
'1440p': true,
'2160p': true
}
},
alwaysTranscodeOriginalResolution: true
}
},
videoStudio: {