Add ability to disable storyboards

pull/6157/head
Chocobozzz 2023-12-27 10:39:09 +01:00
parent 482223cc23
commit b9077c83fc
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 131 additions and 60 deletions

View File

@ -357,6 +357,20 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container formGroupName="storyboards">
<div class="form-group">
<my-peertube-checkbox
inputName="storyboardsEnabled" formControlName="enabled"
i18n-labelText labelText="Enable video storyboards"
>
<ng-container ngProjectAs="description">
<span i18n>Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video</span>
</ng-container>
</my-peertube-checkbox>
</div>
</ng-container>
</div> </div>
</div> </div>

View File

@ -274,6 +274,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
instanceCustomHomepage: { instanceCustomHomepage: {
content: null content: null
},
storyboards: {
enabled: null
} }
} }

View File

@ -873,3 +873,7 @@ client:
# If you enable only one external auth plugin # If you enable only one external auth plugin
# You can automatically redirect your users on this external platform when they click on the login button # You can automatically redirect your users on this external platform when they click on the login button
redirect_on_single_external_auth: false redirect_on_single_external_auth: false
storyboards:
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
enabled: true

View File

@ -883,3 +883,7 @@ client:
# If you enable only one external auth plugin # If you enable only one external auth plugin
# You can automatically redirect your users on this external platform when they click on the login button # You can automatically redirect your users on this external platform when they click on the login button
redirect_on_single_external_auth: false redirect_on_single_external_auth: false
storyboards:
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
enabled: true

View File

@ -257,4 +257,8 @@ export interface CustomConfig {
} }
} }
storyboards: {
enabled: boolean
}
} }

View File

@ -321,6 +321,10 @@ export interface ServerConfig {
} }
} }
} }
storyboards: {
enabled: boolean
}
} }
export type HTMLServerConfig = Omit<ServerConfig, 'signup'> export type HTMLServerConfig = Omit<ServerConfig, 'signup'>

View File

@ -567,6 +567,9 @@ export class ConfigCommand extends AbstractCommand {
disableLocalSearch: true, disableLocalSearch: true,
isDefaultSearch: true isDefaultSearch: true
} }
},
storyboards: {
enabled: true
} }
} }

View File

@ -16,6 +16,7 @@ describe('Test config API validators', function () {
const path = '/api/v1/config/custom' const path = '/api/v1/config/custom'
let server: PeerTubeServer let server: PeerTubeServer
let userAccessToken: string let userAccessToken: string
const updateParams: CustomConfig = { const updateParams: CustomConfig = {
instance: { instance: {
name: 'PeerTube updated', name: 'PeerTube updated',
@ -240,6 +241,9 @@ describe('Test config API validators', function () {
disableLocalSearch: true, disableLocalSearch: true,
isDefaultSearch: true isDefaultSearch: true
} }
},
storyboards: {
enabled: false
} }
} }

View File

@ -123,6 +123,8 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.broadcastMessage.level).to.equal('info') expect(data.broadcastMessage.level).to.equal('info')
expect(data.broadcastMessage.message).to.equal('') expect(data.broadcastMessage.message).to.equal('')
expect(data.broadcastMessage.dismissable).to.be.false expect(data.broadcastMessage.dismissable).to.be.false
expect(data.storyboards.enabled).to.be.true
} }
function checkUpdatedConfig (data: CustomConfig) { function checkUpdatedConfig (data: CustomConfig) {
@ -236,6 +238,8 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.broadcastMessage.level).to.equal('error') expect(data.broadcastMessage.level).to.equal('error')
expect(data.broadcastMessage.message).to.equal('super bad message') expect(data.broadcastMessage.message).to.equal('super bad message')
expect(data.broadcastMessage.dismissable).to.be.true expect(data.broadcastMessage.dismissable).to.be.true
expect(data.storyboards.enabled).to.be.false
} }
const newCustomConfig: CustomConfig = { const newCustomConfig: CustomConfig = {
@ -460,6 +464,9 @@ const newCustomConfig: CustomConfig = {
disableLocalSearch: true, disableLocalSearch: true,
isDefaultSearch: true isDefaultSearch: true
} }
},
storyboards: {
enabled: false
} }
} }

View File

@ -209,6 +209,27 @@ describe('Test video storyboard', function () {
} }
}) })
it('Should not generate storyboards if disabled by the admin', async function () {
this.timeout(60000)
await servers[0].config.updateExistingSubConfig({
newConfig: {
storyboards: {
enabled: false
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' })
await waitJobs(servers)
for (const server of servers) {
const { storyboards } = await server.storyboard.list({ id: uuid })
expect(storyboards).to.have.lengthOf(0)
}
})
after(async function () { after(async function () {
await cleanupTests(servers) await cleanupTests(servers)
}) })

View File

@ -355,6 +355,9 @@ function customConfig (): CustomConfig {
disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
} }
},
storyboards: {
enabled: CONFIG.STORYBOARDS.ENABLED
} }
} }
} }

