Implement Embedded client. Fix #147
This commit is contained in:
parent
e8ac71ff44
commit
360a330dee
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue