aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts54
-rw-r--r--server/tests/api/videos/video-transcoder.ts94
-rw-r--r--server/tests/fixtures/video_short_mp3_256k.mp4bin0 -> 194985 bytes
-rw-r--r--server/tests/fixtures/video_short_no_audio.mp4bin0 -> 34259 bytes
-rw-r--r--server/tests/utils/videos/videos.ts2
6 files changed, 125 insertions, 27 deletions
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 2edfb267e..e614c1892 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
@@ -29,7 +29,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
29 { value: 50 * 1024 * 1024 * 1024, label: '50GB' } 29 { value: 50 * 1024 * 1024 * 1024, label: '50GB' }
30 ] 30 ]
31 transcodingThreadOptions = [ 31 transcodingThreadOptions = [
32 { value: 0, label: 'auto (not optimized)' }, 32 { value: 0, label: 'Auto (via ffmpeg)' },
33 { value: 1, label: '1' }, 33 { value: 1, label: '1' },
34 { value: 2, label: '2' }, 34 { value: 2, label: '2' },
35 { value: 4, label: '4' }, 35 { value: 4, label: '4' },
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index c170fc1a8..ced56b82d 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -56,7 +56,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
56 56
57 try { 57 try {
58 await new Promise<string>((res, rej) => { 58 await new Promise<string>((res, rej) => {
59 ffmpeg(fromPath, { 'niceness': FFMPEG_NICE.THUMBNAIL }) 59 ffmpeg(fromPath, { niceness: FFMPEG_NICE.THUMBNAIL })
60 .on('error', rej) 60 .on('error', rej)
61 .on('end', () => res(imageName)) 61 .on('end', () => res(imageName))
62 .thumbnail(options) 62 .thumbnail(options)
@@ -84,11 +84,13 @@ type TranscodeOptions = {
84 84
85function transcode (options: TranscodeOptions) { 85function transcode (options: TranscodeOptions) {
86 return new Promise<void>(async (res, rej) => { 86 return new Promise<void>(async (res, rej) => {
87 let command = ffmpeg(options.inputPath, { 'niceness': FFMPEG_NICE.TRANSCODING }) 87 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
88 .output(options.outputPath) 88 .output(options.outputPath)
89 .preset(standard) 89 .preset(standard)
90
90 if (CONFIG.TRANSCODING.THREADS > 0) { 91 if (CONFIG.TRANSCODING.THREADS > 0) {
91 command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) // if we don't set any threads ffmpeg will chose automatically 92 // if we don't set any threads ffmpeg will chose automatically
93 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
92 } 94 }
93 95
94 let fps = await getVideoFileFPS(options.inputPath) 96 let fps = await getVideoFileFPS(options.inputPath)
@@ -131,7 +133,8 @@ export {
131 getDurationFromVideoFile, 133 getDurationFromVideoFile,
132 generateImageFromVideoFile, 134 generateImageFromVideoFile,
133 transcode, 135 transcode,
134 getVideoFileFPS 136 getVideoFileFPS,
137 audio
135} 138}
136 139
137// --------------------------------------------------------------------------- 140// ---------------------------------------------------------------------------
@@ -191,17 +194,21 @@ namespace audio {
191 // without position, ffprobe considers the last input only 194 // without position, ffprobe considers the last input only
192 // we make it consider the first input only 195 // we make it consider the first input only
193 // if you pass a file path to pos, then ffprobe acts on that file directly 196 // if you pass a file path to pos, then ffprobe acts on that file directly
194 return new Promise<any>((res, rej) => { 197 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
195 _ffmpeg 198 _ffmpeg.ffprobe(pos, (err,data) => {
196 .ffprobe(pos, (err,data) => { 199 if (err) return rej(err)
197 if (err) return rej(err) 200
198 201 if ('streams' in data) {
199 if ('streams' in data) { 202 const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio')
200 return res(data['streams'].find(stream => stream['codec_type'] === 'audio')) 203 if (audioStream) {
201 } else { 204 return res({
202 rej() 205 absolutePath: data.format.filename,
206 audioStream
207 })
203 } 208 }
204 }) 209 }
210 return res({ absolutePath: data.format.filename })
211 })
205 }) 212 })
206 } 213 }
207 214
@@ -212,7 +219,7 @@ namespace audio {
212 219
213 export const aac = (bitrate: number): number => { 220 export const aac = (bitrate: number): number => {
214 switch (true) { 221 switch (true) {
215 case bitrate > toBits(384): 222 case bitrate > toBits(baseKbitrate):
216 return baseKbitrate 223 return baseKbitrate
217 default: 224 default:
218 return -1 // we interpret it as a signal to copy the audio stream as is 225 return -1 // we interpret it as a signal to copy the audio stream as is
@@ -220,6 +227,11 @@ namespace audio {
220 } 227 }
221 228
222 export const mp3 = (bitrate: number): number => { 229 export const mp3 = (bitrate: number): number => {
230 /*
231 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
232 That's why, when using aac, we can go to lower kbit/sec. The equivalences
233 made here are not made to be accurate, especially with good mp3 encoders.
234 */
223 switch (true) { 235 switch (true) {
224 case bitrate <= toBits(192): 236 case bitrate <= toBits(192):
225 return 128 237 return 128
@@ -248,16 +260,16 @@ async function standard (_ffmpeg) {
248 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 260 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
249 .outputOption('-map_metadata -1') // strip all metadata 261 .outputOption('-map_metadata -1') // strip all metadata
250 .outputOption('-movflags faststart') 262 .outputOption('-movflags faststart')
251 let _audio = audio.get(localFfmpeg) 263 const _audio = await audio.get(localFfmpeg)
252 .then(res => res)
253 .catch(_ => undefined)
254 264
255 if (!_audio) return localFfmpeg.noAudio() 265 if (!_audio.audioStream) {
266 return localFfmpeg.noAudio()
267 }
256 268
257 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 269 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
258 // of course this is far from perfect, but it might save some space in the end 270 // of course this is far from perfect, but it might save some space in the end
259 if (audio.bitrate[_audio['codec_name']]) { 271 if (audio.bitrate[_audio.audioStream['codec_name']]) {
260 _bitrate = audio.bitrate[_audio['codec_name']](_audio['bit_rate']) 272 _bitrate = audio.bitrate[_audio.audioStream['codec_name']](_audio.audioStream['bit_rate'])
261 if (_bitrate === -1) { 273 if (_bitrate === -1) {
262 return localFfmpeg.audioCodec('copy') 274 return localFfmpeg.audioCodec('copy')
263 } 275 }
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index fe750253e..4a39ee3e3 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -2,9 +2,12 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash'
6import * as ffmpeg from 'fluent-ffmpeg'
5import { VideoDetails, VideoState } from '../../../../shared/models/videos' 7import { VideoDetails, VideoState } from '../../../../shared/models/videos'
6import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' 8import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils'
7import { 9import {
10 buildAbsoluteFixturePath,
8 doubleFollow, 11 doubleFollow,
9 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
10 getMyVideos, 13 getMyVideos,
@@ -91,6 +94,89 @@ describe('Test video transcoding', function () {
91 expect(torrent.files[0].path).match(/\.mp4$/) 94 expect(torrent.files[0].path).match(/\.mp4$/)
92 }) 95 })
93 96
97 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
98 this.timeout(60000)
99
100 const videoAttributes = {
101 name: 'mp3_256k',
102 fixture: 'video_short_mp3_256k.mp4'
103 }
104 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
105
106 await waitJobs(servers)
107
108 const res = await getVideosList(servers[1].url)
109
110 const video = res.body.data.find(v => v.name === videoAttributes.name)
111 const res2 = await getVideo(servers[1].url, video.id)
112 const videoDetails: VideoDetails = res2.body
113
114 expect(videoDetails.files).to.have.lengthOf(4)
115
116 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
117 const probe = await audio.get(ffmpeg, path)
118
119 if (probe.audioStream) {
120 expect(probe.audioStream['codec_name']).to.be.equal('aac')
121 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
122 } else {
123 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
124 }
125 })
126
127 it('Should transcode video with no audio and have no audio itself', async function () {
128 this.timeout(60000)
129
130 const videoAttributes = {
131 name: 'no_audio',
132 fixture: 'video_short_no_audio.mp4'
133 }
134 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
135
136 await waitJobs(servers)
137
138 const res = await getVideosList(servers[1].url)
139
140 const video = res.body.data.find(v => v.name === videoAttributes.name)
141 const res2 = await getVideo(servers[1].url, video.id)
142 const videoDetails: VideoDetails = res2.body
143
144 expect(videoDetails.files).to.have.lengthOf(4)
145 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
146 const probe = await audio.get(ffmpeg, path)
147 expect(probe).to.not.have.property('audioStream')
148 })
149
150 it('Should leave the audio untouched, but properly transcode the video', async function () {
151 this.timeout(60000)
152
153 const videoAttributes = {
154 name: 'untouched_audio',
155 fixture: 'video_short.mp4'
156 }
157 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
158
159 await waitJobs(servers)
160
161 const res = await getVideosList(servers[1].url)
162
163 const video = res.body.data.find(v => v.name === videoAttributes.name)
164 const res2 = await getVideo(servers[1].url, video.id)
165 const videoDetails: VideoDetails = res2.body
166
167 expect(videoDetails.files).to.have.lengthOf(4)
168 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
169 const fixtureVideoProbe = await audio.get(ffmpeg, fixturePath)
170 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
171 const videoProbe = await audio.get(ffmpeg, path)
172 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
173 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
174 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
175 } else {
176 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
177 }
178 })
179
94 it('Should transcode a 60 FPS video', async function () { 180 it('Should transcode a 60 FPS video', async function () {
95 this.timeout(60000) 181 this.timeout(60000)
96 182
@@ -105,7 +191,7 @@ describe('Test video transcoding', function () {
105 191
106 const res = await getVideosList(servers[1].url) 192 const res = await getVideosList(servers[1].url)
107 193
108 const video = res.body.data[0] 194 const video = res.body.data.find(v => v.name === videoAttributes.name)
109 const res2 = await getVideo(servers[1].url, video.id) 195 const res2 = await getVideo(servers[1].url, video.id)
110 const videoDetails: VideoDetails = res2.body 196 const videoDetails: VideoDetails = res2.body
111 197
@@ -154,7 +240,7 @@ describe('Test video transcoding', function () {
154 240
155 // Should have my video 241 // Should have my video
156 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10) 242 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
157 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video') 243 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
158 expect(videoToFindInMine).not.to.be.undefined 244 expect(videoToFindInMine).not.to.be.undefined
159 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE) 245 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
160 expect(videoToFindInMine.state.label).to.equal('To transcode') 246 expect(videoToFindInMine.state.label).to.equal('To transcode')
@@ -162,7 +248,7 @@ describe('Test video transcoding', function () {
162 248
163 // Should not list this video 249 // Should not list this video
164 const resVideos = await getVideosList(servers[1].url) 250 const resVideos = await getVideosList(servers[1].url)
165 const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video') 251 const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
166 expect(videoToFindInList).to.be.undefined 252 expect(videoToFindInList).to.be.undefined
167 253
168 // Server 1 should not have the video yet 254 // Server 1 should not have the video yet
diff --git a/server/tests/fixtures/video_short_mp3_256k.mp4 b/server/tests/fixtures/video_short_mp3_256k.mp4
new file mode 100644
index 000000000..4c1c7b45e
--- /dev/null
+++ b/server/tests/fixtures/video_short_mp3_256k.mp4
Binary files differ
diff --git a/server/tests/fixtures/video_short_no_audio.mp4 b/server/tests/fixtures/video_short_no_audio.mp4
new file mode 100644
index 000000000..329d20fba
--- /dev/null
+++ b/server/tests/fixtures/video_short_no_audio.mp4
Binary files differ
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index a9d449c58..b280cccda 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -523,7 +523,7 @@ async function completeVideoCheck (
523 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) 523 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
524 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) 524 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
525 expect(file.size, 525 expect(file.size,
526 'File size for resolution ' + file.resolution.label + ' outside confidence interval.') 526 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
527 .to.be.above(minSize).and.below(maxSize) 527 .to.be.above(minSize).and.below(maxSize)
528 528
529 { 529 {