diff --git a/FoxTube/Assets/Data/Patchnotes.xml b/FoxTube/Assets/Data/Patchnotes.xml
index f8c246f..15e610d 100644
--- a/FoxTube/Assets/Data/Patchnotes.xml
+++ b/FoxTube/Assets/Data/Patchnotes.xml
@@ -21,6 +21,7 @@
- Fixed backward navigation with minimized video
- Player re-design
- Added quality selector to live streams playback
+- Added "Auto" quality option for videos
### Что нового:
- Исправлена проблема получения истории, "Посмотреть позже" и рекомендаций
@@ -41,6 +42,7 @@
- Исправлена обратная навигация при уменьшенном видео
- Редизайн плеера
- Добавлено меню выбора качества для прямых эфиров
+- Добавлено опция "Авто" в меню выбора качеста видео
diff --git a/FoxTube/Classes/ManifestGenerator.cs b/FoxTube/Classes/ManifestGenerator.cs
index e3a344c..0ce6ea5 100644
--- a/FoxTube/Classes/ManifestGenerator.cs
+++ b/FoxTube/Classes/ManifestGenerator.cs
@@ -10,8 +10,8 @@ using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using System.Web;
using System.Xml;
+using Windows.ApplicationModel.Resources;
using Windows.Storage;
using YoutubeExplode.Models.MediaStreams;
@@ -31,119 +31,164 @@ namespace FoxTube.Controls.Player
{
manifest = await roaming.CreateFileAsync("manifest.mpd", CreationCollisionOption.GenerateUniqueName);
}
+ try
+ {
- XmlDocument doc = new XmlDocument();
+ XmlDocument doc = new XmlDocument();
- XmlElement mpd = doc.CreateElement("MPD");
- mpd.SetAttribute("mediaPresentationDuration", meta.ContentDetails.Duration);
- mpd.SetAttribute("minBufferTime", "PT2S");
- mpd.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
- mpd.SetAttribute("type", "static");
+ XmlElement mpd = doc.CreateElement("MPD");
+ mpd.SetAttribute("mediaPresentationDuration", meta.ContentDetails.Duration);
+ mpd.SetAttribute("minBufferTime", "PT2S");
+ mpd.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
+ mpd.SetAttribute("type", "static");
- XmlElement period = doc.CreateElement("Period");
+ XmlElement period = doc.CreateElement("Period");
- XmlElement videoSet = doc.CreateElement("AdaptationSet");
- XmlElement videoMeta = doc.CreateElement("ContentComponent");
- videoMeta.SetAttribute("contentType", "video");
- videoMeta.SetAttribute("id", "1");
- videoSet.AppendChild(videoMeta);
+ XmlElement videoSet = doc.CreateElement("AdaptationSet");
+ XmlElement videoMeta = doc.CreateElement("ContentComponent");
+ videoMeta.SetAttribute("contentType", "video");
+ videoMeta.SetAttribute("id", "1");
+ videoSet.AppendChild(videoMeta);
- StreamInfo streamInfo = await GetInfoAsync(meta, requestedQuality);
+ StreamInfo streamInfo = await GetInfoAsync(meta, requestedQuality);
+ foreach (var i in streamInfo.Video)
+ videoSet.AppendChild(GetVideoPresentation(doc, i));
+
+ XmlElement audioSet = doc.CreateElement("AdaptationSet");
+ XmlElement audioMeta = doc.CreateElement("ContentComponent");
+ audioMeta.SetAttribute("contentType", "audio");
+ audioMeta.SetAttribute("id", "2");
+ audioSet.AppendChild(audioMeta);
+
+ foreach (var i in streamInfo.Audio)
+ audioSet.AppendChild(GetAudioPresentation(doc, i));
+
+ doc.AppendChild(mpd);
+ mpd.AppendChild(period);
+ period.AppendChild(videoSet);
+ period.AppendChild(audioSet);
+
+ doc.Save(await manifest.OpenStreamForWriteAsync());
+
+ return $"ms-appdata:///roaming/{manifest.Name}".ToUri();
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
+
+ private static XmlElement GetVideoPresentation(XmlDocument doc, StreamInfo.VideoInfo info)
+ {
XmlElement representation = doc.CreateElement("Representation");
- representation.SetAttribute("bandwidth", GetBandwidth(requestedQuality.VideoQuality));
- representation.SetAttribute("id", "1");
- representation.SetAttribute("mimeType", $"video/{requestedQuality.Container.GetFileExtension()}");
+ representation.SetAttribute("bandwidth", GetBandwidth(info.Label));
+ representation.SetAttribute("id", info.Itag);
+ representation.SetAttribute("mimeType", info.MimeType);
+ representation.SetAttribute("codecs", info.Codecs);
+ representation.SetAttribute("fps", info.Fps);
+ representation.SetAttribute("height", info.Height);
+ representation.SetAttribute("width", info.Width);
XmlElement baseUrl = doc.CreateElement("BaseURL");
- baseUrl.InnerText = requestedQuality.Url;
+ baseUrl.InnerText = info.Url;
representation.AppendChild(baseUrl);
XmlElement segmentBase = doc.CreateElement("SegmentBase");
- segmentBase.SetAttribute("indexRange", streamInfo.Video.IndexRange);
+ segmentBase.SetAttribute("indexRange", info.IndexRange);
representation.AppendChild(segmentBase);
XmlElement initialization = doc.CreateElement("Initialization");
- initialization.SetAttribute("range", streamInfo.Video.InitRange);
+ initialization.SetAttribute("range", info.InitRange);
segmentBase.AppendChild(initialization);
- videoSet.AppendChild(representation);
-
- XmlElement audioSet = doc.CreateElement("AdaptationSet");
- XmlElement audioMeta = doc.CreateElement("ContentComponent");
- audioMeta.SetAttribute("contentType", "audio");
- audioMeta.SetAttribute("id", "2");
- audioSet.AppendChild(audioMeta);
+ return representation;
+ }
+ private static XmlElement GetAudioPresentation(XmlDocument doc, StreamInfo.AudioInfo info)
+ {
XmlElement audio = doc.CreateElement("Representation");
audio.SetAttribute("bandwidth", "200000");
- audio.SetAttribute("id", "2");
- audio.SetAttribute("sampleRate", streamInfo.Audio.SampleRate);
- audio.SetAttribute("numChannels", streamInfo.Audio.ChannelsCount);
- audio.SetAttribute("codecs", list.Audio.First(i => i.Container.GetFileExtension() == "webm").AudioEncoding.ToString());
- audio.SetAttribute("mimeType", $"audio/{list.Audio.First(i => i.Container.GetFileExtension() == "webm").Container.GetFileExtension()}");
- audioSet.AppendChild(audio);
+ audio.SetAttribute("id", info.Itag);
+ audio.SetAttribute("sampleRate", info.SampleRate);
+ audio.SetAttribute("numChannels", info.ChannelsCount);
+ audio.SetAttribute("codecs", info.Codecs);
+ audio.SetAttribute("mimeType", info.MimeType);
XmlElement audioUrl = doc.CreateElement("BaseURL");
- audioUrl.InnerText = list.Audio.First(i => i.Container.GetFileExtension() == "webm").Url;
+ audioUrl.InnerText = info.Url;
audio.AppendChild(audioUrl);
XmlElement audioSegmentBase = doc.CreateElement("SegmentBase");
- audioSegmentBase.SetAttribute("indexRange", streamInfo.Audio.IndexRange);
- audioSegmentBase.SetAttribute("indexRangeExact", "true");
+ audioSegmentBase.SetAttribute("indexRange", info.IndexRange);
audio.AppendChild(audioSegmentBase);
XmlElement audioInit = doc.CreateElement("Initialization");
- audioInit.SetAttribute("range", streamInfo.Audio.InitRange);
+ audioInit.SetAttribute("range", info.InitRange);
audioSegmentBase.AppendChild(audioInit);
- doc.AppendChild(mpd);
- mpd.AppendChild(period);
- period.AppendChild(videoSet);
- period.AppendChild(audioSet);
-
- doc.Save(await manifest.OpenStreamForWriteAsync());
-
- //TODO: Fix this shit. It doesn't work
- return $"ms-appdata:///roaming/{manifest.Name}".ToUri();
+ return audio;
}
private static async Task GetInfoAsync(Video info, VideoStreamInfo requestedQuality)
{
try
{
+ StreamInfo si = new StreamInfo();
HttpClient http = new HttpClient();
- string response = HttpUtility.HtmlDecode(await http.GetStringAsync($"https://youtube.com/embed/{info.Id}?disable_polymer=true&hl=en"));
+
+ string response = await http.GetStringAsync($"https://youtube.com/watch?v={info.Id}&disable_polymer=true&bpctr=9999999999&hl=en");
IHtmlDocument videoEmbedPageHtml = new HtmlParser().Parse(response);
string playerConfigRaw = Regex.Match(videoEmbedPageHtml.Source.Text,
- @"yt\.setConfig\({'PLAYER_CONFIG': (?\{[^\{\}]*(((?\{)[^\{\}]*)+((?\})[^\{\}]*)+)*(?(Open)(?!))\})")
+ @"ytplayer\.config = (?\{[^\{\}]*(((?\{)[^\{\}]*)+((?\})[^\{\}]*)+)*(?(Open)(?!))\})")
.Groups["Json"].Value;
JToken playerConfigJson = JToken.Parse(playerConfigRaw);
- string sts = playerConfigJson.SelectToken("sts").Value();
- string eurl = WebUtility.UrlEncode($"https://youtube.googleapis.com/v/{info.Id}");
- string url = $"https://youtube.com/get_video_info?video_id={info.Id}&el=embedded&sts={sts}&eurl={eurl}&hl=en";
- string raw = await http.GetStringAsync(url);
+ var playerResponseRaw = playerConfigJson.SelectToken("args.player_response").Value();
+ JToken playerResponseJson = JToken.Parse(playerResponseRaw);
+ string errorReason = playerResponseJson.SelectToken("playabilityStatus.reason")?.Value();
+ if (!string.IsNullOrWhiteSpace(errorReason))
+ throw new InvalidDataException($"Video [{info.Id}] is unplayable. Reason: {errorReason}");
- Dictionary videoInfoDic = SplitQuery(raw);
+ List> adaptiveStreamInfosUrl = playerConfigJson.SelectToken("args.adaptive_fmts")?.Value().Split(',').Select(SplitQuery).ToList();
+ List> video =
+ requestedQuality == null ?
+ adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("quality_label")) :
+ adaptiveStreamInfosUrl.FindAll(i => i.ContainsValue(requestedQuality.VideoQualityLabel));
+ List> audio = adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("audio_sample_rate"));
- StreamInfo si = new StreamInfo();
+ foreach (var i in video)
+ si.Video.Add(new StreamInfo.VideoInfo
+ {
+ IndexRange = i["index"],
+ Url = i["url"],
+ Itag = i["itag"],
+ Fps = i["fps"],
+ Height = i["size"].Split('x')[1],
+ Width = i["size"].Split('x')[0],
+ Codecs = i["type"].Split('"')[1],
+ MimeType = i["type"].Split(';')[0],
+ Label = i["quality_label"]
+ });
- List> adaptiveStreamInfosUrl = videoInfoDic.GetValueOrDefault("adaptive_fmts").Split(',').Select(SplitQuery).ToList();
- Dictionary video = adaptiveStreamInfosUrl.Find(i => i["quality_label"] == requestedQuality.VideoQualityLabel && i["type"].Contains(requestedQuality.Container.GetFileExtension()));
- Dictionary audio = adaptiveStreamInfosUrl.Find(i => i.ContainsKey("audio_sample_rate") && i["type"].Contains("webm"));
-
- si.Video.IndexRange = video["index"];
- si.Audio.ChannelsCount = audio["audio_channels"];
- si.Audio.IndexRange = audio["index"];
- si.Audio.SampleRate = audio["audio_sample_rate"];
+ foreach (var i in audio)
+ si.Audio.Add(new StreamInfo.AudioInfo
+ {
+ ChannelsCount = i["audio_channels"],
+ IndexRange = i["index"],
+ SampleRate = i["audio_sample_rate"],
+ Codecs = i["type"].Split('"')[1],
+ MimeType = i["type"].Split(';')[0],
+ Url = i["url"],
+ Itag = i["itag"]
+ });
return si;
}
catch
{
- return null;
+ return new StreamInfo();
}
}
@@ -173,57 +218,32 @@ namespace FoxTube.Controls.Player
return dic;
}
- private static void AppendVideoSet(XmlDocument doc, XmlElement root, List list)
- {
- for (int k = 0; k < list.Count; k++)
- {
- XmlElement representation = doc.CreateElement("Representation");
- representation.SetAttribute("bandwidth", GetBandwidth(list[k].VideoQuality));
- representation.SetAttribute("height", list[k].Resolution.Height.ToString());
- representation.SetAttribute("width", list[k].Resolution.Width.ToString());
- representation.SetAttribute("id", (k + 1).ToString());
- representation.SetAttribute("mimeType", $"video/{list[k].Container.GetFileExtension()}");
-
- XmlElement baseUrl = doc.CreateElement("BaseURL");
- baseUrl.InnerText = list[k].Url;
- representation.AppendChild(baseUrl);
-
- XmlElement segmentBase = doc.CreateElement("SegmentBase");
- representation.AppendChild(segmentBase);
-
- XmlElement initialization = doc.CreateElement("Initialization");
- initialization.SetAttribute("range", "0-1000");
- segmentBase.AppendChild(initialization);
-
- root.AppendChild(representation);
- }
- }
-
- private static string GetBandwidth(VideoQuality quality)
+ private static string GetBandwidth(string quality)
{
+ string parsed = quality.Split('p')[0];
switch (quality)
{
- case VideoQuality.High4320:
+ case "4320":
return $"16763040";
- case VideoQuality.High3072:
+ case "3072":
return $"11920384";
- case VideoQuality.High2880:
+ case "2880":
return $"11175360";
- case VideoQuality.High2160:
+ case "2160":
return $"8381520";
- case VideoQuality.High1440:
+ case "1440":
return $"5587680";
- case VideoQuality.High1080:
+ case "1080":
return $"4190760";
- case VideoQuality.High720:
+ case "720":
return $"2073921";
- case VideoQuality.Medium480:
+ case "480":
return $"869460";
- case VideoQuality.Medium360:
+ case "360":
return $"686521";
- case VideoQuality.Low240:
+ case "240":
return $"264835";
- case VideoQuality.Low144:
+ case "144":
default:
return $"100000";
}
@@ -263,7 +283,7 @@ namespace FoxTube.Controls.Player
list.Add(new StreamQuality
{
- Resolution = "Auto",
+ Resolution = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto"),
Url = url.ToUri()
});
list.Reverse();
@@ -285,6 +305,14 @@ namespace FoxTube.Controls.Player
{
public string IndexRange { get; set; }
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
+ public string Itag { get; set; }
+ public string Fps { get; set; }
+ public string Url { get; set; }
+ public string Codecs { get; set; }
+ public string MimeType { get; set; }
+ public string Height { get; set; }
+ public string Width { get; set; }
+ public string Label { get; set; }
}
public class AudioInfo
{
@@ -292,10 +320,14 @@ namespace FoxTube.Controls.Player
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
public string SampleRate { get; set; }
public string ChannelsCount { get; set; }
+ public string Codecs { get; set; }
+ public string MimeType { get; set; }
+ public string Url { get; set; }
+ public string Itag { get; set; }
}
- public VideoInfo Video { get; } = new VideoInfo();
- public AudioInfo Audio { get; } = new AudioInfo();
+ public List Video { get; } = new List();
+ public List Audio { get; } = new List();
}
public class StreamQuality
diff --git a/FoxTube/Controls/Player/PlayerControls.cs b/FoxTube/Controls/Player/PlayerControls.cs
index 592e812..53d5e5c 100644
--- a/FoxTube/Controls/Player/PlayerControls.cs
+++ b/FoxTube/Controls/Player/PlayerControls.cs
@@ -205,7 +205,7 @@ namespace FoxTube
object info = (quality.SelectedItem as ComboBoxItem).Tag;
if (info is MuxedStreamInfo)
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as MuxedStreamInfo).Url.ToUri()));
- else if (info is VideoStreamInfo)
+ else if (info is VideoStreamInfo || info == null)
Player.SetPlaybackSource(MediaSource.CreateFromUri(await ManifestGenerator.GetManifest(Meta, info as VideoStreamInfo, MediaStreams)));
else if (info is StreamQuality)
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as StreamQuality).Url));
@@ -437,6 +437,11 @@ namespace FoxTube
qualityList.Sort(new QualityComparer());
qualityList.Reverse();
+ quality.Items.Add(new ComboBoxItem
+ {
+ Content = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto")
+ });
+
foreach (string i in qualityList)
{
object tag;
diff --git a/FoxTube/Pages/SettingsPages/General.xaml b/FoxTube/Pages/SettingsPages/General.xaml
index 1bd03f3..0123479 100644
--- a/FoxTube/Pages/SettingsPages/General.xaml
+++ b/FoxTube/Pages/SettingsPages/General.xaml
@@ -29,6 +29,7 @@
+
diff --git a/FoxTube/Strings/en-US/General.resw b/FoxTube/Strings/en-US/General.resw
index e5d2758..bc61e55 100644
--- a/FoxTube/Strings/en-US/General.resw
+++ b/FoxTube/Strings/en-US/General.resw
@@ -207,4 +207,7 @@
Search relevance language
+
+ Auto
+
\ No newline at end of file
diff --git a/FoxTube/Strings/en-US/VideoPage.resw b/FoxTube/Strings/en-US/VideoPage.resw
index ad8029a..732f809 100644
--- a/FoxTube/Strings/en-US/VideoPage.resw
+++ b/FoxTube/Strings/en-US/VideoPage.resw
@@ -300,4 +300,7 @@
Continue from
+
+ Auto
+
\ No newline at end of file
diff --git a/FoxTube/Strings/ru-RU/General.resw b/FoxTube/Strings/ru-RU/General.resw
index 667f827..b7a646e 100644
--- a/FoxTube/Strings/ru-RU/General.resw
+++ b/FoxTube/Strings/ru-RU/General.resw
@@ -207,4 +207,7 @@
Предпочитаемый язык поиска
+
+ Авто
+
\ No newline at end of file
diff --git a/FoxTube/Strings/ru-RU/VideoPage.resw b/FoxTube/Strings/ru-RU/VideoPage.resw
index 18c82bd..1392183 100644
--- a/FoxTube/Strings/ru-RU/VideoPage.resw
+++ b/FoxTube/Strings/ru-RU/VideoPage.resw
@@ -300,4 +300,7 @@
Продолжить с
+
+ Авто
+
\ No newline at end of file