diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-10-02 12:20:26 +0200 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-10-03 15:31:26 +0200 |
commit | 40298b02546e8225dd21bf6048fe7f224aefc32a (patch) | |
tree | 0a0b981dbeb2af47810adff6553a0df995a03734 /server/models/video/video.ts | |
parent | f0adb2701c1cf404ff63095f71e542bfe6d025ae (diff) | |
download | PeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.tar.gz PeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.tar.zst PeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.zip |
Implement video transcoding on server side
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r-- | server/models/video/video.ts | 105 |
1 files changed, 96 insertions, 9 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e011c3b4d..28df91a7b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -22,7 +22,8 @@ import { | |||
22 | unlinkPromise, | 22 | unlinkPromise, |
23 | renamePromise, | 23 | renamePromise, |
24 | writeFilePromise, | 24 | writeFilePromise, |
25 | createTorrentPromise | 25 | createTorrentPromise, |
26 | statPromise | ||
26 | } from '../../helpers' | 27 | } from '../../helpers' |
27 | import { | 28 | import { |
28 | CONFIG, | 29 | CONFIG, |
@@ -35,7 +36,8 @@ import { | |||
35 | VIDEO_FILE_RESOLUTIONS | 36 | VIDEO_FILE_RESOLUTIONS |
36 | } from '../../initializers' | 37 | } from '../../initializers' |
37 | import { removeVideoToFriends } from '../../lib' | 38 | import { removeVideoToFriends } from '../../lib' |
38 | import { VideoFileInstance } from './video-file-interface' | 39 | import { VideoResolution } from '../../../shared' |
40 | import { VideoFileInstance, VideoFileModel } from './video-file-interface' | ||
39 | 41 | ||
40 | import { addMethodsToModel, getSort } from '../utils' | 42 | import { addMethodsToModel, getSort } from '../utils' |
41 | import { | 43 | import { |
@@ -46,6 +48,7 @@ import { | |||
46 | } from './video-interface' | 48 | } from './video-interface' |
47 | 49 | ||
48 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | 50 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> |
51 | let getOriginalFile: VideoMethods.GetOriginalFile | ||
49 | let generateMagnetUri: VideoMethods.GenerateMagnetUri | 52 | let generateMagnetUri: VideoMethods.GenerateMagnetUri |
50 | let getVideoFilename: VideoMethods.GetVideoFilename | 53 | let getVideoFilename: VideoMethods.GetVideoFilename |
51 | let getThumbnailName: VideoMethods.GetThumbnailName | 54 | let getThumbnailName: VideoMethods.GetThumbnailName |
@@ -55,11 +58,13 @@ let isOwned: VideoMethods.IsOwned | |||
55 | let toFormattedJSON: VideoMethods.ToFormattedJSON | 58 | let toFormattedJSON: VideoMethods.ToFormattedJSON |
56 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | 59 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON |
57 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | 60 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON |
58 | let transcodeVideofile: VideoMethods.TranscodeVideofile | 61 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile |
62 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | ||
59 | let createPreview: VideoMethods.CreatePreview | 63 | let createPreview: VideoMethods.CreatePreview |
60 | let createThumbnail: VideoMethods.CreateThumbnail | 64 | let createThumbnail: VideoMethods.CreateThumbnail |
61 | let getVideoFilePath: VideoMethods.GetVideoFilePath | 65 | let getVideoFilePath: VideoMethods.GetVideoFilePath |
62 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | 66 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash |
67 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | ||
63 | 68 | ||
64 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | 69 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData |
65 | let getDurationFromFile: VideoMethods.GetDurationFromFile | 70 | let getDurationFromFile: VideoMethods.GetDurationFromFile |
@@ -251,6 +256,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
251 | getTorrentFileName, | 256 | getTorrentFileName, |
252 | getVideoFilename, | 257 | getVideoFilename, |
253 | getVideoFilePath, | 258 | getVideoFilePath, |
259 | getOriginalFile, | ||
254 | isOwned, | 260 | isOwned, |
255 | removeFile, | 261 | removeFile, |
256 | removePreview, | 262 | removePreview, |
@@ -259,7 +265,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
259 | toAddRemoteJSON, | 265 | toAddRemoteJSON, |
260 | toFormattedJSON, | 266 | toFormattedJSON, |
261 | toUpdateRemoteJSON, | 267 | toUpdateRemoteJSON, |
262 | transcodeVideofile | 268 | optimizeOriginalVideofile, |
269 | transcodeOriginalVideofile, | ||
270 | getOriginalFileHeight | ||
263 | ] | 271 | ] |
264 | addMethodsToModel(Video, classMethods, instanceMethods) | 272 | addMethodsToModel(Video, classMethods, instanceMethods) |
265 | 273 | ||
@@ -327,9 +335,14 @@ function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.T | |||
327 | return Promise.all(tasks) | 335 | return Promise.all(tasks) |
328 | } | 336 | } |
329 | 337 | ||
338 | getOriginalFile = function (this: VideoInstance) { | ||
339 | if (Array.isArray(this.VideoFiles) === false) return undefined | ||
340 | |||
341 | return this.VideoFiles.find(file => file.resolution === VideoResolution.ORIGINAL) | ||
342 | } | ||
343 | |||
330 | getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { | 344 | getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { |
331 | // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname | 345 | return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname |
332 | return this.uuid + videoFile.extname | ||
333 | } | 346 | } |
334 | 347 | ||
335 | getThumbnailName = function (this: VideoInstance) { | 348 | getThumbnailName = function (this: VideoInstance) { |
@@ -345,8 +358,7 @@ getPreviewName = function (this: VideoInstance) { | |||
345 | 358 | ||
346 | getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { | 359 | getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { |
347 | const extension = '.torrent' | 360 | const extension = '.torrent' |
348 | // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension | 361 | return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension |
349 | return this.uuid + extension | ||
350 | } | 362 | } |
351 | 363 | ||
352 | isOwned = function (this: VideoInstance) { | 364 | isOwned = function (this: VideoInstance) { |
@@ -552,9 +564,10 @@ toUpdateRemoteJSON = function (this: VideoInstance) { | |||
552 | return json | 564 | return json |
553 | } | 565 | } |
554 | 566 | ||
555 | transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) { | 567 | optimizeOriginalVideofile = function (this: VideoInstance) { |
556 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 568 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
557 | const newExtname = '.mp4' | 569 | const newExtname = '.mp4' |
570 | const inputVideoFile = this.getOriginalFile() | ||
558 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | 571 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) |
559 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | 572 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) |
560 | 573 | ||
@@ -575,6 +588,12 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns | |||
575 | return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) | 588 | return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) |
576 | }) | 589 | }) |
577 | .then(() => { | 590 | .then(() => { |
591 | return statPromise(this.getVideoFilePath(inputVideoFile)) | ||
592 | }) | ||
593 | .then(stats => { | ||
594 | return inputVideoFile.set('size', stats.size) | ||
595 | }) | ||
596 | .then(() => { | ||
578 | return this.createTorrentAndSetInfoHash(inputVideoFile) | 597 | return this.createTorrentAndSetInfoHash(inputVideoFile) |
579 | }) | 598 | }) |
580 | .then(() => { | 599 | .then(() => { |
@@ -594,6 +613,74 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns | |||
594 | }) | 613 | }) |
595 | } | 614 | } |
596 | 615 | ||
616 | transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { | ||
617 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
618 | const extname = '.mp4' | ||
619 | |||
620 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
621 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) | ||
622 | |||
623 | const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ | ||
624 | resolution, | ||
625 | extname, | ||
626 | size: 0, | ||
627 | videoId: this.id | ||
628 | }) | ||
629 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) | ||
630 | const resolutionWidthSizes = { | ||
631 | 1: '240x?', | ||
632 | 2: '360x?', | ||
633 | 3: '480x?', | ||
634 | 4: '720x?', | ||
635 | 5: '1080x?' | ||
636 | } | ||
637 | |||
638 | return new Promise<void>((res, rej) => { | ||
639 | ffmpeg(videoInputPath) | ||
640 | .output(videoOutputPath) | ||
641 | .videoCodec('libx264') | ||
642 | .size(resolutionWidthSizes[resolution]) | ||
643 | .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
644 | .outputOption('-movflags faststart') | ||
645 | .on('error', rej) | ||
646 | .on('end', () => { | ||
647 | return statPromise(videoOutputPath) | ||
648 | .then(stats => { | ||
649 | newVideoFile.set('size', stats.size) | ||
650 | |||
651 | return undefined | ||
652 | }) | ||
653 | .then(() => { | ||
654 | return this.createTorrentAndSetInfoHash(newVideoFile) | ||
655 | }) | ||
656 | .then(() => { | ||
657 | return newVideoFile.save() | ||
658 | }) | ||
659 | .then(() => { | ||
660 | return this.VideoFiles.push(newVideoFile) | ||
661 | }) | ||
662 | .then(() => { | ||
663 | return res() | ||
664 | }) | ||
665 | .catch(rej) | ||
666 | }) | ||
667 | .run() | ||
668 | }) | ||
669 | } | ||
670 | |||
671 | getOriginalFileHeight = function (this: VideoInstance) { | ||
672 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | ||
673 | |||
674 | return new Promise<number>((res, rej) => { | ||
675 | ffmpeg.ffprobe(originalFilePath, (err, metadata) => { | ||
676 | if (err) return rej(err) | ||
677 | |||
678 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | ||
679 | return res(videoStream.height) | ||
680 | }) | ||
681 | }) | ||
682 | } | ||
683 | |||
597 | removeThumbnail = function (this: VideoInstance) { | 684 | removeThumbnail = function (this: VideoInstance) { |
598 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | 685 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) |
599 | return unlinkPromise(thumbnailPath) | 686 | return unlinkPromise(thumbnailPath) |