aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/ffmpeg-utils.ts13
-rw-r--r--server/initializers/checker-after-init.ts14
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/config.ts8
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/live/live-manager.ts92
-rw-r--r--server/lib/live/shared/muxing-session.ts12
-rw-r--r--server/models/video/video-live.ts16
-rw-r--r--server/tests/api/live/index.ts1
-rw-r--r--server/tests/api/live/live-rtmps.ts146
-rw-r--r--server/tests/fixtures/rtmps.cert21
-rw-r--r--server/tests/fixtures/rtmps.key28
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
221async function getLiveTranscodingCommand (options: { 221async 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
330function getLiveMuxingCommand (rtmpUrl: string, outPath: string, masterPlaylistName: string) { 329function 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
1003function updateWebserverConfig () { 1005function 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
2import { readFile } from 'fs-extra'
2import { createServer, Server } from 'net' 3import { createServer, Server } from 'net'
4import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
3import { isTestInstance } from '@server/helpers/core-utils' 5import { isTestInstance } from '@server/helpers/core-utils'
4import { 6import {
5 computeResolutionsToTranscode, 7 computeResolutionsToTranscode,
@@ -19,8 +21,8 @@ import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/
19import { VideoState, VideoStreamingPlaylistType } from '@shared/models' 21import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
20import { federateVideoIfNeeded } from '../activitypub/videos' 22import { federateVideoIfNeeded } from '../activitypub/videos'
21import { JobQueue } from '../job-queue' 23import { JobQueue } from '../job-queue'
22import { PeerTubeSocket } from '../peertube-socket'
23import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths' 24import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths'
25import { PeerTubeSocket } from '../peertube-socket'
24import { LiveQuotaStore } from './live-quota-store' 26import { LiveQuotaStore } from './live-quota-store'
25import { LiveSegmentShaStore } from './live-segment-sha-store' 27import { LiveSegmentShaStore } from './live-segment-sha-store'
26import { cleanupLive } from './live-utils' 28import { 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'
5import { LiveVideo, VideoState } from '@shared/models' 5import { LiveVideo, VideoState } from '@shared/models'
6import { VideoModel } from './video' 6import { VideoModel } from './video'
7import { VideoBlacklistModel } from './video-blacklist' 7import { VideoBlacklistModel } from './video-blacklist'
8import { 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 @@
1import './live-constraints' 1import './live-constraints'
2import './live-socket-messages' 2import './live-socket-messages'
3import './live-permanent' 3import './live-permanent'
4import './live-rtmps'
4import './live-save-replay' 5import './live-save-replay'
5import './live-views' 6import './live-views'
6import './live' 7import './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
3import 'mocha'
4import * as chai from 'chai'
5import { VideoPrivacy } from '@shared/models'
6import {
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
19const expect = chai.expect
20
21describe('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-----
2MIIDazCCAlOgAwIBAgIUKNycLAZUs2jFsWUW+zZhBkpLB2wwDQYJKoZIhvcNAQEL
3BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTExMDUxMDA4MzhaFw0yMTEy
5MDUxMDA4MzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
6HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
7AQUAA4IBDwAwggEKAoIBAQDak20d81KG/9mVLU6Qw/uRniC935yf9Rlp8FVCDxUd
8zLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88WDU33Q8ixU/R0czUGq1AEwIjyN30
95NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJMNC0Lit9Go9MDVnGFLkgHia68P72T
10ZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfUY0VAEZlxJ/9zjwYHCT0AKaEPH35E
11dUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GWIqoiIOpdjFUBLs80QOM2aNrLmlyP
12JtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uHZKi5yazNAgMBAAGjUzBRMB0GA1Ud
13DgQWBBSSjhRQdWsybNQMLMhkwV+xiP2uoDAfBgNVHSMEGDAWgBSSjhRQdWsybNQM
14LMhkwV+xiP2uoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC8
15rJu3J5sqVKNQaXOmLPd49RM7KG3Y1KPqbQi1lh+sW6aefZ9daeh3JDYGBZGPG/Fi
16IMMP+LhGG0WqDm4ClK00wyNhBuNPEyzvuN/WMRX5djPxO1IZi+KogFwXsn853Ov9
17oV3nxArNNjDu2n92FiB7RTlXRXPIoRo2zEBcLvveGySn9XUazRzlqx6FAxYe2xsw
18U3cZ6/wwU1YsEZa5bwIQk+gkFj3zDsTyEkn2ntcE2NlR+AhCHKa/yAxgPFycAVPX
192o+wNnc6H4syP98mMGj9hEE3RSJyCPgGBlgi7Swl64G3YygFPJzfLX9YTuxwr/eI
20oitEjF9ljtmdEnf0RdOj
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-----
2MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDak20d81KG/9mV
3LU6Qw/uRniC935yf9Rlp8FVCDxUdzLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88
4WDU33Q8ixU/R0czUGq1AEwIjyN305NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJM
5NC0Lit9Go9MDVnGFLkgHia68P72TZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfU
6Y0VAEZlxJ/9zjwYHCT0AKaEPH35EdUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GW
7IqoiIOpdjFUBLs80QOM2aNrLmlyPJtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uH
8ZKi5yazNAgMBAAECggEAND7C+UK8+jnTl13CBsZhrnfemaQGexGJ5pGkv2p9gKb7
9Gy/Nooty/OdNWtjdNJ5N22YfSRkXulgZxBHNfrHfOU9yedOtIxHRUZx5iXYs36mH
1002cJeUHN3t1MOnkoWTvIGDH4vZUnP1lXV+Gs1rJ2Fht4h7a04cGjQ/H8C1EtDjqX
11kzH2T/gwo5hdGrxifRTs5wCVoP/iUwNtBI4WrY2rfC6sV+NOICgp0xX0NvGWZ8UT
12K1Ntpl8IxnxmeBd26d+Gbjc9d9fIRDtyXby4YOIlDZxnIiZEI0I452JqGl/jrXaP
13F3Troet4OBj5uH5s374d6ubKq66XogiLMIjEj2tYfQKBgQDtuaOu+y549bFJKVc9
14TCiWSOl/0j2kKKG8UG23zMC//AT13WqZDT5ObfOAuMhy70au/PD84D9RU/+gRVWb
15ptfybD9ugRNC8PkmdT82uYtZpS4+Xw4qyWVRgqQFmjSYz63cLcULVi8kiG8XmG5u
16QGgT/tNv5mxhOMUGSxhClOpLBwKBgQDrYO9UrLs+gDVKbHF4Dh+YJpaLnwwF+TFA
17j3ZbkE0XEeeXp/YDgyClmWwEkteJeNljtreCZ9gMkx3JdR9i8uecUQ2tFDBg3cN0
18BZAex2jFwSb0QbfzHNnE07I+aEIfHHjYXjzABl+1Yt95giKjce0Ke+8Zzahue0+9
19lYcAHemQiwKBgQCs9JAbIdJo3NBUW0iGZ19sH7YKciq4wXsSaC27OLPPugrd2m7Q
201arMIwCzWT01KdLyQ0MNqBVJFWT49RjYuuWIEauAuVYLMQkEKu+H4Cx7V0syw7Op
21+4bEa9jr3op/1zE17PLcUaLQ4JZ6w0Ms4Z0XVyH72thlT4lBD+ehoXhohwKBgEtJ
22LAPnY9Sv6Vuup/SAf/aIkSqDarMWa3x85pyO4Tl5zpuha3zgGjcdhYFI/ovIDbBp
23JvUdBeuvup1PSwS5MP+8pSUxCfBRvkyD4v8VRRvLlgwWYSHvnm/oTmDLtCqDTtvV
24+JRq9X3s7BHPYAjrTahGz8lvEGqWIoE/LHkLGEPVAoGAaF3VHuqDfmD9PJUAlsU1
25qxN7yfOd2ve0+66Ghus24DVqUFqwp5f2AxZXYUtSaNUp8fVbqIi+Yq3YDTU2KfId
265QNA/AiKi4VUNLElsG5DZlbszsE5KNp9fWQoggdQ5LND7AGEKeFERHOVQ7C5sc/C
27omIqK5/PsZmaf4OZLyecxJY=
28-----END PRIVATE KEY-----