英文:
Twitch Chat Timing Out after 10 minutes for Unity game project
问题
I need some advice on a Twitch chat game we are working on as I am now banging my head against a wall.
When we start the game and connect to the Twitch API everything works fine for the first 10 minutes, then without any error or warning in the console Twitch updates the chat token. I did add an update to reconnect to the chat automatically but for some reason it fails. If I try to reconnect by manually clicking a button that is used to authorize the tokens to reconnect I get a Listener error as the Authenticator is already in the scene and blocks the new instance as it should.
My question is what am I missing here to both read the timeout and to re-establish the connection?
This is the Authenticator script
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using UnityEngine;
using static TwitchIntegration.Utils.TwitchVariables;
namespace TwitchIntegration
{
public class TwitchAuthenticator : MonoBehaviour
{
// ... (rest of the script)
}
}
This is the CommandManager script
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using UnityEngine;
using static TwitchIntegration.Utils.TwitchVariables;
namespace TwitchIntegration
{
public class TwitchCommandManager : MonoBehaviour
{
// ... (rest of the script)
}
}
Let me know if you need further assistance with these scripts.
英文:
I need some advice on a Twitch chat game we are working on as I am now banging my head against a wall.
When we start the game and connect to the Twitch API everything works fine for the first 10 minutes, then without any error or warning in the console Twitch updates the chat token. I did add an update to reconnect to the chat automatically but for some reason it fails. If I try to reconnect by manually clicking a button that is used to authorise the tokens to reconnect I get a Listener error as the Authenticator is all ready in the scene and blocks the new instance as it should.
My question is what am I missing here to both read the timeout and to re-establish the connection?
This is the Authenticator script
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using UnityEngine;
using static TwitchIntegration.Utils.TwitchVariables;
namespace TwitchIntegration
{
public class TwitchAuthenticator : MonoBehaviour
{
private TwitchSettings _settings;
private static HttpListener _listener;
private static Thread _listenerThread;
private OAuth _oauth;
public bool IsAuthenticated { get; private set; }
private const float Timeout = 10.0f;
private void Awake()
{
_settings = Resources.Load<TwitchSettings>(TwitchSettingsPath);
IsAuthenticated = CheckAuthenticationStatus();
}
private bool CheckAuthenticationStatus()
{
if (!PlayerPrefs.HasKey(TwitchOAuthTokenKey))
{
Log("Twitch client unauthenticated", "yellow");
return false;
}
try
{
_oauth = JsonUtility.FromJson<OAuth>(PlayerPrefs.GetString(TwitchOAuthTokenKey));
if (string.IsNullOrEmpty(_oauth.accessToken))
throw new TwitchCommandException("Invalid Twitch client access token");
IsAuthenticated = true;
Log("Twitch client authenticated", "green");
}
catch (TwitchCommandException e)
{
Log(e.Message, "red");
}
return IsAuthenticated;
}
internal void TryAuthenticate(string username, string channelName, Action<bool> onComplete)
{
PlayerPrefs.SetString(TwitchUsernameKey, username);
PlayerPrefs.SetString(TwitchChannelNameKey, channelName);
StartCoroutine(TryAuthenticateCoroutine(onComplete));
}
private IEnumerator TryAuthenticateCoroutine(Action<bool> onComplete)
{
_listener = new HttpListener();
_listener.Prefixes.Add("http://localhost/");
_listener.Prefixes.Add("http://127.0.0.1/");
_listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
_listener.Start();
_listenerThread = new Thread(StartListener);
_listenerThread.Start();
IsAuthenticated = false;
var url = "https://id.twitch.tv/oauth2/authorize?client_id=" + _settings.clientId +
"&redirect_uri=" + _settings.redirectUri + "&response_type=token&scope=chat:read";
#if UNITY_WEBGL
var webglURL = string.Format("window.open(\"{0}\")", url);
Application.ExternalEval(webglURL);
#else
Application.OpenURL(url);
#endif
var processStartTime = Time.realtimeSinceStartup;
while (!IsAuthenticated)
{
var elapsedTime = Time.realtimeSinceStartup - processStartTime;
if (elapsedTime >= Timeout)
{
Log("Authentication timed out", "red");
onComplete(false);
yield break;
}
yield return null;
}
onComplete?.Invoke(IsAuthenticated);
_listener.Stop();
_listener.Close();
PlayerPrefs.SetString(TwitchOAuthTokenKey, JsonUtility.ToJson(_oauth));
if (!PlayerPrefs.HasKey(TwitchAuthenticatedKey)) PlayerPrefs.SetInt(TwitchAuthenticatedKey, 1);
}
private void StartListener()
{
while (true)
{
if (IsAuthenticated) return;
var result = _listener.BeginGetContext(GetContextCallback, _listener);
result.AsyncWaitHandle.WaitOne();
}
}
private void GetContextCallback(IAsyncResult asyncResult)
{
var context = _listener.EndGetContext(asyncResult);
if (context.Request.HttpMethod == "POST")
{
var dataText = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding).ReadToEnd();
_oauth = JsonUtility.FromJson<OAuth>(dataText);
IsAuthenticated = true;
}
_listener.BeginGetContext(GetContextCallback, null);
const string responseHtmlPage = @"<html><head>
<script src=""https://unpkg.com/axios/dist/axios.min.js""></script>
<script>if (window.location.hash) {
let fragments = window.location.hash.substring(1).split('&').map(x => x.split('=')[1]);
let data = {
accessToken: fragments[0],
scope: fragments[1],
state: fragments[2]
};
axios.post('/', data).then(function(response) {console.log(response); window.close();}).catch(function(error) {console.log(error); window.close();});
}
</script></head>";
var buffer = Encoding.UTF8.GetBytes(responseHtmlPage);
var response = context.Response;
response.ContentType = "text/html";
response.ContentLength64 = buffer.Length;
response.StatusCode = 200;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.OutputStream.Close();
}
private void Log(string message, string color)
{
if (_settings.isDebugMode) print($"<color={color}>{message}</color>");
}
}
}
This is the CommandManager script
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using UnityEngine;
using static TwitchIntegration.Utils.TwitchVariables;
namespace TwitchIntegration
{
public class TwitchCommandManager : MonoBehaviour
{
internal bool IsEnabled { get; set; } = true;
internal static bool IsInitialized { get; private set; }
internal static bool IsConnected => _twitchClient.Connected;
internal TwitchCommand[] GetAllAvailableCommands => _settings.commandList.Where(x => x.enabled).ToArray();
internal List<string> CommandsOnCooldown { get; private set; }
private static TwitchSettings _settings;
private static Dictionary<string, MethodInfo> _methodsDict;
private static Dictionary<MethodInfo, ParameterInfo[]> _methodParameters;
private static Dictionary<MethodInfo, List<TwitchMonoBehaviour>> _methodBehaviours;
private static Dictionary<string, List<MethodInfo>> _typeMethods;
private static Dictionary<string, string> _aliasToCommandName;
private static TcpClient _twitchClient;
private static StreamReader _streamReader;
private static StreamWriter _streamWriter;
private static bool _isConnecting;
private static float _timeUntilTimeout;
private const float Timeout = 10f;
[HideInInspector] public string twitchMessage;
[HideInInspector] public bool newMessageRecieved;
internal static void AddBehaviour(TwitchMonoBehaviour behaviour)
{
if (!IsInitialized) return;
var key = behaviour.GetType().Name;
if (!_typeMethods.ContainsKey(key)) return;
_typeMethods[key].ForEach(method =>
{
if (_methodBehaviours.ContainsKey(method))
_methodBehaviours[method].Add(behaviour);
else
_methodBehaviours.Add(method, new List<TwitchMonoBehaviour> {behaviour});
});
}
internal static void RemoveBehaviour(TwitchMonoBehaviour behaviour)
{
if (!IsInitialized) return;
var key = behaviour.GetType().Name;
if (!_typeMethods.ContainsKey(key)) return;
_typeMethods[key].ForEach(method =>
{
if (_methodBehaviours.ContainsKey(method))
_methodBehaviours[method].Remove(behaviour);
});
}
[ContextMenu("Init")]
public void Init() => Initialize();
internal static void Initialize()
{
if (IsInitialized)
{
Log("Twitch commands are already initialized.", "yellow");
return;
}
Log("Initializing Twitch client...", "yellow");
_methodsDict = new Dictionary<string, MethodInfo>();
_methodParameters = new Dictionary<MethodInfo, ParameterInfo[]>();
_methodBehaviours = new Dictionary<MethodInfo, List<TwitchMonoBehaviour>>();
_typeMethods = new Dictionary<string, List<MethodInfo>>();
_aliasToCommandName = new Dictionary<string, string>();
var methods = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetTypes())
.Where(x => x.IsClass && typeof(TwitchMonoBehaviour).IsAssignableFrom(x))
.SelectMany(x => x.GetMethods())
.Where(x => x.GetCustomAttributes(typeof(TwitchCommandAttribute), false).FirstOrDefault() != null);
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<TwitchCommandAttribute>();
_methodsDict.Add(attr.Name, method);
if (_typeMethods.ContainsKey(method.DeclaringType!.Name))
_typeMethods[method.DeclaringType.Name].Add(method);
else
_typeMethods.Add(method.DeclaringType.Name, new List<MethodInfo> {method});
foreach (var alias in attr.Aliases)
_aliasToCommandName.Add(alias, attr.Name);
_methodParameters[method] = method.GetParameters();
}
IsInitialized = true;
Log("Initialized! Attempting to connect...", "yellow");
Connect();
}
internal static void Connect()
{
_twitchClient = new TcpClient("irc.chat.twitch.tv", 6667);
_streamReader = new StreamReader(_twitchClient.GetStream());
_streamWriter = new StreamWriter(_twitchClient.GetStream());
var oAuth = JsonUtility.FromJson<OAuth>(PlayerPrefs.GetString(TwitchOAuthTokenKey));
var username = PlayerPrefs.GetString(TwitchUsernameKey);
var channelName = PlayerPrefs.GetString(TwitchChannelNameKey);
_streamWriter.WriteLine("PASS oauth:" + oAuth.accessToken);
_streamWriter.WriteLine("NICK " + username.ToLower());
_streamWriter.WriteLine("JOIN #" + channelName.ToLower());
_streamWriter.WriteLine("CAP REQ :twitch.tv/tags");
_streamWriter.Flush();
_timeUntilTimeout = Timeout;
_isConnecting = true;
}
private void FixedUpdate()
{
if (!_isConnecting) return;
_timeUntilTimeout -= Time.fixedDeltaTime;
if (_timeUntilTimeout > 0) return;
TwitchManager.OnFailedToConnect();
Log("Connection timed out, retrying... If several attempts fail, refresh your OAuth token", "red");
Connect();
}
private void ReadChat()
{
if (_twitchClient.Available <= 0 || !IsEnabled) return;
var message = _streamReader.ReadLine();
if (message == null) return;
if (message.Contains(UserMessageCode))
OnMessageReceived(message);
else if (message.Contains(UserJoinCode))
OnJoinedToChat();
}
private void OnMessageReceived(string message)
{
//EXAMPLE:
//@badge-info=;badges=broadcaster/1;client-nonce=0c3f702e8676d24ae22ac5f56706af37;color=#1E90FF;display-name=danqzq;emotes=;
//flags=;id=9ee20670-8fcf-4a77-ab9c-d7aede171b85;mod=0;room-id=201429480;subscriber=0;tmi-sent-ts=1627719386492;turbo=0;
//user-id=201429480;user-type= :danqzq!danqzq@danqzq.tmi.twitch.tv PRIVMSG #danqzq :!join
//Split the incoming data from tags and store the raw message
//(in the case above the raw message would be => danqzq!danqzq@danqzq.tmi.twitch.tv PRIVMSG #danqzq :!join)
var rawMessage = message.Split(new[]{':'}, 2, StringSplitOptions.None)[1];
//Get the typed command from the raw message
rawMessage = rawMessage.Substring(rawMessage.IndexOf(':', 1) + 1);
//Parse the tags into a list of strings
var tags = message.Replace("-", "").Substring(1).Split(';').ToList();
tags[tags.Count - 1] = tags[tags.Count - 1].Replace(rawMessage, "");
//Form a json out of the received tags
var json = "{";
for (var i = 0; i < tags.Count; i++)
{
var entry = tags[i].Split(new[] {'='}, 2, StringSplitOptions.RemoveEmptyEntries);
if (entry.Length < 2) continue;
var isDigit = entry[1].All(char.IsDigit);
if (!isDigit) entry[1] = '"' + entry[1] + '"';
json += '"' + entry[0] + '"' + ':' + entry[1] + (i == tags.Count - 1 ? ' ' : ',');
}
json += "}";
//Parse the json
var twitchUser = JsonUtility.FromJson<TwitchUser>(json);
var playerName = twitchUser.displayname;
if (!string.IsNullOrEmpty(twitchUser.color))
playerName = "<color=" + twitchUser.color + ">" + playerName + "</color>";
Log("Twitch chat - " + playerName + " : " + rawMessage, "white");
twitchMessage = rawMessage;
newMessageRecieved = true;
TwitchManager.OnMessageReceived(twitchUser, rawMessage);
if (!rawMessage.StartsWith(_settings.commandPrefix)) return;
//Get the command and the arguments from the raw message
var splitPoint = rawMessage.IndexOf(' ');
var baseCommand = splitPoint < 0 ? rawMessage.Substring(1) : rawMessage.Substring(1, splitPoint - 1);
var args = splitPoint < 0 ? new string[]{} : rawMessage.Substring(splitPoint + 1).Split(' ');
if (_aliasToCommandName.ContainsKey(baseCommand))
baseCommand = _aliasToCommandName[baseCommand];
if (!_methodsDict.ContainsKey(baseCommand)) return;
var commandInfo = _settings.commandList.Find(x => x.name == baseCommand);
if (!commandInfo.enabled) return;
if (_settings.commandsMode == TwitchCommandsMode.Cooldown)
{
if (CommandsOnCooldown.Contains(baseCommand)) return;
CommandsOnCooldown.Add(baseCommand);
StartCoroutine(CooldownCoroutine(baseCommand, commandInfo.cooldown));
}
TwitchManager.OnCommandReceived(twitchUser, commandInfo);
CallCommand(baseCommand, args);
}
private static void OnJoinedToChat()
{
//EXAMPLE:
//:danqzq!danqzq@danqzq.tmi.twitch.tv JOIN #danqzq
Log("Twitch client successfully connected to the chat!", "green");
TwitchManager.OnJoinedToChat();
_isConnecting = false;
}
private void CallCommand(string commandName, IReadOnlyList<string> args)
{
var method = _methodsDict[commandName];
var parameters = _methodParameters[method];
var filteredArgs = new object[parameters.Length];
for (var i = 0; i < args.Count; i++)
{
object value;
if (parameters[i].ParameterType == typeof(int))
value = int.Parse(args[i]);
else if (parameters[i].ParameterType == typeof(float))
value = float.Parse(args[i]);
else if (parameters[i].ParameterType == typeof(bool))
value = bool.Parse(args[i]);
else if (parameters[i].ParameterType == typeof(string))
value = args[i];
else throw new TwitchCommandException(
"Twitch command arguments can only be int, float, bool, or string");
filteredArgs[i] = value;
}
Debug.Log("Calling command " + commandName);
if (!_methodBehaviours.ContainsKey(method)) return;
_methodBehaviours[method].ForEach(behaviour => method.Invoke(behaviour, filteredArgs));
}
private IEnumerator CooldownCoroutine(string command, float time)
{
yield return new WaitForSeconds(time);
CommandsOnCooldown.Remove(command);
}
private static void Log(string message, string color)
{
if (_settings.isDebugMode) print($"<color={color}>{message}</color>");
}
#region Unity Callbacks
private void Awake()
{
CommandsOnCooldown = new List<string>();
_settings = Resources.Load<TwitchSettings>(TwitchSettingsPath);
if (_settings.initializeOnAwake) StartCoroutine(WaitForAuthentication());
}
private static IEnumerator WaitForAuthentication()
{
yield return new WaitUntil(() => TwitchManager.IsAuthenticated);
if (!IsInitialized) Initialize();
}
private void OnDestroy()
{
_streamReader?.Close();
_streamWriter?.Close();
_twitchClient?.Close();
}
private void Update()
{
if (!IsInitialized) return;
if (!IsConnected)
Connect();
ReadChat();
}
#endregion
}
}
答案1
得分: 1
The Twitch IRC API says you must reply to their keepalive message to stay connected: https://dev.twitch.tv/docs/irc/#keepalive-messages
Server: PING :tmi.twitch.tv
Client: PONG :tmi.twitch.tv
英文:
The Twitch IRC API says you must reply to their keepalive message to stay connected: https://dev.twitch.tv/docs/irc/#keepalive-messages
Server: PING :tmi.twitch.tv
Client: PONG :tmi.twitch.tv
答案2
得分: 0
以下是翻译好的代码部分:
如果其他人遇到问题,这是我用来使其工作的代码。我将它放在Update中。
float PingCounter = 0;
PingCounter += Time.deltaTime;
if (PingCounter > 60)
{
_streamWriter.WriteLine("PING" + _twitchClient);
_streamWriter.Flush();
PingCounter = 0;
}
英文:
If anyone else gets stuck, this is the code I used to get it working. I have placed it in Update
float PingCounter =0;
PingCounter += Time.deltaTime;
if(PingCounter > 60)
{
_streamWriter.WriteLine("PING" + _twitchClient);
_streamWriter.Flush();
PingCounter = 0;
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论