2020-05-31 23:36:23 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
import 'package:http_parser/http_parser.dart';
|
2020-06-05 16:17:08 +02:00
|
|
|
|
|
|
|
import '../../extensions/helpers_extension.dart';
|
2020-11-01 15:05:19 +01:00
|
|
|
import 'generated/player_response.g.dart';
|
2020-06-05 16:17:08 +02:00
|
|
|
import 'stream_info_provider.dart';
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-05-31 23:36:23 +02:00
|
|
|
class PlayerResponse {
|
2020-09-11 13:42:21 +02:00
|
|
|
// Json parsed class
|
|
|
|
PlayerResponseJson _root;
|
|
|
|
|
|
|
|
/// Json parsed map
|
|
|
|
final Map<String, dynamic> _rawJson;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-06-22 17:40:57 +02:00
|
|
|
Iterable<StreamInfoProvider> _muxedStreams;
|
|
|
|
Iterable<StreamInfoProvider> _adaptiveStreams;
|
|
|
|
List<StreamInfoProvider> _streams;
|
|
|
|
Iterable<ClosedCaptionTrack> _closedCaptionTrack;
|
|
|
|
String _videoPlayabilityError;
|
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get playabilityStatus => _root.playabilityStatus.status;
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-23 10:12:08 +02:00
|
|
|
bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error';
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-23 10:12:08 +02:00
|
|
|
bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok';
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get videoTitle => _root.videoDetails.title;
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get videoAuthor => _root.videoDetails.author;
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
DateTime get videoUploadDate =>
|
|
|
|
_root.microformat.playerMicroformatRenderer.uploadDate;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get videoChannelId => _root.videoDetails.channelId;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-23 10:12:08 +02:00
|
|
|
Duration get videoDuration =>
|
2020-09-11 13:42:21 +02:00
|
|
|
Duration(seconds: int.parse(_root.videoDetails.lengthSeconds));
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
List<String> get videoKeywords => _root.videoDetails.keywords ?? const [];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get videoDescription => _root.videoDetails.shortDescription;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
int get videoViewCount => int.parse(_root.videoDetails.viewCount);
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-09-11 13:42:21 +02:00
|
|
|
//TODO: Get these types
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-05-31 23:36:23 +02:00
|
|
|
// Can be null
|
2020-06-23 10:12:08 +02:00
|
|
|
String get previewVideoId =>
|
2020-09-11 13:42:21 +02:00
|
|
|
_rawJson
|
2020-05-31 23:36:23 +02:00
|
|
|
.get('playabilityStatus')
|
|
|
|
?.get('errorScreen')
|
|
|
|
?.get('playerLegacyDesktopYpcTrailerRenderer')
|
2020-06-05 16:17:08 +02:00
|
|
|
?.getValue('trailerVideoId') ??
|
2020-09-11 13:42:21 +02:00
|
|
|
Uri.splitQueryString(_rawJson
|
2020-05-31 23:36:23 +02:00
|
|
|
.get('playabilityStatus')
|
|
|
|
?.get('errorScreen')
|
|
|
|
?.get('')
|
|
|
|
?.get('ypcTrailerRenderer')
|
2020-06-05 16:17:08 +02:00
|
|
|
?.getValue('playerVars') ??
|
2020-05-31 23:36:23 +02:00
|
|
|
'')['video_id'];
|
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
bool get isLive => _root.videoDetails.isLive ?? false;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-03 13:18:37 +02:00
|
|
|
// Can be null
|
2020-09-11 13:42:21 +02:00
|
|
|
String get hlsManifestUrl => _root.streamingData?.hlsManifestUrl;
|
2020-06-03 13:18:37 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-05-31 23:36:23 +02:00
|
|
|
// Can be null
|
2020-09-11 13:42:21 +02:00
|
|
|
String get dashManifestUrl => _root.streamingData?.dashManifestUrl;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
List<StreamInfoProvider> get muxedStreams =>
|
|
|
|
_muxedStreams ??= _root.streamingData?.formats
|
|
|
|
?.map((e) => _StreamInfo(e))
|
|
|
|
?.cast<StreamInfoProvider>()
|
|
|
|
?.toList() ??
|
|
|
|
const <StreamInfoProvider>[];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
List<StreamInfoProvider> get adaptiveStreams =>
|
|
|
|
_adaptiveStreams ??= _root.streamingData?.adaptiveFormats
|
|
|
|
?.map((e) => _StreamInfo(e))
|
|
|
|
?.cast<StreamInfoProvider>()
|
|
|
|
?.toList() ??
|
|
|
|
const [];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-22 17:40:57 +02:00
|
|
|
List<StreamInfoProvider> get streams =>
|
|
|
|
_streams ??= [...muxedStreams, ...adaptiveStreams];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
List<ClosedCaptionTrack> get closedCaptionTrack => _closedCaptionTrack ??=
|
|
|
|
_root.captions?.playerCaptionsTracklistRenderer?.captionTracks
|
2020-06-22 17:40:57 +02:00
|
|
|
?.map((e) => ClosedCaptionTrack(e))
|
2020-09-11 13:42:21 +02:00
|
|
|
?.cast<ClosedCaptionTrack>()
|
|
|
|
?.toList() ??
|
2020-06-22 17:40:57 +02:00
|
|
|
const [];
|
|
|
|
|
2020-09-11 13:42:21 +02:00
|
|
|
/// Can be null
|
|
|
|
String getVideoPlayabilityError() =>
|
|
|
|
_videoPlayabilityError ??= _root.playabilityStatus.reason;
|
2020-06-03 13:18:37 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
PlayerResponse.parse(String raw) : _rawJson = json.decode(raw) {
|
|
|
|
_root = PlayerResponseJson.fromJson(_rawJson);
|
|
|
|
}
|
2020-05-31 23:36:23 +02:00
|
|
|
}
|
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-05-31 23:36:23 +02:00
|
|
|
class ClosedCaptionTrack {
|
2020-09-11 13:42:21 +02:00
|
|
|
// Json parsed class
|
|
|
|
final CaptionTrack _root;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get url => _root.baseUrl;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get languageCode => _root.languageCode;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
String get languageName => _root.name.simpleText;
|
2020-06-03 23:02:21 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-09-11 13:42:21 +02:00
|
|
|
bool get autoGenerated => _root.vssId.toLowerCase().startsWith('a.');
|
2020-06-22 17:40:57 +02:00
|
|
|
|
2020-07-16 20:02:54 +02:00
|
|
|
///
|
2020-06-22 17:40:57 +02:00
|
|
|
ClosedCaptionTrack(this._root);
|
2020-05-31 23:36:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class _StreamInfo extends StreamInfoProvider {
|
2020-06-22 17:40:57 +02:00
|
|
|
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
|
|
|
|
|
2020-09-11 13:42:21 +02:00
|
|
|
// Json parsed class
|
|
|
|
final Format _root;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-06-22 17:40:57 +02:00
|
|
|
int _bitrate;
|
|
|
|
String _container;
|
|
|
|
int _contentLength;
|
|
|
|
int _framerate;
|
|
|
|
String _signature;
|
|
|
|
String _signatureParameter;
|
|
|
|
int _tag;
|
|
|
|
String _url;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
int get bitrate => _bitrate ??= _root.bitrate;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-06-22 17:40:57 +02:00
|
|
|
String get container => _container ??= mimeType.subtype;
|
2020-06-05 16:17:08 +02:00
|
|
|
|
2020-05-31 23:36:23 +02:00
|
|
|
@override
|
|
|
|
int get contentLength =>
|
2020-09-11 13:42:21 +02:00
|
|
|
_contentLength ??= int.tryParse(_root.contentLength ?? '') ??
|
2020-06-22 17:40:57 +02:00
|
|
|
_contentLenExp.firstMatch(url)?.group(1);
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
int get framerate => _framerate ??= _root.fps;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-06-05 16:17:08 +02:00
|
|
|
String get signature =>
|
2020-09-11 13:42:21 +02:00
|
|
|
_signature ??= Uri.splitQueryString(_root.signatureCipher ?? '')['s'];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
String get signatureParameter =>
|
|
|
|
_signatureParameter ??= Uri.splitQueryString(_root.cipher ?? '')['sp'] ??
|
|
|
|
Uri.splitQueryString(_root.signatureCipher ?? '')['sp'];
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
int get tag => _tag ??= _root.itag;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-06-22 17:40:57 +02:00
|
|
|
String get url => _url ??= _getUrl();
|
2020-06-05 16:17:08 +02:00
|
|
|
|
|
|
|
String _getUrl() {
|
2020-09-11 13:42:21 +02:00
|
|
|
var url = _root.url;
|
|
|
|
url ??= Uri.splitQueryString(_root.cipher ?? '')['url'];
|
|
|
|
url ??= Uri.splitQueryString(_root.signatureCipher ?? '')['url'];
|
2020-06-05 16:17:08 +02:00
|
|
|
return url;
|
|
|
|
}
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-06-22 17:40:57 +02:00
|
|
|
bool _isAudioOnly;
|
|
|
|
MediaType _mimeType;
|
|
|
|
String _codecs;
|
|
|
|
|
2020-05-31 23:36:23 +02:00
|
|
|
@override
|
2020-06-05 16:17:08 +02:00
|
|
|
String get videoCodec =>
|
|
|
|
isAudioOnly ? null : codecs.split(',').first.trim().nullIfWhitespace;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
int get videoHeight => _root.height;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
String get videoQualityLabel => _root.qualityLabel;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-09-11 13:42:21 +02:00
|
|
|
int get videoWidth => _root.width;
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-06-22 17:40:57 +02:00
|
|
|
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio';
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-09-11 13:42:21 +02:00
|
|
|
MediaType get mimeType => _mimeType ??= MediaType.parse(_root.mimeType);
|
2020-05-31 23:36:23 +02:00
|
|
|
|
2020-06-22 17:40:57 +02:00
|
|
|
String get codecs =>
|
|
|
|
_codecs ??= mimeType?.parameters['codecs']?.toLowerCase();
|
2020-05-31 23:36:23 +02:00
|
|
|
|
|
|
|
@override
|
2020-06-05 16:17:08 +02:00
|
|
|
String get audioCodec =>
|
2020-06-05 20:08:04 +02:00
|
|
|
isAudioOnly ? codecs : _getAudioCodec(codecs.split(','))?.trim();
|
|
|
|
|
|
|
|
String _getAudioCodec(List<String> codecs) {
|
|
|
|
if (codecs.length == 1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return codecs.last;
|
|
|
|
}
|
2020-06-22 17:40:57 +02:00
|
|
|
|
|
|
|
_StreamInfo(this._root);
|
2020-05-31 23:36:23 +02:00
|
|
|
}
|