diff --git a/YoutubeScript.js b/YoutubeScript.js
index 3ff8b0a5b0808b8546c2ec2a99153b0475f54a21..9a3c9ddc41881a83926f71af67c08568b5abd4af 100644
--- a/YoutubeScript.js
+++ b/YoutubeScript.js
@@ -33,7 +33,7 @@ const URL_YOUTUBE_SPONSORBLOCK = "https://sponsor.ajay.app/api/skipSegments?vide
 const URL_YOUTUBE_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id=";
 
 //Newest to oldest
-const CIPHER_TEST_HASHES = ["b22ef6e7", "a960a0cb", "178de1f2", "4eae42b1", "f98908d1", "0e6aaa83", "d0936ad4", "8e83803a", "30857836", "4cc5d082", "f2f137c6", "1dda5629", "23604418", "71547d26", "b7910ca8"];
+const CIPHER_TEST_HASHES = ["3400486c", "b22ef6e7", "a960a0cb", "178de1f2", "4eae42b1", "f98908d1", "0e6aaa83", "d0936ad4", "8e83803a", "30857836", "4cc5d082", "f2f137c6", "1dda5629", "23604418", "71547d26", "b7910ca8"];
 const CIPHER_TEST_PREFIX = "/s/player/";
 const CIPHER_TEST_SUFFIX = "/player_ias.vflset/en_US/base.js";
 
