]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Add oembed endpoint
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index 4fb4485d8ada89799b744f68fc104b2869f39cd8..0d0048b4a6220bc8a13a2d35cc12a8fea0e38433 100644 (file)
@@ -1,15 +1,14 @@
 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 { UserInstance } from '../user/user-interface'
 import {
   logger,
   isVideoNameValid,
@@ -23,7 +22,11 @@ import {
   unlinkPromise,
   renamePromise,
   writeFilePromise,
-  createTorrentPromise
+  createTorrentPromise,
+  statPromise,
+  generateImageFromVideoFile,
+  transcode,
+  getVideoFileHeight
 } from '../../helpers'
 import {
   CONFIG,
@@ -32,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 {
@@ -47,23 +50,28 @@ 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
+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
@@ -229,7 +237,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     associate,
 
     generateThumbnailFromData,
-    getDurationFromFile,
     list,
     listForApi,
     listOwnedAndPopulateAuthorAndTags,
@@ -248,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,
@@ -260,7 +270,10 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     toAddRemoteJSON,
     toFormattedJSON,
     toUpdateRemoteJSON,
-    transcodeVideofile
+    optimizeOriginalVideofile,
+    transcodeOriginalVideofile,
+    getOriginalFileHeight,
+    getEmbedPath
   ]
   addMethodsToModel(Video, classMethods, instanceMethods)
 
@@ -301,7 +314,7 @@ function associate (models) {
   })
 }
 
-function afterDestroy (video: VideoInstance) {
+function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
   const tasks = []
 
   tasks.push(
@@ -315,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)
@@ -328,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) {
@@ -346,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) {
@@ -355,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) {
@@ -379,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 => {
@@ -415,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
 
@@ -457,26 +500,33 @@ toFormattedJSON = function (this: VideoInstance) {
     likes: this.likes,
     dislikes: this.dislikes,
     tags: map<TagInstance, string>(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
 }
@@ -551,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<void>((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) {
@@ -625,16 +725,6 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin
   })
 }
 
-getDurationFromFile = function (videoPath: string) {
-  return new Promise<number>((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 ]
@@ -875,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<string>((res, rej) => {
-    ffmpeg(videoPath)
-      .on('error', rej)
-      .on('end', () => res(imageName))
-      .thumbnail(options)
-  })
-}