Skip to content

Instantly share code, notes, and snippets.

@n0mimono
Last active December 12, 2024 09:00
Show Gist options
  • Save n0mimono/5da7111a52c4c586f7e2d563f1b63a53 to your computer and use it in GitHub Desktop.
Save n0mimono/5da7111a52c4c586f7e2d563f1b63a53 to your computer and use it in GitHub Desktop.
YouTubeLiveChat for Unity
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System.Linq;
using System.Threading;
using System.Text;
namespace YouTubeLiveChat
{
public class YouTubeLiveChat
{
private readonly string _videoId;
private readonly Fetcher _fetcher;
private readonly LiveChatParamsFetcher _liveParamsFetcher;
private readonly LiveChatFetcher _liveChatFetcher;
private class State
{
public bool IsInitialized { get; set; }
public LiveChatParams LiveChatParams { get; set; }
}
private State _state = new();
private readonly string _liveParamsFetchUrl = "https://www.youtube.com/live_chat";
private readonly string _liveChatFetchUrl = "https://www.youtube.com/youtubei/v1/live_chat/get_live_chat";
private readonly string _userAgent = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) WebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36";
public YouTubeLiveChat(string videoId)
{
_videoId = videoId;
_fetcher = new(_userAgent);
_liveParamsFetcher = new(_liveParamsFetchUrl, _fetcher);
_liveChatFetcher = new(_liveChatFetchUrl, _fetcher);
_state.IsInitialized = false;
}
public async UniTask<LiveChatData> Next(CancellationToken cancellationToken)
{
if (!_state.IsInitialized)
{
_state.LiveChatParams = await _liveParamsFetcher.Get(_videoId, cancellationToken);
if (!_state.LiveChatParams.IsValid)
{
return new LiveChatData
{
Continuation = null,
Renderers = new LiveChatTextMessageRenderer[0],
Status = LiveChatDataStatus.ParamsError
};
}
_state.IsInitialized = true;
}
var liveChatData = await _liveChatFetcher.Post(_state.LiveChatParams, cancellationToken);
if (liveChatData.Continuation != null)
{
_state.LiveChatParams = new LiveChatParams
{
Key = _state.LiveChatParams.Key,
Continuation = liveChatData.Continuation,
VisitorData = _state.LiveChatParams.VisitorData,
ClientVersion = _state.LiveChatParams.ClientVersion
};
}
return liveChatData;
}
}
public struct LiveChatParams
{
public string Key { get; set; }
public string Continuation { get; set; }
public string VisitorData { get; set; }
public string ClientVersion { get; set; }
public bool IsValid => Key != null && Continuation != null && VisitorData != null && ClientVersion != null;
}
public struct LiveChatData
{
public LiveChatTextMessageRenderer[] Renderers { get; set; }
public string Continuation { get; set; }
public LiveChatDataStatus Status { get; set; }
}
public enum LiveChatDataStatus
{
ParamsError,
FetchError,
NoContents,
NoContinuations,
NoActions,
AnyActions,
}
[Serializable]
public class LiveChatRequestData
{
[Serializable]
public class Context
{
[Serializable]
public class Client
{
public string visitorData;
public string userAgent;
public string clientName;
public string clientVersion;
}
public Client client;
}
public Context context;
public string continuation;
}
[Serializable]
public class LiveChatResponseData
{
[Serializable]
public class ContinuationContents
{
[Serializable]
public class LiveChatContinuation
{
[Serializable]
public class Continuation
{
[Serializable]
public class InvalidationContinuationData
{
public string continuation;
}
public InvalidationContinuationData invalidationContinuationData;
}
public Continuation[] continuations;
[Serializable]
public class Action
{
[Serializable]
public class AddChatItemAction
{
[Serializable]
public class Item
{
public LiveChatTextMessageRenderer liveChatTextMessageRenderer;
}
public Item item;
}
public AddChatItemAction addChatItemAction;
}
public Action[] actions;
}
public LiveChatContinuation liveChatContinuation;
}
public ContinuationContents continuationContents;
}
[Serializable]
public class LiveChatTextMessageRenderer
{
[Serializable]
public class Message
{
[Serializable]
public class Run
{
public string text;
}
public Run[] runs;
}
public Message message;
[Serializable]
public class AuthorName
{
public string simpleText;
}
public AuthorName authorName;
[Serializable]
public class AuthorPhoto
{
[Serializable]
public class Thumbnail
{
public string url;
public int width;
public int height;
}
public Thumbnail[] thumbnails;
}
public AuthorPhoto authorPhoto;
public string id;
public string timestampUsec;
public string authorExternalChannelId;
}
public class LiveChatParamsFetcher
{
private string _url;
private Fetcher _fetcher;
public LiveChatParamsFetcher(string url, Fetcher fetcher)
{
_url = url;
_fetcher = fetcher;
}
public async UniTask<LiveChatParams> Get(string videoId, CancellationToken cancellationToken)
{
var url = $"{_url}?v={videoId}";
var (text, success) = await _fetcher.Get(url, cancellationToken);
if (!success)
{
return default;
}
var key = ExtractMatch(text, "\"INNERTUBE_API_KEY\":\"(.+?)\"");
var continuation = ExtractMatch(text, "\"continuation\":\"(.+?)\"");
var visitorData = ExtractMatch(text, "\"visitorData\":\"(.+?)\"");
var clientVersion = ExtractMatch(text, "\"clientVersion\":\"(.+?)\"");
return new LiveChatParams
{
Key = key,
Continuation = continuation,
VisitorData = visitorData,
ClientVersion = clientVersion
};
}
static string ExtractMatch(string html, string pattern)
{
var match = Regex.Match(html, pattern);
return match.Success ? match.Groups[1].Value : null;
}
}
public class LiveChatFetcher
{
private string _url;
private Fetcher _fetcher;
public LiveChatFetcher(string url, Fetcher fetcher)
{
_url = url;
_fetcher = fetcher;
}
public static LiveChatRequestData CreateRequestData(LiveChatParams liveParams, string userAgent)
{
return new LiveChatRequestData
{
context = new LiveChatRequestData.Context
{
client = new LiveChatRequestData.Context.Client
{
visitorData = liveParams.VisitorData,
userAgent = userAgent,
clientName = "WEB",
clientVersion = liveParams.ClientVersion
}
},
continuation = liveParams.Continuation,
};
}
public async UniTask<LiveChatData> Post(LiveChatParams liveParams, CancellationToken cancellationToken)
{
var data = CreateRequestData(liveParams, _fetcher.userAgent);
var url = $"{_url}?key={liveParams.Key}";
var (text, success) = await _fetcher.Post(url, JsonUtility.ToJson(data), cancellationToken);
if (!success)
{
return new LiveChatData
{
Continuation = null,
Renderers = new LiveChatTextMessageRenderer[0],
Status = LiveChatDataStatus.FetchError
};
}
var response = JsonUtility.FromJson<LiveChatResponseData>(text);
if (response == null || response.continuationContents == null || response.continuationContents.liveChatContinuation == null)
{
return new LiveChatData
{
Continuation = null,
Renderers = new LiveChatTextMessageRenderer[0],
Status = LiveChatDataStatus.NoContents
};
}
var continuations = response.continuationContents.liveChatContinuation.continuations;
if (continuations == null || continuations.Length == 0)
{
return new LiveChatData
{
Continuation = null,
Renderers = new LiveChatTextMessageRenderer[0],
Status = LiveChatDataStatus.NoContinuations
};
}
var continuation = continuations.First().invalidationContinuationData.continuation;
var actions = response.continuationContents.liveChatContinuation.actions;
if (actions == null || actions.Length == 0)
{
return new LiveChatData
{
Continuation = continuation,
Renderers = new LiveChatTextMessageRenderer[0],
Status = LiveChatDataStatus.NoActions
};
}
var renderers = actions.Select(action =>
{
return action.addChatItemAction.item.liveChatTextMessageRenderer;
}).ToArray();
return new LiveChatData
{
Continuation = continuation,
Renderers = renderers,
Status = LiveChatDataStatus.AnyActions
};
}
}
public class Fetcher
{
public readonly string userAgent;
public Fetcher(string userAgent)
{
this.userAgent = userAgent;
}
public async UniTask<(string, bool)> Get(string url, CancellationToken cancellationToken)
{
try
{
using var webRequest = UnityWebRequest.Get(url);
webRequest.SetRequestHeader("User-Agent", userAgent);
var (isCanceld, _) = await webRequest.SendWebRequest().WithCancellation(cancellationToken).SuppressCancellationThrow();
if (isCanceld)
{
return (string.Empty, false);
}
if (webRequest.result != UnityWebRequest.Result.Success)
{
return (string.Empty, false);
}
var text = webRequest.downloadHandler.text;
return (text, true);
}
catch (Exception e)
{
Debug.LogError(e);
}
return (string.Empty, false);
}
public async UniTask<(string, bool)> Post(string url, string data, CancellationToken cancellationToken)
{
try
{
using var webRequest = new UnityWebRequest(url, "POST")
{
uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(data)),
downloadHandler = new DownloadHandlerBuffer()
};
webRequest.SetRequestHeader("User-Agent", userAgent);
webRequest.SetRequestHeader("Content-Type", "application/json");
var (isCanceld, _) = await webRequest.SendWebRequest().WithCancellation(cancellationToken).SuppressCancellationThrow();
if (isCanceld)
{
return (string.Empty, false);
}
if (webRequest.result != UnityWebRequest.Result.Success)
{
return (string.Empty, false);
}
var text = webRequest.downloadHandler.text;
return (text, true);
}
catch (Exception e)
{
Debug.LogError(e);
}
return (string.Empty, false);
}
}
}
@n0mimono
Copy link
Author

usage example

using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Threading;

public class NewMonoBehaviourScript : MonoBehaviour
{
    [SerializeField] private string _videoId;
    [SerializeField] private int _interval = 5000;

    private CancellationTokenSource _cts = new();

    void Start()
    {
        Chat().Forget();
    }

    async UniTaskVoid Chat()
    {
        var liveChat = new YouTubeLiveChat.YouTubeLiveChat(_videoId);

        while (true)
        {
            var data = await liveChat.Next(_cts.Token);
            Debug.Log(data.Status);
            Debug.Log(data.Continuation);
            foreach (var renderer in data.Renderers)
            {
                Debug.Log(JsonUtility.ToJson(renderer));
            }

            await UniTask.Delay(_interval);
        }
    }

    void OnDestroy()
    {
        _cts.Cancel();
        _cts.Dispose();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment