From 360a330dee3ca6f9668e6e0239c91a756d2c5721 Mon Sep 17 00:00:00 2001 From: Mattia Date: Sun, 25 Jul 2021 14:47:26 +0200 Subject: [PATCH] Implement Embedded client. Fix #147 --- .../closed_caption_client.dart | 0 .../comments_client.dart | 0 .../clients/embedded_player_client.dart | 135 ++++++++++++++++++ .../video_info_client.dart | 0 .../youtube_http_client.dart | 14 ++ .../closed_caption_client.dart | 2 +- lib/src/videos/comments/comments_client.dart | 2 +- lib/src/videos/comments/comments_list.dart | 3 +- lib/src/videos/streams/streams_client.dart | 17 ++- 9 files changed, 168 insertions(+), 5 deletions(-) rename lib/src/reverse_engineering/{responses => clients}/closed_caption_client.dart (100%) rename lib/src/reverse_engineering/{responses => clients}/comments_client.dart (100%) create mode 100644 lib/src/reverse_engineering/clients/embedded_player_client.dart rename lib/src/reverse_engineering/{responses => clients}/video_info_client.dart (100%) diff --git a/lib/src/reverse_engineering/responses/closed_caption_client.dart b/lib/src/reverse_engineering/clients/closed_caption_client.dart similarity index 100% rename from lib/src/reverse_engineering/responses/closed_caption_client.dart rename to lib/src/reverse_engineering/clients/closed_caption_client.dart diff --git a/lib/src/reverse_engineering/responses/comments_client.dart b/lib/src/reverse_engineering/clients/comments_client.dart similarity index 100% rename from lib/src/reverse_engineering/responses/comments_client.dart rename to lib/src/reverse_engineering/clients/comments_client.dart diff --git a/lib/src/reverse_engineering/clients/embedded_player_client.dart b/lib/src/reverse_engineering/clients/embedded_player_client.dart new file mode 100644 index 0000000..8c9055c --- /dev/null +++ b/lib/src/reverse_engineering/clients/embedded_player_client.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:http_parser/http_parser.dart'; + +import '../../../youtube_explode_dart.dart'; +import '../../exceptions/exceptions.dart'; +import '../../extensions/helpers_extension.dart'; +import '../../retry.dart'; +import '../models/stream_info_provider.dart'; +import '../youtube_http_client.dart'; + +/// +class EmbeddedPlayerClient { + final JsonMap root; + + /// + late final String status = root['playabilityStatus']!['status']!; + + late final String reason = root['playabilityStatus']!['reason'] ?? ''; + + /// + late final bool isVideoAvailable = status.toLowerCase() == 'ok'; + + /// + late final Iterable<_StreamInfo> muxedStreams = root + .get('streamingData') + ?.getList('formats') + ?.map((e) => _StreamInfo(e)) ?? + const []; + + /// + late final Iterable<_StreamInfo> adaptiveStreams = root + .get('streamingData') + ?.getList('adaptiveFormats') + ?.map((e) => _StreamInfo(e)) ?? + const []; + + /// + late final Iterable<_StreamInfo> streams = [ + ...muxedStreams, + ...adaptiveStreams + ]; + + /// + EmbeddedPlayerClient(this.root); + + /// + EmbeddedPlayerClient.parse(String raw) : root = json.decode(raw); + + /// + @alwaysThrows + static Future get( + YoutubeHttpClient httpClient, String videoId) { + final body = { + 'context': const { + 'client': { + 'hl': 'en', + 'clientName': 'ANDROID_EMBEDDED_PLAYER', + 'clientVersion': '16.05' + } + }, + 'videoId': videoId + }; + + final url = Uri.parse('https://www.youtube.com/youtubei/v1/player'); + + return retry(() async { + final raw = await httpClient.post(url, + body: json.encode(body), + headers: { + 'X-Goog-Api-Key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' + }, + validate: true); + + var result = EmbeddedPlayerClient.parse(raw.body); + + if (!result.isVideoAvailable) { + throw VideoUnplayableException.unplayable(VideoId(videoId), + reason: result.reason); + } + return result; + }); + } +} + +class _StreamInfo extends StreamInfoProvider { + final JsonMap root; + + @override + late final int tag = root['itag']!; + + @override + late final String url = root['url']!; + + @override + late final int? contentLength = int.tryParse(root['contentLength'] ?? + StreamInfoProvider.contentLenExp.firstMatch(url)?.group(1) ?? + ''); + + @override + late final int bitrate = root['bitrate']!; + + late final MediaType mimeType = MediaType.parse(root['mimeType']!); + + @override + late final String container = mimeType.subtype; + + late final List codecs = mimeType.parameters['codecs']! + .split(',') + .map((e) => e.trim()) + .toList() + .cast(); + + @override + late final String audioCodec = codecs.last; + + @override + late final String? videoCodec = isAudioOnly ? null : codecs.first; + + late final bool isAudioOnly = mimeType.type == 'audio'; + + @override + late final String? videoQualityLabel = root['quality_label']; + + @override + late final int? videoWidth = root['width']; + + @override + late final int? videoHeight = root['height']; + + @override + late final int? framerate = root['fps'] ?? 0; + _StreamInfo(this.root); +} diff --git a/lib/src/reverse_engineering/responses/video_info_client.dart b/lib/src/reverse_engineering/clients/video_info_client.dart similarity index 100% rename from lib/src/reverse_engineering/responses/video_info_client.dart rename to lib/src/reverse_engineering/clients/video_info_client.dart diff --git a/lib/src/reverse_engineering/youtube_http_client.dart b/lib/src/reverse_engineering/youtube_http_client.dart index 1da973a..242baec 100644 --- a/lib/src/reverse_engineering/youtube_http_client.dart +++ b/lib/src/reverse_engineering/youtube_http_client.dart @@ -78,6 +78,20 @@ class YoutubeHttpClient extends http.BaseClient { return response; } + @override + Future post(Uri url, + {Map? headers, + Object? body, + Encoding? encoding, + bool validate = false}) async { + final response = + await super.post(url, headers: headers, body: body, encoding: encoding); + if (validate) { + _validateResponse(response, response.statusCode); + } + return response; + } + /// Future postString(dynamic url, {Map? body, diff --git a/lib/src/videos/closed_captions/closed_caption_client.dart b/lib/src/videos/closed_captions/closed_caption_client.dart index b50f19a..3a4efa5 100644 --- a/lib/src/videos/closed_captions/closed_caption_client.dart +++ b/lib/src/videos/closed_captions/closed_caption_client.dart @@ -1,7 +1,7 @@ import 'package:youtube_explode_dart/src/reverse_engineering/pages/watch_page.dart'; import '../../extensions/helpers_extension.dart'; -import '../../reverse_engineering/responses/closed_caption_client.dart' as re +import '../../reverse_engineering/clients/closed_caption_client.dart' as re show ClosedCaptionClient; import '../../reverse_engineering/youtube_http_client.dart'; import '../videos.dart'; diff --git a/lib/src/videos/comments/comments_client.dart b/lib/src/videos/comments/comments_client.dart index a23c1b4..f06f714 100644 --- a/lib/src/videos/comments/comments_client.dart +++ b/lib/src/videos/comments/comments_client.dart @@ -1,5 +1,5 @@ import '../../channels/channel_id.dart'; -import '../../reverse_engineering/responses/comments_client.dart' as re; +import '../../reverse_engineering/clients/comments_client.dart' as re; import '../../reverse_engineering/youtube_http_client.dart'; import '../videos.dart'; import 'comment.dart'; diff --git a/lib/src/videos/comments/comments_list.dart b/lib/src/videos/comments/comments_list.dart index 69b724f..fbd8889 100644 --- a/lib/src/videos/comments/comments_list.dart +++ b/lib/src/videos/comments/comments_list.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:youtube_explode_dart/src/reverse_engineering/responses/comments_client.dart' - as re; +import '../../reverse_engineering/clients/comments_client.dart' as re; import '../../../youtube_explode_dart.dart'; diff --git a/lib/src/videos/streams/streams_client.dart b/lib/src/videos/streams/streams_client.dart index a877e12..eb17f23 100644 --- a/lib/src/videos/streams/streams_client.dart +++ b/lib/src/videos/streams/streams_client.dart @@ -1,6 +1,7 @@ import '../../exceptions/exceptions.dart'; import '../../extensions/helpers_extension.dart'; import '../../reverse_engineering/cipher/cipher_operations.dart'; +import '../../reverse_engineering/clients/embedded_player_client.dart'; import '../../reverse_engineering/dash_manifest.dart'; import '../../reverse_engineering/heuristics.dart'; import '../../reverse_engineering/models/stream_info_provider.dart'; @@ -80,6 +81,13 @@ class StreamsClient { return StreamContext(streamInfoProviders, cipherOperations); }*/ + Future _getStreamContextFromEmbeddedClient( + VideoId videoId) async { + final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value); + + return StreamContext(page.streams.toList(), const []); + } + Future _getStreamContextFromWatchPage(VideoId videoId) async { final watchPage = await WatchPage.get(_httpClient, videoId.toString()); @@ -224,7 +232,14 @@ class StreamsClient { Future getManifest(dynamic videoId) async { videoId = VideoId.fromString(videoId); - var context = await _getStreamContextFromWatchPage(videoId); + try { + final context = await _getStreamContextFromEmbeddedClient(videoId); + return _getManifest(context); + } on YoutubeExplodeException { + //TODO: ignore + } + + final context = await _getStreamContextFromWatchPage(videoId); return _getManifest(context); }