diff --git a/TwitchDownloaderWPF/App.config b/TwitchDownloaderWPF/App.config index de5d94f4..3f54f959 100644 --- a/TwitchDownloaderWPF/App.config +++ b/TwitchDownloaderWPF/App.config @@ -84,6 +84,12 @@ 255 + + + + + False + \ No newline at end of file diff --git a/TwitchDownloaderWPF/ChatRoot.cs b/TwitchDownloaderWPF/ChatRoot.cs index e4874f2a..81405b1d 100644 --- a/TwitchDownloaderWPF/ChatRoot.cs +++ b/TwitchDownloaderWPF/ChatRoot.cs @@ -48,6 +48,7 @@ public class Emoticon2 public class Message { public string body { get; set; } + public int bits_spent { get; set; } public List fragments { get; set; } public bool is_action { get; set; } public List user_badges { get; set; } diff --git a/TwitchDownloaderWPF/InfoHelper.cs b/TwitchDownloaderWPF/InfoHelper.cs index 7c192407..01c68b1a 100644 --- a/TwitchDownloaderWPF/InfoHelper.cs +++ b/TwitchDownloaderWPF/InfoHelper.cs @@ -37,13 +37,13 @@ public static async Task GetVideoInfo(int videoId) } } - public static async Task GetVideoToken(int videoId) + public static async Task GetVideoToken(int videoId, string authToken) { using (WebClient client = new WebClient()) { client.Encoding = Encoding.UTF8; client.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko"); - string response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/api/vods/{0}/access_token", videoId)); + string response = await client.DownloadStringTaskAsync(String.Format("https://api.twitch.tv/api/vods/{0}/access_token{1}", videoId, (authToken == "" ? "" : "?oauth_token=" + authToken))); JObject result = JObject.Parse(response); return result; } diff --git a/TwitchDownloaderWPF/PageChatRender.xaml.cs b/TwitchDownloaderWPF/PageChatRender.xaml.cs index 0755c934..04eeadf9 100644 --- a/TwitchDownloaderWPF/PageChatRender.xaml.cs +++ b/TwitchDownloaderWPF/PageChatRender.xaml.cs @@ -104,6 +104,7 @@ private void BackgroundRenderManager_DoWork(object sender, DoWorkEventArgs e) BlockingCollection finalComments = new BlockingCollection(); List thirdPartyEmotes = new List(); List chatBadges = new List(); + List cheerEmotes = new List(); Dictionary chatEmotes = new Dictionary(); Dictionary emojiCache = new Dictionary(); Random rand = new Random(); @@ -132,6 +133,8 @@ private void BackgroundRenderManager_DoWork(object sender, DoWorkEventArgs e) GetThirdPartyEmotes(thirdPartyEmotes, chatJson.streamer, renderOptions, cacheFolder); (sender as BackgroundWorker).ReportProgress(0, new Progress("Fetching Twitter Emojis")); GetTwitterEmojis(emojiCache, chatJson.comments, renderOptions, cacheFolder, emojiRegex); + (sender as BackgroundWorker).ReportProgress(0, new Progress("Fetching Bit Emotes")); + GetBits(cheerEmotes, renderOptions, cacheFolder, emojiRegex); Size canvasSize = new Size(renderOptions.chat_width, renderOptions.text_height); SKPaint nameFont = new SKPaint() { Typeface = SKTypeface.FromFamilyName(renderOptions.font, SKFontStyle.Bold), LcdRenderText = true, SubpixelText = true, TextSize = (float)renderOptions.font_size, IsAntialias = true, HintingLevel = SKPaintHinting.Full, FilterQuality = SKFilterQuality.High }; @@ -151,7 +154,7 @@ private void BackgroundRenderManager_DoWork(object sender, DoWorkEventArgs e) string userName = comment.commenter.display_name.ToString(); int default_x = 2; Point drawPos = new Point(default_x, 0); - string colorHtml = (comment.message.user_color != null ? comment.message.user_color : defaultColors[rand.Next(0, defaultColors.Length)]); + string colorHtml = (comment.message.user_color != null ? comment.message.user_color : defaultColors[Math.Abs(userName.GetHashCode()) % defaultColors.Length]); SKColor userColor = SKColor.Parse(colorHtml); userColor = GenerateUserColor(userColor, renderOptions.background_color); @@ -166,7 +169,7 @@ private void BackgroundRenderManager_DoWork(object sender, DoWorkEventArgs e) sectionImage = DrawTimestamp(sectionImage, imageList, messageFont, renderOptions, comment, canvasSize, ref drawPos, ref default_x); sectionImage = DrawBadges(sectionImage, imageList, renderOptions, chatBadges, comment, canvasSize, ref drawPos); sectionImage = DrawUsername(sectionImage, imageList, renderOptions, nameFont, userName, userColor, canvasSize, ref drawPos); - sectionImage = DrawMessage(sectionImage, imageList, renderOptions, currentGifEmotes, messageFont, emojiCache, chatEmotes, thirdPartyEmotes, comment, canvasSize, ref drawPos, emojiRegex, ref default_x, emoteList, emotePositionList); + sectionImage = DrawMessage(sectionImage, imageList, renderOptions, currentGifEmotes, messageFont, emojiCache, chatEmotes, thirdPartyEmotes, cheerEmotes, comment, canvasSize, ref drawPos, emojiRegex, ref default_x, emoteList, emotePositionList); int finalHeight = 0; foreach (var img in imageList) @@ -239,34 +242,30 @@ private string GetStreamerName(int id) private SKColor GenerateUserColor(SKColor userColor, SKColor background_color) { - if (userColor.Red == 0 && userColor.Green == 0 && userColor.Blue == 0) + float backgroundHue, backgroundSaturation, backgroundBrightness; + background_color.ToHsl(out backgroundHue, out backgroundSaturation, out backgroundBrightness); + float userHue, userSaturation, userBrightness; + userColor.ToHsl(out userHue, out userSaturation, out userBrightness); + + if (backgroundBrightness < 25) { - SKColor newColor = SKColor.Parse("#858585"); - return newColor; + //Dark background + if (userBrightness < 45) + { + userBrightness = 45; + SKColor newColor = SKColor.FromHsl(userHue, userSaturation, userBrightness); + return newColor; + } } - - return userColor; - //Too scuffed, also can mess up if using a chroma key - /* - //I don't really know much about this, but i'll give it a shot - float[] userColorHsl = new float[3]; - float[] backgroundColorHsl = new float[3]; - userColor.ToHsl(out userColorHsl[0], out userColorHsl[1], out userColorHsl[2]); - background_color.ToHsl(out backgroundColorHsl[0], out backgroundColorHsl[1], out backgroundColorHsl[2]); - - if (Math.Abs(userColorHsl[2] - backgroundColorHsl[2]) < 10) + if (Math.Abs(backgroundBrightness - userBrightness) < 10 && backgroundBrightness > 50) { - if (backgroundColorHsl[2] < 50) - userColorHsl[2] += 50; - else - userColorHsl[2] -= 50; - SKColor newColor = SKColor.FromHsl(userColorHsl[0], userColorHsl[1], userColorHsl[2]); + userBrightness -= 20; + SKColor newColor = SKColor.FromHsl(userHue, userSaturation, userBrightness); return newColor; } - else - return userColor; - */ + + return userColor; } private void RenderVideo(RenderOptions renderOptions, Queue finalComments, List comments, object sender) @@ -305,10 +304,14 @@ private void RenderVideo(RenderOptions renderOptions, Queue final UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, - RedirectStandardOutput = true + RedirectStandardOutput = true, + RedirectStandardError = true } }; + //process.ErrorDataReceived += ErrorDataHandler; + process.Start(); + process.BeginErrorReadLine(); process.BeginOutputReadLine(); stopwatch.Start(); @@ -471,8 +474,15 @@ private void RenderVideo(RenderOptions renderOptions, Queue final process.WaitForExit(); } - public SKBitmap DrawMessage(SKBitmap sectionImage, List imageList, RenderOptions renderOptions, List currentGifEmotes, SKPaint messageFont, Dictionary emojiCache, Dictionary chatEmotes, List thirdPartyEmotes, Comment comment, Size canvasSize, ref Point drawPos, string emojiRegex, ref int default_x, List emoteList, List emotePositionList) + private void ErrorDataHandler(object sender, DataReceivedEventArgs e) { + AppendLog(e.Data); + } + + public SKBitmap DrawMessage(SKBitmap sectionImage, List imageList, RenderOptions renderOptions, List currentGifEmotes, SKPaint messageFont, Dictionary emojiCache, Dictionary chatEmotes, List thirdPartyEmotes, List cheerEmotes, Comment comment, Size canvasSize, ref Point drawPos, string emojiRegex, ref int default_x, List emoteList, List emotePositionList) + { + int bitsCount = comment.message.bits_spent; + foreach (var fragment in comment.message.fragments) { if (fragment.emoticon == null) @@ -581,7 +591,27 @@ public SKBitmap DrawMessage(SKBitmap sectionImage, List imageList, Ren } else { - sectionImage = DrawText(sectionImage, output, messageFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, true, default_x); + bool bitsPrinted = false; + try + { + if (bitsCount > 0 && output.Any(char.IsDigit) && cheerEmotes.Any(x => output.Contains(x.prefix))) + { + CheerEmote currentCheerEmote = cheerEmotes.Find(x => output.Contains(x.prefix)); + int bitsIndex = output.IndexOfAny("0123456789".ToCharArray()); + int bitsAmount = Int32.Parse(output.Substring(bitsIndex)); + bitsCount -= bitsAmount; + KeyValuePair tierList = currentCheerEmote.getTier(bitsAmount); + GifEmote emote = new GifEmote(new Point(drawPos.X, drawPos.Y), tierList.Value.name, tierList.Value.codec, tierList.Value.imageScale, tierList.Value.emote_frames); + currentGifEmotes.Add(emote); + drawPos.X += (tierList.Value.width / tierList.Value.imageScale) * renderOptions.image_scale + (3 * renderOptions.image_scale); + sectionImage = DrawText(sectionImage, bitsAmount.ToString(), messageFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, true, default_x); + bitsPrinted = true; + } + } + catch + { } + if (!bitsPrinted) + sectionImage = DrawText(sectionImage, output, messageFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, true, default_x); } } } @@ -904,6 +934,23 @@ private void GetThirdPartyEmotes(List thirdPartyEmotes, Streame SKBitmap temp_emote = SKBitmap.Decode(ms2); thirdPartyEmotes.Add(new ThirdPartyEmote(new List() { temp_emote }, SKCodec.Create(ms), emote["code"].ToString(), emote["imageType"].ToString(), id, 2)); } + foreach (var emote in BBTV_channel["channelEmotes"]) + { + string id = emote["id"].ToString(); + byte[] bytes; + string fileName = Path.Combine(bttvFolder, id + "_2x.png"); + if (File.Exists(fileName)) + bytes = File.ReadAllBytes(fileName); + else + { + bytes = client.DownloadData(String.Format("https://cdn.betterttv.net/emote/{0}/2x", id)); + File.WriteAllBytes(fileName, bytes); + } + MemoryStream ms = new MemoryStream(bytes); + MemoryStream ms2 = new MemoryStream(bytes); + SKBitmap temp_emote = SKBitmap.Decode(ms2); + thirdPartyEmotes.Add(new ThirdPartyEmote(new List() { temp_emote }, SKCodec.Create(ms), emote["code"].ToString(), emote["imageType"].ToString(), id, 2)); + } } catch { } } @@ -1138,6 +1185,60 @@ private void GetEmotes(Dictionary chatEmotes, List co } } } + private void GetBits(List cheerEmotes, RenderOptions renderOptions, string cacheFolder, string emojiRegex) + { + string bitsFolder = Path.Combine(cacheFolder, "bits"); + if (!Directory.Exists(bitsFolder)) + Directory.CreateDirectory(bitsFolder); + + using (WebClient client = new WebClient()) + { + client.Headers.Add("Accept", "application/vnd.twitchtv.v5+json"); + client.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko"); + + JObject globalCheer = JObject.Parse(client.DownloadString("https://api.twitch.tv/kraken/bits/actions")); + + foreach (JToken emoteToken in globalCheer["actions"]) + { + string prefix = emoteToken["prefix"].ToString(); + List> tierList = new List>(); + CheerEmote newEmote = new CheerEmote() {prefix = prefix, tierList = tierList }; + byte[] finalBytes = null; + foreach (JToken tierToken in emoteToken["tiers"]) + { + try + { + int minBits = tierToken["min_bits"].ToObject(); + string fileName = Path.Combine(bitsFolder, prefix + minBits + "_2x.gif"); + + if (File.Exists(fileName)) + { + finalBytes = File.ReadAllBytes(fileName); + } + else + { + byte[] bytes = client.DownloadData(tierToken["images"]["dark"]["animated"]["2"].ToString()); + File.WriteAllBytes(fileName, bytes); + finalBytes = bytes; + } + + if (finalBytes != null) + { + MemoryStream ms = new MemoryStream(finalBytes); + MemoryStream ms2 = new MemoryStream(finalBytes); + SKBitmap finalBitmap = SKBitmap.Decode(ms); + ThirdPartyEmote emote = new ThirdPartyEmote(new List() { finalBitmap }, SKCodec.Create(ms2), prefix, "gif", "", 2); + tierList.Add(new KeyValuePair(minBits, emote)); + } + } + catch + { } + } + cheerEmotes.Add(newEmote); + } + } + } + private void LoadSettings() { try @@ -1310,8 +1411,9 @@ private void Page_Initialized(object sender, EventArgs e) Codec vp8Codec = new Codec() { Name = "VP8", InputArgs = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -", OutputArgs = "-c:v libvpx -crf 18 -b:v 2M -pix_fmt yuva420p -auto-alt-ref 0 \"{save_path}\"" }; Codec vp9Codec = new Codec() { Name = "VP9", InputArgs = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -", OutputArgs = "-c:v libvpx-vp9 -crf 18 -b:v 2M -pix_fmt yuva420p \"{save_path}\"" }; Codec rleCodec = new Codec() { Name = "RLE", InputArgs = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -", OutputArgs = "-c:v qtrle -pix_fmt argb \"{save_path}\"" }; + Codec proresCodec = new Codec() { Name = "ProRes", InputArgs = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -", OutputArgs = "-c:v prores_ks -pix_fmt argb \"{save_path}\"" }; VideoContainer mp4Container = new VideoContainer() { Name = "MP4", SupportedCodecs = new List() { h264Codec, h265Codec } }; - VideoContainer movContainer = new VideoContainer() { Name = "MOV", SupportedCodecs = new List() { h264Codec, h265Codec, rleCodec } }; + VideoContainer movContainer = new VideoContainer() { Name = "MOV", SupportedCodecs = new List() { h264Codec, h265Codec, rleCodec, proresCodec } }; VideoContainer webmContainer = new VideoContainer() { Name = "WEBM", SupportedCodecs = new List() { vp8Codec, vp9Codec } }; VideoContainer mkvContainer = new VideoContainer() { Name = "MKV", SupportedCodecs = new List() { h264Codec, h265Codec, vp8Codec, vp9Codec } }; comboFormat.Items.Add(mp4Container); @@ -1397,4 +1499,23 @@ public override string ToString() return Name; } } + + public class CheerEmote + { + public string prefix { get; set; } + public List> tierList = new List>(); + + public KeyValuePair getTier(int value) + { + KeyValuePair returnPair = tierList.First(); + foreach (KeyValuePair tierPair in tierList) + { + if (tierPair.Key > value) + break; + returnPair = tierPair; + } + + return returnPair; + } + } } \ No newline at end of file diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml b/TwitchDownloaderWPF/PageVodDownload.xaml index 3d6ade79..bcf90512 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml +++ b/TwitchDownloaderWPF/PageVodDownload.xaml @@ -52,12 +52,14 @@