Answered Mar 10, 2020 · 3 votes
It has nothing to do with "_" or "-". "n85KukOXc0A" doesn't work either. Some videos don't return the "url" field, but "cipher". It's an attempt from YouTube to obfuscate the URL.
Your $videoDownloadLink, for the video ID "wXhTHyIgQ_U", looks like:
array ( 0 => array ( 'itag' => 18, 'bitrate' => 568627, 'width' => 640, 'height' => 360, 'lastModified' => '1575010363774854', 'contentLength' => '16085704', 'quality' => 'medium', 'qualityLabel' => '360p', 'projectionType' => 'RECTANGULAR', 'averageBitrate' => 568472, 'audioQuality' => 'AUDIO_QUALITY_LOW', 'approxDurationMs' => '226371', 'audioSampleRate' => '44100', 'audioChannels' => 2, 'cipher' => 's=__L8kZ2zTIc_OfmovvG91jyFU3WN4QTERuPCxA7rHfbHICEhCrCQkmqPth6pmfw5wmrIPOT_ijWceGCWdCeK-lVYXgIARwMGkhKDv&url=https%3A%2F%2Fr4---sn-hpa7kn7s.googlevideo.com%2Fvideoplayback%3Fexpire%3D1583898090%26ei%3DigloXtGYD4bngAeu8YXQCg%26ip%3D2a00%253Aee2%253A1200%253Ae400%253A8c11%253A6897%253A2e00%253Abef0%26id%3Do-AAcaOp-0syooPWmAUuzOfm6gHGPWYCiDlfa-RNdIP34W%26itag%3D18%26source%3Dyoutube%26requiressl%3Dyes%26mm%3D31%252C26%26mn%3Dsn-hpa7kn7s%252Csn-nv47lnly%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D3%26pl%3D32%26gcr%3Dsi%26initcwndbps%3D1023750%26vprv%3D1%26mime%3Dvideo%252Fmp4%26gir%3Dyes%26clen%3D16085704%26ratebypass%3Dyes%26dur%3D226.371%26lmt%3D1575010363774854%26mt%3D1583876448%26fvip%3D4%26fexp%3D23842630%26c%3DWEB%26txp%3D5531432%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cgcr%252Cvprv%252Cmime%252Cgir%252Cclen%252Cratebypass%252Cdur%252Clmt%26lsparams%3Dmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DABSNjpQwRQIgBvV2KI0zNTv-7PsmdoRnpyNBvxeMRJIHSlKjfScxihcCIQDlHa5A-1cGAVReyssZ4YkH2nV2rdN1fel6_-Bkv7CAjA%253D%253D&sp=sig', 'title' => 'Post Malone - Circles', 'mime' => 'video/mp4', 'format' => 'mp4', ),)
As you see, there is no "url" field, but there is a "cipher" field. If we decode it with parse_str($videoDownloadLink[0]['cipher'], $cipher) we get:
array ( 's' => '__L8kZ2zTIc_OfmovvG91jyFU3WN4QTERuPCxA7rHfbHICEhCrCQkmqPth6pmfw5wmrIPOT_ijWceGCWdCeK-lVYXgIARwMGkhKDv', 'url' => 'https://r4---sn-hpa7kn7s.googlevideo.com/videoplayback?expire=1583898090&ei=igloXtGYD4bngAeu8YXQCg&ip=2a00%3Aee2%3A1200%3Ae400%3A8c11%3A6897%3A2e00%3Abef0&id=o-AAcaOp-0syooPWmAUuzOfm6gHGPWYCiDlfa-RNdIP34W&itag=18&source=youtube&requiressl=yes&mm=31%2C26&mn=sn-hpa7kn7s%2Csn-nv47lnly&ms=au%2Conr&mv=m&mvi=3&pl=32&gcr=si&initcwndbps=1023750&vprv=1&mime=video%2Fmp4&gir=yes&clen=16085704&ratebypass=yes&dur=226.371&lmt=1575010363774854&mt=1583876448&fvip=4&fexp=23842630&c=WEB&txp=5531432&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cgcr%2Cvprv%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&lsparams=mm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=ABSNjpQwRQIgBvV2KI0zNTv-7PsmdoRnpyNBvxeMRJIHSlKjfScxihcCIQDlHa5A-1cGAVReyssZ4YkH2nV2rdN1fel6_-Bkv7CAjA%3D%3D', 'sp' => 'sig',)
You need to properly scramble the "s" field value and add it to the URL as the field named with the "sp" field value. The way it needs to be scrambled changes regularly. The current way from https://www.youtube.com/yts/jsbin/player_ias-vfle4a9aa/en_US/base.js is:
var Ps = function(a) { a = a.split(""); Os.Dw(a, 1); Os.hZ(a, 21); Os.An(a, 24); Os.hZ(a, 34); Os.hZ(a, 18); Os.hZ(a, 63); return a.join("") };
var Os = { Dw: function(a, b) { a.splice(0, b) }, An: function(a) { a.reverse() }, hZ: function(a, b) { var c = a[0]; a[0] = a[b % a.length]; a[b % a.length] = c }};
Which translates into PHP as:
function scramble($a) { $a = str_split($a); scr_splice($a, 1); scr_swap($a, 21); scr_reverse($a, 24); scr_swap($a, 34); scr_swap($a, 18); scr_swap($a, 63); return implode('', $a);}function scr_reverse(&$a) { $a = array_reverse($a);}function scr_splice(&$a, $b) { array_splice($a, 0, $b);}function scr_swap(&$a, $b) { $c = $a[0]; $a[0] = $a[$b % count($a)]; $a[$b % count($a)] = $c;}
In your code, you need to check which type of URL you got and get the proper URL.
if (isset($videoDownloadLink[0]['url'])) { $downloadURL = $videoDownloadLink[0]['url'];}else if (isset($videoDownloadLink[0]['cipher'])) { parse_str($videoDownloadLink[0]['cipher'], $cipher); $downloadURL = $cipher['url']."&".$cipher["sp"]."=".scramble($cipher["s"]);}else { die('Error getting YouTube URL!');}
Note: This will only work until YouTube changes the way it's scrambled again.