From e8c64f65ec1a7c8e7ce767e5b218f0d42e9d3985 Mon Sep 17 00:00:00 2001 From: Michael Gordeev Date: Sat, 18 May 2019 17:37:35 +0300 Subject: [PATCH] - Fixed manifest generator - Added "Auto" quality --- FoxTube/Assets/Data/Patchnotes.xml | 2 + FoxTube/Classes/ManifestGenerator.cs | 240 ++++++++++++---------- FoxTube/Controls/Player/PlayerControls.cs | 7 +- FoxTube/Pages/SettingsPages/General.xaml | 1 + FoxTube/Strings/en-US/General.resw | 3 + FoxTube/Strings/en-US/VideoPage.resw | 3 + FoxTube/Strings/ru-RU/General.resw | 3 + FoxTube/Strings/ru-RU/VideoPage.resw | 3 + 8 files changed, 157 insertions(+), 105 deletions(-) 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