diff --git a/utils.bat b/utils.bat deleted file mode 100644 index a4511c2..0000000 --- a/utils.bat +++ /dev/null @@ -1 +0,0 @@ -call %WINDIR%\Microsoft.NET\Framework\v4.0.30319\csc.exe -debug utils.cs \ No newline at end of file diff --git a/utils.cs b/utils.cs deleted file mode 100644 index 830790f..0000000 --- a/utils.cs +++ /dev/null @@ -1,1537 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Runtime.Serialization; -using System.Reflection; -using System.Threading; -using System.Net; -using System.IO; -using System.Diagnostics; - -namespace TwitchAdUtils -{ - class Program - { - static string ClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko";//ilfexgv3nnljz3isbm257gzwrzr7bi - Xtra for Twitch - static string UserAgentChrome = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"; - static string UserAgentFirefox = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"; - static string UserAgent = UserAgentChrome; - static bool UseOldAccessToken = false; - static bool UseAccessTokenTemplate = false; - static bool ShouldNotifyAdWatched = false; - static bool ShouldNotifyAdWatchedMin = true; - static bool ShouldDenyAd = false; - static bool UseFastBread = true;// fast_bread (EXT-X-TWITCH-PREFETCH) - static string PlayerTypeNormal = "site";//embed squad_secondary squad_primary - static string PlayerTypeMiniNoAd = "picture-by-picture";//"thunderdome"; - static string PlayerTypeEmbed = "embed"; - static string Platform = "web"; - static string PlayerBackend = "mediaplayer"; - static string MainM3U8AdditionalParams = ""; - static string AdSignifier = "stitched-ad"; - static string ProxyUrl = ""; - static int TargetResolution = 480; - static TimeSpan LoopDelay = TimeSpan.FromSeconds(1); - - enum RunnerMode - { - Normal, - MiniNoAd, - Proxy, - Embed - } - - static void Main(string[] args) - { - ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; - ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - - if (args.Length >= 1 && args[0] == "build_scripts") - { - // This takes "base.user.js" and updates all of the other scripts based on the cfg values - BuildScripts(); - return; - } - if (args.Length >= 1 && args[0] == "m3u8") - { - // Tests modifications of m3u8 files - Console.WriteLine("Starting local server (http://localhost)"); - TwitchTestServer testServer = new TwitchTestServer(); - testServer.Start(80); - Console.ReadLine(); - return; - } - - Console.Write("Enter channel name: "); - string channel = Console.ReadLine().ToLower(); - Console.WriteLine("Fetching channel '" + channel + "'"); - RunImpl(RunnerMode.Normal, channel); - //RunImpl(RunnerMode.Embed, channel); - //RunImpl(RunnerMode.MiniNoAd, channel); - } - - static void BuildScripts() - { - string[] deprecated = { }; - string baseScriptName = "base"; - string suffixConfg = ".cfg"; - string suffixUserscript = ".user.js"; - string suffixUblock = "-ublock-origin.js"; - string baseFile = Path.Combine(baseScriptName, baseScriptName + ".user.js"); - if (File.Exists(baseFile)) - { - foreach (string dir in Directory.GetDirectories(Environment.CurrentDirectory)) - { - DirectoryInfo dirInfo = new DirectoryInfo(dir); - if (dirInfo.Name != baseScriptName && !deprecated.Contains(dirInfo.Name)) - { - string cfgFile = Path.Combine(dir, dirInfo.Name + suffixConfg); - string userscriptFile = Path.Combine(dir, dirInfo.Name + suffixUserscript); - string ublockFile = Path.Combine(dir, dirInfo.Name + suffixUblock); - if (File.Exists(userscriptFile) && File.Exists(ublockFile) && File.Exists(cfgFile)) - { - Dictionary cfgValues = new Dictionary(); - string[] cfgLines = File.ReadAllLines(cfgFile); - for (int i = 0; i < cfgLines.Length; i++) - { - string line = cfgLines[i]; - if (!string.IsNullOrEmpty(line)) - { - int spaceIndex = line.IndexOf(' '); - if (spaceIndex > 0) - { - cfgValues["scope." + line.Substring(0, spaceIndex).Trim() + " "] = line.Substring(spaceIndex + 1).Trim(); - } - } - } - Console.WriteLine(dir); - foreach (KeyValuePair val in cfgValues) - { - Console.WriteLine(val.Key + "= " + val.Value); - } - Console.WriteLine("============================="); - - StringBuilder sbUserscript = new StringBuilder(); - StringBuilder sbUblock = new StringBuilder(); - string[] lines = File.ReadAllLines(baseFile); - bool modifiedOptions = false; - bool foundUserScriptEnd = false; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - string lineTrimmed = line.Trim(); - if (lineTrimmed.StartsWith("// Modify options based on mode")) - { - modifiedOptions = true; - } - if (lineTrimmed.StartsWith("// @name ")) - { - line = line += " (" + dirInfo.Name + ")"; - } - if (lineTrimmed.StartsWith("// @description")) - { - string url = "https://github.com/pixeltris/TwitchAdSolutions/raw/master/" + dirInfo.Name + "/" + dirInfo.Name + suffixUserscript; - sbUserscript.AppendLine("// @updateURL " + url); - sbUserscript.AppendLine("// @downloadURL " + url); - line = line += " (" + dirInfo.Name + ")"; - } - if (!modifiedOptions) - { - if (!foundUserScriptEnd) - { - sbUserscript.AppendLine(line); - if (line.Contains("/UserScript")) - { - sbUblock.AppendLine("twitch-videoad.js application/javascript"); - foundUserScriptEnd = true; - } - } - else if (lineTrimmed.StartsWith("'use strict'")) - { - sbUserscript.AppendLine(line); - sbUblock.AppendLine(" if ( /(^|\\.)twitch\\.tv$/.test(document.location.hostname) === false ) { return; }"); - } - else - { - foreach (KeyValuePair val in cfgValues) - { - if (line.Contains(val.Key)) - { - line = line.Substring(0, line.IndexOf(val.Key) + val.Key.Length) + "= " + val.Value + ";"; - break; - } - } - sbUserscript.AppendLine(line); - sbUblock.AppendLine(line); - } - } - else - { - sbUserscript.AppendLine(line); - sbUblock.AppendLine(line); - } - } - File.WriteAllText(userscriptFile, sbUserscript.ToString()); - File.WriteAllText(ublockFile, sbUblock.ToString()); - } - } - } - } - using (WebClient wc = new WebClient()) - { - string response = null, token = null, sig = null; - wc.Proxy = null; - string code = wc.DownloadString("https://raw.githubusercontent.com/cleanlock/VideoAdBlockForTwitch/master/chrome/remove_video_ads.js"); - List lines = code.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - for (int i = lines.Count - 1; i >= 0; i--) - { - if (string.IsNullOrWhiteSpace(lines[i])) - { - lines.RemoveAt(i); - } - else - { - lines[i] = " " + lines[i]; - } - } - string manifestStr = wc.DownloadString("https://raw.githubusercontent.com/cleanlock/VideoAdBlockForTwitch/master/chrome/manifest.json"); - ChromeExtensionManifest manifest = JSONSerializer.DeSerialize(manifestStr); - Console.WriteLine("vaft: " + manifest.version); - - string comment = "// This code is directly copied from https://github.com/cleanlock/VideoAdBlockForTwitch (only change is whitespace is removed for the ublock origin script - also indented)"; - - StringBuilder sbUserscript = new StringBuilder(); - sbUserscript.AppendLine("// ==UserScript=="); - sbUserscript.AppendLine("// @name TwitchAdSolutions (vaft)"); - sbUserscript.AppendLine("// @namespace https://github.com/pixeltris/TwitchAdSolutions"); - sbUserscript.AppendLine("// @version " + manifest.version); - sbUserscript.AppendLine("// @description Multiple solutions for blocking Twitch ads (vaft)"); - sbUserscript.AppendLine("// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js"); - sbUserscript.AppendLine("// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js"); - sbUserscript.AppendLine("// @author https://github.com/cleanlock/VideoAdBlockForTwitch#credits"); - sbUserscript.AppendLine("// @match *://*.twitch.tv/*"); - sbUserscript.AppendLine("// @run-at document-start"); - sbUserscript.AppendLine("// @grant none"); - sbUserscript.AppendLine("// ==/UserScript=="); - sbUserscript.AppendLine(comment); - sbUserscript.AppendLine("(function() {"); - sbUserscript.AppendLine(" 'use strict';"); - - StringBuilder sbUblock = new StringBuilder(); - sbUblock.AppendLine(comment); - sbUblock.AppendLine("twitch-videoad.js application/javascript"); - sbUblock.AppendLine("(function() {"); - sbUblock.AppendLine(" if ( /(^|\\.)twitch\\.tv$/.test(document.location.hostname) === false ) { return; }"); - - foreach (string line in lines) - { - sbUserscript.AppendLine(line); - sbUblock.AppendLine(line); - } - - sbUserscript.AppendLine("})();"); - sbUblock.AppendLine("})();"); - - File.WriteAllText(Path.Combine("vaft", "vaft.user.js"), sbUserscript.ToString()); - File.WriteAllText(Path.Combine("vaft", "vaft-ublock-origin.js"), sbUblock.ToString()); - } - } - - static void Run(RunnerMode mode, string channel) - { - Thread thread = new Thread(delegate() - { - RunImpl(mode, channel); - }); - thread.IsBackground = true; - thread.Start(); - } - - static string RunImpl(RunnerMode mode, string channel, bool isFetchingM3U8 = false, bool forceSkipAd = false) - { - string playerType = PlayerTypeNormal; - switch (mode) - { - case RunnerMode.MiniNoAd: - playerType = PlayerTypeMiniNoAd; - break; - case RunnerMode.Embed: - playerType = PlayerTypeEmbed; - break; - } - string cookies = null; - string uniqueId = null; - int cycle = 0; - while (true) - { - if (string.IsNullOrEmpty(cookies)) - { - using (CookieAwareWebClient wc = new CookieAwareWebClient()) - { - wc.Proxy = null; - wc.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; - wc.DownloadString("https://www.twitch.tv/" + channel); - cookies = ProcessCookies(wc.Cookies, out uniqueId); - //Console.WriteLine("unique_id: " + uniqueId); - } - } - if (string.IsNullOrEmpty(uniqueId)) - { - Console.WriteLine("unique_id is null"); - return null; - } - using (WebClient wc = new WebClient()) - { - string response = null, token = null, sig = null; - wc.Proxy = null; - if (mode != RunnerMode.Proxy) - { - if (UseOldAccessToken) - { - wc.Headers.Clear(); - wc.Headers["client-id"] = ClientID; - wc.Headers["accept"] = "application/vnd.twitchtv.v5+json; charset=UTF-8"; - wc.Headers["accept-encoding"] = "gzip, deflate, br"; - wc.Headers["accept-language"] = "en-us"; - wc.Headers["content-type"] = "application/json; charset=UTF-8"; - wc.Headers["origin"] = "https://www.twitch.tv"; - wc.Headers["referer"] = "https://www.twitch.tv/"; - wc.Headers["user-agent"] = UserAgent; - wc.Headers["x-requested-with"] = "XMLHttpRequest"; - wc.Headers["cookie"] = cookies; - response = wc.DownloadString("https://api.twitch.tv/api/channels/" + channel + "/access_token?oauth_token=undefined&need_https=true&platform=" + Platform + "&player_type=" + playerType + "&player_backend=" + PlayerBackend); - if (!string.IsNullOrEmpty(response)) - { - TwitchAccessTokenOld tokenInfo = JSONSerializer.DeSerialize(response); - if (tokenInfo != null && !string.IsNullOrEmpty(tokenInfo.token) && !string.IsNullOrEmpty(tokenInfo.sig)) - { - token = tokenInfo.token; - sig = tokenInfo.sig; - } - } - } - else - { - wc.Headers.Clear(); - wc.Headers["client-id"] = ClientID; - wc.Headers["Device-ID"] = uniqueId; - wc.Headers["accept"] = "*/*"; - wc.Headers["accept-encoding"] = "gzip, deflate, br"; - wc.Headers["accept-language"] = "en-us"; - wc.Headers["content-type"] = "text/plain; charset=UTF-8"; - wc.Headers["origin"] = "https://www.twitch.tv"; - wc.Headers["referer"] = "https://www.twitch.tv/"; - wc.Headers["user-agent"] = UserAgent; - if (UseAccessTokenTemplate) - { - response = wc.UploadString("https://gql.twitch.tv/gql", @"{""operationName"":""PlaybackAccessToken_Template"",""query"":""query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \""" + Platform + @"\"", playerBackend: \""" + PlayerBackend + @"\"", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \""" + Platform + @"\"", playerBackend: \""" + PlayerBackend + @"\"", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}"",""variables"":{""isLive"":true,""login"":""" + channel + @""",""isVod"":false,""vodID"":"""",""playerType"":""" + playerType + @"""}}"); - } - else - { - response = wc.UploadString("https://gql.twitch.tv/gql", @"{""operationName"":""PlaybackAccessToken"",""variables"":{""isLive"":true,""login"":""" + channel + @""",""isVod"":false,""vodID"":"""",""playerType"":""" + playerType + @"""},""extensions"":{""persistedQuery"":{""version"":1,""sha256Hash"":""0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712""}}}"); - } - if (!string.IsNullOrEmpty(response)) - { - TwitchAccessToken tokenInfo = JSONSerializer.DeSerialize(response); - if (tokenInfo != null && tokenInfo.data != null && tokenInfo.data.streamPlaybackAccessToken != null && - !string.IsNullOrEmpty(tokenInfo.data.streamPlaybackAccessToken.value) && !string.IsNullOrEmpty(tokenInfo.data.streamPlaybackAccessToken.signature)) - { - token = tokenInfo.data.streamPlaybackAccessToken.value; - sig = tokenInfo.data.streamPlaybackAccessToken.signature; - } - } - } - } - if (mode == RunnerMode.Proxy || !string.IsNullOrEmpty(token)) - { - string url = null; - if (mode == RunnerMode.Proxy) - { - url = ProxyUrl + channel; - } - else - { - string additionalParams = ""; - if (UseFastBread) - { - additionalParams += "&fast_bread=true"; - } - url = "https://usher.ttvnw.net/api/channel/hls/" + channel + ".m3u8?allow_source=true" + additionalParams + "&sig=" + sig + "&token=" + System.Web.HttpUtility.UrlEncode(token) + MainM3U8AdditionalParams; - } - if (isFetchingM3U8) - { - if (!forceSkipAd || cycle > 0) - { - return url; - } - } - wc.Headers.Clear(); - wc.Headers["accept"] = "application/x-mpegURL, application/vnd.apple.mpegurl, application/json, text/plain"; - wc.Headers["host"] = "usher.ttvnw.net"; - wc.Headers["cookie"] = "DNT=1;"; - wc.Headers["DNT"] = "1"; - wc.Headers["user-agent"] = UserAgent; - string encodingsM3u8 = wc.DownloadString(url); - if (!string.IsNullOrEmpty(encodingsM3u8)) - { - string[] lines = encodingsM3u8.Split('\n'); - string info = lines.FirstOrDefault(x => x.Contains("EXT-X-TWITCH-INFO")); - bool isFuture = false; - if (info != null) - { - Dictionary attr = ParseAttributes(info); - string futureStr; - if (attr.TryGetValue("FUTURE", out futureStr)) - { - isFuture = bool.Parse(futureStr); - } - } - string streamM3u8Url = lines.FirstOrDefault(x => x.EndsWith(".m3u8")); - if (!string.IsNullOrEmpty(streamM3u8Url)) - { - bool foundAd = true; - while (foundAd) - { - string streamM3u8 = wc.DownloadString(streamM3u8Url); - if (!string.IsNullOrEmpty(streamM3u8Url)) - { - if (streamM3u8.Contains(AdSignifier)) - { - Console.WriteLine("has ad " + DateTime.Now.TimeOfDay + " - " + mode + " - future:" + isFuture); - if (ShouldDenyAd) - { - DeclineAd(uniqueId, streamM3u8, sig, token, true); - DeclineAd(uniqueId, streamM3u8, sig, token, false); - } - } - else - { - Console.WriteLine("no ad " + DateTime.Now.TimeOfDay + " - " + mode + " - future:" + isFuture); - } - if ((streamM3u8.Contains(AdSignifier) || forceSkipAd) && - (!UseOldAccessToken && (ShouldNotifyAdWatched || forceSkipAd))) - { - NotifyWatchedAd(uniqueId, streamM3u8); - } - } - else - { - Console.WriteLine("Failed to fetch streamM3u8Url"); - } - if (!ShouldDenyAd) - { - break; - } - else - { - Thread.Sleep(LoopDelay); - } - } - } - else - { - Console.WriteLine("Failed to find streamM3u8Url"); - } - } - else - { - Console.WriteLine("Failed to fetch encodingsM3u8"); - } - } - else - { - Console.WriteLine("Failed to get stream token mode:" + mode); - } - } - Thread.Sleep(LoopDelay); - cycle++; - } - } - - static Dictionary ParseAttributes(string tag) - { - string tagName; - return ParseAttributes(tag, out tagName); - } - - static Dictionary ParseAttributes(string tag, out string tagName) - { - // TODO: Improve this - Dictionary result = new Dictionary(); - tagName = null; - int tagDataSplitIndex = tag.IndexOf(':'); - if (tagDataSplitIndex > 0) - { - tagName = tag.Substring(0, tagDataSplitIndex); - tag = tag.Substring(tagDataSplitIndex + 1); - string[] splitted = tag.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string str in splitted) - { - int index = str.IndexOf('='); - if (index > 0) - { - result[str.Substring(0, index)] = str.Substring(index + 1).Trim('\"'); - } - } - } - return result; - } - - static TValue GetOrDefault(Dictionary dict, TKey key, TValue defaultValue = default(TValue)) - { - TValue result; - if (dict.TryGetValue(key, out result)) - { - return result; - } - return defaultValue; - } - - static void DeclineAd(string uniqueId, string streamM3u8, string sig, string token, bool first) - { - string[] lines = streamM3u8.Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - if (lines[i].Contains(AdSignifier)) - { - Dictionary attr = ParseAttributes(lines[i]); - Dictionary vals = new Dictionary(); - vals["TARG_adSessionID"] = GetOrDefault(attr, "X-TV-TWITCH-AD-AD-SESSION-ID"); - vals["TARG_sig"] = sig; - vals["TARG_token"] = token.Replace("\"", "\\\""); - string str = null; - //string str = @"[{""operationName"":""VideoAdRequestDecline"",""variables"":{""context"":{""adSessionID"":""TARG_adSessionID"",""clientContext"":""{\""isAudioOnly\"":false,\""isMiniTheater\"":false,\""isPIP\"":true,\""isUsingExternalPlayback\"":false}"",""isAudioOnly"":false,""isMiniTheater"":false,""isPIP"":false,""isUsingExternalPlayback"":false,""duration"":30,""isVLM"":false,""rollType"":""PREROLL""}},""extensions"":{""persistedQuery"":{""version"":1,""sha256Hash"":""6f5d9fdc36a3c879cca7debdbe21c62d5cac4ad5b30b635263eff68335b96a71""}}}]"; - if (first) - str = @"[{""operationName"":""VideoAdRequestDecline"",""variables"":{""context"":{""adSessionID"":""TARG_adSessionID"",""clientContext"":{""isAudioOnly"":false,""isMiniTheater"":false,""isPIP"":false,""isUsingExternalPlayback"":false},""duration"":30,""playerContext"":{""contentType"":""LIVE"",""isAutoPlay"":true,""nauthSig"":""TARG_sig"",""nauthToken"":""TARG_token""},""rollType"":""PREROLL"",""isVLM"":false,""commercialID"":""""}},""extensions"":{""persistedQuery"":{""version"":1,""sha256Hash"":""6f5d9fdc36a3c879cca7debdbe21c62d5cac4ad5b30b635263eff68335b96a71""}}}]"; - else - { - vals["TARG_ad_session_id"] = GetOrDefault(attr, "X-TV-TWITCH-AD-AD-SESSION-ID"); - vals["TARG_radToken"] = GetOrDefault(attr, "X-TV-TWITCH-AD-RADS-TOKEN"); - str = @"[{""operationName"":""ClientSideAdEventHandling_RecordAdEvent"",""variables"":{""input"":{""eventName"":""video_ad_request_declined"",""eventPayload"":""{\""reason_channeladfree\"":false,\""reason_channelsub\"":false,\""reason_vod_ads_disabled\"":false,\""reason_bounty\"":false,\""reason_vod_midroll\"":false,\""reason_stream_broadcaster\"":false,\""reason_embed_promo\"":false,\""reason_p4m\"":false,\""reason_lt\"":false,\""reason_raid\"":false,\""reason_midroll_during_preroll\"":false,\""reason_ratelimit\"":false,\""reason_short_vod\"":false,\""reason_turbo\"":false,\""reason_vod_creator\"":false,\""reason_wp\"":false,\""reason_zagd\"":false,\""reason_zagu\"":false,\""reason_midlimit\"":false,\""reason_amazon_product_page\"":false,\""reason_animated_thumbnails\"":false,\""reason_creative_player\"":false,\""reason_dashboard\"":false,\""reason_facebook\"":false,\""reason_frontpage\"":false,\""reason_highlighter\"":false,\""reason_onboarding\"":false,\""reason_pbyp\"":false,\""reason_squad_stream_secondary_player\"":false,\""reason_thunderdome\"":true,\""reason_embed\"":false,\""twitch_correlator\"":\""\"",\""ad_session_id\"":\""TARG_ad_session_id\"",\""roll_type\"":\""preroll\"",\""time_break\"":30}"",""radToken"":""TARG_radToken""}},""extensions"":{""persistedQuery"":{""version"":1,""sha256Hash"":""7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b""}}}]"; - } - foreach (KeyValuePair val in vals) - { - str = str.Replace(val.Key, val.Value); - } - //Console.WriteLine(str); - using (WebClient wc = new WebClient()) - { - wc.Proxy = null; - wc.Headers["Client-Id"] = ClientID; - wc.Headers["X-Device-Id"] = uniqueId; - wc.Headers["accept"] = "*/*"; - wc.Headers["accept-encoding"] = "gzip, deflate, br"; - wc.Headers["accept-language"] = "en-us"; - wc.Headers["content-type"] = "text/plain; charset=UTF-8"; - wc.Headers["origin"] = "https://www.twitch.tv"; - wc.Headers["referer"] = "https://www.twitch.tv/"; - wc.Headers["user-agent"] = UserAgent; - string st2 = wc.UploadString("https://gql.twitch.tv/gql", str); - Console.WriteLine(st2); - } - return; - } - } - } - - static void SendGqlAdEvent(WebClient wc, string eventName, bool includeAdInfo, int adQuartile, int adPos, Dictionary vals) - { - // TARG_eventName TARG_roll_type TARG_radToken TARG_adInfo - // TARG_ad_id TARG_ad_position TARG_duration TARG_creative_id TARG_total_ads TARG_order_id TARG_line_item_id TARG_quartile - string str = @"[{""operationName"":""ClientSideAdEventHandling_RecordAdEvent"",""variables"":{""input"":{""eventName"":""TARG_eventName"",""eventPayload"":""{\""player_mute\"":false,\""player_volume\"":0.5,\""visible\"":true,\""roll_type\"":\""TARG_roll_type\"",\""stitched\"":trueTARG_adInfo}"",""radToken"":""TARG_radToken""}},""extensions"":{""persistedQuery"":{""version"":1,""sha256Hash"":""7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b""}}}]"; - string strAdInfo = @",\""ad_id\"":\""TARG_ad_id\"",\""ad_position\"":TARG_ad_position,\""duration\"":TARG_duration,\""creative_id\"":\""TARG_creative_id\"",\""total_ads\"":TARG_total_ads,\""order_id\"":\""TARG_order_id\"",\""line_item_id\"":\""TARG_line_item_id\""TARG_quartile"; - vals["TARG_eventName"] = eventName; - vals["TARG_quartile"] = adQuartile > 0 ? (@",\""quartile\"":" + adQuartile) : string.Empty; - if (includeAdInfo) - { - foreach (KeyValuePair val in vals) - { - strAdInfo = strAdInfo.Replace(val.Key, val.Value); - } - vals["TARG_adInfo"] = strAdInfo; - } - else - { - vals["TARG_adInfo"] = ""; - } - foreach (KeyValuePair val in vals) - { - str = str.Replace(val.Key, val.Value); - } - //Console.WriteLine(str); - Console.WriteLine("SendGqlAdEvent " + eventName + " adinfo: " + includeAdInfo + " quartile: " + adQuartile + " adPos: " + adPos); - wc.UploadString("https://gql.twitch.tv/gql", str); - } - - static void NotifyWatchedAd(string uniqueId, string streamM3u8) - { - string[] lines = streamM3u8.Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - if (lines[i].Contains(AdSignifier)) - { - Dictionary attr = ParseAttributes(lines[i]); - Dictionary vals = new Dictionary(); - vals["TARG_roll_type"] = GetOrDefault(attr, "X-TV-TWITCH-AD-ROLL-TYPE", "preroll").ToLower(); - vals["TARG_radToken"] = GetOrDefault(attr, "X-TV-TWITCH-AD-RADS-TOKEN"); - vals["TARG_ad_id"] = GetOrDefault(attr, "X-TV-TWITCH-AD-ADVERTISER-ID"); - vals["TARG_duration"] = "30"; - vals["TARG_creative_id"] = GetOrDefault(attr, "X-TV-TWITCH-AD-CREATIVE-ID"); - vals["TARG_total_ads"] = GetOrDefault(attr, "X-TV-TWITCH-AD-POD-LENGTH", "1"); - vals["TARG_order_id"] = GetOrDefault(attr, "X-TV-TWITCH-AD-ORDER-ID"); - vals["TARG_line_item_id"] = GetOrDefault(attr, "X-TV-TWITCH-AD-LINE-ITEM-ID"); - using (WebClient wc = new WebClient()) - { - wc.Proxy = null; - wc.Headers["Client-Id"] = ClientID; - wc.Headers["X-Device-Id"] = uniqueId; - wc.Headers["accept"] = "*/*"; - wc.Headers["accept-encoding"] = "gzip, deflate, br"; - wc.Headers["accept-language"] = "en-us"; - wc.Headers["content-type"] = "text/plain; charset=UTF-8"; - wc.Headers["origin"] = "https://www.twitch.tv"; - wc.Headers["referer"] = "https://www.twitch.tv/"; - wc.Headers["user-agent"] = UserAgent; - if (ShouldNotifyAdWatchedMin) - { - SendGqlAdEvent(wc, "video_ad_pod_complete", false, 0, 0, vals); - } - else - { - int totalAds = int.Parse(vals["TARG_total_ads"]); - for (int adPos = 0; adPos < totalAds; adPos++) - { - vals["TARG_ad_position"] = adPos.ToString(); - SendGqlAdEvent(wc, "video_ad_impression", true, 0, adPos, vals); - for (int quartile = 1; quartile <= 4; quartile++) - { - SendGqlAdEvent(wc, "video_ad_quartile_complete", true, quartile, adPos, vals); - } - SendGqlAdEvent(wc, "video_ad_pod_complete", false, 0, adPos, vals); - } - } - } - break; - } - } - //Console.WriteLine(streamM3u8); - } - - static string ProcessCookies(string str) - { - string uniqueId; - return ProcessCookies(str, out uniqueId); - } - - static string ProcessCookies(string str, out string uniqueId) - { - uniqueId = null; - string result = string.Empty; - string[] cookies = str.Split(','); - foreach (string cookie in cookies) - { - if (cookie.Split(';')[0].Contains('=')) - { - string[] splitted = cookie.Split(';')[0].Split('='); - if (splitted.Length >= 2 && splitted[0] == "unique_id") - { - uniqueId = splitted[1]; - } - result += cookie.Split(';')[0] + ";"; - } - } - return result; - } - - [DataContract] - public class TwitchAccessTokenOld - { - [DataMember] - public string token { get; set; } - [DataMember] - public string sig { get; set; } - } - - [DataContract] - public class TwitchAccessToken - { - [DataMember] - public TwitchAccessToken_data data { get; set; } - } - - [DataContract] - public class TwitchAccessToken_data - { - [DataMember] - public TwitchAccessToken_streamPlaybackAccessToken streamPlaybackAccessToken { get; set; } - } - - [DataContract] - public class TwitchAccessToken_streamPlaybackAccessToken - { - [DataMember] - public string value { get; set; } - [DataMember] - public string signature { get; set; } - } - - [DataContract] - public class ChromeExtensionManifest - { - [DataMember] - public string version { get; set; } - } - - class CookieAwareWebClient : WebClient - { - public CookieContainer CookieContainer { get; set; } - public Uri Uri { get; set; } - - public string Cookies { get; private set; } - - public CookieAwareWebClient() - : this(new CookieContainer()) - { - } - - public CookieAwareWebClient(CookieContainer cookies) - { - this.CookieContainer = new CookieContainer(); - } - - protected override WebResponse GetWebResponse(WebRequest request) - { - WebResponse response = base.GetWebResponse(request); - string setCookieHeader = response.Headers.Get("Set-Cookie"); - Cookies = setCookieHeader; - return response; - } - } - - static class JSONSerializer where TType : class - { - public static TType DeSerialize(string json) - { - return TinyJson.JSONParser.FromJson(json); - } - } - - class TwitchTestServer - { - const string RecordDir = "recordings"; - Dictionary states = new Dictionary(); - class State - { - public bool IsReplay = false; - public string RecordingName = null; - public string RecordingPath = null; - public string ChannelName = null; - public string UrlChRecName { get { return ChannelName + "|" + RecordingName; } } - public string M3U8Normal = null; - public string M3U8Mini = null; - public string M3U8Alt = null; - public Dictionary> M3U8Map = new Dictionary>(); - public Stopwatch Stopwatch = new Stopwatch(); - - public State(string channelName, string name) - { - ChannelName = channelName; - RecordingName = name; - RecordingPath = Path.GetFullPath(Path.Combine(RecordDir, name)); - try - { - if (!Directory.Exists(RecordingPath)) - { - Directory.CreateDirectory(RecordingPath); - } - } - catch - { - } - } - - public void Clear() - { - M3U8Map.Clear(); - Stopwatch.Restart(); - try - { - while (Directory.Exists(RecordingPath)) - { - Directory.Delete(RecordingPath, true); - } - } - catch - { - } - try - { - if (!Directory.Exists(RecordingPath)) - { - Directory.CreateDirectory(RecordingPath); - } - } - catch - { - } - } - - public void Load() - { - M3U8Map.Clear(); - Stopwatch.Restart(); - IsReplay = true; - } - } - - private Thread thread; - private HttpListener listener; - - public void Start(int port) - { - Stop(); - - thread = new Thread(delegate() - { - listener = new HttpListener(); - listener.Prefixes.Add("http://*:" + port + "/"); - listener.Start(); - while (listener != null) - { - try - { - HttpListenerContext context = listener.GetContext(); - Process(context); - } - catch - { - } - } - }); - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - } - - public void Stop() - { - if (listener != null) - { - try - { - listener.Stop(); - } - catch - { - } - listener = null; - } - if (thread != null) - { - try - { - thread.Abort(); - } - catch - { - } - thread = null; - } - } - - private void Process(HttpListenerContext context) - { - try - { - string url = context.Request.Url.OriginalString; - //Console.WriteLine("req " + DateTime.Now.TimeOfDay + " - " + url); - - string response = string.Empty; - string contentType = "text/html"; - - if (url.Contains("favicon.ico")) - { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - context.Response.OutputStream.Close(); - return; - } - - byte[] responseBuffer = null; - if (context.Request.Url.Segments.Length == 1 && context.Request.Url.Segments[0] == "/") - { - response = ""; - } - else if (context.Request.Url.Segments.Length == 2 && context.Request.Url.Segments[1] == "utils.js") - { - response = File.ReadAllText("utils.js"); - } - else if (context.Request.Url.Segments.Length >= 3) - { - string[] reqTypeSplitted = context.Request.Url.Segments[1].Trim('/').Split('_'); - string reqType = reqTypeSplitted[0].ToLower(); - string reqStreamType = reqTypeSplitted.Length > 1 ? reqTypeSplitted[1] : null; - string[] splitted = context.Request.Url.Segments[2].Trim('/').ToLower().Replace("%7c", "|").Split('|'); - string channelName = splitted[0]; - string recordingName = splitted[1]; - if (!string.IsNullOrEmpty(channelName) && !string.IsNullOrEmpty(recordingName)) - { - State state; - if (!states.TryGetValue(recordingName, out state)) - { - states[recordingName] = state = new State(channelName, recordingName); - } - switch (reqType) - { - case "record-begin": - { - state.Clear(); - string normal = RunImpl(RunnerMode.Normal, channelName, true); - if (!string.IsNullOrEmpty(normal)) - { - string mini = RunImpl(RunnerMode.MiniNoAd, channelName, true); - if (!string.IsNullOrEmpty(mini)) - { - //string alt = RunImpl(RunnerMode.Proxy, channelName, true); - //string alt = RunImpl(RunnerMode.Normal, channelName, true, true); - string alt = RunImpl(RunnerMode.Embed, channelName, true); - state.M3U8Normal = normal; - state.M3U8Mini = mini; - state.M3U8Alt = alt; - response = "ok"; - } - } - } - break; - case "replay-begin": - { - DirectoryInfo dir = new DirectoryInfo(Path.Combine(RecordDir, recordingName)); - if (dir.Exists && dir.GetFiles().Length > 0) - { - state.Load(); - response = "ok"; - } - } - break; - case "m3u8": - { - string type = reqTypeSplitted[1].ToLower(); - string m3u8Url = null; - switch (type) - { - case "normal": - case "output": - m3u8Url = state.M3U8Normal; - break; - case "mini": - m3u8Url = state.M3U8Mini; - break; - case "alt": - m3u8Url = state.M3U8Alt; - break; - } - if (!string.IsNullOrEmpty(m3u8Url)) - { - response = GetM3U8(state, m3u8Url, reqStreamType, true); - } - } - break; - case "m3u8-sub": - { - string type = reqTypeSplitted[1].ToLower(); - string m3u8Url = null; - if (!state.IsReplay) - { - m3u8Url = GetM3U8Url(state, reqStreamType); - } - if (!string.IsNullOrEmpty(m3u8Url) || state.IsReplay) - { - response = GetM3U8(state, m3u8Url, reqStreamType, false); - } - } - break; - case "m3u8-seg": - { - // TODO: Load segment, return as binary file - } - break; - default: - Console.WriteLine("Unhandled request '" + reqType + "'"); - break; - } - } - } - - if (responseBuffer == null) - { - responseBuffer = Encoding.UTF8.GetBytes(response == null ? string.Empty : response.ToString()); - } - context.Response.ContentType = contentType; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.ContentLength64 = responseBuffer.Length; - context.Response.OutputStream.Write(responseBuffer, 0, responseBuffer.Length); - context.Response.OutputStream.Flush(); - context.Response.StatusCode = (int)HttpStatusCode.OK; - } - catch (Exception e) - { - Console.WriteLine(e); - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - } - context.Response.OutputStream.Close(); - } - - private string DownloadM3U8(string url) - { - try - { - using (WebClient wc = new WebClient()) - { - wc.Proxy = null; - wc.Headers["accept"] = "application/x-mpegURL, application/vnd.apple.mpegurl, application/json, text/plain"; - wc.Headers["host"] = "usher.ttvnw.net"; - wc.Headers["cookie"] = "DNT=1;"; - wc.Headers["DNT"] = "1"; - wc.Headers["user-agent"] = UserAgent; - return wc.DownloadString(url); - } - } - catch (Exception e) - { - //Console.WriteLine(url); - Console.WriteLine(e); - return null; - } - } - - private string GetM3U8Url(State state, string reqStreamType) - { - Dictionary m3u8Map; - if (state.M3U8Map.TryGetValue(reqStreamType, out m3u8Map) && m3u8Map.Count > 0) - { - string resUrl = null; - int res = int.MaxValue; - string backupUrl = null; - foreach (KeyValuePair mappedUrl in m3u8Map) - { - if (mappedUrl.Key.Contains("x")) - { - int val; - if (int.TryParse(mappedUrl.Key.Split('x')[1], out val)) - { - if (backupUrl == null) - { - backupUrl = mappedUrl.Value; - } - if (val < res && val >= TargetResolution) - { - res = val; - resUrl = mappedUrl.Value; - } - } - } - } - if (string.IsNullOrEmpty(resUrl)) - { - resUrl = backupUrl; - } - return resUrl; - } - return null; - } - - private string GetM3U8(State state, string url, string reqStreamType, bool isMain) - { - string m3u8 = null; - string backupUrl = null; - if (state.IsReplay) - { - // TODO: Load replay m3u8 - } - else - { - m3u8 = DownloadM3U8(url); - if (reqStreamType == "output") - { - backupUrl = GetM3U8Url(state, "mini"); - } - } - if (string.IsNullOrEmpty(m3u8)) - { - return null; - } - if (!state.M3U8Map.ContainsKey(reqStreamType)) - { - state.M3U8Map[reqStreamType] = new Dictionary(); - } - m3u8 = m3u8.Replace("\r", string.Empty); - string prevRes = null; - string[] lines = m3u8.Split('\n'); - string mainM3U8Name = "m3u8-sub_" + reqStreamType; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i].Trim(); - if (line.StartsWith("#")) - { - string tagName; - Dictionary attr = ParseAttributes(line, out tagName); - if (tagName == "#EXT-X-STREAM-INF") - { - attr.TryGetValue("RESOLUTION", out prevRes); - } - } - else if (line.EndsWith(".m3u8")) - { - if (!string.IsNullOrEmpty(prevRes) && !state.M3U8Map[reqStreamType].ContainsKey(prevRes)) - { - state.M3U8Map[reqStreamType][prevRes] = line; - } - lines[i] = "/" + mainM3U8Name + "/" + state.UrlChRecName; - } - else if (line.EndsWith(".ts")) - { - // TODO: Save seg - } - } - if (!isMain && m3u8.Contains("stitched-ad") && !string.IsNullOrEmpty(backupUrl)) - { - string m3u8Backup = DownloadM3U8(backupUrl); - if (!string.IsNullOrEmpty(m3u8Backup)) - { - m3u8Backup = m3u8Backup.Replace("\r", string.Empty); - string[] backupLines = m3u8Backup.Split('\n'); - Dictionary segmentMap = new Dictionary(); - Dictionary segTimes = GetSegmentTimes(lines); - Dictionary backupSegTimes = GetSegmentTimes(backupLines); - foreach (KeyValuePair seg in segTimes) - { - //segmentMap[seg.Value] = backupSegTimes.Last().Value; - long closestTime = long.MaxValue; - long matchingBackupTime = long.MaxValue; - foreach (KeyValuePair backupSeg in backupSegTimes) - { - long timeDiff = Math.Abs(seg.Key - backupSeg.Key); - if (timeDiff < closestTime) - { - closestTime = timeDiff; - matchingBackupTime = backupSeg.Key; - segmentMap[seg.Value] = backupSeg.Value; - } - } - if (closestTime != long.MaxValue) - { - backupSegTimes.Remove(matchingBackupTime); - } - } - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - if (line.Contains("stitched-ad")) - { - line = ""; - } - if (line.StartsWith("#EXTINF:") && !line.Contains(",live")) - { - lines[i] = line.Substring(0, line.IndexOf(',')) + ",live"; - string backupSegment; - segmentMap.TryGetValue(lines[i + 1], out backupSegment); - lines[i + 1] = backupSegment != null ? backupSegment : ""; - } - } - } - } - if (isMain) - { - File.WriteAllText(Path.Combine(state.RecordingPath, mainM3U8Name + "-original"), m3u8); - File.WriteAllLines(Path.Combine(state.RecordingPath, mainM3U8Name), lines); - } - // TODO: Save m3u8 - return string.Join(Environment.NewLine, lines); - } - - private Dictionary GetSegmentTimes(string[] lines) - { - Dictionary result = new Dictionary(); - long lastDate = 0; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - if (line.StartsWith("#EXT-X-PROGRAM-DATE-TIME:")) - { - lastDate = DateTime.Parse(line.Substring(line.IndexOf(":") + 1)).Ticks; - } - else if (line.StartsWith("http")) - { - result[lastDate] = line; - } - } - return result; - } - } - } -} - -namespace TinyJson -{ - // Really simple JSON parser in ~300 lines - // - Attempts to parse JSON files with minimal GC allocation - // - Nice and simple "[1,2,3]".FromJson>() API - // - Classes and structs can be parsed too! - // class Foo { public int Value; } - // "{\"Value\":10}".FromJson() - // - Can parse JSON without type information into Dictionary and List e.g. - // "[1,2,3]".FromJson().GetType() == typeof(List) - // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) - // - No JIT Emit support to support AOT compilation on iOS - // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. - // - Only public fields and property setters on classes/structs will be written to - // - // Limitations: - // - No JIT Emit support to parse structures quickly - // - Limited to parsing <2GB JSON files (due to int.MaxValue) - // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. - public static class JSONParser - { - [ThreadStatic] static Stack> splitArrayPool; - [ThreadStatic] static StringBuilder stringBuilder; - [ThreadStatic] static Dictionary> fieldInfoCache; - [ThreadStatic] static Dictionary> propertyInfoCache; - - public static T FromJson(this string json) - { - // Initialize, if needed, the ThreadStatic variables - if (propertyInfoCache == null) propertyInfoCache = new Dictionary>(); - if (fieldInfoCache == null) fieldInfoCache = new Dictionary>(); - if (stringBuilder == null) stringBuilder = new StringBuilder(); - if (splitArrayPool == null) splitArrayPool = new Stack>(); - - //Remove all whitespace not within strings to make parsing simpler - stringBuilder.Length = 0; - for (int i = 0; i < json.Length; i++) - { - char c = json[i]; - if (c == '"') - { - i = AppendUntilStringEnd(true, i, json); - continue; - } - if (char.IsWhiteSpace(c)) - continue; - - stringBuilder.Append(c); - } - - //Parse the thing! - return (T)ParseValue(typeof(T), stringBuilder.ToString()); - } - - static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) - { - stringBuilder.Append(json[startIdx]); - for (int i = startIdx + 1; i < json.Length; i++) - { - if (json[i] == '\\') - { - if (appendEscapeCharacter) - stringBuilder.Append(json[i]); - stringBuilder.Append(json[i + 1]); - i++;//Skip next character as it is escaped - } - else if (json[i] == '"') - { - stringBuilder.Append(json[i]); - return i; - } - else - stringBuilder.Append(json[i]); - } - return json.Length - 1; - } - - //Splits { :, : } and [ , ] into a list of strings - static List Split(string json) - { - List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); - splitArray.Clear(); - if (json.Length == 2) - return splitArray; - int parseDepth = 0; - stringBuilder.Length = 0; - for (int i = 1; i < json.Length - 1; i++) - { - switch (json[i]) - { - case '[': - case '{': - parseDepth++; - break; - case ']': - case '}': - parseDepth--; - break; - case '"': - i = AppendUntilStringEnd(true, i, json); - continue; - case ',': - case ':': - if (parseDepth == 0) - { - splitArray.Add(stringBuilder.ToString()); - stringBuilder.Length = 0; - continue; - } - break; - } - - stringBuilder.Append(json[i]); - } - - splitArray.Add(stringBuilder.ToString()); - - return splitArray; - } - - internal static object ParseValue(Type type, string json) - { - if (type == typeof(string)) - { - if (json.Length <= 2) - return string.Empty; - StringBuilder parseStringBuilder = new StringBuilder(json.Length); - for (int i = 1; i < json.Length - 1; ++i) - { - if (json[i] == '\\' && i + 1 < json.Length - 1) - { - int j = "\"\\nrtbf/".IndexOf(json[i + 1]); - if (j >= 0) - { - parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); - ++i; - continue; - } - if (json[i + 1] == 'u' && i + 5 < json.Length - 1) - { - UInt32 c = 0; - if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) - { - parseStringBuilder.Append((char)c); - i += 5; - continue; - } - } - } - parseStringBuilder.Append(json[i]); - } - return parseStringBuilder.ToString(); - } - if (type.IsPrimitive) - { - var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); - return result; - } - if (type == typeof(decimal)) - { - decimal result; - decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); - return result; - } - if (json == "null") - { - return null; - } - if (type.IsEnum) - { - if (json[0] == '"') - json = json.Substring(1, json.Length - 2); - try - { - return Enum.Parse(type, json, false); - } - catch - { - return 0; - } - } - if (type.IsArray) - { - Type arrayType = type.GetElementType(); - if (json[0] != '[' || json[json.Length - 1] != ']') - return null; - - List elems = Split(json); - Array newArray = Array.CreateInstance(arrayType, elems.Count); - for (int i = 0; i < elems.Count; i++) - newArray.SetValue(ParseValue(arrayType, elems[i]), i); - splitArrayPool.Push(elems); - return newArray; - } - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) - { - Type listType = type.GetGenericArguments()[0]; - if (json[0] != '[' || json[json.Length - 1] != ']') - return null; - - List elems = Split(json); - var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count }); - for (int i = 0; i < elems.Count; i++) - list.Add(ParseValue(listType, elems[i])); - splitArrayPool.Push(elems); - return list; - } - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - Type keyType, valueType; - { - Type[] args = type.GetGenericArguments(); - keyType = args[0]; - valueType = args[1]; - } - - //Refuse to parse dictionary keys that aren't of type string - if (keyType != typeof(string)) - return null; - //Must be a valid dictionary element - if (json[0] != '{' || json[json.Length - 1] != '}') - return null; - //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON - List elems = Split(json); - if (elems.Count % 2 != 0) - return null; - - var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 }); - for (int i = 0; i < elems.Count; i += 2) - { - if (elems[i].Length <= 2) - continue; - string keyValue = elems[i].Substring(1, elems[i].Length - 2); - object val = ParseValue(valueType, elems[i + 1]); - dictionary[keyValue] = val; - } - return dictionary; - } - if (type == typeof(object)) - { - return ParseAnonymousValue(json); - } - if (json[0] == '{' && json[json.Length - 1] == '}') - { - return ParseObject(type, json); - } - - return null; - } - - static object ParseAnonymousValue(string json) - { - if (json.Length == 0) - return null; - if (json[0] == '{' && json[json.Length - 1] == '}') - { - List elems = Split(json); - if (elems.Count % 2 != 0) - return null; - var dict = new Dictionary(elems.Count / 2); - for (int i = 0; i < elems.Count; i += 2) - dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); - return dict; - } - if (json[0] == '[' && json[json.Length - 1] == ']') - { - List items = Split(json); - var finalList = new List(items.Count); - for (int i = 0; i < items.Count; i++) - finalList.Add(ParseAnonymousValue(items[i])); - return finalList; - } - if (json[0] == '"' && json[json.Length - 1] == '"') - { - string str = json.Substring(1, json.Length - 2); - return str.Replace("\\", string.Empty); - } - if (char.IsDigit(json[0]) || json[0] == '-') - { - if (json.Contains(".")) - { - double result; - double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); - return result; - } - else - { - int result; - int.TryParse(json, out result); - return result; - } - } - if (json == "true") - return true; - if (json == "false") - return false; - // handles json == "null" as well as invalid JSON - return null; - } - - static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo - { - Dictionary nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < members.Length; i++) - { - T member = members[i]; - if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) - continue; - - string name = member.Name; - if (member.IsDefined(typeof(DataMemberAttribute), true)) - { - DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); - if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) - name = dataMemberAttribute.Name; - } - - nameToMember.Add(name, member); - } - - return nameToMember; - } - - static object ParseObject(Type type, string json) - { - object instance = FormatterServices.GetUninitializedObject(type); - - //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON - List elems = Split(json); - if (elems.Count % 2 != 0) - return instance; - - Dictionary nameToField; - Dictionary nameToProperty; - if (!fieldInfoCache.TryGetValue(type, out nameToField)) - { - nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); - fieldInfoCache.Add(type, nameToField); - } - if (!propertyInfoCache.TryGetValue(type, out nameToProperty)) - { - nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); - propertyInfoCache.Add(type, nameToProperty); - } - - for (int i = 0; i < elems.Count; i += 2) - { - if (elems[i].Length <= 2) - continue; - string key = elems[i].Substring(1, elems[i].Length - 2); - string value = elems[i + 1]; - - FieldInfo fieldInfo; - PropertyInfo propertyInfo; - if (nameToField.TryGetValue(key, out fieldInfo)) - fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); - else if (nameToProperty.TryGetValue(key, out propertyInfo)) - propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); - } - - return instance; - } - } -} \ No newline at end of file diff --git a/utils.js b/utils.js deleted file mode 100644 index 6e63ad7..0000000 --- a/utils.js +++ /dev/null @@ -1,168 +0,0 @@ -var defaultChannel = ''; -var defaultName = 'test1'; -var inputWidth = '100px'; -var playerWidth = '800px'; -var defaultVolume = 1; -// -const STATE_STOPPED = 1; -const STATE_RECORDING = 2; -const STATE_REPLAYING = 3; -const STATE_WAITING = 4; -var state = STATE_STOPPED; -// -var channelElement = null; -var nameElement = null; -var stateInfoElement = null; -var streamNormal = null; -var streamMini = null; -var streamOutput = null; -var streamAlt = null; -var streams = []; -// -function updateStateInfo(newState) { - if (newState) { - state = newState; - } - if (!streamNormal || !streamMini || !streamOutput || !streamAlt) { - return; - } - var stateStr = ''; - switch (state) { - case STATE_STOPPED: stateStr = 'stopped'; break; - case STATE_RECORDING: stateStr = 'recording'; break; - case STATE_REPLAYING: stateStr = 'replaying'; break; - case STATE_WAITING: stateStr = 'waiting'; break; - } - stateInfoElement.textContent = ' state: ' + stateStr; -} -function updateStreamInfo(stream) { - stream.InfoElement.textContent = '[' + stream.StreamName + ']'; -} -function setStreamSrc(stream, src) { - stream.hls.loadSource('/' + src + '/' + channelElement.value + '|' + nameElement.value); - stream.hls.attachMedia(stream); -} -function recordOrReplayStream(newState, type) { - if (!channelElement.value) { - alert('Channel name textbox is empty'); - return; - } - stopStream(); - updateStateInfo(STATE_WAITING); - fetch('/' + type + '-begin/' + channelElement.value + '|' + nameElement.value).then(async function(response) { - if (response.status == 200) { - var str = await response.text(); - if (str) { - updateStateInfo(newState); - setStreamSrc(streamNormal, 'm3u8_normal'); - setStreamSrc(streamMini, 'm3u8_mini'); - setStreamSrc(streamOutput, 'm3u8_output'); - setStreamSrc(streamAlt, 'm3u8_alt'); - } else { - stopStream(); - } - } else { - stopStream(); - } - }); -} -function recordStream() { - recordOrReplayStream(STATE_RECORDING, 'record'); -} -function replayStream() { - alert('TODO'); - //recordOrReplayStream(STATE_REPLAYING, 'replay'); -} -function stopStream() { - for (var i = 0; i < streams.length; i++) { - streams[i].hls.stopLoad(); - streams[i].pause(); - } - updateStateInfo(STATE_STOPPED); -} -function createStreamElement(name) { - ///////////////////////////////////////// - var containerElement = document.createElement('div'); - containerElement.style.display = 'inline-block'; - containerElement.style.width = 'auto'; - document.body.appendChild(containerElement); - ///////////////////////////////////////// - var infoElement = document.createElement('span'); - containerElement.appendChild(infoElement); - containerElement.appendChild(document.createElement('br')); - ///////////////////////////////////////// - var stream = document.createElement('video'); - stream.style.maxWidth = playerWidth; - stream.style.width = playerWidth; - stream.InfoElement = infoElement; - stream.StreamName = name; - stream.autoplay = true; - stream.volume = defaultVolume; - stream.hls = new Hls(); - containerElement.appendChild(stream); - ///////////////////////////////////////// - streams.push(stream); - updateStreamInfo(stream); - return stream; -} -function onHlsLoaded() { - ///////////////////////////////////////// - var label1 = document.createElement('span'); - label1.textContent = 'channel:'; - document.body.appendChild(label1); - ///////////////////////////////////////// - channelElement = document.createElement('input'); - channelElement.value = defaultChannel; - channelElement.style.width = inputWidth; - document.body.appendChild(channelElement); - ///////////////////////////////////////// - var label2 = document.createElement('span'); - label2.textContent = 'name:'; - document.body.appendChild(label2); - ///////////////////////////////////////// - nameElement = document.createElement('input'); - nameElement.value = defaultName; - nameElement.style.width = inputWidth; - document.body.appendChild(nameElement); - ///////////////////////////////////////// - var recordBtn = document.createElement('button'); - recordBtn.textContent = 'record'; - recordBtn.onclick = recordStream; - document.body.appendChild(recordBtn); - ///////////////////////////////////////// - var replayBtn = document.createElement('button'); - replayBtn.textContent = 'replay'; - replayBtn.onclick = replayStream; - document.body.appendChild(replayBtn); - ///////////////////////////////////////// - var stopBtn = document.createElement('button'); - stopBtn.textContent = 'stop'; - stopBtn.onclick = stopStream; - document.body.appendChild(stopBtn); - ///////////////////////////////////////// - stateInfoElement = document.createElement('span'); - document.body.appendChild(stateInfoElement); - ///////////////////////////////////////// - document.body.appendChild(document.createElement('br')); - ///////////////////////////////////////// - streamNormal = createStreamElement('normal'); - streamMini = createStreamElement('mini'); - streamOutput = createStreamElement('output'); - streamAlt = createStreamElement('alt'); - updateStateInfo(); -} -function onContentLoaded() { - var script = document.createElement('script'); - script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest'; - script.onload = function() { - onHlsLoaded(); - }; - document.head.appendChild(script); -} -if (document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') { - onContentLoaded(); -} else { - window.addEventListener('DOMContentLoaded', function() { - onContentLoaded(); - }); -} \ No newline at end of file