diff options
Diffstat (limited to 'server/helpers')
-rw-r--r-- | server/helpers/core-utils.ts | 48 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/videos.ts | 7 | ||||
-rw-r--r-- | server/helpers/custom-validators/users.ts | 5 | ||||
-rw-r--r-- | server/helpers/custom-validators/videos.ts | 13 | ||||
-rw-r--r-- | server/helpers/express-utils.ts | 3 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 167 | ||||
-rw-r--r-- | server/helpers/peertube-crypto.ts | 5 | ||||
-rw-r--r-- | server/helpers/utils.ts | 22 | ||||
-rw-r--r-- | server/helpers/video.ts | 4 |
9 files changed, 194 insertions, 80 deletions
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 224e4fe92..84e33c0e9 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -21,6 +21,7 @@ const timeTable = { | |||
21 | week: 3600000 * 24 * 7, | 21 | week: 3600000 * 24 * 7, |
22 | month: 3600000 * 24 * 30 | 22 | month: 3600000 * 24 * 30 |
23 | } | 23 | } |
24 | |||
24 | export function parseDuration (duration: number | string): number { | 25 | export function parseDuration (duration: number | string): number { |
25 | if (typeof duration === 'number') return duration | 26 | if (typeof duration === 'number') return duration |
26 | 27 | ||
@@ -41,6 +42,53 @@ export function parseDuration (duration: number | string): number { | |||
41 | throw new Error('Duration could not be properly parsed') | 42 | throw new Error('Duration could not be properly parsed') |
42 | } | 43 | } |
43 | 44 | ||
45 | export function parseBytes (value: string | number): number { | ||
46 | if (typeof value === 'number') return value | ||
47 | |||
48 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
49 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ | ||
50 | const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/ | ||
51 | const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
52 | const t = /^(\d+)\s*TB$/ | ||
53 | const g = /^(\d+)\s*GB$/ | ||
54 | const m = /^(\d+)\s*MB$/ | ||
55 | const b = /^(\d+)\s*B$/ | ||
56 | let match | ||
57 | |||
58 | if (value.match(tgm)) { | ||
59 | match = value.match(tgm) | ||
60 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
61 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
62 | + parseInt(match[3], 10) * 1024 * 1024 | ||
63 | } else if (value.match(tg)) { | ||
64 | match = value.match(tg) | ||
65 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
66 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
67 | } else if (value.match(tm)) { | ||
68 | match = value.match(tm) | ||
69 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
70 | + parseInt(match[2], 10) * 1024 * 1024 | ||
71 | } else if (value.match(gm)) { | ||
72 | match = value.match(gm) | ||
73 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
74 | + parseInt(match[2], 10) * 1024 * 1024 | ||
75 | } else if (value.match(t)) { | ||
76 | match = value.match(t) | ||
77 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
78 | } else if (value.match(g)) { | ||
79 | match = value.match(g) | ||
80 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
81 | } else if (value.match(m)) { | ||
82 | match = value.match(m) | ||
83 | return parseInt(match[1], 10) * 1024 * 1024 | ||
84 | } else if (value.match(b)) { | ||
85 | match = value.match(b) | ||
86 | return parseInt(match[1], 10) * 1024 | ||
87 | } else { | ||
88 | return parseInt(value, 10) | ||
89 | } | ||
90 | } | ||
91 | |||
44 | function sanitizeUrl (url: string) { | 92 | function sanitizeUrl (url: string) { |
45 | const urlObject = new URL(url) | 93 | const urlObject = new URL(url) |
46 | 94 | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index f88d26561..95fe824b9 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -81,19 +81,20 @@ function isRemoteVideoUrlValid (url: any) { | |||
81 | 81 | ||
82 | return url.type === 'Link' && | 82 | return url.type === 'Link' && |
83 | ( | 83 | ( |
84 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && | 84 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
85 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 && | ||
85 | isActivityPubUrlValid(url.href) && | 86 | isActivityPubUrlValid(url.href) && |
86 | validator.isInt(url.height + '', { min: 0 }) && | 87 | validator.isInt(url.height + '', { min: 0 }) && |
87 | validator.isInt(url.size + '', { min: 0 }) && | 88 | validator.isInt(url.size + '', { min: 0 }) && |
88 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) | 89 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) |
89 | ) || | 90 | ) || |
90 | ( | 91 | ( |
91 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && | 92 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 && |
92 | isActivityPubUrlValid(url.href) && | 93 | isActivityPubUrlValid(url.href) && |
93 | validator.isInt(url.height + '', { min: 0 }) | 94 | validator.isInt(url.height + '', { min: 0 }) |
94 | ) || | 95 | ) || |
95 | ( | 96 | ( |
96 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && | 97 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
97 | validator.isLength(url.href, { min: 5 }) && | 98 | validator.isLength(url.href, { min: 5 }) && |
98 | validator.isInt(url.height + '', { min: 0 }) | 99 | validator.isInt(url.height + '', { min: 0 }) |
99 | ) | 100 | ) |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 90fc74a48..1cb5e5b0f 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -42,6 +42,10 @@ function isUserNSFWPolicyValid (value: any) { | |||
42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | 42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 |
43 | } | 43 | } |
44 | 44 | ||
45 | function isUserWebTorrentEnabledValid (value: any) { | ||
46 | return isBooleanValid(value) | ||
47 | } | ||
48 | |||
45 | function isUserAutoPlayVideoValid (value: any) { | 49 | function isUserAutoPlayVideoValid (value: any) { |
46 | return isBooleanValid(value) | 50 | return isBooleanValid(value) |
47 | } | 51 | } |
@@ -78,6 +82,7 @@ export { | |||
78 | isUserUsernameValid, | 82 | isUserUsernameValid, |
79 | isUserEmailVerifiedValid, | 83 | isUserEmailVerifiedValid, |
80 | isUserNSFWPolicyValid, | 84 | isUserNSFWPolicyValid, |
85 | isUserWebTorrentEnabledValid, | ||
81 | isUserAutoPlayVideoValid, | 86 | isUserAutoPlayVideoValid, |
82 | isUserDisplayNameValid, | 87 | isUserDisplayNameValid, |
83 | isUserDescriptionValid, | 88 | isUserDescriptionValid, |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 9875c68bd..a13b09ac8 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -3,7 +3,7 @@ import 'express-validator' | |||
3 | import { values } from 'lodash' | 3 | import { values } from 'lodash' |
4 | import 'multer' | 4 | import 'multer' |
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' | 6 | import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
7 | import { | 7 | import { |
8 | CONSTRAINTS_FIELDS, | 8 | CONSTRAINTS_FIELDS, |
9 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
@@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video' | |||
22 | 22 | ||
23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
24 | 24 | ||
25 | function isVideoFilterValid (filter: VideoFilter) { | ||
26 | return filter === 'local' || filter === 'all-local' | ||
27 | } | ||
28 | |||
25 | function isVideoCategoryValid (value: any) { | 29 | function isVideoCategoryValid (value: any) { |
26 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined | 30 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined |
27 | } | 31 | } |
@@ -154,7 +158,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use | |||
154 | } | 158 | } |
155 | 159 | ||
156 | async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { | 160 | async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { |
157 | const video = await fetchVideo(id, fetchType) | 161 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
162 | |||
163 | const video = await fetchVideo(id, fetchType, userId) | ||
158 | 164 | ||
159 | if (video === null) { | 165 | if (video === null) { |
160 | res.status(404) | 166 | res.status(404) |
@@ -223,5 +229,6 @@ export { | |||
223 | isVideoExist, | 229 | isVideoExist, |
224 | isVideoImage, | 230 | isVideoImage, |
225 | isVideoChannelOfAccountExist, | 231 | isVideoChannelOfAccountExist, |
226 | isVideoSupportValid | 232 | isVideoSupportValid, |
233 | isVideoFilterValid | ||
227 | } | 234 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 8a9cee8c5..162fe2244 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,7 +2,6 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { User } from '../../shared/models/users' | ||
6 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAsync, generateRandomString } from './utils' |
7 | import { extname } from 'path' | 6 | import { extname } from 'path' |
8 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
@@ -101,7 +100,7 @@ function createReqFiles ( | |||
101 | } | 100 | } |
102 | 101 | ||
103 | function isUserAbleToSearchRemoteURI (res: express.Response) { | 102 | function isUserAbleToSearchRemoteURI (res: express.Response) { |
104 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | 103 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined |
105 | 104 | ||
106 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | 105 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || |
107 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | 106 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 22bc25476..8b9045038 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 55 | return 0 |
56 | } | 56 | } |
57 | 57 | ||
58 | async function getVideoFileBitrate (path: string) { | ||
59 | return new Promise<number>((res, rej) => { | ||
60 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
61 | if (err) return rej(err) | ||
62 | |||
63 | return res(metadata.format.bit_rate) | ||
64 | }) | ||
65 | }) | ||
66 | } | ||
67 | |||
58 | function getDurationFromVideoFile (path: string) { | 68 | function getDurationFromVideoFile (path: string) { |
59 | return new Promise<number>((res, rej) => { | 69 | return new Promise<number>((res, rej) => { |
60 | ffmpeg.ffprobe(path, (err, metadata) => { | 70 | ffmpeg.ffprobe(path, (err, metadata) => { |
@@ -106,45 +116,50 @@ type TranscodeOptions = { | |||
106 | 116 | ||
107 | function transcode (options: TranscodeOptions) { | 117 | function transcode (options: TranscodeOptions) { |
108 | return new Promise<void>(async (res, rej) => { | 118 | return new Promise<void>(async (res, rej) => { |
109 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 119 | try { |
110 | .output(options.outputPath) | 120 | let fps = await getVideoFileFPS(options.inputPath) |
111 | .preset(standard) | ||
112 | |||
113 | if (CONFIG.TRANSCODING.THREADS > 0) { | ||
114 | // if we don't set any threads ffmpeg will chose automatically | ||
115 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
116 | } | ||
117 | |||
118 | let fps = await getVideoFileFPS(options.inputPath) | ||
119 | if (options.resolution !== undefined) { | ||
120 | // '?x720' or '720x?' for example | ||
121 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
122 | command = command.size(size) | ||
123 | |||
124 | // On small/medium resolutions, limit FPS | 121 | // On small/medium resolutions, limit FPS |
125 | if ( | 122 | if ( |
123 | options.resolution !== undefined && | ||
126 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 124 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
127 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 125 | fps > VIDEO_TRANSCODING_FPS.AVERAGE |
128 | ) { | 126 | ) { |
129 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 127 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
130 | } | 128 | } |
131 | } | ||
132 | 129 | ||
133 | if (fps) { | 130 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
134 | // Hard FPS limits | 131 | .output(options.outputPath) |
135 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 132 | command = await presetH264(command, options.resolution, fps) |
136 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
137 | 133 | ||
138 | command = command.withFPS(fps) | 134 | if (CONFIG.TRANSCODING.THREADS > 0) { |
139 | } | 135 | // if we don't set any threads ffmpeg will chose automatically |
136 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
137 | } | ||
138 | |||
139 | if (options.resolution !== undefined) { | ||
140 | // '?x720' or '720x?' for example | ||
141 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
142 | command = command.size(size) | ||
143 | } | ||
144 | |||
145 | if (fps) { | ||
146 | // Hard FPS limits | ||
147 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | ||
148 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
140 | 149 | ||
141 | command | 150 | command = command.withFPS(fps) |
142 | .on('error', (err, stdout, stderr) => { | 151 | } |
143 | logger.error('Error in transcoding job.', { stdout, stderr }) | 152 | |
144 | return rej(err) | 153 | command |
145 | }) | 154 | .on('error', (err, stdout, stderr) => { |
146 | .on('end', res) | 155 | logger.error('Error in transcoding job.', { stdout, stderr }) |
147 | .run() | 156 | return rej(err) |
157 | }) | ||
158 | .on('end', res) | ||
159 | .run() | ||
160 | } catch (err) { | ||
161 | return rej(err) | ||
162 | } | ||
148 | }) | 163 | }) |
149 | } | 164 | } |
150 | 165 | ||
@@ -157,7 +172,8 @@ export { | |||
157 | transcode, | 172 | transcode, |
158 | getVideoFileFPS, | 173 | getVideoFileFPS, |
159 | computeResolutionsToTranscode, | 174 | computeResolutionsToTranscode, |
160 | audio | 175 | audio, |
176 | getVideoFileBitrate | ||
161 | } | 177 | } |
162 | 178 | ||
163 | // --------------------------------------------------------------------------- | 179 | // --------------------------------------------------------------------------- |
@@ -182,11 +198,10 @@ function getVideoFileStream (path: string) { | |||
182 | * and quality. Superfast and ultrafast will give you better | 198 | * and quality. Superfast and ultrafast will give you better |
183 | * performance, but then quality is noticeably worse. | 199 | * performance, but then quality is noticeably worse. |
184 | */ | 200 | */ |
185 | function veryfast (_ffmpeg) { | 201 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
186 | _ffmpeg | 202 | let localCommand = await presetH264(command, resolution, fps) |
187 | .preset(standard) | 203 | localCommand = localCommand.outputOption('-preset:v veryfast') |
188 | .outputOption('-preset:v veryfast') | 204 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) |
189 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | ||
190 | /* | 205 | /* |
191 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 206 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
192 | Our target situation is closer to a livestream than a stream, | 207 | Our target situation is closer to a livestream than a stream, |
@@ -198,31 +213,39 @@ function veryfast (_ffmpeg) { | |||
198 | Make up for most of the loss of grain and macroblocking | 213 | Make up for most of the loss of grain and macroblocking |
199 | with less computing power. | 214 | with less computing power. |
200 | */ | 215 | */ |
216 | |||
217 | return localCommand | ||
201 | } | 218 | } |
202 | 219 | ||
203 | /** | 220 | /** |
204 | * A preset optimised for a stillimage audio video | 221 | * A preset optimised for a stillimage audio video |
205 | */ | 222 | */ |
206 | function audio (_ffmpeg) { | 223 | async function presetStillImageWithAudio ( |
207 | _ffmpeg | 224 | command: ffmpeg.FfmpegCommand, |
208 | .preset(veryfast) | 225 | resolution: VideoResolution, |
209 | .outputOption('-tune stillimage') | 226 | fps: number |
227 | ): Promise<ffmpeg.FfmpegCommand> { | ||
228 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
229 | localCommand = localCommand.outputOption('-tune stillimage') | ||
230 | |||
231 | return localCommand | ||
210 | } | 232 | } |
211 | 233 | ||
212 | /** | 234 | /** |
213 | * A toolbox to play with audio | 235 | * A toolbox to play with audio |
214 | */ | 236 | */ |
215 | namespace audio { | 237 | namespace audio { |
216 | export const get = (_ffmpeg, pos: number | string = 0) => { | 238 | export const get = (option: ffmpeg.FfmpegCommand | string) => { |
217 | // without position, ffprobe considers the last input only | 239 | // without position, ffprobe considers the last input only |
218 | // we make it consider the first input only | 240 | // we make it consider the first input only |
219 | // if you pass a file path to pos, then ffprobe acts on that file directly | 241 | // if you pass a file path to pos, then ffprobe acts on that file directly |
220 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | 242 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { |
221 | _ffmpeg.ffprobe(pos, (err,data) => { | 243 | |
244 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
222 | if (err) return rej(err) | 245 | if (err) return rej(err) |
223 | 246 | ||
224 | if ('streams' in data) { | 247 | if ('streams' in data) { |
225 | const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') | 248 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') |
226 | if (audioStream) { | 249 | if (audioStream) { |
227 | return res({ | 250 | return res({ |
228 | absolutePath: data.format.filename, | 251 | absolutePath: data.format.filename, |
@@ -230,8 +253,15 @@ namespace audio { | |||
230 | }) | 253 | }) |
231 | } | 254 | } |
232 | } | 255 | } |
256 | |||
233 | return res({ absolutePath: data.format.filename }) | 257 | return res({ absolutePath: data.format.filename }) |
234 | }) | 258 | } |
259 | |||
260 | if (typeof option === 'string') { | ||
261 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
262 | } | ||
263 | |||
264 | return option.ffprobe(parseFfprobe) | ||
235 | }) | 265 | }) |
236 | } | 266 | } |
237 | 267 | ||
@@ -273,39 +303,48 @@ namespace audio { | |||
273 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 303 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
274 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 304 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
275 | */ | 305 | */ |
276 | async function standard (_ffmpeg) { | 306 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
277 | let localFfmpeg = _ffmpeg | 307 | let localCommand = command |
278 | .format('mp4') | 308 | .format('mp4') |
279 | .videoCodec('libx264') | 309 | .videoCodec('libx264') |
280 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | 310 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution |
281 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | 311 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it |
282 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | 312 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 |
313 | .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) | ||
283 | .outputOption('-map_metadata -1') // strip all metadata | 314 | .outputOption('-map_metadata -1') // strip all metadata |
284 | .outputOption('-movflags faststart') | 315 | .outputOption('-movflags faststart') |
285 | const _audio = await audio.get(localFfmpeg) | ||
286 | 316 | ||
287 | if (!_audio.audioStream) { | 317 | const parsedAudio = await audio.get(localCommand) |
288 | return localFfmpeg.noAudio() | ||
289 | } | ||
290 | 318 | ||
291 | // we favor VBR, if a good AAC encoder is available | 319 | if (!parsedAudio.audioStream) { |
292 | if ((await checkFFmpegEncoders()).get('libfdk_aac')) { | 320 | localCommand = localCommand.noAudio() |
293 | return localFfmpeg | 321 | } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available |
322 | localCommand = localCommand | ||
294 | .audioCodec('libfdk_aac') | 323 | .audioCodec('libfdk_aac') |
295 | .audioQuality(5) | 324 | .audioQuality(5) |
325 | } else { | ||
326 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | ||
327 | // of course this is far from perfect, but it might save some space in the end | ||
328 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | ||
329 | let bitrate: number | ||
330 | if (audio.bitrate[ audioCodecName ]) { | ||
331 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
332 | |||
333 | if (bitrate === -1) localCommand = localCommand.audioCodec('copy') | ||
334 | else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate) | ||
335 | } | ||
296 | } | 336 | } |
297 | 337 | ||
298 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 338 | // Constrained Encoding (VBV) |
299 | // of course this is far from perfect, but it might save some space in the end | 339 | // https://slhck.info/video/2017/03/01/rate-control.html |
300 | const audioCodecName = _audio.audioStream['codec_name'] | 340 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
301 | let bitrate: number | 341 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
302 | if (audio.bitrate[audioCodecName]) { | 342 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) |
303 | bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate']) | ||
304 | |||
305 | if (bitrate === -1) return localFfmpeg.audioCodec('copy') | ||
306 | } | ||
307 | 343 | ||
308 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) | 344 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
345 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
346 | // https://superuser.com/a/908325 | ||
347 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | ||
309 | 348 | ||
310 | return localFfmpeg | 349 | return localCommand |
311 | } | 350 | } |
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index cb5f27240..8ef7b1359 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -62,10 +62,7 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) | |||
62 | 62 | ||
63 | return jsig.promises | 63 | return jsig.promises |
64 | .verify(signedDocument, options) | 64 | .verify(signedDocument, options) |
65 | .then((result: { verified: boolean }) => { | 65 | .then((result: { verified: boolean }) => result.verified) |
66 | logger.info('coucou', result) | ||
67 | return result.verified | ||
68 | }) | ||
69 | .catch(err => { | 66 | .catch(err => { |
70 | logger.error('Cannot check signature.', { err }) | 67 | logger.error('Cannot check signature.', { err }) |
71 | return false | 68 | return false |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 6228fec04..049c3f8bc 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () { | |||
40 | const application = await ApplicationModel.load() | 40 | const application = await ApplicationModel.load() |
41 | if (!application) throw Error('Could not load Application from database.') | 41 | if (!application) throw Error('Could not load Application from database.') |
42 | 42 | ||
43 | return application.Account.Actor | 43 | const actor = application.Account.Actor |
44 | actor.Account = application.Account | ||
45 | |||
46 | return actor | ||
44 | }) | 47 | }) |
45 | 48 | ||
46 | function generateVideoTmpPath (target: string | ParseTorrent) { | 49 | function generateVideoTmpPath (target: string | ParseTorrent) { |
@@ -77,6 +80,20 @@ async function getVersion () { | |||
77 | return require('../../../package.json').version | 80 | return require('../../../package.json').version |
78 | } | 81 | } |
79 | 82 | ||
83 | /** | ||
84 | * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns | ||
85 | * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does | ||
86 | * not contain a UUID, returns null. | ||
87 | */ | ||
88 | function getUUIDFromFilename (filename: string) { | ||
89 | const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ | ||
90 | const result = filename.match(regex) | ||
91 | |||
92 | if (!result || Array.isArray(result) === false) return null | ||
93 | |||
94 | return result[0] | ||
95 | } | ||
96 | |||
80 | // --------------------------------------------------------------------------- | 97 | // --------------------------------------------------------------------------- |
81 | 98 | ||
82 | export { | 99 | export { |
@@ -86,5 +103,6 @@ export { | |||
86 | getSecureTorrentName, | 103 | getSecureTorrentName, |
87 | getServerActor, | 104 | getServerActor, |
88 | getVersion, | 105 | getVersion, |
89 | generateVideoTmpPath | 106 | generateVideoTmpPath, |
107 | getUUIDFromFilename | ||
90 | } | 108 | } |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index b1577a6b0..1bd21467d 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video' | |||
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 8 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 9 | ||