diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 13 | ||||
-rw-r--r-- | server/initializers/checker-after-init.ts | 14 | ||||
-rw-r--r-- | server/initializers/checker-before-init.ts | 1 | ||||
-rw-r--r-- | server/initializers/config.ts | 8 | ||||
-rw-r--r-- | server/initializers/constants.ts | 4 | ||||
-rw-r--r-- | server/lib/live/live-manager.ts | 92 | ||||
-rw-r--r-- | server/lib/live/shared/muxing-session.ts | 12 | ||||
-rw-r--r-- | server/models/video/video-live.ts | 16 | ||||
-rw-r--r-- | server/tests/api/live/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/live/live-rtmps.ts | 146 | ||||
-rw-r--r-- | server/tests/fixtures/rtmps.cert | 21 | ||||
-rw-r--r-- | server/tests/fixtures/rtmps.key | 28 |
12 files changed, 308 insertions, 48 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 54fd031b7..ec24f357b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -219,7 +219,7 @@ async function transcode (options: TranscodeOptions) { | |||
219 | // --------------------------------------------------------------------------- | 219 | // --------------------------------------------------------------------------- |
220 | 220 | ||
221 | async function getLiveTranscodingCommand (options: { | 221 | async function getLiveTranscodingCommand (options: { |
222 | rtmpUrl: string | 222 | inputUrl: string |
223 | 223 | ||
224 | outPath: string | 224 | outPath: string |
225 | masterPlaylistName: string | 225 | masterPlaylistName: string |
@@ -234,10 +234,9 @@ async function getLiveTranscodingCommand (options: { | |||
234 | availableEncoders: AvailableEncoders | 234 | availableEncoders: AvailableEncoders |
235 | profile: string | 235 | profile: string |
236 | }) { | 236 | }) { |
237 | const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options | 237 | const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options |
238 | const input = rtmpUrl | ||
239 | 238 | ||
240 | const command = getFFmpeg(input, 'live') | 239 | const command = getFFmpeg(inputUrl, 'live') |
241 | 240 | ||
242 | const varStreamMap: string[] = [] | 241 | const varStreamMap: string[] = [] |
243 | 242 | ||
@@ -259,7 +258,7 @@ async function getLiveTranscodingCommand (options: { | |||
259 | const resolutionFPS = computeFPS(fps, resolution) | 258 | const resolutionFPS = computeFPS(fps, resolution) |
260 | 259 | ||
261 | const baseEncoderBuilderParams = { | 260 | const baseEncoderBuilderParams = { |
262 | input, | 261 | input: inputUrl, |
263 | 262 | ||
264 | availableEncoders, | 263 | availableEncoders, |
265 | profile, | 264 | profile, |
@@ -327,8 +326,8 @@ async function getLiveTranscodingCommand (options: { | |||
327 | return command | 326 | return command |
328 | } | 327 | } |
329 | 328 | ||
330 | function getLiveMuxingCommand (rtmpUrl: string, outPath: string, masterPlaylistName: string) { | 329 | function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) { |
331 | const command = getFFmpeg(rtmpUrl, 'live') | 330 | const command = getFFmpeg(inputUrl, 'live') |
332 | 331 | ||
333 | command.outputOption('-c:v copy') | 332 | command.outputOption('-c:v copy') |
334 | command.outputOption('-c:a copy') | 333 | command.outputOption('-c:a copy') |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index 7a9e07482..57ef0d218 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -151,6 +151,20 @@ function checkConfig () { | |||
151 | if (CONFIG.LIVE.ALLOW_REPLAY === true && CONFIG.TRANSCODING.ENABLED === false) { | 151 | if (CONFIG.LIVE.ALLOW_REPLAY === true && CONFIG.TRANSCODING.ENABLED === false) { |
152 | return 'Live allow replay cannot be enabled if transcoding is not enabled.' | 152 | return 'Live allow replay cannot be enabled if transcoding is not enabled.' |
153 | } | 153 | } |
154 | |||
155 | if (CONFIG.LIVE.RTMP.ENABLED === false && CONFIG.LIVE.RTMPS.ENABLED === false) { | ||
156 | return 'You must enable at least RTMP or RTMPS' | ||
157 | } | ||
158 | |||
159 | if (CONFIG.LIVE.RTMPS.ENABLED) { | ||
160 | if (!CONFIG.LIVE.RTMPS.KEY_FILE) { | ||
161 | return 'You must specify a key file to enabled RTMPS' | ||
162 | } | ||
163 | |||
164 | if (!CONFIG.LIVE.RTMPS.CERT_FILE) { | ||
165 | return 'You must specify a cert file to enable RTMPS' | ||
166 | } | ||
167 | } | ||
154 | } | 168 | } |
155 | 169 | ||
156 | // Object storage | 170 | // Object storage |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 39f0cebf6..1015c5e45 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -47,6 +47,7 @@ function checkMissedConfig () { | |||
47 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', | 47 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', |
48 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', | 48 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', |
49 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', | 49 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', |
50 | 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.key_file', 'live.rtmps.cert_file', | ||
50 | 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', | 51 | 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', |
51 | 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', | 52 | 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', |
52 | 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', | 53 | 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 0d5e29499..1288768d8 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -271,9 +271,17 @@ const CONFIG = { | |||
271 | get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') }, | 271 | get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') }, |
272 | 272 | ||
273 | RTMP: { | 273 | RTMP: { |
274 | get ENABLED () { return config.get<boolean>('live.rtmp.enabled') }, | ||
274 | get PORT () { return config.get<number>('live.rtmp.port') } | 275 | get PORT () { return config.get<number>('live.rtmp.port') } |
275 | }, | 276 | }, |
276 | 277 | ||
278 | RTMPS: { | ||
279 | get ENABLED () { return config.get<boolean>('live.rtmps.enabled') }, | ||
280 | get PORT () { return config.get<number>('live.rtmps.port') }, | ||
281 | get KEY_FILE () { return config.get<string>('live.rtmps.key_file') }, | ||
282 | get CERT_FILE () { return config.get<string>('live.rtmps.cert_file') } | ||
283 | }, | ||
284 | |||
277 | TRANSCODING: { | 285 | TRANSCODING: { |
278 | get ENABLED () { return config.get<boolean>('live.transcoding.enabled') }, | 286 | get ENABLED () { return config.get<boolean>('live.transcoding.enabled') }, |
279 | get THREADS () { return config.get<number>('live.transcoding.threads') }, | 287 | get THREADS () { return config.get<number>('live.transcoding.threads') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 3781f9508..fb6bc9a66 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -52,7 +52,8 @@ const WEBSERVER = { | |||
52 | WS: '', | 52 | WS: '', |
53 | HOSTNAME: '', | 53 | HOSTNAME: '', |
54 | PORT: 0, | 54 | PORT: 0, |
55 | RTMP_URL: '' | 55 | RTMP_URL: '', |
56 | RTMPS_URL: '' | ||
56 | } | 57 | } |
57 | 58 | ||
58 | // Sortable columns per schema | 59 | // Sortable columns per schema |
@@ -998,6 +999,7 @@ function updateWebserverUrls () { | |||
998 | WEBSERVER.PORT = CONFIG.WEBSERVER.PORT | 999 | WEBSERVER.PORT = CONFIG.WEBSERVER.PORT |
999 | 1000 | ||
1000 | WEBSERVER.RTMP_URL = 'rtmp://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMP.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH | 1001 | WEBSERVER.RTMP_URL = 'rtmp://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMP.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH |
1002 | WEBSERVER.RTMPS_URL = 'rtmps://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMPS.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH | ||
1001 | } | 1003 | } |
1002 | 1004 | ||
1003 | function updateWebserverConfig () { | 1005 | function updateWebserverConfig () { |
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index d7dc841d9..c75cc3bda 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | 1 | ||
2 | import { readFile } from 'fs-extra' | ||
2 | import { createServer, Server } from 'net' | 3 | import { createServer, Server } from 'net' |
4 | import { createServer as createServerTLS, Server as ServerTLS } from 'tls' | ||
3 | import { isTestInstance } from '@server/helpers/core-utils' | 5 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { | 6 | import { |
5 | computeResolutionsToTranscode, | 7 | computeResolutionsToTranscode, |
@@ -19,8 +21,8 @@ import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/ | |||
19 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | 21 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' |
20 | import { federateVideoIfNeeded } from '../activitypub/videos' | 22 | import { federateVideoIfNeeded } from '../activitypub/videos' |
21 | import { JobQueue } from '../job-queue' | 23 | import { JobQueue } from '../job-queue' |
22 | import { PeerTubeSocket } from '../peertube-socket' | ||
23 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths' | 24 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths' |
25 | import { PeerTubeSocket } from '../peertube-socket' | ||
24 | import { LiveQuotaStore } from './live-quota-store' | 26 | import { LiveQuotaStore } from './live-quota-store' |
25 | import { LiveSegmentShaStore } from './live-segment-sha-store' | 27 | import { LiveSegmentShaStore } from './live-segment-sha-store' |
26 | import { cleanupLive } from './live-utils' | 28 | import { cleanupLive } from './live-utils' |
@@ -40,9 +42,6 @@ const config = { | |||
40 | gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE, | 42 | gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE, |
41 | ping: VIDEO_LIVE.RTMP.PING, | 43 | ping: VIDEO_LIVE.RTMP.PING, |
42 | ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT | 44 | ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT |
43 | }, | ||
44 | transcoding: { | ||
45 | ffmpeg: 'ffmpeg' | ||
46 | } | 45 | } |
47 | } | 46 | } |
48 | 47 | ||
@@ -58,6 +57,9 @@ class LiveManager { | |||
58 | private readonly watchersPerVideo = new Map<number, number[]>() | 57 | private readonly watchersPerVideo = new Map<number, number[]>() |
59 | 58 | ||
60 | private rtmpServer: Server | 59 | private rtmpServer: Server |
60 | private rtmpsServer: ServerTLS | ||
61 | |||
62 | private running = false | ||
61 | 63 | ||
62 | private constructor () { | 64 | private constructor () { |
63 | } | 65 | } |
@@ -73,7 +75,9 @@ class LiveManager { | |||
73 | return this.abortSession(sessionId) | 75 | return this.abortSession(sessionId) |
74 | } | 76 | } |
75 | 77 | ||
76 | this.handleSession(sessionId, streamPath, splittedPath[2]) | 78 | const session = this.getContext().sessions.get(sessionId) |
79 | |||
80 | this.handleSession(sessionId, session.inputOriginUrl + streamPath, splittedPath[2]) | ||
77 | .catch(err => logger.error('Cannot handle sessions.', { err, ...lTags(sessionId) })) | 81 | .catch(err => logger.error('Cannot handle sessions.', { err, ...lTags(sessionId) })) |
78 | }) | 82 | }) |
79 | 83 | ||
@@ -82,12 +86,12 @@ class LiveManager { | |||
82 | }) | 86 | }) |
83 | 87 | ||
84 | registerConfigChangedHandler(() => { | 88 | registerConfigChangedHandler(() => { |
85 | if (!this.rtmpServer && CONFIG.LIVE.ENABLED === true) { | 89 | if (!this.running && CONFIG.LIVE.ENABLED === true) { |
86 | this.run() | 90 | this.run().catch(err => logger.error('Cannot run live server.', { err })) |
87 | return | 91 | return |
88 | } | 92 | } |
89 | 93 | ||
90 | if (this.rtmpServer && CONFIG.LIVE.ENABLED === false) { | 94 | if (this.running && CONFIG.LIVE.ENABLED === false) { |
91 | this.stop() | 95 | this.stop() |
92 | } | 96 | } |
93 | }) | 97 | }) |
@@ -99,23 +103,53 @@ class LiveManager { | |||
99 | setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE) | 103 | setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE) |
100 | } | 104 | } |
101 | 105 | ||
102 | run () { | 106 | async run () { |
103 | logger.info('Running RTMP server on port %d', config.rtmp.port, lTags()) | 107 | this.running = true |
104 | 108 | ||
105 | this.rtmpServer = createServer(socket => { | 109 | if (CONFIG.LIVE.RTMP.ENABLED) { |
106 | const session = new NodeRtmpSession(config, socket) | 110 | logger.info('Running RTMP server on port %d', CONFIG.LIVE.RTMP.PORT, lTags()) |
107 | 111 | ||
108 | session.run() | 112 | this.rtmpServer = createServer(socket => { |
109 | }) | 113 | const session = new NodeRtmpSession(config, socket) |
110 | 114 | ||
111 | this.rtmpServer.on('error', err => { | 115 | session.inputOriginUrl = 'rtmp://127.0.0.1:' + CONFIG.LIVE.RTMP.PORT |
112 | logger.error('Cannot run RTMP server.', { err, ...lTags() }) | 116 | session.run() |
113 | }) | 117 | }) |
118 | |||
119 | this.rtmpServer.on('error', err => { | ||
120 | logger.error('Cannot run RTMP server.', { err, ...lTags() }) | ||
121 | }) | ||
122 | |||
123 | this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT) | ||
124 | } | ||
114 | 125 | ||
115 | this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT) | 126 | if (CONFIG.LIVE.RTMPS.ENABLED) { |
127 | logger.info('Running RTMPS server on port %d', CONFIG.LIVE.RTMPS.PORT, lTags()) | ||
128 | |||
129 | const [ key, cert ] = await Promise.all([ | ||
130 | readFile(CONFIG.LIVE.RTMPS.KEY_FILE), | ||
131 | readFile(CONFIG.LIVE.RTMPS.CERT_FILE) | ||
132 | ]) | ||
133 | const serverOptions = { key, cert } | ||
134 | |||
135 | this.rtmpsServer = createServerTLS(serverOptions, socket => { | ||
136 | const session = new NodeRtmpSession(config, socket) | ||
137 | |||
138 | session.inputOriginUrl = 'rtmps://127.0.0.1:' + CONFIG.LIVE.RTMPS.PORT | ||
139 | session.run() | ||
140 | }) | ||
141 | |||
142 | this.rtmpsServer.on('error', err => { | ||
143 | logger.error('Cannot run RTMPS server.', { err, ...lTags() }) | ||
144 | }) | ||
145 | |||
146 | this.rtmpsServer.listen(CONFIG.LIVE.RTMPS.PORT) | ||
147 | } | ||
116 | } | 148 | } |
117 | 149 | ||
118 | stop () { | 150 | stop () { |
151 | this.running = false | ||
152 | |||
119 | logger.info('Stopping RTMP server.', lTags()) | 153 | logger.info('Stopping RTMP server.', lTags()) |
120 | 154 | ||
121 | this.rtmpServer.close() | 155 | this.rtmpServer.close() |
@@ -174,7 +208,7 @@ class LiveManager { | |||
174 | } | 208 | } |
175 | } | 209 | } |
176 | 210 | ||
177 | private async handleSession (sessionId: string, streamPath: string, streamKey: string) { | 211 | private async handleSession (sessionId: string, inputUrl: string, streamKey: string) { |
178 | const videoLive = await VideoLiveModel.loadByStreamKey(streamKey) | 212 | const videoLive = await VideoLiveModel.loadByStreamKey(streamKey) |
179 | if (!videoLive) { | 213 | if (!videoLive) { |
180 | logger.warn('Unknown live video with stream key %s.', streamKey, lTags(sessionId)) | 214 | logger.warn('Unknown live video with stream key %s.', streamKey, lTags(sessionId)) |
@@ -197,20 +231,18 @@ class LiveManager { | |||
197 | 231 | ||
198 | this.videoSessions.set(video.id, sessionId) | 232 | this.videoSessions.set(video.id, sessionId) |
199 | 233 | ||
200 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
201 | |||
202 | const now = Date.now() | 234 | const now = Date.now() |
203 | const probe = await ffprobePromise(rtmpUrl) | 235 | const probe = await ffprobePromise(inputUrl) |
204 | 236 | ||
205 | const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ | 237 | const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ |
206 | getVideoFileResolution(rtmpUrl, probe), | 238 | getVideoFileResolution(inputUrl, probe), |
207 | getVideoFileFPS(rtmpUrl, probe), | 239 | getVideoFileFPS(inputUrl, probe), |
208 | getVideoFileBitrate(rtmpUrl, probe) | 240 | getVideoFileBitrate(inputUrl, probe) |
209 | ]) | 241 | ]) |
210 | 242 | ||
211 | logger.info( | 243 | logger.info( |
212 | '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', | 244 | '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', |
213 | rtmpUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid) | 245 | inputUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid) |
214 | ) | 246 | ) |
215 | 247 | ||
216 | const allResolutions = this.buildAllResolutionsToTranscode(resolution) | 248 | const allResolutions = this.buildAllResolutionsToTranscode(resolution) |
@@ -226,7 +258,7 @@ class LiveManager { | |||
226 | sessionId, | 258 | sessionId, |
227 | videoLive, | 259 | videoLive, |
228 | streamingPlaylist, | 260 | streamingPlaylist, |
229 | rtmpUrl, | 261 | inputUrl, |
230 | fps, | 262 | fps, |
231 | bitrate, | 263 | bitrate, |
232 | ratio, | 264 | ratio, |
@@ -238,13 +270,13 @@ class LiveManager { | |||
238 | sessionId: string | 270 | sessionId: string |
239 | videoLive: MVideoLiveVideo | 271 | videoLive: MVideoLiveVideo |
240 | streamingPlaylist: MStreamingPlaylistVideo | 272 | streamingPlaylist: MStreamingPlaylistVideo |
241 | rtmpUrl: string | 273 | inputUrl: string |
242 | fps: number | 274 | fps: number |
243 | bitrate: number | 275 | bitrate: number |
244 | ratio: number | 276 | ratio: number |
245 | allResolutions: number[] | 277 | allResolutions: number[] |
246 | }) { | 278 | }) { |
247 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, ratio, rtmpUrl } = options | 279 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, ratio, inputUrl } = options |
248 | const videoUUID = videoLive.Video.uuid | 280 | const videoUUID = videoLive.Video.uuid |
249 | const localLTags = lTags(sessionId, videoUUID) | 281 | const localLTags = lTags(sessionId, videoUUID) |
250 | 282 | ||
@@ -257,7 +289,7 @@ class LiveManager { | |||
257 | sessionId, | 289 | sessionId, |
258 | videoLive, | 290 | videoLive, |
259 | streamingPlaylist, | 291 | streamingPlaylist, |
260 | rtmpUrl, | 292 | inputUrl, |
261 | bitrate, | 293 | bitrate, |
262 | ratio, | 294 | ratio, |
263 | fps, | 295 | fps, |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index b52363af7..c71f4e25f 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -52,7 +52,7 @@ class MuxingSession extends EventEmitter { | |||
52 | private readonly sessionId: string | 52 | private readonly sessionId: string |
53 | private readonly videoLive: MVideoLiveVideo | 53 | private readonly videoLive: MVideoLiveVideo |
54 | private readonly streamingPlaylist: MStreamingPlaylistVideo | 54 | private readonly streamingPlaylist: MStreamingPlaylistVideo |
55 | private readonly rtmpUrl: string | 55 | private readonly inputUrl: string |
56 | private readonly fps: number | 56 | private readonly fps: number |
57 | private readonly allResolutions: number[] | 57 | private readonly allResolutions: number[] |
58 | 58 | ||
@@ -84,7 +84,7 @@ class MuxingSession extends EventEmitter { | |||
84 | sessionId: string | 84 | sessionId: string |
85 | videoLive: MVideoLiveVideo | 85 | videoLive: MVideoLiveVideo |
86 | streamingPlaylist: MStreamingPlaylistVideo | 86 | streamingPlaylist: MStreamingPlaylistVideo |
87 | rtmpUrl: string | 87 | inputUrl: string |
88 | fps: number | 88 | fps: number |
89 | bitrate: number | 89 | bitrate: number |
90 | ratio: number | 90 | ratio: number |
@@ -97,7 +97,7 @@ class MuxingSession extends EventEmitter { | |||
97 | this.sessionId = options.sessionId | 97 | this.sessionId = options.sessionId |
98 | this.videoLive = options.videoLive | 98 | this.videoLive = options.videoLive |
99 | this.streamingPlaylist = options.streamingPlaylist | 99 | this.streamingPlaylist = options.streamingPlaylist |
100 | this.rtmpUrl = options.rtmpUrl | 100 | this.inputUrl = options.inputUrl |
101 | this.fps = options.fps | 101 | this.fps = options.fps |
102 | 102 | ||
103 | this.bitrate = options.bitrate | 103 | this.bitrate = options.bitrate |
@@ -120,7 +120,7 @@ class MuxingSession extends EventEmitter { | |||
120 | 120 | ||
121 | this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED | 121 | this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED |
122 | ? await getLiveTranscodingCommand({ | 122 | ? await getLiveTranscodingCommand({ |
123 | rtmpUrl: this.rtmpUrl, | 123 | inputUrl: this.inputUrl, |
124 | 124 | ||
125 | outPath, | 125 | outPath, |
126 | masterPlaylistName: this.streamingPlaylist.playlistFilename, | 126 | masterPlaylistName: this.streamingPlaylist.playlistFilename, |
@@ -133,7 +133,7 @@ class MuxingSession extends EventEmitter { | |||
133 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 133 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
134 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | 134 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
135 | }) | 135 | }) |
136 | : getLiveMuxingCommand(this.rtmpUrl, outPath, this.streamingPlaylist.playlistFilename) | 136 | : getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename) |
137 | 137 | ||
138 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) | 138 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) |
139 | 139 | ||
@@ -173,7 +173,7 @@ class MuxingSession extends EventEmitter { | |||
173 | } | 173 | } |
174 | 174 | ||
175 | private onFFmpegEnded (outPath: string) { | 175 | private onFFmpegEnded (outPath: string) { |
176 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.rtmpUrl, this.lTags) | 176 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags) |
177 | 177 | ||
178 | setTimeout(() => { | 178 | setTimeout(() => { |
179 | // Wait latest segments generation, and close watchers | 179 | // Wait latest segments generation, and close watchers |
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts index 014491d50..0bc8da022 100644 --- a/server/models/video/video-live.ts +++ b/server/models/video/video-live.ts | |||
@@ -5,6 +5,7 @@ import { AttributesOnly } from '@shared/core-utils' | |||
5 | import { LiveVideo, VideoState } from '@shared/models' | 5 | import { LiveVideo, VideoState } from '@shared/models' |
6 | import { VideoModel } from './video' | 6 | import { VideoModel } from './video' |
7 | import { VideoBlacklistModel } from './video-blacklist' | 7 | import { VideoBlacklistModel } from './video-blacklist' |
8 | import { CONFIG } from '@server/initializers/config' | ||
8 | 9 | ||
9 | @DefaultScope(() => ({ | 10 | @DefaultScope(() => ({ |
10 | include: [ | 11 | include: [ |
@@ -97,11 +98,18 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
97 | } | 98 | } |
98 | 99 | ||
99 | toFormattedJSON (): LiveVideo { | 100 | toFormattedJSON (): LiveVideo { |
101 | let rtmpUrl: string = null | ||
102 | let rtmpsUrl: string = null | ||
103 | |||
104 | // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL | ||
105 | if (this.streamKey) { | ||
106 | if (CONFIG.LIVE.RTMP.ENABLED) rtmpUrl = WEBSERVER.RTMP_URL | ||
107 | if (CONFIG.LIVE.RTMPS.ENABLED) rtmpsUrl = WEBSERVER.RTMPS_URL | ||
108 | } | ||
109 | |||
100 | return { | 110 | return { |
101 | // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL | 111 | rtmpUrl, |
102 | rtmpUrl: this.streamKey | 112 | rtmpsUrl, |
103 | ? WEBSERVER.RTMP_URL | ||
104 | : null, | ||
105 | 113 | ||
106 | streamKey: this.streamKey, | 114 | streamKey: this.streamKey, |
107 | permanentLive: this.permanentLive, | 115 | permanentLive: this.permanentLive, |
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts index e6bcef49f..105416b8d 100644 --- a/server/tests/api/live/index.ts +++ b/server/tests/api/live/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import './live-constraints' | 1 | import './live-constraints' |
2 | import './live-socket-messages' | 2 | import './live-socket-messages' |
3 | import './live-permanent' | 3 | import './live-permanent' |
4 | import './live-rtmps' | ||
4 | import './live-save-replay' | 5 | import './live-save-replay' |
5 | import './live-views' | 6 | import './live-views' |
6 | import './live' | 7 | import './live' |
diff --git a/server/tests/api/live/live-rtmps.ts b/server/tests/api/live/live-rtmps.ts new file mode 100644 index 000000000..378e6df3c --- /dev/null +++ b/server/tests/api/live/live-rtmps.ts | |||
@@ -0,0 +1,146 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { VideoPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | buildAbsoluteFixturePath, | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | sendRTMPStream, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | stopFfmpeg, | ||
15 | testFfmpegStreamError, | ||
16 | waitUntilLivePublishedOnAllServers | ||
17 | } from '../../../../shared/extra-utils' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | describe('Test live RTMPS', function () { | ||
22 | let server: PeerTubeServer | ||
23 | let rtmpUrl: string | ||
24 | let rtmpsUrl: string | ||
25 | |||
26 | async function createLiveWrapper () { | ||
27 | const liveAttributes = { | ||
28 | name: 'live', | ||
29 | channelId: server.store.channel.id, | ||
30 | privacy: VideoPrivacy.PUBLIC, | ||
31 | saveReplay: false | ||
32 | } | ||
33 | |||
34 | const { uuid } = await server.live.create({ fields: liveAttributes }) | ||
35 | |||
36 | const live = await server.live.get({ videoId: uuid }) | ||
37 | const video = await server.videos.get({ id: uuid }) | ||
38 | |||
39 | return Object.assign(video, live) | ||
40 | } | ||
41 | |||
42 | before(async function () { | ||
43 | this.timeout(120000) | ||
44 | |||
45 | server = await createSingleServer(1) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers([ server ]) | ||
49 | await setDefaultVideoChannel([ server ]) | ||
50 | |||
51 | await server.config.updateCustomSubConfig({ | ||
52 | newConfig: { | ||
53 | live: { | ||
54 | enabled: true, | ||
55 | allowReplay: true, | ||
56 | transcoding: { | ||
57 | enabled: false | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | }) | ||
62 | |||
63 | rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live' | ||
64 | rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live' | ||
65 | }) | ||
66 | |||
67 | it('Should enable RTMPS endpoint only', async function () { | ||
68 | this.timeout(240000) | ||
69 | |||
70 | await server.kill() | ||
71 | await server.run({ | ||
72 | live: { | ||
73 | rtmp: { | ||
74 | enabled: false | ||
75 | }, | ||
76 | rtmps: { | ||
77 | enabled: true, | ||
78 | port: server.rtmpsPort, | ||
79 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
80 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
81 | } | ||
82 | } | ||
83 | }) | ||
84 | |||
85 | { | ||
86 | const liveVideo = await createLiveWrapper() | ||
87 | |||
88 | expect(liveVideo.rtmpUrl).to.not.exist | ||
89 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
90 | |||
91 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
92 | await testFfmpegStreamError(command, true) | ||
93 | } | ||
94 | |||
95 | { | ||
96 | const liveVideo = await createLiveWrapper() | ||
97 | |||
98 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
99 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
100 | await stopFfmpeg(command) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should enable both RTMP and RTMPS', async function () { | ||
105 | this.timeout(240000) | ||
106 | |||
107 | await server.kill() | ||
108 | await server.run({ | ||
109 | live: { | ||
110 | rtmp: { | ||
111 | enabled: true, | ||
112 | port: server.rtmpPort | ||
113 | }, | ||
114 | rtmps: { | ||
115 | enabled: true, | ||
116 | port: server.rtmpsPort, | ||
117 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
118 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
119 | } | ||
120 | } | ||
121 | }) | ||
122 | |||
123 | { | ||
124 | const liveVideo = await createLiveWrapper() | ||
125 | |||
126 | expect(liveVideo.rtmpUrl).to.equal(rtmpUrl) | ||
127 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
128 | |||
129 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
130 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
131 | await stopFfmpeg(command) | ||
132 | } | ||
133 | |||
134 | { | ||
135 | const liveVideo = await createLiveWrapper() | ||
136 | |||
137 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
138 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
139 | await stopFfmpeg(command) | ||
140 | } | ||
141 | }) | ||
142 | |||
143 | after(async function () { | ||
144 | await cleanupTests([ server ]) | ||
145 | }) | ||
146 | }) | ||
diff --git a/server/tests/fixtures/rtmps.cert b/server/tests/fixtures/rtmps.cert new file mode 100644 index 000000000..3ef606c52 --- /dev/null +++ b/server/tests/fixtures/rtmps.cert | |||
@@ -0,0 +1,21 @@ | |||
1 | -----BEGIN CERTIFICATE----- | ||
2 | MIIDazCCAlOgAwIBAgIUKNycLAZUs2jFsWUW+zZhBkpLB2wwDQYJKoZIhvcNAQEL | ||
3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM | ||
4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTExMDUxMDA4MzhaFw0yMTEy | ||
5 | MDUxMDA4MzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw | ||
6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB | ||
7 | AQUAA4IBDwAwggEKAoIBAQDak20d81KG/9mVLU6Qw/uRniC935yf9Rlp8FVCDxUd | ||
8 | zLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88WDU33Q8ixU/R0czUGq1AEwIjyN30 | ||
9 | 5NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJMNC0Lit9Go9MDVnGFLkgHia68P72T | ||
10 | ZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfUY0VAEZlxJ/9zjwYHCT0AKaEPH35E | ||
11 | dUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GWIqoiIOpdjFUBLs80QOM2aNrLmlyP | ||
12 | JtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uHZKi5yazNAgMBAAGjUzBRMB0GA1Ud | ||
13 | DgQWBBSSjhRQdWsybNQMLMhkwV+xiP2uoDAfBgNVHSMEGDAWgBSSjhRQdWsybNQM | ||
14 | LMhkwV+xiP2uoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC8 | ||
15 | rJu3J5sqVKNQaXOmLPd49RM7KG3Y1KPqbQi1lh+sW6aefZ9daeh3JDYGBZGPG/Fi | ||
16 | IMMP+LhGG0WqDm4ClK00wyNhBuNPEyzvuN/WMRX5djPxO1IZi+KogFwXsn853Ov9 | ||
17 | oV3nxArNNjDu2n92FiB7RTlXRXPIoRo2zEBcLvveGySn9XUazRzlqx6FAxYe2xsw | ||
18 | U3cZ6/wwU1YsEZa5bwIQk+gkFj3zDsTyEkn2ntcE2NlR+AhCHKa/yAxgPFycAVPX | ||
19 | 2o+wNnc6H4syP98mMGj9hEE3RSJyCPgGBlgi7Swl64G3YygFPJzfLX9YTuxwr/eI | ||
20 | oitEjF9ljtmdEnf0RdOj | ||
21 | -----END CERTIFICATE----- | ||
diff --git a/server/tests/fixtures/rtmps.key b/server/tests/fixtures/rtmps.key new file mode 100644 index 000000000..14a85e70a --- /dev/null +++ b/server/tests/fixtures/rtmps.key | |||
@@ -0,0 +1,28 @@ | |||
1 | -----BEGIN PRIVATE KEY----- | ||
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDak20d81KG/9mV | ||
3 | LU6Qw/uRniC935yf9Rlp8FVCDxUdzLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88 | ||
4 | WDU33Q8ixU/R0czUGq1AEwIjyN305NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJM | ||
5 | NC0Lit9Go9MDVnGFLkgHia68P72TZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfU | ||
6 | Y0VAEZlxJ/9zjwYHCT0AKaEPH35EdUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GW | ||
7 | IqoiIOpdjFUBLs80QOM2aNrLmlyPJtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uH | ||
8 | ZKi5yazNAgMBAAECggEAND7C+UK8+jnTl13CBsZhrnfemaQGexGJ5pGkv2p9gKb7 | ||
9 | Gy/Nooty/OdNWtjdNJ5N22YfSRkXulgZxBHNfrHfOU9yedOtIxHRUZx5iXYs36mH | ||
10 | 02cJeUHN3t1MOnkoWTvIGDH4vZUnP1lXV+Gs1rJ2Fht4h7a04cGjQ/H8C1EtDjqX | ||
11 | kzH2T/gwo5hdGrxifRTs5wCVoP/iUwNtBI4WrY2rfC6sV+NOICgp0xX0NvGWZ8UT | ||
12 | K1Ntpl8IxnxmeBd26d+Gbjc9d9fIRDtyXby4YOIlDZxnIiZEI0I452JqGl/jrXaP | ||
13 | F3Troet4OBj5uH5s374d6ubKq66XogiLMIjEj2tYfQKBgQDtuaOu+y549bFJKVc9 | ||
14 | TCiWSOl/0j2kKKG8UG23zMC//AT13WqZDT5ObfOAuMhy70au/PD84D9RU/+gRVWb | ||
15 | ptfybD9ugRNC8PkmdT82uYtZpS4+Xw4qyWVRgqQFmjSYz63cLcULVi8kiG8XmG5u | ||
16 | QGgT/tNv5mxhOMUGSxhClOpLBwKBgQDrYO9UrLs+gDVKbHF4Dh+YJpaLnwwF+TFA | ||
17 | j3ZbkE0XEeeXp/YDgyClmWwEkteJeNljtreCZ9gMkx3JdR9i8uecUQ2tFDBg3cN0 | ||
18 | BZAex2jFwSb0QbfzHNnE07I+aEIfHHjYXjzABl+1Yt95giKjce0Ke+8Zzahue0+9 | ||
19 | lYcAHemQiwKBgQCs9JAbIdJo3NBUW0iGZ19sH7YKciq4wXsSaC27OLPPugrd2m7Q | ||
20 | 1arMIwCzWT01KdLyQ0MNqBVJFWT49RjYuuWIEauAuVYLMQkEKu+H4Cx7V0syw7Op | ||
21 | +4bEa9jr3op/1zE17PLcUaLQ4JZ6w0Ms4Z0XVyH72thlT4lBD+ehoXhohwKBgEtJ | ||
22 | LAPnY9Sv6Vuup/SAf/aIkSqDarMWa3x85pyO4Tl5zpuha3zgGjcdhYFI/ovIDbBp | ||
23 | JvUdBeuvup1PSwS5MP+8pSUxCfBRvkyD4v8VRRvLlgwWYSHvnm/oTmDLtCqDTtvV | ||
24 | +JRq9X3s7BHPYAjrTahGz8lvEGqWIoE/LHkLGEPVAoGAaF3VHuqDfmD9PJUAlsU1 | ||
25 | qxN7yfOd2ve0+66Ghus24DVqUFqwp5f2AxZXYUtSaNUp8fVbqIi+Yq3YDTU2KfId | ||
26 | 5QNA/AiKi4VUNLElsG5DZlbszsE5KNp9fWQoggdQ5LND7AGEKeFERHOVQ7C5sc/C | ||
27 | omIqK5/PsZmaf4OZLyecxJY= | ||
28 | -----END PRIVATE KEY----- | ||