해당 문제가 나오는 버전
※ Unity Editor Version: 2022.3.45f1
※ Vivox Version: 16.5.4
유니티의 Webgl에서 Vivox를 사용하여 음성 채팅을 연동할 일이 생겼었다. 찾아보니 WebGL 지원은 제한적으로 16.5.0 버전부터 SDK가 지원이 되었다.
https://docs.unity.com/ugs/ko-kr/manual/vivox-unity/manual/Unity/developer-guide/vivox-webgl
Unity SDK WebGL 지원
English日本語 (日本)한국어(대한민국)中文(中国) English日本語 (日本)한국어(대한민국)中文(中国) Unity SDK WebGL 지원#Vivox Unity WebGL SDK는 16.5.0 버전부터 사용할 수 있습니다.이 SDK는 제한된 수준
docs.unity.com
그러던중 샘플 자료를 토대로 작업을 하다가 WebGL에서 그룹 채널을 참가하지 못하는 문제가 있었다.
문제의 코드는 다음과 같다.
private string _playerName = string.Empty;
private const string _chanelName = "TestChannel";
// Start is called before the first frame update
async void Start()
{
await InitializeAsync();
await LoginToVivoxAsync();
await JoinGroupChannelAsync();
Debug.Log("채널 참석 완료!");
}
private void OnDestroy()
{
VivoxService.Instance.LoggedIn -= OnUserLoggedIn;
VivoxService.Instance.LoggedOut -= OnUserLoggedOut;
VivoxService.Instance.ChannelJoined -= OnChannelJoined;
VivoxService.Instance.ChannelLeft -= OnChannelLeft;
}
// 서비스 초기화
private async Task InitializeAsync()
{
try
{
await UnityServices.InitializeAsync();
Debug.Log($"Unity Services의 초기화를 성공하였습니다., UnityServices.State: [{UnityServices.State}]");
await VivoxService.Instance.InitializeAsync();
Debug.Log($"Vivox Services의 초기화를 성공하였습니다.");
_playerName = PlayerNameLookUpTable();
AuthenticationService.Instance.SwitchProfile(_playerName);
await AuthenticationService.Instance.SignInAnonymouslyAsync();
Debug.Log($"Unity Services의 익명 사용자 계정으로 로그인을 성공하였습니다. PlayerName: {_playerName}");
VivoxService.Instance.LoggedIn += OnUserLoggedIn;
VivoxService.Instance.LoggedOut += OnUserLoggedOut;
VivoxService.Instance.ChannelJoined += OnChannelJoined;
VivoxService.Instance.ChannelLeft += OnChannelLeft;
}
catch (ArgumentException e)
{
Console.WriteLine($"InitializeAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// Vivox 로그인
private async Task LoginToVivoxAsync()
{
try
{
LoginOptions options = new LoginOptions()
{
DisplayName = _playerName,
ParticipantUpdateFrequency = ParticipantPropertyUpdateFrequency.FivePerSecond
};
await VivoxService.Instance.LoginAsync(options);
Debug.Log($"Vivox의 로그인을 하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"LoginToVivoxAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// 그룹 채널 참가
private async Task JoinGroupChannelAsync()
{
try
{
await VivoxService.Instance.JoinGroupChannelAsync(_chanelName, ChatCapability.AudioOnly);
Debug.Log($"[{_chanelName}]의 그룹 채널을 참가하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"JoinEchoChannelAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
private void OnUserLoggedIn()
{
Debug.Log("Vivox에 유저가 로그인되었습니다.");
}
private void OnUserLoggedOut()
{
Debug.Log("Vivox의 유저가 로그아웃되었습니다.");
}
private void OnChannelJoined(string channel)
{
Debug.Log($"[{channel}] 채널에 참석하였습니다.");
}
private void OnChannelLeft(string channel)
{
Debug.Log($"[{channel}] 채널을 나갔습니다.");
}
private string PlayerNameLookUpTable()
{
string[] table = new string[] { "Editor", "Window", "Android", "WebGL" };
string result = string.Empty;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
{
result = table[0];
break;
}
case RuntimePlatform.WindowsPlayer:
{
result = table[1];
break;
}
case RuntimePlatform.Android:
{
result = table[2];
break;
}
case RuntimePlatform.WebGLPlayer:
{
result = table[3];
break;
}
}
return result;
}
이유는 Start 함수에서 서비스 초기화, Vivox로그인, 그룹 채널 참석을 한꺼번에 실행했기 때문이다.
검색을 해보니 "WebGL에서 마이크 접근 권한이 요청되지 않았거나 거부된 경우, Vivox 로그인이 실패할 수 있습니다. " 라고 나왔다. 위의 코드는 WebGL의 마이크를 허용/차단을 선택을 하지않은 상태로 Start함수에서 한꺼번에 실행되기 때문이다.
그래서 버튼을 추가하여 서비스 초기화, Vivox로그인, 그룹 채널 참석을 분리를 하고 마이크를 허용인 상태에서 버튼을 순차적으로 실행 시켜서 테스트를 하였다. 그랬더니 WebGL에서 정상적으로 채널 참석을 할 수 있었다.
[SerializeField] private Button _vivoxLogIn = null;
[SerializeField] private Button _vivoxJoinChannel = null;
private string _playerName = string.Empty;
private const string _chanelName = "TestChannel";
// Start is called before the first frame update
async void Start()
{
await InitializeAsync();
//await LoginToVivoxAsync();
//await JoinGroupChannelAsync();
//Debug.Log("채널 참석 완료!");
// Vivox 로그인 버튼
_vivoxLogIn.onClick.AddListener(async () => {
await LoginToVivoxAsync();
});
// 채널 참석 버튼
_vivoxJoinChannel.onClick.AddListener(async () => {
await JoinGroupChannelAsync();
});
}
//...
여기서는 추측이지만, await를 사용해 순차적으로 실행될 것으로 생각했지만, 분리하여 테스트한 결과, Vivox 내부 처리에서 제대로 동작하지 않는 문제가 있는 것으로 보였다. 이로 인해 작동하지 않는 것처럼 보였다.
그런데 버튼으로 눌러서 해결하는 방법은 내가 만들어야하는 프로그램의 로직상 유연하지 않는 방법이다.
내가 만들어야되는 음성채팅은 참여 버튼 하나를 누르면 음성 채팅 참가가 완료가 되어야 한다는 것이다. 보통은 사용자 입장에서 버튼 한번만 누르면 모든게 작동되어야 사용하기 편하다.
그래서 테스트를 이것저것 해보다보니 다음과 같은 결론이 나왔다. 이것은 음성채팅 샘플에도 똑같이 적용이 되어있다.
처리방법은 다음과 같다.
1. 서비스 초기화를 Awake에서 처리한다.
2. Vivox에 유저가 로그인 될 때 부르는 Vivox 유저 로그인이라는 콜백함수를 등록한다. 그리고 콜백함수에 그룹 채널 참가 함수를 넣는다.
3. Start 함수에서 vivoxLogIn이라는 음성채팅 참가 버튼을 만들어 Vivox를 로그인하는 콜백을 등록한다.
다음은 처리방법을 적용한 코드이다.
[SerializeField] private Button _vivoxLogIn = null;
private string _playerName = string.Empty;
private const string _chanelName = "TestChannel";
private async void Awake()
{
await InitializeAsync();
}
// Start is called before the first frame update
private void Start()
{
_vivoxLogIn.onClick.AddListener(async () => {
await LoginToVivoxAsync();
});
}
private void OnDestroy()
{
VivoxService.Instance.LoggedIn -= OnUserLoggedIn;
VivoxService.Instance.LoggedOut -= OnUserLoggedOut;
VivoxService.Instance.ChannelJoined -= OnChannelJoined;
VivoxService.Instance.ChannelLeft -= OnChannelLeft;
}
// 서비스 초기화
private async Task InitializeAsync()
{
try
{
await UnityServices.InitializeAsync();
Debug.Log($"Unity Services의 초기화를 성공하였습니다., UnityServices.State: [{UnityServices.State}]");
await VivoxService.Instance.InitializeAsync();
Debug.Log($"Vivox Services의 초기화를 성공하였습니다.");
_playerName = PlayerNameLookUpTable();
AuthenticationService.Instance.SwitchProfile(_playerName);
await AuthenticationService.Instance.SignInAnonymouslyAsync();
Debug.Log($"Unity Services의 익명 사용자 계정으로 로그인을 성공하였습니다. PlayerName: {_playerName}");
VivoxService.Instance.LoggedIn += OnUserLoggedIn;
}
catch (ArgumentException e)
{
Console.WriteLine($"InitializeAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// Vivox 로그인
private async Task LoginToVivoxAsync()
{
try
{
LoginOptions options = new LoginOptions()
{
DisplayName = _playerName,
ParticipantUpdateFrequency = ParticipantPropertyUpdateFrequency.FivePerSecond
};
await VivoxService.Instance.LoginAsync(options);
Debug.Log($"Vivox의 로그인을 하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"LoginToVivoxAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// 그룹 채널 참가
private async Task JoinGroupChannelAsync()
{
try
{
await VivoxService.Instance.JoinGroupChannelAsync(_chanelName, ChatCapability.AudioOnly);
Debug.Log($"[{_chanelName}]의 그룹 채널을 참가하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"JoinEchoChannelAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// Vivox 유저 로그인
async void OnUserLoggedIn()
{
Debug.Log("Vivox에 유저가 로그인되었습니다.");
await JoinGroupChannelAsync();
}
이런 순서로 코드를 짜야 WebGL에서 제대로 작동이된다.
참고로 다음과 같이 코드를 짜면 작동이 안된다. 그 예시이다.
[SerializeField] private Button _vivoxLogIn = null;
private string _playerName = string.Empty;
private const string _chanelName = "TestChannel";
private async void Awake()
{
await InitializeAsync();
}
// Start is called before the first frame update
private void Start()
{
_vivoxLogIn.onClick.AddListener(async () => {
await LoginToVivoxAsync();
await JoinGroupChannelAsync();
});
}
// ....
Start 함수에 vivox 로그인 및 채널 참가를 같이 넣으면 작동이 안된다.
이유는 LoginTovivoxAsync() 뒤에 JoinGroupChannelAsync()이 실행이 안되는것인데, LoginTovivoxAsync() 에서 위에서 등록한 Vivox 유저 로그인까지는 내부적인 코드가 작동이되는걸로 보이는데, 그 이후의 코드가 작동이 되지않아 LoginTovivoxAsync()에서 반환을 받지못하기 때문에 JoinGroupChannelAsync()가 작동이 안되는걸로 보였다.
※ 참고사항
Vivox 유저 로그인 콜백 함수도 간헐적으로 반환이 되지 않아 로그인이 안되는 경우가 있다. 이 부분은 뭐가 문제인지 잘 모르겠다. 내부적인 코드가 잘못된것인지, 내가 잘못한 것인지...
추후 개선되면 글을 수정하도록 하자.
최종적으로 적용된 코드이다.
[SerializeField] private Button _vivoxLogIn = null;
[SerializeField] private Button _vivoxLogOut = null;
[SerializeField] private Button _vivoxJoinChannel = null;
[SerializeField] private Button _vivoxLeaveAllChannel = null;
private string _playerName = string.Empty;
private const string _chanelName = "TestChannel";
private async void Awake()
{
await InitializeAsync();
}
// Start is called before the first frame update
private void Start()
{
_vivoxLogIn.onClick.AddListener(async () => {
await LoginToVivoxAsync();
});
_vivoxLogOut.onClick.AddListener(async () =>
{
await LogoutOfVivoxServiceAsync();
});
//_vivoxJoinChannel.onClick.AddListener(async () =>
//{
// await JoinGroupChannelAsync();
//});
//_vivoxLeaveAllChannel.onClick.AddListener(async () =>
//{
// await LeaveAllChannelsAsync();
//});
}
private void OnDestroy()
{
VivoxService.Instance.LoggedIn -= OnUserLoggedIn;
VivoxService.Instance.LoggedOut -= OnUserLoggedOut;
VivoxService.Instance.ChannelJoined -= OnChannelJoined;
VivoxService.Instance.ChannelLeft -= OnChannelLeft;
}
// 서비스 초기화
private async Task InitializeAsync()
{
try
{
await UnityServices.InitializeAsync();
Debug.Log($"Unity Services의 초기화를 성공하였습니다., UnityServices.State: [{UnityServices.State}]");
await VivoxService.Instance.InitializeAsync();
Debug.Log($"Vivox Services의 초기화를 성공하였습니다.");
_playerName = PlayerNameLookUpTable();
AuthenticationService.Instance.SwitchProfile(_playerName);
await AuthenticationService.Instance.SignInAnonymouslyAsync();
Debug.Log($"Unity Services의 익명 사용자 계정으로 로그인을 성공하였습니다. PlayerName: {_playerName}");
VivoxService.Instance.LoggedIn += OnUserLoggedIn;
VivoxService.Instance.LoggedOut += OnUserLoggedOut;
VivoxService.Instance.ChannelJoined += OnChannelJoined;
VivoxService.Instance.ChannelLeft += OnChannelLeft;
}
catch (ArgumentException e)
{
Console.WriteLine($"InitializeAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// Vivox 로그인
private async Task LoginToVivoxAsync()
{
try
{
LoginOptions options = new LoginOptions()
{
DisplayName = _playerName,
ParticipantUpdateFrequency = ParticipantPropertyUpdateFrequency.FivePerSecond
};
await VivoxService.Instance.LoginAsync(options);
Debug.Log($"Vivox의 로그인을 하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"LoginToVivoxAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// Vivox 로그아웃
private async Task LogoutOfVivoxServiceAsync()
{
try
{
await VivoxService.Instance.LogoutAsync();
Debug.Log($"Vivox의 로그아웃을 하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"LogoutOfVivoxServiceAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// 에코 채널 참가
private async Task JoinEchoChannelAsync()
{
try
{
await VivoxService.Instance.JoinEchoChannelAsync(_chanelName, ChatCapability.AudioOnly);
Debug.Log($"Vivox의 에코 채널을 참가하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"JoinEchoChannelAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// 그룹 채널 참가
private async Task JoinGroupChannelAsync()
{
try
{
await VivoxService.Instance.JoinGroupChannelAsync(_chanelName, ChatCapability.AudioOnly);
Debug.Log($"[{_chanelName}]의 그룹 채널을 참가하였습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"JoinEchoChannelAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
// 모든 채널 나가기
private async Task LeaveAllChannelsAsync()
{
try
{
await VivoxService.Instance.LeaveAllChannelsAsync();
Debug.Log($"Vivox의 모든 채널을 나갔습니다.");
}
catch (ArgumentException e)
{
Console.WriteLine($"LeaveAllChannelsAsync Processing failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
async void OnUserLoggedIn()
{
Debug.Log("Vivox에 유저가 로그인되었습니다.");
await JoinGroupChannelAsync();
}
void OnUserLoggedOut()
{
Debug.Log("Vivox의 유저가 로그아웃되었습니다.");
}
void OnChannelJoined(string channel)
{
Debug.Log($"[{channel}] 채널에 참석하였습니다.");
}
void OnChannelLeft(string channel)
{
Debug.Log($"[{channel}] 채널을 나갔습니다.");
}
string PlayerNameLookUpTable()
{
string[] table = new string[] { "Editor", "Window", "Android", "WebGL" };
string result = string.Empty;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
{
result = table[0];
break;
}
case RuntimePlatform.WindowsPlayer:
{
result = table[1];
break;
}
case RuntimePlatform.Android:
{
result = table[2];
break;
}
case RuntimePlatform.WebGLPlayer:
{
result = table[3];
break;
}
}
return result;
}