aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actors/image.ts89
-rw-r--r--server/lib/activitypub/actors/shared/creator.ts16
-rw-r--r--server/lib/activitypub/actors/shared/object-to-model-attributes.ts56
-rw-r--r--server/lib/activitypub/actors/updater.ts12
-rw-r--r--server/lib/actor-image.ts14
-rw-r--r--server/lib/auth/oauth-model.ts20
-rw-r--r--server/lib/client-html.ts24
-rw-r--r--server/lib/hls.ts6
-rw-r--r--server/lib/job-queue/handlers/video-edition.ts229
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts12
-rw-r--r--server/lib/job-queue/handlers/video-import.ts8
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts8
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts10
-rw-r--r--server/lib/job-queue/job-queue.ts9
-rw-r--r--server/lib/live/live-manager.ts14
-rw-r--r--server/lib/live/shared/muxing-session.ts4
-rw-r--r--server/lib/local-actor.ts89
-rw-r--r--server/lib/notifier/shared/comment/comment-mention.ts2
-rw-r--r--server/lib/notifier/shared/comment/new-comment-for-video-owner.ts2
-rw-r--r--server/lib/plugins/plugin-helpers-builder.ts2
-rw-r--r--server/lib/plugins/register-helpers.ts2
-rw-r--r--server/lib/server-config-manager.ts5
-rw-r--r--server/lib/thumbnail.ts5
-rw-r--r--server/lib/transcoding/default-transcoding-profiles.ts (renamed from server/lib/transcoding/video-transcoding-profiles.ts)25
-rw-r--r--server/lib/transcoding/transcoding.ts (renamed from server/lib/transcoding/video-transcoding.ts)35
-rw-r--r--server/lib/user.ts61
-rw-r--r--server/lib/video-editor.ts32
-rw-r--r--server/lib/video.ts6
28 files changed, 592 insertions, 205 deletions
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts
index 443ad0a63..d17c2ef1a 100644
--- a/server/lib/activitypub/actors/image.ts
+++ b/server/lib/activitypub/actors/image.ts
@@ -12,53 +12,52 @@ type ImageInfo = {
12 onDisk?: boolean 12 onDisk?: boolean
13} 13}
14 14
15async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { 15async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) {
16 const oldImageModel = type === ActorImageType.AVATAR 16 const avatarsOrBanners = type === ActorImageType.AVATAR
17 ? actor.Avatar 17 ? actor.Avatars
18 : actor.Banner 18 : actor.Banners
19 19
20 if (oldImageModel) { 20 if (imagesInfo.length === 0) {
21 // Don't update the avatar if the file URL did not change 21 await deleteActorImages(actor, type, t)
22 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor 22 }
23
24 for (const imageInfo of imagesInfo) {
25 const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width)
23 26
24 try { 27 if (oldImageModel) {
25 await oldImageModel.destroy({ transaction: t }) 28 // Don't update the avatar if the file URL did not change
29 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) {
30 continue
31 }
26 32
27 setActorImage(actor, type, null) 33 await safeDeleteActorImage(actor, oldImageModel, type, t)
28 } catch (err) {
29 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
30 } 34 }
31 }
32 35
33 if (imageInfo) {
34 const imageModel = await ActorImageModel.create({ 36 const imageModel = await ActorImageModel.create({
35 filename: imageInfo.name, 37 filename: imageInfo.name,
36 onDisk: imageInfo.onDisk ?? false, 38 onDisk: imageInfo.onDisk ?? false,
37 fileUrl: imageInfo.fileUrl, 39 fileUrl: imageInfo.fileUrl,
38 height: imageInfo.height, 40 height: imageInfo.height,
39 width: imageInfo.width, 41 width: imageInfo.width,
40 type 42 type,
43 actorId: actor.id
41 }, { transaction: t }) 44 }, { transaction: t })
42 45
43 setActorImage(actor, type, imageModel) 46 addActorImage(actor, type, imageModel)
44 } 47 }
45 48
46 return actor 49 return actor
47} 50}
48 51
49async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { 52async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) {
50 try { 53 try {
51 if (type === ActorImageType.AVATAR) { 54 const association = buildAssociationName(type)
52 await actor.Avatar.destroy({ transaction: t })
53
54 actor.avatarId = null
55 actor.Avatar = null
56 } else {
57 await actor.Banner.destroy({ transaction: t })
58 55
59 actor.bannerId = null 56 for (const image of actor[association]) {
60 actor.Banner = null 57 await image.destroy({ transaction: t })
61 } 58 }
59
60 actor[association] = []
62 } catch (err) { 61 } catch (err) {
63 logger.error('Cannot remove old image of actor %s.', actor.url, { err }) 62 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
64 } 63 }
@@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy
66 return actor 65 return actor
67} 66}
68 67
68async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) {
69 try {
70 await toDelete.destroy({ transaction: t })
71
72 const association = buildAssociationName(type)
73 actor[association] = actor[association].filter(image => image.id !== toDelete.id)
74 } catch (err) {
75 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
76 }
77}
78
69// --------------------------------------------------------------------------- 79// ---------------------------------------------------------------------------
70 80
71export { 81export {
72 ImageInfo, 82 ImageInfo,
73 83
74 updateActorImageInstance, 84 updateActorImages,
75 deleteActorImageInstance 85 deleteActorImages
76} 86}
77 87
78// --------------------------------------------------------------------------- 88// ---------------------------------------------------------------------------
79 89
80function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { 90function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) {
81 const id = imageModel 91 const association = buildAssociationName(type)
82 ? imageModel.id 92 if (!actor[association]) actor[association] = []
83 : null 93
84 94 actor[association].push(imageModel)
85 if (type === ActorImageType.AVATAR) { 95}
86 actorModel.avatarId = id
87 actorModel.Avatar = imageModel
88 } else {
89 actorModel.bannerId = id
90 actorModel.Banner = imageModel
91 }
92 96
93 return actorModel 97function buildAssociationName (type: ActorImageType) {
98 return type === ActorImageType.AVATAR
99 ? 'Avatars'
100 : 'Banners'
94} 101}
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts
index 999aed97d..500bc9912 100644
--- a/server/lib/activitypub/actors/shared/creator.ts
+++ b/server/lib/activitypub/actors/shared/creator.ts
@@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server'
6import { VideoChannelModel } from '@server/models/video/video-channel' 6import { VideoChannelModel } from '@server/models/video/video-channel'
7import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' 7import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
8import { ActivityPubActor, ActorImageType } from '@shared/models' 8import { ActivityPubActor, ActorImageType } from '@shared/models'
9import { updateActorImageInstance } from '../image' 9import { updateActorImages } from '../image'
10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' 10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes'
11import { fetchActorFollowsCount } from './url-to-object' 11import { fetchActorFollowsCount } from './url-to-object'
12 12
13export class APActorCreator { 13export class APActorCreator {
@@ -27,11 +27,11 @@ export class APActorCreator {
27 return sequelizeTypescript.transaction(async t => { 27 return sequelizeTypescript.transaction(async t => {
28 const server = await this.setServer(actorInstance, t) 28 const server = await this.setServer(actorInstance, t)
29 29
30 await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t)
31 await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t)
32
33 const { actorCreated, created } = await this.saveActor(actorInstance, t) 30 const { actorCreated, created } = await this.saveActor(actorInstance, t)
34 31
32 await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t)
33 await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t)
34
35 await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) 35 await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)
36 36
37 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance 37 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
@@ -71,10 +71,10 @@ export class APActorCreator {
71 } 71 }
72 72
73 private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { 73 private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
74 const imageInfo = getImageInfoFromObject(this.actorObject, type) 74 const imagesInfo = getImagesInfoFromObject(this.actorObject, type)
75 if (!imageInfo) return 75 if (imagesInfo.length === 0) return
76 76
77 return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) 77 return updateActorImages(actor as MActorImages, type, imagesInfo, t)
78 } 78 }
79 79
80 private async saveActor (actor: MActor, t: Transaction) { 80 private async saveActor (actor: MActor, t: Transaction) {
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
index 23bc972e5..f6a78c457 100644
--- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
@@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor'
4import { FilteredModelAttributes } from '@server/types' 4import { FilteredModelAttributes } from '@server/types'
5import { getLowercaseExtension } from '@shared/core-utils' 5import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
7import { ActivityPubActor, ActorImageType } from '@shared/models' 7import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models'
8 8
9function getActorAttributesFromObject ( 9function getActorAttributesFromObject (
10 actorObject: ActivityPubActor, 10 actorObject: ActivityPubActor,
@@ -30,33 +30,36 @@ function getActorAttributesFromObject (
30 } 30 }
31} 31}
32 32
33function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { 33function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
34 const mimetypes = MIMETYPES.IMAGE 34 const iconsOrImages = type === ActorImageType.AVATAR
35 const icon = type === ActorImageType.AVATAR 35 ? actorObject.icons || actorObject.icon
36 ? actorObject.icon
37 : actorObject.image 36 : actorObject.image
38 37
39 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 38 return normalizeIconOrImage(iconsOrImages).map(iconOrImage => {
39 const mimetypes = MIMETYPES.IMAGE
40 40
41 let extension: string 41 if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined
42 42
43 if (icon.mediaType) { 43 let extension: string
44 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
45 } else {
46 const tmp = getLowercaseExtension(icon.url)
47 44
48 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp 45 if (iconOrImage.mediaType) {
49 } 46 extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType]
47 } else {
48 const tmp = getLowercaseExtension(iconOrImage.url)
50 49
51 if (!extension) return undefined 50 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
51 }
52 52
53 return { 53 if (!extension) return undefined
54 name: buildUUID() + extension, 54
55 fileUrl: icon.url, 55 return {
56 height: icon.height, 56 name: buildUUID() + extension,
57 width: icon.width, 57 fileUrl: iconOrImage.url,
58 type 58 height: iconOrImage.height,
59 } 59 width: iconOrImage.width,
60 type
61 }
62 })
60} 63}
61 64
62function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { 65function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
@@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
65 68
66export { 69export {
67 getActorAttributesFromObject, 70 getActorAttributesFromObject,
68 getImageInfoFromObject, 71 getImagesInfoFromObject,
69 getActorDisplayNameFromObject 72 getActorDisplayNameFromObject
70} 73}
74
75// ---------------------------------------------------------------------------
76
77function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] {
78 if (Array.isArray(icon)) return icon
79 if (icon) return [ icon ]
80
81 return []
82}
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts
index 042438d9c..fe94af9f1 100644
--- a/server/lib/activitypub/actors/updater.ts
+++ b/server/lib/activitypub/actors/updater.ts
@@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' 5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models'
6import { ActivityPubActor, ActorImageType } from '@shared/models' 6import { ActivityPubActor, ActorImageType } from '@shared/models'
7import { getOrCreateAPOwner } from './get' 7import { getOrCreateAPOwner } from './get'
8import { updateActorImageInstance } from './image' 8import { updateActorImages } from './image'
9import { fetchActorFollowsCount } from './shared' 9import { fetchActorFollowsCount } from './shared'
10import { getImageInfoFromObject } from './shared/object-to-model-attributes' 10import { getImagesInfoFromObject } from './shared/object-to-model-attributes'
11 11
12export class APActorUpdater { 12export class APActorUpdater {
13 13
@@ -29,8 +29,8 @@ export class APActorUpdater {
29 } 29 }
30 30
31 async update () { 31 async update () {
32 const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) 32 const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR)
33 const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) 33 const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER)
34 34
35 try { 35 try {
36 await this.updateActorInstance(this.actor, this.actorObject) 36 await this.updateActorInstance(this.actor, this.actorObject)
@@ -47,8 +47,8 @@ export class APActorUpdater {
47 } 47 }
48 48
49 await runInReadCommittedTransaction(async t => { 49 await runInReadCommittedTransaction(async t => {
50 await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) 50 await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t)
51 await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) 51 await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t)
52 }) 52 })
53 53
54 await runInReadCommittedTransaction(async t => { 54 await runInReadCommittedTransaction(async t => {
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts
new file mode 100644
index 000000000..e9bd148f6
--- /dev/null
+++ b/server/lib/actor-image.ts
@@ -0,0 +1,14 @@
1import maxBy from 'lodash/maxBy'
2
3function getBiggestActorImage <T extends { width: number }> (images: T[]) {
4 const image = maxBy(images, 'width')
5
6 // If width is null, maxBy won't return a value
7 if (!image) return images[0]
8
9 return image
10}
11
12export {
13 getBiggestActorImage
14}
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts
index 5d68f44e9..910fdeec1 100644
--- a/server/lib/auth/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -5,14 +5,14 @@ import { ActorModel } from '@server/models/actor/actor'
5import { MOAuthClient } from '@server/types/models' 5import { MOAuthClient } from '@server/types/models'
6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
7import { MUser } from '@server/types/models/user/user' 7import { MUser } from '@server/types/models/user/user'
8import { UserAdminFlag } from '@shared/models/users/user-flag.model' 8import { pick } from '@shared/core-utils'
9import { UserRole } from '@shared/models/users/user-role' 9import { UserRole } from '@shared/models/users/user-role'
10import { logger } from '../../helpers/logger' 10import { logger } from '../../helpers/logger'
11import { CONFIG } from '../../initializers/config' 11import { CONFIG } from '../../initializers/config'
12import { OAuthClientModel } from '../../models/oauth/oauth-client' 12import { OAuthClientModel } from '../../models/oauth/oauth-client'
13import { OAuthTokenModel } from '../../models/oauth/oauth-token' 13import { OAuthTokenModel } from '../../models/oauth/oauth-token'
14import { UserModel } from '../../models/user/user' 14import { UserModel } from '../../models/user/user'
15import { createUserAccountAndChannelAndPlaylist } from '../user' 15import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user'
16import { TokensCache } from './tokens-cache' 16import { TokensCache } from './tokens-cache'
17 17
18type TokenInfo = { 18type TokenInfo = {
@@ -229,19 +229,13 @@ async function createUserFromExternal (pluginAuth: string, options: {
229 const actor = await ActorModel.loadLocalByName(options.username) 229 const actor = await ActorModel.loadLocalByName(options.username)
230 if (actor) return null 230 if (actor) return null
231 231
232 const userToCreate = new UserModel({ 232 const userToCreate = buildUser({
233 username: options.username, 233 ...pick(options, [ 'username', 'email', 'role' ]),
234
235 emailVerified: null,
234 password: null, 236 password: null,
235 email: options.email,
236 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
237 p2pEnabled: CONFIG.DEFAULTS.P2P.WEBAPP.ENABLED,
238 autoPlayVideo: true,
239 role: options.role,
240 videoQuota: CONFIG.USER.VIDEO_QUOTA,
241 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
242 adminFlags: UserAdminFlag.NONE,
243 pluginAuth 237 pluginAuth
244 }) as MUser 238 })
245 239
246 const { user } = await createUserAccountAndChannelAndPlaylist({ 240 const { user } = await createUserAccountAndChannelAndPlaylist({
247 userToCreate, 241 userToCreate,
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 19354ab70..945bc712f 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -1,8 +1,10 @@
1import express from 'express' 1import express from 'express'
2import { readFile } from 'fs-extra' 2import { readFile } from 'fs-extra'
3import memoizee from 'memoizee'
3import { join } from 'path' 4import { join } from 'path'
4import validator from 'validator' 5import validator from 'validator'
5import { toCompleteUUID } from '@server/helpers/custom-validators/misc' 6import { toCompleteUUID } from '@server/helpers/custom-validators/misc'
7import { ActorImageModel } from '@server/models/actor/actor-image'
6import { root } from '@shared/core-utils' 8import { root } from '@shared/core-utils'
7import { escapeHTML } from '@shared/core-utils/renderer' 9import { escapeHTML } from '@shared/core-utils/renderer'
8import { sha256 } from '@shared/extra-utils' 10import { sha256 } from '@shared/extra-utils'
@@ -16,10 +18,11 @@ import { mdToOneLinePlainText } from '../helpers/markdown'
16import { CONFIG } from '../initializers/config' 18import { CONFIG } from '../initializers/config'
17import { 19import {
18 ACCEPT_HEADERS, 20 ACCEPT_HEADERS,
19 ACTOR_IMAGES_SIZE,
20 CUSTOM_HTML_TAG_COMMENTS, 21 CUSTOM_HTML_TAG_COMMENTS,
21 EMBED_SIZE, 22 EMBED_SIZE,
22 FILES_CONTENT_HASH, 23 FILES_CONTENT_HASH,
24 MEMOIZE_LENGTH,
25 MEMOIZE_TTL,
23 PLUGIN_GLOBAL_CSS_PATH, 26 PLUGIN_GLOBAL_CSS_PATH,
24 WEBSERVER 27 WEBSERVER
25} from '../initializers/constants' 28} from '../initializers/constants'
@@ -29,8 +32,14 @@ import { VideoModel } from '../models/video/video'
29import { VideoChannelModel } from '../models/video/video-channel' 32import { VideoChannelModel } from '../models/video/video-channel'
30import { VideoPlaylistModel } from '../models/video/video-playlist' 33import { VideoPlaylistModel } from '../models/video/video-playlist'
31import { MAccountActor, MChannelActor } from '../types/models' 34import { MAccountActor, MChannelActor } from '../types/models'
35import { getBiggestActorImage } from './actor-image'
32import { ServerConfigManager } from './server-config-manager' 36import { ServerConfigManager } from './server-config-manager'
33 37
38const getPlainTextDescriptionCached = memoizee(mdToOneLinePlainText, {
39 maxAge: MEMOIZE_TTL.MD_TO_PLAIN_TEXT_CLIENT_HTML,
40 max: MEMOIZE_LENGTH.MD_TO_PLAIN_TEXT_CLIENT_HTML
41})
42
34type Tags = { 43type Tags = {
35 ogType: string 44 ogType: string
36 twitterCard: 'player' | 'summary' | 'summary_large_image' 45 twitterCard: 'player' | 'summary' | 'summary_large_image'
@@ -103,7 +112,7 @@ class ClientHtml {
103 res.status(HttpStatusCode.NOT_FOUND_404) 112 res.status(HttpStatusCode.NOT_FOUND_404)
104 return html 113 return html
105 } 114 }
106 const description = mdToOneLinePlainText(video.description) 115 const description = getPlainTextDescriptionCached(video.description)
107 116
108 let customHtml = ClientHtml.addTitleTag(html, video.name) 117 let customHtml = ClientHtml.addTitleTag(html, video.name)
109 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 118 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -164,7 +173,7 @@ class ClientHtml {
164 return html 173 return html
165 } 174 }
166 175
167 const description = mdToOneLinePlainText(videoPlaylist.description) 176 const description = getPlainTextDescriptionCached(videoPlaylist.description)
168 177
169 let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name) 178 let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name)
170 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 179 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -263,7 +272,7 @@ class ClientHtml {
263 return ClientHtml.getIndexHTML(req, res) 272 return ClientHtml.getIndexHTML(req, res)
264 } 273 }
265 274
266 const description = mdToOneLinePlainText(entity.description) 275 const description = getPlainTextDescriptionCached(entity.description)
267 276
268 let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName()) 277 let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName())
269 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 278 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -273,10 +282,11 @@ class ClientHtml {
273 const siteName = CONFIG.INSTANCE.NAME 282 const siteName = CONFIG.INSTANCE.NAME
274 const title = entity.getDisplayName() 283 const title = entity.getDisplayName()
275 284
285 const avatar = getBiggestActorImage(entity.Actor.Avatars)
276 const image = { 286 const image = {
277 url: entity.Actor.getAvatarUrl(), 287 url: ActorImageModel.getImageUrl(avatar),
278 width: ACTOR_IMAGES_SIZE.AVATARS.width, 288 width: avatar?.width,
279 height: ACTOR_IMAGES_SIZE.AVATARS.height 289 height: avatar?.height
280 } 290 }
281 291
282 const ogType = 'website' 292 const ogType = 'website'
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 985f50587..43043315b 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -4,7 +4,7 @@ import { basename, dirname, join } from 'path'
4import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' 4import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models'
5import { sha256 } from '@shared/extra-utils' 5import { sha256 } from '@shared/extra-utils'
6import { VideoStorage } from '@shared/models' 6import { VideoStorage } from '@shared/models'
7import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' 7import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg'
8import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
9import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' 9import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
10import { generateRandomString } from '../helpers/utils' 10import { generateRandomString } from '../helpers/utils'
@@ -40,10 +40,10 @@ async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlayl
40 const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) 40 const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
41 41
42 await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { 42 await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => {
43 const size = await getVideoStreamSize(videoFilePath) 43 const size = await getVideoStreamDimensionsInfo(videoFilePath)
44 44
45 const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) 45 const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file)
46 const resolution = `RESOLUTION=${size.width}x${size.height}` 46 const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}`
47 47
48 let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` 48 let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}`
49 if (file.fps) line += ',FRAME-RATE=' + file.fps 49 if (file.fps) line += ',FRAME-RATE=' + file.fps
diff --git a/server/lib/job-queue/handlers/video-edition.ts b/server/lib/job-queue/handlers/video-edition.ts
new file mode 100644
index 000000000..c5ba0452f
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-edition.ts
@@ -0,0 +1,229 @@
1import { Job } from 'bull'
2import { move, remove } from 'fs-extra'
3import { join } from 'path'
4import { addIntroOutro, addWatermark, cutVideo } from '@server/helpers/ffmpeg'
5import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent'
6import { CONFIG } from '@server/initializers/config'
7import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
8import { generateWebTorrentVideoFilename } from '@server/lib/paths'
9import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
10import { isAbleToUploadVideo } from '@server/lib/user'
11import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video'
12import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor'
13import { VideoPathManager } from '@server/lib/video-path-manager'
14import { buildNextVideoState } from '@server/lib/video-state'
15import { UserModel } from '@server/models/user/user'
16import { VideoModel } from '@server/models/video/video'
17import { VideoFileModel } from '@server/models/video/video-file'
18import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models'
19import { getLowercaseExtension, pick } from '@shared/core-utils'
20import {
21 buildFileMetadata,
22 buildUUID,
23 ffprobePromise,
24 getFileSize,
25 getVideoStreamDimensionsInfo,
26 getVideoStreamDuration,
27 getVideoStreamFPS
28} from '@shared/extra-utils'
29import {
30 VideoEditionPayload,
31 VideoEditionTaskPayload,
32 VideoEditorTask,
33 VideoEditorTaskCutPayload,
34 VideoEditorTaskIntroPayload,
35 VideoEditorTaskOutroPayload,
36 VideoEditorTaskWatermarkPayload,
37 VideoState
38} from '@shared/models'
39import { logger, loggerTagsFactory } from '../../../helpers/logger'
40
41const lTagsBase = loggerTagsFactory('video-edition')
42
43async function processVideoEdition (job: Job) {
44 const payload = job.data as VideoEditionPayload
45
46 logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id)
47
48 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
49
50 // No video, maybe deleted?
51 if (!video) {
52 logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID))
53 return undefined
54 }
55
56 await checkUserQuotaOrThrow(video, payload)
57
58 const inputFile = video.getMaxQualityFile()
59
60 const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
61 let tmpInputFilePath: string
62 let outputPath: string
63
64 for (const task of payload.tasks) {
65 const outputFilename = buildUUID() + inputFile.extname
66 outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
67
68 await processTask({
69 inputPath: tmpInputFilePath ?? originalFilePath,
70 video,
71 outputPath,
72 task
73 })
74
75 if (tmpInputFilePath) await remove(tmpInputFilePath)
76
77 // For the next iteration
78 tmpInputFilePath = outputPath
79 }
80
81 return outputPath
82 })
83
84 logger.info('Video edition ended for video %s.', video.uuid)
85
86 const newFile = await buildNewFile(video, editionResultPath)
87
88 const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
89 await move(editionResultPath, outputPath)
90
91 await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
92
93 await removeAllFiles(video, newFile)
94
95 await newFile.save()
96
97 video.state = buildNextVideoState()
98 video.duration = await getVideoStreamDuration(outputPath)
99 await video.save()
100
101 await federateVideoIfNeeded(video, false, undefined)
102
103 if (video.state === VideoState.TO_TRANSCODE) {
104 const user = await UserModel.loadByVideoId(video.id)
105
106 await addOptimizeOrMergeAudioJob(video, newFile, user, false)
107 } else if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
108 await addMoveToObjectStorageJob(video, false)
109 }
110}
111
112// ---------------------------------------------------------------------------
113
114export {
115 processVideoEdition
116}
117
118// ---------------------------------------------------------------------------
119
120type TaskProcessorOptions <T extends VideoEditionTaskPayload = VideoEditionTaskPayload> = {
121 inputPath: string
122 outputPath: string
123 video: MVideo
124 task: T
125}
126
127const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
128 'add-intro': processAddIntroOutro,
129 'add-outro': processAddIntroOutro,
130 'cut': processCut,
131 'add-watermark': processAddWatermark
132}
133
134async function processTask (options: TaskProcessorOptions) {
135 const { video, task } = options
136
137 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task })
138
139 const processor = taskProcessors[options.task.name]
140 if (!process) throw new Error('Unknown task ' + task.name)
141
142 return processor(options)
143}
144
145function processAddIntroOutro (options: TaskProcessorOptions<VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload>) {
146 const { task } = options
147
148 return addIntroOutro({
149 ...pick(options, [ 'inputPath', 'outputPath' ]),
150
151 introOutroPath: task.options.file,
152 type: task.name === 'add-intro'
153 ? 'intro'
154 : 'outro',
155
156 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
157 profile: CONFIG.TRANSCODING.PROFILE
158 })
159}
160
161function processCut (options: TaskProcessorOptions<VideoEditorTaskCutPayload>) {
162 const { task } = options
163
164 return cutVideo({
165 ...pick(options, [ 'inputPath', 'outputPath' ]),
166
167 start: task.options.start,
168 end: task.options.end
169 })
170}
171
172function processAddWatermark (options: TaskProcessorOptions<VideoEditorTaskWatermarkPayload>) {
173 const { task } = options
174
175 return addWatermark({
176 ...pick(options, [ 'inputPath', 'outputPath' ]),
177
178 watermarkPath: task.options.file,
179
180 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
181 profile: CONFIG.TRANSCODING.PROFILE
182 })
183}
184
185async function buildNewFile (video: MVideoId, path: string) {
186 const videoFile = new VideoFileModel({
187 extname: getLowercaseExtension(path),
188 size: await getFileSize(path),
189 metadata: await buildFileMetadata(path),
190 videoStreamingPlaylistId: null,
191 videoId: video.id
192 })
193
194 const probe = await ffprobePromise(path)
195
196 videoFile.fps = await getVideoStreamFPS(path, probe)
197 videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution
198
199 videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname)
200
201 return videoFile
202}
203
204async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) {
205 const hls = video.getHLSPlaylist()
206
207 if (hls) {
208 await video.removeStreamingPlaylistFiles(hls)
209 await hls.destroy()
210 }
211
212 for (const file of video.VideoFiles) {
213 if (file.id === webTorrentFileException.id) continue
214
215 await video.removeWebTorrentFileAndTorrent(file)
216 await file.destroy()
217 }
218}
219
220async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoEditionPayload) {
221 const user = await UserModel.loadByVideoId(video.id)
222
223 const filePathFinder = (i: number) => (payload.tasks[i] as VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload).options.file
224
225 const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder)
226 if (await isAbleToUploadVideo(user.id, additionalBytes) === false) {
227 throw new Error('Quota exceeded for this user to edit the video')
228 }
229}
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 0d9e80cb8..6b2d60317 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -1,18 +1,18 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { copy, stat } from 'fs-extra' 2import { copy, stat } from 'fs-extra'
3import { getLowercaseExtension } from '@shared/core-utils'
4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
5import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
6import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' 5import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
7import { generateWebTorrentVideoFilename } from '@server/lib/paths' 6import { generateWebTorrentVideoFilename } from '@server/lib/paths'
8import { addMoveToObjectStorageJob } from '@server/lib/video' 7import { addMoveToObjectStorageJob } from '@server/lib/video'
9import { VideoPathManager } from '@server/lib/video-path-manager' 8import { VideoPathManager } from '@server/lib/video-path-manager'
9import { VideoModel } from '@server/models/video/video'
10import { VideoFileModel } from '@server/models/video/video-file'
10import { MVideoFullLight } from '@server/types/models' 11import { MVideoFullLight } from '@server/types/models'
12import { getLowercaseExtension } from '@shared/core-utils'
11import { VideoFileImportPayload, VideoStorage } from '@shared/models' 13import { VideoFileImportPayload, VideoStorage } from '@shared/models'
12import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 14import { getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg'
13import { logger } from '../../../helpers/logger' 15import { logger } from '../../../helpers/logger'
14import { VideoModel } from '../../../models/video/video'
15import { VideoFileModel } from '../../../models/video/video-file'
16 16
17async function processVideoFileImport (job: Job) { 17async function processVideoFileImport (job: Job) {
18 const payload = job.data as VideoFileImportPayload 18 const payload = job.data as VideoFileImportPayload
@@ -45,9 +45,9 @@ export {
45// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
46 46
47async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { 47async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
48 const { resolution } = await getVideoFileResolution(inputFilePath) 48 const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath)
49 const { size } = await stat(inputFilePath) 49 const { size } = await stat(inputFilePath)
50 const fps = await getVideoFileFPS(inputFilePath) 50 const fps = await getVideoStreamFPS(inputFilePath)
51 51
52 const fileExt = getLowercaseExtension(inputFilePath) 52 const fileExt = getLowercaseExtension(inputFilePath)
53 53
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index b6e05d8f5..b3ca28c2f 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -25,7 +25,7 @@ import {
25 VideoResolution, 25 VideoResolution,
26 VideoState 26 VideoState
27} from '@shared/models' 27} from '@shared/models'
28import { ffprobePromise, getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 28import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg'
29import { logger } from '../../../helpers/logger' 29import { logger } from '../../../helpers/logger'
30import { getSecureTorrentName } from '../../../helpers/utils' 30import { getSecureTorrentName } from '../../../helpers/utils'
31import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' 31import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
@@ -121,10 +121,10 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
121 121
122 const { resolution } = await isAudioFile(tempVideoPath, probe) 122 const { resolution } = await isAudioFile(tempVideoPath, probe)
123 ? { resolution: VideoResolution.H_NOVIDEO } 123 ? { resolution: VideoResolution.H_NOVIDEO }
124 : await getVideoFileResolution(tempVideoPath) 124 : await getVideoStreamDimensionsInfo(tempVideoPath)
125 125
126 const fps = await getVideoFileFPS(tempVideoPath, probe) 126 const fps = await getVideoStreamFPS(tempVideoPath, probe)
127 const duration = await getDurationFromVideoFile(tempVideoPath, probe) 127 const duration = await getVideoStreamDuration(tempVideoPath, probe)
128 128
129 // Prepare video file object for creation in database 129 // Prepare video file object for creation in database
130 const fileExt = getLowercaseExtension(tempVideoPath) 130 const fileExt = getLowercaseExtension(tempVideoPath)
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index a04cfa2c9..497f6612a 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,12 +1,12 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { pathExists, readdir, remove } from 'fs-extra' 2import { pathExists, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 4import { ffprobePromise, getAudioStream, getVideoStreamDuration, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg'
5import { VIDEO_LIVE } from '@server/initializers/constants' 5import { VIDEO_LIVE } from '@server/initializers/constants'
6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' 6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live'
7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' 7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths'
8import { generateVideoMiniature } from '@server/lib/thumbnail' 8import { generateVideoMiniature } from '@server/lib/thumbnail'
9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' 9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding'
10import { VideoPathManager } from '@server/lib/video-path-manager' 10import { VideoPathManager } from '@server/lib/video-path-manager'
11import { moveToNextState } from '@server/lib/video-state' 11import { moveToNextState } from '@server/lib/video-state'
12import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
@@ -96,7 +96,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
96 const probe = await ffprobePromise(concatenatedTsFilePath) 96 const probe = await ffprobePromise(concatenatedTsFilePath)
97 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) 97 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe)
98 98
99 const { resolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) 99 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
100 100
101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ 101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
102 video: videoWithFiles, 102 video: videoWithFiles,
@@ -107,7 +107,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
107 }) 107 })
108 108
109 if (!durationDone) { 109 if (!durationDone) {
110 videoWithFiles.duration = await getDurationFromVideoFile(outputPath) 110 videoWithFiles.duration = await getVideoStreamDuration(outputPath)
111 await videoWithFiles.save() 111 await videoWithFiles.save()
112 112
113 durationDone = true 113 durationDone = true
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 5540b791d..512979734 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -1,5 +1,5 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' 2import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg'
3import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' 3import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video'
4import { VideoPathManager } from '@server/lib/video-path-manager' 4import { VideoPathManager } from '@server/lib/video-path-manager'
5import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' 5import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
@@ -16,7 +16,7 @@ import {
16 VideoTranscodingPayload 16 VideoTranscodingPayload
17} from '@shared/models' 17} from '@shared/models'
18import { retryTransactionWrapper } from '../../../helpers/database-utils' 18import { retryTransactionWrapper } from '../../../helpers/database-utils'
19import { computeLowerResolutionsToTranscode } from '../../../helpers/ffprobe-utils' 19import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg'
20import { logger, loggerTagsFactory } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { CONFIG } from '../../../initializers/config' 21import { CONFIG } from '../../../initializers/config'
22import { VideoModel } from '../../../models/video/video' 22import { VideoModel } from '../../../models/video/video'
@@ -25,7 +25,7 @@ import {
25 mergeAudioVideofile, 25 mergeAudioVideofile,
26 optimizeOriginalVideofile, 26 optimizeOriginalVideofile,
27 transcodeNewWebTorrentResolution 27 transcodeNewWebTorrentResolution
28} from '../../transcoding/video-transcoding' 28} from '../../transcoding/transcoding'
29 29
30type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> 30type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void>
31 31
@@ -174,10 +174,10 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
174async function onVideoFirstWebTorrentTranscoding ( 174async function onVideoFirstWebTorrentTranscoding (
175 videoArg: MVideoWithFile, 175 videoArg: MVideoWithFile,
176 payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload, 176 payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload,
177 transcodeType: TranscodeOptionsType, 177 transcodeType: TranscodeVODOptionsType,
178 user: MUserId 178 user: MUserId
179) { 179) {
180 const { resolution, isPortraitMode, audioStream } = await videoArg.getMaxQualityFileInfo() 180 const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile()
181 181
182 // Maybe the video changed in database, refresh it 182 // Maybe the video changed in database, refresh it
183 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) 183 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid)
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 22bd1f5d2..e10a3bab5 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -14,6 +14,7 @@ import {
14 JobType, 14 JobType,
15 MoveObjectStoragePayload, 15 MoveObjectStoragePayload,
16 RefreshPayload, 16 RefreshPayload,
17 VideoEditionPayload,
17 VideoFileImportPayload, 18 VideoFileImportPayload,
18 VideoImportPayload, 19 VideoImportPayload,
19 VideoLiveEndingPayload, 20 VideoLiveEndingPayload,
@@ -31,6 +32,7 @@ import { refreshAPObject } from './handlers/activitypub-refresher'
31import { processActorKeys } from './handlers/actor-keys' 32import { processActorKeys } from './handlers/actor-keys'
32import { processEmail } from './handlers/email' 33import { processEmail } from './handlers/email'
33import { processMoveToObjectStorage } from './handlers/move-to-object-storage' 34import { processMoveToObjectStorage } from './handlers/move-to-object-storage'
35import { processVideoEdition } from './handlers/video-edition'
34import { processVideoFileImport } from './handlers/video-file-import' 36import { processVideoFileImport } from './handlers/video-file-import'
35import { processVideoImport } from './handlers/video-import' 37import { processVideoImport } from './handlers/video-import'
36import { processVideoLiveEnding } from './handlers/video-live-ending' 38import { processVideoLiveEnding } from './handlers/video-live-ending'
@@ -53,6 +55,7 @@ type CreateJobArgument =
53 { type: 'actor-keys', payload: ActorKeysPayload } | 55 { type: 'actor-keys', payload: ActorKeysPayload } |
54 { type: 'video-redundancy', payload: VideoRedundancyPayload } | 56 { type: 'video-redundancy', payload: VideoRedundancyPayload } |
55 { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } | 57 { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } |
58 { type: 'video-edition', payload: VideoEditionPayload } |
56 { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } 59 { type: 'move-to-object-storage', payload: MoveObjectStoragePayload }
57 60
58export type CreateJobOptions = { 61export type CreateJobOptions = {
@@ -75,7 +78,8 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
75 'video-live-ending': processVideoLiveEnding, 78 'video-live-ending': processVideoLiveEnding,
76 'actor-keys': processActorKeys, 79 'actor-keys': processActorKeys,
77 'video-redundancy': processVideoRedundancy, 80 'video-redundancy': processVideoRedundancy,
78 'move-to-object-storage': processMoveToObjectStorage 81 'move-to-object-storage': processMoveToObjectStorage,
82 'video-edition': processVideoEdition
79} 83}
80 84
81const jobTypes: JobType[] = [ 85const jobTypes: JobType[] = [
@@ -93,7 +97,8 @@ const jobTypes: JobType[] = [
93 'video-redundancy', 97 'video-redundancy',
94 'actor-keys', 98 'actor-keys',
95 'video-live-ending', 99 'video-live-ending',
96 'move-to-object-storage' 100 'move-to-object-storage',
101 'video-edition'
97] 102]
98 103
99class JobQueue { 104class JobQueue {
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts
index 33e49acc1..21c34a9a4 100644
--- a/server/lib/live/live-manager.ts
+++ b/server/lib/live/live-manager.ts
@@ -5,10 +5,10 @@ import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
5import { 5import {
6 computeLowerResolutionsToTranscode, 6 computeLowerResolutionsToTranscode,
7 ffprobePromise, 7 ffprobePromise,
8 getVideoFileBitrate, 8 getVideoStreamBitrate,
9 getVideoFileFPS, 9 getVideoStreamFPS,
10 getVideoFileResolution 10 getVideoStreamDimensionsInfo
11} from '@server/helpers/ffprobe-utils' 11} from '@server/helpers/ffmpeg'
12import { logger, loggerTagsFactory } from '@server/helpers/logger' 12import { logger, loggerTagsFactory } from '@server/helpers/logger'
13import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' 13import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
14import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE } from '@server/initializers/constants' 14import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE } from '@server/initializers/constants'
@@ -226,9 +226,9 @@ class LiveManager {
226 const probe = await ffprobePromise(inputUrl) 226 const probe = await ffprobePromise(inputUrl)
227 227
228 const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ 228 const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([
229 getVideoFileResolution(inputUrl, probe), 229 getVideoStreamDimensionsInfo(inputUrl, probe),
230 getVideoFileFPS(inputUrl, probe), 230 getVideoStreamFPS(inputUrl, probe),
231 getVideoFileBitrate(inputUrl, probe) 231 getVideoStreamBitrate(inputUrl, probe)
232 ]) 232 ])
233 233
234 logger.info( 234 logger.info(
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts
index 22a47942a..f5f473039 100644
--- a/server/lib/live/shared/muxing-session.ts
+++ b/server/lib/live/shared/muxing-session.ts
@@ -5,14 +5,14 @@ import { FfmpegCommand } from 'fluent-ffmpeg'
5import { appendFile, ensureDir, readFile, stat } from 'fs-extra' 5import { appendFile, ensureDir, readFile, stat } from 'fs-extra'
6import { basename, join } from 'path' 6import { basename, join } from 'path'
7import { EventEmitter } from 'stream' 7import { EventEmitter } from 'stream'
8import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' 8import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg'
9import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' 9import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
10import { CONFIG } from '@server/initializers/config' 10import { CONFIG } from '@server/initializers/config'
11import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants' 11import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants'
12import { VideoFileModel } from '@server/models/video/video-file' 12import { VideoFileModel } from '@server/models/video/video-file'
13import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models' 13import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models'
14import { getLiveDirectory } from '../../paths' 14import { getLiveDirectory } from '../../paths'
15import { VideoTranscodingProfilesManager } from '../../transcoding/video-transcoding-profiles' 15import { VideoTranscodingProfilesManager } from '../../transcoding/default-transcoding-profiles'
16import { isAbleToUploadVideo } from '../../user' 16import { isAbleToUploadVideo } from '../../user'
17import { LiveQuotaStore } from '../live-quota-store' 17import { LiveQuotaStore } from '../live-quota-store'
18import { LiveSegmentShaStore } from '../live-segment-sha-store' 18import { LiveSegmentShaStore } from '../live-segment-sha-store'
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts
index c6826759b..01046d017 100644
--- a/server/lib/local-actor.ts
+++ b/server/lib/local-actor.ts
@@ -1,5 +1,5 @@
1import 'multer'
2import { queue } from 'async' 1import { queue } from 'async'
2import { remove } from 'fs-extra'
3import LRUCache from 'lru-cache' 3import LRUCache from 'lru-cache'
4import { join } from 'path' 4import { join } from 'path'
5import { ActorModel } from '@server/models/actor/actor' 5import { ActorModel } from '@server/models/actor/actor'
@@ -13,7 +13,7 @@ import { CONFIG } from '../initializers/config'
13import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' 13import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants'
14import { sequelizeTypescript } from '../initializers/database' 14import { sequelizeTypescript } from '../initializers/database'
15import { MAccountDefault, MActor, MChannelDefault } from '../types/models' 15import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
16import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors' 16import { deleteActorImages, updateActorImages } from './activitypub/actors'
17import { sendUpdateActor } from './activitypub/send' 17import { sendUpdateActor } from './activitypub/send'
18 18
19function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { 19function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
@@ -33,64 +33,69 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
33 }) as MActor 33 }) as MActor
34} 34}
35 35
36async function updateLocalActorImageFile ( 36async function updateLocalActorImageFiles (
37 accountOrChannel: MAccountDefault | MChannelDefault, 37 accountOrChannel: MAccountDefault | MChannelDefault,
38 imagePhysicalFile: Express.Multer.File, 38 imagePhysicalFile: Express.Multer.File,
39 type: ActorImageType 39 type: ActorImageType
40) { 40) {
41 const imageSize = type === ActorImageType.AVATAR 41 const processImageSize = async (imageSize: { width: number, height: number }) => {
42 ? ACTOR_IMAGES_SIZE.AVATARS 42 const extension = getLowercaseExtension(imagePhysicalFile.filename)
43 : ACTOR_IMAGES_SIZE.BANNERS 43
44 44 const imageName = buildUUID() + extension
45 const extension = getLowercaseExtension(imagePhysicalFile.filename) 45 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
46 46 await processImage(imagePhysicalFile.path, destination, imageSize, true)
47 const imageName = buildUUID() + extension 47
48 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) 48 return {
49 await processImage(imagePhysicalFile.path, destination, imageSize) 49 imageName,
50 50 imageSize
51 return retryTransactionWrapper(() => { 51 }
52 return sequelizeTypescript.transaction(async t => { 52 }
53 const actorImageInfo = { 53
54 name: imageName, 54 const processedImages = await Promise.all(ACTOR_IMAGES_SIZE[type].map(processImageSize))
55 fileUrl: null, 55 await remove(imagePhysicalFile.path)
56 height: imageSize.height, 56
57 width: imageSize.width, 57 return retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => {
58 onDisk: true 58 const actorImagesInfo = processedImages.map(({ imageName, imageSize }) => ({
59 } 59 name: imageName,
60 60 fileUrl: null,
61 const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) 61 height: imageSize.height,
62 await updatedActor.save({ transaction: t }) 62 width: imageSize.width,
63 63 onDisk: true
64 await sendUpdateActor(accountOrChannel, t) 64 }))
65 65
66 return type === ActorImageType.AVATAR 66 const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t)
67 ? updatedActor.Avatar 67 await updatedActor.save({ transaction: t })
68 : updatedActor.Banner 68
69 }) 69 await sendUpdateActor(accountOrChannel, t)
70 }) 70
71 return type === ActorImageType.AVATAR
72 ? updatedActor.Avatars
73 : updatedActor.Banners
74 }))
71} 75}
72 76
73async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { 77async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
74 return retryTransactionWrapper(() => { 78 return retryTransactionWrapper(() => {
75 return sequelizeTypescript.transaction(async t => { 79 return sequelizeTypescript.transaction(async t => {
76 const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) 80 const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t)
77 await updatedActor.save({ transaction: t }) 81 await updatedActor.save({ transaction: t })
78 82
79 await sendUpdateActor(accountOrChannel, t) 83 await sendUpdateActor(accountOrChannel, t)
80 84
81 return updatedActor.Avatar 85 return updatedActor.Avatars
82 }) 86 })
83 }) 87 })
84} 88}
85 89
86type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } 90type DownloadImageQueueTask = {
91 fileUrl: string
92 filename: string
93 type: ActorImageType
94 size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0]
95}
87 96
88const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { 97const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
89 const size = task.type === ActorImageType.AVATAR 98 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size)
90 ? ACTOR_IMAGES_SIZE.AVATARS
91 : ACTOR_IMAGES_SIZE.BANNERS
92
93 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
94 .then(() => cb()) 99 .then(() => cb())
95 .catch(err => cb(err)) 100 .catch(err => cb(err))
96}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) 101}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
@@ -110,7 +115,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.
110 115
111export { 116export {
112 actorImagePathUnsafeCache, 117 actorImagePathUnsafeCache,
113 updateLocalActorImageFile, 118 updateLocalActorImageFiles,
114 deleteLocalActorImageFile, 119 deleteLocalActorImageFile,
115 pushActorImageProcessInQueue, 120 pushActorImageProcessInQueue,
116 buildActorInstance 121 buildActorInstance
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts
index 765cbaad9..ecd1687b4 100644
--- a/server/lib/notifier/shared/comment/comment-mention.ts
+++ b/server/lib/notifier/shared/comment/comment-mention.ts
@@ -77,7 +77,7 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
77 userId: user.id, 77 userId: user.id,
78 commentId: this.payload.id 78 commentId: this.payload.id
79 }) 79 })
80 notification.Comment = this.payload 80 notification.VideoComment = this.payload
81 81
82 return notification 82 return notification
83 } 83 }
diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts
index b76fc15bf..757502703 100644
--- a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts
+++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts
@@ -44,7 +44,7 @@ export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwner
44 userId: user.id, 44 userId: user.id,
45 commentId: this.payload.id 45 commentId: this.payload.id
46 }) 46 })
47 notification.Comment = this.payload 47 notification.VideoComment = this.payload
48 48
49 return notification 49 return notification
50 } 50 }
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts
index 78e4a28ad..897271c0b 100644
--- a/server/lib/plugins/plugin-helpers-builder.ts
+++ b/server/lib/plugins/plugin-helpers-builder.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { join } from 'path' 2import { join } from 'path'
3import { ffprobePromise } from '@server/helpers/ffprobe-utils' 3import { ffprobePromise } from '@server/helpers/ffmpeg/ffprobe-utils'
4import { buildLogger } from '@server/helpers/logger' 4import { buildLogger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { WEBSERVER } from '@server/initializers/constants' 6import { WEBSERVER } from '@server/initializers/constants'
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
index d1756040a..f4d405676 100644
--- a/server/lib/plugins/register-helpers.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -21,7 +21,7 @@ import {
21 VideoPlaylistPrivacy, 21 VideoPlaylistPrivacy,
22 VideoPrivacy 22 VideoPrivacy
23} from '@shared/models' 23} from '@shared/models'
24import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' 24import { VideoTranscodingProfilesManager } from '../transcoding/default-transcoding-profiles'
25import { buildPluginHelpers } from './plugin-helpers-builder' 25import { buildPluginHelpers } from './plugin-helpers-builder'
26 26
27export class RegisterHelpers { 27export class RegisterHelpers {
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index d97f21eb7..38512f384 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -8,7 +8,7 @@ import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuth
8import { Hooks } from './plugins/hooks' 8import { Hooks } from './plugins/hooks'
9import { PluginManager } from './plugins/plugin-manager' 9import { PluginManager } from './plugins/plugin-manager'
10import { getThemeOrDefault } from './plugins/theme-utils' 10import { getThemeOrDefault } from './plugins/theme-utils'
11import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles' 11import { VideoTranscodingProfilesManager } from './transcoding/default-transcoding-profiles'
12 12
13/** 13/**
14 * 14 *
@@ -151,6 +151,9 @@ class ServerConfigManager {
151 port: CONFIG.LIVE.RTMP.PORT 151 port: CONFIG.LIVE.RTMP.PORT
152 } 152 }
153 }, 153 },
154 videoEditor: {
155 enabled: CONFIG.VIDEO_EDITOR.ENABLED
156 },
154 import: { 157 import: {
155 videos: { 158 videos: {
156 http: { 159 http: {
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index 36270e5c1..aa2d7a813 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,7 +1,6 @@
1import { join } from 'path' 1import { join } from 'path'
2import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 2import { ThumbnailType } from '@shared/models'
3import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 3import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils'
4import { generateImageFilename, processImage } from '../helpers/image-utils'
5import { downloadImage } from '../helpers/requests' 4import { downloadImage } from '../helpers/requests'
6import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
7import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 6import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
diff --git a/server/lib/transcoding/video-transcoding-profiles.ts b/server/lib/transcoding/default-transcoding-profiles.ts
index dcc8d4c5c..ba98a11ca 100644
--- a/server/lib/transcoding/video-transcoding-profiles.ts
+++ b/server/lib/transcoding/default-transcoding-profiles.ts
@@ -2,8 +2,14 @@
2import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
3import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils' 3import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils'
4import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos' 4import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos'
5import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' 5import {
6import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils' 6 buildStreamSuffix,
7 canDoQuickAudioTranscode,
8 ffprobePromise,
9 getAudioStream,
10 getMaxAudioBitrate,
11 resetSupportedEncoders
12} from '../../helpers/ffmpeg'
7 13
8/** 14/**
9 * 15 *
@@ -15,8 +21,14 @@ import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBi
15 * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 21 * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
16 */ 22 */
17 23
24// ---------------------------------------------------------------------------
25// Default builders
26// ---------------------------------------------------------------------------
27
18const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => { 28const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
19 const { fps, inputRatio, inputBitrate, resolution } = options 29 const { fps, inputRatio, inputBitrate, resolution } = options
30
31 // TODO: remove in 4.2, fps is not optional anymore
20 if (!fps) return { outputOptions: [ ] } 32 if (!fps) return { outputOptions: [ ] }
21 33
22 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution }) 34 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
@@ -45,10 +57,10 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOp
45 } 57 }
46} 58}
47 59
48const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => { 60const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio }) => {
49 const probe = await ffprobePromise(input) 61 const probe = await ffprobePromise(input)
50 62
51 if (await canDoQuickAudioTranscode(input, probe)) { 63 if (canCopyAudio && await canDoQuickAudioTranscode(input, probe)) {
52 logger.debug('Copy audio stream %s by AAC encoder.', input) 64 logger.debug('Copy audio stream %s by AAC encoder.', input)
53 return { copy: true, outputOptions: [ ] } 65 return { copy: true, outputOptions: [ ] }
54 } 66 }
@@ -75,7 +87,10 @@ const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum })
75 return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } 87 return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
76} 88}
77 89
78// Used to get and update available encoders 90// ---------------------------------------------------------------------------
91// Profile manager to get and change default profiles
92// ---------------------------------------------------------------------------
93
79class VideoTranscodingProfilesManager { 94class VideoTranscodingProfilesManager {
80 private static instance: VideoTranscodingProfilesManager 95 private static instance: VideoTranscodingProfilesManager
81 96
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/transcoding.ts
index 9942a067b..d55364e25 100644
--- a/server/lib/transcoding/video-transcoding.ts
+++ b/server/lib/transcoding/transcoding.ts
@@ -6,8 +6,15 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 6import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
7import { VideoResolution, VideoStorage } from '../../../shared/models/videos' 7import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
9import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' 9import {
10import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' 10 canDoQuickTranscode,
11 getVideoStreamDuration,
12 buildFileMetadata,
13 getVideoStreamFPS,
14 transcodeVOD,
15 TranscodeVODOptions,
16 TranscodeVODOptionsType
17} from '../../helpers/ffmpeg'
11import { CONFIG } from '../../initializers/config' 18import { CONFIG } from '../../initializers/config'
12import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' 19import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
13import { VideoFileModel } from '../../models/video/video-file' 20import { VideoFileModel } from '../../models/video/video-file'
@@ -21,7 +28,7 @@ import {
21 getHlsResolutionPlaylistFilename 28 getHlsResolutionPlaylistFilename
22} from '../paths' 29} from '../paths'
23import { VideoPathManager } from '../video-path-manager' 30import { VideoPathManager } from '../video-path-manager'
24import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' 31import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
25 32
26/** 33/**
27 * 34 *
@@ -38,13 +45,13 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
38 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { 45 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => {
39 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) 46 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
40 47
41 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) 48 const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
42 ? 'quick-transcode' 49 ? 'quick-transcode'
43 : 'video' 50 : 'video'
44 51
45 const resolution = toEven(inputVideoFile.resolution) 52 const resolution = toEven(inputVideoFile.resolution)
46 53
47 const transcodeOptions: TranscodeOptions = { 54 const transcodeOptions: TranscodeVODOptions = {
48 type: transcodeType, 55 type: transcodeType,
49 56
50 inputPath: videoInputPath, 57 inputPath: videoInputPath,
@@ -59,7 +66,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
59 } 66 }
60 67
61 // Could be very long! 68 // Could be very long!
62 await transcode(transcodeOptions) 69 await transcodeVOD(transcodeOptions)
63 70
64 // Important to do this before getVideoFilename() to take in account the new filename 71 // Important to do this before getVideoFilename() to take in account the new filename
65 inputVideoFile.extname = newExtname 72 inputVideoFile.extname = newExtname
@@ -121,7 +128,7 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V
121 job 128 job
122 } 129 }
123 130
124 await transcode(transcodeOptions) 131 await transcodeVOD(transcodeOptions)
125 132
126 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) 133 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
127 }) 134 })
@@ -158,7 +165,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio
158 } 165 }
159 166
160 try { 167 try {
161 await transcode(transcodeOptions) 168 await transcodeVOD(transcodeOptions)
162 169
163 await remove(audioInputPath) 170 await remove(audioInputPath)
164 await remove(tmpPreviewPath) 171 await remove(tmpPreviewPath)
@@ -175,7 +182,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio
175 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) 182 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
176 // ffmpeg generated a new video file, so update the video duration 183 // ffmpeg generated a new video file, so update the video duration
177 // See https://trac.ffmpeg.org/ticket/5456 184 // See https://trac.ffmpeg.org/ticket/5456
178 video.duration = await getDurationFromVideoFile(videoTranscodedPath) 185 video.duration = await getVideoStreamDuration(videoTranscodedPath)
179 await video.save() 186 await video.save()
180 187
181 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) 188 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
@@ -239,8 +246,8 @@ async function onWebTorrentVideoFileTranscoding (
239 outputPath: string 246 outputPath: string
240) { 247) {
241 const stats = await stat(transcodingPath) 248 const stats = await stat(transcodingPath)
242 const fps = await getVideoFileFPS(transcodingPath) 249 const fps = await getVideoStreamFPS(transcodingPath)
243 const metadata = await getMetadataFromFile(transcodingPath) 250 const metadata = await buildFileMetadata(transcodingPath)
244 251
245 await move(transcodingPath, outputPath, { overwrite: true }) 252 await move(transcodingPath, outputPath, { overwrite: true })
246 253
@@ -299,7 +306,7 @@ async function generateHlsPlaylistCommon (options: {
299 job 306 job
300 } 307 }
301 308
302 await transcode(transcodeOptions) 309 await transcodeVOD(transcodeOptions)
303 310
304 // Create or update the playlist 311 // Create or update the playlist
305 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) 312 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
@@ -344,8 +351,8 @@ async function generateHlsPlaylistCommon (options: {
344 const stats = await stat(videoFilePath) 351 const stats = await stat(videoFilePath)
345 352
346 newVideoFile.size = stats.size 353 newVideoFile.size = stats.size
347 newVideoFile.fps = await getVideoFileFPS(videoFilePath) 354 newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
348 newVideoFile.metadata = await getMetadataFromFile(videoFilePath) 355 newVideoFile.metadata = await buildFileMetadata(videoFilePath)
349 356
350 await createTorrentAndSetInfoHash(playlist, newVideoFile) 357 await createTorrentAndSetInfoHash(playlist, newVideoFile)
351 358
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 0d292ac90..ea755f4be 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,9 +1,11 @@
1import { Transaction } from 'sequelize/types' 1import { Transaction } from 'sequelize/types'
2import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config'
2import { UserModel } from '@server/models/user/user' 4import { UserModel } from '@server/models/user/user'
3import { MActorDefault } from '@server/types/models/actor' 5import { MActorDefault } from '@server/types/models/actor'
4import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
5import { ActivityPubActorType } from '../../shared/models/activitypub' 7import { ActivityPubActorType } from '../../shared/models/activitypub'
6import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 8import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users'
7import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' 9import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
8import { sequelizeTypescript } from '../initializers/database' 10import { sequelizeTypescript } from '../initializers/database'
9import { AccountModel } from '../models/account/account' 11import { AccountModel } from '../models/account/account'
@@ -22,6 +24,53 @@ import { createWatchLaterPlaylist } from './video-playlist'
22 24
23type ChannelNames = { name: string, displayName: string } 25type ChannelNames = { name: string, displayName: string }
24 26
27function buildUser (options: {
28 username: string
29 password: string
30 email: string
31
32 role?: UserRole // Default to UserRole.User
33 adminFlags?: UserAdminFlag // Default to UserAdminFlag.NONE
34
35 emailVerified: boolean | null
36
37 videoQuota?: number // Default to CONFIG.USER.VIDEO_QUOTA
38 videoQuotaDaily?: number // Default to CONFIG.USER.VIDEO_QUOTA_DAILY
39
40 pluginAuth?: string
41}): MUser {
42 const {
43 username,
44 password,
45 email,
46 role = UserRole.USER,
47 emailVerified,
48 videoQuota = CONFIG.USER.VIDEO_QUOTA,
49 videoQuotaDaily = CONFIG.USER.VIDEO_QUOTA_DAILY,
50 adminFlags = UserAdminFlag.NONE,
51 pluginAuth
52 } = options
53
54 return new UserModel({
55 username,
56 password,
57 email,
58
59 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
60 p2pEnabled: CONFIG.DEFAULTS.P2P.WEBAPP.ENABLED,
61 autoPlayVideo: true,
62
63 role,
64 emailVerified,
65 adminFlags,
66
67 videoQuota: videoQuota,
68 videoQuotaDaily: videoQuotaDaily,
69
70 pluginAuth
71 })
72}
73
25async function createUserAccountAndChannelAndPlaylist (parameters: { 74async function createUserAccountAndChannelAndPlaylist (parameters: {
26 userToCreate: MUser 75 userToCreate: MUser
27 userDisplayName?: string 76 userDisplayName?: string
@@ -117,7 +166,7 @@ async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
117 const email = isPendingEmail ? user.pendingEmail : user.email 166 const email = isPendingEmail ? user.pendingEmail : user.email
118 const username = user.username 167 const username = user.username
119 168
120 await Emailer.Instance.addVerifyEmailJob(username, email, url) 169 Emailer.Instance.addVerifyEmailJob(username, email, url)
121} 170}
122 171
123async function getOriginalVideoFileTotalFromUser (user: MUserId) { 172async function getOriginalVideoFileTotalFromUser (user: MUserId) {
@@ -159,6 +208,11 @@ async function isAbleToUploadVideo (userId: number, newVideoSize: number) {
159 const uploadedTotal = newVideoSize + totalBytes 208 const uploadedTotal = newVideoSize + totalBytes
160 const uploadedDaily = newVideoSize + totalBytesDaily 209 const uploadedDaily = newVideoSize + totalBytesDaily
161 210
211 logger.debug(
212 'Check user %d quota to upload another video.', userId,
213 { totalBytes, totalBytesDaily, videoQuota: user.videoQuota, videoQuotaDaily: user.videoQuotaDaily, newVideoSize }
214 )
215
162 if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota 216 if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota
163 if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily 217 if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily
164 218
@@ -174,7 +228,8 @@ export {
174 createUserAccountAndChannelAndPlaylist, 228 createUserAccountAndChannelAndPlaylist,
175 createLocalAccountWithoutKeys, 229 createLocalAccountWithoutKeys,
176 sendVerifyUserEmail, 230 sendVerifyUserEmail,
177 isAbleToUploadVideo 231 isAbleToUploadVideo,
232 buildUser
178} 233}
179 234
180// --------------------------------------------------------------------------- 235// ---------------------------------------------------------------------------
diff --git a/server/lib/video-editor.ts b/server/lib/video-editor.ts
new file mode 100644
index 000000000..99b0bd949
--- /dev/null
+++ b/server/lib/video-editor.ts
@@ -0,0 +1,32 @@
1import { MVideoFullLight } from "@server/types/models"
2import { getVideoStreamDuration } from "@shared/extra-utils"
3import { VideoEditorTask } from "@shared/models"
4
5function buildTaskFileFieldname (indice: number, fieldName = 'file') {
6 return `tasks[${indice}][options][${fieldName}]`
7}
8
9function getTaskFile (files: Express.Multer.File[], indice: number, fieldName = 'file') {
10 return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName))
11}
12
13async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, tasks: VideoEditorTask[], fileFinder: (i: number) => string) {
14 let additionalDuration = 0
15
16 for (let i = 0; i < tasks.length; i++) {
17 const task = tasks[i]
18
19 if (task.name !== 'add-intro' && task.name !== 'add-outro') continue
20
21 const filePath = fileFinder(i)
22 additionalDuration += await getVideoStreamDuration(filePath)
23 }
24
25 return (video.getMaxQualityFile().size / video.duration) * additionalDuration
26}
27
28export {
29 approximateIntroOutroAdditionalSize,
30 buildTaskFileFieldname,
31 getTaskFile
32}
diff --git a/server/lib/video.ts b/server/lib/video.ts
index 2690f953d..ec4256c1a 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -81,7 +81,7 @@ async function setVideoTags (options: {
81 video.Tags = tagInstances 81 video.Tags = tagInstances
82} 82}
83 83
84async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId) { 84async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId, isNewVideo = true) {
85 let dataInput: VideoTranscodingPayload 85 let dataInput: VideoTranscodingPayload
86 86
87 if (videoFile.isAudio()) { 87 if (videoFile.isAudio()) {
@@ -90,13 +90,13 @@ async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoF
90 resolution: DEFAULT_AUDIO_RESOLUTION, 90 resolution: DEFAULT_AUDIO_RESOLUTION,
91 videoUUID: video.uuid, 91 videoUUID: video.uuid,
92 createHLSIfNeeded: true, 92 createHLSIfNeeded: true,
93 isNewVideo: true 93 isNewVideo
94 } 94 }
95 } else { 95 } else {
96 dataInput = { 96 dataInput = {
97 type: 'optimize-to-webtorrent', 97 type: 'optimize-to-webtorrent',
98 videoUUID: video.uuid, 98 videoUUID: video.uuid,
99 isNewVideo: true 99 isNewVideo
100 } 100 }
101 } 101 }
102 102