1
0
Fork 0

Implement video transcoding on server side

This commit is contained in:
Chocobozzz 2017-10-02 12:20:26 +02:00
parent f0adb2701c
commit 40298b0254
19 changed files with 344 additions and 128 deletions

View file

@ -41,7 +41,14 @@ user:
video_quota: -1 video_quota: -1
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag # 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: transcoding:
enabled: false enabled: false
threads: 2 threads: 2
resolutions: # Only created if the original video has a higher resolution
240p: true
360p: true
480p: true
720p: true
1080p: true

View file

@ -39,13 +39,12 @@ import {
getFormattedObjects, getFormattedObjects,
renamePromise renamePromise
} from '../../../helpers' } from '../../../helpers'
import { TagInstance } from '../../../models' import { TagInstance, VideoInstance } from '../../../models'
import { VideoCreate, VideoUpdate } from '../../../../shared' import { VideoCreate, VideoUpdate, VideoResolution } from '../../../../shared'
import { abuseVideoRouter } from './abuse' import { abuseVideoRouter } from './abuse'
import { blacklistRouter } from './blacklist' import { blacklistRouter } from './blacklist'
import { rateVideoRouter } from './rate' import { rateVideoRouter } from './rate'
import { VideoInstance } from '../../../models/video/video-interface'
const videosRouter = express.Router() const videosRouter = express.Router()
@ -195,7 +194,7 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
.then(({ author, tagInstances, video }) => { .then(({ author, tagInstances, video }) => {
const videoFileData = { const videoFileData = {
extname: extname(videoPhysicalFile.filename), extname: extname(videoPhysicalFile.filename),
resolution: 0, // TODO: improve readability, resolution: VideoResolution.ORIGINAL,
size: videoPhysicalFile.size size: videoPhysicalFile.size
} }
@ -230,7 +229,7 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
} }
tasks.push( tasks.push(
JobScheduler.Instance.createJob(t, 'videoTranscoder', dataInput) JobScheduler.Instance.createJob(t, 'videoFileOptimizer', dataInput)
) )
} }

View file

@ -11,7 +11,9 @@ import {
rename, rename,
unlink, unlink,
writeFile, writeFile,
access access,
stat,
Stats
} from 'fs' } from 'fs'
import * as mkdirp from 'mkdirp' import * as mkdirp from 'mkdirp'
import * as bcrypt from 'bcrypt' 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 bcryptHashPromise = promisify2<any, string|number, string>(bcrypt.hash)
const createTorrentPromise = promisify2<string, any, any>(createTorrent) const createTorrentPromise = promisify2<string, any, any>(createTorrent)
const rimrafPromise = promisify1WithVoid<string>(rimraf) const rimrafPromise = promisify1WithVoid<string>(rimraf)
const statPromise = promisify1<string, Stats>(stat)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -115,5 +118,6 @@ export {
bcryptGenSaltPromise, bcryptGenSaltPromise,
bcryptHashPromise, bcryptHashPromise,
createTorrentPromise, createTorrentPromise,
rimrafPromise rimrafPromise,
statPromise
} }

View file

