Add ability to force transcoding
This commit is contained in:
parent
ac8f81e373
commit
89aa333110
|
@ -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.`)
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue