import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http_parser/http_parser.dart'; import '../../../youtube_explode_dart.dart'; import '../../extensions/helpers_extension.dart'; import '../../retry.dart'; import '../models/stream_info_provider.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, StreamSource.muxed)) ?? const []; /// late final Iterable<_StreamInfo> adaptiveStreams = root .get('streamingData') ?.getList('adaptiveFormats') ?.map((e) => _StreamInfo(e, StreamSource.adaptive)) ?? const []; /// late final List<_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', 'clientVersion': '16.46.37' } }, 'videoId': videoId }; final url = Uri.parse( 'http://127.0.0.1:8080/www.youtube.com:443/youtubei/v1/player'); return retry(httpClient, () 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 { static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)'); /// Json parsed map final JsonMap root; @override late final int? bitrate = root.getT('bitrate'); @override late final String container = codec.subtype; @override late final int? contentLength = int.tryParse( root.getT('contentLength') ?? _contentLenExp.firstMatch(url)?.group(1) ?? ''); @override late final int? framerate = root.getT('fps'); @override late final String? signature = Uri.splitQueryString(root.getT('signatureCipher') ?? '')['s']; @override late final String? signatureParameter = Uri.splitQueryString( root.getT('cipher') ?? '')['sp'] ?? Uri.splitQueryString(root.getT('signatureCipher') ?? '')['sp']; @override late final int tag = root.getT('itag')!; @override late final String url = root.getT('url') ?? Uri.splitQueryString(root.getT('cipher') ?? '')['url'] ?? Uri.splitQueryString(root.getT('signatureCipher') ?? '')['url']!; @override late final String? videoCodec = isAudioOnly ? null : codecs?.split(',').firstOrNull?.trim().nullIfWhitespace; @override late final int? videoHeight = root.getT('height'); @override @Deprecated('Use qualityLabel') String get videoQualityLabel => qualityLabel; @override late final String qualityLabel = root.getT('qualityLabel') ?? 'tiny'; // Not sure if 'tiny' is the correct placeholder. @override late final int? videoWidth = root.getT('width'); late final bool isAudioOnly = codec.type == 'audio'; @override late final MediaType codec = _getMimeType()!; MediaType? _getMimeType() { var mime = root.getT('mimeType'); if (mime == null) { return null; } return MediaType.parse(mime); } late final String? codecs = codec.parameters['codecs']?.toLowerCase(); @override late final String? audioCodec = isAudioOnly ? codecs : _getAudioCodec(codecs?.split(','))?.trim(); String? _getAudioCodec(List? codecs) { if (codecs == null) { return null; } if (codecs.length == 1) { return null; } return codecs.last; } @override final StreamSource source; _StreamInfo(this.root, this.source); }