Archived
1
0

- Fixed manifest generator

- Added "Auto" quality
This commit is contained in:
Michael Gordeev
2019-05-18 17:37:35 +03:00
parent af989c5c45
commit e8c64f65ec
8 changed files with 157 additions and 105 deletions
+2
View File
@@ -21,6 +21,7 @@
- Fixed backward navigation with minimized video - Fixed backward navigation with minimized video
- Player re-design - Player re-design
- Added quality selector to live streams playback - Added quality selector to live streams playback
- Added "Auto" quality option for videos
</en-US> </en-US>
<ru-RU>### Что нового: <ru-RU>### Что нового:
- Исправлена проблема получения истории, "Посмотреть позже" и рекомендаций - Исправлена проблема получения истории, "Посмотреть позже" и рекомендаций
@@ -41,6 +42,7 @@
- Исправлена обратная навигация при уменьшенном видео - Исправлена обратная навигация при уменьшенном видео
- Редизайн плеера - Редизайн плеера
- Добавлено меню выбора качества для прямых эфиров - Добавлено меню выбора качества для прямых эфиров
- Добавлено опция "Авто" в меню выбора качеста видео
</ru-RU> </ru-RU>
</content> </content>
</item> </item>
+136 -104
View File
@@ -10,8 +10,8 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using System.Xml; using System.Xml;
using Windows.ApplicationModel.Resources;
using Windows.Storage; using Windows.Storage;
using YoutubeExplode.Models.MediaStreams; using YoutubeExplode.Models.MediaStreams;
@@ -31,119 +31,164 @@ namespace FoxTube.Controls.Player
{ {
manifest = await roaming.CreateFileAsync("manifest.mpd", CreationCollisionOption.GenerateUniqueName); manifest = await roaming.CreateFileAsync("manifest.mpd", CreationCollisionOption.GenerateUniqueName);
} }
try
{
XmlDocument doc = new XmlDocument(); XmlDocument doc = new XmlDocument();
XmlElement mpd = doc.CreateElement("MPD"); XmlElement mpd = doc.CreateElement("MPD");
mpd.SetAttribute("mediaPresentationDuration", meta.ContentDetails.Duration); mpd.SetAttribute("mediaPresentationDuration", meta.ContentDetails.Duration);
mpd.SetAttribute("minBufferTime", "PT2S"); mpd.SetAttribute("minBufferTime", "PT2S");
mpd.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011"); mpd.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
mpd.SetAttribute("type", "static"); mpd.SetAttribute("type", "static");
XmlElement period = doc.CreateElement("Period"); XmlElement period = doc.CreateElement("Period");
XmlElement videoSet = doc.CreateElement("AdaptationSet"); XmlElement videoSet = doc.CreateElement("AdaptationSet");
XmlElement videoMeta = doc.CreateElement("ContentComponent"); XmlElement videoMeta = doc.CreateElement("ContentComponent");
videoMeta.SetAttribute("contentType", "video"); videoMeta.SetAttribute("contentType", "video");
videoMeta.SetAttribute("id", "1"); videoMeta.SetAttribute("id", "1");
videoSet.AppendChild(videoMeta); 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"); XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("bandwidth", GetBandwidth(requestedQuality.VideoQuality)); representation.SetAttribute("bandwidth", GetBandwidth(info.Label));
representation.SetAttribute("id", "1"); representation.SetAttribute("id", info.Itag);
representation.SetAttribute("mimeType", $"video/{requestedQuality.Container.GetFileExtension()}"); 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"); XmlElement baseUrl = doc.CreateElement("BaseURL");
baseUrl.InnerText = requestedQuality.Url; baseUrl.InnerText = info.Url;
representation.AppendChild(baseUrl); representation.AppendChild(baseUrl);
XmlElement segmentBase = doc.CreateElement("SegmentBase"); XmlElement segmentBase = doc.CreateElement("SegmentBase");
segmentBase.SetAttribute("indexRange", streamInfo.Video.IndexRange); segmentBase.SetAttribute("indexRange", info.IndexRange);
representation.AppendChild(segmentBase); representation.AppendChild(segmentBase);
XmlElement initialization = doc.CreateElement("Initialization"); XmlElement initialization = doc.CreateElement("Initialization");
initialization.SetAttribute("range", streamInfo.Video.InitRange); initialization.SetAttribute("range", info.InitRange);
segmentBase.AppendChild(initialization); segmentBase.AppendChild(initialization);
videoSet.AppendChild(representation); return representation;
}
XmlElement audioSet = doc.CreateElement("AdaptationSet");
XmlElement audioMeta = doc.CreateElement("ContentComponent");
audioMeta.SetAttribute("contentType", "audio");
audioMeta.SetAttribute("id", "2");
audioSet.AppendChild(audioMeta);
private static XmlElement GetAudioPresentation(XmlDocument doc, StreamInfo.AudioInfo info)
{
XmlElement audio = doc.CreateElement("Representation"); XmlElement audio = doc.CreateElement("Representation");
audio.SetAttribute("bandwidth", "200000"); audio.SetAttribute("bandwidth", "200000");
audio.SetAttribute("id", "2"); audio.SetAttribute("id", info.Itag);
audio.SetAttribute("sampleRate", streamInfo.Audio.SampleRate); audio.SetAttribute("sampleRate", info.SampleRate);
audio.SetAttribute("numChannels", streamInfo.Audio.ChannelsCount); audio.SetAttribute("numChannels", info.ChannelsCount);
audio.SetAttribute("codecs", list.Audio.First(i => i.Container.GetFileExtension() == "webm").AudioEncoding.ToString()); audio.SetAttribute("codecs", info.Codecs);
audio.SetAttribute("mimeType", $"audio/{list.Audio.First(i => i.Container.GetFileExtension() == "webm").Container.GetFileExtension()}"); audio.SetAttribute("mimeType", info.MimeType);
audioSet.AppendChild(audio);
XmlElement audioUrl = doc.CreateElement("BaseURL"); XmlElement audioUrl = doc.CreateElement("BaseURL");
audioUrl.InnerText = list.Audio.First(i => i.Container.GetFileExtension() == "webm").Url; audioUrl.InnerText = info.Url;
audio.AppendChild(audioUrl); audio.AppendChild(audioUrl);
XmlElement audioSegmentBase = doc.CreateElement("SegmentBase"); XmlElement audioSegmentBase = doc.CreateElement("SegmentBase");
audioSegmentBase.SetAttribute("indexRange", streamInfo.Audio.IndexRange); audioSegmentBase.SetAttribute("indexRange", info.IndexRange);
audioSegmentBase.SetAttribute("indexRangeExact", "true");
audio.AppendChild(audioSegmentBase); audio.AppendChild(audioSegmentBase);
XmlElement audioInit = doc.CreateElement("Initialization"); XmlElement audioInit = doc.CreateElement("Initialization");
audioInit.SetAttribute("range", streamInfo.Audio.InitRange); audioInit.SetAttribute("range", info.InitRange);
audioSegmentBase.AppendChild(audioInit); audioSegmentBase.AppendChild(audioInit);
doc.AppendChild(mpd); return audio;
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();
} }
private static async Task<StreamInfo> GetInfoAsync(Video info, VideoStreamInfo requestedQuality) private static async Task<StreamInfo> GetInfoAsync(Video info, VideoStreamInfo requestedQuality)
{ {
try try
{ {
StreamInfo si = new StreamInfo();
HttpClient http = new HttpClient(); 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); IHtmlDocument videoEmbedPageHtml = new HtmlParser().Parse(response);
string playerConfigRaw = Regex.Match(videoEmbedPageHtml.Source.Text, string playerConfigRaw = Regex.Match(videoEmbedPageHtml.Source.Text,
@"yt\.setConfig\({'PLAYER_CONFIG': (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})") @"ytplayer\.config = (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})")
.Groups["Json"].Value; .Groups["Json"].Value;
JToken playerConfigJson = JToken.Parse(playerConfigRaw); JToken playerConfigJson = JToken.Parse(playerConfigRaw);
string sts = playerConfigJson.SelectToken("sts").Value<string>();
string eurl = WebUtility.UrlEncode($"https://youtube.googleapis.com/v/{info.Id}"); var playerResponseRaw = playerConfigJson.SelectToken("args.player_response").Value<string>();
string url = $"https://youtube.com/get_video_info?video_id={info.Id}&el=embedded&sts={sts}&eurl={eurl}&hl=en"; JToken playerResponseJson = JToken.Parse(playerResponseRaw);
string raw = await http.GetStringAsync(url); string errorReason = playerResponseJson.SelectToken("playabilityStatus.reason")?.Value<string>();
if (!string.IsNullOrWhiteSpace(errorReason))
throw new InvalidDataException($"Video [{info.Id}] is unplayable. Reason: {errorReason}");
Dictionary<string, string> videoInfoDic = SplitQuery(raw); List<Dictionary<string, string>> adaptiveStreamInfosUrl = playerConfigJson.SelectToken("args.adaptive_fmts")?.Value<string>().Split(',').Select(SplitQuery).ToList();
List<Dictionary<string, string>> video =
requestedQuality == null ?
adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("quality_label")) :
adaptiveStreamInfosUrl.FindAll(i => i.ContainsValue(requestedQuality.VideoQualityLabel));
List<Dictionary<string, string>> 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<Dictionary<string, string>> adaptiveStreamInfosUrl = videoInfoDic.GetValueOrDefault("adaptive_fmts").Split(',').Select(SplitQuery).ToList(); foreach (var i in audio)
Dictionary<string, string> video = adaptiveStreamInfosUrl.Find(i => i["quality_label"] == requestedQuality.VideoQualityLabel && i["type"].Contains(requestedQuality.Container.GetFileExtension())); si.Audio.Add(new StreamInfo.AudioInfo
Dictionary<string, string> audio = adaptiveStreamInfosUrl.Find(i => i.ContainsKey("audio_sample_rate") && i["type"].Contains("webm")); {
ChannelsCount = i["audio_channels"],
si.Video.IndexRange = video["index"]; IndexRange = i["index"],
si.Audio.ChannelsCount = audio["audio_channels"]; SampleRate = i["audio_sample_rate"],
si.Audio.IndexRange = audio["index"]; Codecs = i["type"].Split('"')[1],
si.Audio.SampleRate = audio["audio_sample_rate"]; MimeType = i["type"].Split(';')[0],
Url = i["url"],
Itag = i["itag"]
});
return si; return si;
} }
catch catch
{ {
return null; return new StreamInfo();
} }
} }
@@ -173,57 +218,32 @@ namespace FoxTube.Controls.Player
return dic; return dic;
} }
private static void AppendVideoSet(XmlDocument doc, XmlElement root, List<VideoStreamInfo> list) private static string GetBandwidth(string quality)
{
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)
{ {
string parsed = quality.Split('p')[0];
switch (quality) switch (quality)
{ {
case VideoQuality.High4320: case "4320":
return $"16763040"; return $"16763040";
case VideoQuality.High3072: case "3072":
return $"11920384"; return $"11920384";
case VideoQuality.High2880: case "2880":
return $"11175360"; return $"11175360";
case VideoQuality.High2160: case "2160":
return $"8381520"; return $"8381520";
case VideoQuality.High1440: case "1440":
return $"5587680"; return $"5587680";
case VideoQuality.High1080: case "1080":
return $"4190760"; return $"4190760";
case VideoQuality.High720: case "720":
return $"2073921"; return $"2073921";
case VideoQuality.Medium480: case "480":
return $"869460"; return $"869460";
case VideoQuality.Medium360: case "360":
return $"686521"; return $"686521";
case VideoQuality.Low240: case "240":
return $"264835"; return $"264835";
case VideoQuality.Low144: case "144":
default: default:
return $"100000"; return $"100000";
} }
@@ -263,7 +283,7 @@ namespace FoxTube.Controls.Player
list.Add(new StreamQuality list.Add(new StreamQuality
{ {
Resolution = "Auto", Resolution = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto"),
Url = url.ToUri() Url = url.ToUri()
}); });
list.Reverse(); list.Reverse();
@@ -285,6 +305,14 @@ namespace FoxTube.Controls.Player
{ {
public string IndexRange { get; set; } public string IndexRange { get; set; }
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}"; 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 public class AudioInfo
{ {
@@ -292,10 +320,14 @@ namespace FoxTube.Controls.Player
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}"; public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
public string SampleRate { get; set; } public string SampleRate { get; set; }
public string ChannelsCount { 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 List<VideoInfo> Video { get; } = new List<VideoInfo>();
public AudioInfo Audio { get; } = new AudioInfo(); public List<AudioInfo> Audio { get; } = new List<AudioInfo>();
} }
public class StreamQuality public class StreamQuality
+6 -1
View File
@@ -205,7 +205,7 @@ namespace FoxTube
object info = (quality.SelectedItem as ComboBoxItem).Tag; object info = (quality.SelectedItem as ComboBoxItem).Tag;
if (info is MuxedStreamInfo) if (info is MuxedStreamInfo)
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as MuxedStreamInfo).Url.ToUri())); 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))); Player.SetPlaybackSource(MediaSource.CreateFromUri(await ManifestGenerator.GetManifest(Meta, info as VideoStreamInfo, MediaStreams)));
else if (info is StreamQuality) else if (info is StreamQuality)
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as StreamQuality).Url)); Player.SetPlaybackSource(MediaSource.CreateFromUri((info as StreamQuality).Url));
@@ -437,6 +437,11 @@ namespace FoxTube
qualityList.Sort(new QualityComparer()); qualityList.Sort(new QualityComparer());
qualityList.Reverse(); qualityList.Reverse();
quality.Items.Add(new ComboBoxItem
{
Content = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto")
});
foreach (string i in qualityList) foreach (string i in qualityList)
{ {
object tag; object tag;
+1
View File
@@ -29,6 +29,7 @@
<TextBlock x:Uid="/General/playback" Text="Playback" FontSize="22" Margin="0,10,0,0"/> <TextBlock x:Uid="/General/playback" Text="Playback" FontSize="22" Margin="0,10,0,0"/>
<ComboBox x:Uid="/General/quality" Width="250" Header="Default video playback quality" Name="quality" SelectionChanged="quality_SelectionChanged"> <ComboBox x:Uid="/General/quality" Width="250" Header="Default video playback quality" Name="quality" SelectionChanged="quality_SelectionChanged">
<ComboBoxItem Tag="remember" x:Uid="/General/remember" Content="Remember my choice"/> <ComboBoxItem Tag="remember" x:Uid="/General/remember" Content="Remember my choice"/>
<ComboBoxItem Tag="auto" x:Uid="/General/auto" Content="Auto"/>
</ComboBox> </ComboBox>
<ToggleSwitch x:Uid="/General/metered" OnContent="Notify when playing on metered connection" OffContent="Notify when playing on metered connection" Name="mobileWarning" Toggled="mobileWarning_Toggled"/> <ToggleSwitch x:Uid="/General/metered" OnContent="Notify when playing on metered connection" OffContent="Notify when playing on metered connection" Name="mobileWarning" Toggled="mobileWarning_Toggled"/>
<ToggleSwitch x:Uid="/General/autoplay" OnContent="Play videos automatically" OffContent="Play videos automatically" Name="autoplay" Toggled="autoplay_Toggled"/> <ToggleSwitch x:Uid="/General/autoplay" OnContent="Play videos automatically" OffContent="Play videos automatically" Name="autoplay" Toggled="autoplay_Toggled"/>
+3
View File
@@ -207,4 +207,7 @@
<data name="relevanceLanguage.Header" xml:space="preserve"> <data name="relevanceLanguage.Header" xml:space="preserve">
<value>Search relevance language</value> <value>Search relevance language</value>
</data> </data>
<data name="auto.Content" xml:space="preserve">
<value>Auto</value>
</data>
</root> </root>
+3
View File
@@ -300,4 +300,7 @@
<data name="continue" xml:space="preserve"> <data name="continue" xml:space="preserve">
<value>Continue from</value> <value>Continue from</value>
</data> </data>
<data name="auto" xml:space="preserve">
<value>Auto</value>
</data>
</root> </root>
+3
View File
@@ -207,4 +207,7 @@
<data name="relevanceLanguage.Header" xml:space="preserve"> <data name="relevanceLanguage.Header" xml:space="preserve">
<value>Предпочитаемый язык поиска</value> <value>Предпочитаемый язык поиска</value>
</data> </data>
<data name="auto.Content" xml:space="preserve">
<value>Авто</value>
</data>
</root> </root>
+3
View File
@@ -300,4 +300,7 @@
<data name="continue" xml:space="preserve"> <data name="continue" xml:space="preserve">
<value>Продолжить с</value> <value>Продолжить с</value>
</data> </data>
<data name="auto" xml:space="preserve">
<value>Авто</value>
</data>
</root> </root>