aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts6
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts12
-rw-r--r--config/test.yaml2
-rw-r--r--server/controllers/api/config.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts54
-rw-r--r--server/lib/video-transcoding.ts55
-rw-r--r--server/tests/api/videos/audio-only.ts108
-rw-r--r--server/tests/api/videos/index.ts1
8 files changed, 156 insertions, 84 deletions
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
index 445b14b2b..86be03af7 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-button.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -75,11 +75,15 @@ class ResolutionMenuButton extends MenuButton {
75 // Skip auto resolution, we'll add it ourselves 75 // Skip auto resolution, we'll add it ourselves
76 if (d.id === -1) continue 76 if (d.id === -1) continue
77 77
78 const label = d.id === 0
79 ? this.player.localize('Audio-only')
80 : d.label
81
78 this.menu.addChild(new ResolutionMenuItem( 82 this.menu.addChild(new ResolutionMenuItem(
79 this.player_, 83 this.player_,
80 { 84 {
81 id: d.id, 85 id: d.id,
82 label: d.id == 0 ? this.player .localize('Audio-only') : d.label, 86 label,
83 selected: d.selected, 87 selected: d.selected,
84 callback: data.qualitySwitchCallback 88 callback: data.qualitySwitchCallback
85 }) 89 })
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 007fc58cc..8b5690cea 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -186,15 +186,15 @@ class WebTorrentPlugin extends Plugin {
186 this.player.bigPlayButton.hide() 186 this.player.bigPlayButton.hide()
187 } 187 }
188 188
189 // Audio-only (resolutionId == 0) gets special treatment 189 // Audio-only (resolutionId === 0) gets special treatment
190 if (resolutionId > 0) { 190 if (resolutionId === 0) {
191 // Hide poster to have black background
192 this.player.removeClass('vjs-playing-audio-only-content')
193 this.player.posterImage.hide()
194 } else {
195 // Audio-only: show poster, do not auto-hide controls 191 // Audio-only: show poster, do not auto-hide controls
196 this.player.addClass('vjs-playing-audio-only-content') 192 this.player.addClass('vjs-playing-audio-only-content')
197 this.player.posterImage.show() 193 this.player.posterImage.show()
194 } else {
195 // Hide poster to have black background
196 this.player.removeClass('vjs-playing-audio-only-content')
197 this.player.posterImage.hide()
198 } 198 }
199 199
200 const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) 200 const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
diff --git a/config/test.yaml b/config/test.yaml
index eedd28537..3ab391504 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -72,7 +72,7 @@ transcoding:
72 allow_audio_files: false 72 allow_audio_files: false
73 threads: 2 73 threads: 2
74 resolutions: 74 resolutions:
75 0p: true 75 0p: false
76 240p: true 76 240p: true
77 360p: true 77 360p: true
78 480p: true 78 480p: true
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 8a00f9835..c593fa302 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -357,7 +357,7 @@ function convertCustomConfigBody (body: CustomConfig) {
357 function keyConverter (k: string) { 357 function keyConverter (k: string) {
358 // Transcoding resolutions exception 358 // Transcoding resolutions exception
359 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 360 if (k === '0p') return k
361 361
362 return snakeCase(k) 362 return snakeCase(k)
363 } 363 }
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 2d9ce2bfa..ff80991b2 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -35,15 +35,9 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
35async function getVideoFileSize (path: string) { 35async function getVideoFileSize (path: string) {
36 const videoStream = await getVideoStreamFromFile(path) 36 const videoStream = await getVideoStreamFromFile(path)
37 37
38 return videoStream == null 38 return videoStream === null
39 ? { 39 ? { width: 0, height: 0 }
40 width: 0, 40 : { width: videoStream.width, height: videoStream.height }
41 height: 0
42 }
43 : {
44 width: videoStream.width,
45 height: videoStream.height
46 }
47} 41}
48 42
49async function getVideoFileResolution (path: string) { 43async function getVideoFileResolution (path: string) {
@@ -57,13 +51,10 @@ async function getVideoFileResolution (path: string) {
57 51
58async function getVideoFileFPS (path: string) { 52async function getVideoFileFPS (path: string) {
59 const videoStream = await getVideoStreamFromFile(path) 53 const videoStream = await getVideoStreamFromFile(path)
60 54 if (videoStream === null) return 0
61 if (videoStream == null) {
62 return 0
63 }
64 55
65 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { 56 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
66 const valuesText: string = videoStream[key] 57 const valuesText: string = videoStream[ key ]
67 if (!valuesText) continue 58 if (!valuesText) continue
68 59
69 const [ frames, seconds ] = valuesText.split('/') 60 const [ frames, seconds ] = valuesText.split('/')
@@ -128,7 +119,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
128 } 119 }
129} 120}
130 121
131type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio' 122type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
132 123
133interface BaseTranscodeOptions { 124interface BaseTranscodeOptions {
134 type: TranscodeOptionsType 125 type: TranscodeOptionsType
@@ -159,11 +150,15 @@ interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
159 audioPath: string 150 audioPath: string
160} 151}
161 152
162interface SplitAudioTranscodeOptions extends BaseTranscodeOptions { 153interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
163 type: 'split-audio' 154 type: 'only-audio'
164} 155}
165 156
166type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions 157type TranscodeOptions = HLSTranscodeOptions
158 | VideoTranscodeOptions
159 | MergeAudioTranscodeOptions
160 | OnlyAudioTranscodeOptions
161 | QuickTranscodeOptions
167 162
168function transcode (options: TranscodeOptions) { 163function transcode (options: TranscodeOptions) {
169 return new Promise<void>(async (res, rej) => { 164 return new Promise<void>(async (res, rej) => {
@@ -177,8 +172,8 @@ function transcode (options: TranscodeOptions) {
177 command = await buildHLSCommand(command, options) 172 command = await buildHLSCommand(command, options)
178 } else if (options.type === 'merge-audio') { 173 } else if (options.type === 'merge-audio') {
179 command = await buildAudioMergeCommand(command, options) 174 command = await buildAudioMergeCommand(command, options)
180 } else if (options.type === 'split-audio') { 175 } else if (options.type === 'only-audio') {
181 command = await buildAudioSplitCommand(command, options) 176 command = await buildOnlyAudioCommand(command, options)
182 } else { 177 } else {
183 command = await buildx264Command(command, options) 178 command = await buildx264Command(command, options)
184 } 179 }
@@ -220,7 +215,7 @@ async function canDoQuickTranscode (path: string): Promise<boolean> {
220 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false 215 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
221 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false 216 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
222 217
223 // check audio params (if audio stream exists) 218 // check audio params (if audio stream exists)
224 if (parsedAudio.audioStream) { 219 if (parsedAudio.audioStream) {
225 if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false 220 if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false
226 221
@@ -293,8 +288,8 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
293 return command 288 return command
294} 289}
295 290
296async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) { 291async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) {
297 command = await presetAudioSplit(command) 292 command = await presetOnlyAudio(command)
298 293
299 return command 294 return command
300} 295}
@@ -350,9 +345,7 @@ function getVideoStreamFromFile (path: string) {
350 if (err) return rej(err) 345 if (err) return rej(err)
351 346
352 const videoStream = metadata.streams.find(s => s.codec_type === 'video') 347 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
353 //if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) 348 return res(videoStream || null)
354
355 return res(videoStream)
356 }) 349 })
357 }) 350 })
358} 351}
@@ -384,7 +377,7 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string,
384 * A toolbox to play with audio 377 * A toolbox to play with audio
385 */ 378 */
386namespace audio { 379namespace audio {
387 export const get = (option: string) => { 380 export const get = (videoPath: string) => {
388 // without position, ffprobe considers the last input only 381 // without position, ffprobe considers the last input only
389 // we make it consider the first input only 382 // we make it consider the first input only
390 // if you pass a file path to pos, then ffprobe acts on that file directly 383 // if you pass a file path to pos, then ffprobe acts on that file directly
@@ -394,7 +387,7 @@ namespace audio {
394 if (err) return rej(err) 387 if (err) return rej(err)
395 388
396 if ('streams' in data) { 389 if ('streams' in data) {
397 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') 390 const audioStream = data.streams.find(stream => stream[ 'codec_type' ] === 'audio')
398 if (audioStream) { 391 if (audioStream) {
399 return res({ 392 return res({
400 absolutePath: data.format.filename, 393 absolutePath: data.format.filename,
@@ -406,7 +399,7 @@ namespace audio {
406 return res({ absolutePath: data.format.filename }) 399 return res({ absolutePath: data.format.filename })
407 } 400 }
408 401
409 return ffmpeg.ffprobe(option, parseFfprobe) 402 return ffmpeg.ffprobe(videoPath, parseFfprobe)
410 }) 403 })
411 } 404 }
412 405
@@ -506,8 +499,7 @@ async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.Ffmpeg
506 .audioCodec('copy') 499 .audioCodec('copy')
507} 500}
508 501
509 502async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
510async function presetAudioSplit (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
511 return command 503 return command
512 .format('mp4') 504 .format('mp4')
513 .audioCodec('copy') 505 .audioCodec('copy')
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 9dd54837f..ab5200936 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -83,51 +83,18 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
83 83
84 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO 84 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
85 ? { 85 ? {
86 type: 'split-audio' as 'split-audio', 86 type: 'only-audio' as 'only-audio',
87 inputPath: videoInputPath, 87 inputPath: videoInputPath,
88 outputPath: videoTranscodedPath, 88 outputPath: videoTranscodedPath,
89 resolution, 89 resolution
90 } 90 }
91 : { 91 : {
92 type: 'video' as 'video', 92 type: 'video' as 'video',
93 inputPath: videoInputPath, 93 inputPath: videoInputPath,
94 outputPath: videoTranscodedPath, 94 outputPath: videoTranscodedPath,
95 resolution, 95 resolution,
96 isPortraitMode: isPortrait 96 isPortraitMode: isPortrait
97 } 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
125 const transcodeOptions = {
126 type: 'split-audio' as 'split-audio',
127 inputPath: videoInputPath,
128 outputPath: videoTranscodedPath,
129 resolution
130 }
131 98
132 await transcode(transcodeOptions) 99 await transcode(transcodeOptions)
133 100
diff --git a/server/tests/api/videos/audio-only.ts b/server/tests/api/videos/audio-only.ts
new file mode 100644
index 000000000..1ccae4351
--- /dev/null
+++ b/server/tests/api/videos/audio-only.ts
@@ -0,0 +1,108 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 checkDirectoryIsEmpty,
7 checkSegmentHash,
8 checkTmpIsEmpty,
9 cleanupTests,
10 doubleFollow,
11 flushAndRunMultipleServers,
12 getPlaylist,
13 getVideo, makeGetRequest, makeRawRequest,
14 removeVideo, root,
15 ServerInfo,
16 setAccessTokensToServers, updateCustomSubConfig,
17 updateVideo,
18 uploadVideo,
19 waitJobs, webtorrentAdd
20} from '../../../../shared/extra-utils'
21import { VideoDetails } from '../../../../shared/models/videos'
22import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
23import { join } from 'path'
24import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
25import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution, audio, getVideoFileSize } from '@server/helpers/ffmpeg-utils'
26
27const expect = chai.expect
28
29describe('Test audio only video transcoding', function () {
30 let servers: ServerInfo[] = []
31 let videoUUID: string
32
33 before(async function () {
34 this.timeout(120000)
35
36 const configOverride = {
37 transcoding: {
38 enabled: true,
39 resolutions: {
40 '0p': true,
41 '240p': true,
42 '360p': false,
43 '480p': false,
44 '720p': false,
45 '1080p': false,
46 '2160p': false
47 },
48 hls: {
49 enabled: true
50 },
51 webtorrent: {
52 enabled: true
53 }
54 }
55 }
56 servers = await flushAndRunMultipleServers(2, configOverride)
57
58 // Get the access tokens
59 await setAccessTokensToServers(servers)
60
61 // Server 1 and server 2 follow each other
62 await doubleFollow(servers[0], servers[1])
63 })
64
65 it('Should upload a video and transcode it', async function () {
66 this.timeout(120000)
67
68 const resUpload = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'audio only'})
69 videoUUID = resUpload.body.video.uuid
70
71 await waitJobs(servers)
72
73 for (const server of servers) {
74 const res = await getVideo(server.url, videoUUID)
75 const video: VideoDetails = res.body
76
77 expect(video.streamingPlaylists).to.have.lengthOf(1)
78
79 for (const files of [ video.files, video.streamingPlaylists[0].files ]) {
80 expect(files).to.have.lengthOf(3)
81 expect(files[0].resolution.id).to.equal(720)
82 expect(files[1].resolution.id).to.equal(240)
83 expect(files[2].resolution.id).to.equal(0)
84 }
85 }
86 })
87
88 it('0p transcoded video should not have video', async function () {
89 const paths = [
90 join(root(), 'test' + servers[ 0 ].internalServerNumber, 'videos', videoUUID + '-0.mp4'),
91 join(root(), 'test' + servers[ 0 ].internalServerNumber, 'streaming-playlists', 'hls', videoUUID, videoUUID + '-0-fragmented.mp4')
92 ]
93
94 for (const path of paths) {
95 const { audioStream } = await audio.get(path)
96 expect(audioStream[ 'codec_name' ]).to.be.equal('aac')
97 expect(audioStream[ 'bit_rate' ]).to.be.at.most(384 * 8000)
98
99 const size = await getVideoFileSize(path)
100 expect(size.height).to.equal(0)
101 expect(size.width).to.equal(0)
102 }
103 })
104
105 after(async function () {
106 await cleanupTests(servers)
107 })
108})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 72e6061bb..4d35d3b7c 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -1,3 +1,4 @@
1import './audio-only'
1import './multiple-servers' 2import './multiple-servers'
2import './services' 3import './services'
3import './single-server' 4import './single-server'