<my-live-documentation-link></my-live-documentation-link>
</div>
- <div class="form-group">
+ <div *ngIf="liveVideo.rtmpUrl" class="form-group">
<label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
<my-input-toggle-hidden inputId="liveVideoRTMPUrl" [value]="liveVideo.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden>
</div>
+ <div *ngIf="liveVideo.rtmpsUrl" class="form-group">
+ <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label>
+ <my-input-toggle-hidden inputId="liveVideoRTMPSUrl" [value]="liveVideo.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden>
+ </div>
+
<div class="form-group">
<label for="liveVideoStreamKey" i18n>Live stream key</label>
<my-input-toggle-hidden inputId="liveVideoStreamKey" [value]="liveVideo.streamKey" [withCopy]="true" [readonly]="true"></my-input-toggle-hidden>
<my-live-documentation-link></my-live-documentation-link>
</div>
- <div class="form-group">
+ <div *ngIf="live.rtmpUrl" class="form-group">
<label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
<my-input-toggle-hidden inputId="liveVideoRTMPUrl" [value]="live.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden>
</div>
+ <div *ngIf="live.rtmpsUrl" class="form-group">
+ <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label>
+ <my-input-toggle-hidden inputId="liveVideoRTMPSUrl" [value]="live.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden>
+ </div>
+
<div class="form-group">
<label for="liveVideoStreamKey" i18n>Live stream key</label>
<my-input-toggle-hidden inputId="liveVideoStreamKey" [value]="live.streamKey" [withCopy]="true" [readonly]="true"></my-input-toggle-hidden>
# Your firewall should accept traffic from this port in TCP if you enable live
rtmp:
+ enabled: true
port: 1935
+ rtmps:
+ enabled: false
+ port: 1936
+ # Absolute path
+ key_file: ''
+ # Absolute path
+ cert_file: ''
+
# Allow to transcode the live streaming in multiple live resolutions
transcoding:
enabled: true
# Your firewall should accept traffic from this port in TCP if you enable live
rtmp:
+ enabled: true
port: 1935
+ rtmps:
+ enabled: false
+ port: 1936
+ key_file: ''
+ cert_file: ''
+
# Allow to transcode the live streaming in multiple live resolutions
transcoding:
enabled: true
.catch(err => logger.error('Cannot update streaming playlist infohashes.', { err }))
LiveManager.Instance.init()
- if (CONFIG.LIVE.ENABLED) LiveManager.Instance.run()
+ if (CONFIG.LIVE.ENABLED) await LiveManager.Instance.run()
// Make server listening
server.listen(port, hostname, async () => {
// ---------------------------------------------------------------------------
async function getLiveTranscodingCommand (options: {
- rtmpUrl: string
+ inputUrl: string
outPath: string
masterPlaylistName: string
availableEncoders: AvailableEncoders
profile: string
}) {
- const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
- const input = rtmpUrl
+ const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
- const command = getFFmpeg(input, 'live')
+ const command = getFFmpeg(inputUrl, 'live')
const varStreamMap: string[] = []
const resolutionFPS = computeFPS(fps, resolution)
const baseEncoderBuilderParams = {
- input,
+ input: inputUrl,
availableEncoders,
profile,
return command
}
-function getLiveMuxingCommand (rtmpUrl: string, outPath: string, masterPlaylistName: string) {
- const command = getFFmpeg(rtmpUrl, 'live')
+function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) {
+ const command = getFFmpeg(inputUrl, 'live')
command.outputOption('-c:v copy')
command.outputOption('-c:a copy')
if (CONFIG.LIVE.ALLOW_REPLAY === true && CONFIG.TRANSCODING.ENABLED === false) {
return 'Live allow replay cannot be enabled if transcoding is not enabled.'
}
+
+ if (CONFIG.LIVE.RTMP.ENABLED === false && CONFIG.LIVE.RTMPS.ENABLED === false) {
+ return 'You must enable at least RTMP or RTMPS'
+ }
+
+ if (CONFIG.LIVE.RTMPS.ENABLED) {
+ if (!CONFIG.LIVE.RTMPS.KEY_FILE) {
+ return 'You must specify a key file to enabled RTMPS'
+ }
+
+ if (!CONFIG.LIVE.RTMPS.CERT_FILE) {
+ return 'You must specify a cert file to enable RTMPS'
+ }
+ }
}
// Object storage
'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
'search.search_index.disable_local_search', 'search.search_index.is_default_search',
'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
+ 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.key_file', 'live.rtmps.cert_file',
'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
RTMP: {
+ get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
get PORT () { return config.get<number>('live.rtmp.port') }
},
+ RTMPS: {
+ get ENABLED () { return config.get<boolean>('live.rtmps.enabled') },
+ get PORT () { return config.get<number>('live.rtmps.port') },
+ get KEY_FILE () { return config.get<string>('live.rtmps.key_file') },
+ get CERT_FILE () { return config.get<string>('live.rtmps.cert_file') }
+ },
+
TRANSCODING: {
get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
get THREADS () { return config.get<number>('live.transcoding.threads') },
WS: '',
HOSTNAME: '',
PORT: 0,
- RTMP_URL: ''
+ RTMP_URL: '',
+ RTMPS_URL: ''
}
// Sortable columns per schema
WEBSERVER.PORT = CONFIG.WEBSERVER.PORT
WEBSERVER.RTMP_URL = 'rtmp://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMP.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH
+ WEBSERVER.RTMPS_URL = 'rtmps://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMPS.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH
}
function updateWebserverConfig () {
+import { readFile } from 'fs-extra'
import { createServer, Server } from 'net'
+import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
import { isTestInstance } from '@server/helpers/core-utils'
import {
computeResolutionsToTranscode,
import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
import { federateVideoIfNeeded } from '../activitypub/videos'
import { JobQueue } from '../job-queue'
-import { PeerTubeSocket } from '../peertube-socket'
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../paths'
+import { PeerTubeSocket } from '../peertube-socket'
import { LiveQuotaStore } from './live-quota-store'
import { LiveSegmentShaStore } from './live-segment-sha-store'
import { cleanupLive } from './live-utils'
gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE,
ping: VIDEO_LIVE.RTMP.PING,
ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT
- },
- transcoding: {
- ffmpeg: 'ffmpeg'
}
}
private readonly watchersPerVideo = new Map<number, number[]>()
private rtmpServer: Server
+ private rtmpsServer: ServerTLS
+
+ private running = false
private constructor () {
}
return this.abortSession(sessionId)
}
- this.handleSession(sessionId, streamPath, splittedPath[2])
+ const session = this.getContext().sessions.get(sessionId)
+
+ this.handleSession(sessionId, session.inputOriginUrl + streamPath, splittedPath[2])
.catch(err => logger.error('Cannot handle sessions.', { err, ...lTags(sessionId) }))
})
})
registerConfigChangedHandler(() => {
- if (!this.rtmpServer && CONFIG.LIVE.ENABLED === true) {
- this.run()
+ if (!this.running && CONFIG.LIVE.ENABLED === true) {
+ this.run().catch(err => logger.error('Cannot run live server.', { err }))
return
}
- if (this.rtmpServer && CONFIG.LIVE.ENABLED === false) {
+ if (this.running && CONFIG.LIVE.ENABLED === false) {
this.stop()
}
})
setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE)
}
- run () {
- logger.info('Running RTMP server on port %d', config.rtmp.port, lTags())
+ async run () {
+ this.running = true
- this.rtmpServer = createServer(socket => {
- const session = new NodeRtmpSession(config, socket)
+ if (CONFIG.LIVE.RTMP.ENABLED) {
+ logger.info('Running RTMP server on port %d', CONFIG.LIVE.RTMP.PORT, lTags())
- session.run()
- })
+ this.rtmpServer = createServer(socket => {
+ const session = new NodeRtmpSession(config, socket)
- this.rtmpServer.on('error', err => {
- logger.error('Cannot run RTMP server.', { err, ...lTags() })
- })
+ session.inputOriginUrl = 'rtmp://127.0.0.1:' + CONFIG.LIVE.RTMP.PORT
+ session.run()
+ })
+
+ this.rtmpServer.on('error', err => {
+ logger.error('Cannot run RTMP server.', { err, ...lTags() })
+ })
+
+ this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT)
+ }
- this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT)
+ if (CONFIG.LIVE.RTMPS.ENABLED) {
+ logger.info('Running RTMPS server on port %d', CONFIG.LIVE.RTMPS.PORT, lTags())
+
+ const [ key, cert ] = await Promise.all([
+ readFile(CONFIG.LIVE.RTMPS.KEY_FILE),
+ readFile(CONFIG.LIVE.RTMPS.CERT_FILE)
+ ])
+ const serverOptions = { key, cert }
+
+ this.rtmpsServer = createServerTLS(serverOptions, socket => {
+ const session = new NodeRtmpSession(config, socket)
+
+ session.inputOriginUrl = 'rtmps://127.0.0.1:' + CONFIG.LIVE.RTMPS.PORT
+ session.run()
+ })
+
+ this.rtmpsServer.on('error', err => {
+ logger.error('Cannot run RTMPS server.', { err, ...lTags() })
+ })
+
+ this.rtmpsServer.listen(CONFIG.LIVE.RTMPS.PORT)
+ }
}
stop () {
+ this.running = false
+
logger.info('Stopping RTMP server.', lTags())
this.rtmpServer.close()
}
}
- private async handleSession (sessionId: string, streamPath: string, streamKey: string) {
+ private async handleSession (sessionId: string, inputUrl: string, streamKey: string) {
const videoLive = await VideoLiveModel.loadByStreamKey(streamKey)
if (!videoLive) {
logger.warn('Unknown live video with stream key %s.', streamKey, lTags(sessionId))
this.videoSessions.set(video.id, sessionId)
- const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath
-
const now = Date.now()
- const probe = await ffprobePromise(rtmpUrl)
+ const probe = await ffprobePromise(inputUrl)
const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([
- getVideoFileResolution(rtmpUrl, probe),
- getVideoFileFPS(rtmpUrl, probe),
- getVideoFileBitrate(rtmpUrl, probe)
+ getVideoFileResolution(inputUrl, probe),
+ getVideoFileFPS(inputUrl, probe),
+ getVideoFileBitrate(inputUrl, probe)
])
logger.info(
'%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)',
- rtmpUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid)
+ inputUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid)
)
const allResolutions = this.buildAllResolutionsToTranscode(resolution)
sessionId,
videoLive,
streamingPlaylist,
- rtmpUrl,
+ inputUrl,
fps,
bitrate,
ratio,
sessionId: string
videoLive: MVideoLiveVideo
streamingPlaylist: MStreamingPlaylistVideo
- rtmpUrl: string
+ inputUrl: string
fps: number
bitrate: number
ratio: number
allResolutions: number[]
}) {
- const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, ratio, rtmpUrl } = options
+ const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, ratio, inputUrl } = options
const videoUUID = videoLive.Video.uuid
const localLTags = lTags(sessionId, videoUUID)
sessionId,
videoLive,
streamingPlaylist,
- rtmpUrl,
+ inputUrl,
bitrate,
ratio,
fps,
private readonly sessionId: string
private readonly videoLive: MVideoLiveVideo
private readonly streamingPlaylist: MStreamingPlaylistVideo
- private readonly rtmpUrl: string
+ private readonly inputUrl: string
private readonly fps: number
private readonly allResolutions: number[]
sessionId: string
videoLive: MVideoLiveVideo
streamingPlaylist: MStreamingPlaylistVideo
- rtmpUrl: string
+ inputUrl: string
fps: number
bitrate: number
ratio: number
this.sessionId = options.sessionId
this.videoLive = options.videoLive
this.streamingPlaylist = options.streamingPlaylist
- this.rtmpUrl = options.rtmpUrl
+ this.inputUrl = options.inputUrl
this.fps = options.fps
this.bitrate = options.bitrate
this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED
? await getLiveTranscodingCommand({
- rtmpUrl: this.rtmpUrl,
+ inputUrl: this.inputUrl,
outPath,
masterPlaylistName: this.streamingPlaylist.playlistFilename,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: CONFIG.LIVE.TRANSCODING.PROFILE
})
- : getLiveMuxingCommand(this.rtmpUrl, outPath, this.streamingPlaylist.playlistFilename)
+ : getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename)
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags)
}
private onFFmpegEnded (outPath: string) {
- logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.rtmpUrl, this.lTags)
+ logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags)
setTimeout(() => {
// Wait latest segments generation, and close watchers
import { LiveVideo, VideoState } from '@shared/models'
import { VideoModel } from './video'
import { VideoBlacklistModel } from './video-blacklist'
+import { CONFIG } from '@server/initializers/config'
@DefaultScope(() => ({
include: [
}
toFormattedJSON (): LiveVideo {
+ let rtmpUrl: string = null
+ let rtmpsUrl: string = null
+
+ // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL
+ if (this.streamKey) {
+ if (CONFIG.LIVE.RTMP.ENABLED) rtmpUrl = WEBSERVER.RTMP_URL
+ if (CONFIG.LIVE.RTMPS.ENABLED) rtmpsUrl = WEBSERVER.RTMPS_URL
+ }
+
return {
- // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL
- rtmpUrl: this.streamKey
- ? WEBSERVER.RTMP_URL
- : null,
+ rtmpUrl,
+ rtmpsUrl,
streamKey: this.streamKey,
permanentLive: this.permanentLive,
import './live-constraints'
import './live-socket-messages'
import './live-permanent'
+import './live-rtmps'
import './live-save-replay'
import './live-views'
import './live'
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import * as chai from 'chai'
+import { VideoPrivacy } from '@shared/models'
+import {
+ buildAbsoluteFixturePath,
+ cleanupTests,
+ createSingleServer,
+ PeerTubeServer,
+ sendRTMPStream,
+ setAccessTokensToServers,
+ setDefaultVideoChannel,
+ stopFfmpeg,
+ testFfmpegStreamError,
+ waitUntilLivePublishedOnAllServers
+} from '../../../../shared/extra-utils'
+
+const expect = chai.expect
+
+describe('Test live RTMPS', function () {
+ let server: PeerTubeServer
+ let rtmpUrl: string
+ let rtmpsUrl: string
+
+ async function createLiveWrapper () {
+ const liveAttributes = {
+ name: 'live',
+ channelId: server.store.channel.id,
+ privacy: VideoPrivacy.PUBLIC,
+ saveReplay: false
+ }
+
+ const { uuid } = await server.live.create({ fields: liveAttributes })
+
+ const live = await server.live.get({ videoId: uuid })
+ const video = await server.videos.get({ id: uuid })
+
+ return Object.assign(video, live)
+ }
+
+ before(async function () {
+ this.timeout(120000)
+
+ server = await createSingleServer(1)
+
+ // Get the access tokens
+ await setAccessTokensToServers([ server ])
+ await setDefaultVideoChannel([ server ])
+
+ await server.config.updateCustomSubConfig({
+ newConfig: {
+ live: {
+ enabled: true,
+ allowReplay: true,
+ transcoding: {
+ enabled: false
+ }
+ }
+ }
+ })
+
+ rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live'
+ rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live'
+ })
+
+ it('Should enable RTMPS endpoint only', async function () {
+ this.timeout(240000)
+
+ await server.kill()
+ await server.run({
+ live: {
+ rtmp: {
+ enabled: false
+ },
+ rtmps: {
+ enabled: true,
+ port: server.rtmpsPort,
+ key_file: buildAbsoluteFixturePath('rtmps.key'),
+ cert_file: buildAbsoluteFixturePath('rtmps.cert')
+ }
+ }
+ })
+
+ {
+ const liveVideo = await createLiveWrapper()
+
+ expect(liveVideo.rtmpUrl).to.not.exist
+ expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
+
+ const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
+ await testFfmpegStreamError(command, true)
+ }
+
+ {
+ const liveVideo = await createLiveWrapper()
+
+ const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
+ await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
+ await stopFfmpeg(command)
+ }
+ })
+
+ it('Should enable both RTMP and RTMPS', async function () {
+ this.timeout(240000)
+
+ await server.kill()
+ await server.run({
+ live: {
+ rtmp: {
+ enabled: true,
+ port: server.rtmpPort
+ },
+ rtmps: {
+ enabled: true,
+ port: server.rtmpsPort,
+ key_file: buildAbsoluteFixturePath('rtmps.key'),
+ cert_file: buildAbsoluteFixturePath('rtmps.cert')
+ }
+ }
+ })
+
+ {
+ const liveVideo = await createLiveWrapper()
+
+ expect(liveVideo.rtmpUrl).to.equal(rtmpUrl)
+ expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
+
+ const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
+ await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
+ await stopFfmpeg(command)
+ }
+
+ {
+ const liveVideo = await createLiveWrapper()
+
+ const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
+ await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
+ await stopFfmpeg(command)
+ }
+ })
+
+ after(async function () {
+ await cleanupTests([ server ])
+ })
+})
--- /dev/null
+-----BEGIN CERTIFICATE-----
+MIIDazCCAlOgAwIBAgIUKNycLAZUs2jFsWUW+zZhBkpLB2wwDQYJKoZIhvcNAQEL
+BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTExMDUxMDA4MzhaFw0yMTEy
+MDUxMDA4MzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
+HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDak20d81KG/9mVLU6Qw/uRniC935yf9Rlp8FVCDxUd
+zLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88WDU33Q8ixU/R0czUGq1AEwIjyN30
+5NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJMNC0Lit9Go9MDVnGFLkgHia68P72T
+ZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfUY0VAEZlxJ/9zjwYHCT0AKaEPH35E
+dUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GWIqoiIOpdjFUBLs80QOM2aNrLmlyP
+JtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uHZKi5yazNAgMBAAGjUzBRMB0GA1Ud
+DgQWBBSSjhRQdWsybNQMLMhkwV+xiP2uoDAfBgNVHSMEGDAWgBSSjhRQdWsybNQM
+LMhkwV+xiP2uoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC8
+rJu3J5sqVKNQaXOmLPd49RM7KG3Y1KPqbQi1lh+sW6aefZ9daeh3JDYGBZGPG/Fi
+IMMP+LhGG0WqDm4ClK00wyNhBuNPEyzvuN/WMRX5djPxO1IZi+KogFwXsn853Ov9
+oV3nxArNNjDu2n92FiB7RTlXRXPIoRo2zEBcLvveGySn9XUazRzlqx6FAxYe2xsw
+U3cZ6/wwU1YsEZa5bwIQk+gkFj3zDsTyEkn2ntcE2NlR+AhCHKa/yAxgPFycAVPX
+2o+wNnc6H4syP98mMGj9hEE3RSJyCPgGBlgi7Swl64G3YygFPJzfLX9YTuxwr/eI
+oitEjF9ljtmdEnf0RdOj
+-----END CERTIFICATE-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDak20d81KG/9mV
+LU6Qw/uRniC935yf9Rlp8FVCDxUdzLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88
+WDU33Q8ixU/R0czUGq1AEwIjyN305NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJM
+NC0Lit9Go9MDVnGFLkgHia68P72TZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfU
+Y0VAEZlxJ/9zjwYHCT0AKaEPH35EdUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GW
+IqoiIOpdjFUBLs80QOM2aNrLmlyPJtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uH
+ZKi5yazNAgMBAAECggEAND7C+UK8+jnTl13CBsZhrnfemaQGexGJ5pGkv2p9gKb7
+Gy/Nooty/OdNWtjdNJ5N22YfSRkXulgZxBHNfrHfOU9yedOtIxHRUZx5iXYs36mH
+02cJeUHN3t1MOnkoWTvIGDH4vZUnP1lXV+Gs1rJ2Fht4h7a04cGjQ/H8C1EtDjqX
+kzH2T/gwo5hdGrxifRTs5wCVoP/iUwNtBI4WrY2rfC6sV+NOICgp0xX0NvGWZ8UT
+K1Ntpl8IxnxmeBd26d+Gbjc9d9fIRDtyXby4YOIlDZxnIiZEI0I452JqGl/jrXaP
+F3Troet4OBj5uH5s374d6ubKq66XogiLMIjEj2tYfQKBgQDtuaOu+y549bFJKVc9
+TCiWSOl/0j2kKKG8UG23zMC//AT13WqZDT5ObfOAuMhy70au/PD84D9RU/+gRVWb
+ptfybD9ugRNC8PkmdT82uYtZpS4+Xw4qyWVRgqQFmjSYz63cLcULVi8kiG8XmG5u
+QGgT/tNv5mxhOMUGSxhClOpLBwKBgQDrYO9UrLs+gDVKbHF4Dh+YJpaLnwwF+TFA
+j3ZbkE0XEeeXp/YDgyClmWwEkteJeNljtreCZ9gMkx3JdR9i8uecUQ2tFDBg3cN0
+BZAex2jFwSb0QbfzHNnE07I+aEIfHHjYXjzABl+1Yt95giKjce0Ke+8Zzahue0+9
+lYcAHemQiwKBgQCs9JAbIdJo3NBUW0iGZ19sH7YKciq4wXsSaC27OLPPugrd2m7Q
+1arMIwCzWT01KdLyQ0MNqBVJFWT49RjYuuWIEauAuVYLMQkEKu+H4Cx7V0syw7Op
++4bEa9jr3op/1zE17PLcUaLQ4JZ6w0Ms4Z0XVyH72thlT4lBD+ehoXhohwKBgEtJ
+LAPnY9Sv6Vuup/SAf/aIkSqDarMWa3x85pyO4Tl5zpuha3zgGjcdhYFI/ovIDbBp
+JvUdBeuvup1PSwS5MP+8pSUxCfBRvkyD4v8VRRvLlgwWYSHvnm/oTmDLtCqDTtvV
++JRq9X3s7BHPYAjrTahGz8lvEGqWIoE/LHkLGEPVAoGAaF3VHuqDfmD9PJUAlsU1
+qxN7yfOd2ve0+66Ghus24DVqUFqwp5f2AxZXYUtSaNUp8fVbqIi+Yq3YDTU2KfId
+5QNA/AiKi4VUNLElsG5DZlbszsE5KNp9fWQoggdQ5LND7AGEKeFERHOVQ7C5sc/C
+omIqK5/PsZmaf4OZLyecxJY=
+-----END PRIVATE KEY-----
port?: number
rtmpPort?: number
+ rtmpsPort?: number
parallel?: boolean
internalServerNumber: number
this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
+ this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937
this.port = 9000 + this.internalServerNumber
this.url = `http://localhost:${this.port}`
export interface LiveVideo {
rtmpUrl: string
+ rtmpsUrl: string
+
streamKey: string
saveReplay: boolean
permanentLive: boolean
properties:
rtmpUrl:
type: string
+ rtmpsUrl:
+ type: string
streamKey:
type: string
description: RTMP stream key to use to stream into this live video