diff options
author | Chocobozzz <me@florianbigard.com> | 2019-05-21 10:05:12 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-05-21 10:05:12 +0200 |
commit | 73b3aa6429dfb2e31628fa09a479dce318289d7d (patch) | |
tree | 88cf5c7c49ba89c18633a4a64a4acfc8d40b4a50 /server | |
parent | fd822c1c699fb89bb1c3218e047e1d842bc1ba1a (diff) | |
parent | 618750486ee2732e0ad3525349e4d42f29e1803e (diff) | |
download | PeerTube-73b3aa6429dfb2e31628fa09a479dce318289d7d.tar.gz PeerTube-73b3aa6429dfb2e31628fa09a479dce318289d7d.tar.zst PeerTube-73b3aa6429dfb2e31628fa09a479dce318289d7d.zip |
Merge branch 'feature/audio-upload' into develop
Diffstat (limited to 'server')
27 files changed, 408 insertions, 169 deletions
diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg new file mode 100644 index 000000000..a19173eac --- /dev/null +++ b/server/assets/default-audio-background.jpg | |||
Binary files differ | |||
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 40012c03b..d9ce6a153 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -255,6 +255,7 @@ function customConfig (): CustomConfig { | |||
255 | transcoding: { | 255 | transcoding: { |
256 | enabled: CONFIG.TRANSCODING.ENABLED, | 256 | enabled: CONFIG.TRANSCODING.ENABLED, |
257 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, | 257 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, |
258 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, | ||
258 | threads: CONFIG.TRANSCODING.THREADS, | 259 | threads: CONFIG.TRANSCODING.THREADS, |
259 | resolutions: { | 260 | resolutions: { |
260 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 261 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 1a18a8ae8..40a2c972b 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -6,7 +6,14 @@ import { logger } from '../../../helpers/logger' | |||
6 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 6 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
7 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 7 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
8 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 8 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
9 | import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' | 9 | import { |
10 | DEFAULT_AUDIO_RESOLUTION, | ||
11 | MIMETYPES, | ||
12 | VIDEO_CATEGORIES, | ||
13 | VIDEO_LANGUAGES, | ||
14 | VIDEO_LICENCES, | ||
15 | VIDEO_PRIVACIES | ||
16 | } from '../../../initializers/constants' | ||
10 | import { | 17 | import { |
11 | changeVideoChannelShare, | 18 | changeVideoChannelShare, |
12 | federateVideoIfNeeded, | 19 | federateVideoIfNeeded, |
@@ -54,6 +61,7 @@ import { CONFIG } from '../../../initializers/config' | |||
54 | import { sequelizeTypescript } from '../../../initializers/database' | 61 | import { sequelizeTypescript } from '../../../initializers/database' |
55 | import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' | 62 | import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' |
56 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 63 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
64 | import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' | ||
57 | 65 | ||
58 | const auditLogger = auditLoggerFactory('videos') | 66 | const auditLogger = auditLoggerFactory('videos') |
59 | const videosRouter = express.Router() | 67 | const videosRouter = express.Router() |
@@ -191,18 +199,19 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
191 | const video = new VideoModel(videoData) | 199 | const video = new VideoModel(videoData) |
192 | video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 200 | video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
193 | 201 | ||
194 | // Build the file object | ||
195 | const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) | ||
196 | const fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
197 | |||
198 | const videoFileData = { | 202 | const videoFileData = { |
199 | extname: extname(videoPhysicalFile.filename), | 203 | extname: extname(videoPhysicalFile.filename), |
200 | resolution: videoFileResolution, | 204 | size: videoPhysicalFile.size |
201 | size: videoPhysicalFile.size, | ||
202 | fps | ||
203 | } | 205 | } |
204 | const videoFile = new VideoFileModel(videoFileData) | 206 | const videoFile = new VideoFileModel(videoFileData) |
205 | 207 | ||
208 | if (!videoFile.isAudio()) { | ||
209 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
210 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
211 | } else { | ||
212 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
213 | } | ||
214 | |||
206 | // Move physical file | 215 | // Move physical file |
207 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | 216 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR |
208 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | 217 | const destination = join(videoDir, video.getVideoFilename(videoFile)) |
@@ -279,9 +288,21 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
279 | 288 | ||
280 | if (video.state === VideoState.TO_TRANSCODE) { | 289 | if (video.state === VideoState.TO_TRANSCODE) { |
281 | // Put uuid because we don't have id auto incremented for now | 290 | // Put uuid because we don't have id auto incremented for now |
282 | const dataInput = { | 291 | let dataInput: VideoTranscodingPayload |
283 | videoUUID: videoCreated.uuid, | 292 | |
284 | isNewVideo: true | 293 | if (videoFile.isAudio()) { |
294 | dataInput = { | ||
295 | type: 'merge-audio' as 'merge-audio', | ||
296 | resolution: DEFAULT_AUDIO_RESOLUTION, | ||
297 | videoUUID: videoCreated.uuid, | ||
298 | isNewVideo: true | ||
299 | } | ||
300 | } else { | ||
301 | dataInput = { | ||
302 | type: 'optimize' as 'optimize', | ||
303 | videoUUID: videoCreated.uuid, | ||
304 | isNewVideo: true | ||
305 | } | ||
285 | } | 306 | } |
286 | 307 | ||
287 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | 308 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 05019fcc2..d57dba6ce 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -181,7 +181,7 @@ async function getVideoCaption (req: express.Request, res: express.Response) { | |||
181 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE }) | 181 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE }) |
182 | } | 182 | } |
183 | 183 | ||
184 | async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { | 184 | async function generateNodeinfo (req: express.Request, res: express.Response) { |
185 | const { totalVideos } = await VideoModel.getStats() | 185 | const { totalVideos } = await VideoModel.getStats() |
186 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() | 186 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() |
187 | const { totalUsers } = await UserModel.getStats() | 187 | const { totalUsers } = await UserModel.getStats() |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index e0a1d56a5..00f3f198b 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -74,7 +74,18 @@ function createReqFiles ( | |||
74 | }, | 74 | }, |
75 | 75 | ||
76 | filename: async (req, file, cb) => { | 76 | filename: async (req, file, cb) => { |
77 | const extension = mimeTypes[ file.mimetype ] || extname(file.originalname) | 77 | let extension: string |
78 | const fileExtension = extname(file.originalname) | ||
79 | const extensionFromMimetype = mimeTypes[ file.mimetype ] | ||
80 | |||
81 | // Take the file extension if we don't understand the mime type | ||
82 | // We have the OGG/OGV exception too because firefox sends a bad mime type when sending an OGG file | ||
83 | if (fileExtension === '.ogg' || fileExtension === '.ogv' || !extensionFromMimetype) { | ||
84 | extension = fileExtension | ||
85 | } else { | ||
86 | extension = extensionFromMimetype | ||
87 | } | ||
88 | |||
78 | let randomString = '' | 89 | let randomString = '' |
79 | 90 | ||
80 | try { | 91 | try { |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 2fdf34cb7..c180da832 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -117,37 +117,50 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
117 | } | 117 | } |
118 | } | 118 | } |
119 | 119 | ||
120 | type TranscodeOptions = { | 120 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' |
121 | |||
122 | interface BaseTranscodeOptions { | ||
123 | type: TranscodeOptionsType | ||
121 | inputPath: string | 124 | inputPath: string |
122 | outputPath: string | 125 | outputPath: string |
123 | resolution: VideoResolution | 126 | resolution: VideoResolution |
124 | isPortraitMode?: boolean | 127 | isPortraitMode?: boolean |
125 | doQuickTranscode?: Boolean | 128 | } |
126 | 129 | ||
127 | hlsPlaylist?: { | 130 | interface HLSTranscodeOptions extends BaseTranscodeOptions { |
131 | type: 'hls' | ||
132 | hlsPlaylist: { | ||
128 | videoFilename: string | 133 | videoFilename: string |
129 | } | 134 | } |
130 | } | 135 | } |
131 | 136 | ||
137 | interface QuickTranscodeOptions extends BaseTranscodeOptions { | ||
138 | type: 'quick-transcode' | ||
139 | } | ||
140 | |||
141 | interface VideoTranscodeOptions extends BaseTranscodeOptions { | ||
142 | type: 'video' | ||
143 | } | ||
144 | |||
145 | interface MergeAudioTranscodeOptions extends BaseTranscodeOptions { | ||
146 | type: 'merge-audio' | ||
147 | audioPath: string | ||
148 | } | ||
149 | |||
150 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions | ||
151 | |||
132 | function transcode (options: TranscodeOptions) { | 152 | function transcode (options: TranscodeOptions) { |
133 | return new Promise<void>(async (res, rej) => { | 153 | return new Promise<void>(async (res, rej) => { |
134 | try { | 154 | try { |
135 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 155 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
136 | .output(options.outputPath) | 156 | .output(options.outputPath) |
137 | 157 | ||
138 | if (options.doQuickTranscode) { | 158 | if (options.type === 'quick-transcode') { |
139 | if (options.hlsPlaylist) { | 159 | command = await buildQuickTranscodeCommand(command) |
140 | throw(Error("Quick transcode and HLS can't be used at the same time")) | 160 | } else if (options.type === 'hls') { |
141 | } | ||
142 | |||
143 | command | ||
144 | .format('mp4') | ||
145 | .addOption('-c:v copy') | ||
146 | .addOption('-c:a copy') | ||
147 | .outputOption('-map_metadata -1') // strip all metadata | ||
148 | .outputOption('-movflags faststart') | ||
149 | } else if (options.hlsPlaylist) { | ||
150 | command = await buildHLSCommand(command, options) | 161 | command = await buildHLSCommand(command, options) |
162 | } else if (options.type === 'merge-audio') { | ||
163 | command = await buildAudioMergeCommand(command, options) | ||
151 | } else { | 164 | } else { |
152 | command = await buildx264Command(command, options) | 165 | command = await buildx264Command(command, options) |
153 | } | 166 | } |
@@ -163,7 +176,7 @@ function transcode (options: TranscodeOptions) { | |||
163 | return rej(err) | 176 | return rej(err) |
164 | }) | 177 | }) |
165 | .on('end', () => { | 178 | .on('end', () => { |
166 | return onTranscodingSuccess(options) | 179 | return fixHLSPlaylistIfNeeded(options) |
167 | .then(() => res()) | 180 | .then(() => res()) |
168 | .catch(err => rej(err)) | 181 | .catch(err => rej(err)) |
169 | }) | 182 | }) |
@@ -205,6 +218,8 @@ export { | |||
205 | getVideoFileResolution, | 218 | getVideoFileResolution, |
206 | getDurationFromVideoFile, | 219 | getDurationFromVideoFile, |
207 | generateImageFromVideoFile, | 220 | generateImageFromVideoFile, |
221 | TranscodeOptions, | ||
222 | TranscodeOptionsType, | ||
208 | transcode, | 223 | transcode, |
209 | getVideoFileFPS, | 224 | getVideoFileFPS, |
210 | computeResolutionsToTranscode, | 225 | computeResolutionsToTranscode, |
@@ -215,7 +230,7 @@ export { | |||
215 | 230 | ||
216 | // --------------------------------------------------------------------------- | 231 | // --------------------------------------------------------------------------- |
217 | 232 | ||
218 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 233 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) { |
219 | let fps = await getVideoFileFPS(options.inputPath) | 234 | let fps = await getVideoFileFPS(options.inputPath) |
220 | // On small/medium resolutions, limit FPS | 235 | // On small/medium resolutions, limit FPS |
221 | if ( | 236 | if ( |
@@ -226,7 +241,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
226 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 241 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
227 | } | 242 | } |
228 | 243 | ||
229 | command = await presetH264(command, options.resolution, fps) | 244 | command = await presetH264(command, options.inputPath, options.resolution, fps) |
230 | 245 | ||
231 | if (options.resolution !== undefined) { | 246 | if (options.resolution !== undefined) { |
232 | // '?x720' or '720x?' for example | 247 | // '?x720' or '720x?' for example |
@@ -245,7 +260,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
245 | return command | 260 | return command |
246 | } | 261 | } |
247 | 262 | ||
248 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 263 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { |
264 | command = command.loop(undefined) | ||
265 | |||
266 | command = await presetH264VeryFast(command, options.audioPath, options.resolution) | ||
267 | |||
268 | command = command.input(options.audioPath) | ||
269 | .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error | ||
270 | .outputOption('-tune stillimage') | ||
271 | .outputOption('-shortest') | ||
272 | |||
273 | return command | ||
274 | } | ||
275 | |||
276 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | ||
277 | command = await presetCopy(command) | ||
278 | |||
279 | command = command.outputOption('-map_metadata -1') // strip all metadata | ||
280 | .outputOption('-movflags faststart') | ||
281 | |||
282 | return command | ||
283 | } | ||
284 | |||
285 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | ||
249 | const videoPath = getHLSVideoPath(options) | 286 | const videoPath = getHLSVideoPath(options) |
250 | 287 | ||
251 | command = await presetCopy(command) | 288 | command = await presetCopy(command) |
@@ -261,19 +298,19 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod | |||
261 | return command | 298 | return command |
262 | } | 299 | } |
263 | 300 | ||
264 | function getHLSVideoPath (options: TranscodeOptions) { | 301 | function getHLSVideoPath (options: HLSTranscodeOptions) { |
265 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | 302 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` |
266 | } | 303 | } |
267 | 304 | ||
268 | async function onTranscodingSuccess (options: TranscodeOptions) { | 305 | async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { |
269 | if (!options.hlsPlaylist) return | 306 | if (options.type !== 'hls') return |
270 | 307 | ||
271 | // Fix wrong mapping with some ffmpeg versions | ||
272 | const fileContent = await readFile(options.outputPath) | 308 | const fileContent = await readFile(options.outputPath) |
273 | 309 | ||
274 | const videoFileName = options.hlsPlaylist.videoFilename | 310 | const videoFileName = options.hlsPlaylist.videoFilename |
275 | const videoFilePath = getHLSVideoPath(options) | 311 | const videoFilePath = getHLSVideoPath(options) |
276 | 312 | ||
313 | // Fix wrong mapping with some ffmpeg versions | ||
277 | const newContent = fileContent.toString() | 314 | const newContent = fileContent.toString() |
278 | .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) | 315 | .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) |
279 | 316 | ||
@@ -300,44 +337,27 @@ function getVideoStreamFromFile (path: string) { | |||
300 | * and quality. Superfast and ultrafast will give you better | 337 | * and quality. Superfast and ultrafast will give you better |
301 | * performance, but then quality is noticeably worse. | 338 | * performance, but then quality is noticeably worse. |
302 | */ | 339 | */ |
303 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { | 340 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) { |
304 | let localCommand = await presetH264(command, resolution, fps) | 341 | let localCommand = await presetH264(command, input, resolution, fps) |
342 | |||
305 | localCommand = localCommand.outputOption('-preset:v veryfast') | 343 | localCommand = localCommand.outputOption('-preset:v veryfast') |
306 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) | 344 | |
307 | /* | 345 | /* |
308 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 346 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
309 | Our target situation is closer to a livestream than a stream, | 347 | Our target situation is closer to a livestream than a stream, |
310 | since we want to reduce as much a possible the encoding burden, | 348 | since we want to reduce as much a possible the encoding burden, |
311 | altough not to the point of a livestream where there is a hard | 349 | although not to the point of a livestream where there is a hard |
312 | constraint on the frames per second to be encoded. | 350 | constraint on the frames per second to be encoded. |
313 | |||
314 | why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'? | ||
315 | Make up for most of the loss of grain and macroblocking | ||
316 | with less computing power. | ||
317 | */ | 351 | */ |
318 | 352 | ||
319 | return localCommand | 353 | return localCommand |
320 | } | 354 | } |
321 | 355 | ||
322 | /** | 356 | /** |
323 | * A preset optimised for a stillimage audio video | ||
324 | */ | ||
325 | async function presetStillImageWithAudio ( | ||
326 | command: ffmpeg.FfmpegCommand, | ||
327 | resolution: VideoResolution, | ||
328 | fps: number | ||
329 | ): Promise<ffmpeg.FfmpegCommand> { | ||
330 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
331 | localCommand = localCommand.outputOption('-tune stillimage') | ||
332 | |||
333 | return localCommand | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * A toolbox to play with audio | 357 | * A toolbox to play with audio |
338 | */ | 358 | */ |
339 | namespace audio { | 359 | namespace audio { |
340 | export const get = (option: ffmpeg.FfmpegCommand | string) => { | 360 | export const get = (option: string) => { |
341 | // without position, ffprobe considers the last input only | 361 | // without position, ffprobe considers the last input only |
342 | // we make it consider the first input only | 362 | // we make it consider the first input only |
343 | // if you pass a file path to pos, then ffprobe acts on that file directly | 363 | // if you pass a file path to pos, then ffprobe acts on that file directly |
@@ -359,11 +379,7 @@ namespace audio { | |||
359 | return res({ absolutePath: data.format.filename }) | 379 | return res({ absolutePath: data.format.filename }) |
360 | } | 380 | } |
361 | 381 | ||
362 | if (typeof option === 'string') { | 382 | return ffmpeg.ffprobe(option, parseFfprobe) |
363 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
364 | } | ||
365 | |||
366 | return option.ffprobe(parseFfprobe) | ||
367 | }) | 383 | }) |
368 | } | 384 | } |
369 | 385 | ||
@@ -405,7 +421,7 @@ namespace audio { | |||
405 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 421 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
406 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 422 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
407 | */ | 423 | */ |
408 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { | 424 | async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) { |
409 | let localCommand = command | 425 | let localCommand = command |
410 | .format('mp4') | 426 | .format('mp4') |
411 | .videoCodec('libx264') | 427 | .videoCodec('libx264') |
@@ -416,7 +432,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol | |||
416 | .outputOption('-map_metadata -1') // strip all metadata | 432 | .outputOption('-map_metadata -1') // strip all metadata |
417 | .outputOption('-movflags faststart') | 433 | .outputOption('-movflags faststart') |
418 | 434 | ||
419 | const parsedAudio = await audio.get(localCommand) | 435 | const parsedAudio = await audio.get(input) |
420 | 436 | ||
421 | if (!parsedAudio.audioStream) { | 437 | if (!parsedAudio.audioStream) { |
422 | localCommand = localCommand.noAudio() | 438 | localCommand = localCommand.noAudio() |
@@ -425,28 +441,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol | |||
425 | .audioCodec('libfdk_aac') | 441 | .audioCodec('libfdk_aac') |
426 | .audioQuality(5) | 442 | .audioQuality(5) |
427 | } else { | 443 | } else { |
428 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 444 | // we try to reduce the ceiling bitrate by making rough matches of bitrates |
429 | // of course this is far from perfect, but it might save some space in the end | 445 | // of course this is far from perfect, but it might save some space in the end |
446 | localCommand = localCommand.audioCodec('aac') | ||
447 | |||
430 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | 448 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] |
431 | let bitrate: number | ||
432 | if (audio.bitrate[ audioCodecName ]) { | ||
433 | localCommand = localCommand.audioCodec('aac') | ||
434 | 449 | ||
435 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | 450 | if (audio.bitrate[ audioCodecName ]) { |
451 | const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
436 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | 452 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) |
437 | } | 453 | } |
438 | } | 454 | } |
439 | 455 | ||
440 | // Constrained Encoding (VBV) | 456 | if (fps) { |
441 | // https://slhck.info/video/2017/03/01/rate-control.html | 457 | // Constrained Encoding (VBV) |
442 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | 458 | // https://slhck.info/video/2017/03/01/rate-control.html |
443 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | 459 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
444 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) | 460 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
445 | 461 | localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ]) | |
446 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. | 462 | |
447 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | 463 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
448 | // https://superuser.com/a/908325 | 464 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html |
449 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | 465 | // https://superuser.com/a/908325 |
466 | localCommand = localCommand.outputOption(`-g ${fps * 2}`) | ||
467 | } | ||
450 | 468 | ||
451 | return localCommand | 469 | return localCommand |
452 | } | 470 | } |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 723755c45..2be300a57 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -148,6 +148,7 @@ const CONFIG = { | |||
148 | TRANSCODING: { | 148 | TRANSCODING: { |
149 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, | 149 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, |
150 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, | 150 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, |
151 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, | ||
151 | get THREADS () { return config.get<number>('transcoding.threads') }, | 152 | get THREADS () { return config.get<number>('transcoding.threads') }, |
152 | RESOLUTIONS: { | 153 | RESOLUTIONS: { |
153 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 154 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 62778ae58..8a11101ff 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { JobType, VideoRateType, VideoState } from '../../shared/models' | 2 | import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' |
3 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 3 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
4 | import { FollowState } from '../../shared/models/actors' | 4 | import { FollowState } from '../../shared/models/actors' |
5 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' | 5 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' |
6 | // Do not use barrels, remain constants as independent as possible | 6 | // Do not use barrels, remain constants as independent as possible |
7 | import { isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 7 | import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' |
8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
9 | import { invert } from 'lodash' | 9 | import { invert } from 'lodash' |
10 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' | 10 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' |
@@ -228,7 +228,7 @@ let CONSTRAINTS_FIELDS = { | |||
228 | max: 2 * 1024 * 1024 // 2MB | 228 | max: 2 * 1024 * 1024 // 2MB |
229 | } | 229 | } |
230 | }, | 230 | }, |
231 | EXTNAME: buildVideosExtname(), | 231 | EXTNAME: [] as string[], |
232 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 | 232 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 |
233 | DURATION: { min: 0 }, // Number | 233 | DURATION: { min: 0 }, // Number |
234 | TAGS: { min: 0, max: 5 }, // Number of total tags | 234 | TAGS: { min: 0, max: 5 }, // Number of total tags |
@@ -300,6 +300,8 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { | |||
300 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) | 300 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) |
301 | } | 301 | } |
302 | 302 | ||
303 | const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P | ||
304 | |||
303 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { | 305 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { |
304 | LIKE: 'like', | 306 | LIKE: 'like', |
305 | DISLIKE: 'dislike' | 307 | DISLIKE: 'dislike' |
@@ -380,8 +382,18 @@ const VIDEO_PLAYLIST_TYPES = { | |||
380 | } | 382 | } |
381 | 383 | ||
382 | const MIMETYPES = { | 384 | const MIMETYPES = { |
385 | AUDIO: { | ||
386 | MIMETYPE_EXT: { | ||
387 | 'audio/mpeg': '.mp3', | ||
388 | 'audio/mp3': '.mp3', | ||
389 | 'application/ogg': '.ogg', | ||
390 | 'audio/ogg': '.ogg', | ||
391 | 'audio/flac': '.flac' | ||
392 | }, | ||
393 | EXT_MIMETYPE: null as { [ id: string ]: string } | ||
394 | }, | ||
383 | VIDEO: { | 395 | VIDEO: { |
384 | MIMETYPE_EXT: buildVideoMimetypeExt(), | 396 | MIMETYPE_EXT: null as { [ id: string ]: string }, |
385 | EXT_MIMETYPE: null as { [ id: string ]: string } | 397 | EXT_MIMETYPE: null as { [ id: string ]: string } |
386 | }, | 398 | }, |
387 | IMAGE: { | 399 | IMAGE: { |
@@ -403,7 +415,7 @@ const MIMETYPES = { | |||
403 | } | 415 | } |
404 | } | 416 | } |
405 | } | 417 | } |
406 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | 418 | MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT) |
407 | 419 | ||
408 | // --------------------------------------------------------------------------- | 420 | // --------------------------------------------------------------------------- |
409 | 421 | ||
@@ -429,7 +441,7 @@ const ACTIVITY_PUB = { | |||
429 | COLLECTION_ITEMS_PER_PAGE: 10, | 441 | COLLECTION_ITEMS_PER_PAGE: 10, |
430 | FETCH_PAGE_LIMIT: 100, | 442 | FETCH_PAGE_LIMIT: 100, |
431 | URL_MIME_TYPES: { | 443 | URL_MIME_TYPES: { |
432 | VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT), | 444 | VIDEO: [] as string[], |
433 | TORRENT: [ 'application/x-bittorrent' ], | 445 | TORRENT: [ 'application/x-bittorrent' ], |
434 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] | 446 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] |
435 | }, | 447 | }, |
@@ -497,8 +509,8 @@ const THUMBNAILS_SIZE = { | |||
497 | height: 122 | 509 | height: 122 |
498 | } | 510 | } |
499 | const PREVIEWS_SIZE = { | 511 | const PREVIEWS_SIZE = { |
500 | width: 560, | 512 | width: 850, |
501 | height: 315 | 513 | height: 480 |
502 | } | 514 | } |
503 | const AVATARS_SIZE = { | 515 | const AVATARS_SIZE = { |
504 | width: 120, | 516 | width: 120, |
@@ -543,6 +555,10 @@ const REDUNDANCY = { | |||
543 | 555 | ||
544 | const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) | 556 | const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) |
545 | 557 | ||
558 | const ASSETS_PATH = { | ||
559 | DEFAULT_AUDIO_BACKGROUND: join(root(), 'server', 'assets', 'default-audio-background.jpg') | ||
560 | } | ||
561 | |||
546 | // --------------------------------------------------------------------------- | 562 | // --------------------------------------------------------------------------- |
547 | 563 | ||
548 | const CUSTOM_HTML_TAG_COMMENTS = { | 564 | const CUSTOM_HTML_TAG_COMMENTS = { |
@@ -612,6 +628,7 @@ if (isTestInstance() === true) { | |||
612 | } | 628 | } |
613 | 629 | ||
614 | updateWebserverUrls() | 630 | updateWebserverUrls() |
631 | updateWebserverConfig() | ||
615 | 632 | ||
616 | registerConfigChangedHandler(() => { | 633 | registerConfigChangedHandler(() => { |
617 | updateWebserverUrls() | 634 | updateWebserverUrls() |
@@ -681,12 +698,14 @@ export { | |||
681 | RATES_LIMIT, | 698 | RATES_LIMIT, |
682 | MIMETYPES, | 699 | MIMETYPES, |
683 | CRAWL_REQUEST_CONCURRENCY, | 700 | CRAWL_REQUEST_CONCURRENCY, |
701 | DEFAULT_AUDIO_RESOLUTION, | ||
684 | JOB_COMPLETED_LIFETIME, | 702 | JOB_COMPLETED_LIFETIME, |
685 | HTTP_SIGNATURE, | 703 | HTTP_SIGNATURE, |
686 | VIDEO_IMPORT_STATES, | 704 | VIDEO_IMPORT_STATES, |
687 | VIDEO_VIEW_LIFETIME, | 705 | VIDEO_VIEW_LIFETIME, |
688 | CONTACT_FORM_LIFETIME, | 706 | CONTACT_FORM_LIFETIME, |
689 | VIDEO_PLAYLIST_PRIVACIES, | 707 | VIDEO_PLAYLIST_PRIVACIES, |
708 | ASSETS_PATH, | ||
690 | loadLanguages, | 709 | loadLanguages, |
691 | buildLanguages | 710 | buildLanguages |
692 | } | 711 | } |
@@ -700,15 +719,21 @@ function buildVideoMimetypeExt () { | |||
700 | 'video/mp4': '.mp4' | 719 | 'video/mp4': '.mp4' |
701 | } | 720 | } |
702 | 721 | ||
703 | if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { | 722 | if (CONFIG.TRANSCODING.ENABLED) { |
704 | Object.assign(data, { | 723 | if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { |
705 | 'video/quicktime': '.mov', | 724 | Object.assign(data, { |
706 | 'video/x-msvideo': '.avi', | 725 | 'video/quicktime': '.mov', |
707 | 'video/x-flv': '.flv', | 726 | 'video/x-msvideo': '.avi', |
708 | 'video/x-matroska': '.mkv', | 727 | 'video/x-flv': '.flv', |
709 | 'application/octet-stream': '.mkv', | 728 | 'video/x-matroska': '.mkv', |
710 | 'video/avi': '.avi' | 729 | 'application/octet-stream': '.mkv', |
711 | }) | 730 | 'video/avi': '.avi' |
731 | }) | ||
732 | } | ||
733 | |||
734 | if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) { | ||
735 | Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT) | ||
736 | } | ||
712 | } | 737 | } |
713 | 738 | ||
714 | return data | 739 | return data |
@@ -724,16 +749,15 @@ function updateWebserverUrls () { | |||
724 | } | 749 | } |
725 | 750 | ||
726 | function updateWebserverConfig () { | 751 | function updateWebserverConfig () { |
727 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
728 | |||
729 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() | 752 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() |
730 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | 753 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) |
754 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
755 | |||
756 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
731 | } | 757 | } |
732 | 758 | ||
733 | function buildVideosExtname () { | 759 | function buildVideosExtname () { |
734 | return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS | 760 | return Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE) |
735 | ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ] | ||
736 | : [ '.mp4', '.ogv', '.webm' ] | ||
737 | } | 761 | } |
738 | 762 | ||
739 | function loadLanguages () { | 763 | function loadLanguages () { |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index 14be7f24a..a68619d07 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -21,7 +21,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
21 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) | 21 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) |
22 | if (!video) return undefined | 22 | if (!video) return undefined |
23 | 23 | ||
24 | if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) } | 24 | if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() } |
25 | 25 | ||
26 | return this.loadRemoteFile(videoUUID) | 26 | return this.loadRemoteFile(videoUUID) |
27 | } | 27 | } |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 921d9a083..8cacb0ef3 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { VideoModel } from '../../../models/video/video' | 3 | import { VideoModel } from '../../../models/video/video' |
4 | import { publishVideoIfNeeded } from './video-transcoding' | 4 | import { publishNewResolutionIfNeeded } from './video-transcoding' |
5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
6 | import { copy, stat } from 'fs-extra' | 6 | import { copy, stat } from 'fs-extra' |
7 | import { VideoFileModel } from '../../../models/video/video-file' | 7 | import { VideoFileModel } from '../../../models/video/video-file' |
@@ -25,7 +25,7 @@ async function processVideoFileImport (job: Bull.Job) { | |||
25 | 25 | ||
26 | await updateVideoFile(video, payload.filePath) | 26 | await updateVideoFile(video, payload.filePath) |
27 | 27 | ||
28 | await publishVideoIfNeeded(video) | 28 | await publishNewResolutionIfNeeded(video) |
29 | return video | 29 | return video |
30 | } | 30 | } |
31 | 31 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 1650916a6..50e159245 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -209,6 +209,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
209 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { | 209 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { |
210 | // Put uuid because we don't have id auto incremented for now | 210 | // Put uuid because we don't have id auto incremented for now |
211 | const dataInput = { | 211 | const dataInput = { |
212 | type: 'optimize' as 'optimize', | ||
212 | videoUUID: videoImportUpdated.Video.uuid, | 213 | videoUUID: videoImportUpdated.Video.uuid, |
213 | isNewVideo: true | 214 | isNewVideo: true |
214 | } | 215 | } |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 48cac517e..e9b84ecd6 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -8,18 +8,39 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 12 | import { Notifier } from '../../notifier' |
13 | import { CONFIG } from '../../../initializers/config' | 13 | import { CONFIG } from '../../../initializers/config' |
14 | 14 | ||
15 | export type VideoTranscodingPayload = { | 15 | interface BaseTranscodingPayload { |
16 | videoUUID: string | 16 | videoUUID: string |
17 | resolution?: VideoResolution | ||
18 | isNewVideo?: boolean | 17 | isNewVideo?: boolean |
18 | } | ||
19 | |||
20 | interface HLSTranscodingPayload extends BaseTranscodingPayload { | ||
21 | type: 'hls' | ||
22 | isPortraitMode?: boolean | ||
23 | resolution: VideoResolution | ||
24 | } | ||
25 | |||
26 | interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { | ||
27 | type: 'new-resolution' | ||
19 | isPortraitMode?: boolean | 28 | isPortraitMode?: boolean |
20 | generateHlsPlaylist?: boolean | 29 | resolution: VideoResolution |
30 | } | ||
31 | |||
32 | interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { | ||
33 | type: 'merge-audio' | ||
34 | resolution: VideoResolution | ||
35 | } | ||
36 | |||
37 | interface OptimizeTranscodingPayload extends BaseTranscodingPayload { | ||
38 | type: 'optimize' | ||
21 | } | 39 | } |
22 | 40 | ||
41 | export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload | ||
42 | | OptimizeTranscodingPayload | MergeAudioTranscodingPayload | ||
43 | |||
23 | async function processVideoTranscoding (job: Bull.Job) { | 44 | async function processVideoTranscoding (job: Bull.Job) { |
24 | const payload = job.data as VideoTranscodingPayload | 45 | const payload = job.data as VideoTranscodingPayload |
25 | logger.info('Processing video file in job %d.', job.id) | 46 | logger.info('Processing video file in job %d.', job.id) |
@@ -31,14 +52,18 @@ async function processVideoTranscoding (job: Bull.Job) { | |||
31 | return undefined | 52 | return undefined |
32 | } | 53 | } |
33 | 54 | ||
34 | if (payload.generateHlsPlaylist) { | 55 | if (payload.type === 'hls') { |
35 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) | 56 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) |
36 | 57 | ||
37 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | 58 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) |
38 | } else if (payload.resolution) { // Transcoding in other resolution | 59 | } else if (payload.type === 'new-resolution') { |
39 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 60 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
40 | 61 | ||
41 | await retryTransactionWrapper(publishVideoIfNeeded, video, payload) | 62 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
63 | } else if (payload.type === 'merge-audio') { | ||
64 | await mergeAudioVideofile(video, payload.resolution) | ||
65 | |||
66 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | ||
42 | } else { | 67 | } else { |
43 | await optimizeVideofile(video) | 68 | await optimizeVideofile(video) |
44 | 69 | ||
@@ -62,7 +87,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) { | |||
62 | }) | 87 | }) |
63 | } | 88 | } |
64 | 89 | ||
65 | async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodingPayload) { | 90 | async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) { |
66 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 91 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
67 | // Maybe the video changed in database, refresh it | 92 | // Maybe the video changed in database, refresh it |
68 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 93 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
@@ -94,7 +119,7 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi | |||
94 | await createHlsJobIfEnabled(payload) | 119 | await createHlsJobIfEnabled(payload) |
95 | } | 120 | } |
96 | 121 | ||
97 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoTranscodingPayload) { | 122 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) { |
98 | if (videoArg === undefined) return undefined | 123 | if (videoArg === undefined) return undefined |
99 | 124 | ||
100 | // Outside the transaction (IO on disk) | 125 | // Outside the transaction (IO on disk) |
@@ -120,6 +145,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video | |||
120 | 145 | ||
121 | for (const resolution of resolutionsEnabled) { | 146 | for (const resolution of resolutionsEnabled) { |
122 | const dataInput = { | 147 | const dataInput = { |
148 | type: 'new-resolution' as 'new-resolution', | ||
123 | videoUUID: videoDatabase.uuid, | 149 | videoUUID: videoDatabase.uuid, |
124 | resolution | 150 | resolution |
125 | } | 151 | } |
@@ -149,27 +175,27 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video | |||
149 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | 175 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) |
150 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | 176 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) |
151 | 177 | ||
152 | await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) | 178 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }) |
179 | await createHlsJobIfEnabled(hlsPayload) | ||
153 | } | 180 | } |
154 | 181 | ||
155 | // --------------------------------------------------------------------------- | 182 | // --------------------------------------------------------------------------- |
156 | 183 | ||
157 | export { | 184 | export { |
158 | processVideoTranscoding, | 185 | processVideoTranscoding, |
159 | publishVideoIfNeeded | 186 | publishNewResolutionIfNeeded |
160 | } | 187 | } |
161 | 188 | ||
162 | // --------------------------------------------------------------------------- | 189 | // --------------------------------------------------------------------------- |
163 | 190 | ||
164 | function createHlsJobIfEnabled (payload?: VideoTranscodingPayload) { | 191 | function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: number, isPortraitMode?: boolean }) { |
165 | // Generate HLS playlist? | 192 | // Generate HLS playlist? |
166 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { | 193 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { |
167 | const hlsTranscodingPayload = { | 194 | const hlsTranscodingPayload = { |
195 | type: 'hls' as 'hls', | ||
168 | videoUUID: payload.videoUUID, | 196 | videoUUID: payload.videoUUID, |
169 | resolution: payload.resolution, | 197 | resolution: payload.resolution, |
170 | isPortraitMode: payload.isPortraitMode, | 198 | isPortraitMode: payload.isPortraitMode |
171 | |||
172 | generateHlsPlaylist: true | ||
173 | } | 199 | } |
174 | 200 | ||
175 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) | 201 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 950b14c3b..18bdcded4 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { VideoFileModel } from '../models/video/video-file' | 1 | import { VideoFileModel } from '../models/video/video-file' |
2 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 2 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
3 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
4 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 4 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants' |
5 | import { VideoModel } from '../models/video/video' | 5 | import { VideoModel } from '../models/video/video' |
6 | import { ThumbnailModel } from '../models/video/thumbnail' | 6 | import { ThumbnailModel } from '../models/video/thumbnail' |
7 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 7 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
@@ -45,8 +45,10 @@ function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, | |||
45 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { | 45 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { |
46 | const input = video.getVideoFilePath(videoFile) | 46 | const input = video.getVideoFilePath(videoFile) |
47 | 47 | ||
48 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type) | 48 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) |
49 | const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | 49 | const thumbnailCreator = videoFile.isAudio() |
50 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) | ||
51 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | ||
50 | 52 | ||
51 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | 53 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) |
52 | } | 54 | } |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index d6b6b251a..8d786e0ef 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { getVideoFileFPS, transcode, canDoQuickTranscode } from '../helpers/ffmpeg-utils' | 3 | import { canDoQuickTranscode, getVideoFileFPS, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' |
4 | import { ensureDir, move, remove, stat } from 'fs-extra' | 4 | import { ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
@@ -23,13 +23,15 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
23 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 23 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) |
24 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 24 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
25 | 25 | ||
26 | const doQuickTranscode = await(canDoQuickTranscode(videoInputPath)) | 26 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) |
27 | ? 'quick-transcode' | ||
28 | : 'video' | ||
27 | 29 | ||
28 | const transcodeOptions = { | 30 | const transcodeOptions: TranscodeOptions = { |
31 | type: transcodeType as any, // FIXME: typing issue | ||
29 | inputPath: videoInputPath, | 32 | inputPath: videoInputPath, |
30 | outputPath: videoTranscodedPath, | 33 | outputPath: videoTranscodedPath, |
31 | resolution: inputVideoFile.resolution, | 34 | resolution: inputVideoFile.resolution |
32 | doQuickTranscode | ||
33 | } | 35 | } |
34 | 36 | ||
35 | // Could be very long! | 37 | // Could be very long! |
@@ -39,19 +41,11 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
39 | await remove(videoInputPath) | 41 | await remove(videoInputPath) |
40 | 42 | ||
41 | // Important to do this before getVideoFilename() to take in account the new file extension | 43 | // Important to do this before getVideoFilename() to take in account the new file extension |
42 | inputVideoFile.set('extname', newExtname) | 44 | inputVideoFile.extname = newExtname |
43 | |||
44 | const stats = await stat(videoTranscodedPath) | ||
45 | const fps = await getVideoFileFPS(videoTranscodedPath) | ||
46 | 45 | ||
47 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 46 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) |
48 | await move(videoTranscodedPath, videoOutputPath) | ||
49 | 47 | ||
50 | inputVideoFile.set('size', stats.size) | 48 | await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
51 | inputVideoFile.set('fps', fps) | ||
52 | |||
53 | await video.createTorrentAndSetInfoHash(inputVideoFile) | ||
54 | await inputVideoFile.save() | ||
55 | } catch (err) { | 49 | } catch (err) { |
56 | // Auto destruction... | 50 | // Auto destruction... |
57 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | 51 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) |
@@ -81,6 +75,7 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
81 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) | 75 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) |
82 | 76 | ||
83 | const transcodeOptions = { | 77 | const transcodeOptions = { |
78 | type: 'video' as 'video', | ||
84 | inputPath: videoInputPath, | 79 | inputPath: videoInputPath, |
85 | outputPath: videoTranscodedPath, | 80 | outputPath: videoTranscodedPath, |
86 | resolution, | 81 | resolution, |
@@ -89,19 +84,37 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
89 | 84 | ||
90 | await transcode(transcodeOptions) | 85 | await transcode(transcodeOptions) |
91 | 86 | ||
92 | const stats = await stat(videoTranscodedPath) | 87 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) |
93 | const fps = await getVideoFileFPS(videoTranscodedPath) | 88 | } |
89 | |||
90 | async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) { | ||
91 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
92 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | ||
93 | const newExtname = '.mp4' | ||
94 | |||
95 | const inputVideoFile = video.getOriginalFile() | ||
94 | 96 | ||
95 | await move(videoTranscodedPath, videoOutputPath) | 97 | const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) |
98 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | ||
96 | 99 | ||
97 | newVideoFile.set('size', stats.size) | 100 | const transcodeOptions = { |
98 | newVideoFile.set('fps', fps) | 101 | type: 'merge-audio' as 'merge-audio', |
102 | inputPath: video.getPreview().getPath(), | ||
103 | outputPath: videoTranscodedPath, | ||
104 | audioPath: audioInputPath, | ||
105 | resolution | ||
106 | } | ||
99 | 107 | ||
100 | await video.createTorrentAndSetInfoHash(newVideoFile) | 108 | await transcode(transcodeOptions) |
101 | 109 | ||
102 | await newVideoFile.save() | 110 | await remove(audioInputPath) |
103 | 111 | ||
104 | video.VideoFiles.push(newVideoFile) | 112 | // Important to do this before getVideoFilename() to take in account the new file extension |
113 | inputVideoFile.extname = newExtname | ||
114 | |||
115 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | ||
116 | |||
117 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | ||
105 | } | 118 | } |
106 | 119 | ||
107 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | 120 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { |
@@ -112,6 +125,7 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti | |||
112 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 125 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) |
113 | 126 | ||
114 | const transcodeOptions = { | 127 | const transcodeOptions = { |
128 | type: 'hls' as 'hls', | ||
115 | inputPath: videoInputPath, | 129 | inputPath: videoInputPath, |
116 | outputPath, | 130 | outputPath, |
117 | resolution, | 131 | resolution, |
@@ -140,8 +154,34 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti | |||
140 | }) | 154 | }) |
141 | } | 155 | } |
142 | 156 | ||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
143 | export { | 159 | export { |
144 | generateHlsPlaylist, | 160 | generateHlsPlaylist, |
145 | optimizeVideofile, | 161 | optimizeVideofile, |
146 | transcodeOriginalVideofile | 162 | transcodeOriginalVideofile, |
163 | mergeAudioVideofile | ||
164 | } | ||
165 | |||
166 | // --------------------------------------------------------------------------- | ||
167 | |||
168 | async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) { | ||
169 | const stats = await stat(transcodingPath) | ||
170 | const fps = await getVideoFileFPS(transcodingPath) | ||
171 | |||
172 | await move(transcodingPath, outputPath) | ||
173 | |||
174 | videoFile.set('size', stats.size) | ||
175 | videoFile.set('fps', fps) | ||
176 | |||
177 | await video.createTorrentAndSetInfoHash(videoFile) | ||
178 | |||
179 | const updatedVideoFile = await videoFile.save() | ||
180 | |||
181 | // Add it if this is a new created file | ||
182 | if (video.VideoFiles.some(f => f.id === videoFile.id) === false) { | ||
183 | video.VideoFiles.push(updatedVideoFile) | ||
184 | } | ||
185 | |||
186 | return video | ||
147 | } | 187 | } |
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index 206e9a3d6..8faf0adba 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -107,10 +107,12 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
107 | return WEBSERVER.URL + staticPath + this.filename | 107 | return WEBSERVER.URL + staticPath + this.filename |
108 | } | 108 | } |
109 | 109 | ||
110 | removeThumbnail () { | 110 | getPath () { |
111 | const directory = ThumbnailModel.types[this.type].directory | 111 | const directory = ThumbnailModel.types[this.type].directory |
112 | const thumbnailPath = join(directory, this.filename) | 112 | return join(directory, this.filename) |
113 | } | ||
113 | 114 | ||
114 | return remove(thumbnailPath) | 115 | removeThumbnail () { |
116 | return remove(this.getPath()) | ||
115 | } | 117 | } |
116 | } | 118 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 2203a7aba..05c490759 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -24,6 +24,7 @@ import { VideoModel } from './video' | |||
24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' | 26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' |
27 | import { MIMETYPES } from '../../initializers/constants' | ||
27 | 28 | ||
28 | @Table({ | 29 | @Table({ |
29 | tableName: 'videoFile', | 30 | tableName: 'videoFile', |
@@ -161,6 +162,10 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
161 | })) | 162 | })) |
162 | } | 163 | } |
163 | 164 | ||
165 | isAudio () { | ||
166 | return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] | ||
167 | } | ||
168 | |||
164 | hasSameUniqueKeysThan (other: VideoFileModel) { | 169 | hasSameUniqueKeysThan (other: VideoFileModel) { |
165 | return this.fps === other.fps && | 170 | return this.fps === other.fps && |
166 | this.resolution === other.resolution && | 171 | this.resolution === other.resolution && |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 2a2ec606a..8155e11ab 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -59,6 +59,7 @@ describe('Test config API validators', function () { | |||
59 | transcoding: { | 59 | transcoding: { |
60 | enabled: true, | 60 | enabled: true, |
61 | allowAdditionalExtensions: true, | 61 | allowAdditionalExtensions: true, |
62 | allowAudioFiles: true, | ||
62 | threads: 1, | 63 | threads: 1, |
63 | resolutions: { | 64 | resolutions: { |
64 | '240p': false, | 65 | '240p': false, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index ca389b7b6..8ea21158a 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | registerUser, | 15 | registerUser, |
16 | reRunServer, ServerInfo, | 16 | reRunServer, ServerInfo, |
17 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
18 | updateCustomConfig | 18 | updateCustomConfig, uploadVideo |
19 | } from '../../../../shared/extra-utils' | 19 | } from '../../../../shared/extra-utils' |
20 | import { ServerConfig } from '../../../../shared/models' | 20 | import { ServerConfig } from '../../../../shared/models' |
21 | 21 | ||
@@ -52,6 +52,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
52 | expect(data.user.videoQuotaDaily).to.equal(-1) | 52 | expect(data.user.videoQuotaDaily).to.equal(-1) |
53 | expect(data.transcoding.enabled).to.be.false | 53 | expect(data.transcoding.enabled).to.be.false |
54 | expect(data.transcoding.allowAdditionalExtensions).to.be.false | 54 | expect(data.transcoding.allowAdditionalExtensions).to.be.false |
55 | expect(data.transcoding.allowAudioFiles).to.be.false | ||
55 | expect(data.transcoding.threads).to.equal(2) | 56 | expect(data.transcoding.threads).to.equal(2) |
56 | expect(data.transcoding.resolutions['240p']).to.be.true | 57 | expect(data.transcoding.resolutions['240p']).to.be.true |
57 | expect(data.transcoding.resolutions['360p']).to.be.true | 58 | expect(data.transcoding.resolutions['360p']).to.be.true |
@@ -102,6 +103,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
102 | expect(data.transcoding.enabled).to.be.true | 103 | expect(data.transcoding.enabled).to.be.true |
103 | expect(data.transcoding.threads).to.equal(1) | 104 | expect(data.transcoding.threads).to.equal(1) |
104 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | 105 | expect(data.transcoding.allowAdditionalExtensions).to.be.true |
106 | expect(data.transcoding.allowAudioFiles).to.be.true | ||
105 | expect(data.transcoding.resolutions['240p']).to.be.false | 107 | expect(data.transcoding.resolutions['240p']).to.be.false |
106 | expect(data.transcoding.resolutions['360p']).to.be.true | 108 | expect(data.transcoding.resolutions['360p']).to.be.true |
107 | expect(data.transcoding.resolutions['480p']).to.be.true | 109 | expect(data.transcoding.resolutions['480p']).to.be.true |
@@ -158,6 +160,9 @@ describe('Test config', function () { | |||
158 | expect(data.video.file.extensions).to.contain('.webm') | 160 | expect(data.video.file.extensions).to.contain('.webm') |
159 | expect(data.video.file.extensions).to.contain('.ogv') | 161 | expect(data.video.file.extensions).to.contain('.ogv') |
160 | 162 | ||
163 | await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, 400) | ||
164 | await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, 400) | ||
165 | |||
161 | expect(data.contactForm.enabled).to.be.true | 166 | expect(data.contactForm.enabled).to.be.true |
162 | }) | 167 | }) |
163 | 168 | ||
@@ -215,6 +220,7 @@ describe('Test config', function () { | |||
215 | transcoding: { | 220 | transcoding: { |
216 | enabled: true, | 221 | enabled: true, |
217 | allowAdditionalExtensions: true, | 222 | allowAdditionalExtensions: true, |
223 | allowAudioFiles: true, | ||
218 | threads: 1, | 224 | threads: 1, |
219 | resolutions: { | 225 | resolutions: { |
220 | '240p': false, | 226 | '240p': false, |
@@ -269,6 +275,12 @@ describe('Test config', function () { | |||
269 | expect(data.video.file.extensions).to.contain('.ogv') | 275 | expect(data.video.file.extensions).to.contain('.ogv') |
270 | expect(data.video.file.extensions).to.contain('.flv') | 276 | expect(data.video.file.extensions).to.contain('.flv') |
271 | expect(data.video.file.extensions).to.contain('.mkv') | 277 | expect(data.video.file.extensions).to.contain('.mkv') |
278 | expect(data.video.file.extensions).to.contain('.mp3') | ||
279 | expect(data.video.file.extensions).to.contain('.ogg') | ||
280 | expect(data.video.file.extensions).to.contain('.flac') | ||
281 | |||
282 | await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, 200) | ||
283 | await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, 200) | ||
272 | }) | 284 | }) |
273 | 285 | ||
274 | it('Should have the configuration updated after a restart', async function () { | 286 | it('Should have the configuration updated after a restart', async function () { |
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts index 634654626..3ab2fe120 100644 --- a/server/tests/api/server/jobs.ts +++ b/server/tests/api/server/jobs.ts | |||
@@ -26,7 +26,7 @@ describe('Test jobs', function () { | |||
26 | }) | 26 | }) |
27 | 27 | ||
28 | it('Should create some jobs', async function () { | 28 | it('Should create some jobs', async function () { |
29 | this.timeout(30000) | 29 | this.timeout(60000) |
30 | 30 | ||
31 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) | 31 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) |
32 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) | 32 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) |
diff --git a/server/tests/api/travis-2.sh b/server/tests/api/travis-2.sh index 82c1864b4..ba7a061b0 100644 --- a/server/tests/api/travis-2.sh +++ b/server/tests/api/travis-2.sh | |||
@@ -5,5 +5,5 @@ set -eu | |||
5 | serverFiles=$(find server/tests/api/server -type f | grep -v index.ts | xargs echo) | 5 | serverFiles=$(find server/tests/api/server -type f | grep -v index.ts | xargs echo) |
6 | usersFiles=$(find server/tests/api/users -type f | grep -v index.ts | xargs echo) | 6 | usersFiles=$(find server/tests/api/users -type f | grep -v index.ts | xargs echo) |
7 | 7 | ||
8 | MOCHA_PARALLEL=true mocha-parallel-tests --max-parallel $1 --timeout 5000 --exit --require ts-node/register --bail \ | 8 | MOCHA_PARALLEL=true mocha --timeout 5000 --exit --require ts-node/register --bail \ |
9 | $serverFiles $usersFiles | 9 | $serverFiles $usersFiles |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 09b461200..e9625e5f7 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -215,7 +215,7 @@ describe('Test multiple servers', function () { | |||
215 | files: [ | 215 | files: [ |
216 | { | 216 | { |
217 | resolution: 240, | 217 | resolution: 240, |
218 | size: 187000 | 218 | size: 189000 |
219 | }, | 219 | }, |
220 | { | 220 | { |
221 | resolution: 360, | 221 | resolution: 360, |
@@ -223,7 +223,7 @@ describe('Test multiple servers', function () { | |||
223 | }, | 223 | }, |
224 | { | 224 | { |
225 | resolution: 480, | 225 | resolution: 480, |
226 | size: 383000 | 226 | size: 384000 |
227 | }, | 227 | }, |
228 | { | 228 | { |
229 | resolution: 720, | 229 | resolution: 720, |
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts index 38e232e5f..17172331f 100644 --- a/server/tests/api/videos/services.ts +++ b/server/tests/api/videos/services.ts | |||
@@ -41,8 +41,8 @@ describe('Test services', function () { | |||
41 | expect(res.body.width).to.equal(560) | 41 | expect(res.body.width).to.equal(560) |
42 | expect(res.body.height).to.equal(315) | 42 | expect(res.body.height).to.equal(315) |
43 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) | 43 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) |
44 | expect(res.body.thumbnail_width).to.equal(560) | 44 | expect(res.body.thumbnail_width).to.equal(850) |
45 | expect(res.body.thumbnail_height).to.equal(315) | 45 | expect(res.body.thumbnail_height).to.equal(480) |
46 | }) | 46 | }) |
47 | 47 | ||
48 | it('Should have a valid oEmbed response with small max height query', async function () { | 48 | it('Should have a valid oEmbed response with small max height query', async function () { |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts index 504c50dee..39178bb1a 100644 --- a/server/tests/api/videos/video-hls.ts +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -21,12 +21,11 @@ import { | |||
21 | import { VideoDetails } from '../../../../shared/models/videos' | 21 | import { VideoDetails } from '../../../../shared/models/videos' |
22 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 22 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
23 | import { join } from 'path' | 23 | import { join } from 'path' |
24 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | ||
24 | 25 | ||
25 | const expect = chai.expect | 26 | const expect = chai.expect |
26 | 27 | ||
27 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | 28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resolutions = [ 240, 360, 480, 720 ]) { |
28 | const resolutions = [ 240, 360, 480, 720 ] | ||
29 | |||
30 | for (const server of servers) { | 29 | for (const server of servers) { |
31 | const res = await getVideo(server.url, videoUUID) | 30 | const res = await getVideo(server.url, videoUUID) |
32 | const videoDetails: VideoDetails = res.body | 31 | const videoDetails: VideoDetails = res.body |
@@ -41,9 +40,8 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | |||
41 | 40 | ||
42 | const masterPlaylist = res2.text | 41 | const masterPlaylist = res2.text |
43 | 42 | ||
44 | expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25') | ||
45 | |||
46 | for (const resolution of resolutions) { | 43 | for (const resolution of resolutions) { |
44 | expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+')) | ||
47 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | 45 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) |
48 | } | 46 | } |
49 | } | 47 | } |
@@ -70,11 +68,21 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | |||
70 | describe('Test HLS videos', function () { | 68 | describe('Test HLS videos', function () { |
71 | let servers: ServerInfo[] = [] | 69 | let servers: ServerInfo[] = [] |
72 | let videoUUID = '' | 70 | let videoUUID = '' |
71 | let videoAudioUUID = '' | ||
73 | 72 | ||
74 | before(async function () { | 73 | before(async function () { |
75 | this.timeout(120000) | 74 | this.timeout(120000) |
76 | 75 | ||
77 | servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } }) | 76 | const configOverride = { |
77 | transcoding: { | ||
78 | enabled: true, | ||
79 | allow_audio_files: true, | ||
80 | hls: { | ||
81 | enabled: true | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | servers = await flushAndRunMultipleServers(2, configOverride) | ||
78 | 86 | ||
79 | // Get the access tokens | 87 | // Get the access tokens |
80 | await setAccessTokensToServers(servers) | 88 | await setAccessTokensToServers(servers) |
@@ -86,17 +94,28 @@ describe('Test HLS videos', function () { | |||
86 | it('Should upload a video and transcode it to HLS', async function () { | 94 | it('Should upload a video and transcode it to HLS', async function () { |
87 | this.timeout(120000) | 95 | this.timeout(120000) |
88 | 96 | ||
89 | { | 97 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) |
90 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) | 98 | videoUUID = res.body.video.uuid |
91 | videoUUID = res.body.video.uuid | ||
92 | } | ||
93 | 99 | ||
94 | await waitJobs(servers) | 100 | await waitJobs(servers) |
95 | 101 | ||
96 | await checkHlsPlaylist(servers, videoUUID) | 102 | await checkHlsPlaylist(servers, videoUUID) |
97 | }) | 103 | }) |
98 | 104 | ||
105 | it('Should upload an audio file and transcode it to HLS', async function () { | ||
106 | this.timeout(120000) | ||
107 | |||
108 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' }) | ||
109 | videoAudioUUID = res.body.video.uuid | ||
110 | |||
111 | await waitJobs(servers) | ||
112 | |||
113 | await checkHlsPlaylist(servers, videoAudioUUID, [ DEFAULT_AUDIO_RESOLUTION ]) | ||
114 | }) | ||
115 | |||
99 | it('Should update the video', async function () { | 116 | it('Should update the video', async function () { |
117 | this.timeout(10000) | ||
118 | |||
100 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) | 119 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) |
101 | 120 | ||
102 | await waitJobs(servers) | 121 | await waitJobs(servers) |
@@ -104,13 +123,17 @@ describe('Test HLS videos', function () { | |||
104 | await checkHlsPlaylist(servers, videoUUID) | 123 | await checkHlsPlaylist(servers, videoUUID) |
105 | }) | 124 | }) |
106 | 125 | ||
107 | it('Should delete the video', async function () { | 126 | it('Should delete videos', async function () { |
127 | this.timeout(10000) | ||
128 | |||
108 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) | 129 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) |
130 | await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID) | ||
109 | 131 | ||
110 | await waitJobs(servers) | 132 | await waitJobs(servers) |
111 | 133 | ||
112 | for (const server of servers) { | 134 | for (const server of servers) { |
113 | await getVideo(server.url, videoUUID, 404) | 135 | await getVideo(server.url, videoUUID, 404) |
136 | await getVideo(server.url, videoAudioUUID, 404) | ||
114 | } | 137 | } |
115 | }) | 138 | }) |
116 | 139 | ||
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index cfd0c8430..90ade1652 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | getMyVideos, | 14 | getMyVideos, |
15 | getVideo, | 15 | getVideo, |
16 | getVideosList, | 16 | getVideosList, |
17 | makeGetRequest, | ||
17 | root, | 18 | root, |
18 | ServerInfo, | 19 | ServerInfo, |
19 | setAccessTokensToServers, | 20 | setAccessTokensToServers, |
@@ -365,6 +366,56 @@ describe('Test video transcoding', function () { | |||
365 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | 366 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false |
366 | }) | 367 | }) |
367 | 368 | ||
369 | it('Should merge an audio file with the preview file', async function () { | ||
370 | this.timeout(60000) | ||
371 | |||
372 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | ||
373 | await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg) | ||
374 | |||
375 | await waitJobs(servers) | ||
376 | |||
377 | for (const server of servers) { | ||
378 | const res = await getVideosList(server.url) | ||
379 | |||
380 | const video = res.body.data.find(v => v.name === 'audio_with_preview') | ||
381 | const res2 = await getVideo(server.url, video.id) | ||
382 | const videoDetails: VideoDetails = res2.body | ||
383 | |||
384 | expect(videoDetails.files).to.have.lengthOf(1) | ||
385 | |||
386 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 }) | ||
387 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 }) | ||
388 | |||
389 | const magnetUri = videoDetails.files[ 0 ].magnetUri | ||
390 | expect(magnetUri).to.contain('.mp4') | ||
391 | } | ||
392 | }) | ||
393 | |||
394 | it('Should upload an audio file and choose a default background image', async function () { | ||
395 | this.timeout(60000) | ||
396 | |||
397 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } | ||
398 | await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg) | ||
399 | |||
400 | await waitJobs(servers) | ||
401 | |||
402 | for (const server of servers) { | ||
403 | const res = await getVideosList(server.url) | ||
404 | |||
405 | const video = res.body.data.find(v => v.name === 'audio_without_preview') | ||
406 | const res2 = await getVideo(server.url, video.id) | ||
407 | const videoDetails = res2.body | ||
408 | |||
409 | expect(videoDetails.files).to.have.lengthOf(1) | ||
410 | |||
411 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 }) | ||
412 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 }) | ||
413 | |||
414 | const magnetUri = videoDetails.files[ 0 ].magnetUri | ||
415 | expect(magnetUri).to.contain('.mp4') | ||
416 | } | ||
417 | }) | ||
418 | |||
368 | after(async function () { | 419 | after(async function () { |
369 | await cleanupTests(servers) | 420 | await cleanupTests(servers) |
370 | }) | 421 | }) |
diff --git a/server/tests/fixtures/preview.jpg b/server/tests/fixtures/preview.jpg index c40ece838..cb5692281 100644 --- a/server/tests/fixtures/preview.jpg +++ b/server/tests/fixtures/preview.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/sample.ogg b/server/tests/fixtures/sample.ogg new file mode 100644 index 000000000..0d7f43eb7 --- /dev/null +++ b/server/tests/fixtures/sample.ogg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1-preview.webm.jpg b/server/tests/fixtures/video_short1-preview.webm.jpg index d2a068b78..157d3ca9a 100644 --- a/server/tests/fixtures/video_short1-preview.webm.jpg +++ b/server/tests/fixtures/video_short1-preview.webm.jpg | |||
Binary files differ | |||