View File

@ -5,7 +5,7 @@ import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-q
import { Hooks } from '@server/lib/plugins/hooks.js' import { Hooks } from '@server/lib/plugins/hooks.js'
import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js' import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
import { uploadx } from '@server/lib/uploadx.js' import { uploadx } from '@server/lib/uploadx.js'
import { buildMoveJob } from '@server/lib/video.js' import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video.js'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js' import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
import { buildNewFile } from '@server/lib/video-file.js' import { buildNewFile } from '@server/lib/video-file.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js' import { VideoPathManager } from '@server/lib/video-path-manager.js'
@ -152,14 +152,7 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide
} }
}, },
{ buildStoryboardJobIfNeeded({ video, federate: false }),
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
// No need to federate, we process these jobs sequentially
federate: false
}
},
{ {
type: 'federate-video' as 'federate-video', type: 'federate-video' as 'federate-video',

View File

@ -6,7 +6,7 @@ import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js'
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js' import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
import { Redis } from '@server/lib/redis.js' import { Redis } from '@server/lib/redis.js'
import { uploadx } from '@server/lib/uploadx.js' import { uploadx } from '@server/lib/uploadx.js'
import { buildLocalVideoFromReq, buildMoveJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video.js' import { buildLocalVideoFromReq, buildMoveJob, buildStoryboardJobIfNeeded, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video.js'
import { buildNewFile } from '@server/lib/video-file.js' import { buildNewFile } from '@server/lib/video-file.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js' import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { buildNextVideoState } from '@server/lib/video-state.js' import { buildNextVideoState } from '@server/lib/video-state.js'
@ -248,14 +248,7 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide
} }
}, },
{ buildStoryboardJobIfNeeded({ video, federate: false }),
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
// No need to federate, we process these jobs sequentially
federate: false
}
},
{ {
type: 'notify', type: 'notify',

View File

@ -84,7 +84,8 @@ function checkMissedConfig () {
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', '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.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution', 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution',
'live.transcoding.remote_runners.enabled' 'live.transcoding.remote_runners.enabled',
'storyboards.enabled'
] ]
const requiredAlternatives = [ const requiredAlternatives = [

View File

@ -610,8 +610,10 @@ const CONFIG = {
get DISABLE_LOCAL_SEARCH () { return config.get<boolean>('search.search_index.disable_local_search') }, get DISABLE_LOCAL_SEARCH () { return config.get<boolean>('search.search_index.disable_local_search') },
get IS_DEFAULT_SEARCH () { return config.get<boolean>('search.search_index.is_default_search') } get IS_DEFAULT_SEARCH () { return config.get<boolean>('search.search_index.is_default_search') }
} }
},
STORYBOARDS: {
get ENABLED () { return config.get<boolean>('storyboards.enabled') }
} }
} }
function registerConfigChangedHandler (fun: Function) { function registerConfigChangedHandler (fun: Function) {
@ -682,7 +684,7 @@ export function reloadConfig () {
return process.env.NODE_CONFIG_DIR.split(':') return process.env.NODE_CONFIG_DIR.split(':')
} }
return [ join(root(), 'config') ] return [join(root(), 'config')]
} }
function purge () { function purge () {

View File

@ -25,7 +25,7 @@ import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-t
import { isAbleToUploadVideo } from '@server/lib/user.js' import { isAbleToUploadVideo } from '@server/lib/user.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js' import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { buildNextVideoState } from '@server/lib/video-state.js' import { buildNextVideoState } from '@server/lib/video-state.js'
import { buildMoveJob } from '@server/lib/video.js' import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video.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 { getLowercaseExtension } from '@peertube/peertube-node-utils'
@ -307,13 +307,7 @@ async function afterImportSuccess (options: {
} }
// Generate the storyboard in the job queue, and don't forget to federate an update after // Generate the storyboard in the job queue, and don't forget to federate an update after
await JobQueue.Instance.createJob({ await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: true }))
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
federate: true
}
})
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
await JobQueue.Instance.createJob( await JobQueue.Instance.createJob(

View File

@ -30,6 +30,7 @@ import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoS
import { logger, loggerTagsFactory } from '../../../helpers/logger.js' import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { JobQueue } from '../job-queue.js' import { JobQueue } from '../job-queue.js'
import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js' import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js'
import { buildStoryboardJobIfNeeded } from '@server/lib/video.js'
const lTags = loggerTagsFactory('live', 'job') const lTags = loggerTagsFactory('live', 'job')
@ -302,11 +303,5 @@ async function cleanupLiveAndFederate (options: {
} }
function createStoryboardJob (video: MVideo) { function createStoryboardJob (video: MVideo) {
return JobQueue.Instance.createJob({ return JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: true }))
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
federate: true
}
})
} }

View File

