aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r--server/models/video/video.ts207
1 files changed, 84 insertions, 123 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c376d769e..2ba6cf25f 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,12 +1,12 @@
1import * as safeBuffer from 'safe-buffer' 1import * as safeBuffer from 'safe-buffer'
2const Buffer = safeBuffer.Buffer 2const Buffer = safeBuffer.Buffer
3import * as ffmpeg from 'fluent-ffmpeg'
4import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
5import { map } from 'lodash' 4import { map } from 'lodash'
6import * as parseTorrent from 'parse-torrent' 5import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 6import { join } from 'path'
8import * as Sequelize from 'sequelize' 7import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird' 8import * as Promise from 'bluebird'
9import { maxBy } from 'lodash'
10 10
11import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
12import { 12import {
@@ -23,7 +23,10 @@ import {
23 renamePromise, 23 renamePromise,
24 writeFilePromise, 24 writeFilePromise,
25 createTorrentPromise, 25 createTorrentPromise,
26 statPromise 26 statPromise,
27 generateImageFromVideoFile,
28 transcode,
29 getVideoFileHeight
27} from '../../helpers' 30} from '../../helpers'
28import { 31import {
29 CONFIG, 32 CONFIG,
@@ -32,8 +35,7 @@ import {
32 VIDEO_CATEGORIES, 35 VIDEO_CATEGORIES,
33 VIDEO_LICENCES, 36 VIDEO_LICENCES,
34 VIDEO_LANGUAGES, 37 VIDEO_LANGUAGES,
35 THUMBNAILS_SIZE, 38 THUMBNAILS_SIZE
36 VIDEO_FILE_RESOLUTIONS
37} from '../../initializers' 39} from '../../initializers'
38import { removeVideoToFriends } from '../../lib' 40import { removeVideoToFriends } from '../../lib'
39import { VideoResolution } from '../../../shared' 41import { VideoResolution } from '../../../shared'
@@ -67,7 +69,6 @@ let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
67let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 69let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
68 70
69let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 71let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
70let getDurationFromFile: VideoMethods.GetDurationFromFile
71let list: VideoMethods.List 72let list: VideoMethods.List
72let listForApi: VideoMethods.ListForApi 73let listForApi: VideoMethods.ListForApi
73let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 74let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
@@ -233,7 +234,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
233 associate, 234 associate,
234 235
235 generateThumbnailFromData, 236 generateThumbnailFromData,
236 getDurationFromFile,
237 list, 237 list,
238 listForApi, 238 listForApi,
239 listOwnedAndPopulateAuthorAndTags, 239 listOwnedAndPopulateAuthorAndTags,
@@ -338,11 +338,12 @@ function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.T
338getOriginalFile = function (this: VideoInstance) { 338getOriginalFile = function (this: VideoInstance) {
339 if (Array.isArray(this.VideoFiles) === false) return undefined 339 if (Array.isArray(this.VideoFiles) === false) return undefined
340 340
341 return this.VideoFiles.find(file => file.resolution === VideoResolution.ORIGINAL) 341 // The original file is the file that have the higher resolution
342 return maxBy(this.VideoFiles, file => file.resolution)
342} 343}
343 344
344getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { 345getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
345 return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname 346 return this.uuid + '-' + videoFile.resolution + videoFile.extname
346} 347}
347 348
348getThumbnailName = function (this: VideoInstance) { 349getThumbnailName = function (this: VideoInstance) {
@@ -358,7 +359,7 @@ getPreviewName = function (this: VideoInstance) {
358 359
359getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { 360getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
360 const extension = '.torrent' 361 const extension = '.torrent'
361 return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension 362 return this.uuid + '-' + videoFile.resolution + extension
362} 363}
363 364
364isOwned = function (this: VideoInstance) { 365isOwned = function (this: VideoInstance) {
@@ -366,11 +367,20 @@ isOwned = function (this: VideoInstance) {
366} 367}
367 368
368createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { 369createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
369 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null) 370 return generateImageFromVideoFile(
371 this.getVideoFilePath(videoFile),
372 CONFIG.STORAGE.PREVIEWS_DIR,
373 this.getPreviewName()
374 )
370} 375}
371 376
372createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { 377createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
373 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE) 378 return generateImageFromVideoFile(
379 this.getVideoFilePath(videoFile),
380 CONFIG.STORAGE.THUMBNAILS_DIR,
381 this.getThumbnailName(),
382 THUMBNAILS_SIZE
383 )
374} 384}
375 385
376getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { 386getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
@@ -480,8 +490,7 @@ toFormattedJSON = function (this: VideoInstance) {
480 // Format and sort video files 490 // Format and sort video files
481 json.files = this.VideoFiles 491 json.files = this.VideoFiles
482 .map(videoFile => { 492 .map(videoFile => {
483 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution] 493 let resolutionLabel = videoFile.resolution + 'p'
484 if (!resolutionLabel) resolutionLabel = 'Unknown'
485 494
486 const videoFileJson = { 495 const videoFileJson = {
487 resolution: videoFile.resolution, 496 resolution: videoFile.resolution,
@@ -578,46 +587,42 @@ optimizeOriginalVideofile = function (this: VideoInstance) {
578 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) 587 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
579 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) 588 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
580 589
581 return new Promise<void>((res, rej) => { 590 const transcodeOptions = {
582 ffmpeg(videoInputPath) 591 inputPath: videoInputPath,
583 .output(videoOutputPath) 592 outputPath: videoOutputPath
584 .videoCodec('libx264') 593 }
585 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 594
586 .outputOption('-movflags faststart') 595 return transcode(transcodeOptions)
587 .on('error', rej) 596 .then(() => {
588 .on('end', () => { 597 return unlinkPromise(videoInputPath)
589 598 })
590 return unlinkPromise(videoInputPath) 599 .then(() => {
591 .then(() => { 600 // Important to do this before getVideoFilename() to take in account the new file extension
592 // Important to do this before getVideoFilename() to take in account the new file extension 601 inputVideoFile.set('extname', newExtname)
593 inputVideoFile.set('extname', newExtname) 602
594 603 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
595 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) 604 })
596 }) 605 .then(() => {
597 .then(() => { 606 return statPromise(this.getVideoFilePath(inputVideoFile))
598 return statPromise(this.getVideoFilePath(inputVideoFile)) 607 })
599 }) 608 .then(stats => {
600 .then(stats => { 609 return inputVideoFile.set('size', stats.size)
601 return inputVideoFile.set('size', stats.size) 610 })
602 }) 611 .then(() => {
603 .then(() => { 612 return this.createTorrentAndSetInfoHash(inputVideoFile)
604 return this.createTorrentAndSetInfoHash(inputVideoFile) 613 })
605 }) 614 .then(() => {
606 .then(() => { 615 return inputVideoFile.save()
607 return inputVideoFile.save() 616 })
608 }) 617 .then(() => {
609 .then(() => { 618 return undefined
610 return res() 619 })
611 }) 620 .catch(err => {
612 .catch(err => { 621 // Auto destruction...
613 // Auto destruction... 622 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
614 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 623
615 624 throw err
616 return rej(err) 625 })
617 })
618 })
619 .run()
620 })
621} 626}
622 627
623transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { 628transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
@@ -634,52 +639,37 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes
634 videoId: this.id 639 videoId: this.id
635 }) 640 })
636 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) 641 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
637 const resolutionOption = `${resolution}x?` // '720x?' for example 642
638 643 const transcodeOptions = {
639 return new Promise<void>((res, rej) => { 644 inputPath: videoInputPath,
640 ffmpeg(videoInputPath) 645 outputPath: videoOutputPath,
641 .output(videoOutputPath) 646 resolution
642 .videoCodec('libx264') 647 }
643 .size(resolutionOption) 648 return transcode(transcodeOptions)
644 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 649 .then(() => {
645 .outputOption('-movflags faststart') 650 return statPromise(videoOutputPath)
646 .on('error', rej) 651 })
647 .on('end', () => { 652 .then(stats => {
648 return statPromise(videoOutputPath) 653 newVideoFile.set('size', stats.size)
649 .then(stats => { 654
650 newVideoFile.set('size', stats.size) 655 return undefined
651 656 })
652 return undefined 657 .then(() => {
653 }) 658 return this.createTorrentAndSetInfoHash(newVideoFile)
654 .then(() => { 659 })
655 return this.createTorrentAndSetInfoHash(newVideoFile) 660 .then(() => {
656 }) 661 return newVideoFile.save()
657 .then(() => { 662 })
658 return newVideoFile.save() 663 .then(() => {
659 }) 664 return this.VideoFiles.push(newVideoFile)
660 .then(() => { 665 })
661 return this.VideoFiles.push(newVideoFile) 666 .then(() => undefined)
662 })
663 .then(() => {
664 return res()
665 })
666 .catch(rej)
667 })
668 .run()
669 })
670} 667}
671 668
672getOriginalFileHeight = function (this: VideoInstance) { 669getOriginalFileHeight = function (this: VideoInstance) {
673 const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) 670 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
674 671
675 return new Promise<number>((res, rej) => { 672 return getVideoFileHeight(originalFilePath)
676 ffmpeg.ffprobe(originalFilePath, (err, metadata) => {
677 if (err) return rej(err)
678
679 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
680 return res(videoStream.height)
681 })
682 })
683} 673}
684 674
685removeThumbnail = function (this: VideoInstance) { 675removeThumbnail = function (this: VideoInstance) {
@@ -714,16 +704,6 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin
714 }) 704 })
715} 705}
716 706
717getDurationFromFile = function (videoPath: string) {
718 return new Promise<number>((res, rej) => {
719 ffmpeg.ffprobe(videoPath, (err, metadata) => {
720 if (err) return rej(err)
721
722 return res(Math.floor(metadata.format.duration))
723 })
724 })
725}
726
727list = function () { 707list = function () {
728 const query = { 708 const query = {
729 include: [ Video['sequelize'].models.VideoFile ] 709 include: [ Video['sequelize'].models.VideoFile ]
@@ -964,22 +944,3 @@ function createBaseVideosWhere () {
964 } 944 }
965 } 945 }
966} 946}
967
968function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
969 const options = {
970 filename: imageName,
971 count: 1,
972 folder
973 }
974
975 if (size) {
976 options['size'] = size
977 }
978
979 return new Promise<string>((res, rej) => {
980 ffmpeg(videoPath)
981 .on('error', rej)
982 .on('end', () => res(imageName))
983 .thumbnail(options)
984 })
985}