aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorfrankdelange <yetangitu-f@unternet.org>2019-11-01 02:06:19 +0100
committerChocobozzz <me@florianbigard.com>2019-11-25 10:59:47 +0100
commit5c7d650827cc471a03e7fa18362bcbcbe5d30838 (patch)
tree41c96e3c9e2dcd4f15166e4f13b427ef116ea4f1 /server
parentdee6fe1e4f5c024fd387e8c2b306c174b24aa8b3 (diff)
downloadPeerTube-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.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts45
-rw-r--r--server/initializers/config.ts1
-rw-r--r--server/lib/video-transcoding.ts46
-rw-r--r--server/middlewares/validators/config.ts1
-rw-r--r--server/tests/api/check-params/config.ts1
-rw-r--r--server/tests/api/server/config.ts1
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) {
34async function getVideoFileSize (path: string) { 35async 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
43async function getVideoFileResolution (path: string) { 49async function getVideoFileResolution (path: string) {
@@ -52,6 +58,10 @@ async function getVideoFileResolution (path: string) {
52async function getVideoFileFPS (path: string) { 58async 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
121type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' 131type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio'
122 132
123interface BaseTranscodeOptions { 133interface 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
152type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions 162interface SplitAudioTranscodeOptions extends BaseTranscodeOptions {
163 type: 'split-audio'
164}
165
166type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions
153 167
154function transcode (options: TranscodeOptions) { 168function 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
296async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) {
297 command = await presetAudioSplit(command)
298
299 return command
300}
301
279async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { 302async 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
510async 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 */
107async 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,