Search code examples
javascriptnode.jswebrtcsdprtcp

Unknown reason of receiving empty audio stream using `wrtc` RTCAudioSink


I work on POC project that should receive a remote audio stream and write it in to the file. I can subscribe on the data event using wrtc RTCAudioSink but each recieved chunk is:

ArrayBuffer { [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 220 more bytes>, byteLength: 320 }

And the result file is just a mp3 file of silence.

I'm not sure is that me doing something wrong or he library that I'm using cause this trouble.

Here is the backend code snippet:

const path = require('path');
const io = require('socket.io');
const {
  nonstandard: {
    RTCAudioSink,
    RTCVideoSink
  },
  RTCPeerConnection,
  RTCSessionDescription,
  RTCIceCandidate
} = require('wrtc');

const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
const ffmpeg = require('fluent-ffmpeg');
ffmpeg.setFfmpegPath(ffmpegPath);
const { PassThrough } = require('stream');
const { StreamInput } = require('fluent-ffmpeg-multistream');

const clientHost = require('../config/connections').CLIENT_URL[process.env.NODE_ENV];

let audioSink;
let videoSink;
let audioStream;

const outputAudioPath = path.join(__dirname, 'out-audio.mp3');

const beforeOffer = () => {
  const peerConnection = new RTCPeerConnection({
    sdpSemantic: 'unified-plan'
  });

  return peerConnection;
};

module.exports = (http) => {
  const socketIO = io(http, {
    allowEIO3: true,
    cors: {
      origin: clientHost,
      methods: ["GET", "POST"],
      transport: ['websocket'],
      credentials: true
    }
  });

  socketIO.on('connection', (socket) => {
    let pc;

    socket.on('chatConnected', ({ roomId, senderId }) => {
      console.log({ roomId, senderId })
      socket.join(roomId);

      pc = beforeOffer();

      pc.onicecandidate = (event) => {
        if (!event.candidate) {
          return null;
        }

        socket.to(roomId).emit('newICECandidate', {
          roomId,
          senderId,
          candidate: event.candidate
        })
      };

      pc.ontrack = (event) => {
        if (event.track.kind !== 'audio') {
          return null;
        }

        audioStream = new PassThrough();
        audioSink = new RTCAudioSink(event.track);
        console.log('id ', event.track.id)
        console.log('label ', event.track.label)
        console.log('enabled ', event.track.enabled)
        console.log('muted ', event.track.muted)
        console.log('readyState', event.track.readyState)
        audioSink.addEventListener('data', ({ samples: { buffer } }) => {
          audioStream.write(Buffer.from(buffer));
        });

        ffmpeg()
          .addInput((new StreamInput(audioStream)).url)
          .addInputOptions([
            '-f s16le',
            '-ar 48k',
            '-ac 1',
          ])
          .on('start', () => {
            console.log('Start recording')
          })
          .on('end', () => {
            console.log('Stop recording')
          })
          .output(outputAudioPath)
          .run();
      };
    });

    // socket.on('participantConnected', (params) => {
    //   socket.to(params.roomId).emit('participantConnected', params);
    // });

    socket.on('participantDisconnected', (params) => {
      audioSink && audioSink.stop();
      audioSink && audioSink.removeEventListener('data');

      audioStream && audioStream.end();

      // socket.to(params.roomId).emit('participantDisconnected', params.senderId);
    });

    socket.on('message', (params) => {
      socket.to(params.roomId).emit('message', params);
    });

    socket.on('videoChatOffer', async (params) => {
      await pc.setRemoteDescription(new RTCSessionDescription(params.sdp));

      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);

      socket.to(params.roomId).emit('videoChatAnswer', { ...params, sdp: pc.localDescription });
      // socket.to(params.roomId).emit('videoChatOffer', params);
    });

    // socket.on('videoChatAnswer', (params) => {
    //  socket.to(params.roomId).emit('videoChatAnswer', params);
    // });

    socket.on('newICECandidate', async (params) => {
      const candidate = new RTCIceCandidate(params.candidate);

      await pc.addIceCandidate(candidate);

      // socket.to(params.roomId).emit('newICECandidate', params);
    });

    socket.on('cameraAction', (params) => {
      socket.to(params.roomId).emit('cameraAction', params);
    });

    socket.on('microphoneAction', (params) => {
      socket.to(params.roomId).emit('microphoneAction', params);
    });
  });
};

I tried to receive an audio stream with RTCAudioSink but all the chunks were empty.

I expect to receive an actual audio data from the client.


Solution

  • The reason I was getting an empty audio stream is completely silly. Somehow, the socket.to method was not working, and although I was receiving an offer, the answer was not being sent back to the remote peer. The peer was adding tracks, and the ontrack event was triggered, but until the answer is received by the remote peer, their audio stream will remain empty. After I fixed the sockets, it started working as expected. I was focused on the wrong thing and missed the obvious.