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> getVideoInfoDictionary(String videoId) async { var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId'); var url = 'https://youtube.com/get_video_info?video_id=$videoId' '&el=embedded&eurl=$eurl&hl=en'; var raw = (await client.get(url)).body; return Uri.splitQueryString(raw); } /// Return a [Video] instance. /// Use this to extract general info about a video. Future