Last active
December 12, 2024 09:00
-
-
Save n0mimono/5da7111a52c4c586f7e2d563f1b63a53 to your computer and use it in GitHub Desktop.
YouTubeLiveChat for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
usage example