Add ability to force transcoding
This commit is contained in:
parent
ac8f81e373
commit
89aa333110
11 changed files with 88 additions and 28 deletions
|
@ -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.`)
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export interface VideoTranscodingCreate {
|
||||
transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
|
||||
|
||||
forceTranscoding?: boolean // Default false
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue