diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/video-interface.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 207 |
2 files changed, 84 insertions, 125 deletions
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 340426f45..6a3db4f3e 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts | |||
@@ -35,7 +35,6 @@ export namespace VideoMethods { | |||
35 | 35 | ||
36 | // Return thumbnail name | 36 | // Return thumbnail name |
37 | export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> | 37 | export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> |
38 | export type GetDurationFromFile = (videoPath: string) => Promise<number> | ||
39 | 38 | ||
40 | export type List = () => Promise<VideoInstance[]> | 39 | export type List = () => Promise<VideoInstance[]> |
41 | export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]> | 40 | export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]> |
@@ -65,7 +64,6 @@ export namespace VideoMethods { | |||
65 | 64 | ||
66 | export interface VideoClass { | 65 | export interface VideoClass { |
67 | generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | 66 | generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData |
68 | getDurationFromFile: VideoMethods.GetDurationFromFile | ||
69 | list: VideoMethods.List | 67 | list: VideoMethods.List |
70 | listForApi: VideoMethods.ListForApi | 68 | listForApi: VideoMethods.ListForApi |
71 | listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags | 69 | listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags |
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 @@ | |||
1 | import * as safeBuffer from 'safe-buffer' | 1 | import * as safeBuffer from 'safe-buffer' |
2 | const Buffer = safeBuffer.Buffer | 2 | const Buffer = safeBuffer.Buffer |
3 | import * as ffmpeg from 'fluent-ffmpeg' | ||
4 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
5 | import { map } from 'lodash' | 4 | import { map } from 'lodash' |
6 | import * as parseTorrent from 'parse-torrent' | 5 | import * as parseTorrent from 'parse-torrent' |
7 | import { join } from 'path' | 6 | import { join } from 'path' |
8 | import * as Sequelize from 'sequelize' | 7 | import * as Sequelize from 'sequelize' |
9 | import * as Promise from 'bluebird' | 8 | import * as Promise from 'bluebird' |
9 | import { maxBy } from 'lodash' | ||
10 | 10 | ||
11 | import { TagInstance } from './tag-interface' | 11 | import { TagInstance } from './tag-interface' |
12 | import { | 12 | import { |
@@ -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' |
28 | import { | 31 | import { |
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' |
38 | import { removeVideoToFriends } from '../../lib' | 40 | import { removeVideoToFriends } from '../../lib' |
39 | import { VideoResolution } from '../../../shared' | 41 | import { VideoResolution } from '../../../shared' |
@@ -67,7 +69,6 @@ let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | |||
67 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | 69 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight |
68 | 70 | ||
69 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | 71 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData |
70 | let getDurationFromFile: VideoMethods.GetDurationFromFile | ||
71 | let list: VideoMethods.List | 72 | let list: VideoMethods.List |
72 | let listForApi: VideoMethods.ListForApi | 73 | let listForApi: VideoMethods.ListForApi |
73 | let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID | 74 | let 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 | |||
338 | getOriginalFile = function (this: VideoInstance) { | 338 | getOriginalFile = 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 | ||
344 | getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { | 345 | getVideoFilename = 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 | ||
348 | getThumbnailName = function (this: VideoInstance) { | 349 | getThumbnailName = function (this: VideoInstance) { |
@@ -358,7 +359,7 @@ getPreviewName = function (this: VideoInstance) { | |||
358 | 359 | ||
359 | getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { | 360 | getTorrentFileName = 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 | ||
364 | isOwned = function (this: VideoInstance) { | 365 | isOwned = function (this: VideoInstance) { |
@@ -366,11 +367,20 @@ isOwned = function (this: VideoInstance) { | |||
366 | } | 367 | } |
367 | 368 | ||
368 | createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { | 369 | createPreview = 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 | ||
372 | createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { | 377 | createThumbnail = 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 | ||
376 | getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { | 386 | getVideoFilePath = 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 | ||
623 | transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { | 628 | transcodeOriginalVideofile = 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 | ||
672 | getOriginalFileHeight = function (this: VideoInstance) { | 669 | getOriginalFileHeight = 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 | ||
685 | removeThumbnail = function (this: VideoInstance) { | 675 | removeThumbnail = function (this: VideoInstance) { |
@@ -714,16 +704,6 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin | |||
714 | }) | 704 | }) |
715 | } | 705 | } |
716 | 706 | ||
717 | getDurationFromFile = 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 | |||
727 | list = function () { | 707 | list = 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 | |||
968 | function 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 | } | ||