@@ -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 < StreamInfo > 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': (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})" )
@"ytplayer\.config = (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})" )
. Groups [ "Json" ] . Value ;
JToken playerConfigJson = JToken . Parse ( playerConfigRaw ) ;
string sts = playerConfigJson . SelectToken ( "sts" ) . Value < string > ( ) ;
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 < string > ( ) ;
JToken playerResponseJson = JToken . Parse ( playerResponseRaw ) ;
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 ( ) ;
Dictionary < string , string > video = adaptiveStreamInfosUrl . Find ( i = > i [ "quality_label" ] = = requestedQuality . VideoQualityLabel & & i [ "type" ] . Contains ( requestedQuality . Container . GetFileExtension ( ) ) ) ;
Dictionary < string , string > 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 < VideoStreamInfo > 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 = "A uto",
Resolution = ResourceLoader . GetForCurrentView ( "VideoPage" ) . GetString ( "/VideoPage/a uto") ,
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 < VideoInfo > Video { get ; } = new List < VideoInfo > ( ) ;
public List < AudioInfo > Audio { get ; } = new List < AudioInfo > ( ) ;
}
public class StreamQuality