diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 637484622..44fc6dc26 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -286,6 +286,14 @@
>
+
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index e64750713..c238a6c81 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -116,6 +116,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
enabled: null,
threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
allowAdditionalExtensions: null,
+ allowAudioFiles: null,
resolutions: {}
},
autoBlacklist: {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index b147b75b0..d8ba4df89 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -561,8 +561,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private flushPlayer () {
// Remove player if it exists
if (this.player) {
- this.player.dispose()
- this.player = undefined
+ try {
+ this.player.dispose()
+ this.player = undefined
+ } catch (err) {
+ console.error('Cannot dispose player.', err)
+ }
}
}
}
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 6cdd54372..31cbc7dfd 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -117,8 +117,17 @@ export class PeertubePlayerManager {
videojs(options.common.playerElement, videojsOptions, function (this: any) {
const player = this
- player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options))
- player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options))
+ let alreadyFallback = false
+
+ player.tech_.one('error', () => {
+ if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
+ alreadyFallback = true
+ })
+
+ player.one('error', () => {
+ if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
+ alreadyFallback = true
+ })
self.addContextMenu(mode, player, options.common.embedUrl)
diff --git a/config/default.yaml b/config/default.yaml
index 37ef4366f..9c9fd93dd 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -174,6 +174,8 @@ transcoding:
enabled: true
# Allow your users to upload .mkv, .mov, .avi, .flv videos
allow_additional_extensions: true
+ # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
+ allow_audio_files: true
threads: 1
resolutions: # Only created if the original video has a higher resolution, uses more storage!
240p: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index f84e15670..0ab99ac45 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -188,6 +188,8 @@ transcoding:
enabled: true
# Allow your users to upload .mkv, .mov, .avi, .flv videos
allow_additional_extensions: true
+ # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
+ allow_audio_files: true
threads: 1
resolutions: # Only created if the original video has a higher resolution, uses more storage!
240p: false
diff --git a/config/test.yaml b/config/test.yaml
index 682530840..7dabe433c 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -55,6 +55,7 @@ signup:
transcoding:
enabled: true
allow_additional_extensions: false
+ allow_audio_files: false
threads: 2
resolutions:
240p: true
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts
index 4a677eacb..2b7cb5177 100755
--- a/scripts/create-transcoding-job.ts
+++ b/scripts/create-transcoding-job.ts
@@ -2,6 +2,7 @@ import * as program from 'commander'
import { VideoModel } from '../server/models/video/video'
import { initDatabaseModels } from '../server/initializers'
import { JobQueue } from '../server/lib/job-queue'
+import { VideoTranscodingPayload } from '../server/lib/job-queue/handlers/video-transcoding'
program
.option('-v, --video [videoUUID]', 'Video UUID')
@@ -31,15 +32,9 @@ async function run () {
const video = await VideoModel.loadByUUIDWithFile(program['video'])
if (!video) throw new Error('Video not found.')
- const dataInput = {
- videoUUID: video.uuid,
- isNewVideo: false,
- resolution: undefined
- }
-
- if (program.resolution !== undefined) {
- dataInput.resolution = program.resolution
- }
+ const dataInput: VideoTranscodingPayload = program.resolution !== undefined
+ ? { type: 'new-resolution' as 'new-resolution', videoUUID: video.uuid, isNewVideo: false, resolution: program.resolution }
+ : { type: 'optimize' as 'optimize', videoUUID: video.uuid, isNewVideo: false }
await JobQueue.Instance.init()
await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg
new file mode 100644
index 000000000..a19173eac
Binary files /dev/null and b/server/assets/default-audio-background.jpg 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 {
transcoding: {
enabled: CONFIG.TRANSCODING.ENABLED,
allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
+ allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
threads: CONFIG.TRANSCODING.THREADS,
resolutions: {
'240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 1a18a8ae8..a2a615a79 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -1,12 +1,12 @@
import * as express from 'express'
import { extname, join } from 'path'
-import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
+import { VideoCreate, VideoPrivacy, VideoResolution, VideoState, VideoUpdate } from '../../../../shared'
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
-import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
+import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
import {
changeVideoChannelShare,
federateVideoIfNeeded,
@@ -54,6 +54,7 @@ import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database'
import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
+import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
@@ -191,18 +192,19 @@ async function addVideo (req: express.Request, res: express.Response) {
const video = new VideoModel(videoData)
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
- // Build the file object
- const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
- const fps = await getVideoFileFPS(videoPhysicalFile.path)
-
const videoFileData = {
extname: extname(videoPhysicalFile.filename),
- resolution: videoFileResolution,
- size: videoPhysicalFile.size,
- fps
+ size: videoPhysicalFile.size
}
const videoFile = new VideoFileModel(videoFileData)
+ if (!videoFile.isAudio()) {
+ videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
+ videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
+ } else {
+ videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
+ }
+
// Move physical file
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
const destination = join(videoDir, video.getVideoFilename(videoFile))
@@ -279,9 +281,21 @@ async function addVideo (req: express.Request, res: express.Response) {
if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now
- const dataInput = {
- videoUUID: videoCreated.uuid,
- isNewVideo: true
+ let dataInput: VideoTranscodingPayload
+
+ if (videoFile.isAudio()) {
+ dataInput = {
+ type: 'merge-audio' as 'merge-audio',
+ resolution: DEFAULT_AUDIO_RESOLUTION,
+ videoUUID: videoCreated.uuid,
+ isNewVideo: true
+ }
+ } else {
+ dataInput = {
+ type: 'optimize' as 'optimize',
+ videoUUID: videoCreated.uuid,
+ isNewVideo: true
+ }
}
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) {
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
}
-async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) {
+async function generateNodeinfo (req: express.Request, res: express.Response) {
const { totalVideos } = await VideoModel.getStats()
const { totalLocalVideoComments } = await VideoCommentModel.getStats()
const { totalUsers } = await UserModel.getStats()
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
}
}
-type TranscodeOptions = {
+type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio'
+
+interface BaseTranscodeOptions {
+ type: TranscodeOptionsType
inputPath: string
outputPath: string
resolution: VideoResolution
isPortraitMode?: boolean
- doQuickTranscode?: Boolean
+}
- hlsPlaylist?: {
+interface HLSTranscodeOptions extends BaseTranscodeOptions {
+ type: 'hls'
+ hlsPlaylist: {
videoFilename: string
}
}
+interface QuickTranscodeOptions extends BaseTranscodeOptions {
+ type: 'quick-transcode'
+}
+
+interface VideoTranscodeOptions extends BaseTranscodeOptions {
+ type: 'video'
+}
+
+interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
+ type: 'merge-audio'
+ audioPath: string
+}
+
+type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions
+
function transcode (options: TranscodeOptions) {
return new Promise(async (res, rej) => {
try {
let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
.output(options.outputPath)
- if (options.doQuickTranscode) {
- if (options.hlsPlaylist) {
- throw(Error("Quick transcode and HLS can't be used at the same time"))
- }
-
- command
- .format('mp4')
- .addOption('-c:v copy')
- .addOption('-c:a copy')
- .outputOption('-map_metadata -1') // strip all metadata
- .outputOption('-movflags faststart')
- } else if (options.hlsPlaylist) {
+ if (options.type === 'quick-transcode') {
+ command = await buildQuickTranscodeCommand(command)
+ } else if (options.type === 'hls') {
command = await buildHLSCommand(command, options)
+ } else if (options.type === 'merge-audio') {
+ command = await buildAudioMergeCommand(command, options)
} else {
command = await buildx264Command(command, options)
}
@@ -163,7 +176,7 @@ function transcode (options: TranscodeOptions) {
return rej(err)
})
.on('end', () => {
- return onTranscodingSuccess(options)
+ return fixHLSPlaylistIfNeeded(options)
.then(() => res())
.catch(err => rej(err))
})
@@ -205,6 +218,8 @@ export {
getVideoFileResolution,
getDurationFromVideoFile,
generateImageFromVideoFile,
+ TranscodeOptions,
+ TranscodeOptionsType,
transcode,
getVideoFileFPS,
computeResolutionsToTranscode,
@@ -215,7 +230,7 @@ export {
// ---------------------------------------------------------------------------
-async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
+async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
let fps = await getVideoFileFPS(options.inputPath)
// On small/medium resolutions, limit FPS
if (
@@ -226,7 +241,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
fps = VIDEO_TRANSCODING_FPS.AVERAGE
}
- command = await presetH264(command, options.resolution, fps)
+ command = await presetH264(command, options.inputPath, options.resolution, fps)
if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
@@ -245,7 +260,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
return command
}
-async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
+async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
+ command = command.loop(undefined)
+
+ command = await presetH264VeryFast(command, options.audioPath, options.resolution)
+
+ command = command.input(options.audioPath)
+ .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
+ .outputOption('-tune stillimage')
+ .outputOption('-shortest')
+
+ return command
+}
+
+async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
+ command = await presetCopy(command)
+
+ command = command.outputOption('-map_metadata -1') // strip all metadata
+ .outputOption('-movflags faststart')
+
+ return command
+}
+
+async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
const videoPath = getHLSVideoPath(options)
command = await presetCopy(command)
@@ -261,19 +298,19 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod
return command
}
-function getHLSVideoPath (options: TranscodeOptions) {
+function getHLSVideoPath (options: HLSTranscodeOptions) {
return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
}
-async function onTranscodingSuccess (options: TranscodeOptions) {
- if (!options.hlsPlaylist) return
+async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
+ if (options.type !== 'hls') return
- // Fix wrong mapping with some ffmpeg versions
const fileContent = await readFile(options.outputPath)
const videoFileName = options.hlsPlaylist.videoFilename
const videoFilePath = getHLSVideoPath(options)
+ // Fix wrong mapping with some ffmpeg versions
const newContent = fileContent.toString()
.replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
@@ -300,44 +337,27 @@ function getVideoStreamFromFile (path: string) {
* and quality. Superfast and ultrafast will give you better
* performance, but then quality is noticeably worse.
*/
-async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise {
- let localCommand = await presetH264(command, resolution, fps)
+async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
+ let localCommand = await presetH264(command, input, resolution, fps)
+
localCommand = localCommand.outputOption('-preset:v veryfast')
- .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
+
/*
MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
Our target situation is closer to a livestream than a stream,
since we want to reduce as much a possible the encoding burden,
- altough not to the point of a livestream where there is a hard
+ although not to the point of a livestream where there is a hard
constraint on the frames per second to be encoded.
-
- why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'?
- Make up for most of the loss of grain and macroblocking
- with less computing power.
*/
return localCommand
}
-/**
- * A preset optimised for a stillimage audio video
- */
-async function presetStillImageWithAudio (
- command: ffmpeg.FfmpegCommand,
- resolution: VideoResolution,
- fps: number
-): Promise {
- let localCommand = await presetH264VeryFast(command, resolution, fps)
- localCommand = localCommand.outputOption('-tune stillimage')
-
- return localCommand
-}
-
/**
* A toolbox to play with audio
*/
namespace audio {
- export const get = (option: ffmpeg.FfmpegCommand | string) => {
+ export const get = (option: string) => {
// without position, ffprobe considers the last input only
// we make it consider the first input only
// if you pass a file path to pos, then ffprobe acts on that file directly
@@ -359,11 +379,7 @@ namespace audio {
return res({ absolutePath: data.format.filename })
}
- if (typeof option === 'string') {
- return ffmpeg.ffprobe(option, parseFfprobe)
- }
-
- return option.ffprobe(parseFfprobe)
+ return ffmpeg.ffprobe(option, parseFfprobe)
})
}
@@ -405,7 +421,7 @@ namespace audio {
* As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
* See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
*/
-async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise {
+async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
let localCommand = command
.format('mp4')
.videoCodec('libx264')
@@ -416,7 +432,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
.outputOption('-map_metadata -1') // strip all metadata
.outputOption('-movflags faststart')
- const parsedAudio = await audio.get(localCommand)
+ const parsedAudio = await audio.get(input)
if (!parsedAudio.audioStream) {
localCommand = localCommand.noAudio()
@@ -425,28 +441,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
.audioCodec('libfdk_aac')
.audioQuality(5)
} else {
- // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
+ // we try to reduce the ceiling bitrate by making rough matches of bitrates
// of course this is far from perfect, but it might save some space in the end
- const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
- let bitrate: number
- if (audio.bitrate[ audioCodecName ]) {
- localCommand = localCommand.audioCodec('aac')
+ localCommand = localCommand.audioCodec('aac')
- bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
+ const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
+
+ if (audio.bitrate[ audioCodecName ]) {
+ const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
}
}
- // Constrained Encoding (VBV)
- // https://slhck.info/video/2017/03/01/rate-control.html
- // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
- const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
- localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
+ if (fps) {
+ // Constrained Encoding (VBV)
+ // https://slhck.info/video/2017/03/01/rate-control.html
+ // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
+ const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
+ localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
- // Keyframe interval of 2 seconds for faster seeking and resolution switching.
- // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
- // https://superuser.com/a/908325
- localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
+ // Keyframe interval of 2 seconds for faster seeking and resolution switching.
+ // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
+ // https://superuser.com/a/908325
+ localCommand = localCommand.outputOption(`-g ${fps * 2}`)
+ }
return localCommand
}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 4f77e144d..4515bc804 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -140,6 +140,7 @@ const CONFIG = {
TRANSCODING: {
get ENABLED () { return config.get('transcoding.enabled') },
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get('transcoding.allow_additional_extensions') },
+ get ALLOW_AUDIO_FILES () { return config.get('transcoding.allow_audio_files') },
get THREADS () { return config.get('transcoding.threads') },
RESOLUTIONS: {
get '240p' () { return config.get('transcoding.resolutions.240p') },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 62778ae58..718d0893b 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,10 +1,10 @@
import { join } from 'path'
-import { JobType, VideoRateType, VideoState } from '../../shared/models'
+import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors'
import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible
-import { isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
+import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
import { invert } from 'lodash'
import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
@@ -228,7 +228,7 @@ let CONSTRAINTS_FIELDS = {
max: 2 * 1024 * 1024 // 2MB
}
},
- EXTNAME: buildVideosExtname(),
+ EXTNAME: [] as string[],
INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
DURATION: { min: 0 }, // Number
TAGS: { min: 0, max: 5 }, // Number of total tags
@@ -300,6 +300,8 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
}
+const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
+
const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
LIKE: 'like',
DISLIKE: 'dislike'
@@ -380,8 +382,18 @@ const VIDEO_PLAYLIST_TYPES = {
}
const MIMETYPES = {
+ AUDIO: {
+ MIMETYPE_EXT: {
+ 'audio/mpeg': '.mp3',
+ 'audio/mp3': '.mp3',
+ 'application/ogg': '.ogg',
+ 'audio/ogg': '.ogg',
+ 'audio/flac': '.flac'
+ },
+ EXT_MIMETYPE: null as { [ id: string ]: string }
+ },
VIDEO: {
- MIMETYPE_EXT: buildVideoMimetypeExt(),
+ MIMETYPE_EXT: null as { [ id: string ]: string },
EXT_MIMETYPE: null as { [ id: string ]: string }
},
IMAGE: {
@@ -403,7 +415,7 @@ const MIMETYPES = {
}
}
}
-MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
+MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
// ---------------------------------------------------------------------------
@@ -429,7 +441,7 @@ const ACTIVITY_PUB = {
COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 100,
URL_MIME_TYPES: {
- VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT),
+ VIDEO: [] as string[],
TORRENT: [ 'application/x-bittorrent' ],
MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
},
@@ -543,6 +555,10 @@ const REDUNDANCY = {
const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
+const ASSETS_PATH = {
+ DEFAULT_AUDIO_BACKGROUND: join(root(), 'server', 'assets', 'default-audio-background.jpg')
+}
+
// ---------------------------------------------------------------------------
const CUSTOM_HTML_TAG_COMMENTS = {
@@ -612,6 +628,7 @@ if (isTestInstance() === true) {
}
updateWebserverUrls()
+updateWebserverConfig()
registerConfigChangedHandler(() => {
updateWebserverUrls()
@@ -681,12 +698,14 @@ export {
RATES_LIMIT,
MIMETYPES,
CRAWL_REQUEST_CONCURRENCY,
+ DEFAULT_AUDIO_RESOLUTION,
JOB_COMPLETED_LIFETIME,
HTTP_SIGNATURE,
VIDEO_IMPORT_STATES,
VIDEO_VIEW_LIFETIME,
CONTACT_FORM_LIFETIME,
VIDEO_PLAYLIST_PRIVACIES,
+ ASSETS_PATH,
loadLanguages,
buildLanguages
}
@@ -700,15 +719,21 @@ function buildVideoMimetypeExt () {
'video/mp4': '.mp4'
}
- if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
- Object.assign(data, {
- 'video/quicktime': '.mov',
- 'video/x-msvideo': '.avi',
- 'video/x-flv': '.flv',
- 'video/x-matroska': '.mkv',
- 'application/octet-stream': '.mkv',
- 'video/avi': '.avi'
- })
+ if (CONFIG.TRANSCODING.ENABLED) {
+ if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
+ Object.assign(data, {
+ 'video/quicktime': '.mov',
+ 'video/x-msvideo': '.avi',
+ 'video/x-flv': '.flv',
+ 'video/x-matroska': '.mkv',
+ 'application/octet-stream': '.mkv',
+ 'video/avi': '.avi'
+ })
+ }
+
+ if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) {
+ Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT)
+ }
}
return data
@@ -724,16 +749,15 @@ function updateWebserverUrls () {
}
function updateWebserverConfig () {
- CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
-
MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
+ ACTIVITY_PUB.URL_MIME_TYPES.VIDEO = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
+
+ CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
}
function buildVideosExtname () {
- return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS
- ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ]
- : [ '.mp4', '.ogv', '.webm' ]
+ return Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE)
}
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 {
const video = await VideoModel.loadByUUIDWithFile(videoUUID)
if (!video) return undefined
- if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) }
+ if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() }
return this.loadRemoteFile(videoUUID)
}
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 @@
import * as Bull from 'bull'
import { logger } from '../../../helpers/logger'
import { VideoModel } from '../../../models/video/video'
-import { publishVideoIfNeeded } from './video-transcoding'
+import { publishNewResolutionIfNeeded } from './video-transcoding'
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { copy, stat } from 'fs-extra'
import { VideoFileModel } from '../../../models/video/video-file'
@@ -25,7 +25,7 @@ async function processVideoFileImport (job: Bull.Job) {
await updateVideoFile(video, payload.filePath)
- await publishVideoIfNeeded(video)
+ await publishNewResolutionIfNeeded(video)
return video
}
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, videoImport: Vide
if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now
const dataInput = {
+ type: 'optimize' as 'optimize',
videoUUID: videoImportUpdated.Video.uuid,
isNewVideo: true
}
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'
import { sequelizeTypescript } from '../../../initializers'
import * as Bluebird from 'bluebird'
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
-import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding'
+import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
import { Notifier } from '../../notifier'
import { CONFIG } from '../../../initializers/config'
-export type VideoTranscodingPayload = {
+interface BaseTranscodingPayload {
videoUUID: string
- resolution?: VideoResolution
isNewVideo?: boolean
- isPortraitMode?: boolean
- generateHlsPlaylist?: boolean
}
+interface HLSTranscodingPayload extends BaseTranscodingPayload {
+ type: 'hls'
+ isPortraitMode?: boolean
+ resolution: VideoResolution
+}
+
+interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
+ type: 'new-resolution'
+ isPortraitMode?: boolean
+ resolution: VideoResolution
+}
+
+interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {
+ type: 'merge-audio'
+ resolution: VideoResolution
+}
+
+interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
+ type: 'optimize'
+}
+
+export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload
+ | OptimizeTranscodingPayload | MergeAudioTranscodingPayload
+
async function processVideoTranscoding (job: Bull.Job) {
const payload = job.data as VideoTranscodingPayload
logger.info('Processing video file in job %d.', job.id)
@@ -31,14 +52,18 @@ async function processVideoTranscoding (job: Bull.Job) {
return undefined
}
- if (payload.generateHlsPlaylist) {
+ if (payload.type === 'hls') {
await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false)
await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
- } else if (payload.resolution) { // Transcoding in other resolution
+ } else if (payload.type === 'new-resolution') {
await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
- await retryTransactionWrapper(publishVideoIfNeeded, video, payload)
+ await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
+ } else if (payload.type === 'merge-audio') {
+ await mergeAudioVideofile(video, payload.resolution)
+
+ await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
} else {
await optimizeVideofile(video)
@@ -62,7 +87,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
})
}
-async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodingPayload) {
+async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
// Maybe the video changed in database, refresh it
let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -94,7 +119,7 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi
await createHlsJobIfEnabled(payload)
}
-async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoTranscodingPayload) {
+async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) {
if (videoArg === undefined) return undefined
// Outside the transaction (IO on disk)
@@ -120,6 +145,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
for (const resolution of resolutionsEnabled) {
const dataInput = {
+ type: 'new-resolution' as 'new-resolution',
videoUUID: videoDatabase.uuid,
resolution
}
@@ -149,27 +175,27 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
- await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
+ const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })
+ await createHlsJobIfEnabled(hlsPayload)
}
// ---------------------------------------------------------------------------
export {
processVideoTranscoding,
- publishVideoIfNeeded
+ publishNewResolutionIfNeeded
}
// ---------------------------------------------------------------------------
-function createHlsJobIfEnabled (payload?: VideoTranscodingPayload) {
+function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: number, isPortraitMode?: boolean }) {
// Generate HLS playlist?
if (payload && CONFIG.TRANSCODING.HLS.ENABLED) {
const hlsTranscodingPayload = {
+ type: 'hls' as 'hls',
videoUUID: payload.videoUUID,
resolution: payload.resolution,
- isPortraitMode: payload.isPortraitMode,
-
- generateHlsPlaylist: true
+ isPortraitMode: payload.isPortraitMode
}
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 @@
import { VideoFileModel } from '../models/video/video-file'
import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
import { CONFIG } from '../initializers/config'
-import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
+import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants'
import { VideoModel } from '../models/video/video'
import { ThumbnailModel } from '../models/video/thumbnail'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
@@ -45,8 +45,10 @@ function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel,
function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
const input = video.getVideoFilePath(videoFile)
- const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type)
- const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width })
+ const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
+ const thumbnailCreator = videoFile.isAudio()
+ ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
+ : () => generateImageFromVideoFile(input, basePath, filename, { height, width })
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
}
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 @@
import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
import { join } from 'path'
-import { getVideoFileFPS, transcode, canDoQuickTranscode } from '../helpers/ffmpeg-utils'
+import { canDoQuickTranscode, getVideoFileFPS, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
import { ensureDir, move, remove, stat } from 'fs-extra'
import { logger } from '../helpers/logger'
import { VideoResolution } from '../../shared/models/videos'
@@ -23,13 +23,15 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
- const doQuickTranscode = await(canDoQuickTranscode(videoInputPath))
+ const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
+ ? 'quick-transcode'
+ : 'video'
- const transcodeOptions = {
+ const transcodeOptions: TranscodeOptions = {
+ type: transcodeType as any, // FIXME: typing issue
inputPath: videoInputPath,
outputPath: videoTranscodedPath,
- resolution: inputVideoFile.resolution,
- doQuickTranscode
+ resolution: inputVideoFile.resolution
}
// Could be very long!
@@ -39,19 +41,11 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
await remove(videoInputPath)
// Important to do this before getVideoFilename() to take in account the new file extension
- inputVideoFile.set('extname', newExtname)
-
- const stats = await stat(videoTranscodedPath)
- const fps = await getVideoFileFPS(videoTranscodedPath)
+ inputVideoFile.extname = newExtname
const videoOutputPath = video.getVideoFilePath(inputVideoFile)
- await move(videoTranscodedPath, videoOutputPath)
- inputVideoFile.set('size', stats.size)
- inputVideoFile.set('fps', fps)
-
- await video.createTorrentAndSetInfoHash(inputVideoFile)
- await inputVideoFile.save()
+ await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
} catch (err) {
// Auto destruction...
video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
@@ -81,6 +75,7 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
const transcodeOptions = {
+ type: 'video' as 'video',
inputPath: videoInputPath,
outputPath: videoTranscodedPath,
resolution,
@@ -89,19 +84,37 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
await transcode(transcodeOptions)
- const stats = await stat(videoTranscodedPath)
- const fps = await getVideoFileFPS(videoTranscodedPath)
+ return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
+}
- await move(videoTranscodedPath, videoOutputPath)
+async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) {
+ const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
+ const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
+ const newExtname = '.mp4'
- newVideoFile.set('size', stats.size)
- newVideoFile.set('fps', fps)
+ const inputVideoFile = video.getOriginalFile()
- await video.createTorrentAndSetInfoHash(newVideoFile)
+ const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
+ const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
- await newVideoFile.save()
+ const transcodeOptions = {
+ type: 'merge-audio' as 'merge-audio',
+ inputPath: video.getPreview().getPath(),
+ outputPath: videoTranscodedPath,
+ audioPath: audioInputPath,
+ resolution
+ }
- video.VideoFiles.push(newVideoFile)
+ await transcode(transcodeOptions)
+
+ await remove(audioInputPath)
+
+ // Important to do this before getVideoFilename() to take in account the new file extension
+ inputVideoFile.extname = newExtname
+
+ const videoOutputPath = video.getVideoFilePath(inputVideoFile)
+
+ return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
}
async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
@@ -112,6 +125,7 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
const transcodeOptions = {
+ type: 'hls' as 'hls',
inputPath: videoInputPath,
outputPath,
resolution,
@@ -140,8 +154,34 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
})
}
+// ---------------------------------------------------------------------------
+
export {
generateHlsPlaylist,
optimizeVideofile,
- transcodeOriginalVideofile
+ transcodeOriginalVideofile,
+ mergeAudioVideofile
+}
+
+// ---------------------------------------------------------------------------
+
+async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) {
+ const stats = await stat(transcodingPath)
+ const fps = await getVideoFileFPS(transcodingPath)
+
+ await move(transcodingPath, outputPath)
+
+ videoFile.set('size', stats.size)
+ videoFile.set('fps', fps)
+
+ await video.createTorrentAndSetInfoHash(videoFile)
+
+ const updatedVideoFile = await videoFile.save()
+
+ // Add it if this is a new created file
+ if (video.VideoFiles.some(f => f.id === videoFile.id) === false) {
+ video.VideoFiles.push(updatedVideoFile)
+ }
+
+ return video
}
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 {
return WEBSERVER.URL + staticPath + this.filename
}
- removeThumbnail () {
+ getPath () {
const directory = ThumbnailModel.types[this.type].directory
- const thumbnailPath = join(directory, this.filename)
+ return join(directory, this.filename)
+ }
- return remove(thumbnailPath)
+ removeThumbnail () {
+ return remove(this.getPath())
}
}
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'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { FindOptions, QueryTypes, Transaction } from 'sequelize'
+import { MIMETYPES } from '../../initializers/constants'
@Table({
tableName: 'videoFile',
@@ -161,6 +162,10 @@ export class VideoFileModel extends Model {
}))
}
+ isAudio () {
+ return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
+ }
+
hasSameUniqueKeysThan (other: VideoFileModel) {
return this.fps === other.fps &&
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 () {
transcoding: {
enabled: true,
allowAdditionalExtensions: true,
+ allowAudioFiles: true,
threads: 1,
resolutions: {
'240p': false,
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index ca389b7b6..2ad477c99 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -52,6 +52,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
expect(data.user.videoQuotaDaily).to.equal(-1)
expect(data.transcoding.enabled).to.be.false
expect(data.transcoding.allowAdditionalExtensions).to.be.false
+ expect(data.transcoding.allowAudioFiles).to.be.false
expect(data.transcoding.threads).to.equal(2)
expect(data.transcoding.resolutions['240p']).to.be.true
expect(data.transcoding.resolutions['360p']).to.be.true
@@ -102,6 +103,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.transcoding.enabled).to.be.true
expect(data.transcoding.threads).to.equal(1)
expect(data.transcoding.allowAdditionalExtensions).to.be.true
+ expect(data.transcoding.allowAudioFiles).to.be.true
expect(data.transcoding.resolutions['240p']).to.be.false
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
@@ -215,6 +217,7 @@ describe('Test config', function () {
transcoding: {
enabled: true,
allowAdditionalExtensions: true,
+ allowAudioFiles: true,
threads: 1,
resolutions: {
'240p': false,
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index deb77e9c0..a5f5989e0 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -91,6 +91,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
transcoding: {
enabled: true,
allowAdditionalExtensions: true,
+ allowAudioFiles: true,
threads: 1,
resolutions: {
'240p': false,
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index ca52eff4b..4cc379b2a 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -54,6 +54,7 @@ export interface CustomConfig {
transcoding: {
enabled: boolean
allowAdditionalExtensions: boolean
+ allowAudioFiles: boolean
threads: number
resolutions: {
'240p': boolean