Implement Embedded client. Fix #147

This commit is contained in:
Mattia 2021-07-25 14:47:26 +02:00
parent e8ac71ff44
commit 360a330dee
9 changed files with 168 additions and 5 deletions

View File

@ -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<EmbeddedPlayerClient> 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<String> codecs = mimeType.parameters['codecs']!
.split(',')
.map((e) => e.trim())
.toList()
.cast<String>();
@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);
}

View File

@ -78,6 +78,20 @@ class YoutubeHttpClient extends http.BaseClient {
return response;
}
@override
Future<http.Response> post(Uri url,
{Map<String, String>? 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<String> postString(dynamic url,
{Map<String, String>? body,

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<StreamContext> _getStreamContextFromEmbeddedClient(
VideoId videoId) async {
final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value);
return StreamContext(page.streams.toList(), const []);
}
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
final watchPage = await WatchPage.get(_httpClient, videoId.toString());
@ -224,7 +232,14 @@ class StreamsClient {
Future<StreamManifest> 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);
}