devopvoid / webrtc-java

WebRTC for desktop platforms running Java
Apache License 2.0
248 stars 60 forks source link

Question: Change playback/capture devices after a WebRTC connection is created and used #29

Closed Stasyanych closed 2 years ago

Stasyanych commented 2 years ago

How do I change the audio playback or capture device during an established communication session? For example, if a bluetooth headset was connected during a conversation and there is a need to transfer the stream to it. Is it possible to do this in the current implementation of the library?

I did it like this on Kotlin

private val deviceChangeListener = object: DeviceChangeListener {
        private val TAG = "DeviceChangeListener"

    override fun deviceConnected(device: Device?) {
            LogUtils.info(TAG, "Connected: $device")
            device?.let { device ->
                audioDeviceModule?.let { deviceModule ->
                    deviceModule.playoutDevices.find {
                        it.descriptor == device.descriptor
                    }?.let {
                        LogUtils.info(TAG, "Set play out device = ${it.name}")
                        deviceModule.setPlayoutDevice(it)
                    }

                }

            }

        }

    override fun deviceDisconnected(device: Device?) {
            LogUtils.info(TAG, "Disconnected: $device")
            // do something
        }

}

The code is given as an example, it does not take into account the connection/disconnection of the microphone, only the playback device. The listener perfectly catches device change events , but deviceModule.setPlayoutDevice(it) does not lead to any changes. In the deviceConnected(device: Device?) method nothing happens, but in the deviceDisconnected(device: Device?) method an exception may appear - Set playout device failed I'm sure I'm doing something wrong. Is there a way to switch devices dynamically? Thank you in advance for the answer!

devopvoid commented 2 years ago

I never tried that, due to lack of time. As far as I know, the native WebRTC implementation does not catch a device change, even if you enumerate the devices after one is (dis)connected. The implementation with (dis)connected callbacks is my own. Thus its a bit hard to tell device module to switch the device.

The next thing to consider are the media tracks managed by the peer connection, since each track (mic/cam) is bound to a device. You can switch tracks or the sources of tracks, but if the source is not updated it obviously goes wrong.

I would handle this issue with a low priority.

Stasyanych commented 2 years ago

Thank you for your quick response! I want to share the method that I tried. Maybe it will be useful. I found on the Internet that it is possible to replace a track by calling the sender.replaceTrack method and specify AudioTrack as an input parameter. In the browser implementation WebRTC, it looks something like this::

//...
var audioTrack = stream.getAudioTracks()[0];
var sender = pc.getSenders().find(function(s) {
      return s.track.kind == audioTrack.kind;
    });
 sender.replaceTrack(audioTrack);
//...

I tried to do something similar in the DeviceChangeListener.deviceConnected method. I re-created AudioTrack and tried to replace the track. This did not give a positive result. Code on kotlin::

connection.senders.find {
    it.track.kind.equals(MediaStreamTrack.AUDIO_TRACK_KIND)
}?.let { rtcSender ->
      val audioOptions = AudioOptions().apply {
          this.noiseSuppression = true
          this.echoCancellation = true
      }
      val audioSource = peerConnectionFactory?.createAudioSource(audioOptions)
      val audioTrack = peerConnectionFactory?.createAudioTrack("audioTrack", audioSource)
      rtcSender.replaceTrack(audioTrack)
  }

I also made an attempt to replace rtcSender.replaceTrack(audioTrack) on

rtcSender.track.dispose() // otherwise, the removeTrack method will hang
connection.removeTrack(rtcSender)
connection.addTrack(audioTrack, listOf("stream"))

also to no avail

devopvoid commented 2 years ago

Hi @Stasyanych, this should work at runtime when the media is already flowing and you send/receive audio:

AudioDevice captureDevice; // Get desired device.
AudioDevice playbackDevice; // Get desired device.

AudioDeviceModule deviceModule = new AudioDeviceModule();
// For sending audio.
deviceModule.setRecordingDevice(captureDevice);
deviceModule.initRecording();
// For receiving audio.
deviceModule.setPlayoutDevice(playbackDevice);
deviceModule.initPlayout();

PeerConnectionFactory factory = new PeerConnectionFactory(deviceModule);

// Initialize PeerConnection and establish connection.

// Change audio recording and playback device at runtime.

// For sending audio.
deviceModule.stopRecording();
deviceModule.setRecordingDevice(newRecordingDevice);
deviceModule.initRecording();
deviceModule.startRecording();

// For receiving audio.
deviceModule.stopPlayout();
deviceModule.setPlayoutDevice(newPlaybackDevice);
deviceModule.initPlayout();
deviceModule.startPlayout();
Stasyanych commented 2 years ago

Thank you! I will definitely check it out.