import 'dart:convert';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as html;
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart' show MediaType;
import 'cipher/cipher.dart';
import 'extensions/extensions.dart';
import 'models/models.dart';
import 'parser.dart' as parser;
/// YoutubeExplode entry class.
class YoutubeExplode {
static final _regMatchExp = RegExp(r'youtube\..+?/watch.*?v=(.*?)(?:&|/|$)');
static final _shortMatchExp = RegExp(r'youtu\.be/(.*?)(?:\?|&|/|$)');
static final _embedMatchExp = RegExp(r'youtube\..+?/embed/(.*?)(?:\?|&|/|$)');
static final _playerConfigRegexp = RegExp(
r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);",
multiLine: true,
caseSensitive: false);
static final _contentLenRegexp = RegExp(r'clen=(\d+)');
/// HTTP Client.
// Visible only for extensions.
http.Client client;
/// Initialize [YoutubeExplode] class and http client.
YoutubeExplode() : client = http.Client();
/// Returns a [Future] that completes with a [MediaStreamInfoSet]
/// Use this to extract the muxed, audio and video streams from a video.
Future getVideoMediaStream(String videoId) async {
if (!validateVideoId(videoId)) {
throw ArgumentError.value(videoId, 'videoId', 'Invalid video id');
}
var playerConfiguration = await getPlayerConfiguration(videoId);
var muxedStreamInfoMap = {};
var audioStreamInfoMap = {};
var videoStreamInfoMap = {};
var muxedStreamInfoDics =
playerConfiguration.muxedStreamInfosUrlEncoded?.split(',');
if (muxedStreamInfoDics != null) {
// TODO: Implement muxedStreamInfoDics
throw UnsupportedError(
'muxedStreamInfoDics not null not implemented yet.');
}
if (playerConfiguration.muxedStreamInfoJson != null) {
for (var streamInfoJson in playerConfiguration.muxedStreamInfoJson) {
var itag = streamInfoJson['itag'] as int;
var urlString = streamInfoJson['url'] as String;
Uri url;
if (urlString.isNullOrWhiteSpace &&
!playerConfiguration.playerSourceUrl.isNullOrWhiteSpace) {
var cipher = streamInfoJson['cipher'] as String;
url = await decipherUrl(
playerConfiguration.playerSourceUrl, cipher, client);
}
url ??= Uri.parse(urlString);
var contentLength =
_parseContentLength(streamInfoJson['contentLength'], urlString);
// Extract container
var mimeType = MediaType.parse(streamInfoJson['mimeType'] as String);
var container = parser.stringToContainer(mimeType.subtype);
var codecs = mimeType.parameters['codecs'].split(',');
// Extract audio encoding
var audioEncoding = parser.audioEncodingFromString(codecs.last);
// Extract video encoding
var videoEncoding = parser.videoEncodingFromString(codecs.first);
// Extract video quality from itag.
var videoQuality = parser.videoQualityFromITag(itag);
// Get video quality label
var videoQualityLabel = parser.videoQualityToLabel(videoQuality);
// Get video resolution
var resolution = parser.videoQualityToResolution(videoQuality);
assert(url != null);
assert(contentLength != null && contentLength != -1);
muxedStreamInfoMap[itag] = MuxedStreamInfo(
itag,
url,
container,
contentLength,
audioEncoding,
videoEncoding,
videoQualityLabel,
videoQuality,
resolution);
}
}
var adaptiveStreamInfoDics =
playerConfiguration.adaptiveStreamInfosUrlEncoded?.split(',');
if (adaptiveStreamInfoDics != null) {
// TODO: Implement adaptiveStreamInfoDics
throw UnsupportedError(
'adaptiveStreamInfoDics not null not implemented yet.');
}
if (playerConfiguration.adaptiveStreamInfosJson != null) {
for (var streamInfoJson in playerConfiguration.adaptiveStreamInfosJson) {
var itag = streamInfoJson['itag'] as int;
var urlString = streamInfoJson['url'] as String;
var bitrate = streamInfoJson['bitrate'] as int;
Uri url;
if (urlString.isNullOrWhiteSpace &&
!playerConfiguration.playerSourceUrl.isNullOrWhiteSpace) {
var cipher = streamInfoJson['cipher'] as String;
url = await decipherUrl(
playerConfiguration.playerSourceUrl, cipher, client);
}
url ??= Uri.parse(urlString);
var contentLength =
_parseContentLength(streamInfoJson['contentLength'], urlString);
// Extract container
var mimeType = MediaType.parse(streamInfoJson['mimeType'] as String);
var container = parser.stringToContainer(mimeType.subtype);
var codecs = mimeType.parameters['codecs'].toLowerCase();
// Audio only
if (streamInfoJson['audioSampleRate'] != null) {
var audioEncoding = parser.audioEncodingFromString(codecs);
audioStreamInfoMap[itag] = AudioStreamInfo(
itag, url, container, contentLength, bitrate, audioEncoding);
} else {
// Video only
var videoEncoding = codecs == 'unknown'
? VideoEncoding.av1
: parser.videoEncodingFromString(codecs);
var videoQualityLabel = streamInfoJson['qualityLabel'] as String;
var videoQuality = parser.videoQualityFromLabel(videoQualityLabel);
var width = streamInfoJson['width'] as int;
var height = streamInfoJson['height'] as int;
var resolution = VideoResolution(width, height);
var framerate = streamInfoJson['fps'];
videoStreamInfoMap[itag] = VideoStreamInfo(
itag,
url,
container,
contentLength,
bitrate,
videoEncoding,
videoQualityLabel,
videoQuality,
resolution,
framerate);
}
}
}
var sortedMuxed = muxedStreamInfoMap.values.toList()
..sort((a, b) => a.videoQuality.index.compareTo(b.videoQuality.index));
var sortedAudio = audioStreamInfoMap.values.toList()
..sort((a, b) => a.bitrate.compareTo(b.bitrate));
var sortedVideo = videoStreamInfoMap.values.toList()
..sort((a, b) => a.videoQuality.index.compareTo(b.videoQuality.index));
return MediaStreamInfoSet(
sortedMuxed,
sortedAudio,
sortedVideo,
playerConfiguration.hlsManifestUrl,
playerConfiguration.video,
playerConfiguration.validUntil);
}
/// Returns the player configuration for a given video.
Future getPlayerConfiguration(String videoId) async {
var body = (await client.get(
'https://www.youtube.com/embed/$videoId?disable_polymer=true&hl=en'))
.body;
var document = html.parse(body);
var playerConfigRaw = document
.getElementsByTagName('script')
.map((e) => e.innerHtml)
.map((e) => _playerConfigRegexp?.firstMatch(e)?.group(1))
.firstWhere((s) => s?.trim()?.isNotEmpty ?? false);
var playerConfigJson = json.decode(playerConfigRaw);
// Extract player source URL.
var playerSourceUrl =
'https://youtube.com${playerConfigJson['assets']['js']}';
// Get video info dictionary.
var videoInfoDic = await getVideoInfoDictionary(videoId);
var playerResponseJson = json.decode(videoInfoDic['player_response']);
var playAbility = playerResponseJson['playabilityStatus'];
if (playAbility['status'].toString().toLowerCase() == 'error') {
throw Exception('Video [$videoId] is unavailable');
}
var errorReason = playAbility['reason'] as String;
// Valid configuration
if (errorReason.isNullOrWhiteSpace) {
// Extract if it is a live stream.
var isLiveStream = playerResponseJson['videoDetails']['isLive'] == true;
var videoInfo = playerResponseJson['videoDetails'];
var video = Video(
videoId,
videoInfo['author'],
null,
videoInfo['title'],
videoInfo['shortDescription'],
ThumbnailSet(videoId),
Duration(seconds: int.parse(videoInfo['lengthSeconds'])),
videoInfo['keywords'].cast(),
Statistics(int.parse(videoInfo['viewCount']), 0, 0));
var streamingData = playerResponseJson['streamingData'];
var validUntil = DateTime.now()
.add(Duration(seconds: int.parse(streamingData['expiresInSeconds'])));
var hlsManifestUrl =
isLiveStream ? streamingData['hlsManifestUrl'] : null;
var dashManifestUrl =
isLiveStream ? null : streamingData['dashManifestUrl'];
var muxedStreamInfosUrlEncoded =
isLiveStream ? null : videoInfoDic['url_encoded_fmt_stream_map'];
var adaptiveStreamInfosUrlEncoded =
isLiveStream ? null : videoInfoDic['adaptive_fmts'];
var muxedStreamInfosJson = isLiveStream ? null : streamingData['formats'];
var adaptiveStreamInfosJson =
isLiveStream ? null : streamingData['adaptiveFormats'];
return PlayerConfiguration(
playerSourceUrl,
dashManifestUrl,
hlsManifestUrl,
muxedStreamInfosUrlEncoded,
adaptiveStreamInfosUrlEncoded,
muxedStreamInfosJson,
adaptiveStreamInfosJson,
video,
validUntil);
}
throw UnimplementedError(
'Get from video watch page or purchase video not implemented yet');
}
/// Returns the video info dictionary for a given vide.
Future