Skip to content

Commit

Permalink
Merge pull request #72 from frankvHoof93/development
Browse files Browse the repository at this point in the history
V1.1.0 - Bugfix WebSocket
  • Loading branch information
frankvHoof93 authored Mar 24, 2024
2 parents 896a93f + 5375783 commit ac4390b
Show file tree
Hide file tree
Showing 35 changed files with 542 additions and 203 deletions.
25 changes: 24 additions & 1 deletion CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] -
## [1.1.0] -

Bugfix for failing/closing Websocket-Connections

### Added

- Added ControlAction-Enum
- Added OnLiveResumed-Event
- Added Reconnect-Timeout used when an exception occurs whilst connecting
- Added full log of (outer) message when logging unparsed data

### Fixed

- Removed br from accepted encodings
- Fixed an issue with Websocket-Headers & -Parameters causing the SocketConnection to close instantly.

### Changed

- Changed Signing to use new Fetch-Endpoint
- Changed some parameters in the Unity-SampleScene to reduce overhead for default users.
- Removed unneccesary memory allocations in Dispatcher-script


## [1.0.0] - 2023-10-09

Initial full release of TikTokLiveSharp/TikTokLiveUnity

Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Frank van Hoof
Copyright (c) 2024 Frank van Hoof

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TikTokLiveSharp / TikTokLiveUnity v1.0.0
# TikTokLiveSharp / TikTokLiveUnity v1.1.0

[![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/frankvhoof93/ )
![Issues](https://img.shields.io/github/issues/frankvHoof93/TikTokLiveSharp)
Expand Down
2 changes: 1 addition & 1 deletion Setup_CSharp.MD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TikTokLiveSharp v1.0.0
# TikTokLiveSharp v1.1.0

## Downloading the Source Code
Download the TikTokLiveSharp-SourceCode from the [Releases-Page](https://github.com/frankvHoof93/TikTokLiveSharp/releases/)
Expand Down
2 changes: 1 addition & 1 deletion Setup_Unity.MD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TikTokLiveUnity v1.0.0
# TikTokLiveUnity v1.1.0

## Installing the Project in Unity
- Open the Package Manager Window **(Window -> Package Manager)**
Expand Down
12 changes: 10 additions & 2 deletions TikTokLiveSharp/Client/Config/ClientSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ namespace TikTokLiveSharp.Client.Config
public struct ClientSettings
{
/// <summary>
/// Timeout for Connections (in Seconds)
/// Timeout for HTTP-Connections (in Seconds)
/// </summary>
#if UNITY
[UnityEngine.Header("Settings")]
[UnityEngine.Tooltip("Timeout for Connections (in Seconds)")]
[UnityEngine.Tooltip("Timeout for HTTP-Connections (in Seconds)")]
#endif
public float Timeout;

/// <summary>
/// Interval before re-attempting a failed connection
/// </summary>
#if UNITY
[UnityEngine.Tooltip("Interval before re-attempting a failed connection")]
#endif
public float ReconnectInterval;

/// <summary>
/// Polling-Interval for Socket-Connection (in Seconds)
/// </summary>
Expand Down
25 changes: 16 additions & 9 deletions TikTokLiveSharp/Client/Config/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class Constants
/// <summary>
/// Signing API by Isaac Kogan
/// </summary>
public const string TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/sign_url";
public const string TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/fetch";

/// <summary>
/// Default TimeOut for Connections
Expand All @@ -28,14 +28,19 @@ public static class Constants
/// <summary>
/// Default Polling-Time for Socket-Connection
/// </summary>
public const float DEFAULT_POLLTIME = .5f;
public const float DEFAULT_POLLTIME = 1f;
/// <summary>
/// Default Reconnection-Interval
/// </summary>
public const float DEFAULT_RECONNECT_TIMEOUT = 1f;

/// <summary>
/// Default Settings for Client
/// </summary>
public static readonly ClientSettings DEFAULT_SETTINGS = new ClientSettings()
{
Timeout = DEFAULT_TIMEOUT,
ReconnectInterval = DEFAULT_RECONNECT_TIMEOUT,
PollingInterval = DEFAULT_POLLTIME,
ClientLanguage = "en-US",
EnableCompression = true,
Expand Down Expand Up @@ -63,7 +68,7 @@ public static class Constants
{ "browser_name", "Mozilla" },
{ "browser_online", true },
{ "browser_platform", "Win32" },
{ "browser_version", "5.0 (Windows NT 2010.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" },
{ "browser_version", "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36" },
{ "cookie_enabled", true },
{ "cursor", "" },
{ "internal_ext", "" },
Expand All @@ -75,16 +80,18 @@ public static class Constants
{ "is_page_visible", true },
{ "did_rule", 3 },
{ "fetch_rule", 1 },
{ "last_rtt", 0 },
{ "live_id", 12 },
{ "resp_content_type", "protobuf" },
{ "screen_height", 1152 },
{ "screen_width", 2048 },
{ "tz_name", "Europe/London" },
{ "referer", "https, //www.tiktok.com/" },
{ "root_referer", "https, //www.tiktok.com/" },
{ "msToken", ""},
{ "version_code", 180800},
{ "webcast_sdk_version", "1.3.2" },
{ "update_version_code", "1.3.2" }
{ "msToken", "" },
{ "version_code", 180800 },
{ "webcast_sdk_version", "1.3.0" },
{ "update_version_code", "1.3.0" }
});

/// <summary>
Expand All @@ -93,15 +100,15 @@ public static class Constants
public static readonly IReadOnlyDictionary<string, string> DEFAULT_REQUEST_HEADERS = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
{
{ "Connection", "keep-alive" },
{ "authority", "www.tiktok.com" },
{ "Cache-Control", "no-cache" },
{ "Accept", "text/html,application/json,application/protobuf" },
{ "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36" },
{ "Referer", "https://www.tiktok.com/" },
{ "Origin", "https://www.tiktok.com" },
{ "Accept-Language", "en-US,en; q=0.8" }

});

public static readonly KeyValuePair<string, string> COMPRESSION_HEADER = new KeyValuePair<string, string>( "Accept-Encoding", "gzip, deflate, br" );
public static readonly KeyValuePair<string, string> COMPRESSION_HEADER = new KeyValuePair<string, string>( "Accept-Encoding", "gzip, deflate" );
}
}
7 changes: 7 additions & 0 deletions TikTokLiveSharp/Client/HTTP/ITikTokHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public interface ITikTokHttpRequest
/// <exception cref="Exception">Requests should not be reused</exception>
Task<HttpContent> Post(HttpContent content);

