import '../../exceptions/exceptions.dart'; import '../../extensions/helpers_extension.dart'; import '../../reverse_engineering/cipher/cipher_operations.dart'; import '../../reverse_engineering/heuristics.dart'; import '../../reverse_engineering/responses/responses.dart'; import '../../reverse_engineering/youtube_http_client.dart'; import '../video_id.dart'; import 'bitrate.dart'; import 'filesize.dart'; import 'framerate.dart'; import 'stream_container.dart'; import 'stream_context.dart'; import 'stream_info.dart'; import 'stream_manifest.dart'; import 'streams.dart'; /// Queries related to media streams of YouTube videos. class StreamsClient { final YoutubeHttpClient _httpClient; /// Initializes an instance of [StreamsClient] StreamsClient(this._httpClient); Future _getDashManifest( Uri dashManifestUrl, Iterable cipherOperations) { var signature = DashManifest.getSignatureFromUrl(dashManifestUrl.toString()); if (!signature.isNullOrWhiteSpace) { signature = cipherOperations.decipher(signature!); dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature); } return DashManifest.get(_httpClient, dashManifestUrl); } Future _getStreamContextFromVideoInfo(VideoId videoId) async { var embedPage = await EmbedPage.get(_httpClient, videoId.toString()); var playerConfig = embedPage.playerConfig; if (playerConfig == null) { throw VideoUnplayableException.unplayable(videoId); } var playerSource = await PlayerSource.get( _httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl); var cipherOperations = playerSource.getCipherOperations(); var videoInfoResponse = await VideoInfoResponse.get( _httpClient, videoId.toString(), playerSource.sts); var playerResponse = videoInfoResponse.playerResponse; var previewVideoId = playerResponse.previewVideoId; if (!previewVideoId.isNullOrWhiteSpace) { throw VideoRequiresPurchaseException.preview( videoId, VideoId(previewVideoId!)); } if (!playerResponse.isVideoPlayable) { throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } if (playerResponse.isLive) { throw VideoUnplayableException.liveStream(videoId); } var streamInfoProviders = [ ...videoInfoResponse.streams, ...playerResponse.streams ]; var dashManifestUrl = playerResponse.dashManifestUrl; if (!dashManifestUrl.isNullOrWhiteSpace) { var dashManifest = await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); streamInfoProviders.addAll(dashManifest.streams); } return StreamContext(streamInfoProviders, cipherOperations); } Future _getStreamContextFromWatchPage(VideoId videoId) async { var watchPage = await WatchPage.get(_httpClient, videoId.toString()); WatchPlayerConfig? playerConfig; try { playerConfig = watchPage.playerConfig; } on FormatException { playerConfig = null; } var playerResponse = playerConfig?.playerResponse ?? watchPage.playerResponse; if (playerResponse == null) { throw VideoUnplayableException.unplayable(videoId); } var previewVideoId = playerResponse.previewVideoId; if (!previewVideoId.isNullOrWhiteSpace) { throw VideoRequiresPurchaseException.preview( videoId, VideoId(previewVideoId!)); } var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl; var playerSource = !playerSourceUrl.isNullOrWhiteSpace ? await PlayerSource.get(_httpClient, playerSourceUrl!) : null; var cipherOperations = playerSource?.getCipherOperations() ?? const []; if (!playerResponse.isVideoPlayable) { throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } if (playerResponse.isLive) { throw VideoUnplayableException.liveStream(videoId); } var streamInfoProviders = [ ...playerResponse.streams, ]; var dashManifestUrl = playerResponse.dashManifestUrl; if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) { var dashManifest = await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); streamInfoProviders.addAll(dashManifest.streams); } return StreamContext(streamInfoProviders, cipherOperations); } Future _getManifest(StreamContext streamContext) async { // To make sure there are no duplicates streams, group them by tag var streams = {}; for (final streamInfo in streamContext.streamInfoProviders) { var tag = streamInfo.tag; var url = Uri.parse(streamInfo.url); // Signature var signature = streamInfo.signature; var signatureParameter = streamInfo.signatureParameter ?? 'signature'; if (!signature.isNullOrWhiteSpace) { signature = streamContext.cipherOperations.decipher(signature!); url = url.setQueryParam(signatureParameter, signature); } // Content length var contentLength = streamInfo.contentLength ?? await _httpClient.getContentLength(url, validate: false) ?? 0; if (contentLength <= 0) { continue; } // Common var container = StreamContainer.parse(streamInfo.container!); var fileSize = FileSize(contentLength); var bitrate = Bitrate(streamInfo.bitrate!); var audioCodec = streamInfo.audioCodec; var videoCodec = streamInfo.videoCodec; // Muxed or Video-only if (!videoCodec.isNullOrWhiteSpace) { var framerate = Framerate(streamInfo.framerate ?? 24); var videoQualityLabel = streamInfo.videoQualityLabel ?? VideoQualityUtil.getLabelFromTagWithFramerate( tag, framerate.framesPerSecond.toDouble()); var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel); var videoWidth = streamInfo.videoWidth; var videoHeight = streamInfo.videoHeight; var videoResolution = videoWidth != -1 && videoHeight != -1 ? VideoResolution(videoWidth ?? 0, videoHeight ?? 0) : videoQuality.toVideoResolution(); // Muxed if (!audioCodec.isNullOrWhiteSpace) { streams[tag] = MuxedStreamInfo( tag, url, container, fileSize, bitrate, audioCodec!, videoCodec!, videoQualityLabel, videoQuality, videoResolution, framerate); continue; } // Video only streams[tag] = VideoOnlyStreamInfo( tag, url, container, fileSize, bitrate, videoCodec!, videoQualityLabel, videoQuality, videoResolution, framerate); continue; } // Audio-only if (!audioCodec.isNullOrWhiteSpace) { streams[tag] = AudioOnlyStreamInfo( tag, url, container, fileSize, bitrate, audioCodec!); } // #if DEBUG // throw FatalFailureException("Stream info doesn't contain audio/video codec information."); } return StreamManifest(streams.values); } /// Gets the manifest that contains information /// about available streams in the specified video. Future getManifest(dynamic videoId) async { videoId = VideoId.fromString(videoId); // We can try to extract the manifest from two sources: // get_video_info and the video watch page. // In some cases one works, in some cases another does. try { var context = await _getStreamContextFromVideoInfo(videoId); return _getManifest(context); } on YoutubeExplodeException { var context = await _getStreamContextFromWatchPage(videoId); return _getManifest(context); } } /// Gets the HTTP Live Stream (HLS) manifest URL /// for the specified video (if it's a live video stream). Future getHttpLiveStreamUrl(VideoId videoId) async { var videoInfoResponse = await VideoInfoResponse.get(_httpClient, videoId.toString()); var playerResponse = videoInfoResponse.playerResponse; if (!playerResponse.isVideoPlayable) { throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } var hlsManifest = playerResponse.hlsManifestUrl; if (hlsManifest == null) { throw VideoUnplayableException.notLiveStream(videoId); } return hlsManifest; } /// Gets the actual stream which is identified by the specified metadata. Stream> get(StreamInfo streamInfo) => _httpClient.getStream(streamInfo); }