/// <summary> /// Gets the manifest containing information about available closed caption tracks on the specified video. /// </summary> public async ValueTask <ClosedCaptionManifest> GetManifestAsync( VideoId videoId, CancellationToken cancellationToken = default) { var videoInfo = await _controller.GetVideoInfoAsync(videoId, cancellationToken); var playerResponse = videoInfo.TryGetPlayerResponse() ?? throw new YoutubeExplodeException("Could not extract player response."); var trackInfos = playerResponse .GetClosedCaptionTracks() .Select(t => { var url = t.TryGetUrl() ?? throw new YoutubeExplodeException("Could not extract track URL."); var languageCode = t.TryGetLanguageCode() ?? throw new YoutubeExplodeException("Could not extract track language code."); var languageName = t.TryGetLanguageName() ?? throw new YoutubeExplodeException("Could not extract track language name."); var isAutoGenerated = t.IsAutoGenerated(); return(new ClosedCaptionTrackInfo( url, new Language(languageCode, languageName), isAutoGenerated )); }) .ToArray(); return(new ClosedCaptionManifest(trackInfos)); }
private async ValueTask PopulateStreamInfosAsync( ICollection <IStreamInfo> streamInfos, VideoId videoId, CancellationToken cancellationToken = default) { var watchPage = await _controller.GetVideoWatchPageAsync(videoId, cancellationToken); // Try to get player source (failing is ok because there's a decent chance we won't need it) var playerSourceUrl = watchPage.TryGetPlayerSourceUrl(); var playerSource = !string.IsNullOrWhiteSpace(playerSourceUrl) ? await _controller.GetPlayerSourceAsync(playerSourceUrl, cancellationToken) : null; var signatureScrambler = playerSource?.TryGetSignatureScrambler() ?? SignatureScrambler.Null; var playerResponseFromWatchPage = watchPage.TryGetPlayerResponse(); if (playerResponseFromWatchPage is not null) { var purchasePreviewVideoId = playerResponseFromWatchPage.TryGetPreviewVideoId(); if (!string.IsNullOrWhiteSpace(purchasePreviewVideoId)) { throw new VideoRequiresPurchaseException( $"Video '{videoId}' requires purchase and cannot be played.", purchasePreviewVideoId ); } if (playerResponseFromWatchPage.IsVideoPlayable()) { // Extract streams from watch page await PopulateStreamInfosAsync( streamInfos, watchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from player response await PopulateStreamInfosAsync( streamInfos, playerResponseFromWatchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from DASH manifest var dashManifestUrlRaw = playerResponseFromWatchPage.TryGetDashManifestUrl(); if (!string.IsNullOrWhiteSpace(dashManifestUrlRaw)) { var dashManifestUrl = UnscrambleDashManifestUrl(signatureScrambler, dashManifestUrlRaw); var dashManifest = await _controller.GetDashManifestAsync(dashManifestUrl, cancellationToken); await PopulateStreamInfosAsync( streamInfos, dashManifest.GetStreams(), signatureScrambler, cancellationToken ); } } // If successfully retrieved streams, return if (streamInfos.Any()) { return; } } // Try to get streams from video info // Note: it seems YouTube has stopped using get_video_info and replaced it with an // internal API endpoint that resolves player response directly. // This may be an area for future improvement. var signatureTimestamp = playerSource?.TryGetSignatureTimestamp() ?? ""; var videoInfo = await _controller.GetVideoInfoAsync(videoId, signatureTimestamp, cancellationToken); var playerResponseFromVideoInfo = videoInfo.TryGetPlayerResponse(); if (playerResponseFromVideoInfo is not null) { if (playerResponseFromVideoInfo.IsVideoPlayable()) { // Extract streams from video info await PopulateStreamInfosAsync( streamInfos, videoInfo.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from player response await PopulateStreamInfosAsync( streamInfos, playerResponseFromVideoInfo.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from DASH manifest var dashManifestUrlRaw = playerResponseFromVideoInfo.TryGetDashManifestUrl(); if (!string.IsNullOrWhiteSpace(dashManifestUrlRaw)) { var dashManifestUrl = UnscrambleDashManifestUrl(signatureScrambler, dashManifestUrlRaw); var dashManifest = await _controller.GetDashManifestAsync(dashManifestUrl, cancellationToken); await PopulateStreamInfosAsync( streamInfos, dashManifest.GetStreams(), signatureScrambler, cancellationToken ); } // If successfully retrieved streams, return if (streamInfos.Any()) { return; } } else { var errorMessage = playerResponseFromVideoInfo.TryGetVideoPlayabilityError(); throw new VideoUnplayableException($"Video '{videoId}' is unplayable. Reason: {errorMessage}."); } } // Couldn't extract any streams throw new VideoUnplayableException($"Video '{videoId}' does not contain any playable streams."); }