youtube_explode/lib/src/videos/streams/streams_client.dart

232 lines
7.7 KiB
Dart
Raw Normal View History

2020-06-03 13:18:37 +02:00
import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
2020-06-03 23:02:21 +02:00
import '../../reverse_engineering/cipher/cipher_operations.dart';
2021-07-25 14:47:26 +02:00
import '../../reverse_engineering/clients/embedded_player_client.dart';
import '../../reverse_engineering/dash_manifest.dart';
2020-06-03 23:02:21 +02:00
import '../../reverse_engineering/heuristics.dart';
import '../../reverse_engineering/models/stream_info_provider.dart';
2021-07-21 02:06:02 +02:00
import '../../reverse_engineering/pages/watch_page.dart';
import '../../reverse_engineering/player/player_source.dart';
2020-06-03 23:02:21 +02:00
import '../../reverse_engineering/youtube_http_client.dart';
2020-06-03 13:18:37 +02:00
import '../video_id.dart';
2020-06-03 23:02:21 +02:00
import 'streams.dart';
2020-06-03 13:18:37 +02:00
/// Queries related to media streams of YouTube videos.
2020-06-03 23:02:21 +02:00
class StreamsClient {
2020-06-03 13:18:37 +02:00
final YoutubeHttpClient _httpClient;
/// Initializes an instance of [StreamsClient]
2020-06-03 23:02:21 +02:00
StreamsClient(this._httpClient);
2020-06-03 13:18:37 +02:00
2021-03-18 22:22:55 +01:00
Future<DashManifest> _getDashManifest(
Uri dashManifestUrl, Iterable<CipherOperation> cipherOperations) {
var signature =
DashManifest.getSignatureFromUrl(dashManifestUrl.toString());
2020-06-03 13:18:37 +02:00
if (!signature.isNullOrWhiteSpace) {
2021-03-11 14:20:10 +01:00
signature = cipherOperations.decipher(signature!);
2020-06-03 13:18:37 +02:00
dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature);
}
return DashManifest.get(_httpClient, dashManifestUrl);
}
2021-07-25 14:47:26 +02:00
Future<StreamContext> _getStreamContextFromEmbeddedClient(
VideoId videoId) async {
final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value);
return StreamContext(page.streams.toList(), const []);
}
2020-06-03 13:18:37 +02:00
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
final watchPage = await WatchPage.get(_httpClient, videoId.toString());
2020-12-25 23:29:01 +01:00
final playerConfig = watchPage.playerConfig;
2021-03-18 22:22:55 +01:00
var playerResponse =
2021-07-05 10:17:00 +02:00
watchPage.playerResponse ?? playerConfig?.playerResponse;
if (playerResponse == null) {
2020-06-03 13:18:37 +02:00
throw VideoUnplayableException.unplayable(videoId);
}
var previewVideoId = playerResponse.previewVideoId;
2021-03-11 14:20:10 +01:00
if (!previewVideoId.isNullOrWhiteSpace) {
2021-03-18 22:22:55 +01:00
throw VideoRequiresPurchaseException.preview(
videoId, VideoId(previewVideoId!));
2020-06-03 13:18:37 +02:00
}
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
2021-03-18 22:22:55 +01:00
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
? await PlayerSource.get(_httpClient, playerSourceUrl!)
: null;
var cipherOperations =
playerSource?.getCipherOperations() ?? const <CipherOperation>[];
2020-06-03 13:18:37 +02:00
if (!playerResponse.isVideoPlayable) {
2021-03-18 22:22:55 +01:00
throw VideoUnplayableException.unplayable(videoId,
reason: playerResponse.videoPlayabilityError ?? '');
2020-06-03 13:18:37 +02:00
}
if (playerResponse.isLive) {
throw VideoUnplayableException.liveStream(videoId);
}
var streamInfoProviders = <StreamInfoProvider>[
...playerResponse.streams,
];
2020-06-03 13:18:37 +02:00
var dashManifestUrl = playerResponse.dashManifestUrl;
2020-12-25 23:29:01 +01:00
if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) {
2021-03-18 22:22:55 +01:00
var dashManifest =
await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
2020-06-03 13:18:37 +02:00
streamInfoProviders.addAll(dashManifest.streams);
}
return StreamContext(streamInfoProviders, cipherOperations);
}
Future<StreamManifest> _getManifest(StreamContext streamContext) async {
// To make sure there are no duplicates streams, group them by tag
var streams = <int, StreamInfo>{};
2021-03-11 14:20:10 +01:00
for (final streamInfo in streamContext.streamInfoProviders) {
2020-06-03 13:18:37 +02:00
var tag = streamInfo.tag;
var url = Uri.parse(streamInfo.url);
// Signature
var signature = streamInfo.signature;
2020-07-16 20:02:54 +02:00
var signatureParameter = streamInfo.signatureParameter ?? 'signature';
2020-06-03 13:18:37 +02:00
if (!signature.isNullOrWhiteSpace) {
2021-03-11 14:20:10 +01:00
signature = streamContext.cipherOperations.decipher(signature!);
2020-06-05 20:08:04 +02:00
url = url.setQueryParam(signatureParameter, signature);
2020-06-03 13:18:37 +02:00
}
2021-09-28 16:49:38 +02:00
// Content length - Dont try to get content length of a dash stream.
var contentLength = streamInfo.source == StreamSource.dash
? 0
: streamInfo.contentLength ??
await _httpClient.getContentLength(url, validate: false) ??
0;
if (contentLength == 0 && streamInfo.source != StreamSource.dash) {
continue;
}
2020-06-03 13:18:37 +02:00
// Common
2021-03-11 14:20:10 +01:00
var container = StreamContainer.parse(streamInfo.container!);
2020-06-03 13:18:37 +02:00
var fileSize = FileSize(contentLength);
2021-09-28 16:49:38 +02:00
var bitrate = Bitrate(streamInfo.bitrate ?? 0);
2020-06-03 13:18:37 +02:00
var audioCodec = streamInfo.audioCodec;
var videoCodec = streamInfo.videoCodec;
// Muxed or Video-only
if (!videoCodec.isNullOrWhiteSpace) {
var framerate = Framerate(streamInfo.framerate ?? 24);
var videoQuality = VideoQualityUtil.fromLabel(streamInfo.qualityLabel);
2020-06-03 13:18:37 +02:00
var videoWidth = streamInfo.videoWidth;
var videoHeight = streamInfo.videoHeight;
2021-03-18 22:22:55 +01:00
var videoResolution = videoWidth != -1 && videoHeight != -1
? VideoResolution(videoWidth ?? 0, videoHeight ?? 0)
: videoQuality.toVideoResolution();
2020-06-03 13:18:37 +02:00
// Muxed
if (!audioCodec.isNullOrWhiteSpace &&
streamInfo.source != StreamSource.adaptive) {
2021-03-18 22:22:55 +01:00
streams[tag] = MuxedStreamInfo(
tag,
url,
container,
fileSize,
bitrate,
audioCodec!,
videoCodec!,
streamInfo.qualityLabel,
videoQuality,
videoResolution,
framerate,
streamInfo.codec,
);
2020-06-03 13:18:37 +02:00
continue;
}
// Video only
streams[tag] = VideoOnlyStreamInfo(
2021-03-18 22:22:55 +01:00
tag,
url,
container,
fileSize,
bitrate,
videoCodec!,
streamInfo.qualityLabel,
2021-03-18 22:22:55 +01:00
videoQuality,
videoResolution,
2021-09-28 16:49:38 +02:00
framerate,
2021-10-04 13:00:22 +02:00
streamInfo.fragments ?? const [],
streamInfo.codec);
2020-06-03 13:18:37 +02:00
continue;
}
// Audio-only
2020-06-05 20:08:04 +02:00
if (!audioCodec.isNullOrWhiteSpace) {
2021-10-04 13:00:22 +02:00
streams[tag] = AudioOnlyStreamInfo(
tag,
url,
container,
fileSize,
bitrate,
audioCodec!,
streamInfo.qualityLabel,
2021-10-04 13:00:22 +02:00
streamInfo.fragments ?? const [],
streamInfo.codec);
2020-06-03 13:18:37 +02:00
}
// #if DEBUG
// throw FatalFailureException("Stream info doesn't contain audio/video codec information.");
}
return StreamManifest(streams.values);
}
/// Gets the manifest that contains information
/// about available streams in the specified video.
2020-06-05 20:08:04 +02:00
Future<StreamManifest> getManifest(dynamic videoId) async {
videoId = VideoId.fromString(videoId);
2021-07-25 14:47:26 +02:00
try {
final context = await _getStreamContextFromEmbeddedClient(videoId);
2021-07-25 14:47:26 +02:00
return _getManifest(context);
} on YoutubeExplodeException {
//TODO: ignore
}
final context = await _getStreamContextFromWatchPage(videoId);
2021-07-25 14:47:26 +02:00
return _getManifest(context);
2020-06-03 13:18:37 +02:00
}
/// Gets the HTTP Live Stream (HLS) manifest URL
/// for the specified video (if it's a live video stream).
Future<String> getHttpLiveStreamUrl(VideoId videoId) async {
2021-07-05 10:17:00 +02:00
final watchPage = await WatchPage.get(_httpClient, videoId.value);
final playerResponse = watchPage.playerResponse;
if (playerResponse == null) {
throw TransientFailureException(
'Couldn\'t extract the playerResponse from the Watch Page!');
}
2020-06-03 13:18:37 +02:00
if (!playerResponse.isVideoPlayable) {
2021-03-18 22:22:55 +01:00
throw VideoUnplayableException.unplayable(videoId,
reason: playerResponse.videoPlayabilityError ?? '');
2020-06-03 13:18:37 +02:00
}
var hlsManifest = playerResponse.hlsManifestUrl;
if (hlsManifest == null) {
throw VideoUnplayableException.notLiveStream(videoId);
}
return hlsManifest;
}
/// Gets the actual stream which is identified by the specified metadata.
2021-03-18 22:22:55 +01:00
Stream<List<int>> get(StreamInfo streamInfo) =>
_httpClient.getStream(streamInfo);
2020-06-03 13:18:37 +02:00
}