Twitch聊天在Unity游戏项目中10分钟后超时

huangapple go评论72阅读模式
英文:

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;
}

huangapple
  • 本文由 发表于 2023年4月6日 22:22:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/75950629.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定