Support RTMPS
This commit is contained in:
parent
8dd754c767
commit
df1db951c5
|
@ -214,11 +214,16 @@
|
|||
<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>
|
||||
|
|
|
@ -15,11 +15,16 @@
|
|||
<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>
|
||||
|
|
|
@ -370,8 +370,17 @@ live:
|
|||
|
||||
# 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
|
||||
|
|
|
@ -380,8 +380,15 @@ live:
|
|||
|
||||
# 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
|
||||
|
|
|
@ -306,7 +306,7 @@ async function startApplication () {
|
|||
.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 () => {
|
||||
|
|
|
@ -219,7 +219,7 @@ async function transcode (options: TranscodeOptions) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getLiveTranscodingCommand (options: {
|
||||
rtmpUrl: string
|
||||
inputUrl: string
|
||||
|
||||
outPath: string
|
||||
masterPlaylistName: string
|
||||
|
@ -234,10 +234,9 @@ async function getLiveTranscodingCommand (options: {
|
|||
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[] = []
|
||||
|
||||
|
@ -259,7 +258,7 @@ async function getLiveTranscodingCommand (options: {
|
|||
const resolutionFPS = computeFPS(fps, resolution)
|
||||
|
||||
const baseEncoderBuilderParams = {
|
||||
input,
|
||||
input: inputUrl,
|
||||
|
||||
availableEncoders,
|
||||
profile,
|
||||
|
@ -327,8 +326,8 @@ async function getLiveTranscodingCommand (options: {
|
|||
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')
|
||||
|
|
|
@ -151,6 +151,20 @@ function checkConfig () {
|
|||
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
|
||||
|
|
|
@ -47,6 +47,7 @@ function checkMissedConfig () {
|
|||
'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',
|
||||
|
|
|
@ -271,9 +271,17 @@ const CONFIG = {
|
|||
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') },
|
||||
|
|
|
@ -52,7 +52,8 @@ const WEBSERVER = {
|
|||
WS: '',
|
||||
HOSTNAME: '',
|
||||
PORT: 0,
|
||||
RTMP_URL: ''
|
||||
RTMP_URL: '',
|
||||
RTMPS_URL: ''
|
||||
}
|
||||
|
||||
// Sortable columns per schema
|
||||
|
@ -998,6 +999,7 @@ function updateWebserverUrls () {
|
|||
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 () {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
|
||||
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,
|
||||
|
@ -19,8 +21,8 @@ import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/
|
|||
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'
|
||||
|
@ -40,9 +42,6 @@ const config = {
|
|||
gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE,
|
||||
ping: VIDEO_LIVE.RTMP.PING,
|
||||
ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT
|
||||
},
|
||||
transcoding: {
|
||||
ffmpeg: 'ffmpeg'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +57,9 @@ class LiveManager {
|
|||
private readonly watchersPerVideo = new Map<number, number[]>()
|
||||
|
||||
private rtmpServer: Server
|
||||
private rtmpsServer: ServerTLS
|
||||
|
||||
private running = false
|
||||
|
||||
private constructor () {
|
||||
}
|
||||
|
@ -73,7 +75,9 @@ class LiveManager {
|
|||
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) }))
|
||||
})
|
||||
|
||||
|
@ -82,12 +86,12 @@ class LiveManager {
|
|||
})
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
@ -99,23 +103,53 @@ class LiveManager {
|
|||
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.listen(CONFIG.LIVE.RTMP.PORT)
|
||||
this.rtmpServer.on('error', err => {
|
||||
logger.error('Cannot run RTMP server.', { err, ...lTags() })
|
||||
})
|
||||
|
||||
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()
|
||||
|
@ -174,7 +208,7 @@ class LiveManager {
|
|||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
@ -197,20 +231,18 @@ class LiveManager {
|
|||
|
||||
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)
|
||||
|
@ -226,7 +258,7 @@ class LiveManager {
|
|||
sessionId,
|
||||
videoLive,
|
||||
streamingPlaylist,
|
||||
rtmpUrl,
|
||||
inputUrl,
|
||||
fps,
|
||||
bitrate,
|
||||
ratio,
|
||||
|
@ -238,13 +270,13 @@ class LiveManager {
|
|||
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)
|
||||
|
||||
|
@ -257,7 +289,7 @@ class LiveManager {
|
|||
sessionId,
|
||||
videoLive,
|
||||
streamingPlaylist,
|
||||
rtmpUrl,
|
||||
inputUrl,
|
||||
bitrate,
|
||||
ratio,
|
||||
fps,
|
||||
|
|
|
@ -52,7 +52,7 @@ class MuxingSession extends EventEmitter {
|
|||
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[]
|
||||
|
||||
|
@ -84,7 +84,7 @@ class MuxingSession extends EventEmitter {
|
|||
sessionId: string
|
||||
videoLive: MVideoLiveVideo
|
||||
streamingPlaylist: MStreamingPlaylistVideo
|
||||
rtmpUrl: string
|
||||
inputUrl: string
|
||||
fps: number
|
||||
bitrate: number
|
||||
ratio: number
|
||||
|
@ -97,7 +97,7 @@ class MuxingSession extends EventEmitter {
|
|||
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
|
||||
|
@ -120,7 +120,7 @@ class MuxingSession extends EventEmitter {
|
|||
|
||||
this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED
|
||||
? await getLiveTranscodingCommand({
|
||||
rtmpUrl: this.rtmpUrl,
|
||||
inputUrl: this.inputUrl,
|
||||
|
||||
outPath,
|
||||
masterPlaylistName: this.streamingPlaylist.playlistFilename,
|
||||
|
@ -133,7 +133,7 @@ class MuxingSession extends EventEmitter {
|
|||
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)
|
||||
|
||||
|
@ -173,7 +173,7 @@ class MuxingSession extends EventEmitter {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AttributesOnly } from '@shared/core-utils'
|
|||
import { LiveVideo, VideoState } from '@shared/models'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoBlacklistModel } from './video-blacklist'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
|
||||
@DefaultScope(() => ({
|
||||
include: [
|
||||
|
@ -97,11 +98,18 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './live-constraints'
|
||||
import './live-socket-messages'
|
||||
import './live-permanent'
|
||||
import './live-rtmps'
|
||||
import './live-save-replay'
|
||||
import './live-views'
|
||||
import './live'
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/* 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 ])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
-----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-----
|
|
@ -0,0 +1,28 @@
|
|||
-----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-----
|
|
@ -56,6 +56,7 @@ export class PeerTubeServer {
|
|||
port?: number
|
||||
|
||||
rtmpPort?: number
|
||||
rtmpsPort?: number
|
||||
|
||||
parallel?: boolean
|
||||
internalServerNumber: number
|
||||
|
@ -154,6 +155,7 @@ export class PeerTubeServer {
|
|||
|
||||
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}`
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface LiveVideo {
|
||||
rtmpUrl: string
|
||||
rtmpsUrl: string
|
||||
|
||||
streamKey: string
|
||||
saveReplay: boolean
|
||||
permanentLive: boolean
|
||||
|
|
|
@ -7258,6 +7258,8 @@ components:
|
|||
properties:
|
||||
rtmpUrl:
|
||||
type: string
|
||||
rtmpsUrl:
|
||||
type: string
|
||||
streamKey:
|
||||
type: string
|
||||
description: RTMP stream key to use to stream into this live video
|
||||
|
|
Loading…
Reference in New Issue