2026-03-18 20:09:32 +03:00

490 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC.Samples;
using UnityEngine;
using UnityEngine.UI;
namespace Unity.WebRTC
{
class AudioSample : MonoBehaviour
{
[SerializeField] private AudioSource inputAudioSource;
[SerializeField] private AudioSource outputAudioSource;
[SerializeField] private Toggle toggleEnableMicrophone;
[SerializeField] private Toggle toggleLoopback;
[SerializeField] private Dropdown dropdownAudioClips;
[SerializeField] private Dropdown dropdownMicrophoneDevices;
[SerializeField] private Dropdown dropdownAudioCodecs;
[SerializeField] private Dropdown dropdownSpeakerMode;
[SerializeField] private Dropdown dropdownDSPBufferSize;
[SerializeField] private Dropdown dropdownBandwidth;
[SerializeField] private Button buttonStart;
[SerializeField] private Button buttonCall;
[SerializeField] private Button buttonPause;
[SerializeField] private Button buttonResume;
[SerializeField] private Button buttonHangup;
[SerializeField] private AudioClip[] audioclipList;
[SerializeField] private Text textBandwidth;
private RTCPeerConnection _pc1, _pc2;
private MediaStream _sendStream;
private MediaStream _receiveStream;
private AudioClip m_clipInput;
private AudioStreamTrack m_audioTrack;
private List<RTCRtpCodecCapability> availableCodecs = new List<RTCRtpCodecCapability>();
int m_samplingFrequency = 48000;
int m_lengthSeconds = 1;
private string m_deviceName = null;
private Dictionary<string, ulong?> bandwidthOptions = new Dictionary<string, ulong?>()
{
{ "undefined", null },
{ "320", 320 },
{ "160", 160 },
{ "80", 80 },
{ "40", 40 },
{ "20", 20 },
};
private Dictionary<string, int> dspBufferSizeOptions = new Dictionary<string, int>()
{
{ "Best Latency", 256 },
{ "Good Latency", 512 },
{ "Best Performance", 1024 },
};
void Start()
{
StartCoroutine(WebRTC.Update());
StartCoroutine(LoopStatsCoroutine());
toggleEnableMicrophone.isOn = false;
toggleEnableMicrophone.onValueChanged.AddListener(OnEnableMicrophone);
toggleEnableMicrophone.isOn = false;
toggleLoopback.onValueChanged.AddListener(OnChangeLoopback);
dropdownAudioClips.interactable = true;
dropdownAudioClips.options =
audioclipList.Select(clip => new Dropdown.OptionData(clip.name)).ToList();
dropdownMicrophoneDevices.interactable = false;
dropdownMicrophoneDevices.options =
Microphone.devices.Select(name => new Dropdown.OptionData(name)).ToList();
dropdownMicrophoneDevices.onValueChanged.AddListener(OnDeviceChanged);
var audioConf = AudioSettings.GetConfiguration();
dropdownSpeakerMode.options =
Enum.GetNames(typeof(AudioSpeakerMode)).Select(mode => new Dropdown.OptionData(mode)).ToList();
dropdownSpeakerMode.value = (int)audioConf.speakerMode;
dropdownSpeakerMode.onValueChanged.AddListener(OnSpeakerModeChanged);
dropdownDSPBufferSize.options =
dspBufferSizeOptions.Select(clip => new Dropdown.OptionData(clip.Key)).ToList();
dropdownDSPBufferSize.onValueChanged.AddListener(OnDSPBufferSizeChanged);
// best latency is default
OnDSPBufferSizeChanged(dropdownDSPBufferSize.value);
dropdownAudioCodecs.AddOptions(new List<string> { "Default" });
var codecs = RTCRtpSender.GetCapabilities(TrackKind.Audio).codecs;
var excludeCodecTypes = new[] { "audio/CN", "audio/telephone-event" };
foreach (var codec in codecs)
{
if (excludeCodecTypes.Count(type => codec.mimeType.Contains(type)) > 0)
continue;
availableCodecs.Add(codec);
}
dropdownAudioCodecs.AddOptions(availableCodecs.Select(codec =>
new Dropdown.OptionData(CodecToOptionName(codec))).ToList());
dropdownBandwidth.options = bandwidthOptions
.Select(pair => new Dropdown.OptionData { text = pair.Key })
.ToList();
dropdownBandwidth.onValueChanged.AddListener(OnBandwidthChanged);
dropdownBandwidth.interactable = false;
// Update UI
OnDeviceChanged(dropdownMicrophoneDevices.value);
buttonStart.onClick.AddListener(OnStart);
buttonCall.onClick.AddListener(OnCall);
buttonPause.onClick.AddListener(OnPause);
buttonResume.onClick.AddListener(OnResume);
buttonHangup.onClick.AddListener(OnHangUp);
}
static string CodecToOptionName(RTCRtpCodecCapability cap)
{
return string.Format($"{cap.mimeType} " +
$"{cap.clockRate} " +
$"channel={cap.channels}");
}
void OnStart()
{
if (toggleEnableMicrophone.isOn)
{
m_deviceName = dropdownMicrophoneDevices.captionText.text;
m_clipInput = Microphone.Start(m_deviceName, true, m_lengthSeconds, m_samplingFrequency);
// set the latency to “0” samples before the audio starts to play.
while (!(Microphone.GetPosition(m_deviceName) > 0)) { }
}
else
{
var clipIndex = dropdownAudioClips.value;
m_clipInput = audioclipList[clipIndex];
}
inputAudioSource.loop = true;
inputAudioSource.clip = m_clipInput;
inputAudioSource.Play();
buttonStart.interactable = false;
buttonCall.interactable = true;
buttonHangup.interactable = true;
dropdownSpeakerMode.interactable = false;
dropdownDSPBufferSize.interactable = false;
dropdownAudioCodecs.interactable = false;
}
void OnEnableMicrophone(bool enable)
{
dropdownMicrophoneDevices.interactable = enable;
dropdownAudioClips.interactable = !enable;
}
void OnChangeLoopback(bool loopback)
{
if (m_audioTrack != null)
{
m_audioTrack.Loopback = loopback;
}
}
void OnCall()
{
buttonCall.interactable = false;
buttonPause.interactable = true;
dropdownBandwidth.interactable = true;
_receiveStream = new MediaStream();
_receiveStream.OnAddTrack += OnAddTrack;
_sendStream = new MediaStream();
var configuration = GetSelectedSdpSemantics();
_pc1 = new RTCPeerConnection(ref configuration)
{
OnIceCandidate = candidate => _pc2.AddIceCandidate(candidate),
OnNegotiationNeeded = () => StartCoroutine(PeerNegotiationNeeded(_pc1))
};
_pc2 = new RTCPeerConnection(ref configuration)
{
OnIceCandidate = candidate => _pc1.AddIceCandidate(candidate),
OnTrack = e => _receiveStream.AddTrack(e.Track),
};
var transceiver2 = _pc2.AddTransceiver(TrackKind.Audio);
transceiver2.Direction = RTCRtpTransceiverDirection.RecvOnly;
m_audioTrack = new AudioStreamTrack(inputAudioSource);
m_audioTrack.Loopback = toggleLoopback.isOn;
_pc1.AddTrack(m_audioTrack, _sendStream);
var transceiver1 = _pc1.GetTransceivers().First();
if (dropdownAudioCodecs.value == 0)
{
var error = transceiver1.SetCodecPreferences(this.availableCodecs.ToArray());
if (error != RTCErrorType.None)
Debug.LogError(error);
}
else
{
var codec = availableCodecs[dropdownAudioCodecs.value - 1];
var error = transceiver1.SetCodecPreferences(new[] { codec });
if (error != RTCErrorType.None)
Debug.LogError(error);
}
}
void OnPause()
{
var transceiver1 = _pc1.GetTransceivers().First();
var track = transceiver1.Sender.Track;
track.Enabled = false;
buttonResume.gameObject.SetActive(true);
buttonPause.gameObject.SetActive(false);
}
void OnResume()
{
var transceiver1 = _pc1.GetTransceivers().First();
var track = transceiver1.Sender.Track;
track.Enabled = true;
buttonResume.gameObject.SetActive(false);
buttonPause.gameObject.SetActive(true);
}
void OnAddTrack(MediaStreamTrackEvent e)
{
var track = e.Track as AudioStreamTrack;
outputAudioSource.SetTrack(track);
outputAudioSource.loop = true;
outputAudioSource.Play();
}
void OnHangUp()
{
Microphone.End(m_deviceName);
m_clipInput = null;
m_audioTrack?.Dispose();
_receiveStream?.Dispose();
_sendStream?.Dispose();
_pc1?.Dispose();
_pc2?.Dispose();
_pc1 = null;
_pc2 = null;
inputAudioSource.Stop();
outputAudioSource.Stop();
buttonStart.interactable = true;
buttonCall.interactable = false;
buttonHangup.interactable = false;
buttonPause.interactable = false;
buttonResume.gameObject.SetActive(false);
buttonPause.gameObject.SetActive(true);
dropdownSpeakerMode.interactable = true;
dropdownDSPBufferSize.interactable = true;
dropdownAudioCodecs.interactable = true;
dropdownBandwidth.interactable = false;
}
void OnDeviceChanged(int value)
{
if (dropdownMicrophoneDevices.options.Count == 0)
return;
m_deviceName = dropdownMicrophoneDevices.options[value].text;
Microphone.GetDeviceCaps(m_deviceName, out int minFreq, out int maxFreq);
}
private void OnBandwidthChanged(int index)
{
if (_pc1 == null || _pc2 == null)
return;
ulong? bandwidth = bandwidthOptions.Values.ElementAt(index);
RTCRtpSender sender = _pc1.GetSenders().First();
RTCRtpSendParameters parameters = sender.GetParameters();
if (bandwidth == null)
{
parameters.encodings[0].maxBitrate = null;
parameters.encodings[0].minBitrate = null;
}
else
{
parameters.encodings[0].maxBitrate = bandwidth * 1000;
parameters.encodings[0].minBitrate = bandwidth * 1000;
}
RTCError error = sender.SetParameters(parameters);
if (error.errorType != RTCErrorType.None)
{
Debug.LogErrorFormat("RTCRtpSender.SetParameters failed {0}", error.errorType);
}
Debug.Log("SetParameters:" + bandwidth);
}
void OnSpeakerModeChanged(int value)
{
var audioConf = AudioSettings.GetConfiguration();
audioConf.speakerMode = (AudioSpeakerMode)value;
Debug.Log(audioConf.speakerMode);
if (!AudioSettings.Reset(audioConf))
{
Debug.LogError("Failed changing Audio Settings");
}
}
void OnDSPBufferSizeChanged(int value)
{
var audioConf = AudioSettings.GetConfiguration();
audioConf.dspBufferSize = dspBufferSizeOptions.Values.ToArray()[value];
if (!AudioSettings.Reset(audioConf))
{
Debug.LogError("Failed changing Audio Settings");
}
}
private static RTCConfiguration GetSelectedSdpSemantics()
{
RTCConfiguration config = default;
config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } };
return config;
}
IEnumerator PeerNegotiationNeeded(RTCPeerConnection pc)
{
var op = pc.CreateOffer();
yield return op;
if (!op.IsError)
{
if (pc.SignalingState != RTCSignalingState.Stable)
{
yield break;
}
yield return StartCoroutine(OnCreateOfferSuccess(pc, op.Desc));
}
else
{
var error = op.Error;
OnSetSessionDescriptionError(ref error);
}
}
private RTCPeerConnection GetOtherPc(RTCPeerConnection pc)
{
return (pc == _pc1) ? _pc2 : _pc1;
}
private IEnumerator OnCreateOfferSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
{
var op = pc.SetLocalDescription(ref desc);
yield return op;
if (!op.IsError)
{
OnSetLocalSuccess(pc);
}
else
{
var error = op.Error;
OnSetSessionDescriptionError(ref error);
}
var otherPc = GetOtherPc(pc);
var op2 = otherPc.SetRemoteDescription(ref desc);
yield return op2;
if (op2.IsError)
{
var error = op2.Error;
OnSetSessionDescriptionError(ref error);
}
var op3 = otherPc.CreateAnswer();
yield return op3;
if (!op3.IsError)
{
yield return OnCreateAnswerSuccess(otherPc, op3.Desc);
}
}
IEnumerator OnCreateAnswerSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
{
var op = pc.SetLocalDescription(ref desc);
yield return op;
if (!op.IsError)
{
OnSetLocalSuccess(pc);
}
else
{
var error = op.Error;
OnSetSessionDescriptionError(ref error);
}
var otherPc = GetOtherPc(pc);
var op2 = otherPc.SetRemoteDescription(ref desc);
yield return op2;
if (op2.IsError)
{
var error = op2.Error;
OnSetSessionDescriptionError(ref error);
}
}
private void OnSetLocalSuccess(RTCPeerConnection pc)
{
Debug.Log("SetLocalDescription complete");
}
static void OnSetSessionDescriptionError(ref RTCError error)
{
Debug.LogError($"Error Detail Type: {error.message}");
}
private IEnumerator LoopStatsCoroutine()
{
while (true)
{
yield return StartCoroutine(UpdateStatsCoroutine());
yield return new WaitForSeconds(1f);
}
}
private IEnumerator UpdateStatsCoroutine()
{
RTCRtpSender sender = _pc1?.GetSenders().First();
if (sender == null)
yield break;
RTCStatsReportAsyncOperation op = sender.GetStats();
yield return op;
if (op.IsError)
{
Debug.LogErrorFormat("RTCRtpSender.GetStats() is failed {0}", op.Error.errorType);
}
else
{
UpdateStatsPacketSize(op.Value);
}
}
private RTCStatsReport lastResult = null;
private void UpdateStatsPacketSize(RTCStatsReport res)
{
foreach (RTCStats stats in res.Stats.Values)
{
if (!(stats is RTCOutboundRTPStreamStats report))
{
continue;
}
long now = report.Timestamp;
ulong bytes = report.bytesSent;
if (lastResult != null)
{
if (!lastResult.TryGetValue(report.Id, out RTCStats last))
continue;
var lastStats = last as RTCOutboundRTPStreamStats;
var duration = (double)(now - lastStats.Timestamp) / 1000000;
ulong bitrate = (ulong)(8 * (bytes - lastStats.bytesSent) / duration);
textBandwidth.text = (bitrate / 1000.0f).ToString("f2");
//if (autoScroll.isOn)
//{
// statsField.MoveTextEnd(false);
//}
}
}
lastResult = res;
}
}
}