diff options
author | frankdelange <yetangitu-f@unternet.org> | 2019-11-01 02:06:19 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-11-25 10:59:47 +0100 |
commit | 5c7d650827cc471a03e7fa18362bcbcbe5d30838 (patch) | |
tree | 41c96e3c9e2dcd4f15166e4f13b427ef116ea4f1 /server | |
parent | dee6fe1e4f5c024fd387e8c2b306c174b24aa8b3 (diff) | |
download | PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.tar.gz PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.tar.zst PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.zip |
Add audio-only option to transcoders and player
This patch adds an audio-only option to PeerTube by means of a new transcoding configuration which creates mp4 files which only contain an audio stream. This new transcoder has a resolution of '0' and is presented in the preferences and in the player resolution menu as 'Audio-only' (localised). When playing such streams the player shows the file thumbnail as background and disables controls autohide.
Audio-only files can be shared and streamed just like any other file. They can be downloaded as well, the resulting file will be an mp4 container with a single audio stream.
This patch is a proof of concept to show the feasibility of 'true' audio-only support. There are better ways of doing this which also enable multiple audio streams for a given video stream (e.g. DASH) but as this would entail a fundamental change in the way PeerTube works it is a bridge too far for a simple proof of concept.
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/config.ts | 2 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 45 | ||||
-rw-r--r-- | server/initializers/config.ts | 1 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 46 | ||||
-rw-r--r-- | server/middlewares/validators/config.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/config.ts | 1 | ||||
-rw-r--r-- | server/tests/api/server/config.ts | 1 |
7 files changed, 87 insertions, 10 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 70e8aa970..8a00f9835 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -300,6 +300,7 @@ function customConfig (): CustomConfig { | |||
300 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, | 300 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, |
301 | threads: CONFIG.TRANSCODING.THREADS, | 301 | threads: CONFIG.TRANSCODING.THREADS, |
302 | resolutions: { | 302 | resolutions: { |
303 | '0p': CONFIG.TRANSCODING.RESOLUTIONS[ '0p' ], | ||
303 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 304 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
304 | '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], | 305 | '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], |
305 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], | 306 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], |
@@ -356,6 +357,7 @@ function convertCustomConfigBody (body: CustomConfig) { | |||
356 | function keyConverter (k: string) { | 357 | function keyConverter (k: string) { |
357 | // Transcoding resolutions exception | 358 | // Transcoding resolutions exception |
358 | if (/^\d{3,4}p$/.exec(k)) return k | 359 | if (/^\d{3,4}p$/.exec(k)) return k |
360 | if (/^0p$/.exec(k)) return k | ||
359 | 361 | ||
360 | return snakeCase(k) | 362 | return snakeCase(k) |
361 | } | 363 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 7a4ac0970..2d9ce2bfa 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -14,6 +14,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
14 | 14 | ||
15 | // Put in the order we want to proceed jobs | 15 | // Put in the order we want to proceed jobs |
16 | const resolutions = [ | 16 | const resolutions = [ |
17 | VideoResolution.H_NOVIDEO, | ||
17 | VideoResolution.H_480P, | 18 | VideoResolution.H_480P, |
18 | VideoResolution.H_360P, | 19 | VideoResolution.H_360P, |
19 | VideoResolution.H_720P, | 20 | VideoResolution.H_720P, |
@@ -34,10 +35,15 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
34 | async function getVideoFileSize (path: string) { | 35 | async function getVideoFileSize (path: string) { |
35 | const videoStream = await getVideoStreamFromFile(path) | 36 | const videoStream = await getVideoStreamFromFile(path) |
36 | 37 | ||
37 | return { | 38 | return videoStream == null |
38 | width: videoStream.width, | 39 | ? { |
39 | height: videoStream.height | 40 | width: 0, |
40 | } | 41 | height: 0 |
42 | } | ||
43 | : { | ||
44 | width: videoStream.width, | ||
45 | height: videoStream.height | ||
46 | } | ||
41 | } | 47 | } |
42 | 48 | ||
43 | async function getVideoFileResolution (path: string) { | 49 | async function getVideoFileResolution (path: string) { |
@@ -52,6 +58,10 @@ async function getVideoFileResolution (path: string) { | |||
52 | async function getVideoFileFPS (path: string) { | 58 | async function getVideoFileFPS (path: string) { |
53 | const videoStream = await getVideoStreamFromFile(path) | 59 | const videoStream = await getVideoStreamFromFile(path) |
54 | 60 | ||
61 | if (videoStream == null) { | ||
62 | return 0 | ||
63 | } | ||
64 | |||
55 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | 65 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
56 | const valuesText: string = videoStream[key] | 66 | const valuesText: string = videoStream[key] |
57 | if (!valuesText) continue | 67 | if (!valuesText) continue |
@@ -118,7 +128,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
118 | } | 128 | } |
119 | } | 129 | } |
120 | 130 | ||
121 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 131 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio' |
122 | 132 | ||
123 | interface BaseTranscodeOptions { | 133 | interface BaseTranscodeOptions { |
124 | type: TranscodeOptionsType | 134 | type: TranscodeOptionsType |
@@ -149,7 +159,11 @@ interface MergeAudioTranscodeOptions extends BaseTranscodeOptions { | |||
149 | audioPath: string | 159 | audioPath: string |
150 | } | 160 | } |
151 | 161 | ||
152 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions | 162 | interface SplitAudioTranscodeOptions extends BaseTranscodeOptions { |
163 | type: 'split-audio' | ||
164 | } | ||
165 | |||
166 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions | ||
153 | 167 | ||
154 | function transcode (options: TranscodeOptions) { | 168 | function transcode (options: TranscodeOptions) { |
155 | return new Promise<void>(async (res, rej) => { | 169 | return new Promise<void>(async (res, rej) => { |
@@ -163,6 +177,8 @@ function transcode (options: TranscodeOptions) { | |||
163 | command = await buildHLSCommand(command, options) | 177 | command = await buildHLSCommand(command, options) |
164 | } else if (options.type === 'merge-audio') { | 178 | } else if (options.type === 'merge-audio') { |
165 | command = await buildAudioMergeCommand(command, options) | 179 | command = await buildAudioMergeCommand(command, options) |
180 | } else if (options.type === 'split-audio') { | ||
181 | command = await buildAudioSplitCommand(command, options) | ||
166 | } else { | 182 | } else { |
167 | command = await buildx264Command(command, options) | 183 | command = await buildx264Command(command, options) |
168 | } | 184 | } |
@@ -198,6 +214,7 @@ async function canDoQuickTranscode (path: string): Promise<boolean> { | |||
198 | const resolution = await getVideoFileResolution(path) | 214 | const resolution = await getVideoFileResolution(path) |
199 | 215 | ||
200 | // check video params | 216 | // check video params |
217 | if (videoStream == null) return false | ||
201 | if (videoStream[ 'codec_name' ] !== 'h264') return false | 218 | if (videoStream[ 'codec_name' ] !== 'h264') return false |
202 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false | 219 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false |
203 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | 220 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false |
@@ -276,6 +293,12 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M | |||
276 | return command | 293 | return command |
277 | } | 294 | } |
278 | 295 | ||
296 | async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) { | ||
297 | command = await presetAudioSplit(command) | ||
298 | |||
299 | return command | ||
300 | } | ||
301 | |||
279 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | 302 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { |
280 | command = await presetCopy(command) | 303 | command = await presetCopy(command) |
281 | 304 | ||
@@ -327,7 +350,7 @@ function getVideoStreamFromFile (path: string) { | |||
327 | if (err) return rej(err) | 350 | if (err) return rej(err) |
328 | 351 | ||
329 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 352 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') |
330 | if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) | 353 | //if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) |
331 | 354 | ||
332 | return res(videoStream) | 355 | return res(videoStream) |
333 | }) | 356 | }) |
@@ -482,3 +505,11 @@ async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.Ffmpeg | |||
482 | .videoCodec('copy') | 505 | .videoCodec('copy') |
483 | .audioCodec('copy') | 506 | .audioCodec('copy') |
484 | } | 507 | } |
508 | |||
509 | |||
510 | async function presetAudioSplit (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | ||
511 | return command | ||
512 | .format('mp4') | ||
513 | .audioCodec('copy') | ||
514 | .noVideo() | ||
515 | } | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 6d5d55487..c6e478f57 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -168,6 +168,7 @@ const CONFIG = { | |||
168 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, | 168 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, |
169 | get THREADS () { return config.get<number>('transcoding.threads') }, | 169 | get THREADS () { return config.get<number>('transcoding.threads') }, |
170 | RESOLUTIONS: { | 170 | RESOLUTIONS: { |
171 | get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, | ||
171 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 172 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
172 | get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') }, | 173 | get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') }, |
173 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, | 174 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 9243d1742..9dd54837f 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -81,12 +81,52 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR | |||
81 | const videoOutputPath = getVideoFilePath(video, newVideoFile) | 81 | const videoOutputPath = getVideoFilePath(video, newVideoFile) |
82 | const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) | 82 | const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) |
83 | 83 | ||
84 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO | ||
85 | ? { | ||
86 | type: 'split-audio' as 'split-audio', | ||
87 | inputPath: videoInputPath, | ||
88 | outputPath: videoTranscodedPath, | ||
89 | resolution, | ||
90 | } | ||
91 | : { | ||
92 | type: 'video' as 'video', | ||
93 | inputPath: videoInputPath, | ||
94 | outputPath: videoTranscodedPath, | ||
95 | resolution, | ||
96 | isPortraitMode: isPortrait | ||
97 | } | ||
98 | |||
99 | await transcode(transcodeOptions) | ||
100 | |||
101 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | ||
102 | } | ||
103 | |||
104 | /** | ||
105 | * Extract audio into a separate audio-only mp4. | ||
106 | */ | ||
107 | async function splitAudioFile (video: MVideoWithFile) { | ||
108 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
109 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | ||
110 | const extname = '.mp4' | ||
111 | const resolution = VideoResolution.H_NOVIDEO | ||
112 | |||
113 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
114 | const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) | ||
115 | |||
116 | const newVideoFile = new VideoFileModel({ | ||
117 | resolution, | ||
118 | extname, | ||
119 | size: 0, | ||
120 | videoId: video.id | ||
121 | }) | ||
122 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) | ||
123 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) | ||
124 | |||
84 | const transcodeOptions = { | 125 | const transcodeOptions = { |
85 | type: 'video' as 'video', | 126 | type: 'split-audio' as 'split-audio', |
86 | inputPath: videoInputPath, | 127 | inputPath: videoInputPath, |
87 | outputPath: videoTranscodedPath, | 128 | outputPath: videoTranscodedPath, |
88 | resolution, | 129 | resolution |
89 | isPortraitMode: isPortrait | ||
90 | } | 130 | } |
91 | 131 | ||
92 | await transcode(transcodeOptions) | 132 | await transcode(transcodeOptions) |
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 1db907f91..d86fa700b 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -37,6 +37,7 @@ const customConfigUpdateValidator = [ | |||
37 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), | 37 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), |
38 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), | 38 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), |
39 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), | 39 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), |
40 | body('transcoding.resolutions.0p').isBoolean().withMessage('Should have a valid transcoding 0p resolution enabled boolean'), | ||
40 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), | 41 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), |
41 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), | 42 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), |
42 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), | 43 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 3c558d4ea..443fbcb60 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -85,6 +85,7 @@ describe('Test config API validators', function () { | |||
85 | allowAudioFiles: true, | 85 | allowAudioFiles: true, |
86 | threads: 1, | 86 | threads: 1, |
87 | resolutions: { | 87 | resolutions: { |
88 | '0p': false, | ||
88 | '240p': false, | 89 | '240p': false, |
89 | '360p': true, | 90 | '360p': true, |
90 | '480p': true, | 91 | '480p': true, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index a494858b3..cf99e5c0a 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -274,6 +274,7 @@ describe('Test config', function () { | |||
274 | allowAudioFiles: true, | 274 | allowAudioFiles: true, |
275 | threads: 1, | 275 | threads: 1, |
276 | resolutions: { | 276 | resolutions: { |
277 | '0p': false, | ||
277 | '240p': false, | 278 | '240p': false, |
278 | '360p': true, | 279 | '360p': true, |
279 | '480p': true, | 280 | '480p': true, |