1 import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
2 import { truncate } from 'lodash'
3 import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
4 import { VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models'
5 import { PeerTubeServer } from '../server/server'
7 function sendRTMPStream (options: {
10 fixtureName?: string // default video_short.mp4
11 copyCodecs?: boolean // default false
13 const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
15 const fixture = buildAbsoluteFixturePath(fixtureName)
17 const command = ffmpeg(fixture)
18 command.inputOption('-stream_loop -1')
19 command.inputOption('-re')
22 command.outputOption('-c copy')
24 command.outputOption('-c:v libx264')
25 command.outputOption('-g 120')
26 command.outputOption('-x264-params "no-scenecut=1"')
27 command.outputOption('-r 60')
30 command.outputOption('-f flv')
32 const rtmpUrl = rtmpBaseUrl + '/' + streamKey
33 command.output(rtmpUrl)
35 command.on('error', err => {
36 if (err?.message?.includes('Exiting normally')) return
38 if (process.env.DEBUG) console.error(err)
41 if (process.env.DEBUG) {
42 command.on('stderr', data => console.log(data))
43 command.on('stdout', data => console.log(data))
51 function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
52 return new Promise<void>((res, rej) => {
53 command.on('error', err => {
63 async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
67 await waitFfmpegUntilError(command, 45000)
72 await stopFfmpeg(command)
74 if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
75 if (!shouldHaveError && error) throw error
78 async function stopFfmpeg (command: FfmpegCommand) {
79 command.kill('SIGINT')
84 async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
85 for (const server of servers) {
86 await server.live.waitUntilPublished({ videoId })
90 async function waitUntilLiveWaitingOnAllServers (servers: PeerTubeServer[], videoId: string) {
91 for (const server of servers) {
92 await server.live.waitUntilWaiting({ videoId })
96 async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServer[], videoId: string) {
97 for (const server of servers) {
98 await server.live.waitUntilReplacedByReplay({ videoId })
102 async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) {
103 const include = VideoInclude.BLACKLISTED
104 const privacyOneOf = [ VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.PUBLIC, VideoPrivacy.UNLISTED ]
106 const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include, privacyOneOf })
108 const videoNameSuffix = ` - ${new Date(liveDetails.publishedAt).toLocaleString()}`
109 const truncatedVideoName = truncate(liveDetails.name, {
110 length: 120 - videoNameSuffix.length
112 const toFind = truncatedVideoName + videoNameSuffix
114 return data.find(v => v.name === toFind)
119 waitFfmpegUntilError,
120 testFfmpegStreamError,
123 waitUntilLivePublishedOnAllServers,
124 waitUntilLiveReplacedByReplayOnAllServers,
125 waitUntilLiveWaitingOnAllServers,
127 findExternalSavedVideo