Implement video transcoding on server side
This commit is contained in:
parent
f0adb2701c
commit
40298b0254
|
@ -41,7 +41,14 @@ user:
|
|||
video_quota: -1
|
||||
|
||||
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
|
||||
# Uses a lot of CPU!
|
||||
# In addition, if some resolutions are enabled the mp4 video file will be transcoded to these new resolutions.
|
||||
# Uses a lot of CPU and increases storage!
|
||||
transcoding:
|
||||
enabled: false
|
||||
threads: 2
|
||||
resolutions: # Only created if the original video has a higher resolution
|
||||
240p: true
|
||||
360p: true
|
||||
480p: true
|
||||
720p: true
|
||||
1080p: true
|
||||
|
|
|
@ -39,13 +39,12 @@ import {
|
|||
getFormattedObjects,
|
||||
renamePromise
|
||||
} from '../../../helpers'
|
||||
import { TagInstance } from '../../../models'
|
||||
import { VideoCreate, VideoUpdate } from '../../../../shared'
|
||||
import { TagInstance, VideoInstance } from '../../../models'
|
||||
import { VideoCreate, VideoUpdate, VideoResolution } from '../../../../shared'
|
||||
|
||||
import { abuseVideoRouter } from './abuse'
|
||||
import { blacklistRouter } from './blacklist'
|
||||
import { rateVideoRouter } from './rate'
|
||||
import { VideoInstance } from '../../../models/video/video-interface'
|
||||
|
||||
const videosRouter = express.Router()
|
||||
|
||||
|
@ -195,7 +194,7 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
|
|||
.then(({ author, tagInstances, video }) => {
|
||||
const videoFileData = {
|
||||
extname: extname(videoPhysicalFile.filename),
|
||||
resolution: 0, // TODO: improve readability,
|
||||
resolution: VideoResolution.ORIGINAL,
|
||||
size: videoPhysicalFile.size
|
||||
}
|
||||
|
||||
|
@ -230,7 +229,7 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
|
|||
}
|
||||
|
||||
tasks.push(
|
||||
JobScheduler.Instance.createJob(t, 'videoTranscoder', dataInput)
|
||||
JobScheduler.Instance.createJob(t, 'videoFileOptimizer', dataInput)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
rename,
|
||||
unlink,
|
||||
writeFile,
|
||||
access
|
||||
access,
|
||||
stat,
|
||||
Stats
|
||||
} from 'fs'
|
||||
import * as mkdirp from 'mkdirp'
|
||||
import * as bcrypt from 'bcrypt'
|
||||
|
@ -92,6 +94,7 @@ const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
|
|||
const bcryptHashPromise = promisify2<any, string|number, string>(bcrypt.hash)
|
||||
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
|
||||
const rimrafPromise = promisify1WithVoid<string>(rimraf)
|
||||
const statPromise = promisify1<string, Stats>(stat)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -115,5 +118,6 @@ export {
|
|||
bcryptGenSaltPromise,
|
||||
bcryptHashPromise,
|
||||
createTorrentPromise,
|
||||
rimrafPromise
|
||||
rimrafPromise,
|
||||
statPromise
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as Promise from 'bluebird'
|
|||
import { pseudoRandomBytesPromise } from './core-utils'
|
||||
import { CONFIG, database as db } from '../initializers'
|
||||
import { ResultList } from '../../shared'
|
||||
import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
|
||||
|
||||
function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
res.type('json').status(400).end()
|
||||
|
@ -13,11 +14,11 @@ function generateRandomString (size: number) {
|
|||
return pseudoRandomBytesPromise(size).then(raw => raw.toString('hex'))
|
||||
}
|
||||
|
||||
interface FormatableToJSON {
|
||||
interface FormattableToJSON {
|
||||
toFormattedJSON ()
|
||||
}
|
||||
|
||||
function getFormattedObjects<U, T extends FormatableToJSON> (objects: T[], objectsTotal: number) {
|
||||
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) {
|
||||
const formattedObjects: U[] = []
|
||||
|
||||
objects.forEach(object => {
|
||||
|
@ -47,6 +48,27 @@ function isSignupAllowed () {
|
|||
})
|
||||
}
|
||||
|
||||
function computeResolutionsToTranscode (videoFileHeight: number) {
|
||||
const resolutionsEnabled: number[] = []
|
||||
const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
|
||||
|
||||
const resolutions = [
|
||||
VideoResolution.H_240P,
|
||||
VideoResolution.H_360P,
|
||||
VideoResolution.H_480P,
|
||||
VideoResolution.H_720P,
|
||||
VideoResolution.H_1080P
|
||||
]
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
if (configResolutions[resolution.toString()] === true && videoFileHeight >= resolution) {
|
||||
resolutionsEnabled.push(resolution)
|
||||
}
|
||||
}
|
||||
|
||||
return resolutionsEnabled
|
||||
}
|
||||
|
||||
type SortType = { sortModel: any, sortValue: string }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -56,5 +78,6 @@ export {
|
|||
generateRandomString,
|
||||
getFormattedObjects,
|
||||
isSignupAllowed,
|
||||
computeResolutionsToTranscode,
|
||||
SortType
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ import {
|
|||
RequestEndpoint,
|
||||
RequestVideoEventType,
|
||||
RequestVideoQaduType,
|
||||
JobState
|
||||
JobState,
|
||||
VideoResolution
|
||||
} from '../../shared/models'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -85,7 +86,14 @@ const CONFIG = {
|
|||
},
|
||||
TRANSCODING: {
|
||||
ENABLED: config.get<boolean>('transcoding.enabled'),
|
||||
THREADS: config.get<number>('transcoding.threads')
|
||||
THREADS: config.get<number>('transcoding.threads'),
|
||||
RESOLUTIONS: {
|
||||
'240' : config.get<boolean>('transcoding.resolutions.240p'),
|
||||
'360': config.get<boolean>('transcoding.resolutions.360p'),
|
||||
'480': config.get<boolean>('transcoding.resolutions.480p'),
|
||||
'720': config.get<boolean>('transcoding.resolutions.720p'),
|
||||
'1080': config.get<boolean>('transcoding.resolutions.1080p')
|
||||
}
|
||||
},
|
||||
CACHE: {
|
||||
PREVIEWS: {
|
||||
|
@ -144,7 +152,7 @@ const VIDEO_CATEGORIES = {
|
|||
9: 'Comedy',
|
||||
10: 'Entertainment',
|
||||
11: 'News',
|
||||
12: 'Howto',
|
||||
12: 'How To',
|
||||
13: 'Education',
|
||||
14: 'Activism',
|
||||
15: 'Science & Technology',
|
||||
|
@ -179,15 +187,17 @@ const VIDEO_LANGUAGES = {
|
|||
11: 'German',
|
||||
12: 'Korean',
|
||||
13: 'French',
|
||||
14: 'Italien'
|
||||
14: 'Italian'
|
||||
}
|
||||
|
||||
const VIDEO_FILE_RESOLUTIONS = {
|
||||
// TODO: use VideoResolution when https://github.com/Microsoft/TypeScript/issues/13042 is fixed
|
||||
const VIDEO_FILE_RESOLUTIONS: { [ id: number ]: string } = {
|
||||
0: 'original',
|
||||
1: '360p',
|
||||
2: '480p',
|
||||
3: '720p',
|
||||
4: '1080p'
|
||||
240: '240p',
|
||||
360: '360p',
|
||||
480: '480p',
|
||||
720: '720p',
|
||||
1080: '1080p'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -202,7 +212,7 @@ const FRIEND_SCORE = {
|
|||
|
||||
// Number of points we add/remove from a friend after a successful/bad request
|
||||
const PODS_SCORE = {
|
||||
MALUS: -10,
|
||||
PENALTY: -10,
|
||||
BONUS: 10
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as videoTranscoder from './video-transcoder'
|
||||
import * as videoFileOptimizer from './video-file-optimizer'
|
||||
import * as videoFileTranscoder from './video-file-transcoder'
|
||||
|
||||
export interface JobHandler<T> {
|
||||
process (data: object): T
|
||||
|
@ -7,7 +8,8 @@ export interface JobHandler<T> {
|
|||
}
|
||||
|
||||
const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
|
||||
videoTranscoder
|
||||
videoFileOptimizer,
|
||||
videoFileTranscoder
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import * as Promise from 'bluebird'
|
||||
|
||||
import { database as db } from '../../../initializers/database'
|
||||
import { logger, computeResolutionsToTranscode } from '../../../helpers'
|
||||
import { VideoInstance } from '../../../models'
|
||||
import { addVideoToFriends } from '../../friends'
|
||||
import { JobScheduler } from '../job-scheduler'
|
||||
|
||||
function process (data: { videoUUID: string }) {
|
||||
return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => {
|
||||
return video.optimizeOriginalVideofile().then(() => video)
|
||||
})
|
||||
}
|
||||
|
||||
function onError (err: Error, jobId: number) {
|
||||
logger.error('Error when optimized video file in job %d.', jobId, err)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function onSuccess (jobId: number, video: VideoInstance) {
|
||||
logger.info('Job %d is a success.', jobId)
|
||||
|
||||
video.toAddRemoteJSON()
|
||||
.then(remoteVideo => {
|
||||
// Now we'll add the video's meta data to our friends
|
||||
return addVideoToFriends(remoteVideo, null)
|
||||
})
|
||||
.then(() => {
|
||||
return video.getOriginalFileHeight()
|
||||
})
|
||||
.then(originalFileHeight => {
|
||||
// Create transcoding jobs if there are enabled resolutions
|
||||
const resolutionsEnabled = computeResolutionsToTranscode(originalFileHeight)
|
||||
logger.info(
|
||||
'Resolutions computed for video %s and origin file height of %d.', video.uuid, originalFileHeight,
|
||||
{ resolutions: resolutionsEnabled }
|
||||
)
|
||||
|
||||
if (resolutionsEnabled.length === 0) return undefined
|
||||
|
||||
return db.sequelize.transaction(t => {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
resolutionsEnabled.forEach(resolution => {
|
||||
const dataInput = {
|
||||
videoUUID: video.uuid,
|
||||
resolution
|
||||
}
|
||||
|
||||
const p = JobScheduler.Instance.createJob(t, 'videoFileTranscoder', dataInput)
|
||||
tasks.push(p)
|
||||
})
|
||||
|
||||
return Promise.all(tasks).then(() => resolutionsEnabled)
|
||||
})
|
||||
})
|
||||
.then(resolutionsEnabled => {
|
||||
if (resolutionsEnabled === undefined) {
|
||||
logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
|
||||
return undefined
|
||||
}
|
||||
|
||||
logger.info('Transcoding jobs created for uuid %s.', video.uuid, { resolutionsEnabled })
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.debug('Cannot transcode the video.', err)
|
||||
throw err
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
process,
|
||||
onError,
|
||||
onSuccess
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import { database as db } from '../../../initializers/database'
|
||||
import { updateVideoToFriends } from '../../friends'
|
||||
import { logger } from '../../../helpers'
|
||||
import { addVideoToFriends } from '../../../lib'
|
||||
import { VideoInstance } from '../../../models'
|
||||
import { VideoResolution } from '../../../../shared'
|
||||
|
||||
function process (data: { videoUUID: string }) {
|
||||
function process (data: { videoUUID: string, resolution: VideoResolution }) {
|
||||
return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => {
|
||||
// TODO: handle multiple resolutions
|
||||
const videoFile = video.VideoFiles[0]
|
||||
return video.transcodeVideofile(videoFile).then(() => video)
|
||||
return video.transcodeOriginalVideofile(data.resolution).then(() => video)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,10 +18,10 @@ function onError (err: Error, jobId: number) {
|
|||
function onSuccess (jobId: number, video: VideoInstance) {
|
||||
logger.info('Job %d is a success.', jobId)
|
||||
|
||||
video.toAddRemoteJSON().then(remoteVideo => {
|
||||
// Now we'll add the video's meta data to our friends
|
||||
return addVideoToFriends(remoteVideo, null)
|
||||
})
|
||||
const remoteVideo = video.toUpdateRemoteJSON()
|
||||
|
||||
// Now we'll add the video's meta data to our friends
|
||||
return updateVideoToFriends(remoteVideo, null)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
|
@ -219,7 +219,7 @@ updatePodsScore = function (goodPods: number[], badPods: number[]) {
|
|||
}
|
||||
|
||||
if (badPods.length !== 0) {
|
||||
incrementScores(badPods, PODS_SCORE.MALUS)
|
||||
incrementScores(badPods, PODS_SCORE.PENALTY)
|
||||
.then(() => removeBadPods())
|
||||
.catch(err => {
|
||||
if (err) logger.error('Cannot decrement scores of bad pods.', err)
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
isUserDisplayNSFWValid,
|
||||
isUserVideoQuotaValid
|
||||
} from '../../helpers'
|
||||
import { VideoResolution } from '../../../shared'
|
||||
|
||||
import { addMethodsToModel } from '../utils'
|
||||
import {
|
||||
|
@ -245,7 +246,7 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
|
|||
// attributes = [] because we don't want other fields than the sum
|
||||
const query = {
|
||||
where: {
|
||||
resolution: 0 // Original, TODO: improve readability
|
||||
resolution: VideoResolution.ORIGINAL
|
||||
},
|
||||
include: [
|
||||
{
|
||||
|
|
|
@ -7,60 +7,17 @@ import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
|
|||
|
||||
// Don't use barrel, import just what we need
|
||||
import { Video as FormattedVideo } from '../../../shared/models/videos/video.model'
|
||||
import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
|
||||
import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
|
||||
import { ResultList } from '../../../shared/models/result-list.model'
|
||||
|
||||
export type FormattedRemoteVideoFile = {
|
||||
infoHash: string
|
||||
resolution: number
|
||||
extname: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type FormattedAddRemoteVideo = {
|
||||
uuid: string
|
||||
name: string
|
||||
category: number
|
||||
licence: number
|
||||
language: number
|
||||
nsfw: boolean
|
||||
description: string
|
||||
author: string
|
||||
duration: number
|
||||
thumbnailData: string
|
||||
tags: string[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
views: number
|
||||
likes: number
|
||||
dislikes: number
|
||||
files: FormattedRemoteVideoFile[]
|
||||
}
|
||||
|
||||
export type FormattedUpdateRemoteVideo = {
|
||||
uuid: string
|
||||
name: string
|
||||
category: number
|
||||
licence: number
|
||||
language: number
|
||||
nsfw: boolean
|
||||
description: string
|
||||
author: string
|
||||
duration: number
|
||||
tags: string[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
views: number
|
||||
likes: number
|
||||
dislikes: number
|
||||
files: FormattedRemoteVideoFile[]
|
||||
}
|
||||
|
||||
export namespace VideoMethods {
|
||||
export type GetThumbnailName = (this: VideoInstance) => string
|
||||
export type GetPreviewName = (this: VideoInstance) => string
|
||||
export type IsOwned = (this: VideoInstance) => boolean
|
||||
export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
|
||||
|
||||
export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
|
||||
export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
|
||||
export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
|
||||
export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
|
||||
|
@ -69,10 +26,12 @@ export namespace VideoMethods {
|
|||
export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
|
||||
export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
|
||||
|
||||
export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormattedAddRemoteVideo>
|
||||
export type ToUpdateRemoteJSON = (this: VideoInstance) => FormattedUpdateRemoteVideo
|
||||
export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData>
|
||||
export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
|
||||
|
||||
export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise<void>
|
||||
export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
|
||||
export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
|
||||
export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number>
|
||||
|
||||
// Return thumbnail name
|
||||
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
|
||||
|
@ -147,6 +106,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
|
|||
createPreview: VideoMethods.CreatePreview
|
||||
createThumbnail: VideoMethods.CreateThumbnail
|
||||
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
|
||||
getOriginalFile: VideoMethods.GetOriginalFile
|
||||
generateMagnetUri: VideoMethods.GenerateMagnetUri
|
||||
getPreviewName: VideoMethods.GetPreviewName
|
||||
getThumbnailName: VideoMethods.GetThumbnailName
|
||||
|
@ -161,9 +121,12 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
|
|||
toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
|
||||
toFormattedJSON: VideoMethods.ToFormattedJSON
|
||||
toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
|
||||
transcodeVideofile: VideoMethods.TranscodeVideofile
|
||||
optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
|
||||
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
|
||||
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
|
||||
|
||||
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
|
||||
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
|
||||
setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ import {
|
|||
unlinkPromise,
|
||||
renamePromise,
|
||||
writeFilePromise,
|
||||
createTorrentPromise
|
||||
createTorrentPromise,
|
||||
statPromise
|
||||
} from '../../helpers'
|
||||
import {
|
||||
CONFIG,
|
||||
|
@ -35,7 +36,8 @@ import {
|
|||
VIDEO_FILE_RESOLUTIONS
|
||||
} from '../../initializers'
|
||||
import { removeVideoToFriends } from '../../lib'
|
||||
import { VideoFileInstance } from './video-file-interface'
|
||||
import { VideoResolution } from '../../../shared'
|
||||
import { VideoFileInstance, VideoFileModel } from './video-file-interface'
|
||||
|
||||
import { addMethodsToModel, getSort } from '../utils'
|
||||
import {
|
||||
|
@ -46,6 +48,7 @@ import {
|
|||
} from './video-interface'
|
||||
|
||||
let Video: Sequelize.Model<VideoInstance, VideoAttributes>
|
||||
let getOriginalFile: VideoMethods.GetOriginalFile
|
||||
let generateMagnetUri: VideoMethods.GenerateMagnetUri
|
||||
let getVideoFilename: VideoMethods.GetVideoFilename
|
||||
let getThumbnailName: VideoMethods.GetThumbnailName
|
||||
|
@ -55,11 +58,13 @@ let isOwned: VideoMethods.IsOwned
|
|||
let toFormattedJSON: VideoMethods.ToFormattedJSON
|
||||
let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
|
||||
let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
|
||||
let transcodeVideofile: VideoMethods.TranscodeVideofile
|
||||
let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
|
||||
let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
|
||||
let createPreview: VideoMethods.CreatePreview
|
||||
let createThumbnail: VideoMethods.CreateThumbnail
|
||||
let getVideoFilePath: VideoMethods.GetVideoFilePath
|
||||
let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
|
||||
let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
|
||||
|
||||
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
|
||||
let getDurationFromFile: VideoMethods.GetDurationFromFile
|
||||
|
@ -251,6 +256,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
|
|||
getTorrentFileName,
|
||||
getVideoFilename,
|
||||
getVideoFilePath,
|
||||
getOriginalFile,
|
||||
isOwned,
|
||||
removeFile,
|
||||
removePreview,
|
||||
|
@ -259,7 +265,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
|
|||
toAddRemoteJSON,
|
||||
toFormattedJSON,
|
||||
toUpdateRemoteJSON,
|
||||
transcodeVideofile
|
||||
optimizeOriginalVideofile,
|
||||
transcodeOriginalVideofile,
|
||||
getOriginalFileHeight
|
||||
]
|
||||
addMethodsToModel(Video, classMethods, instanceMethods)
|
||||
|
||||
|
@ -327,9 +335,14 @@ function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.T
|
|||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
getOriginalFile = function (this: VideoInstance) {
|
||||
if (Array.isArray(this.VideoFiles) === false) return undefined
|
||||
|
||||
return this.VideoFiles.find(file => file.resolution === VideoResolution.ORIGINAL)
|
||||
}
|
||||
|
||||
getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
||||
// return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
|
||||
return this.uuid + videoFile.extname
|
||||
return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
|
||||
}
|
||||
|
||||
getThumbnailName = function (this: VideoInstance) {
|
||||
|
@ -345,8 +358,7 @@ getPreviewName = function (this: VideoInstance) {
|
|||
|
||||
getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
||||
const extension = '.torrent'
|
||||
// return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
|
||||
return this.uuid + extension
|
||||
return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
|
||||
}
|
||||
|
||||
isOwned = function (this: VideoInstance) {
|
||||
|
@ -552,9 +564,10 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
|
|||
return json
|
||||
}
|
||||
|
||||
transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
|
||||
optimizeOriginalVideofile = function (this: VideoInstance) {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const newExtname = '.mp4'
|
||||
const inputVideoFile = this.getOriginalFile()
|
||||
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
|
||||
const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
|
||||
|
||||
|
@ -574,6 +587,12 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
|
|||
|
||||
return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
|
||||
})
|
||||
.then(() => {
|
||||
return statPromise(this.getVideoFilePath(inputVideoFile))
|
||||
})
|
||||
.then(stats => {
|
||||
return inputVideoFile.set('size', stats.size)
|
||||
})
|
||||
.then(() => {
|
||||
return this.createTorrentAndSetInfoHash(inputVideoFile)
|
||||
})
|
||||
|
@ -594,6 +613,74 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
|
|||
})
|
||||
}
|
||||
|
||||
transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const extname = '.mp4'
|
||||
|
||||
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
|
||||
const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
|
||||
|
||||
const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
|
||||
resolution,
|
||||
extname,
|
||||
size: 0,
|
||||
videoId: this.id
|
||||
})
|
||||
const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
|
||||
const resolutionWidthSizes = {
|
||||
1: '240x?',
|
||||
2: '360x?',
|
||||
3: '480x?',
|
||||
4: '720x?',
|
||||
5: '1080x?'
|
||||
}
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
ffmpeg(videoInputPath)
|
||||
.output(videoOutputPath)
|
||||
.videoCodec('libx264')
|
||||
.size(resolutionWidthSizes[resolution])
|
||||
.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
|
||||
.outputOption('-movflags faststart')
|
||||
.on('error', rej)
|
||||
.on('end', () => {
|
||||
return statPromise(videoOutputPath)
|
||||
.then(stats => {
|
||||
newVideoFile.set('size', stats.size)
|
||||
|
||||
return undefined
|
||||
})
|
||||
.then(() => {
|
||||
return this.createTorrentAndSetInfoHash(newVideoFile)
|
||||
})
|
||||
.then(() => {
|
||||
return newVideoFile.save()
|
||||
})
|
||||
.then(() => {
|
||||
return this.VideoFiles.push(newVideoFile)
|
||||
})
|
||||
.then(() => {
|
||||
return res()
|
||||
})
|
||||
.catch(rej)
|
||||
})
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
getOriginalFileHeight = function (this: VideoInstance) {
|
||||
const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
|
||||
|
||||
return new Promise<number>((res, rej) => {
|
||||
ffmpeg.ffprobe(originalFilePath, (err, metadata) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
|
||||
return res(videoStream.height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
removeThumbnail = function (this: VideoInstance) {
|
||||
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
|
||||
return unlinkPromise(thumbnailPath)
|
||||
|
|
|
@ -129,7 +129,7 @@ describe('Test multiple pods', function () {
|
|||
})
|
||||
|
||||
it('Should upload the video on pod 2 and propagate on each pod', async function () {
|
||||
this.timeout(60000)
|
||||
this.timeout(120000)
|
||||
|
||||
const videoAttributes = {
|
||||
name: 'my super name for pod 2',
|
||||
|
@ -143,12 +143,12 @@ describe('Test multiple pods', function () {
|
|||
}
|
||||
await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
|
||||
|
||||
// Transcoding, so wait more that 22 seconds
|
||||
await wait(42000)
|
||||
// Transcoding, so wait more than 22000
|
||||
await wait(60000)
|
||||
|
||||
// All pods should have this video
|
||||
for (const server of servers) {
|
||||
let baseMagnet = null
|
||||
let baseMagnet = {}
|
||||
|
||||
const res = await getVideosList(server.url)
|
||||
|
||||
|
@ -172,27 +172,50 @@ describe('Test multiple pods', function () {
|
|||
expect(dateIsValid(video.updatedAt)).to.be.true
|
||||
expect(video.author).to.equal('root')
|
||||
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.files).to.have.lengthOf(5)
|
||||
|
||||
const file = video.files[0]
|
||||
const magnetUri = file.magnetUri
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
expect(file.resolution).to.equal(0)
|
||||
expect(file.resolutionLabel).to.equal('original')
|
||||
expect(file.size).to.equal(942961)
|
||||
// Check common attributes
|
||||
for (const file of video.files) {
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
|
||||
if (server.url !== 'http://localhost:9002') {
|
||||
expect(video.isLocal).to.be.false
|
||||
} else {
|
||||
expect(video.isLocal).to.be.true
|
||||
if (server.url !== 'http://localhost:9002') {
|
||||
expect(video.isLocal).to.be.false
|
||||
} else {
|
||||
expect(video.isLocal).to.be.true
|
||||
}
|
||||
|
||||
// All pods should have the same magnet Uri
|
||||
if (baseMagnet[file.resolution] === undefined) {
|
||||
baseMagnet[file.resolution] = file.magnet
|
||||
} else {
|
||||
expect(baseMagnet[file.resolution]).to.equal(file.magnet)
|
||||
}
|
||||
}
|
||||
|
||||
// All pods should have the same magnet Uri
|
||||
if (baseMagnet === null) {
|
||||
baseMagnet = magnetUri
|
||||
} else {
|
||||
expect(baseMagnet).to.equal(magnetUri)
|
||||
}
|
||||
const originalFile = video.files.find(f => f.resolution === 0)
|
||||
expect(originalFile).not.to.be.undefined
|
||||
expect(originalFile.resolutionLabel).to.equal('original')
|
||||
expect(originalFile.size).to.equal(711327)
|
||||
|
||||
const file240p = video.files.find(f => f.resolution === 1)
|
||||
expect(file240p).not.to.be.undefined
|
||||
expect(file240p.resolutionLabel).to.equal('240p')
|
||||
expect(file240p.size).to.equal(139953)
|
||||
|
||||
const file360p = video.files.find(f => f.resolution === 2)
|
||||
expect(file360p).not.to.be.undefined
|
||||
expect(file360p.resolutionLabel).to.equal('360p')
|
||||
expect(file360p.size).to.equal(169926)
|
||||
|
||||
const file480p = video.files.find(f => f.resolution === 3)
|
||||
expect(file480p).not.to.be.undefined
|
||||
expect(file480p.resolutionLabel).to.equal('480p')
|
||||
expect(file480p.size).to.equal(206758)
|
||||
|
||||
const file720p = video.files.find(f => f.resolution === 4)
|
||||
expect(file720p).not.to.be.undefined
|
||||
expect(file720p.resolutionLabel).to.equal('720p')
|
||||
expect(file720p.size).to.equal(314913)
|
||||
|
||||
const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath)
|
||||
expect(test).to.equal(true)
|
||||
|
|
|
@ -42,6 +42,8 @@ describe('Test video transcoding', function () {
|
|||
|
||||
const res = await getVideosList(servers[0].url)
|
||||
const video = res.body.data[0]
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
|
||||
const magnetUri = video.files[0].magnetUri
|
||||
expect(magnetUri).to.match(/\.webm/)
|
||||
|
||||
|
@ -66,6 +68,8 @@ describe('Test video transcoding', function () {
|
|||
const res = await getVideosList(servers[1].url)
|
||||
|
||||
const video = res.body.data[0]
|
||||
expect(video.files).to.have.lengthOf(5)
|
||||
|
||||
const magnetUri = video.files[0].magnetUri
|
||||
expect(magnetUri).to.match(/\.mp4/)
|
||||
|
||||
|
|
|
@ -12,14 +12,15 @@ import {
|
|||
runServer,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
uploadVideo
|
||||
uploadVideo,
|
||||
wait
|
||||
} from '../utils'
|
||||
|
||||
describe('Test update host scripts', function () {
|
||||
let server: ServerInfo
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
this.timeout(60000)
|
||||
|
||||
await flushTests()
|
||||
|
||||
|
@ -28,36 +29,43 @@ describe('Test update host scripts', function () {
|
|||
port: 9256
|
||||
}
|
||||
}
|
||||
server = await runServer(1, overrideConfig)
|
||||
// Run server 2 to have transcoding enabled
|
||||
server = await runServer(2, overrideConfig)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
// Upload two videos for our needs
|
||||
const videoAttributes = {}
|
||||
await uploadVideo(server.url, server.accessToken, videoAttributes)
|
||||
await uploadVideo(server.url, server.accessToken, videoAttributes)
|
||||
await wait(30000)
|
||||
})
|
||||
|
||||
it('Should update torrent hosts', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
killallServers([ server ])
|
||||
server = await runServer(1)
|
||||
// Run server with standard configuration
|
||||
server = await runServer(2)
|
||||
|
||||
const env = getEnvCli(server)
|
||||
await execCLI(`${env} npm run update-host`)
|
||||
|
||||
const res = await getVideosList(server.url)
|
||||
const videos = res.body.data
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
||||
expect(videos[0].files[0].magnetUri).to.contain('localhost%3A9001%2Ftracker%2Fsocket')
|
||||
expect(videos[0].files[0].magnetUri).to.contain('localhost%3A9001%2Fstatic%2Fwebseed%2F')
|
||||
for (const video of videos) {
|
||||
expect(video.files).to.have.lengthOf(5)
|
||||
|
||||
expect(videos[1].files[0].magnetUri).to.contain('localhost%3A9001%2Ftracker%2Fsocket')
|
||||
expect(videos[1].files[0].magnetUri).to.contain('localhost%3A9001%2Fstatic%2Fwebseed%2F')
|
||||
for (const file of video.files) {
|
||||
expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket')
|
||||
expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2Fwebseed%2F')
|
||||
|
||||
const torrent = await parseTorrentVideo(server, videos[0].uuid)
|
||||
expect(torrent.announce[0]).to.equal('ws://localhost:9001/tracker/socket')
|
||||
expect(torrent.urlList[0]).to.contain('http://localhost:9001/static/webseed')
|
||||
const torrent = await parseTorrentVideo(server, video.uuid, file.resolutionLabel)
|
||||
expect(torrent.announce[0]).to.equal('ws://localhost:9002/tracker/socket')
|
||||
expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
|
|
|
@ -238,9 +238,10 @@ function rateVideo (url: string, accessToken: string, id: number, rating: string
|
|||
.expect(specialStatus)
|
||||
}
|
||||
|
||||
function parseTorrentVideo (server: ServerInfo, videoUUID: string) {
|
||||
function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolutionLabel: string) {
|
||||
return new Promise<any>((res, rej) => {
|
||||
const torrentPath = join(__dirname, '..', '..', '..', 'test' + server.serverNumber, 'torrents', videoUUID + '.torrent')
|
||||
const torrentName = videoUUID + '-' + resolutionLabel + '.torrent'
|
||||
const torrentPath = join(__dirname, '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName)
|
||||
readFile(torrentPath, (err, data) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
|
|
|
@ -2,8 +2,6 @@ export interface RemoteVideoUpdateData {
|
|||
uuid: string
|
||||
tags: string[]
|
||||
name: string
|
||||
extname: string
|
||||
infoHash: string
|
||||
category: number
|
||||
licence: number
|
||||
language: number
|
||||
|
|
|
@ -6,5 +6,6 @@ export * from './video-abuse.model'
|
|||
export * from './video-blacklist.model'
|
||||
export * from './video-create.model'
|
||||
export * from './video-rate.type'
|
||||
export * from './video-resolution.enum'
|
||||
export * from './video-update.model'
|
||||
export * from './video.model'
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export enum VideoResolution {
|
||||
ORIGINAL = 0,
|
||||
H_240P = 240,
|
||||
H_360P = 360,
|
||||
H_480P = 480,
|
||||
H_720P = 720,
|
||||
H_1080P = 1080
|
||||
}
|
Loading…
Reference in New Issue