X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=0d0048b4a6220bc8a13a2d35cc12a8fea0e38433;hb=d8755eed1e452d2efbfc983af0e9d228d152bf6b;hp=8c69fe189e98e6992e4a008010e4978aec8e4b0c;hpb=980246ea8f1c51a137eaf0c441ef7e3b6fb88810;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8c69fe189..0d0048b4a 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,12 +1,12 @@ import * as safeBuffer from 'safe-buffer' const Buffer = safeBuffer.Buffer -import * as ffmpeg from 'fluent-ffmpeg' import * as magnetUtil from 'magnet-uri' import { map } from 'lodash' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' +import { maxBy } from 'lodash' import { TagInstance } from './tag-interface' import { @@ -22,7 +22,11 @@ import { unlinkPromise, renamePromise, writeFilePromise, - createTorrentPromise + createTorrentPromise, + statPromise, + generateImageFromVideoFile, + transcode, + getVideoFileHeight } from '../../helpers' import { CONFIG, @@ -31,11 +35,11 @@ import { VIDEO_CATEGORIES, VIDEO_LICENCES, VIDEO_LANGUAGES, - THUMBNAILS_SIZE, - VIDEO_FILE_RESOLUTIONS + THUMBNAILS_SIZE } 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,23 +50,28 @@ import { } from './video-interface' let Video: Sequelize.Model +let getOriginalFile: VideoMethods.GetOriginalFile let generateMagnetUri: VideoMethods.GenerateMagnetUri let getVideoFilename: VideoMethods.GetVideoFilename let getThumbnailName: VideoMethods.GetThumbnailName +let getThumbnailPath: VideoMethods.GetThumbnailPath let getPreviewName: VideoMethods.GetPreviewName +let getPreviewPath: VideoMethods.GetPreviewPath let getTorrentFileName: VideoMethods.GetTorrentFileName 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 getEmbedPath: VideoMethods.GetEmbedPath let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData -let getDurationFromFile: VideoMethods.GetDurationFromFile let list: VideoMethods.List let listForApi: VideoMethods.ListForApi let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID @@ -228,7 +237,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da associate, generateThumbnailFromData, - getDurationFromFile, list, listForApi, listOwnedAndPopulateAuthorAndTags, @@ -247,10 +255,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da createTorrentAndSetInfoHash, generateMagnetUri, getPreviewName, + getPreviewPath, getThumbnailName, + getThumbnailPath, getTorrentFileName, getVideoFilename, getVideoFilePath, + getOriginalFile, isOwned, removeFile, removePreview, @@ -259,7 +270,10 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da toAddRemoteJSON, toFormattedJSON, toUpdateRemoteJSON, - transcodeVideofile + optimizeOriginalVideofile, + transcodeOriginalVideofile, + getOriginalFileHeight, + getEmbedPath ] addMethodsToModel(Video, classMethods, instanceMethods) @@ -300,7 +314,7 @@ function associate (models) { }) } -function afterDestroy (video: VideoInstance) { +function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { const tasks = [] tasks.push( @@ -314,10 +328,10 @@ function afterDestroy (video: VideoInstance) { tasks.push( video.removePreview(), - removeVideoToFriends(removeVideoToFriendsParams) + removeVideoToFriends(removeVideoToFriendsParams, options.transaction) ) - // TODO: check files is populated + // Remove physical files and torrents video.VideoFiles.forEach(file => { video.removeFile(file), video.removeTorrent(file) @@ -327,9 +341,15 @@ function afterDestroy (video: VideoInstance) { return Promise.all(tasks) } +getOriginalFile = function (this: VideoInstance) { + if (Array.isArray(this.VideoFiles) === false) return undefined + + // The original file is the file that have the higher resolution + return maxBy(this.VideoFiles, file => file.resolution) +} + getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { - // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname - return this.uuid + videoFile.extname + return this.uuid + '-' + videoFile.resolution + videoFile.extname } getThumbnailName = function (this: VideoInstance) { @@ -345,8 +365,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 + '-' + videoFile.resolution + extension } isOwned = function (this: VideoInstance) { @@ -354,11 +373,22 @@ isOwned = function (this: VideoInstance) { } createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { - return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null) + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.PREVIEWS_DIR, + this.getPreviewName() + ) } createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { - return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE) + const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height + + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.THUMBNAILS_DIR, + this.getThumbnailName(), + imageSize + ) } getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { @@ -378,6 +408,8 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil return createTorrentPromise(this.getVideoFilePath(videoFile), options) .then(torrent => { const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + logger.info('Creating torrent %s.', filePath) + return writeFilePromise(filePath, torrent).then(() => torrent) }) .then(torrent => { @@ -414,6 +446,18 @@ generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) return magnetUtil.encode(magnetHash) } +getEmbedPath = function (this: VideoInstance) { + return '/videos/embed/' + this.uuid +} + +getThumbnailPath = function (this: VideoInstance) { + return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) +} + +getPreviewPath = function (this: VideoInstance) { + return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) +} + toFormattedJSON = function (this: VideoInstance) { let podHost @@ -456,26 +500,33 @@ toFormattedJSON = function (this: VideoInstance) { likes: this.likes, dislikes: this.dislikes, tags: map(this.Tags, 'name'), - thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), - previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), + thumbnailPath: this.getThumbnailPath(), + previewPath: this.getPreviewPath(), + embedPath: this.getEmbedPath(), createdAt: this.createdAt, updatedAt: this.updatedAt, files: [] } - this.VideoFiles.forEach(videoFile => { - let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution] - if (!resolutionLabel) resolutionLabel = 'Unknown' - - const videoFileJson = { - resolution: videoFile.resolution, - resolutionLabel, - magnetUri: this.generateMagnetUri(videoFile), - size: videoFile.size - } - - json.files.push(videoFileJson) - }) + // Format and sort video files + json.files = this.VideoFiles + .map(videoFile => { + let resolutionLabel = videoFile.resolution + 'p' + + const videoFileJson = { + resolution: videoFile.resolution, + resolutionLabel, + magnetUri: this.generateMagnetUri(videoFile), + size: videoFile.size + } + + return videoFileJson + }) + .sort((a, b) => { + if (a.resolution < b.resolution) return 1 + if (a.resolution === b.resolution) return 0 + return -1 + }) return json } @@ -550,46 +601,96 @@ 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) - return new Promise((res, rej) => { - ffmpeg(videoInputPath) - .output(videoOutputPath) - .videoCodec('libx264') - .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) - .outputOption('-movflags faststart') - .on('error', rej) - .on('end', () => { - - return unlinkPromise(videoInputPath) - .then(() => { - // Important to do this before getVideoFilename() to take in account the new file extension - inputVideoFile.set('extname', newExtname) - - return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) - }) - .then(() => { - return this.createTorrentAndSetInfoHash(inputVideoFile) - }) - .then(() => { - return inputVideoFile.save() - }) - .then(() => { - return res() - }) - .catch(err => { - // Auto destruction... - this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) - - return rej(err) - }) - }) - .run() + const transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath + } + + return transcode(transcodeOptions) + .then(() => { + return unlinkPromise(videoInputPath) + }) + .then(() => { + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.set('extname', newExtname) + + 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) + }) + .then(() => { + return inputVideoFile.save() + }) + .then(() => { + return undefined + }) + .catch(err => { + // Auto destruction... + this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + + throw err + }) +} + +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 transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath, + resolution + } + return transcode(transcodeOptions) + .then(() => { + 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(() => undefined) +} + +getOriginalFileHeight = function (this: VideoInstance) { + const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) + + return getVideoFileHeight(originalFilePath) } removeThumbnail = function (this: VideoInstance) { @@ -624,16 +725,6 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin }) } -getDurationFromFile = function (videoPath: string) { - return new Promise((res, rej) => { - ffmpeg.ffprobe(videoPath, (err, metadata) => { - if (err) return rej(err) - - return res(Math.floor(metadata.format.duration)) - }) - }) -} - list = function () { const query = { include: [ Video['sequelize'].models.VideoFile ] @@ -874,22 +965,3 @@ function createBaseVideosWhere () { } } } - -function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { - const options = { - filename: imageName, - count: 1, - folder - } - - if (size) { - options['size'] = size - } - - return new Promise((res, rej) => { - ffmpeg(videoPath) - .on('error', rej) - .on('end', () => res(imageName)) - .thumbnail(options) - }) -}