PlainTransport + Consumer not sending any VP8 RTP packets to external process

I have a Mediasoup 3.12 backend that should mirror a VP8 video Producer into a PlainTransport and then pipe that RTP into FFmpeg (or any external tool) to grab a single JPEG frame. But no matter what I try, FFmpeg never sees a single packet—so it feels like Mediasoup isn’t actually sending my stream out of the PlainTransport.


Environment

  • Mediasoup v3.12 on Node.js (Windows)
  • FFmpeg 7.1.1 (MSYS2 build)

code

const { spawn } = require('child_process');


const transport = await router.createPlainTransport({
  listenIp: '127.0.0.1',
  rtcpMux:  true,
  comedia:  false
});
console.log('[POC] PlainTransport tuple:', transport.tuple);


await transport.connect({
  ip:   transport.tuple.localIp,
  port: transport.tuple.localPort
});
console.log('[POC] Transport connected →', transport.tuple.localPort);


const consumer = await transport.consume({
  producerId,
  rtpCapabilities,
  paused: false
});
await consumer.resume();
console.log('[POC] Consumer resumed, packets now streaming');


const ffmpeg = spawn('ffmpeg', [
  '-protocol_whitelist', 'file,crypto,data,udp,rtp',
  '-probesize',         '5000000',
  '-analyzeduration',   '5000000',
  '-f',                 'rtp',
  '-i',                 `rtp://127.0.0.1:${transport.tuple.localPort}`,
  '-frames:v',          '1',
  '-q:v',               '2',
  'snapshot.jpg'
]);
ffmpeg.stderr.on('data', d => console.error('[FFmpeg]', d.toString()));
ffmpeg.on('exit', code => console.log('[POC] FFmpeg exited with', code));

Logs

[POC] PlainTransport tuple: { localIp: '127.0.0.1', localPort: 5004, … }
[POC] Transport connected → 5004
[POC] Consumer resumed, packets now streaming
[
  {
    "bitrate": 923840,
    "byteCount": 7386470,
    "firCount": 0,
    "fractionLost": 0,
    "kind": "video",
    "mimeType": "video/VP8",
    "nackCount": 0,
    "nackPacketCount": 0,
    "packetCount": 6463,
    "packetsDiscarded": 0,
    "packetsLost": 0,
    "packetsRepaired": 0,
    "packetsRetransmitted": 0,
    "pliCount": 0,
    "rtxPacketsDiscarded": 0,
    "rtxSsrc": 201200048,
    "score": 10,
    "ssrc": 201200047,
    "timestamp": 1601779499,
    "type": "outbound-rtp"
  },
  {
    "bitrate": 917466,
    "byteCount": 7334770,
    "firCount": 0,
    "fractionLost": 0,
    "jitter": 7,
    "kind": "video",
    "mimeType": "video/VP8",
    "nackCount": 0,
    "nackPacketCount": 0,
    "packetCount": 6463,
    "packetsDiscarded": 0,
    "packetsLost": 0,
    "packetsRepaired": 0,
    "packetsRetransmitted": 4,
    "pliCount": 0,
    "roundTripTime": 1.2664794921875,
    "rtxPacketsDiscarded": 0,
    "rtxSsrc": 1621317588,
    "score": 10,
    "ssrc": 418701061,
    "timestamp": 1601779499,
    "type": "inbound-rtp"
  }
]

[FFmpeg] ffmpeg version 7.1.1-full_build-www.gyan.dev …   // Banner only, then hangs
[POC] FFmpeg exited with <non-zero>  (or never exits)

no frame output, and FFmpeg just blocks waiting for input.


What I’ve already tried

  • comedia on/off — both modes, always the same
  • explicit consumer.resume() after paused=true
  • writing an SDP file with a=framesize & a=framerate
  • passing -video_size or -framesize on the FFmpeg CLI
  • spawning FFmpeg before vs. after transport.connect() and consumer.resume()
  • huge -probesize/-analyzeduration to force codec detection

No matter the combination, FFmpeg never receives a single packet.


