1
0
Fork 0

Add ability to force transcoding

This commit is contained in:
Chocobozzz 2023-07-28 11:07:03 +02:00
parent ac8f81e373
commit 89aa333110
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
11 changed files with 88 additions and 28 deletions

View File

@ -3,7 +3,7 @@ import { finalize } from 'rxjs/operators'
import { Component, OnInit, ViewChild } from '@angular/core' import { Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
import { formatICU } from '@app/helpers' import { formatICU, getAbsoluteAPIUrl } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms' import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@ -166,7 +166,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
const files = getAllFiles(video) const files = getAllFiles(video)
return files.some(f => !f.fileUrl.startsWith(window.location.origin)) return files.some(f => !f.fileUrl.startsWith(getAbsoluteAPIUrl()))
} }
canRemoveOneFile (video: Video) { canRemoveOneFile (video: Video) {
@ -294,7 +294,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
} }
private runTranscoding (videos: Video[], type: 'hls' | 'web-video') { private runTranscoding (videos: Video[], type: 'hls' | 'web-video') {
this.videoService.runTranscoding(videos.map(v => v.id), type) this.videoService.runTranscoding({ videoIds: videos.map(v => v.id), type, askForForceTranscodingIfNeeded: false })
.subscribe({ .subscribe({
next: () => { next: () => {
this.notifier.success($localize`Transcoding jobs created.`) this.notifier.success($localize`Transcoding jobs created.`)

View File

@ -257,9 +257,12 @@ export class Video implements VideoServerModel {
} }
canRunTranscoding (user: AuthUser) { canRunTranscoding (user: AuthUser) {
return this.canRunForcedTranscoding(user) && this.state.id !== VideoState.TO_TRANSCODE
}
canRunForcedTranscoding (user: AuthUser) {
return this.isLocal && return this.isLocal &&
user && user.hasRight(UserRight.RUN_VIDEO_TRANSCODING) && user && user.hasRight(UserRight.RUN_VIDEO_TRANSCODING)
this.state.id !== VideoState.TO_TRANSCODE
} }
hasHLS () { hasHLS () {

View File

@ -1,9 +1,9 @@
import { SortMeta } from 'primeng/api' import { SortMeta } from 'primeng/api'
import { from, Observable, of } from 'rxjs' import { from, Observable, of, throwError } from 'rxjs'
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' import { AuthService, ComponentPaginationLight, ConfirmService, RestExtractor, RestService, ServerService, UserService } from '@app/core'
import { objectToFormData } from '@app/helpers' import { objectToFormData } from '@app/helpers'
import { arrayify } from '@shared/core-utils' import { arrayify } from '@shared/core-utils'
import { import {
@ -11,6 +11,7 @@ import {
FeedFormat, FeedFormat,
NSFWPolicyType, NSFWPolicyType,
ResultList, ResultList,
ServerErrorCode,
Storyboard, Storyboard,
UserVideoRate, UserVideoRate,
UserVideoRateType, UserVideoRateType,
@ -33,8 +34,8 @@ import { AccountService } from '../account/account.service'
import { VideoChannel, VideoChannelService } from '../video-channel' import { VideoChannel, VideoChannelService } from '../video-channel'
import { VideoDetails } from './video-details.model' import { VideoDetails } from './video-details.model'
import { VideoEdit } from './video-edit.model' import { VideoEdit } from './video-edit.model'
import { Video } from './video.model'
import { VideoPasswordService } from './video-password.service' import { VideoPasswordService } from './video-password.service'
import { Video } from './video.model'
export type CommonVideoParams = { export type CommonVideoParams = {
videoPagination?: ComponentPaginationLight videoPagination?: ComponentPaginationLight
@ -64,7 +65,8 @@ export class VideoService {
private authHttp: HttpClient, private authHttp: HttpClient,
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private restService: RestService, private restService: RestService,
private serverService: ServerService private serverService: ServerService,
private confirmService: ConfirmService
) {} ) {}
getVideoViewUrl (uuid: string) { getVideoViewUrl (uuid: string) {
@ -325,17 +327,53 @@ export class VideoService {
.pipe(catchError(err => this.restExtractor.handleError(err))) .pipe(catchError(err => this.restExtractor.handleError(err)))
} }
runTranscoding (videoIds: (number | string)[], type: 'hls' | 'web-video') { runTranscoding (options: {
const body: VideoTranscodingCreate = { transcodingType: type } videoIds: (number | string)[]
type: 'hls' | 'web-video'
askForForceTranscodingIfNeeded: boolean
forceTranscoding?: boolean
}): Observable<any> {
const { videoIds, type, askForForceTranscodingIfNeeded, forceTranscoding } = options
if (askForForceTranscodingIfNeeded && videoIds.length !== 1) {
throw new Error('Cannot ask to force transcoding on multiple videos')
}
const body: VideoTranscodingCreate = { transcodingType: type, forceTranscoding }
return from(videoIds) return from(videoIds)
.pipe( .pipe(
concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)), concatMap(id => {
return this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)
.pipe(
catchError(err => {
if (askForForceTranscodingIfNeeded && err.error?.code === ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED) {
const message = $localize`PeerTube considers this video is already being transcoded.` +
// eslint-disable-next-line max-len
$localize` If you think PeerTube is wrong (video in broken state after a crash etc.), you can force transcoding on this video.` +
` Do you still want to run transcoding?`
return from(this.confirmService.confirm(message, $localize`Force transcoding`))
.pipe(
switchMap(res => {
if (res === false) return throwError(() => err)
return this.runTranscoding({ videoIds, type, askForForceTranscodingIfNeeded: false, forceTranscoding: true })
})
)
}
return throwError(() => err)
})
)
}),
toArray(), toArray(),
catchError(err => this.restExtractor.handleError(err)) catchError(err => this.restExtractor.handleError(err))
) )
} }
// ---------------------------------------------------------------------------
loadCompleteDescription (descriptionPath: string) { loadCompleteDescription (descriptionPath: string) {
return this.authHttp return this.authHttp
.get<{ description: string }>(environment.apiUrl + descriptionPath) .get<{ description: string }>(environment.apiUrl + descriptionPath)

View File

@ -198,8 +198,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
return this.video.canRemoveFiles(this.user) return this.video.canRemoveFiles(this.user)
} }
canRunTranscoding () { canRunForcedTranscoding () {
return this.video.canRunTranscoding(this.user) return this.video.canRunForcedTranscoding(this.user)
} }
/* Action handlers */ /* Action handlers */
@ -291,10 +291,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
} }
runTranscoding (video: Video, type: 'hls' | 'web-video') { runTranscoding (video: Video, type: 'hls' | 'web-video') {
this.videoService.runTranscoding([ video.id ], type) this.videoService.runTranscoding({ videoIds: [ video.id ], type, askForForceTranscodingIfNeeded: true })
.subscribe({ .subscribe({
next: () => { next: () => {
this.notifier.success($localize`Transcoding jobs created for ${video.name}.`) this.notifier.success($localize`Transcoding jobs created for "${video.name}".`)
this.transcodingCreated.emit() this.transcodingCreated.emit()
}, },
@ -390,13 +390,13 @@ export class VideoActionsDropdownComponent implements OnChanges {
{ {
label: $localize`Run HLS transcoding`, label: $localize`Run HLS transcoding`,
handler: ({ video }) => this.runTranscoding(video, 'hls'), handler: ({ video }) => this.runTranscoding(video, 'hls'),
isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), isDisplayed: () => this.displayOptions.transcoding && this.canRunForcedTranscoding(),
iconName: 'cog' iconName: 'cog'
}, },
{ {
label: $localize`Run Web Video transcoding`, label: $localize`Run Web Video transcoding`,
handler: ({ video }) => this.runTranscoding(video, 'web-video'), handler: ({ video }) => this.runTranscoding(video, 'web-video'),
isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), isDisplayed: () => this.displayOptions.transcoding && this.canRunForcedTranscoding(),
iconName: 'cog' iconName: 'cog'
}, },
{ {

View File

@ -3,6 +3,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job' import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job'
import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions' import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions'
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares'
@ -30,6 +31,8 @@ async function createTranscoding (req: express.Request, res: express.Response) {
const body: VideoTranscodingCreate = req.body const body: VideoTranscodingCreate = req.body
await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscode')
const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile() const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile()
const resolutions = await Hooks.wrapObject( const resolutions = await Hooks.wrapObject(

View File

@ -1,9 +1,10 @@
import express from 'express' import express from 'express'
import { body } from 'express-validator' import { body } from 'express-validator'
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding' import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { VideoJobInfoModel } from '@server/models/video/video-job-info' import { VideoJobInfoModel } from '@server/models/video/video-job-info'
import { HttpStatusCode } from '@shared/models' import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@shared/models'
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
const createTranscodingValidator = [ const createTranscodingValidator = [
@ -12,6 +13,11 @@ const createTranscodingValidator = [
body('transcodingType') body('transcodingType')
.custom(isValidCreateTranscodingType), .custom(isValidCreateTranscodingType),
body('forceTranscoding')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res, 'all')) return if (!await doesVideoExist(req.params.videoId, res, 'all')) return
@ -32,11 +38,14 @@ const createTranscodingValidator = [
}) })
} }
// Prefer using job info table instead of video state because before 4.0 failed transcoded video were stuck in "TO_TRANSCODE" state const body = req.body as VideoTranscodingCreate
if (body.forceTranscoding === true) return next()
const info = await VideoJobInfoModel.load(video.id) const info = await VideoJobInfoModel.load(video.id)
if (info && info.pendingTranscode > 0) { if (info && info.pendingTranscode > 0) {
return res.fail({ return res.fail({
status: HttpStatusCode.CONFLICT_409, status: HttpStatusCode.CONFLICT_409,
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED,
message: 'This video is already being transcoded' message: 'This video is already being transcoded'
}) })
} }

View File

@ -93,15 +93,17 @@ describe('Test transcoding API validators', function () {
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' })
await waitJobs(servers) await waitJobs(servers)
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
await waitJobs(servers) await waitJobs(servers)
}) })
it('Should not run transcoding on a video that is already being transcoded', async function () { it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () {
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' })
const expectedStatus = HttpStatusCode.CONFLICT_409 const expectedStatus = HttpStatusCode.CONFLICT_409
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
}) })
after(async function () { after(async function () {

View File

@ -52,7 +52,9 @@ export const enum ServerErrorCode {
UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token', UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token',
VIDEO_REQUIRES_PASSWORD = 'video_requires_password', VIDEO_REQUIRES_PASSWORD = 'video_requires_password',
INCORRECT_VIDEO_PASSWORD = 'incorrect_video_password' INCORRECT_VIDEO_PASSWORD = 'incorrect_video_password',
VIDEO_ALREADY_BEING_TRANSCODED = 'video_already_being_transcoded'
} }
/** /**

View File

@ -1,3 +1,5 @@
export interface VideoTranscodingCreate { export interface VideoTranscodingCreate {
transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
forceTranscoding?: boolean // Default false
} }

View File

@ -775,19 +775,16 @@ export class VideosCommand extends AbstractCommand {
}) })
} }
runTranscoding (options: OverrideCommandOptions & { runTranscoding (options: OverrideCommandOptions & VideoTranscodingCreate & {
videoId: number | string videoId: number | string
transcodingType: 'hls' | 'webtorrent' | 'web-video'
}) { }) {
const path = '/api/v1/videos/' + options.videoId + '/transcoding' const path = '/api/v1/videos/' + options.videoId + '/transcoding'
const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
return this.postBodyRequest({ return this.postBodyRequest({
...options, ...options,
path, path,
fields, fields: pick(options, [ 'transcodingType', 'forceTranscoding' ]),
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
}) })

View File

@ -4914,6 +4914,10 @@ paths:
enum: enum:
- hls - hls
- web-video - web-video
forceTranscoding:
type: boolean
default: false
description: If the video is stuck in transcoding state, do it anyway
required: required:
- transcodingType - transcodingType
responses: responses: