aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-02 12:20:26 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-10-03 15:31:26 +0200
commit40298b02546e8225dd21bf6048fe7f224aefc32a (patch)
tree0a0b981dbeb2af47810adff6553a0df995a03734 /server/models
parentf0adb2701c1cf404ff63095f71e542bfe6d025ae (diff)
downloadPeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.tar.gz
PeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.tar.zst
PeerTube-40298b02546e8225dd21bf6048fe7f224aefc32a.zip
Implement video transcoding on server side
Diffstat (limited to 'server/models')
-rw-r--r--server/models/pod/pod.ts2
-rw-r--r--server/models/user/user.ts3
-rw-r--r--server/models/video/video-interface.ts63
-rw-r--r--server/models/video/video.ts105
4 files changed, 112 insertions, 61 deletions
diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts
index df6412721..1440ac9b4 100644
--- a/server/models/pod/pod.ts
+++ b/server/models/pod/pod.ts
@@ -219,7 +219,7 @@ updatePodsScore = function (goodPods: number[], badPods: number[]) {
219 } 219 }
220 220
221 if (badPods.length !== 0) { 221 if (badPods.length !== 0) {
222 incrementScores(badPods, PODS_SCORE.MALUS) 222 incrementScores(badPods, PODS_SCORE.PENALTY)
223 .then(() => removeBadPods()) 223 .then(() => removeBadPods())
224 .catch(err => { 224 .catch(err => {
225 if (err) logger.error('Cannot decrement scores of bad pods.', err) 225 if (err) logger.error('Cannot decrement scores of bad pods.', err)
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 79a595528..7a21dbefa 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -12,6 +12,7 @@ import {
12 isUserDisplayNSFWValid, 12 isUserDisplayNSFWValid,
13 isUserVideoQuotaValid 13 isUserVideoQuotaValid
14} from '../../helpers' 14} from '../../helpers'
15import { VideoResolution } from '../../../shared'
15 16
16import { addMethodsToModel } from '../utils' 17import { addMethodsToModel } from '../utils'
17import { 18import {
@@ -245,7 +246,7 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
245 // attributes = [] because we don't want other fields than the sum 246 // attributes = [] because we don't want other fields than the sum
246 const query = { 247 const query = {
247 where: { 248 where: {
248 resolution: 0 // Original, TODO: improve readability 249 resolution: VideoResolution.ORIGINAL
249 }, 250 },
250 include: [ 251 include: [
251 { 252 {
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index fb31c6a8f..340426f45 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -7,60 +7,17 @@ import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
7 7
8// Don't use barrel, import just what we need 8// Don't use barrel, import just what we need
9import { Video as FormattedVideo } from '../../../shared/models/videos/video.model' 9import { Video as FormattedVideo } from '../../../shared/models/videos/video.model'
10import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
11import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
10import { ResultList } from '../../../shared/models/result-list.model' 12import { ResultList } from '../../../shared/models/result-list.model'
11 13
12export type FormattedRemoteVideoFile = {
13 infoHash: string
14 resolution: number
15 extname: string
16 size: number
17}
18
19export type FormattedAddRemoteVideo = {
20 uuid: string
21 name: string
22 category: number
23 licence: number
24 language: number
25 nsfw: boolean
26 description: string
27 author: string
28 duration: number
29 thumbnailData: string
30 tags: string[]
31 createdAt: Date
32 updatedAt: Date
33 views: number
34 likes: number
35 dislikes: number
36 files: FormattedRemoteVideoFile[]
37}
38
39export type FormattedUpdateRemoteVideo = {
40 uuid: string
41 name: string
42 category: number
43 licence: number
44 language: number
45 nsfw: boolean
46 description: string
47 author: string
48 duration: number
49 tags: string[]
50 createdAt: Date
51 updatedAt: Date
52 views: number
53 likes: number
54 dislikes: number
55 files: FormattedRemoteVideoFile[]
56}
57
58export namespace VideoMethods { 14export namespace VideoMethods {
59 export type GetThumbnailName = (this: VideoInstance) => string 15 export type GetThumbnailName = (this: VideoInstance) => string
60 export type GetPreviewName = (this: VideoInstance) => string 16 export type GetPreviewName = (this: VideoInstance) => string
61 export type IsOwned = (this: VideoInstance) => boolean 17 export type IsOwned = (this: VideoInstance) => boolean
62 export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo 18 export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
63 19
20 export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
64 export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string 21 export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
65 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string 22 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
66 export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string 23 export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
@@ -69,10 +26,12 @@ export namespace VideoMethods {
69 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string 26 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
70 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> 27 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
71 28
72 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormattedAddRemoteVideo> 29 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData>
73 export type ToUpdateRemoteJSON = (this: VideoInstance) => FormattedUpdateRemoteVideo 30 export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
74 31
75 export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise<void> 32 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
33 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
34 export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number>
76 35
77 // Return thumbnail name 36 // Return thumbnail name
78 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 37 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@@ -147,6 +106,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
147 createPreview: VideoMethods.CreatePreview 106 createPreview: VideoMethods.CreatePreview
148 createThumbnail: VideoMethods.CreateThumbnail 107 createThumbnail: VideoMethods.CreateThumbnail
149 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash 108 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
109 getOriginalFile: VideoMethods.GetOriginalFile
150 generateMagnetUri: VideoMethods.GenerateMagnetUri 110 generateMagnetUri: VideoMethods.GenerateMagnetUri
151 getPreviewName: VideoMethods.GetPreviewName 111 getPreviewName: VideoMethods.GetPreviewName
152 getThumbnailName: VideoMethods.GetThumbnailName 112 getThumbnailName: VideoMethods.GetThumbnailName
@@ -161,9 +121,12 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
161 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 121 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
162 toFormattedJSON: VideoMethods.ToFormattedJSON 122 toFormattedJSON: VideoMethods.ToFormattedJSON
163 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 123 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
164 transcodeVideofile: VideoMethods.TranscodeVideofile 124 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
125 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
126 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
165 127
166 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> 128 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
129 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
167 setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string> 130 setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
168} 131}
169 132
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'
27import { 28import {
28 CONFIG, 29 CONFIG,
@@ -35,7 +36,8 @@ import {
35 VIDEO_FILE_RESOLUTIONS 36 VIDEO_FILE_RESOLUTIONS
36} from '../../initializers' 37} from '../../initializers'
37import { removeVideoToFriends } from '../../lib' 38import { removeVideoToFriends } from '../../lib'
38import { VideoFileInstance } from './video-file-interface' 39import { VideoResolution } from '../../../shared'
40import { VideoFileInstance, VideoFileModel } from './video-file-interface'
39 41
40import { addMethodsToModel, getSort } from '../utils' 42import { addMethodsToModel, getSort } from '../utils'
41import { 43import {
@@ -46,6 +48,7 @@ import {
46} from './video-interface' 48} from './video-interface'
47 49
48let Video: Sequelize.Model<VideoInstance, VideoAttributes> 50let Video: Sequelize.Model<VideoInstance, VideoAttributes>
51let getOriginalFile: VideoMethods.GetOriginalFile
49let generateMagnetUri: VideoMethods.GenerateMagnetUri 52let generateMagnetUri: VideoMethods.GenerateMagnetUri
50let getVideoFilename: VideoMethods.GetVideoFilename 53let getVideoFilename: VideoMethods.GetVideoFilename
51let getThumbnailName: VideoMethods.GetThumbnailName 54let getThumbnailName: VideoMethods.GetThumbnailName
@@ -55,11 +58,13 @@ let isOwned: VideoMethods.IsOwned
55let toFormattedJSON: VideoMethods.ToFormattedJSON 58let toFormattedJSON: VideoMethods.ToFormattedJSON
56let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 59let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
57let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 60let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
58let transcodeVideofile: VideoMethods.TranscodeVideofile 61let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
62let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
59let createPreview: VideoMethods.CreatePreview 63let createPreview: VideoMethods.CreatePreview
60let createThumbnail: VideoMethods.CreateThumbnail 64let createThumbnail: VideoMethods.CreateThumbnail
61let getVideoFilePath: VideoMethods.GetVideoFilePath 65let getVideoFilePath: VideoMethods.GetVideoFilePath
62let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash 66let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
67let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
63 68
64let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 69let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
65let getDurationFromFile: VideoMethods.GetDurationFromFile 70let 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
338getOriginalFile = 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
330getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { 344getVideoFilename = 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
335getThumbnailName = function (this: VideoInstance) { 348getThumbnailName = function (this: VideoInstance) {
@@ -345,8 +358,7 @@ getPreviewName = function (this: VideoInstance) {
345 358
346getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { 359getTorrentFileName = 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
352isOwned = function (this: VideoInstance) { 364isOwned = function (this: VideoInstance) {
@@ -552,9 +564,10 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
552 return json 564 return json
553} 565}
554 566
555transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) { 567optimizeOriginalVideofile = 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
616transcodeOriginalVideofile = 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
671getOriginalFileHeight = 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
597removeThumbnail = function (this: VideoInstance) { 684removeThumbnail = 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)