/// <summary>
/// Sends an async get request
/// </summary>
/// <returns>HttpResponse-Message (including Headers)</returns>
/// <exception cref="Exception">Requests should not be reused</exception>
Task<HttpResponseMessage> GetResponse();

/// <summary>
/// Sets the queries for the request
/// </summary>
Expand Down
116 changes: 67 additions & 49 deletions TikTokLiveSharp/Client/HTTP/TikTokHTTPClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using System.Text;
using System.Threading.Tasks;
using TikTokLiveSharp.Client.Config;
using TikTokLiveSharp.Client.Socket;
using TikTokLiveSharp.Errors.Connections;
using TikTokLiveSharp.Errors.Permissions;
using TikTokLiveSharp.Models.Protobuf.Messages;

Expand Down Expand Up @@ -73,9 +75,9 @@ internal TikTokHttpClient(TimeSpan timeout, bool enableCompression = true, IWebP
/// <param name="parameters">Additional Parameters for Request</param>
/// <param name="signUrl">Whether to sign URL using API</param>
/// <returns>Task to Await. Result is Response from WebCast-API</returns>
internal async Task<Response> GetDeserializedMessage(string path, IDictionary<string, object> parameters = null, bool signUrl = false)
internal async Task<Response> GetDeserializedMessage(string path, IDictionary<string, object> parameters = null)
{
HttpContent get = await GetRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters, signUrl);
HttpContent get = await GetRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters);
return Serializer.Deserialize<Response>(await get.ReadAsStreamAsync());
}

Expand All @@ -86,9 +88,9 @@ internal async Task<Response> GetDeserializedMessage(string path, IDictionary<st
/// <param name="parameters">Additional Parameters for Request</param>
/// <param name="signUrl">Whether to sign URL using API</param>
/// <returns>Task to Await. Result is JSON-Object</returns>
internal async Task<JObject> GetJObjectFromWebcastApi(string path, IDictionary<string, object> parameters = null, bool signUrl = false)
internal async Task<JObject> GetJObjectFromWebcastApi(string path, IDictionary<string, object> parameters = null)
{
HttpContent get = await GetRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters, signUrl);
HttpContent get = await GetRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters);
return JObject.Parse(await get.ReadAsStringAsync());
}