@@ -86,13 +86,20 @@ const REGEX_ASR = new RegExp(/<text .*?start="(.*?)" .*?dur="(.*?)".*?>(.*?)<\/t
 const USER_AGENT_WINDOWS = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
 const USER_AGENT_PHONE = "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.153 Mobile Safari/537.36";
 const USER_AGENT_TABLET = "Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1";
-const USER_AGENT_IOS = "com.google.ios.youtube/17.31.4(iPhone14,5; U; CPU iOS 15_6 like Mac OS X; US)";
+
+const IOS_APP_VERSION = "19.14.3"
+const IOS_DEVICE_VERSION = "iPhone15,4"
+const IOS_OS_VERSION = "17_4_1"
+const IOS_OS_VERSION_DETAILED = "17.4.1.21E237"
+const USER_AGENT_IOS = "com.google.ios.youtube/" + IOS_APP_VERSION + "(" + IOS_DEVICE_VERSION + "; U; CPU iOS " + IOS_OS_VERSION + " like Mac OS X; US)";
+
 const USER_AGENT_ANDROID = "com.google.android.youtube/17.31.35 (Linux; U; Android 12; US) gzip";
 const USER_AGENT_TVHTML5_EMBED = "Mozilla/5.0 (CrKey armv7l 1.5.16041) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.0 Safari/537.36";
 
 const USE_MOBILE_PAGES = true;
 const USE_ANDROID_FALLBACK = false;
 const USE_IOS_FALLBACK = true;
+const USE_IOS_VIDEOS_FALLBACK = false;
 
 const SORT_VIEWS_STRING = "Views";
 const SORT_RATING_STRING = "Rating";
@@ -346,7 +353,7 @@ source.getChannelTemplateByClaimMap = () => {
 source.isContentDetailsUrl = (url) => {
 	return REGEX_VIDEO_URL_DESKTOP.test(url) || REGEX_VIDEO_URL_SHARE.test(url) || REGEX_VIDEO_URL_SHARE_LIVE.test(url) || REGEX_VIDEO_URL_SHORT.test(url) || REGEX_VIDEO_URL_CLIP.test(url) || REGEX_VIDEO_URL_EMBED.test(url);
 };
-source.getContentDetails = (url, useAuth) => {
+source.getContentDetails = (url, useAuth, simplify) => {
 	useAuth = !!_settings?.authDetails || !!useAuth;
 
     url = convertIfOtherUrl(url);
@@ -419,6 +426,13 @@ source.getContentDetails = (url, useAuth) => {
 		console.log("Initial Player Data", initialPlayerData);
 	}
 
+	let creationData = {
+		url: url,
+		initialData: initialData,
+		initialPlayerData: initialPlayerData,
+		jsUrl: jsUrl
+	};
+
 	const videoDetails = extractVideoPage_VideoDetails(initialData, initialPlayerData, {
 		url: url
 	}, jsUrl, useLogin);
@@ -456,9 +470,22 @@ source.getContentDetails = (url, useAuth) => {
 		if(iosData?.streamingData?.hlsManifestUrl) {
 			log("Using iOS HLS substitute");
 			const existingUrl = videoDetails.hls.url;
+			videoDetails.hls.name = "HLS (IOS)";
 			videoDetails.hls.url = iosData.streamingData.hlsManifestUrl;
-			if(existingUrl == videoDetails.live?.url)
+			if(existingUrl == videoDetails.live?.url) {
+				videoDetails.live.name = "HLS (IOS)";
 				videoDetails.live.url = iosData.streamingData.hlsManifestUrl;
+			}
+		}
+	}
+	else if(USE_IOS_VIDEOS_FALLBACK && !simplify) {
+		const iosData = requestIOSStreamingData(videoDetails.id.value);
+		if(IS_TESTING)
+			console.log("IOS Streaming Data", iosData);
+
+		if(iosData?.streamingData?.adaptiveFormats) {
+			let newDescriptor = extractAdaptiveFormats_VideoDescriptor(iosData.streamingData.adaptiveFormats, jsUrl, creationData, "IOS ");
+			videoDetails.video = newDescriptor;
 		}
 	}
 
@@ -2181,12 +2208,12 @@ function requestIOSStreamingData(videoId) {
 		context: {
 			client: {
 				"clientName": "IOS",
-				"clientVersion": "17.31.4",
+             	"clientVersion": IOS_APP_VERSION,//"17.31.4",^M
 				"deviceMake": "Apple",
-				"deviceModel": "iPhone14,5",
+				"deviceModel": IOS_DEVICE_VERSION,//"iPhone14,5",^M
 				"platform": "MOBILE",
 				"osName": "iOS",
-				"osVersion": "15.6.0.19G71",
+				"osVersion": IOS_OS_VERSION_DETAILED,//"15.6.0.19G71",^M
 				"hl": langDisplay,
 				"gl": langRegion,
 			},
@@ -2903,6 +2930,89 @@ function extractVideoPage_VideoDetails(initialData, initialPlayerData, contextDa
 	}
     return result;
 }
+function extractAdaptiveFormats_VideoDescriptor(adaptiveSources, jsUrl, contextData, prefix) {   
+	const nonce = randomString(16);   
+	return adaptiveSources ? new UnMuxVideoSourceDescriptor(   
+			adaptiveSources.filter(x=>x.mimeType.startsWith("video/")).map(y=>{   
+					const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);   
+					const container = y.mimeType.substring(0, y.mimeType.indexOf(';'));   
+					if(codecs.startsWith("av01"))   
+							return null; //AV01 is unsupported.   
+
+					const logItag = y.itag ==  134;   
+					if(logItag) {   
+							//log(videoDetails.title + " || Format " + container + " - " + y.itag + " - " + y.width);   
+							log("Source Parameters:\n" + JSON.stringify({   
+									url: y.url,   
+									cipher: y.cipher,   
+									signatureCipher: y.signatureCipher   
+							}, null, "   "));   
+					}   
+					   
+					let url = decryptUrlN(y.url, jsUrl, logItag) ?? decryptUrl(y.cipher, jsUrl, logItag) ?? decryptUrl(y.signatureCipher, jsUrl, logItag);   
+					if(url.indexOf("&cpn=") < 0)   
+							url = url + "&cpn=" + nonce;   
+
+					const duration = parseInt(parseInt(y.approxDurationMs) / 1000) ?? 0;   
+					if(isNaN(duration))   
+							return null;   
+
+					if(!y.initRange?.end || !y.indexRange?.end)   
+							return null;   
+
+					return new YTVideoSource({   
+							name: prefix + y.height + "p" + (y.fps ? y.fps : "") + " " + container,   
+							url: url,   
+							width: y.width,   
+							height: y.height,   
+							duration: (!isNaN(duration)) ? duration : 0,   
+							container: y.mimeType.substring(0, y.mimeType.indexOf(';')),   
+							codec: codecs,   
+							bitrate: y.bitrate,   
+
+							itagId: y.itag,   
+							initStart: parseInt(y.initRange?.start),   
+							initEnd: parseInt(y.initRange?.end),   
+							indexStart: parseInt(y.indexRange?.start),   
+							indexEnd: parseInt(y.indexRange?.end)   
+					}, contextData);   
+			}).filter(x=>x != null),   
+			adaptiveSources.filter(x=>x.mimeType.startsWith("audio/")).map(y=>{   
+					const codecs = y.mimeType.substring(y.mimeType.indexOf('codecs=\"') + 8).slice(0, -1);   
+					const container = y.mimeType.substring(0, y.mimeType.indexOf(';'));   
+
+					let url = decryptUrlN(y.url, jsUrl) ?? decryptUrl(y.cipher, jsUrl) ?? decryptUrl(y.signatureCipher, jsUrl);   
+					if(url.indexOf("&cpn=") < 0)   
+							url = url + "&cpn=" + nonce;   
+					   
+					const duration = parseInt(parseInt(y.approxDurationMs) / 1000);   
+					if(isNaN(duration))   
+							return null;   
+
+					if(!y.initRange?.end || !y.indexRange?.end)   
+							return null;   
+
+					return new YTAudioSource({   
+							name: prefix + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs),   
+							container: container,   
+							bitrate: y.bitrate,   
+							url: url,   
+							duration: (!isNaN(duration)) ? duration : 0,   
+							container: y.mimeType.substring(0, y.mimeType.indexOf(';')),   
+							codec: codecs,   
+							language: ytLangIdToLanguage(y.audioTrack?.id),   
+
+							itagId: y.itag,   
+							initStart: parseInt(y.initRange?.start),   
+							initEnd: parseInt(y.initRange?.end),   
+							indexStart: parseInt(y.indexRange?.start),   
+							indexEnd: parseInt(y.indexRange?.end),   
+							audioChannels: y.audioChannels   
+					}, contextData);   
+			}).filter(x=>x!=null),   
+	) : new VideoSourceDescriptor([])   
+}
+
 function toSRTTime(sec, withDot) {
 	let hours = 0;
 	let minutes = 0;
@@ -4270,8 +4380,11 @@ const REGEX_CIPHERS = [
 	new RegExp("\\b([\\w$]{2,})\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;"),
 	new RegExp("\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(")
 ];
-const REGEX_DECRYPT_N = /\.get\(\"n\"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/;
-const REGEX_DECRYPT_N2 = /[a-zA-Z0-9$_]+=String\.fromCharCode\(110\),[a-zA-Z0-9$_]+=[a-zA-Z0-9$_]+\.get\([a-zA-Z0-9$_]+\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/;
+const REGEX_DECRYPT_N_VARIANTS = [
+	/\.get\(\"n\"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/,
+	/[a-zA-Z0-9$_]+=String\.fromCharCode\(110\),[a-zA-Z0-9$_]+=[a-zA-Z0-9$_]+\.get\([a-zA-Z0-9$_]+\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/,
+	/[a-zA-Z]+="[n]+"\[.+\],[a-zA-Z0-9$_]+=[a-zA-Z0-9$_]+\.get\([a-zA-Z0-9$_]+\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/
+];
 const REGEX_PARAM_N = new RegExp("[?&]n=([^&]*)");
 const STS_REGEX = new RegExp("signatureTimestamp[=:](\\d+)");
 
@@ -4427,10 +4540,14 @@ function clearCipher(jsUrl) {
 function getNDecryptorFunctionCode(code, jsUrl) {
 	if(_nDecrypt[jsUrl])
 		return _nDecrypt[jsUrl];
-	let nDecryptFunctionArrNameMatch = REGEX_DECRYPT_N.exec(code);
-	if(!nDecryptFunctionArrNameMatch) {
-		console.log("NDecryptor failed, trying fallback");
-		nDecryptFunctionArrNameMatch = REGEX_DECRYPT_N2.exec(code);
+	let nDecryptFunctionArrNameMatch = undefined;
+	for(let i = 0; i < REGEX_DECRYPT_N_VARIANTS.length; i++) {
+		nDecryptFunctionArrNameMatch = REGEX_DECRYPT_N_VARIANTS[i].exec(code);
+		if(!nDecryptFunctionArrNameMatch) {
+			console.log("NDecryptor failed, trying fallback to [" + i + 2 + "]");
+		}
+		else
+			break;
 	}
 	if(!nDecryptFunctionArrNameMatch) {
         if(bridge.devSubmit) bridge.devSubmit("getNDecryptorFunctionCode - Failed to find n decryptor (name)", jsUrl);