I’m sharing this here in case someone finds this useful.
It’d be very helpful if someone can provide feedback on whether what I’m doing even makes any sense… I definitely lost track of all of the ports more than once.
Note, if this even works, this is most likely brittle and probably only handles VP8 and/or H264. Let me know if I should put this up elsewhere so folks can contribute to making it better.
ReEncoder.js
import execa from 'execa'
import { getPort, releasePort } from './utils'
export default class ReEncoder {
/**
*
* @param {import('mediasoup').types.Producer} producer
* @param {import('mediasoup').types.Router} router
*/
constructor (producer, router) {
/** @type {import('mediasoup').types.Producer} */
this.producer = producer
/** @type {import('mediasoup').types.Router} */
this.router = router
/** @type {import('mediasoup').types.PlainTransport} */
this.plainRTPConsumerTransport = undefined
/** @type {import('mediasoup').types.PlainTransport} */
this.plainRTPProducerTransport = undefined
/** @type {import('mediasoup').types.Consumer} */
this.consumer = undefined
/** @type {number} */
this.remoteRTPPort = undefined
/** @type {number} */
this.remoteRTCPPort = undefined
/** @type {number} */
this.localRTCPPort = undefined
/** @type {execa.ExecaChildProcess<string>} */
this.process = undefined
}
async start () {
this.producer.observer.on('close', async () => {
this.plainRTPConsumerTransport?.close()
this.plainRTPProducerTransport?.close()
})
this.plainRTPConsumerTransport = await router.createPlainTransport({
listenIp: '127.0.0.1',
comedia: false,
rtcpMux: false,
appData: {
reencoder: true,
forProducer: this.producer.id
}
})
const remoteRTPPort = this.remoteRTPPort = await getPort()
const remoteRTCPPort = this.remoteRTCPPort = await getPort()
await this.plainRTPConsumerTransport.connect({
ip: '127.0.0.1',
port: remoteRTPPort,
rtcpPort: remoteRTCPPort
})
this.localRTCPPort = this.plainRTPConsumerTransport.tuple.localPort
this.plainRTPConsumerTransport.observer.on('close', () => {
releasePort(remoteRTPPort)
releasePort(remoteRTCPPort)
})
const codecs = []
const routerCodec = this.router.rtpCapabilities.codecs.find(x => x.kind === this.producer.kind)
codecs.push(routerCodec)
this.consumer = await this.plainRTPConsumerTransport.consume({
producerId: this.producer.id,
rtpCapabilities: this.router.rtpCapabilities,
paused: false,
appData: {
forProducer: this.producer.id,
reencoder: true
}
})
this.consumer.observer.on('close', async () => {
this.process?.kill('SIGKILL')
})
this.plainRTPProducerTransport = await router.createPlainTransport({
listenIp: '127.0.0.1',
rtcpMux: false,
comedia: true
})
await this.createReEncoder()
// await this.consumer.resume()
this.reEncodedProducer = await this.plainRTPProducerTransport.produce({
kind: this.producer.kind,
rtpParameters: this.consumer.rtpParameters,
paused: false,
appData: {
...this.producer.appData,
forProducer: this.producer.id,
reencoder: true,
pid: this.process.pid
}
})
}
async createReEncoder () {
// Get the SDP
const sdp = createSDP(this.consumer, this.remoteRTPPort, this.remoteRTCPPort)
// Now start the ffmpeg process
const cmdlineArray = [
'ffmpeg',
'-y',
'-hide_banner',
'-loglevel',
'error',
'-protocol_whitelist',
'file,pipe,udp,rtp',
'-fflags',
'+genpts',
'-f',
'sdp',
'-i',
'pipe:0'
]
const ssrc = this.consumer.rtpParameters.encodings[0].ssrc
const payloadType = this.consumer.rtpParameters.codecs[0].payloadType
let selectChar
switch (this.producer.kind) {
case 'video':
cmdlineArray.push(...[
'-map',
'0:v:0',
'-c:v',
'copy'
])
selectChar = 'v'
break
case 'audio':
cmdlineArray.push(...[
'-map',
'0:a:0',
'-strict',
'-2',
'-c:a',
'copy'
])
selectChar = 'a'
break
}
const {
plainRTPProducerTransport: {
tuple: {
localIp: destIP,
localPort: destPort
},
rtcpTuple
}
} = this
const { localPort: destRTCPPort } = rtcpTuple
cmdlineArray.push(...[
'-f',
'tee',
`[select=${selectChar}:f=rtp:ssrc=${ssrc}:payload_type=${payloadType}]rtp://${destIP}:${destPort}?rtcpport=${destRTCPPort}`
])
const cmdline = cmdlineArray.join(' ')
this.process = execa.command(cmdline, {
input: sdp
})
}
}
/**
*
* @param {import('mediasoup').types.Consumer} consumer
* @param {number} rtpPort
* @param {number} rtcpPort
* @returns
*/
export function createSDP (consumer, rtpPort, rtcpPort) {
const { rtpParameters, kind } = consumer
const codecInfo = getCodecInfoFromRTPParameters(kind, rtpParameters)
const sdp = [
'v=0',
'o=- 0 0 IN IP4 127.0.0.1',
's=FFmpeg',
'c=IN IP4 127.0.0.1',
't=0 0'
]
switch (kind) {
case 'video':
sdp.push(...[
`m=video ${rtpPort} RTP/AVP ${codecInfo.payloadType}`,
`a=rtpmap:${codecInfo.payloadType} ${codecInfo.codecName}/${codecInfo.clockRate}`
])
break
case 'audio':
sdp.push(...[
`m=audio ${rtpPort} RTP/AVP ${codecInfo.payloadType}`,
`a=rtpmap:${codecInfo.payloadType} ${codecInfo.codecName}/${codecInfo.clockRate}/${codecInfo.channels}`
])
break
}
sdp.push('a=sendonly')
sdp.push('')
return sdp.join('\n')
}
/**
*
* @param {'video' | 'audio'} kind
* @param {import('mediasoup').types.RtpParameters} rtpParameters
* @returns
*/
export function getCodecInfoFromRTPParameters (kind, rtpParameters) {
return {
payloadType: rtpParameters.codecs[0].payloadType,
codecName: rtpParameters.codecs[0].mimeType.replace(`${kind}/`, ''),
clockRate: rtpParameters.codecs[0].clockRate,
channels: kind === 'audio' ? rtpParameters.codecs[0].channels : undefined
}
}