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 { ActivatedRoute, Router } from '@angular/router'
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 { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@ -166,7 +166,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
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) {
@ -294,7 +294,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
}
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({
next: () => {
this.notifier.success($localize`Transcoding jobs created.`)

View File

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

View File

@ -1,9 +1,9 @@
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 { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
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 { arrayify } from '@shared/core-utils'
import {
@ -11,6 +11,7 @@ import {
FeedFormat,
NSFWPolicyType,
ResultList,
ServerErrorCode,
Storyboard,
UserVideoRate,
UserVideoRateType,
@ -33,8 +34,8 @@ import { AccountService } from '../account/account.service'
import { VideoChannel, VideoChannelService } from '../video-channel'
import { VideoDetails } from './video-details.model'
import { VideoEdit } from './video-edit.model'
import { Video } from './video.model'
import { VideoPasswordService } from './video-password.service'
import { Video } from './video.model'
export type CommonVideoParams = {
videoPagination?: ComponentPaginationLight
@ -64,7 +65,8 @@ export class VideoService {
private authHttp: HttpClient,
private restExtractor: RestExtractor,
private restService: RestService,
private serverService: ServerService
private serverService: ServerService,
private confirmService: ConfirmService
) {}
getVideoViewUrl (uuid: string) {
@ -325,17 +327,53 @@ export class VideoService {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
runTranscoding (videoIds: (number | string)[], type: 'hls' | 'web-video') {
const body: VideoTranscodingCreate = { transcodingType: type }
runTranscoding (options: {
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)
.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(),
catchError(err => this.restExtractor.handleError(err))
)
}
// ---------------------------------------------------------------------------
loadCompleteDescription (descriptionPath: string) {
return this.authHttp
.get<{ description: string }>(environment.apiUrl + descriptionPath)

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import express from 'express'
import { body } from 'express-validator'
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding'
import { CONFIG } from '@server/initializers/config'
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'
const createTranscodingValidator = [
@ -12,6 +13,11 @@ const createTranscodingValidator = [
body('transcodingType')
.custom(isValidCreateTranscodingType),
body('forceTranscoding')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) 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)
if (info && info.pendingTranscode > 0) {
return res.fail({
status: HttpStatusCode.CONFLICT_409,
type: ServerErrorCode.VIDEO_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 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)
})
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' })
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', forceTranscoding: true })
})
after(async function () {

View File

@ -52,7 +52,9 @@ export const enum ServerErrorCode {
UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token',
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 {
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
transcodingType: 'hls' | 'webtorrent' | 'web-video'
}) {
const path = '/api/v1/videos/' + options.videoId + '/transcoding'
const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
return this.postBodyRequest({
...options,
path,
fields,
fields: pick(options, [ 'transcodingType', 'forceTranscoding' ]),
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})

View File

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