My Questions

  1. Is there a step in the PlainTransport lifecycle I’m missing?
  2. Do I need to wire RTCP explicitly (e.g. a separate rtcpPort) even with rtcpMux: true?
  3. How can I verify that Mediasoup is actually pushing RTP out of the transport?
  4. Any minimal working example of sending VP8 RTP from a Mediasoup consumer into FFmpeg (or a raw UDP listener) would be hugely appreciated.

Thanks in advance for any guidance—right now it feels like Mediasoup simply isn’t sending my stream out, and I can’t figure out why.

This is the only thing you should focus first. Enable periodic stats on the mediasoup Consumer and on its PlainTransport and check sent bitrate/packets, etc. Also enable “rtp” in logTags in the Worker and ensure your DEBUG env includes mediasoup* or *.

However… what is this?

await transport.connect({
  ip:   transport.tuple.localIp,
  port: transport.tuple.localPort
});

Are you telling the mediasoup PlainTransport to connect to its own local IP:port instead of calling connect() with the IP and port where your ffmpeg is listening?

Oh! Thanks for the clarification. I was confused about IP & PORT for connect even after reading docs. Going to try this, hope I will get video on other side and enabling the logs as you suggested.

Finally, After Carefully reading docs again, I find below line useful -

Check your mediasoup router.rtpCapabilities and create a subset of them with the RTP capabilities supported by your external endpoint. It's critical that you keep the same codec preferredPayloadType values and RTP header extension preferredId values.

on THIS documentation.
Below are the main changes that I did to previous code -

  • Non-comedia PlainTransport (rtcpMux: true, comedia: false)
  • Explicit transport.connect({ ip: '127.0.0.1', port: YOUR_PORT }) pointing at VLC’s listen port
  • Auto-generate a tiny SDP with the negotiated payloadType and clockRate
  • Open that SDP in VLC (or via ffplay -i video.sdp)

Below is the full code -

const fs        = require('fs');
const path      = require('path');
const { spawn } = require('child_process');

(async () => {
  try {
    // 1) Create a non-comedia PlainTransport
    const transport = await router.createPlainTransport({
      listenIp : '127.0.0.1',
      rtcpMux  : true,
      comedia  : false
    });
    console.log('[POC] PlainTransport tuple:', transport.tuple);

    // 2) Start your external listener on a fixed port, e.g. 5004
    //    (no need to bind here, VLC/ffplay will do it)
    const port = 5004;

    // 3) Tell mediasoup to push RTP to that listener
    await transport.connect({ ip: '127.0.0.1', port });
    console.log(`[POC] Transport sending RTP → 127.0.0.1:${port}`);

    // 4) Create and resume the consumer
    const consumer = await transport.consume({
      producerId:      producer.id,
      rtpCapabilities: router.rtpCapabilities,
      paused:          false
    });
    await consumer.resume();
    console.log('[POC] Consumer resumed');

    // 5) Grab the negotiated VP8 parameters
    const codec = consumer.rtpParameters.codecs.find(c => c.mimeType.toLowerCase() === 'video/vp8');
    const payloadType = codec.payloadType;          // e.g. 96
    const clockRate   = codec.clockRate;            // e.g. 90000

    // 6) Build a simple SDP for ffplay/VLC
    const sdp = [
      'v=0',
      'o=- 0 0 IN IP4 127.0.0.1',
      's=mediasoup-stream',
      'c=IN IP4 127.0.0.1',
      't=0 0',
      `m=video ${port} RTP/AVP ${payloadType}`,
      `a=rtpmap:${payloadType} VP8/${clockRate}`
    ].join('\r\n') + '\r\n';

    const sdpPath = path.join(process.cwd(), 'video.sdp');
    fs.writeFileSync(sdpPath, sdp);
    console.log('[POC] Wrote SDP to', sdpPath);
    console.log(sdp);

    console.log('TO Play, in VLC: Media → Open File… → select video.sdp\n');

  } catch (err) {
    console.error('FATAL ERROR:', err);
  }
})();

Thanks @ibc for the directions.