/// <inheritdoc /> public async Task <MediaStreamInfoSet> GetVideoMediaStreamInfosAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException($"Invalid YouTube video ID [{videoId}].", nameof(videoId)); } // Get player configuration var playerConfiguration = await GetPlayerConfigurationAsync(videoId).ConfigureAwait(false); // Prepare stream info maps var muxedStreamInfoMap = new Dictionary <int, MuxedStreamInfo>(); var audioStreamInfoMap = new Dictionary <int, AudioStreamInfo>(); var videoStreamInfoMap = new Dictionary <int, VideoStreamInfo>(); // Get muxed stream infos var muxedStreamInfoDics = playerConfiguration.MuxedStreamInfosUrlEncoded.EmptyIfNull().Split(",").Select(Url.SplitQuery); foreach (var streamInfoDic in muxedStreamInfoDics) { // Extract info var itag = streamInfoDic["itag"].ParseInt(); var url = streamInfoDic["url"]; // Decipher signature if needed var signature = streamInfoDic.GetValueOrDefault("s"); if (!signature.IsNullOrWhiteSpace()) { // Get cipher operations (cached) var cipherOperations = await GetCipherOperationsAsync(playerConfiguration.PlayerSourceUrl).ConfigureAwait(false); // Decipher signature signature = cipherOperations.Decipher(signature); // Set the corresponding parameter in the URL var signatureParameter = streamInfoDic.GetValueOrDefault("sp") ?? "signature"; url = Url.SetQueryParameter(url, signatureParameter, signature); } // Try to extract content length, otherwise get it manually var contentLength = Regex.Match(url, @"clen=(\d+)").Groups[1].Value.ParseLongOrDefault(); if (contentLength <= 0) { // Send HEAD request and get content length contentLength = await _httpClient.GetContentLengthAsync(url, false).ConfigureAwait(false) ?? 0; // If content length is still not available - stream is gone or faulty if (contentLength <= 0) { continue; } } // Extract container var containerRaw = streamInfoDic["type"].SubstringUntil(";").SubstringAfter("/"); var container = Heuristics.ContainerFromString(containerRaw); // Extract audio encoding var audioEncodingRaw = streamInfoDic["type"].SubstringAfter("codecs=\"").SubstringUntil("\"").Split(", ").Last(); var audioEncoding = Heuristics.AudioEncodingFromString(audioEncodingRaw); // Extract video encoding var videoEncodingRaw = streamInfoDic["type"].SubstringAfter("codecs=\"").SubstringUntil("\"").Split(", ").First(); var videoEncoding = Heuristics.VideoEncodingFromString(videoEncodingRaw); // Determine video quality from itag var videoQuality = Heuristics.VideoQualityFromItag(itag); // Determine video quality label from video quality var videoQualityLabel = Heuristics.VideoQualityToLabel(videoQuality); // Determine video resolution from video quality var resolution = Heuristics.VideoQualityToResolution(videoQuality); // Add to list muxedStreamInfoMap[itag] = new MuxedStreamInfo(itag, url, container, contentLength, audioEncoding, videoEncoding, videoQualityLabel, videoQuality, resolution); } // Get adaptive stream infos var adaptiveStreamInfoDics = playerConfiguration.AdaptiveStreamInfosUrlEncoded.EmptyIfNull().Split(",").Select(Url.SplitQuery); foreach (var streamInfoDic in adaptiveStreamInfoDics) { // Extract info var itag = streamInfoDic["itag"].ParseInt(); var url = streamInfoDic["url"]; var bitrate = streamInfoDic["bitrate"].ParseLong(); // Decipher signature if needed var signature = streamInfoDic.GetValueOrDefault("s"); if (!signature.IsNullOrWhiteSpace()) { // Get cipher operations (cached) var cipherOperations = await GetCipherOperationsAsync(playerConfiguration.PlayerSourceUrl).ConfigureAwait(false); // Decipher signature signature = cipherOperations.Decipher(signature); // Set the corresponding parameter in the URL var signatureParameter = streamInfoDic.GetValueOrDefault("sp") ?? "signature"; url = Url.SetQueryParameter(url, signatureParameter, signature); } // Try to extract content length, otherwise get it manually var contentLength = streamInfoDic.GetValueOrDefault("clen").ParseLongOrDefault(); if (contentLength <= 0) { // Send HEAD request and get content length contentLength = await _httpClient.GetContentLengthAsync(url, false).ConfigureAwait(false) ?? 0; // If content length is still not available - stream is gone or faulty if (contentLength <= 0) { continue; } } // Extract container var containerRaw = streamInfoDic["type"].SubstringUntil(";").SubstringAfter("/"); var container = Heuristics.ContainerFromString(containerRaw); // If audio-only if (streamInfoDic["type"].StartsWith("audio/", StringComparison.OrdinalIgnoreCase)) { // Extract audio encoding var audioEncodingRaw = streamInfoDic["type"].SubstringAfter("codecs=\"").SubstringUntil("\""); var audioEncoding = Heuristics.AudioEncodingFromString(audioEncodingRaw); // Add stream audioStreamInfoMap[itag] = new AudioStreamInfo(itag, url, container, contentLength, bitrate, audioEncoding); } // If video-only else { // Extract video encoding var videoEncodingRaw = streamInfoDic["type"].SubstringAfter("codecs=\"").SubstringUntil("\""); var videoEncoding = !videoEncodingRaw.Equals("unknown", StringComparison.OrdinalIgnoreCase) ? Heuristics.VideoEncodingFromString(videoEncodingRaw) : VideoEncoding.Av1; // HACK: issue 246 // Extract video quality label and video quality var videoQualityLabel = streamInfoDic["quality_label"]; var videoQuality = Heuristics.VideoQualityFromLabel(videoQualityLabel); // Extract resolution var width = streamInfoDic["size"].SubstringUntil("x").ParseInt(); var height = streamInfoDic["size"].SubstringAfter("x").ParseInt(); var resolution = new VideoResolution(width, height); // Extract framerate var framerate = streamInfoDic["fps"].ParseInt(); // Add to list videoStreamInfoMap[itag] = new VideoStreamInfo(itag, url, container, contentLength, bitrate, videoEncoding, videoQualityLabel, videoQuality, resolution, framerate); } } // Get dash manifest var dashManifestUrl = playerConfiguration.DashManifestUrl; if (!dashManifestUrl.IsNullOrWhiteSpace()) { // Extract signature var signature = Regex.Match(dashManifestUrl, "/s/(.*?)(?:/|$)").Groups[1].Value; // Decipher signature if needed if (!signature.IsNullOrWhiteSpace()) { // Get cipher operations (cached) var cipherOperations = await GetCipherOperationsAsync(playerConfiguration.PlayerSourceUrl).ConfigureAwait(false); // Decipher signature signature = cipherOperations.Decipher(signature); // Set the corresponding parameter in the URL dashManifestUrl = Url.SetRouteParameter(dashManifestUrl, "signature", signature); } // Get DASH manifest XML var dashManifestXml = await GetDashManifestXmlAsync(dashManifestUrl).ConfigureAwait(false); // Get representation nodes (skip partial streams) var streamInfoXmls = dashManifestXml.Descendants("Representation").Where(s => s.Descendants("Initialization").FirstOrDefault()?.Attribute("sourceURL")?.Value.Contains("sq/") != true); // Get DASH stream infos foreach (var streamInfoXml in streamInfoXmls) { // Extract info var itag = (int)streamInfoXml.Attribute("id"); var url = (string)streamInfoXml.Element("BaseURL"); var contentLength = Regex.Match(url, @"clen[/=](\d+)").Groups[1].Value.ParseLong(); var bitrate = (long)streamInfoXml.Attribute("bandwidth"); // Extract container var containerRaw = Regex.Match(url, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value.UrlDecode(); var container = Heuristics.ContainerFromString(containerRaw); // If audio-only if (streamInfoXml.Element("AudioChannelConfiguration") != null) { // Extract audio encoding var audioEncodingRaw = (string)streamInfoXml.Attribute("codecs"); var audioEncoding = Heuristics.AudioEncodingFromString(audioEncodingRaw); // Add to list audioStreamInfoMap[itag] = new AudioStreamInfo(itag, url, container, contentLength, bitrate, audioEncoding); } // If video-only else { // Extract video encoding var videoEncodingRaw = (string)streamInfoXml.Attribute("codecs"); var videoEncoding = Heuristics.VideoEncodingFromString(videoEncodingRaw); // Extract resolution var width = (int)streamInfoXml.Attribute("width"); var height = (int)streamInfoXml.Attribute("height"); var resolution = new VideoResolution(width, height); // Extract framerate var framerate = (int)streamInfoXml.Attribute("frameRate"); // Determine video quality from itag var videoQuality = Heuristics.VideoQualityFromItag(itag); // Determine video quality label from video quality and framerate var videoQualityLabel = Heuristics.VideoQualityToLabel(videoQuality, framerate); // Add to list videoStreamInfoMap[itag] = new VideoStreamInfo(itag, url, container, contentLength, bitrate, videoEncoding, videoQualityLabel, videoQuality, resolution, framerate); } } } // Finalize stream info collections var muxedStreamInfos = muxedStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); var audioStreamInfos = audioStreamInfoMap.Values.OrderByDescending(s => s.Bitrate).ToArray(); var videoStreamInfos = videoStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); return(new MediaStreamInfoSet(muxedStreamInfos, audioStreamInfos, videoStreamInfos, playerConfiguration.HlsManifestUrl, playerConfiguration.ValidUntil)); }