Expand All @@ -98,9 +100,9 @@ internal async Task<JObject> GetJObjectFromWebcastApi(string path, IDictionary<s
/// <param name="uniqueId">ID for Host</param>
/// <param name="signUrl">Whether to sign URL using API</param>
/// <returns>Task to Await. Result is HTML for Live-Page</returns>
internal async Task<string> GetLivestreamPage(string uniqueId, IDictionary<string, object> parameters = null, bool signUrl = false)
internal async Task<string> GetLivestreamPage(string uniqueId, IDictionary<string, object> parameters = null)
{
HttpContent get = await GetRequest($"{Constants.TIKTOK_URL_WEB}@{uniqueId}/live/", parameters, signUrl);
HttpContent get = await GetRequest($"{Constants.TIKTOK_URL_WEB}@{uniqueId}/live/", parameters);
return await get.ReadAsStringAsync();
}

Expand All @@ -110,12 +112,61 @@ internal async Task<string> GetLivestreamPage(string uniqueId, IDictionary<strin
/// <param name="uniqueId">ID for Host</param>
/// <param name="signUrl">Whether to sign URL using API</param>
/// <returns>Task to Await. Result is HTML for Profile-Page</returns>
internal async Task<string> GetProfilePage(string uniqueId, IDictionary<string, object> parameters = null, bool signUrl = false)
internal async Task<string> GetProfilePage(string uniqueId, IDictionary<string, object> parameters = null)
{
HttpContent get = await GetRequest($"{Constants.TIKTOK_URL_WEB}@{uniqueId}", parameters, signUrl);
HttpContent get = await GetRequest($"{Constants.TIKTOK_URL_WEB}@{uniqueId}", parameters);
return await get.ReadAsStringAsync();
}

internal async Task<TikTokWebSocketConnectionData> GetSignedWebsocketData(string roomId)
{
ITikTokHttpRequest request = new TikTokHttpRequest(Constants.TIKTOK_SIGN_API, false, false)
.SetQueries(new Dictionary<string, object>()
{
{ "client", CLIENT_NAME },
{ "uuc", clientNum },
{ "room_id", roomId }
});
HttpResponseMessage response = await request.GetResponse();
if (response.StatusCode == HttpStatusCode.NotFound)
throw new HttpRequestException("Request responded with 404 NOT_FOUND");
if (!response.IsSuccessStatusCode)
{
if ((int)response.StatusCode == 429) // TooManyRequests
{
if (response.Headers.TryGetValues("RateLimit-Reset", out IEnumerable<string> rateHeaders))
{
TimeSpan span = TimeSpan.FromSeconds(long.Parse(rateHeaders.First()));
throw new HttpRequestException($"[{(int)response.StatusCode}] Rate Limit Reached. Try again in {span:mm\\:ss}.");
}
else
{
throw new HttpRequestException($"[{(int)response.StatusCode}] Rate Limit Reached.");
}
}
else
{
throw new HttpRequestException($"Request was unsuccessful [{(int)response.StatusCode}].");
}
}
if (response.Headers.TryGetValues("x-set-tt-cookie", out IEnumerable<string> cookieHeaders))
{
try
{
Response signingResponse = Serializer.Deserialize<Response>(await response.Content.ReadAsStreamAsync());
return new TikTokWebSocketConnectionData(roomId, cookieHeaders.First(), signingResponse);
}
catch (Exception ex)
{
throw new SignConnectionException("Could not parse response from Signing-Server.", ex);
}
}
else
{
throw new SignConnectionException("Signing-Server did not return required header(s).");
}
}

/// <summary>
/// Posts JSON-Object to WebCast-API
/// </summary>
Expand All @@ -124,9 +175,9 @@ internal async Task<string> GetProfilePage(string uniqueId, IDictionary<string,
/// <param name="json">Object to Post</param>
/// <param name="signUrl">Whether to sign URL using API</param>
/// <returns>Task to Await. Result is result of Post</returns>
internal async Task<JObject> PostJObjectToWebcastApi(string path, IDictionary<string, object> parameters, JObject json, bool signUrl = false)
internal async Task<JObject> PostJObjectToWebcastApi(string path, IDictionary<string, object> parameters, JObject json)
{
HttpContent post = await PostRequest(Constants.TIKTOK_URL_WEBCAST + path, json.ToString(Newtonsoft.Json.Formatting.None), parameters, signUrl);
HttpContent post = await PostRequest(Constants.TIKTOK_URL_WEBCAST + path, json.ToString(Newtonsoft.Json.Formatting.None), parameters);
return JObject.Parse(await post.ReadAsStringAsync());
}
#endregion
Expand All @@ -150,9 +201,9 @@ private static ITikTokHttpRequest BuildRequest(string url, bool enableCompressio
/// <param name="parameters">Additional Parameters for Request</param>
/// <param name="signUrl">Whether to Sign URL using API</param>
/// <returns>Task to await</returns>
private async Task<HttpContent> GetRequest(string url, IDictionary<string, object> parameters = null, bool signUrl = false)
private async Task<HttpContent> GetRequest(string url, IDictionary<string, object> parameters = null)
{
ITikTokHttpRequest request = BuildRequest(signUrl ? await GetSignedUrl(url, parameters) : url, compression, signUrl ? null : parameters);
ITikTokHttpRequest request = BuildRequest(url, compression, parameters);
return await request.Get();
}

Expand All @@ -163,9 +214,9 @@ private async Task<HttpContent> GetRequest(string url, IDictionary<string, objec
/// <param name="parameters">Additional Parameters for Request</param>
/// <param name="signUrl">Whether to Sign URL using API</param>
/// <returns>Task to await</returns>
private async Task<HttpContent>PostRequest(string url, IDictionary<string, object> parameters = null, bool signUrl = false)
private async Task<HttpContent> PostRequest(string url, IDictionary<string, object> parameters = null)
{
ITikTokHttpRequest request = BuildRequest(signUrl ? await GetSignedUrl(url, parameters) : url, compression, signUrl ? null : parameters);
ITikTokHttpRequest request = BuildRequest(url, compression, parameters);
return await request.Post(null);
}

Expand All @@ -177,44 +228,11 @@ private async Task<HttpContent>PostRequest(string url, IDictionary<string, objec
/// <param name="parameters">Additional Parameters for Request</param>
/// <param name="signUrl">Whether to Sign URL using API</param>
/// <returns>Task to await</returns>
private async Task<HttpContent> PostRequest(string url, string data, IDictionary<string, object> parameters = null, bool signUrl = false)
private async Task<HttpContent> PostRequest(string url, string data, IDictionary<string, object> parameters = null)
{
ITikTokHttpRequest request = BuildRequest(signUrl ? await GetSignedUrl(url, parameters) : url, compression, signUrl ? null : parameters);
ITikTokHttpRequest request = BuildRequest(url, compression, parameters);
return await request.Post(new StringContent(data, Encoding.UTF8));
}

/// <summary>
/// Signs URL using WebAPI
/// </summary>
/// <param name="url">URL to sign</param>
/// <param name="parameters">Additional Parameters for Request</param>
/// <returns>Task to await. Result is Signed URL</returns>
/// <exception cref="InsufficientPermissionException"></exception>
private async Task<string> GetSignedUrl(string url, IDictionary<string, object> parameters = null)
{
string getParams = parameters != null ? $"?{string.Join("&", parameters.Select(x => $"{x.Key}={x.Value}"))}" : string.Empty;
ITikTokHttpRequest request = new TikTokHttpRequest(Constants.TIKTOK_SIGN_API, false)
.SetQueries(new Dictionary<string, object>()
{
{ "client", CLIENT_NAME },
{ "uuc", clientNum },
{ "url", url + getParams }
});
HttpContent content = await request.Get();
try
{
JObject jObj = JObject.Parse(await content.ReadAsStringAsync());
string signedUrl = jObj["signedUrl"]?.Value<string>();
string userAgent = jObj["User-Agent"]?.Value<string>();
TikTokHttpRequest.CurrentHeaders.Remove("User-Agent");
TikTokHttpRequest.CurrentHeaders.Add("User-Agent", userAgent);
return signedUrl;
}
catch (Exception e)
{
throw new InsufficientPermissionException("Insufficient values have been supplied for signing. Likely due to an update. Post an issue on GitHub or in the Discord.", e);
}
}
#endregion
#endregion
}
Expand Down
Loading

0 comments on commit ac4390b

Please sign in to comment.