@ -336,7 +336,9 @@ class JobQueue {
.catch(err => logger.error('Cannot create job.', { err, options })) .catch(err => logger.error('Cannot create job.', { err, options }))
} }
createJob (options: CreateJobArgument & CreateJobOptions) { createJob (options: CreateJobArgument & CreateJobOptions | undefined) {
if (!options) return
const queue: Queue = this.queues[options.type] const queue: Queue = this.queues[options.type]
if (queue === undefined) { if (queue === undefined) {
logger.error('Unknown queue %s: cannot create job.', options.type) logger.error('Unknown queue %s: cannot create job.', options.type)

View File

@ -290,6 +290,10 @@ class ServerConfigManager {
users: CONFIG.VIEWS.VIDEOS.WATCHING_INTERVAL.USERS users: CONFIG.VIEWS.VIDEOS.WATCHING_INTERVAL.USERS
} }
} }
},
storyboards: {
enabled: CONFIG.STORYBOARDS.ENABLED
} }
} }
} }

View File

@ -16,6 +16,7 @@ import { buildFileMetadata } 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.js'
// 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: {
@ -247,14 +248,7 @@ export async function onWebVideoFileTranscoding (options: {
video.VideoFiles = await video.$get('VideoFiles') video.VideoFiles = await video.$get('VideoFiles')
if (wasAudioFile) { if (wasAudioFile) {
await JobQueue.Instance.createJob({ await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video, federate: false }))
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
// No need to federate, we process these jobs sequentially
federate: false
}
})
} }
return { video, videoFile } return { video, videoFile }

View File

@ -11,6 +11,7 @@ import { VideoStudioTranscodingJobHandler } from './runners/index.js'
import { getTranscodingJobPriority } from './transcoding/transcoding-priority.js' 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.js'
const lTags = loggerTagsFactory('video-studio') const lTags = loggerTagsFactory('video-studio')
@ -106,13 +107,7 @@ export async function onVideoStudioEnded (options: {
await video.save() await video.save()
return JobQueue.Instance.createSequentialJobFlow( return JobQueue.Instance.createSequentialJobFlow(
{ buildStoryboardJobIfNeeded({ video, federate: false }),
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
federate: false
}
},
{ {
type: 'federate-video' as 'federate-video', type: 'federate-video' as 'federate-video',

View File

@ -16,7 +16,7 @@ import { TagModel } from '@server/models/video/tag.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js' import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { FilteredModelAttributes } from '@server/types/index.js' import { FilteredModelAttributes } from '@server/types/index.js'
import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models/index.js' import { MThumbnail, MVideo, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models/index.js'
import { CreateJobArgument, JobQueue } from './job-queue/job-queue.js' import { CreateJobArgument, JobQueue } from './job-queue/job-queue.js'
import { updateLocalVideoMiniatureFromExisting } from './thumbnail.js' import { updateLocalVideoMiniatureFromExisting } from './thumbnail.js'
import { moveFilesIfPrivacyChanged } from './video-privacy.js' import { moveFilesIfPrivacyChanged } from './video-privacy.js'
@ -117,6 +117,37 @@ export async function buildMoveJob (options: {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function buildStoryboardJobIfNeeded (options: {
video: MVideo
federate: boolean
}) {
const { video, federate } = options
if (CONFIG.STORYBOARDS.ENABLED) {
return {
type: 'generate-video-storyboard' as 'generate-video-storyboard',
payload: {
videoUUID: video.uuid,
federate
}
}
}
if (federate === true) {
return {
type: 'federate-video' as 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideoForFederation: false
}
}
}
return undefined
}
// ---------------------------------------------------------------------------
export async function getVideoDuration (videoId: number | string) { export async function getVideoDuration (videoId: number | string) {
const video = await VideoModel.load(videoId) const video = await VideoModel.load(videoId)

View File

@ -4,6 +4,7 @@ import { initDatabaseModels } from '@server/initializers/database.js'
import { JobQueue } from '@server/lib/job-queue/index.js' import { JobQueue } from '@server/lib/job-queue/index.js'
import { StoryboardModel } from '@server/models/video/storyboard.js' import { StoryboardModel } from '@server/models/video/storyboard.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { buildStoryboardJobIfNeeded } from '@server/lib/video.js'
program program
.description('Generate videos storyboard') .description('Generate videos storyboard')
@ -60,13 +61,7 @@ async function run () {
if (videoFull.isLive) continue if (videoFull.isLive) continue
await JobQueue.Instance.createJob({ await JobQueue.Instance.createJob(buildStoryboardJobIfNeeded({ video: videoFull, federate: true }))
type: 'generate-video-storyboard',
payload: {
videoUUID: videoFull.uuid,
federate: true
}
})
console.log(`Created generate-storyboard job for ${videoFull.name}.`) console.log(`Created generate-storyboard job for ${videoFull.name}.`)
} }