"re-encoder" server side / keyFrameRequestDelay 5s

Hi, Just spent the weekend contemplating migrating a project from Janus to MediaSoup. I read through the docs, and watched the JanusCon presentation. Love how well though out MediaSoup is. Congrats! Great work!
My first task is to create a large few-to-many broadcast that scales beyond a single host. I’ve read through the scalability section, especially the “Red” section https://mediasoup.org/documentation/v3/scalability/
I have 2 question that I am hoping somebody here can answer:

  1. has the server side “re-encoder” been written??? if so, is it available somewhere?
  2. I am already used to having upto 5 seconds of “black screen” by shielding the presenter with a max PLI rate of 5s. This is acceptable for my use case. Does keyFrameRequestDelay work for this?

There is no a “promised reencoder that we will write”. However you can use libmediasoupclient C++ library for that.

Yes.

Hi guys. I will also need to implement such an re encoder. But it this point the documentation really falls short. Leaving you at a point where one does not know how to go further. What is this re encoder? Hiw can it be implemented? I have no freaking idea…

Initially you say you need such a re encoder. Then you go on and ask:

So you need it although you don’t know what it is?!

Do re-check the documentation. It’s very clear IMHO what it is and how you COULD implement such a reencoder.

As far as i understand this re encoder is somewhere beteween the producer and consumer and has to regenerate keyFrames as requested by the client. It is then itself a consumer and produce? What does it has to do with tge c++ lib? Do i have ti write my own video encoder in c++ to get this thing running?

What does it has to do with the c++ lib?
Lots…

You will need to write your own process but you can do so with the help of the given library. Mediasoup documents well but for these purposes they’re left in the developers hands to create what it is they exactly want. There’s no pre-mades outside the demo’s.

The reencoder is or maybe a libmediasoupclient based app that consumes a track from a real client (via webrtcTransport.consume() in mediasoup server), gets the consumer.track and passes it to webrtcTransport.produce() and injects it back in mediasoup for real clients to consume it, so it’s decoding and encoding again. It may even consume VP8 and reencode it using H264 or whatever.

1 Like

Has anyone managed to write up an re-encoder? I don’t understand enough to build one out :slightly_frowning_face:

Would I be correct in assuming that this cannot be written using the nodeJS APIs? I say this because I don’t find a method of extracting the track from a consumer.

Hi Kaplan. How many is “many” in your use case. In general, you can scale to quite a large number of receiving clients before you need a re-encoder. This is dependent on your encodings rtp parameters, goals, and use case, of course. But especially if you’re willing to set keyFrameRequestDelay to 5 seconds, you may not need to re-encode.

No?

No? Many references on using FFMPEG/GStreamer.

As for write-ups there’s a few however your app may vary from others but there’s plent of FFMPEG/GStreamer usage examples to get you started. Process is transcoding, you decode and encode stream.

Assuming I’m not changing encoding formats or any of that, is that really all that I would need to do? Somehow feed a WebRTC-transport-based producer into FFMPEG, get the resulting RTP stream into mediasoup via plainRTP-transport and advertise/use this producer instead of the WebRTC one?

What about the parts where it receives+reacts to a PLI/FIR? Do I get that for free via FFMPEG and plainRTP-transport? Sorry, very new to all of this

What about the parts where it receives+reacts to a PLI/FIR? Do I get that for free via FFMPEG and plainRTP-transport? Sorry, very new to all of this

Mediasoup will handle the PLIs for you.

Last time I looked at the ffmpeg rtp source code, it wasn’t possible to configure it to handle FIRs. (That could have changed, of course.)

It’s definitely possible to wire up RTCP FIR stuff back to the GStreamer encoding pipeline. In theory it’s fairly simple – both GStreamer and Mediasoup just need to be configured to know which RTCP options to enable and need to know the right ports. But figuring out the details might require reading through the rtpsession or rtpbin module source code to figure out (I don’t remember where the RTCP code is). If this project is important to you, you might want to consider hiring Asymptotic, who do GStreamer and WebRTC consulting, have reasonable rights, are very professional, and really, really know their stuff.

1 Like

You tell us, I think you’ll find your answer quickly. :slight_smile:

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
  }
}

In my experience, one of the ways this approach may be brittle is that +genpts and copy don’t play nicely together.

It’s worth testing heavily with packet loss in the incoming stream to surface timestamp and a/v sync issues.

I tried piping the re-encoded stream from one node to another node and then creating a webrtc consumer for the re-encoded producerID on the second node. I end up with a black video that eventually displays a frame before becoming stuck on that frame. :frowning:
If I pipe the original producer, everything works. If I pipe the re-encoded producer, then I get a black screen.

Would really appreciate some help on how to implement a re-encoder using ffmpeg for VP8.
On the client-side, this is what the inbound RTP looks like:

RTCInboundRTPVideoStream_483466091 (inbound-rtp, mid=0, VP8)
Statistics RTCInboundRTPVideoStream_483466091
timestamp	10/16/2022, 1:10:49 PM
ssrc	483466091
kind	video
trackId	DEPRECATED_RTCMediaStreamTrack_receiver_308
transportId	RTCTransport_0_1
codecId	RTCCodec_0_Inbound_101
[codec]	VP8 (101)
mediaType	video
jitter	0.002
packetsLost	2072
trackIdentifier	ba57c64c-c118-4c7c-9e13-01dc639eb419
mid	0
packetsReceived	967059
[packetsReceived/s]	222.0064575166266
bytesReceived	1351309984
[bytesReceived_in_bits/s]	2502400.787498492
headerBytesReceived	30945888
[headerBytesReceived_in_bits/s]	56833.65312425641
lastPacketReceivedTimestamp	1665940249506
[lastPacketReceivedTimestamp]	10/16/2022, 1:10:49 PM
jitterBufferDelay	27.622
[jitterBufferDelay/jitterBufferEmittedCount_in_ms]	0
jitterBufferEmittedCount	1639
framesReceived	1634
[framesReceived/s]	0
[framesReceived-framesDecoded-framesDropped]	-6
frameWidth	1280
frameHeight	960
framesDecoded	1640
[framesDecoded/s]	0
keyFramesDecoded	23
[keyFramesDecoded/s]	0
framesDropped	0
totalDecodeTime	9.156385
[totalDecodeTime/framesDecoded_in_ms]	0
totalProcessingDelay	123.34863899999999
[totalProcessingDelay/framesDecoded_in_ms]	0
totalAssemblyTime*	2.955362
[totalAssemblyTime*/framesAssembledFromMultiplePackets*_in_ms]	0
framesAssembledFromMultiplePackets*	1640
totalInterFrameDelay	3877.6129999999985
[totalInterFrameDelay/framesDecoded_in_ms]	0
totalSquaredInterFrameDelay	923334.1427090003
[interFrameDelayStDev_in_ms]	0
decoderImplementation	libvpx
firCount	0
pliCount	1850
nackCount	83744
qpSum	54086
[qpSum/framesDecoded]	0
minPlayoutDelay*	0