diff options
author | Chocobozzz <me@florianbigard.com> | 2019-05-16 08:58:39 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-05-16 08:58:39 +0200 |
commit | 1600235a2f4e30c5d4e7d4342d1c299845decc60 (patch) | |
tree | 8d170d3133a978526474532fc33ac4ba4878f632 | |
parent | bec4ea343987c69252b84d02f444c0f033d4a3f9 (diff) | |
parent | 658a47ab6812586537c3db8a5a003e287c47beb7 (diff) | |
download | PeerTube-1600235a2f4e30c5d4e7d4342d1c299845decc60.tar.gz PeerTube-1600235a2f4e30c5d4e7d4342d1c299845decc60.tar.zst PeerTube-1600235a2f4e30c5d4e7d4342d1c299845decc60.zip |
Merge remote-tracking branch 'origin/pr/1785' into develop
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 49 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 13 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 20 |
3 files changed, 72 insertions, 10 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 76b744de8..2fdf34cb7 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -31,7 +31,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
31 | } | 31 | } |
32 | 32 | ||
33 | async function getVideoFileSize (path: string) { | 33 | async function getVideoFileSize (path: string) { |
34 | const videoStream = await getVideoFileStream(path) | 34 | const videoStream = await getVideoStreamFromFile(path) |
35 | 35 | ||
36 | return { | 36 | return { |
37 | width: videoStream.width, | 37 | width: videoStream.width, |
@@ -49,7 +49,7 @@ async function getVideoFileResolution (path: string) { | |||
49 | } | 49 | } |
50 | 50 | ||
51 | async function getVideoFileFPS (path: string) { | 51 | async function getVideoFileFPS (path: string) { |
52 | const videoStream = await getVideoFileStream(path) | 52 | const videoStream = await getVideoStreamFromFile(path) |
53 | 53 | ||
54 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | 54 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
55 | const valuesText: string = videoStream[key] | 55 | const valuesText: string = videoStream[key] |
@@ -122,6 +122,7 @@ type TranscodeOptions = { | |||
122 | outputPath: string | 122 | outputPath: string |
123 | resolution: VideoResolution | 123 | resolution: VideoResolution |
124 | isPortraitMode?: boolean | 124 | isPortraitMode?: boolean |
125 | doQuickTranscode?: Boolean | ||
125 | 126 | ||
126 | hlsPlaylist?: { | 127 | hlsPlaylist?: { |
127 | videoFilename: string | 128 | videoFilename: string |
@@ -134,7 +135,18 @@ function transcode (options: TranscodeOptions) { | |||
134 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 135 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
135 | .output(options.outputPath) | 136 | .output(options.outputPath) |
136 | 137 | ||
137 | if (options.hlsPlaylist) { | 138 | if (options.doQuickTranscode) { |
139 | if (options.hlsPlaylist) { | ||
140 | throw(Error("Quick transcode and HLS can't be used at the same time")) | ||
141 | } | ||
142 | |||
143 | command | ||
144 | .format('mp4') | ||
145 | .addOption('-c:v copy') | ||
146 | .addOption('-c:a copy') | ||
147 | .outputOption('-map_metadata -1') // strip all metadata | ||
148 | .outputOption('-movflags faststart') | ||
149 | } else if (options.hlsPlaylist) { | ||
138 | command = await buildHLSCommand(command, options) | 150 | command = await buildHLSCommand(command, options) |
139 | } else { | 151 | } else { |
140 | command = await buildx264Command(command, options) | 152 | command = await buildx264Command(command, options) |
@@ -162,6 +174,30 @@ function transcode (options: TranscodeOptions) { | |||
162 | }) | 174 | }) |
163 | } | 175 | } |
164 | 176 | ||
177 | async function canDoQuickTranscode (path: string): Promise<boolean> { | ||
178 | // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway) | ||
179 | const videoStream = await getVideoStreamFromFile(path) | ||
180 | const parsedAudio = await audio.get(path) | ||
181 | const fps = await getVideoFileFPS(path) | ||
182 | const bitRate = await getVideoFileBitrate(path) | ||
183 | const resolution = await getVideoFileResolution(path) | ||
184 | |||
185 | // check video params | ||
186 | if (videoStream[ 'codec_name' ] !== 'h264') return false | ||
187 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | ||
188 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false | ||
189 | |||
190 | // check audio params (if audio stream exists) | ||
191 | if (parsedAudio.audioStream) { | ||
192 | if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false | ||
193 | |||
194 | const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
195 | if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false | ||
196 | } | ||
197 | |||
198 | return true | ||
199 | } | ||
200 | |||
165 | // --------------------------------------------------------------------------- | 201 | // --------------------------------------------------------------------------- |
166 | 202 | ||
167 | export { | 203 | export { |
@@ -173,7 +209,8 @@ export { | |||
173 | getVideoFileFPS, | 209 | getVideoFileFPS, |
174 | computeResolutionsToTranscode, | 210 | computeResolutionsToTranscode, |
175 | audio, | 211 | audio, |
176 | getVideoFileBitrate | 212 | getVideoFileBitrate, |
213 | canDoQuickTranscode | ||
177 | } | 214 | } |
178 | 215 | ||
179 | // --------------------------------------------------------------------------- | 216 | // --------------------------------------------------------------------------- |
@@ -243,7 +280,7 @@ async function onTranscodingSuccess (options: TranscodeOptions) { | |||
243 | await writeFile(options.outputPath, newContent) | 280 | await writeFile(options.outputPath, newContent) |
244 | } | 281 | } |
245 | 282 | ||
246 | function getVideoFileStream (path: string) { | 283 | function getVideoStreamFromFile (path: string) { |
247 | return new Promise<any>((res, rej) => { | 284 | return new Promise<any>((res, rej) => { |
248 | ffmpeg.ffprobe(path, (err, metadata) => { | 285 | ffmpeg.ffprobe(path, (err, metadata) => { |
249 | if (err) return rej(err) | 286 | if (err) return rej(err) |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 0fe0ff12a..8e906a1eb 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { getVideoFileFPS, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, transcode, canDoQuickTranscode } from '../helpers/ffmpeg-utils' |
4 | import { ensureDir, move, remove, stat } from 'fs-extra' | 4 | import { ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
@@ -11,6 +11,9 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
11 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | 11 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' |
12 | import { CONFIG } from '../initializers/config' | 12 | import { CONFIG } from '../initializers/config' |
13 | 13 | ||
14 | /** | ||
15 | * Optimize the original video file and replace it. The resolution is not changed. | ||
16 | */ | ||
14 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { | 17 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { |
15 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 18 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
16 | const newExtname = '.mp4' | 19 | const newExtname = '.mp4' |
@@ -19,10 +22,13 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
19 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 22 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) |
20 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) | 23 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) |
21 | 24 | ||
25 | const doQuickTranscode = await(canDoQuickTranscode(videoInputPath)) | ||
26 | |||
22 | const transcodeOptions = { | 27 | const transcodeOptions = { |
23 | inputPath: videoInputPath, | 28 | inputPath: videoInputPath, |
24 | outputPath: videoTranscodedPath, | 29 | outputPath: videoTranscodedPath, |
25 | resolution: inputVideoFile.resolution | 30 | resolution: inputVideoFile.resolution, |
31 | doQuickTranscode | ||
26 | } | 32 | } |
27 | 33 | ||
28 | // Could be very long! | 34 | // Could be very long! |
@@ -52,6 +58,9 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
52 | } | 58 | } |
53 | } | 59 | } |
54 | 60 | ||
61 | /** | ||
62 | * Transcode the original video file to a lower resolution. | ||
63 | */ | ||
55 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { | 64 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { |
56 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 65 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
57 | const extname = '.mp4' | 66 | const extname = '.mp4' |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 45a8c09f0..cfd0c8430 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -4,7 +4,7 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' | 6 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' |
7 | import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 7 | import { audio, canDoQuickTranscode, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
8 | import { | 8 | import { |
9 | buildAbsoluteFixturePath, | 9 | buildAbsoluteFixturePath, |
10 | cleanupTests, | 10 | cleanupTests, |
@@ -18,10 +18,10 @@ import { | |||
18 | ServerInfo, | 18 | ServerInfo, |
19 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
20 | uploadVideo, | 20 | uploadVideo, |
21 | waitJobs, | ||
21 | webtorrentAdd | 22 | webtorrentAdd |
22 | } from '../../../../shared/extra-utils' | 23 | } from '../../../../shared/extra-utils' |
23 | import { join } from 'path' | 24 | import { join } from 'path' |
24 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
25 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | 25 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' |
26 | 26 | ||
27 | const expect = chai.expect | 27 | const expect = chai.expect |
@@ -324,6 +324,15 @@ describe('Test video transcoding', function () { | |||
324 | it('Should accept and transcode additional extensions', async function () { | 324 | it('Should accept and transcode additional extensions', async function () { |
325 | this.timeout(300000) | 325 | this.timeout(300000) |
326 | 326 | ||
327 | let tempFixturePath: string | ||
328 | |||
329 | { | ||
330 | tempFixturePath = await generateHighBitrateVideo() | ||
331 | |||
332 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
333 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) | ||
334 | } | ||
335 | |||
327 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | 336 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { |
328 | const videoAttributes = { | 337 | const videoAttributes = { |
329 | name: fixture, | 338 | name: fixture, |
@@ -349,6 +358,13 @@ describe('Test video transcoding', function () { | |||
349 | } | 358 | } |
350 | }) | 359 | }) |
351 | 360 | ||
361 | it('Should correctly detect if quick transcode is possible', async function () { | ||
362 | this.timeout(10000) | ||
363 | |||
364 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true | ||
365 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | ||
366 | }) | ||
367 | |||
352 | after(async function () { | 368 | after(async function () { |
353 | await cleanupTests(servers) | 369 | await cleanupTests(servers) |
354 | }) | 370 | }) |