@ -4,6 +4,7 @@ import * as Promise from 'bluebird'
import { pseudoRandomBytesPromise } from './core-utils' import { pseudoRandomBytesPromise } from './core-utils'
import { CONFIG, database as db } from '../initializers' import { CONFIG, database as db } from '../initializers'
import { ResultList } from '../../shared' import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
res.type('json').status(400).end() res.type('json').status(400).end()
@ -13,11 +14,11 @@ function generateRandomString (size: number) {
return pseudoRandomBytesPromise(size).then(raw => raw.toString('hex')) return pseudoRandomBytesPromise(size).then(raw => raw.toString('hex'))
} }
interface FormatableToJSON { interface FormattableToJSON {
toFormattedJSON () toFormattedJSON ()
} }
function getFormattedObjects<U, T extends FormatableToJSON> (objects: T[], objectsTotal: number) { function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) {
const formattedObjects: U[] = [] const formattedObjects: U[] = []
objects.forEach(object => { 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 } type SortType = { sortModel: any, sortValue: string }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -56,5 +78,6 @@ export {
generateRandomString, generateRandomString,
getFormattedObjects, getFormattedObjects,
isSignupAllowed, isSignupAllowed,
computeResolutionsToTranscode,
SortType SortType
} }

View file

@ -10,7 +10,8 @@ import {
RequestEndpoint, RequestEndpoint,
RequestVideoEventType, RequestVideoEventType,
RequestVideoQaduType, RequestVideoQaduType,
JobState JobState,
VideoResolution
} from '../../shared/models' } from '../../shared/models'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -85,7 +86,14 @@ const CONFIG = {
}, },
TRANSCODING: { TRANSCODING: {
ENABLED: config.get<boolean>('transcoding.enabled'), 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: { CACHE: {
PREVIEWS: { PREVIEWS: {
@ -144,7 +152,7 @@ const VIDEO_CATEGORIES = {
9: 'Comedy', 9: 'Comedy',
10: 'Entertainment', 10: 'Entertainment',
11: 'News', 11: 'News',
12: 'Howto', 12: 'How To',
13: 'Education', 13: 'Education',
14: 'Activism', 14: 'Activism',
15: 'Science & Technology', 15: 'Science & Technology',
@ -179,15 +187,17 @@ const VIDEO_LANGUAGES = {
11: 'German', 11: 'German',
12: 'Korean', 12: 'Korean',
13: 'French', 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', 0: 'original',
1: '360p', 240: '240p',
2: '480p', 360: '360p',
3: '720p', 480: '480p',
4: '1080p' 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 // Number of points we add/remove from a friend after a successful/bad request
const PODS_SCORE = { const PODS_SCORE = {
MALUS: -10, PENALTY: -10,
BONUS: 10 BONUS: 10
} }

View file

@ -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> { export interface JobHandler<T> {
process (data: object): T process (data: object): T
@ -7,7 +8,8 @@ export interface JobHandler<T> {
} }
const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = { const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
videoTranscoder videoFileOptimizer,
videoFileTranscoder
} }
export { export {

View file

@ -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
}

View file

@ -1,13 +1,12 @@
import { database as db } from '../../../initializers/database' import { database as db } from '../../../initializers/database'
import { updateVideoToFriends } from '../../friends'
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { addVideoToFriends } from '../../../lib'
import { VideoInstance } from '../../../models' 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 => { return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => {
// TODO: handle multiple resolutions return video.transcodeOriginalVideofile(data.resolution).then(() => video)
const videoFile = video.VideoFiles[0]
return video.transcodeVideofile(videoFile).then(() => video)
}) })
} }
@ -19,10 +18,10 @@ function onError (err: Error, jobId: number) {
function onSuccess (jobId: number, video: VideoInstance) { function onSuccess (jobId: number, video: VideoInstance) {
logger.info('Job %d is a success.', jobId) logger.info('Job %d is a success.', jobId)
video.toAddRemoteJSON().then(remoteVideo => { const remoteVideo = video.toUpdateRemoteJSON()
// Now we'll add the video's meta data to our friends
return addVideoToFriends(remoteVideo, null) // Now we'll add the video's meta data to our friends
}) return updateVideoToFriends(remoteVideo, null)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -219,7 +219,7 @@ updatePodsScore = function (goodPods: number[], badPods: number[]) {
} }
if (badPods.length !== 0) { if (badPods.length !== 0) {
incrementScores(badPods, PODS_SCORE.MALUS) incrementScores(badPods, PODS_SCORE.PENALTY)
.then(() => removeBadPods()) .then(() => removeBadPods())
.catch(err => { .catch(err => {
if (err) logger.error('Cannot decrement scores of bad pods.', err) if (err) logger.error('Cannot decrement scores of bad pods.', err)

View file

@ -12,6 +12,7 @@ import {
isUserDisplayNSFWValid, isUserDisplayNSFWValid,
isUserVideoQuotaValid isUserVideoQuotaValid
} from '../../helpers' } from '../../helpers'
import { VideoResolution } from '../../../shared'
import { addMethodsToModel } from '../utils' import { addMethodsToModel } from '../utils'
import { import {
@ -245,7 +246,7 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
// attributes = [] because we don't want other fields than the sum // attributes = [] because we don't want other fields than the sum
const query = { const query = {
where: { where: {
resolution: 0 // Original, TODO: improve readability resolution: VideoResolution.ORIGINAL
}, },
include: [ include: [
{ {

View file

@ -7,60 +7,17 @@ import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
// Don't use barrel, import just what we need // Don't use barrel, import just what we need
import { Video as FormattedVideo } from '../../../shared/models/videos/video.model' 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' 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 namespace VideoMethods {
export type GetThumbnailName = (this: VideoInstance) => string export type GetThumbnailName = (this: VideoInstance) => string
export type GetPreviewName = (this: VideoInstance) => string export type GetPreviewName = (this: VideoInstance) => string
export type IsOwned = (this: VideoInstance) => boolean export type IsOwned = (this: VideoInstance) => boolean
export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type GetVideoFilename = (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 GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormattedAddRemoteVideo> export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData>
export type ToUpdateRemoteJSON = (this: VideoInstance) => FormattedUpdateRemoteVideo 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 // Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@ -147,6 +106,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
createPreview: VideoMethods.CreatePreview createPreview: VideoMethods.CreatePreview
createThumbnail: VideoMethods.CreateThumbnail createThumbnail: VideoMethods.CreateThumbnail
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
getOriginalFile: VideoMethods.GetOriginalFile
generateMagnetUri: VideoMethods.GenerateMagnetUri generateMagnetUri: VideoMethods.GenerateMagnetUri
getPreviewName: VideoMethods.GetPreviewName getPreviewName: VideoMethods.GetPreviewName
getThumbnailName: VideoMethods.GetThumbnailName getThumbnailName: VideoMethods.GetThumbnailName
@ -161,9 +121,12 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
toAddRemoteJSON: VideoMethods.ToAddRemoteJSON toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
toFormattedJSON: VideoMethods.ToFormattedJSON toFormattedJSON: VideoMethods.ToFormattedJSON
toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
transcodeVideofile: VideoMethods.TranscodeVideofile optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string> setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
} }

View file

@ -22,7 +22,8 @@ import {
unlinkPromise, unlinkPromise,
renamePromise, renamePromise,
writeFilePromise, writeFilePromise,
createTorrentPromise createTorrentPromise,
statPromise
} from '../../helpers' } from '../../helpers'
import { import {
CONFIG, CONFIG,
@ -35,7 +36,8 @@ import {
VIDEO_FILE_RESOLUTIONS VIDEO_FILE_RESOLUTIONS
} from '../../initializers' } from '../../initializers'
import { removeVideoToFriends } from '../../lib' 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 { addMethodsToModel, getSort } from '../utils'
import { import {
@ -46,6 +48,7 @@ import {
} from './video-interface' } from './video-interface'
let Video: Sequelize.Model<VideoInstance, VideoAttributes> let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile
let generateMagnetUri: VideoMethods.GenerateMagnetUri let generateMagnetUri: VideoMethods.GenerateMagnetUri
let getVideoFilename: VideoMethods.GetVideoFilename let getVideoFilename: VideoMethods.GetVideoFilename
let getThumbnailName: VideoMethods.GetThumbnailName let getThumbnailName: VideoMethods.GetThumbnailName
@ -55,11 +58,13 @@ let isOwned: VideoMethods.IsOwned
let toFormattedJSON: VideoMethods.ToFormattedJSON let toFormattedJSON: VideoMethods.ToFormattedJSON
let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
let transcodeVideofile: VideoMethods.TranscodeVideofile let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
let createPreview: VideoMethods.CreatePreview let createPreview: VideoMethods.CreatePreview
let createThumbnail: VideoMethods.CreateThumbnail let createThumbnail: VideoMethods.CreateThumbnail
let getVideoFilePath: VideoMethods.GetVideoFilePath let getVideoFilePath: VideoMethods.GetVideoFilePath
let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let getDurationFromFile: VideoMethods.GetDurationFromFile let getDurationFromFile: VideoMethods.GetDurationFromFile
@ -251,6 +256,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
getTorrentFileName, getTorrentFileName,
getVideoFilename, getVideoFilename,
getVideoFilePath, getVideoFilePath,
getOriginalFile,
isOwned, isOwned,
removeFile, removeFile,
removePreview, removePreview,
@ -259,7 +265,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
toAddRemoteJSON, toAddRemoteJSON,
toFormattedJSON, toFormattedJSON,
toUpdateRemoteJSON, toUpdateRemoteJSON,
transcodeVideofile optimizeOriginalVideofile,
transcodeOriginalVideofile,
getOriginalFileHeight
] ]
addMethodsToModel(Video, classMethods, instanceMethods) addMethodsToModel(Video, classMethods, instanceMethods)
@ -327,9 +335,14 @@ function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.T
return Promise.all(tasks) 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) { getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
// return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
return this.uuid + videoFile.extname
} }
getThumbnailName = function (this: VideoInstance) { getThumbnailName = function (this: VideoInstance) {
@ -345,8 +358,7 @@ getPreviewName = function (this: VideoInstance) {
getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
const extension = '.torrent' const extension = '.torrent'
// return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
return this.uuid + extension
} }
isOwned = function (this: VideoInstance) { isOwned = function (this: VideoInstance) {
@ -552,9 +564,10 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
return json return json
} }
transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) { optimizeOriginalVideofile = function (this: VideoInstance) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4' const newExtname = '.mp4'
const inputVideoFile = this.getOriginalFile()
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
@ -574,6 +587,12 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
}) })
.then(() => {
return statPromise(this.getVideoFilePath(inputVideoFile))
})
.then(stats => {
return inputVideoFile.set('size', stats.size)
})
.then(() => { .then(() => {
return this.createTorrentAndSetInfoHash(inputVideoFile) 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) { removeThumbnail = function (this: VideoInstance) {
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
return unlinkPromise(thumbnailPath) return unlinkPromise(thumbnailPath)

View file

@ -129,7 +129,7 @@ describe('Test multiple pods', function () {
}) })
it('Should upload the video on pod 2 and propagate on each pod', async function () { it('Should upload the video on pod 2 and propagate on each pod', async function () {
this.timeout(60000) this.timeout(120000)
const videoAttributes = { const videoAttributes = {
name: 'my super name for pod 2', 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) await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
// Transcoding, so wait more that 22 seconds // Transcoding, so wait more than 22000
await wait(42000) await wait(60000)
// All pods should have this video // All pods should have this video
for (const server of servers) { for (const server of servers) {
let baseMagnet = null let baseMagnet = {}
const res = await getVideosList(server.url) const res = await getVideosList(server.url)
@ -172,27 +172,50 @@ describe('Test multiple pods', function () {
expect(dateIsValid(video.updatedAt)).to.be.true expect(dateIsValid(video.updatedAt)).to.be.true
expect(video.author).to.equal('root') 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] // Check common attributes
const magnetUri = file.magnetUri for (const file of video.files) {
expect(file.magnetUri).to.have.lengthOf.above(2) 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)
if (server.url !== 'http://localhost:9002') { if (server.url !== 'http://localhost:9002') {
expect(video.isLocal).to.be.false expect(video.isLocal).to.be.false
} else { } else {
expect(video.isLocal).to.be.true 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 const originalFile = video.files.find(f => f.resolution === 0)
if (baseMagnet === null) { expect(originalFile).not.to.be.undefined
baseMagnet = magnetUri expect(originalFile.resolutionLabel).to.equal('original')
} else { expect(originalFile.size).to.equal(711327)
expect(baseMagnet).to.equal(magnetUri)
} 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) const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath)
expect(test).to.equal(true) expect(test).to.equal(true)

View file

@ -42,6 +42,8 @@ describe('Test video transcoding', function () {
const res = await getVideosList(servers[0].url) const res = await getVideosList(servers[0].url)
const video = res.body.data[0] const video = res.body.data[0]
expect(video.files).to.have.lengthOf(1)
const magnetUri = video.files[0].magnetUri const magnetUri = video.files[0].magnetUri
expect(magnetUri).to.match(/\.webm/) expect(magnetUri).to.match(/\.webm/)
@ -66,6 +68,8 @@ describe('Test video transcoding', function () {
const res = await getVideosList(servers[1].url) const res = await getVideosList(servers[1].url)
const video = res.body.data[0] const video = res.body.data[0]
expect(video.files).to.have.lengthOf(5)
const magnetUri = video.files[0].magnetUri const magnetUri = video.files[0].magnetUri
expect(magnetUri).to.match(/\.mp4/) expect(magnetUri).to.match(/\.mp4/)

View file

@ -12,14 +12,15 @@ import {
runServer, runServer,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
uploadVideo uploadVideo,
wait
} from '../utils' } from '../utils'
describe('Test update host scripts', function () { describe('Test update host scripts', function () {
let server: ServerInfo let server: ServerInfo
before(async function () { before(async function () {
this.timeout(30000) this.timeout(60000)
await flushTests() await flushTests()
@ -28,36 +29,43 @@ describe('Test update host scripts', function () {
port: 9256 port: 9256
} }
} }
server = await runServer(1, overrideConfig) // Run server 2 to have transcoding enabled
server = await runServer(2, overrideConfig)
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ server ])
// Upload two videos for our needs // Upload two videos for our needs
const videoAttributes = {} const videoAttributes = {}
await uploadVideo(server.url, server.accessToken, videoAttributes) await uploadVideo(server.url, server.accessToken, 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 () { it('Should update torrent hosts', async function () {
this.timeout(30000) this.timeout(30000)
killallServers([ server ]) killallServers([ server ])
server = await runServer(1) // Run server with standard configuration
server = await runServer(2)
const env = getEnvCli(server) const env = getEnvCli(server)
await execCLI(`${env} npm run update-host`) await execCLI(`${env} npm run update-host`)
const res = await getVideosList(server.url) const res = await getVideosList(server.url)
const videos = res.body.data const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[0].files[0].magnetUri).to.contain('localhost%3A9001%2Ftracker%2Fsocket') for (const video of videos) {
expect(videos[0].files[0].magnetUri).to.contain('localhost%3A9001%2Fstatic%2Fwebseed%2F') expect(video.files).to.have.lengthOf(5)
expect(videos[1].files[0].magnetUri).to.contain('localhost%3A9001%2Ftracker%2Fsocket') for (const file of video.files) {
expect(videos[1].files[0].magnetUri).to.contain('localhost%3A9001%2Fstatic%2Fwebseed%2F') 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) const torrent = await parseTorrentVideo(server, video.uuid, file.resolutionLabel)
expect(torrent.announce[0]).to.equal('ws://localhost:9001/tracker/socket') expect(torrent.announce[0]).to.equal('ws://localhost:9002/tracker/socket')
expect(torrent.urlList[0]).to.contain('http://localhost:9001/static/webseed') expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed')
}
}
}) })
after(async function () { after(async function () {

View file

@ -238,9 +238,10 @@ function rateVideo (url: string, accessToken: string, id: number, rating: string
.expect(specialStatus) .expect(specialStatus)
} }
function parseTorrentVideo (server: ServerInfo, videoUUID: string) { function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolutionLabel: string) {
return new Promise<any>((res, rej) => { 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) => { readFile(torrentPath, (err, data) => {
if (err) return rej(err) if (err) return rej(err)

View file

@ -2,8 +2,6 @@ export interface RemoteVideoUpdateData {
uuid: string uuid: string
tags: string[] tags: string[]
name: string name: string
extname: string
infoHash: string
category: number category: number
licence: number licence: number
language: number language: number

View file

@ -6,5 +6,6 @@ export * from './video-abuse.model'
export * from './video-blacklist.model' export * from './video-blacklist.model'
export * from './video-create.model' export * from './video-create.model'
export * from './video-rate.type' export * from './video-rate.type'
export * from './video-resolution.enum'
export * from './video-update.model' export * from './video-update.model'
export * from './video.model' export * from './video.model'

View file

@ -0,0 +1,8 @@
export enum VideoResolution {
ORIGINAL = 0,
H_240P = 240,
H_360P = 360,
H_480P = 480,
H_720P = 720,
H_1080P = 1080
}