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 @@
-
+
+ OAuth (optional) Only required for sub only VODs. All 3rd party OAuth tokens will not work. Click to see YouTube video on how to get OAuth token.(?):
+ Re-encode to CFR Fixes de-sync in Adobe Premiere, will take significantly more time and may result in quality loss.(?):
@@ -77,11 +79,13 @@
-
+
+
+
-
+
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index 8ef28222..d28c53f7 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Linq;
@@ -27,6 +28,7 @@
using Xabe.FFmpeg;
using WpfAnimatedGif;
using TwitchDownloader.Properties;
+using Xabe.FFmpeg.Events;
namespace TwitchDownloaderWPF
{
@@ -55,7 +57,11 @@ private void SetEnabled(bool isEnabled)
numEndMinute.IsEnabled = isEnabled;
numEndSecond.IsEnabled = isEnabled;
btnDownload.IsEnabled = isEnabled;
- numDownloadThreads.IsEnabled = isEnabled;
+ }
+ private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
+ {
+ Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
+ e.Handled = true;
}
private async void btnGetInfo_Click(object sender, RoutedEventArgs e)
@@ -67,7 +73,7 @@ private async void btnGetInfo_Click(object sender, RoutedEventArgs e)
try
{
Task taskInfo = InfoHelper.GetVideoInfo(videoId);
- Task taskAccessToken = InfoHelper.GetVideoToken(videoId);
+ Task taskAccessToken = InfoHelper.GetVideoToken(videoId, textOauth.Text);
await Task.WhenAll(taskInfo, taskAccessToken);
string thumbUrl = taskInfo.Result["data"][0]["thumbnail_url"].ToString().Replace("%{width}", 512.ToString()).Replace("%{height}", 290.ToString());
Task thumbImage = InfoHelper.GetThumb(thumbUrl);
@@ -278,14 +284,67 @@ private void BackgroundDownloadManager_DoWork(object sender, DoWorkEventArgs e)
catch { }
}
}
-
- (sender as BackgroundWorker).ReportProgress(0, "Finalizing MP4 (3/3)");
+
+ bool isVFR = false;
+ if (options.encode_cfr)
+ {
+ var process = new Process
+ {
+ StartInfo =
+ {
+ FileName = "ffmpeg.exe",
+ Arguments = $"-i \"" + Path.Combine(downloadFolder, "output.ts") + "\" -vf vfrdet -f null -",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ }
+ };
+ string output = "";
+ process.ErrorDataReceived += delegate(object o, DataReceivedEventArgs args)
+ {
+ if (args.Data != null && args.Data.Contains("Parsed_vfrdet"))
+ {
+ output += args.Data;
+ }
+ };
+ process.Start();
+ process.BeginErrorReadLine();
+ process.BeginOutputReadLine();
+ process.WaitForExit();
+ double VFR = double.Parse(output.Substring(output.IndexOf("VFR:") + 4, 8));
+ if (VFR == 0.0)
+ {
+ AppendLog("Constant framerate detected, no need to re-encode");
+ }
+ else
+ {
+ isVFR = true;
+ AppendLog("Detected variable framerate, re-encoding");
+ }
+ }
+
+ if (isVFR)
+ (sender as BackgroundWorker).ReportProgress(0, "Re-encoding MP4 (3/3)");
+ else
+ (sender as BackgroundWorker).ReportProgress(0, "Finalizing MP4 (3/3)");
string outputConvert = options.filename;
Task info = MediaInfo.Get(Path.Combine(downloadFolder, "output.ts"));
Task.WaitAll(info);
double seekTime = options.crop_begin;
- double seekDuration = info.Result.Duration.TotalSeconds - seekTime - options.crop_end;
- Task conversionResult = Conversion.New().Start(String.Format("-y -i \"{0}\" -ss {1} -analyzeduration {2} -t {3} -avoid_negative_ts make_zero -vcodec copy \"{4}\"", Path.Combine(downloadFolder, "output.ts"), seekTime.ToString(), int.MaxValue, seekDuration.ToString(), outputConvert));
+ double seekDuration = Math.Round(info.Result.Duration.TotalSeconds - seekTime - options.crop_end);
+ Task conversionResult = null;
+ if (isVFR)
+ {
+ int newFps = (int)Math.Ceiling(info.Result.VideoStreams.First().FrameRate);
+ conversionResult = Conversion.New().Start(String.Format("-y -i \"{0}\" -ss {1} -analyzeduration {2} -t {3} -crf 20 -filter:v fps=fps={4} \"{5}\"", Path.Combine(downloadFolder, "output.ts"), seekTime.ToString(), int.MaxValue, seekDuration.ToString(), newFps, outputConvert));
+ }
+ else
+ {
+ conversionResult = Conversion.New().Start(String.Format("-y -i \"{0}\" -ss {1} -analyzeduration {2} -t {3} -avoid_negative_ts make_zero -acodec copy -vcodec copy \"{4}\"", Path.Combine(downloadFolder, "output.ts"), seekTime.ToString(), int.MaxValue, seekDuration.ToString(), outputConvert));
+ }
+
Task.WaitAll(conversionResult);
if (Directory.Exists(downloadFolder))
DeleteDirectory(downloadFolder);
@@ -477,6 +536,8 @@ private void Page_Initialized(object sender, EventArgs e)
SetEnabled(false);
WebRequest.DefaultWebProxy = null;
numDownloadThreads.Value = Settings.Default.VodDownloadThreads;
+ textOauth.Text = Settings.Default.OAuth;
+ checkCFR.IsChecked = Settings.Default.EncodeCFR;
}
private void numDownloadThreads_ValueChanged(object sender, RoutedPropertyChangedEventArgs