diff options
author | Chocobozzz <me@florianbigard.com> | 2021-08-06 10:39:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-08-06 10:39:40 +0200 |
commit | c826f34a45757b324a20f71665b44ed10e6953b5 (patch) | |
tree | ff29bdff8b4519bbdbbcd3aa0d68521ce2b06ff5 | |
parent | 421ff4618da64f0849353383f690a014024c40da (diff) | |
download | PeerTube-c826f34a45757b324a20f71665b44ed10e6953b5.tar.gz PeerTube-c826f34a45757b324a20f71665b44ed10e6953b5.tar.zst PeerTube-c826f34a45757b324a20f71665b44ed10e6953b5.zip |
Limit live bitrate
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 21 | ||||
-rw-r--r-- | server/helpers/ffprobe-utils.ts | 13 | ||||
-rw-r--r-- | server/lib/live/live-manager.ts | 28 | ||||
-rw-r--r-- | server/lib/live/shared/muxing-session.ts | 5 | ||||
-rw-r--r-- | server/lib/transcoding/video-transcoding-profiles.ts | 33 | ||||
-rw-r--r-- | server/tests/api/check-params/live.ts | 4 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 40 | ||||
-rw-r--r-- | shared/extra-utils/videos/live-command.ts | 5 | ||||
-rw-r--r-- | shared/extra-utils/videos/live.ts | 23 | ||||
-rw-r--r-- | shared/models/videos/video-transcoding.model.ts | 1 |
10 files changed, 122 insertions, 51 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 61c8a6db2..7f84a049f 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -6,7 +6,7 @@ import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | |||
6 | import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' | 6 | import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { execPromise, promisify0 } from './core-utils' | 8 | import { execPromise, promisify0 } from './core-utils' |
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, ffprobePromise, getAudioStream, getVideoFileBitrate, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
12 | 12 | ||
@@ -218,11 +218,12 @@ async function getLiveTranscodingCommand (options: { | |||
218 | 218 | ||
219 | resolutions: number[] | 219 | resolutions: number[] |
220 | fps: number | 220 | fps: number |
221 | bitrate: number | ||
221 | 222 | ||
222 | availableEncoders: AvailableEncoders | 223 | availableEncoders: AvailableEncoders |
223 | profile: string | 224 | profile: string |
224 | }) { | 225 | }) { |
225 | const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile, masterPlaylistName } = options | 226 | const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName } = options |
226 | const input = rtmpUrl | 227 | const input = rtmpUrl |
227 | 228 | ||
228 | const command = getFFmpeg(input, 'live') | 229 | const command = getFFmpeg(input, 'live') |
@@ -253,6 +254,7 @@ async function getLiveTranscodingCommand (options: { | |||
253 | profile, | 254 | profile, |
254 | 255 | ||
255 | fps: resolutionFPS, | 256 | fps: resolutionFPS, |
257 | inputBitrate: bitrate, | ||
256 | resolution, | 258 | resolution, |
257 | streamNum: i, | 259 | streamNum: i, |
258 | videoType: 'live' as 'live' | 260 | videoType: 'live' as 'live' |
@@ -260,7 +262,7 @@ async function getLiveTranscodingCommand (options: { | |||
260 | 262 | ||
261 | { | 263 | { |
262 | const streamType: StreamType = 'video' | 264 | const streamType: StreamType = 'video' |
263 | const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType })) | 265 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) |
264 | if (!builderResult) { | 266 | if (!builderResult) { |
265 | throw new Error('No available live video encoder found') | 267 | throw new Error('No available live video encoder found') |
266 | } | 268 | } |
@@ -284,7 +286,7 @@ async function getLiveTranscodingCommand (options: { | |||
284 | 286 | ||
285 | { | 287 | { |
286 | const streamType: StreamType = 'audio' | 288 | const streamType: StreamType = 'audio' |
287 | const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType })) | 289 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) |
288 | if (!builderResult) { | 290 | if (!builderResult) { |
289 | throw new Error('No available live audio encoder found') | 291 | throw new Error('No available live audio encoder found') |
290 | } | 292 | } |
@@ -510,10 +512,11 @@ async function getEncoderBuilderResult (options: { | |||
510 | videoType: 'vod' | 'live' | 512 | videoType: 'vod' | 'live' |
511 | 513 | ||
512 | resolution: number | 514 | resolution: number |
515 | inputBitrate: number | ||
513 | fps?: number | 516 | fps?: number |
514 | streamNum?: number | 517 | streamNum?: number |
515 | }) { | 518 | }) { |
516 | const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options | 519 | const { availableEncoders, input, profile, resolution, streamType, fps, inputBitrate, streamNum, videoType } = options |
517 | 520 | ||
518 | const encodersToTry = availableEncoders.encodersToTry[videoType][streamType] | 521 | const encodersToTry = availableEncoders.encodersToTry[videoType][streamType] |
519 | const encoders = availableEncoders.available[videoType] | 522 | const encoders = availableEncoders.available[videoType] |
@@ -543,7 +546,7 @@ async function getEncoderBuilderResult (options: { | |||
543 | } | 546 | } |
544 | } | 547 | } |
545 | 548 | ||
546 | const result = await builder({ input, resolution, fps, streamNum }) | 549 | const result = await builder({ input, resolution, inputBitrate, fps, streamNum }) |
547 | 550 | ||
548 | return { | 551 | return { |
549 | result, | 552 | result, |
@@ -573,8 +576,11 @@ async function presetVideo (options: { | |||
573 | 576 | ||
574 | addDefaultEncoderGlobalParams({ command }) | 577 | addDefaultEncoderGlobalParams({ command }) |
575 | 578 | ||
579 | const probe = await ffprobePromise(input) | ||
580 | |||
576 | // Audio encoder | 581 | // Audio encoder |
577 | const parsedAudio = await getAudioStream(input) | 582 | const parsedAudio = await getAudioStream(input, probe) |
583 | const bitrate = await getVideoFileBitrate(input, probe) | ||
578 | 584 | ||
579 | let streamsToProcess: StreamType[] = [ 'audio', 'video' ] | 585 | let streamsToProcess: StreamType[] = [ 'audio', 'video' ] |
580 | 586 | ||
@@ -593,6 +599,7 @@ async function presetVideo (options: { | |||
593 | availableEncoders, | 599 | availableEncoders, |
594 | profile, | 600 | profile, |
595 | fps, | 601 | fps, |
602 | inputBitrate: bitrate, | ||
596 | videoType: 'vod' as 'vod' | 603 | videoType: 'vod' as 'vod' |
597 | }) | 604 | }) |
598 | 605 | ||
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index ef2aa3f89..bc87e49b1 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts | |||
@@ -175,10 +175,19 @@ async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.Ffprobe | |||
175 | return new VideoFileMetadata(metadata) | 175 | return new VideoFileMetadata(metadata) |
176 | } | 176 | } |
177 | 177 | ||
178 | async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) { | 178 | async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData): Promise<number> { |
179 | const metadata = await getMetadataFromFile(path, existingProbe) | 179 | const metadata = await getMetadataFromFile(path, existingProbe) |
180 | 180 | ||
181 | return metadata.format.bit_rate as number | 181 | let bitrate = metadata.format.bit_rate as number |
182 | if (bitrate && !isNaN(bitrate)) return bitrate | ||
183 | |||
184 | const videoStream = await getVideoStreamFromFile(path, existingProbe) | ||
185 | if (!videoStream) return undefined | ||
186 | |||
187 | bitrate = videoStream?.bit_rate | ||
188 | if (bitrate && !isNaN(bitrate)) return bitrate | ||
189 | |||
190 | return undefined | ||
182 | } | 191 | } |
183 | 192 | ||
184 | async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) { | 193 | async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) { |
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index f106d69fb..b19ecef6f 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -1,7 +1,13 @@ | |||
1 | 1 | ||
2 | import { createServer, Server } from 'net' | 2 | import { createServer, Server } from 'net' |
3 | import { isTestInstance } from '@server/helpers/core-utils' | 3 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { |
5 | computeResolutionsToTranscode, | ||
6 | ffprobePromise, | ||
7 | getVideoFileBitrate, | ||
8 | getVideoFileFPS, | ||
9 | getVideoFileResolution | ||
10 | } from '@server/helpers/ffprobe-utils' | ||
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 11 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 12 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants' | 13 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants' |
@@ -193,11 +199,20 @@ class LiveManager { | |||
193 | 199 | ||
194 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | 200 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath |
195 | 201 | ||
196 | const [ { videoFileResolution }, fps ] = await Promise.all([ | 202 | const now = Date.now() |
197 | getVideoFileResolution(rtmpUrl), | 203 | const probe = await ffprobePromise(rtmpUrl) |
198 | getVideoFileFPS(rtmpUrl) | 204 | |
205 | const [ { videoFileResolution }, fps, bitrate ] = await Promise.all([ | ||
206 | getVideoFileResolution(rtmpUrl, probe), | ||
207 | getVideoFileFPS(rtmpUrl, probe), | ||
208 | getVideoFileBitrate(rtmpUrl, probe) | ||
199 | ]) | 209 | ]) |
200 | 210 | ||
211 | logger.info( | ||
212 | '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', | ||
213 | rtmpUrl, Date.now() - now, bitrate, fps, videoFileResolution, lTags(sessionId, video.uuid) | ||
214 | ) | ||
215 | |||
201 | const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution) | 216 | const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution) |
202 | 217 | ||
203 | logger.info( | 218 | logger.info( |
@@ -213,6 +228,7 @@ class LiveManager { | |||
213 | streamingPlaylist, | 228 | streamingPlaylist, |
214 | rtmpUrl, | 229 | rtmpUrl, |
215 | fps, | 230 | fps, |
231 | bitrate, | ||
216 | allResolutions | 232 | allResolutions |
217 | }) | 233 | }) |
218 | } | 234 | } |
@@ -223,9 +239,10 @@ class LiveManager { | |||
223 | streamingPlaylist: MStreamingPlaylistVideo | 239 | streamingPlaylist: MStreamingPlaylistVideo |
224 | rtmpUrl: string | 240 | rtmpUrl: string |
225 | fps: number | 241 | fps: number |
242 | bitrate: number | ||
226 | allResolutions: number[] | 243 | allResolutions: number[] |
227 | }) { | 244 | }) { |
228 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, rtmpUrl } = options | 245 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, rtmpUrl } = options |
229 | const videoUUID = videoLive.Video.uuid | 246 | const videoUUID = videoLive.Video.uuid |
230 | const localLTags = lTags(sessionId, videoUUID) | 247 | const localLTags = lTags(sessionId, videoUUID) |
231 | 248 | ||
@@ -239,6 +256,7 @@ class LiveManager { | |||
239 | videoLive, | 256 | videoLive, |
240 | streamingPlaylist, | 257 | streamingPlaylist, |
241 | rtmpUrl, | 258 | rtmpUrl, |
259 | bitrate, | ||
242 | fps, | 260 | fps, |
243 | allResolutions | 261 | allResolutions |
244 | }) | 262 | }) |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 709d6c615..62708b14b 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -54,6 +54,7 @@ class MuxingSession extends EventEmitter { | |||
54 | private readonly streamingPlaylist: MStreamingPlaylistVideo | 54 | private readonly streamingPlaylist: MStreamingPlaylistVideo |
55 | private readonly rtmpUrl: string | 55 | private readonly rtmpUrl: string |
56 | private readonly fps: number | 56 | private readonly fps: number |
57 | private readonly bitrate: number | ||
57 | private readonly allResolutions: number[] | 58 | private readonly allResolutions: number[] |
58 | 59 | ||
59 | private readonly videoId: number | 60 | private readonly videoId: number |
@@ -83,6 +84,7 @@ class MuxingSession extends EventEmitter { | |||
83 | streamingPlaylist: MStreamingPlaylistVideo | 84 | streamingPlaylist: MStreamingPlaylistVideo |
84 | rtmpUrl: string | 85 | rtmpUrl: string |
85 | fps: number | 86 | fps: number |
87 | bitrate: number | ||
86 | allResolutions: number[] | 88 | allResolutions: number[] |
87 | }) { | 89 | }) { |
88 | super() | 90 | super() |
@@ -94,6 +96,7 @@ class MuxingSession extends EventEmitter { | |||
94 | this.streamingPlaylist = options.streamingPlaylist | 96 | this.streamingPlaylist = options.streamingPlaylist |
95 | this.rtmpUrl = options.rtmpUrl | 97 | this.rtmpUrl = options.rtmpUrl |
96 | this.fps = options.fps | 98 | this.fps = options.fps |
99 | this.bitrate = options.bitrate | ||
97 | this.allResolutions = options.allResolutions | 100 | this.allResolutions = options.allResolutions |
98 | 101 | ||
99 | this.videoId = this.videoLive.Video.id | 102 | this.videoId = this.videoLive.Video.id |
@@ -118,6 +121,8 @@ class MuxingSession extends EventEmitter { | |||
118 | 121 | ||
119 | resolutions: this.allResolutions, | 122 | resolutions: this.allResolutions, |
120 | fps: this.fps, | 123 | fps: this.fps, |
124 | bitrate: this.bitrate, | ||
125 | |||
121 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 126 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
122 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | 127 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
123 | }) | 128 | }) |
diff --git a/server/lib/transcoding/video-transcoding-profiles.ts b/server/lib/transcoding/video-transcoding-profiles.ts index c5ea72a5f..2309f38d4 100644 --- a/server/lib/transcoding/video-transcoding-profiles.ts +++ b/server/lib/transcoding/video-transcoding-profiles.ts | |||
@@ -1,14 +1,7 @@ | |||
1 | import { logger } from '@server/helpers/logger' | 1 | import { logger } from '@server/helpers/logger' |
2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos' | 2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos' |
3 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' | 3 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' |
4 | import { | 4 | import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils' |
5 | canDoQuickAudioTranscode, | ||
6 | ffprobePromise, | ||
7 | getAudioStream, | ||
8 | getMaxAudioBitrate, | ||
9 | getVideoFileBitrate, | ||
10 | getVideoStreamFromFile | ||
11 | } from '../../helpers/ffprobe-utils' | ||
12 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | 5 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' |
13 | 6 | ||
14 | /** | 7 | /** |
@@ -22,8 +15,8 @@ import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | |||
22 | // * https://slhck.info/video/2017/03/01/rate-control.html | 15 | // * https://slhck.info/video/2017/03/01/rate-control.html |
23 | // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | 16 | // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
24 | 17 | ||
25 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { | 18 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => { |
26 | const targetBitrate = await buildTargetBitrate({ input, resolution, fps }) | 19 | const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps }) |
27 | if (!targetBitrate) return { outputOptions: [ ] } | 20 | if (!targetBitrate) return { outputOptions: [ ] } |
28 | 21 | ||
29 | return { | 22 | return { |
@@ -36,8 +29,8 @@ const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, reso | |||
36 | } | 29 | } |
37 | } | 30 | } |
38 | 31 | ||
39 | const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, streamNum }) => { | 32 | const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => { |
40 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | 33 | const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps }) |
41 | 34 | ||
42 | return { | 35 | return { |
43 | outputOptions: [ | 36 | outputOptions: [ |
@@ -237,20 +230,16 @@ export { | |||
237 | } | 230 | } |
238 | 231 | ||
239 | // --------------------------------------------------------------------------- | 232 | // --------------------------------------------------------------------------- |
240 | async function buildTargetBitrate (options: { | 233 | |
241 | input: string | 234 | function buildTargetBitrate (options: { |
235 | inputBitrate: number | ||
242 | resolution: VideoResolution | 236 | resolution: VideoResolution |
243 | fps: number | 237 | fps: number |
244 | }) { | 238 | }) { |
245 | const { input, resolution, fps } = options | 239 | const { inputBitrate, resolution, fps } = options |
246 | const probe = await ffprobePromise(input) | ||
247 | |||
248 | const videoStream = await getVideoStreamFromFile(input, probe) | ||
249 | if (!videoStream) return undefined | ||
250 | 240 | ||
251 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | 241 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
242 | if (!inputBitrate) return targetBitrate | ||
252 | 243 | ||
253 | // Don't transcode to an higher bitrate than the original file | 244 | return Math.min(targetBitrate, inputBitrate) |
254 | const fileBitrate = await getVideoFileBitrate(input, probe) | ||
255 | return Math.min(targetBitrate, fileBitrate) | ||
256 | } | 245 | } |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 700b4724d..8e1d655d4 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -417,7 +417,7 @@ describe('Test video lives API validator', function () { | |||
417 | 417 | ||
418 | const live = await command.get({ videoId: video.id }) | 418 | const live = await command.get({ videoId: video.id }) |
419 | 419 | ||
420 | const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey) | 420 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
421 | 421 | ||
422 | await command.waitUntilPublished({ videoId: video.id }) | 422 | await command.waitUntilPublished({ videoId: video.id }) |
423 | await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 423 | await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
@@ -430,7 +430,7 @@ describe('Test video lives API validator', function () { | |||
430 | 430 | ||
431 | const live = await command.get({ videoId: video.id }) | 431 | const live = await command.get({ videoId: video.id }) |
432 | 432 | ||
433 | const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey) | 433 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
434 | 434 | ||
435 | await command.waitUntilPublished({ videoId: video.id }) | 435 | await command.waitUntilPublished({ videoId: video.id }) |
436 | 436 | ||
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index d555cff19..4095cdb1c 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { basename, join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | 6 | import { ffprobePromise, getVideoFileBitrate, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' |
7 | import { | 7 | import { |
8 | checkLiveCleanupAfterSave, | 8 | checkLiveCleanupAfterSave, |
9 | checkLiveSegmentHash, | 9 | checkLiveSegmentHash, |
@@ -302,21 +302,21 @@ describe('Test live', function () { | |||
302 | 302 | ||
303 | liveVideo = await createLiveWrapper() | 303 | liveVideo = await createLiveWrapper() |
304 | 304 | ||
305 | const command = sendRTMPStream(rtmpUrl + '/bad-live', liveVideo.streamKey) | 305 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey }) |
306 | await testFfmpegStreamError(command, true) | 306 | await testFfmpegStreamError(command, true) |
307 | }) | 307 | }) |
308 | 308 | ||
309 | it('Should not allow a stream without the appropriate stream key', async function () { | 309 | it('Should not allow a stream without the appropriate stream key', async function () { |
310 | this.timeout(60000) | 310 | this.timeout(60000) |
311 | 311 | ||
312 | const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key') | 312 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' }) |
313 | await testFfmpegStreamError(command, true) | 313 | await testFfmpegStreamError(command, true) |
314 | }) | 314 | }) |
315 | 315 | ||
316 | it('Should succeed with the correct params', async function () { | 316 | it('Should succeed with the correct params', async function () { |
317 | this.timeout(60000) | 317 | this.timeout(60000) |
318 | 318 | ||
319 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | 319 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) |
320 | await testFfmpegStreamError(command, false) | 320 | await testFfmpegStreamError(command, false) |
321 | }) | 321 | }) |
322 | 322 | ||
@@ -340,7 +340,7 @@ describe('Test live', function () { | |||
340 | 340 | ||
341 | await servers[0].blacklist.add({ videoId: liveVideo.uuid }) | 341 | await servers[0].blacklist.add({ videoId: liveVideo.uuid }) |
342 | 342 | ||
343 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | 343 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) |
344 | await testFfmpegStreamError(command, true) | 344 | await testFfmpegStreamError(command, true) |
345 | }) | 345 | }) |
346 | 346 | ||
@@ -351,7 +351,7 @@ describe('Test live', function () { | |||
351 | 351 | ||
352 | await servers[0].videos.remove({ id: liveVideo.uuid }) | 352 | await servers[0].videos.remove({ id: liveVideo.uuid }) |
353 | 353 | ||
354 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | 354 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) |
355 | await testFfmpegStreamError(command, true) | 355 | await testFfmpegStreamError(command, true) |
356 | }) | 356 | }) |
357 | }) | 357 | }) |
@@ -468,6 +468,34 @@ describe('Test live', function () { | |||
468 | await stopFfmpeg(ffmpegCommand) | 468 | await stopFfmpeg(ffmpegCommand) |
469 | }) | 469 | }) |
470 | 470 | ||
471 | it('Should correctly set the appropriate bitrate depending on the input', async function () { | ||
472 | this.timeout(120000) | ||
473 | |||
474 | liveVideoId = await createLiveWrapper(false) | ||
475 | |||
476 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ | ||
477 | videoId: liveVideoId, | ||
478 | fixtureName: 'video_short.mp4', | ||
479 | copyCodecs: true | ||
480 | }) | ||
481 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
482 | await waitJobs(servers) | ||
483 | |||
484 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
485 | |||
486 | const masterPlaylist = video.streamingPlaylists[0].playlistUrl | ||
487 | const probe = await ffprobePromise(masterPlaylist) | ||
488 | |||
489 | const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate)) | ||
490 | for (const bitrate of bitrates) { | ||
491 | expect(bitrate).to.exist | ||
492 | expect(isNaN(bitrate)).to.be.false | ||
493 | expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate | ||
494 | } | ||
495 | |||
496 | await stopFfmpeg(ffmpegCommand) | ||
497 | }) | ||
498 | |||
471 | it('Should enable transcoding with some resolutions and correctly save them', async function () { | 499 | it('Should enable transcoding with some resolutions and correctly save them', async function () { |
472 | this.timeout(200000) | 500 | this.timeout(200000) |
473 | 501 | ||
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts index bf9486a05..81ae458e0 100644 --- a/shared/extra-utils/videos/live-command.ts +++ b/shared/extra-utils/videos/live-command.ts | |||
@@ -68,11 +68,12 @@ export class LiveCommand extends AbstractCommand { | |||
68 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { | 68 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { |
69 | videoId: number | string | 69 | videoId: number | string |
70 | fixtureName?: string | 70 | fixtureName?: string |
71 | copyCodecs?: boolean | ||
71 | }) { | 72 | }) { |
72 | const { videoId, fixtureName } = options | 73 | const { videoId, fixtureName, copyCodecs } = options |
73 | const videoLive = await this.get({ videoId }) | 74 | const videoLive = await this.get({ videoId }) |
74 | 75 | ||
75 | return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName) | 76 | return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs }) |
76 | } | 77 | } |
77 | 78 | ||
78 | async runAndTestStreamError (options: OverrideCommandOptions & { | 79 | async runAndTestStreamError (options: OverrideCommandOptions & { |
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index 94f5f5b59..6aa405b19 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts | |||
@@ -7,16 +7,29 @@ import { join } from 'path' | |||
7 | import { buildAbsoluteFixturePath, wait } from '../miscs' | 7 | import { buildAbsoluteFixturePath, wait } from '../miscs' |
8 | import { PeerTubeServer } from '../server/server' | 8 | import { PeerTubeServer } from '../server/server' |
9 | 9 | ||
10 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { | 10 | function sendRTMPStream (options: { |
11 | rtmpBaseUrl: string | ||
12 | streamKey: string | ||
13 | fixtureName?: string // default video_short.mp4 | ||
14 | copyCodecs?: boolean // default false | ||
15 | }) { | ||
16 | const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options | ||
17 | |||
11 | const fixture = buildAbsoluteFixturePath(fixtureName) | 18 | const fixture = buildAbsoluteFixturePath(fixtureName) |
12 | 19 | ||
13 | const command = ffmpeg(fixture) | 20 | const command = ffmpeg(fixture) |
14 | command.inputOption('-stream_loop -1') | 21 | command.inputOption('-stream_loop -1') |
15 | command.inputOption('-re') | 22 | command.inputOption('-re') |
16 | command.outputOption('-c:v libx264') | 23 | |
17 | command.outputOption('-g 50') | 24 | if (copyCodecs) { |
18 | command.outputOption('-keyint_min 2') | 25 | command.outputOption('-c:v libx264') |
19 | command.outputOption('-r 60') | 26 | command.outputOption('-g 50') |
27 | command.outputOption('-keyint_min 2') | ||
28 | command.outputOption('-r 60') | ||
29 | } else { | ||
30 | command.outputOption('-c copy') | ||
31 | } | ||
32 | |||
20 | command.outputOption('-f flv') | 33 | command.outputOption('-f flv') |
21 | 34 | ||
22 | const rtmpUrl = rtmpBaseUrl + '/' + streamKey | 35 | const rtmpUrl = rtmpBaseUrl + '/' + streamKey |
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts index 3f2382ce8..f1fe4609b 100644 --- a/shared/models/videos/video-transcoding.model.ts +++ b/shared/models/videos/video-transcoding.model.ts | |||
@@ -5,6 +5,7 @@ import { VideoResolution } from './video-resolution.enum' | |||
5 | export type EncoderOptionsBuilder = (params: { | 5 | export type EncoderOptionsBuilder = (params: { |
6 | input: string | 6 | input: string |
7 | resolution: VideoResolution | 7 | resolution: VideoResolution |
8 | inputBitrate: number | ||
8 | fps?: number | 9 | fps?: number |
9 | streamNum?: number | 10 | streamNum?: number |
10 | }) => Promise<EncoderOptions> | EncoderOptions | 11 | }) => Promise<EncoderOptions> | EncoderOptions |