]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Implement video transcoding on server side
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index 4fb4485d8ada89799b744f68fc104b2869f39cd8..28df91a7b868ad0db00c20f1d2400fc012073d99 100644 (file)
@@ -9,7 +9,6 @@ import * as Sequelize from 'sequelize'
 import * as Promise from 'bluebird'
 
 import { TagInstance } from './tag-interface'
-import { UserInstance } from '../user/user-interface'
 import {
   logger,
   isVideoNameValid,
@@ -23,7 +22,8 @@ import {
   unlinkPromise,
   renamePromise,
   writeFilePromise,
-  createTorrentPromise
+  createTorrentPromise,
+  statPromise
 } from '../../helpers'
 import {
   CONFIG,
@@ -36,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 {
@@ -47,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
@@ -56,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
@@ -252,6 +256,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     getTorrentFileName,
     getVideoFilename,
     getVideoFilePath,
+    getOriginalFile,
     isOwned,
     removeFile,
     removePreview,
@@ -260,7 +265,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     toAddRemoteJSON,
     toFormattedJSON,
     toUpdateRemoteJSON,
-    transcodeVideofile
+    optimizeOriginalVideofile,
+    transcodeOriginalVideofile,
+    getOriginalFileHeight
   ]
   addMethodsToModel(Video, classMethods, instanceMethods)
 
@@ -301,7 +308,7 @@ function associate (models) {
   })
 }
 
-function afterDestroy (video: VideoInstance) {
+function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
   const tasks = []
 
   tasks.push(
@@ -315,10 +322,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)
@@ -328,9 +335,14 @@ function afterDestroy (video: VideoInstance) {
   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) {
@@ -346,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) {
@@ -379,6 +390,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 => {
@@ -551,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)
 
@@ -573,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)
           })
@@ -593,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)