Permalink
| <?php | |
| define('AUDIO', 0x08); | |
| define('VIDEO', 0x09); | |
| define('AKAMAI_ENC_AUDIO', 0x0A); | |
| define('AKAMAI_ENC_VIDEO', 0x0B); | |
| define('SCRIPT_DATA', 0x12); | |
| define('FRAME_TYPE_INFO', 0x05); | |
| define('CODEC_ID_AVC', 0x07); | |
| define('CODEC_ID_AAC', 0x0A); | |
| define('AVC_SEQUENCE_HEADER', 0x00); | |
| define('AAC_SEQUENCE_HEADER', 0x00); | |
| define('AVC_NALU', 0x01); | |
| define('AVC_SEQUENCE_END', 0x02); | |
| define('FRAMEFIX_STEP', 40); | |
| define('INVALID_TIMESTAMP', -1); | |
| define('STOP_PROCESSING', 2); | |
| class CLI | |
| { | |
| protected static $ACCEPTED = array(); | |
| var $params = array(); | |
| function __construct($options = array(), $handleUnknown = false) | |
| { | |
| global $argc, $argv; | |
| if (count($options)) | |
| self::$ACCEPTED = $options; | |
| // Parse params | |
| if ($argc > 1) | |
| { | |
| $paramSwitch = false; | |
| for ($i = 1; $i < $argc; $i++) | |
| { | |
| $arg = $argv[$i]; | |
| $isSwitch = preg_match('/^-+/', $arg); | |
| if ($isSwitch) | |
| $arg = preg_replace('/^-+/', '', $arg); | |
| if ($paramSwitch and $isSwitch) | |
| $this->error("[param] expected after '$paramSwitch' switch (" . self::$ACCEPTED[1][$paramSwitch] . ')'); | |
| else if (!$paramSwitch and !$isSwitch) | |
| { | |
| if ($handleUnknown) | |
| $this->params['unknown'][] = $arg; | |
| else | |
| $this->error("'$arg' is an invalid option, use --help to display valid switches."); | |
| } | |
| else if (!$paramSwitch and $isSwitch) | |
| { | |
| if (isset($this->params[$arg])) | |
| $this->error("'$arg' switch can't occur more than once"); | |
| $this->params[$arg] = true; | |
| if (isset(self::$ACCEPTED[1][$arg])) | |
| $paramSwitch = $arg; | |
| else if (!isset(self::$ACCEPTED[0][$arg])) | |
| $this->error("there's no '$arg' switch, use --help to display all switches."); | |
| } | |
| else if ($paramSwitch and !$isSwitch) | |
| { | |
| $this->params[$paramSwitch] = $arg; | |
| $paramSwitch = false; | |
| } | |
| } | |
| } | |
| // Final check | |
| foreach ($this->params as $k => $v) | |
| if (isset(self::$ACCEPTED[1][$k]) and $v === true) | |
| $this->error("[param] expected after '$k' switch (" . self::$ACCEPTED[1][$k] . ')'); | |
| } | |
| function displayHelp() | |
| { | |
| LogInfo("You can use the script with following options:\n"); | |
| foreach (self::$ACCEPTED[0] as $key => $value) | |
| LogInfo(sprintf(" --%-17s %s", $key, $value)); | |
| foreach (self::$ACCEPTED[1] as $key => $value) | |
| LogInfo(sprintf(" --%-9s%-8s %s", $key, " [param]", $value)); | |
| } | |
| function error($msg) | |
| { | |
| LogError($msg); | |
| } | |
| function getParam($name) | |
| { | |
| if (isset($this->params[$name])) | |
| return $this->params[$name]; | |
| else | |
| return false; | |
| } | |
| } | |
| class cURL | |
| { | |
| var $headers, $user_agent, $compression, $cookie_file; | |
| var $active, $cert_check, $fragProxy, $maxSpeed, $proxy, $response; | |
| var $mh, $ch, $mrc; | |
| static $ref = 0; | |
| function __construct($cookies = true, $cookie = 'Cookies.txt', $compression = 'gzip', $proxy = '') | |
| { | |
| $this->headers = $this->headers(); | |
| $this->user_agent = 'Mozilla/5.0 (Windows NT 5.1; rv:41.0) Gecko/20100101 Firefox/41.0'; | |
| $this->compression = $compression; | |
| $this->cookies = $cookies; | |
| if ($this->cookies == true) | |
| $this->cookie($cookie); | |
| $this->cert_check = false; | |
| $this->fragProxy = false; | |
| $this->maxSpeed = 0; | |
| $this->proxy = $proxy; | |
| self::$ref++; | |
| } | |
| function __destruct() | |
| { | |
| $this->stopDownloads(); | |
| if ((self::$ref <= 1) and file_exists($this->cookie_file)) | |
| unlink($this->cookie_file); | |
| self::$ref--; | |
| } | |
| function headers() | |
| { | |
| $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; | |
| $headers[] = 'Connection: Keep-Alive'; | |
| return $headers; | |
| } | |
| function cookie($cookie_file) | |
| { | |
| if (file_exists($cookie_file)) | |
| $this->cookie_file = $cookie_file; | |
| else | |
| { | |
| $file = fopen($cookie_file, 'w') or $this->error('The cookie file could not be opened. Make sure this directory has the correct permissions.'); | |
| $this->cookie_file = $cookie_file; | |
| fclose($file); | |
| } | |
| } | |
| function get($url) | |
| { | |
| $process = curl_init($url); | |
| $options = array( | |
| CURLOPT_HTTPHEADER => $this->headers, | |
| CURLOPT_HEADER => 0, | |
| CURLOPT_USERAGENT => $this->user_agent, | |
| CURLOPT_ENCODING => $this->compression, | |
| CURLOPT_TIMEOUT => 30, | |
| CURLOPT_RETURNTRANSFER => 1, | |
| CURLOPT_FOLLOWLOCATION => 1 | |
| ); | |
| curl_setopt_array($process, $options); | |
| if (!$this->cert_check) | |
| curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false); | |
| if ($this->cookies == true) | |
| { | |
| curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); | |
| curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); | |
| } | |
| if ($this->proxy) | |
| $this->setProxy($process, $this->proxy); | |
| $this->response = curl_exec($process); | |
| if ($this->response !== false) | |
| $status = curl_getinfo($process, CURLINFO_HTTP_CODE); | |
| curl_close($process); | |
| if (isset($status)) | |
| return $status; | |
| else | |
| return false; | |
| } | |
| function post($url, $data) | |
| { | |
| $process = curl_init($url); | |
| $headers = $this->headers; | |
| $headers[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'; | |
| $options = array( | |
| CURLOPT_HTTPHEADER => $headers, | |
| CURLOPT_HEADER => 1, | |
| CURLOPT_USERAGENT => $this->user_agent, | |
| CURLOPT_ENCODING => $this->compression, | |
| CURLOPT_TIMEOUT => 30, | |
| CURLOPT_RETURNTRANSFER => 1, | |
| CURLOPT_FOLLOWLOCATION => 1, | |
| CURLOPT_POST => 1, | |
| CURLOPT_POSTFIELDS => $data | |
| ); | |
| curl_setopt_array($process, $options); | |
| if (!$this->cert_check) | |
| curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false); | |
| if ($this->cookies == true) | |
| { | |
| curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); | |
| curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); | |
| } | |
| if ($this->proxy) | |
| $this->setProxy($process, $this->proxy); | |
| $return = curl_exec($process); | |
| curl_close($process); | |
| return $return; | |
| } | |
| function setProxy(&$process, $proxy) | |
| { | |
| $type = ""; | |
| $separator = strpos($proxy, "://"); | |
| if ($separator !== false) | |
| { | |
| $type = strtolower(substr($proxy, 0, $separator)); | |
| $proxy = substr($proxy, $separator + 3); | |
| } | |
| switch ($type) | |
| { | |
| case "socks4": | |
| $type = CURLPROXY_SOCKS4; | |
| break; | |
| case "socks5": | |
| $type = CURLPROXY_SOCKS5; | |
| break; | |
| default: | |
| $type = CURLPROXY_HTTP; | |
| } | |
| curl_setopt($process, CURLOPT_PROXY, $proxy); | |
| curl_setopt($process, CURLOPT_PROXYTYPE, $type); | |
| } | |
| function addDownload($url, $id) | |
| { | |
| if (!isset($this->mh)) | |
| $this->mh = curl_multi_init(); | |
| if (isset($this->ch[$id])) | |
| return false; | |
| $download =& $this->ch[$id]; | |
| $download['id'] = $id; | |
| $download['url'] = $url; | |
| $download['ch'] = curl_init($url); | |
| $options = array( | |
| CURLOPT_HTTPHEADER => $this->headers, | |
| CURLOPT_HEADER => 0, | |
| CURLOPT_USERAGENT => $this->user_agent, | |
| CURLOPT_ENCODING => $this->compression, | |
| CURLOPT_LOW_SPEED_LIMIT => 1024, | |
| CURLOPT_LOW_SPEED_TIME => 10, | |
| CURLOPT_BINARYTRANSFER => 1, | |
| CURLOPT_RETURNTRANSFER => 1, | |
| CURLOPT_FOLLOWLOCATION => 1 | |
| ); | |
| curl_setopt_array($download['ch'], $options); | |
| if (!$this->cert_check) | |
| curl_setopt($download['ch'], CURLOPT_SSL_VERIFYPEER, false); | |
| if ($this->cookies == true) | |
| { | |
| curl_setopt($download['ch'], CURLOPT_COOKIEFILE, $this->cookie_file); | |
| curl_setopt($download['ch'], CURLOPT_COOKIEJAR, $this->cookie_file); | |
| } | |
| if ($this->fragProxy and $this->proxy) | |
| $this->setProxy($download['ch'], $this->proxy); | |
| if ($this->maxSpeed > 0) | |
| curl_setopt($download['ch'], CURLOPT_MAX_RECV_SPEED_LARGE, $this->maxSpeed); | |
| curl_multi_add_handle($this->mh, $download['ch']); | |
| do | |
| { | |
| $this->mrc = curl_multi_exec($this->mh, $this->active); | |
| } while ($this->mrc == CURLM_CALL_MULTI_PERFORM); | |
| return true; | |
| } | |
| function checkDownloads() | |
| { | |
| if (isset($this->mh)) | |
| { | |
| curl_multi_select($this->mh); | |
| $this->mrc = curl_multi_exec($this->mh, $this->active); | |
| if ($this->mrc != CURLM_OK) | |
| return false; | |
| while ($info = curl_multi_info_read($this->mh)) | |
| { | |
| foreach ($this->ch as $download) | |
| if ($download['ch'] == $info['handle']) | |
| break; | |
| $array['id'] = $download['id']; | |
| $array['url'] = $download['url']; | |
| $info = curl_getinfo($download['ch']); | |
| if ($info['http_code'] == 0) | |
| { | |
| /* if curl fails due to network connectivity issues or some other reason it's * | |
| * better to add some delay before next try to avoid busy loop. */ | |
| LogDebug("Fragment " . $download['id'] . ": " . curl_error($download['ch'])); | |
| usleep(1000000); | |
| $array['status'] = false; | |
| $array['response'] = ""; | |
| } | |
| else if ($info['http_code'] == 200) | |
| { | |
| if ($info['size_download'] >= $info['download_content_length']) | |
| { | |
| $array['status'] = $info['http_code']; | |
| $array['response'] = curl_multi_getcontent($download['ch']); | |
| } | |
| else | |
| { | |
| $array['status'] = false; | |
| $array['response'] = ""; | |
| } | |
| } | |
| else | |
| { | |
| $array['status'] = $info['http_code']; | |
| $array['response'] = curl_multi_getcontent($download['ch']); | |
| } | |
| $downloads[] = $array; | |
| curl_multi_remove_handle($this->mh, $download['ch']); | |
| curl_close($download['ch']); | |
| unset($this->ch[$download['id']]); | |
| } | |
| if (isset($downloads) and (count($downloads) > 0)) | |
| return $downloads; | |
| } | |
| return false; | |
| } | |
| function stopDownloads() | |
| { | |
| if (isset($this->mh)) | |
| { | |
| if (isset($this->ch)) | |
| { | |
| foreach ($this->ch as $download) | |
| { | |
| curl_multi_remove_handle($this->mh, $download['ch']); | |
| curl_close($download['ch']); | |
| } | |
| unset($this->ch); | |
| } | |
| curl_multi_close($this->mh); | |
| unset($this->mh); | |
| } | |
| } | |
| function error($error) | |
| { | |
| LogError("cURL Error : $error"); | |
| } | |
| } | |
| class AkamaiDecryptor | |
| { | |
| var $ecmID, $ecmTimestamp, $ecmVersion, $kdfVersion, $dccAccReserved, $prevEcmID; | |
| var $aes_cbc, $debug, $decryptBytes, $decryptorTest, $encryptor, $lastKeyUrl, $sessionID, $sessionKey, $sessionKeyUrl; | |
| var $packetIV, $packetKey, $packetSalt, $saltAesKey; | |
| function __construct() | |
| { | |
| if (extension_loaded("openssl")) | |
| { | |
| $this->encryptor = "openssl"; | |
| } | |
| else if (extension_loaded("mcrypt")) | |
| { | |
| $this->encryptor = "mcrypt"; | |
| $this->aes_cbc = mcrypt_module_open('rijndael-128', '', 'cbc', ''); | |
| } | |
| else | |
| { | |
| $this->encryptor = false; | |
| LogInfo("You need to install either 'mcrypt' or 'openssl' extension to decrypt some encrypted streams."); | |
| } | |
| $this->debug = false; | |
| $this->decryptorTest = false; | |
| $this->lastKeyUrl = ""; | |
| $this->sessionID = ""; | |
| $this->sessionKey = ""; | |
| $this->sessionKeyUrl = ""; | |
| $this->InitDecryptor(); | |
| } | |
| function InitDecryptor() | |
| { | |
| $this->dccAccReserved = null; | |
| $this->decryptBytes = 0; | |
| $this->ecmID = null; | |
| $this->ecmTimestamp = null; | |
| $this->ecmVersion = null; | |
| $this->kdfVersion = null; | |
| $this->packetIV = null; | |
| $this->prevEcmID = null; | |
| $this->saltAesKey = null; | |
| } | |
| function AesDecrypt($cipherData, $key, $iv) | |
| { | |
| $decrypted = ""; | |
| if ($this->encryptor == "openssl") | |
| { | |
| $decrypted = openssl_decrypt(base64_encode($cipherData), "aes-128-cbc", $key, OPENSSL_ZERO_PADDING, $iv); | |
| } | |
| else if ($this->encryptor == "mcrypt") | |
| { | |
| mcrypt_generic_init($this->aes_cbc, $key, $iv); | |
| $decrypted = mdecrypt_generic($this->aes_cbc, $cipherData); | |
| mcrypt_generic_deinit($this->aes_cbc); | |
| } | |
| else | |
| { | |
| LogError("Failed to find suitable decryption library"); | |
| } | |
| return $decrypted; | |
| } | |
| function KDF() | |
| { | |
| $debug = $this->debug; | |
| if ($this->decryptorTest) | |
| $debug = false; | |
| // KDF constants | |
| $hmacKey = unhexlify("3b27bdc9e00fd5995d60a1ee0aa057a9f1416ed085b21762110f1c2204ddf80ec8caab003070fd43baafdde27aeb3194ece5c1adff406a51185eb5dd7300c058"); | |
| $hmacData1 = unhexlify("d1ba6371c56ce6b498f1718228b0aa112f24a47bcad757a1d0b3f4c2b8bd637cb8080d9c8e7855b36a85722a60552a6c00"); | |
| $hmacData2 = unhexlify("d1ba6371c56ce6b498f1718228b0aa112f24a47bcad757a1d0b3f4c2b8bd637cb8080d9c8e7855b36a85722a60552a6c01"); | |
| // Decrypt packet salt | |
| if ($this->ecmID !== $this->prevEcmID) | |
| { | |
| $saltHmacKey = hash_hmac("sha1", $this->sessionKey . $this->packetIV, $hmacKey, true); | |
| LogDebug("SaltHmacKey : " . hexlify($saltHmacKey), $debug); | |
| $this->saltAesKey = substr(hash_hmac("sha1", $hmacData1, $saltHmacKey, true), 0, 16); | |
| LogDebug("SaltAesKey : " . hexlify($this->saltAesKey), $debug); | |
| $this->prevEcmID = $this->ecmID; | |
| } | |
| LogDebug("EncryptedSalt: " . hexlify($this->packetSalt), $debug); | |
| $decryptedSalt = $this->AesDecrypt($this->packetSalt, $this->saltAesKey, $this->packetIV); | |
| LogDebug("DecryptedSalt: " . hexlify($decryptedSalt), $debug); | |
| $this->decryptBytes = ReadInt32($decryptedSalt, 0); | |
| LogDebug("DecryptBytes : " . $this->decryptBytes, $debug); | |
| $decryptedSalt = substr($decryptedSalt, 4, 16); | |
| LogDebug("DecryptedSalt: " . hexlify($decryptedSalt), $debug); | |
| // Generate final packet decryption key | |
| $finalHmacKey = hash_hmac("sha1", $decryptedSalt, $hmacKey, true); | |
| LogDebug("FinalHmacKey : " . hexlify($finalHmacKey), $debug); | |
| $this->packetKey = substr(hash_hmac("sha1", $hmacData2, $finalHmacKey, true), 0, 16); | |
| LogDebug("PacketKey : " . hexlify($this->packetKey), $debug); | |
| } | |
| function Decrypt($data, $pos, $opt = array()) | |
| { | |
| /** @var cURL $cc */ | |
| $auth = ""; | |
| $baseUrl = ""; | |
| $cc = null; | |
| extract($opt, EXTR_IF_EXISTS); | |
| $debug = $this->debug; | |
| if ($this->decryptorTest) | |
| $debug = false; | |
| LogDebug("\n----- Akamai Decryption Start -----", $debug); | |
| // Parse packet header | |
| $byte = ReadByte($data, $pos++); | |
| $this->ecmVersion = $byte >> 4; | |
| if ($this->ecmVersion != 11) | |
| $this->ecmVersion = $byte; | |
| $this->ecmID = ReadInt32($data, $pos); | |
| $this->ecmTimestamp = ReadInt32($data, $pos + 4); | |
| $this->kdfVersion = ReadInt16($data, $pos + 8); | |
| $pos += 10; | |
| $this->dccAccReserved = ReadByte($data, $pos++); | |
| LogDebug("ECM Version : " . $this->ecmVersion . ", ECM ID: " . $this->ecmID . ", ECM Timestamp: " . $this->ecmTimestamp . ", KDF Version: " . $this->kdfVersion . ", DccAccReserved: " . $this->dccAccReserved, $debug); | |
| $byte = ReadByte($data, $pos++); | |
| $iv = (($byte & 2) > 0) ? true : false; | |
| $key = (($byte & 4) > 0) ? true : false; | |
| if ($iv) | |
| { | |
| $this->packetIV = substr($data, $pos, 16); | |
| $pos += 16; | |
| LogDebug("PacketIV : " . hexlify($this->packetIV), $debug); | |
| } | |
| if ($key) | |
| { | |
| $this->sessionKeyUrl = ReadString($data, $pos); | |
| LogDebug("SessionKeyUrl: " . $this->sessionKeyUrl, $debug); | |
| $keyPath = substr($this->sessionKeyUrl, strrpos($this->sessionKeyUrl, '/')); | |
| $keyUrl = JoinUrl($baseUrl, $keyPath) . $auth; | |
| // Download key file if required | |
| if ($this->sessionKeyUrl !== $this->lastKeyUrl) | |
| { | |
| if (!$baseUrl and !$this->sessionKey) | |
| LogError("Unable to download session key without manifest url. you must specify it manually using 'adkey' switch."); | |
| else | |
| { | |
| if ($baseUrl) | |
| { | |
| LogDebug("Downloading new session key from " . $keyUrl, $debug); | |
| $status = $cc->get($keyUrl); | |
| if ($status == 200) | |
| { | |
| $this->sessionID = "_" . substr($keyPath, strlen("/key_")); | |
| $this->sessionKey = $cc->response; | |
| } | |
| else | |
| { | |
| LogDebug("Failed to download new session key, Status: " . $status, $debug); | |
| $this->sessionID = ""; | |
| } | |
| } | |
| $this->lastKeyUrl = $this->sessionKeyUrl; | |
| if (!$this->sessionKey) | |
| LogError("Failed to download akamai session decryption key"); | |
| LogInfo("SessionKey: " . hexlify($this->sessionKey)); | |
| } | |
| } | |
| } | |
| LogDebug("SessionKey : " . hexlify($this->sessionKey), $debug); | |
| $reserved = ReadByte($data, $pos++); | |
| $this->packetSalt = substr($data, $pos, 32); | |
| $pos += 32; | |
| $reservedBlock1 = substr($data, $pos, 20); | |
| $reservedBlock2 = substr($data, $pos + 20, 20); | |
| $pos += 40; | |
| LogDebug("ReservedByte : " . $reserved . ", ReservedBlock1: " . hexlify($reservedBlock1) . ", ReservedBlock2: " . hexlify($reservedBlock2), $debug); | |
| // Generate packet decryption key | |
| if (!$this->sessionKey) | |
| LogError("Fragments can't be decrypted properly without corresponding session key.", 2); | |
| $this->KDF(); | |
| // Decrypt packet data | |
| $encryptedData = substr($data, $pos); | |
| LogDebug("EncryptedData: " . hexlify(substr($encryptedData, 0, 64)), $debug); | |
| $lastBlockData = substr($encryptedData, $this->decryptBytes); | |
| $encryptedData = substr($encryptedData, 0, $this->decryptBytes); | |
| $decryptedData = ""; | |
| if ($this->decryptBytes > 0) | |
| $decryptedData = $this->AesDecrypt($encryptedData, $this->packetKey, $this->packetIV); | |
| $decryptedData .= $lastBlockData; | |
| LogDebug("DecryptedData: " . hexlify(substr($decryptedData, 0, 64)), $debug); | |
| LogDebug("----- Akamai Decryption End -----\n", $debug); | |
| return $decryptedData; | |
| } | |
| } | |
| class F4F | |
| { | |
| var $audio, $auth, $baseFilename, $baseTS, $baseUrl, $bootstrapUrl, $debug, $decoderTest, $duration, $fileCount, $filesize, $fixWindow; | |
| var $format, $live, $media, $metadata, $outDir, $outFile, $parallel, $play, $processed, $quality, $rename, $sessionID, $srt, $video; | |
| var $prevTagSize, $tagHeaderLen; | |
| var $segTable, $fragTable, $frags, $fragCount, $lastFrag, $fragUrl, $discontinuity; | |
| var $negTS, $prevAudioTS, $prevVideoTS, $pAudioTagLen, $pVideoTagLen, $pAudioTagPos, $pVideoTagPos; | |
| var $prevAVC_Header, $prevAAC_Header, $AVC_HeaderWritten, $AAC_HeaderWritten; | |
| function __construct() | |
| { | |
| $this->auth = ""; | |
| $this->baseFilename = ""; | |
| $this->baseUrl = ""; | |
| $this->bootstrapUrl = ""; | |
| $this->debug = false; | |
| $this->decoderTest = false; | |
| $this->fileCount = 1; | |
| $this->fixWindow = 1000; | |
| $this->format = ""; | |
| $this->live = false; | |
| $this->metadata = true; | |
| $this->outDir = ""; | |
| $this->outFile = ""; | |
| $this->parallel = 8; | |
| $this->play = false; | |
| $this->processed = false; | |
| $this->quality = "high"; | |
| $this->rename = false; | |
| $this->sessionID = ""; | |
| $this->segTable = array(); | |
| $this->fragTable = array(); | |
| $this->segStart = false; | |
| $this->fragStart = false; | |
| $this->frags = array(); | |
| $this->fragCount = 0; | |
| $this->lastFrag = 0; | |
| $this->discontinuity = ""; | |
| $this->InitDecoder(); | |
| } | |
| function InitDecoder() | |
| { | |
| $this->audio = false; | |
| $this->duration = 0; | |
| $this->filesize = 0; | |
| $this->video = false; | |
| $this->prevTagSize = 4; | |
| $this->tagHeaderLen = 11; | |
| $this->baseTS = INVALID_TIMESTAMP; | |
| $this->negTS = INVALID_TIMESTAMP; | |
| $this->prevAudioTS = INVALID_TIMESTAMP; | |
| $this->prevVideoTS = INVALID_TIMESTAMP; | |
| $this->pAudioTagLen = 0; | |
| $this->pVideoTagLen = 0; | |
| $this->pAudioTagPos = 0; | |
| $this->pVideoTagPos = 0; | |
| $this->prevAVC_Header = false; | |
| $this->prevAAC_Header = false; | |
| $this->AVC_HeaderWritten = false; | |
| $this->AAC_HeaderWritten = false; | |
| } | |
| function GetManifest(cURL $cc, $manifest) | |
| { | |
| $status = $cc->get($manifest); | |
| if ($status == 403) | |
| LogError("Access Denied! Unable to download the manifest."); | |
| else if ($status != 200) | |
| LogError("Unable to download the manifest"); | |
| $xml = preg_replace('/&(?!amp;)/', '&', trim($cc->response)); | |
| $xml = simplexml_load_string($xml); | |
| if (!$xml) | |
| LogError("Failed to load xml"); | |
| $namespace = $xml->getDocNamespaces(); | |
| $namespace = $namespace['']; | |
| $xml->registerXPathNamespace("ns", $namespace); | |
| return $xml; | |
| } | |
| function ParseManifest(cURL $cc, $parentManifest) | |
| { | |
| /** @var SimpleXMLElement $xml */ | |
| LogInfo("Processing manifest info...."); | |
| $xml = $this->GetManifest($cc, $parentManifest); | |
| // Extract baseUrl from manifest url | |
| $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); | |
| if (isset($baseUrl[0])) | |
| $baseUrl = GetString($baseUrl[0]); | |
| else | |
| { | |
| $baseUrl = $parentManifest; | |
| if (strpos($baseUrl, '?') !== false) | |
| $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')); | |
| $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/')); | |
| } | |
| $childManifests = array(); | |
| $url = $xml->xpath("/ns:manifest/ns:media[@*]"); | |
| if (isset($url[0]['href'])) | |
| { | |
| $count = 1; | |
| foreach ($url as $childManifest) | |
| { | |
| if (isset($childManifest['bitrate'])) | |
| $bitrate = floor(GetString($childManifest['bitrate'])); | |
| else | |
| $bitrate = $count++; | |
| $entry =& $childManifests[$bitrate]; | |
| $entry['bitrate'] = $bitrate; | |
| $entry['url'] = AbsoluteUrl($baseUrl, GetString($childManifest['href'])); | |
| $entry['xml'] = $this->GetManifest($cc, $entry['url']); | |
| } | |
| unset($entry, $childManifest); | |
| } | |
| else | |
| { | |
| $childManifests[0]['bitrate'] = 0; | |
| $childManifests[0]['url'] = $parentManifest; | |
| $childManifests[0]['xml'] = $xml; | |
| } | |
| $count = 1; | |
| foreach ($childManifests as $childManifest) | |
| { | |
| $xml = $childManifest['xml']; | |
| // Extract baseUrl from manifest url | |
| $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); | |
| if (isset($baseUrl[0])) | |
| $baseUrl = GetString($baseUrl[0]); | |
| else | |
| { | |
| $baseUrl = $childManifest['url']; | |
| if (strpos($baseUrl, '?') !== false) | |
| $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')); | |
| $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/')); | |
| } | |
| $streams = $xml->xpath("/ns:manifest/ns:media"); | |
| foreach ($streams as $stream) | |
| { | |
| $array = array(); | |
| foreach ($stream->attributes() as $k => $v) | |
| $array[strtolower($k)] = GetString($v); | |
| $array['metadata'] = GetString($stream->{'metadata'}); | |
| $stream = $array; | |
| if (isset($stream['bitrate'])) | |
| { | |
| if ($stream['bitrate'] > $childManifest['bitrate']) | |
| $bitrate = floor($stream['bitrate']); | |
| else | |
| $bitrate = $childManifest['bitrate']; | |
| } | |
| else if ($childManifest['bitrate'] > 0) | |
| $bitrate = $childManifest['bitrate']; | |
| else | |
| $bitrate = $count++; | |
| while (isset($this->media[$bitrate])) | |
| $bitrate++; | |
| $streamId = isset($stream[strtolower('streamId')]) ? $stream[strtolower('streamId')] : ""; | |
| $mediaEntry =& $this->media[$bitrate]; | |
| $mediaEntry['baseUrl'] = $baseUrl; | |
| $mediaEntry['url'] = preg_replace('/ /', '%20', $stream['url']); | |
| if (isRtmpUrl($mediaEntry['baseUrl']) or isRtmpUrl($mediaEntry['url'])) | |
| LogError("Provided manifest is not a valid HDS manifest"); | |
| // Use embedded auth information when available | |
| $idx = strpos($mediaEntry['url'], '?'); | |
| if ($idx !== false) | |
| { | |
| $mediaEntry['queryString'] = substr($mediaEntry['url'], $idx); | |
| $mediaEntry['url'] = substr($mediaEntry['url'], 0, $idx); | |
| if (strlen($this->auth) != 0 and strcmp($this->auth, $mediaEntry['queryString']) != 0) | |
| LogDebug("Manifest overrides 'auth': " . $mediaEntry['queryString']); | |
| } | |
| else | |
| $mediaEntry['queryString'] = $this->auth; | |
| if (isset($stream[strtolower('bootstrapInfoId')])) | |
| $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo[@id='" . $stream[strtolower('bootstrapInfoId')] . "']"); | |
| else | |
| $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo"); | |
| if (isset($bootstrap[0]['url'])) | |
| { | |
| $mediaEntry['bootstrapUrl'] = AbsoluteUrl($mediaEntry['baseUrl'], GetString($bootstrap[0]['url'])); | |
| if (strpos($mediaEntry['bootstrapUrl'], '?') === false) | |
| $mediaEntry['bootstrapUrl'] .= $this->auth; | |
| } | |
| else | |
| $mediaEntry['bootstrap'] = base64_decode(GetString($bootstrap[0])); | |
| if (isset($stream['metadata'])) | |
| $mediaEntry['metadata'] = base64_decode($stream['metadata']); | |
| else | |
| $mediaEntry['metadata'] = ""; | |
| } | |
| unset($mediaEntry, $childManifest); | |
| } | |
| // Available qualities | |
| $bitrates = array(); | |
| if (!count($this->media)) | |
| LogError("No media entry found"); | |
| krsort($this->media, SORT_NUMERIC); | |
| LogDebug("Manifest Entries:\n"); | |
| LogDebug(sprintf(" %-8s%s", "Bitrate", "URL")); | |
| for ($i = 0; $i < count($this->media); $i++) | |
| { | |
| $key = KeyName($this->media, $i); | |
| $bitrates[] = $key; | |
| LogDebug(sprintf(" %-8d%s", $key, $this->media[$key]['url'])); | |
| } | |
| LogDebug(""); | |
| LogInfo("Quality Selection:\n Available: " . implode(' ', $bitrates)); | |
| // Quality selection | |
| $key = $this->quality; | |
| if (is_numeric($key) and isset($this->media[$key])) | |
| $this->media = $this->media[$key]; | |
| else | |
| { | |
| $this->quality = strtolower($this->quality); | |
| switch ($this->quality) | |
| { | |
| case "low": | |
| $this->quality = 2; | |
| break; | |
| case "medium": | |
| $this->quality = 1; | |
| break; | |
| default: | |
| $this->quality = 0; | |
| } | |
| while ($this->quality >= 0) | |
| { | |
| $key = KeyName($this->media, $this->quality); | |
| if ($key !== NULL) | |
| { | |
| $this->media = $this->media[$key]; | |
| break; | |
| } | |
| else | |
| $this->quality -= 1; | |
| } | |
| } | |
| LogInfo(" Selected : " . $key); | |
| // Parse initial bootstrap info | |
| $this->baseUrl = $this->media['baseUrl']; | |
| if (isset($this->media['bootstrapUrl'])) | |
| { | |
| $this->bootstrapUrl = $this->media['bootstrapUrl']; | |
| $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); | |
| } | |
| else | |
| { | |
| $bootstrapInfo = $this->media['bootstrap']; | |
| ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
| if ($boxType == "abst") | |
| $this->ParseBootstrapBox($bootstrapInfo, $pos); | |
| else | |
| LogError("Failed to parse bootstrap info"); | |
| } | |
| } | |
| function UpdateBootstrapInfo(cURL $cc, $bootstrapUrl) | |
| { | |
| $fragNum = $this->fragCount; | |
| $retries = 0; | |
| // Backup original headers and add no-cache directive for fresh bootstrap info | |
| $headers = $cc->headers; | |
| $cc->headers[] = "Cache-Control: no-cache"; | |
| $cc->headers[] = "Pragma: no-cache"; | |
| while ($retries < 30) | |
| { | |
| $bootstrapPos = 0; | |
| LogDebug("Updating bootstrap info, Available fragments: " . $this->fragCount); | |
| $status = $cc->get($bootstrapUrl); | |
| if ($status == 200) | |
| { | |
| $bootstrapInfo = $cc->response; | |
| ReadBoxHeader($bootstrapInfo, $bootstrapPos, $boxType, $boxSize); | |
| if ($boxType == "abst") | |
| $this->ParseBootstrapBox($bootstrapInfo, $bootstrapPos); | |
| else | |
| LogError("Failed to parse bootstrap info"); | |
| LogDebug("Update complete, Available fragments: " . $this->fragCount); | |
| } | |
| else | |
| LogInfo("Failed to refresh bootstrap info, Status: " . $status); | |
| if ($this->fragCount <= $fragNum) | |
| { | |
| LogInfo("Updating bootstrap info, Retries: " . ++$retries, true); | |
| usleep(4000000); | |
| } | |
| else | |
| break; | |
| } | |
| // Restore original headers | |
| $cc->headers = $headers; | |
| } | |
| function ParseBootstrapBox($bootstrapInfo, $pos) | |
| { | |
| $version = ReadByte($bootstrapInfo, $pos); | |
| $flags = ReadInt24($bootstrapInfo, $pos + 1); | |
| $bootstrapVersion = ReadInt32($bootstrapInfo, $pos + 4); | |
| $byte = ReadByte($bootstrapInfo, $pos + 8); | |
| $profile = ($byte & 0xC0) >> 6; | |
| if (($byte & 0x20) >> 5) | |
| { | |
| $this->live = true; | |
| $this->metadata = false; | |
| } | |
| $update = ($byte & 0x10) >> 4; | |
| if (!$update) | |
| { | |
| $this->segTable = array(); | |
| $this->fragTable = array(); | |
| } | |
| $timescale = ReadInt32($bootstrapInfo, $pos + 9); | |
| $currentMediaTime = ReadInt64($bootstrapInfo, $pos + 13); | |
| $smpteTimeCodeOffset = ReadInt64($bootstrapInfo, $pos + 21); | |
| $pos += 29; | |
| $movieIdentifier = ReadString($bootstrapInfo, $pos); | |
| $serverEntryCount = ReadByte($bootstrapInfo, $pos++); | |
| for ($i = 0; $i < $serverEntryCount; $i++) | |
| $serverEntryTable[$i] = ReadString($bootstrapInfo, $pos); | |
| $qualityEntryCount = ReadByte($bootstrapInfo, $pos++); | |
| for ($i = 0; $i < $qualityEntryCount; $i++) | |
| $qualityEntryTable[$i] = ReadString($bootstrapInfo, $pos); | |
| $drmData = ReadString($bootstrapInfo, $pos); | |
| $metadata = ReadString($bootstrapInfo, $pos); | |
| $segRunTableCount = ReadByte($bootstrapInfo, $pos++); | |
| $segTable = array(); | |
| LogDebug(sprintf("%s:", "Segment Tables")); | |
| for ($i = 0; $i < $segRunTableCount; $i++) | |
| { | |
| LogDebug(sprintf("\nTable %d:", $i + 1)); | |
| ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
| if ($boxType == "asrt") | |
| $segTable[$i] = $this->ParseAsrtBox($bootstrapInfo, $pos); | |
| $pos += $boxSize; | |
| } | |
| $fragRunTableCount = ReadByte($bootstrapInfo, $pos++); | |
| $fragTable = array(); | |
| LogDebug(sprintf("%s:", "Fragment Tables")); | |
| for ($i = 0; $i < $fragRunTableCount; $i++) | |
| { | |
| LogDebug(sprintf("\nTable %d:", $i + 1)); | |
| ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
| if ($boxType == "afrt") | |
| $fragTable[$i] = $this->ParseAfrtBox($bootstrapInfo, $pos); | |
| $pos += $boxSize; | |
| } | |
| $this->segTable = array_replace($this->segTable, $segTable[0]); | |
| $this->fragTable = array_replace($this->fragTable, $fragTable[0]); | |
| $this->ParseSegAndFragTable(); | |
| } | |
| function ParseAsrtBox($asrt, $pos) | |
| { | |
| $segTable = array(); | |
| $version = ReadByte($asrt, $pos); | |
| $flags = ReadInt24($asrt, $pos + 1); | |
| $qualityEntryCount = ReadByte($asrt, $pos + 4); | |
| $pos += 5; | |
| for ($i = 0; $i < $qualityEntryCount; $i++) | |
| $qualitySegmentUrlModifiers[$i] = ReadString($asrt, $pos); | |
| $segCount = ReadInt32($asrt, $pos); | |
| $pos += 4; | |
| LogDebug(sprintf(" %-8s%-10s", "Number", "Fragments")); | |
| for ($i = 0; $i < $segCount; $i++) | |
| { | |
| $firstSegment = ReadInt32($asrt, $pos); | |
| $segEntry =& $segTable[$firstSegment]; | |
| $segEntry['firstSegment'] = $firstSegment; | |
| $segEntry['fragmentsPerSegment'] = ReadInt32($asrt, $pos + 4); | |
| if ($segEntry['fragmentsPerSegment'] & 0x80000000) | |
| $segEntry['fragmentsPerSegment'] = 0; | |
| $pos += 8; | |
| } | |
| unset($segEntry); | |
| foreach ($segTable as $segEntry) | |
| LogDebug(sprintf(" %-8s%-10s", $segEntry['firstSegment'], $segEntry['fragmentsPerSegment'])); | |
| LogDebug(""); | |
| return $segTable; | |
| } | |
| function ParseAfrtBox($afrt, $pos) | |
| { | |
| $fragTable = array(); | |
| $version = ReadByte($afrt, $pos); | |
| $flags = ReadInt24($afrt, $pos + 1); | |
| $timescale = ReadInt32($afrt, $pos + 4); | |
| $qualityEntryCount = ReadByte($afrt, $pos + 8); | |
| $pos += 9; | |
| for ($i = 0; $i < $qualityEntryCount; $i++) | |
| $qualitySegmentUrlModifiers[$i] = ReadString($afrt, $pos); | |
| $fragEntries = ReadInt32($afrt, $pos); | |
| $pos += 4; | |
| LogDebug(sprintf(" %-12s%-16s%-16s%-16s", "Number", "Timestamp", "Duration", "Discontinuity")); | |
| for ($i = 0; $i < $fragEntries; $i++) | |
| { | |
| $firstFragment = ReadInt32($afrt, $pos); | |
| $fragEntry =& $fragTable[$firstFragment]; | |
| $fragEntry['firstFragment'] = $firstFragment; | |
| $fragEntry['firstFragmentTimestamp'] = ReadInt64($afrt, $pos + 4); | |
| $fragEntry['fragmentDuration'] = ReadInt32($afrt, $pos + 12); | |
| $fragEntry['discontinuityIndicator'] = ""; | |
| $pos += 16; | |
| if ($fragEntry['fragmentDuration'] == 0) | |
| $fragEntry['discontinuityIndicator'] = ReadByte($afrt, $pos++); | |
| } | |
| unset($fragEntry); | |
| foreach ($fragTable as $fragEntry) | |
| LogDebug(sprintf(" %-12s%-16s%-16s%-16s", $fragEntry['firstFragment'], $fragEntry['firstFragmentTimestamp'], $fragEntry['fragmentDuration'], $fragEntry['discontinuityIndicator'])); | |
| LogDebug(""); | |
| return $fragTable; | |
| } | |
| function ParseSegAndFragTable() | |
| { | |
| $firstSegment = reset($this->segTable); | |
| $lastSegment = end($this->segTable); | |
| $firstFragment = reset($this->fragTable); | |
| $lastFragment = end($this->fragTable); | |
| // Check if live stream is still live | |
| if (($lastFragment['fragmentDuration'] == 0) and ($lastFragment['discontinuityIndicator'] == 0)) | |
| { | |
| $this->live = false; | |
| array_pop($this->fragTable); | |
| $lastFragment = end($this->fragTable); | |
| } | |
| // Count total fragments by adding all entries in compactly coded segment table | |
| $invalidFragCount = false; | |
| $prev = reset($this->segTable); | |
| $this->fragCount = $prev['fragmentsPerSegment']; | |
| while ($current = next($this->segTable)) | |
| { | |
| $this->fragCount += ($current['firstSegment'] - $prev['firstSegment'] - 1) * $prev['fragmentsPerSegment']; | |
| $this->fragCount += $current['fragmentsPerSegment']; | |
| $prev = $current; | |
| } | |
| if (!($this->fragCount & 0x80000000)) | |
| $this->fragCount += $firstFragment['firstFragment'] - 1; | |
| if ($this->fragCount & 0x80000000) | |
| { | |
| $this->fragCount = 0; | |
| $invalidFragCount = true; | |
| } | |
| if ($this->fragCount < $lastFragment['firstFragment']) | |
| $this->fragCount = $lastFragment['firstFragment']; | |
| // Determine starting segment and fragment | |
| if ($this->segStart === false) | |
| { | |
| if ($this->live) | |
| $this->segStart = $lastSegment['firstSegment']; | |
| else | |
| $this->segStart = $firstSegment['firstSegment']; | |
| if ($this->segStart < 1) | |
| $this->segStart = 1; | |
| } | |
| if ($this->fragStart === false) | |
| { | |
| if ($this->live and !$invalidFragCount) | |
| $this->fragStart = $this->fragCount - 2; | |
| else | |
| $this->fragStart = $firstFragment['firstFragment'] - 1; | |
| if ($this->fragStart < 0) | |
| $this->fragStart = 0; | |
| } | |
| } | |
| function GetSegmentFromFragment($fragNum) | |
| { | |
| $firstSegment = reset($this->segTable); | |
| $lastSegment = end($this->segTable); | |
| $firstFragment = reset($this->fragTable); | |
| $lastFragment = end($this->fragTable); | |
| if (count($this->segTable) == 1) | |
| return $firstSegment['firstSegment']; | |
| else | |
| { | |
| $prev = $firstSegment['firstSegment']; | |
| $start = $firstFragment['firstFragment']; | |
| for ($i = $firstSegment['firstSegment']; $i <= $lastSegment['firstSegment']; $i++) | |
| { | |
| if (isset($this->segTable[$i])) | |
| $seg = $this->segTable[$i]; | |
| else | |
| $seg = $prev; | |
| $end = $start + $seg['fragmentsPerSegment']; | |
| if (($fragNum >= $start) and ($fragNum < $end)) | |
| return $i; | |
| $prev = $seg; | |
| $start = $end; | |
| } | |
| } | |
| return $lastSegment['firstSegment']; | |
| } | |
| function DownloadFragments($manifest, $opt = array()) | |
| { | |
| /** @var cURL $cc */ | |
| $cc = null; | |
| $start = 0; | |
| extract($opt, EXTR_IF_EXISTS); | |
| $this->ParseManifest($cc, $manifest); | |
| $segNum = $this->segStart; | |
| $fragNum = $this->fragStart; | |
| if ($start) | |
| { | |
| $segNum = $this->GetSegmentFromFragment($start); | |
| $fragNum = $start - 1; | |
| $this->segStart = $segNum; | |
| $this->fragStart = $fragNum; | |
| } | |
| $this->lastFrag = $fragNum; | |
| $firstFragment = reset($this->fragTable); | |
| LogInfo(sprintf("Fragments Total: %s, First: %s, Start: %s, Parallel: %s", $this->fragCount, $firstFragment['firstFragment'], $fragNum + 1, $this->parallel)); | |
| // Extract baseFilename | |
| $this->baseFilename = $this->media['url']; | |
| if (substr($this->baseFilename, -1) == '/') | |
| $this->baseFilename = substr($this->baseFilename, 0, -1); | |
| $this->baseFilename = RemoveExtension($this->baseFilename); | |
| $lastSlash = strrpos($this->baseFilename, '/'); | |
| if ($lastSlash !== false) | |
| $this->baseFilename = substr($this->baseFilename, $lastSlash + 1); | |
| if (strpos($manifest, '?')) | |
| $manifestHash = md5(substr($manifest, 0, strpos($manifest, '?'))); | |
| else | |
| $manifestHash = md5($manifest); | |
| if (strlen($this->baseFilename) > 32) | |
| $this->baseFilename = md5($this->baseFilename); | |
| $this->baseFilename = $manifestHash . "_" . $this->baseFilename . "_Seg" . $segNum . "-Frag"; | |
| if ($fragNum >= $this->fragCount) | |
| LogError("No fragment available for downloading"); | |
| $this->fragUrl = AbsoluteUrl($this->baseUrl, $this->media['url']); | |
| LogDebug("Base Fragment Url:\n" . $this->fragUrl . "\n"); | |
| LogDebug("Downloading Fragments:\n"); | |
| $firstFrag = true; | |
| $status = false; | |
| while (($fragNum < $this->fragCount) or $cc->active) | |
| { | |
| while ((count($cc->ch) < $this->parallel) and ($fragNum < $this->fragCount)) | |
| { | |
| // Download first fragment to determine initial parameters | |
| if ($firstFrag and (count($cc->ch) > 0)) | |
| break; | |
| $frag = array(); | |
| $fragNum = $fragNum + 1; | |
| $frag['id'] = $fragNum; | |
| LogInfo("Downloading " . $fragNum . "/" . $this->fragCount . " fragments", true); | |
| if (in_array_field($fragNum, "firstFragment", $this->fragTable, true)) | |
| $this->discontinuity = value_in_array_field($fragNum, "firstFragment", "discontinuityIndicator", $this->fragTable, true); | |
| else | |
| { | |
| $closest = reset($this->fragTable); | |
| $closest = $closest['firstFragment']; | |
| while ($current = next($this->fragTable)) | |
| { | |
| if ($current['firstFragment'] < $fragNum) | |
| $closest = $current['firstFragment']; | |
| else | |
| break; | |
| } | |
| $this->discontinuity = value_in_array_field($closest, "firstFragment", "discontinuityIndicator", $this->fragTable, true); | |
| } | |
| if ($this->discontinuity !== "") | |
| { | |
| LogDebug("Skipping fragment " . $fragNum . " due to discontinuity, Type: " . $this->discontinuity); | |
| $frag['response'] = false; | |
| $this->rename = true; | |
| } | |
| else if (file_exists($this->baseFilename . $fragNum)) | |
| { | |
| LogDebug("Fragment " . $fragNum . " is already downloaded"); | |
| $frag['response'] = file_get_contents($this->baseFilename . $fragNum); | |
| } | |
| if (isset($frag['response'])) | |
| { | |
| $status = $this->WriteFragment($frag, $opt); | |
| if ($status === STOP_PROCESSING) | |
| break 2; | |
| else | |
| continue; | |
| } | |
| LogDebug("Adding fragment " . $fragNum . " to download queue"); | |
| $segNum = $this->GetSegmentFromFragment($fragNum); | |
| $cc->addDownload($this->fragUrl . "Seg" . $segNum . "-Frag" . $fragNum . $this->sessionID . $this->media['queryString'], $fragNum); | |
| } | |
| $downloads = $cc->checkDownloads(); | |
| if ($downloads !== false) | |
| { | |
| for ($i = 0; $i < count($downloads); $i++) | |
| { | |
| $frag = array(); | |
| $download = $downloads[$i]; | |
| $frag['id'] = $download['id']; | |
| if ($download['status'] == 200) | |
| { | |
| if ($this->VerifyFragment($download['response'])) | |
| { | |
| LogDebug("Fragment " . $this->baseFilename . $download['id'] . " successfully downloaded"); | |
| if (!($this->live or $this->play)) | |
| file_put_contents($this->baseFilename . $download['id'], $download['response']); | |
| $frag['response'] = $download['response']; | |
| $firstFrag = false; | |
| } | |
| else | |
| { | |
| LogDebug("Fragment " . $download['id'] . " failed to verify"); | |
| LogDebug("Adding fragment " . $download['id'] . " to download queue"); | |
| $cc->addDownload($download['url'], $download['id']); | |
| } | |
| } | |
| else if ($download['status'] === false) | |
| { | |
| LogDebug("Fragment " . $download['id'] . " failed to download"); | |
| LogDebug("Adding fragment " . $download['id'] . " to download queue"); | |
| $cc->addDownload($download['url'], $download['id']); | |
| } | |
| else if ($download['status'] == 403) | |
| LogError("Access Denied! Unable to download fragments."); | |
| else if ($download['status'] == 503) | |
| { | |
| LogDebug("Fragment " . $download['id'] . " seems temporary unavailable"); | |
| LogDebug("Adding fragment " . $download['id'] . " to download queue"); | |
| $cc->addDownload($download['url'], $download['id']); | |
| } | |
| else | |
| { | |
| LogDebug("Fragment " . $download['id'] . " doesn't exist, Status: " . $download['status']); | |
| $frag['response'] = false; | |
| $this->rename = true; | |
| /* Resync with latest available fragment when we are left behind due to slow * | |
| * connection and short live window on streaming server. make sure to reset * | |
| * the last written fragment. */ | |
| if ($this->live and ($fragNum >= $this->fragCount) and ($i + 1 == count($downloads)) and !$cc->active) | |
| { | |
| LogDebug("Trying to resync with latest available fragment"); | |
| $status = $this->WriteFragment($frag, $opt); | |
| if ($status === STOP_PROCESSING) | |
| break 2; | |
| unset($frag['response']); | |
| $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); | |
| $fragNum = $this->fragCount - 1; | |
| $this->lastFrag = $fragNum; | |
| } | |
| } | |
| if (isset($frag['response'])) | |
| { | |
| $status = $this->WriteFragment($frag, $opt); | |
| if ($status === STOP_PROCESSING) | |
| break 2; | |
| } | |
| } | |
| unset($downloads, $download); | |
| } | |
| if ($this->live and ($fragNum >= $this->fragCount) and !$cc->active) | |
| $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); | |
| } | |
| if ($status === true) | |
| LogInfo(""); | |
| LogDebug("\nAll fragments downloaded successfully\n"); | |
| $cc->stopDownloads(); | |
| $this->processed = true; | |
| } | |
| function VerifyFragment(&$frag) | |
| { | |
| $fragPos = 0; | |
| $fragLen = strlen($frag); | |
| /* Some moronic servers add wrong boxSize in header causing fragment verification * | |
| * to fail so we have to fix the boxSize before processing the fragment. */ | |
| while ($fragPos < $fragLen) | |
| { | |
| ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); | |
| if ($boxType == "mdat") | |
| { | |
| $len = strlen(substr($frag, $fragPos, $boxSize)); | |
| if ($boxSize and ($len == $boxSize)) | |
| return true; | |
| else | |
| { | |
| $boxSize = $fragLen - $fragPos; | |
| WriteBoxSize($frag, $fragPos, $boxType, $boxSize); | |
| return true; | |
| } | |
| } | |
| $fragPos += $boxSize; | |
| } | |
| return false; | |
| } | |
| function DecodeFragment($frag, $fragNum, $opt = array()) | |
| { | |
| $ad = null; | |
| $flvFile = null; | |
| $flvWrite = true; | |
| extract($opt, EXTR_IF_EXISTS); | |
| $debug = $this->debug; | |
| if ($this->decoderTest) | |
| $debug = false; | |
| $flvData = ""; | |
| $flvTag = ""; | |
| $fragPos = 0; | |
| $packetTS = 0; | |
| $fragLen = strlen($frag); | |
| if (!$this->VerifyFragment($frag)) | |
| { | |
| LogInfo("Skipping fragment number " . $fragNum); | |
| return false; | |
| } | |
| while ($fragPos < $fragLen) | |
| { | |
| ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); | |
| if ($boxType == "mdat") | |
| { | |
| $fragLen = $fragPos + $boxSize; | |
| break; | |
| } | |
| $fragPos += $boxSize; | |
| } | |
| /** | |
| * Initialize Akamai decryptor | |
| * @var AkamaiDecryptor $ad | |
| */ | |
| $ad->debug = $this->debug; | |
| $ad->decryptorTest = $this->decoderTest; | |
| $ad->InitDecryptor(); | |
| LogDebug(sprintf("\nFragment %d:\n" . $this->format . "%-16s", $fragNum, "Type", "CurrentTS", "PreviousTS", "Size", "Position"), $debug); | |
| while ($fragPos < $fragLen) | |
| { | |
| $packetType = ReadByte($frag, $fragPos); | |
| $packetSize = ReadInt24($frag, $fragPos + 1); | |
| $packetTS = ReadInt24($frag, $fragPos + 4); | |
| $packetTS = $packetTS | (ReadByte($frag, $fragPos + 7) << 24); | |
| if ($packetTS & 0x80000000) | |
| $packetTS &= 0x7FFFFFFF; | |
| $totalTagLen = $this->tagHeaderLen + $packetSize + $this->prevTagSize; | |
| $tagHeader = substr($frag, $fragPos, $this->tagHeaderLen); | |
| $tagData = substr($frag, $fragPos + $this->tagHeaderLen, $packetSize); | |
| // Remove Akamai encryption | |
| if (($packetType == AKAMAI_ENC_AUDIO) or ($packetType == AKAMAI_ENC_VIDEO)) | |
| { | |
| $opt['auth'] = $this->media['queryString']; | |
| $opt['baseUrl'] = $this->baseUrl; | |
| $tagData = $ad->Decrypt($tagData, 0, $opt); | |
| $packetType = ($packetType == AKAMAI_ENC_AUDIO ? AUDIO : VIDEO); | |
| $packetSize = strlen($tagData); | |
| WriteByte($tagHeader, 0, $packetType); | |
| WriteInt24($tagHeader, 1, $packetSize); | |
| $this->sessionID = $ad->sessionID; | |
| } | |
| // Try to fix the odd timestamps and make them zero based | |
| $currentTS = $packetTS; | |
| $lastTS = $this->prevVideoTS >= $this->prevAudioTS ? $this->prevVideoTS : $this->prevAudioTS; | |
| $fixedTS = $lastTS + FRAMEFIX_STEP; | |
| if (($this->baseTS == INVALID_TIMESTAMP) and (($packetType == AUDIO) or ($packetType == VIDEO))) | |
| $this->baseTS = $packetTS; | |
| if (($this->baseTS > 1000) and ($packetTS >= $this->baseTS)) | |
| $packetTS -= $this->baseTS; | |
| if ($lastTS != INVALID_TIMESTAMP) | |
| { | |
| $timeShift = $packetTS - $lastTS; | |
| if ($timeShift > $this->fixWindow) | |
| { | |
| LogDebug("Timestamp gap detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " Timeshift=" . $timeShift, $debug); | |
| if ($this->baseTS < $packetTS) | |
| $this->baseTS += $timeShift - FRAMEFIX_STEP; | |
| else | |
| $this->baseTS = $timeShift - FRAMEFIX_STEP; | |
| $packetTS = $fixedTS; | |
| } | |
| else | |
| { | |
| $lastTS = $packetType == VIDEO ? $this->prevVideoTS : $this->prevAudioTS; | |
| if ($packetTS < ($lastTS - $this->fixWindow)) | |
| { | |
| if (($this->negTS != INVALID_TIMESTAMP) and (($packetTS + $this->negTS) < ($lastTS - $this->fixWindow))) | |
| $this->negTS = INVALID_TIMESTAMP; | |
| if ($this->negTS == INVALID_TIMESTAMP) | |
| { | |
| $this->negTS = $fixedTS - $packetTS; | |
| LogDebug("Negative timestamp detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug); | |
| $packetTS = $fixedTS; | |
| } | |
| else | |
| { | |
| if (($packetTS + $this->negTS) <= ($lastTS + $this->fixWindow)) | |
| $packetTS += $this->negTS; | |
| else | |
| { | |
| $this->negTS = $fixedTS - $packetTS; | |
| LogDebug("Negative timestamp override: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug); | |
| $packetTS = $fixedTS; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if ($packetTS != $currentTS) | |
| WriteFlvTimestamp($tagHeader, 0, $packetTS); | |
| switch ($packetType) | |
| { | |
| case AUDIO: | |
| if ($packetTS > $this->prevAudioTS - $this->fixWindow) | |
| { | |
| $FrameInfo = ReadByte($tagData, 0); | |
| $CodecID = ($FrameInfo & 0xF0) >> 4; | |
| if ($CodecID == CODEC_ID_AAC) | |
| { | |
| $AAC_PacketType = ReadByte($tagData, 1); | |
| if ($AAC_PacketType == AAC_SEQUENCE_HEADER) | |
| { | |
| if ($this->AAC_HeaderWritten) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
| break; | |
| } | |
| else | |
| { | |
| LogDebug("Writing AAC sequence header", $debug); | |
| $this->AAC_HeaderWritten = true; | |
| } | |
| } | |
| else if (!$this->AAC_HeaderWritten) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Discarding audio packet received before AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
| break; | |
| } | |
| } | |
| if ($packetSize > 0) | |
| { | |
| // Check for packets with non-monotonic audio timestamps and fix them | |
| if (!(($CodecID == CODEC_ID_AAC) and (($AAC_PacketType == AAC_SEQUENCE_HEADER) or $this->prevAAC_Header))) | |
| if (($this->prevAudioTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevAudioTS)) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Fixing audio timestamp", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
| $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevAudioTS - $packetTS); | |
| WriteFlvTimestamp($tagHeader, 0, $packetTS); | |
| } | |
| $flvTag = $tagHeader . $tagData; | |
| $flvTagLen = strlen($flvTag); | |
| WriteInt32($flvTag, $flvTagLen, $flvTagLen); | |
| $flvTagLen = strlen($flvTag); | |
| if ($flvWrite and is_resource($flvFile)) | |
| { | |
| $this->pAudioTagPos = ftell($flvFile); | |
| $status = fwrite($flvFile, $flvTag, $flvTagLen); | |
| if (!$status) | |
| LogError("Failed to write flv data to file"); | |
| if ($debug) | |
| LogDebug(sprintf($this->format . "%-16s", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize, $this->pAudioTagPos)); | |
| } | |
| else | |
| { | |
| $flvData .= $flvTag; | |
| if ($debug) | |
| LogDebug(sprintf($this->format, "AUDIO", $packetTS, $this->prevAudioTS, $packetSize)); | |
| } | |
| if (($CodecID == CODEC_ID_AAC) and ($AAC_PacketType == AAC_SEQUENCE_HEADER)) | |
| $this->prevAAC_Header = true; | |
| else | |
| $this->prevAAC_Header = false; | |
| $this->prevAudioTS = $packetTS; | |
| $this->pAudioTagLen = $flvTagLen; | |
| } | |
| else | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized audio packet", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
| } | |
| else | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping audio packet in fragment " . $fragNum, "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
| if (!$this->audio) | |
| $this->audio = true; | |
| break; | |
| case VIDEO: | |
| if ($packetTS > $this->prevVideoTS - $this->fixWindow) | |
| { | |
| $FrameInfo = ReadByte($tagData, 0); | |
| $FrameType = ($FrameInfo & 0xF0) >> 4; | |
| $CodecID = $FrameInfo & 0x0F; | |
| if ($FrameType == FRAME_TYPE_INFO) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping video info frame", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| break; | |
| } | |
| if ($CodecID == CODEC_ID_AVC) | |
| { | |
| $AVC_PacketType = ReadByte($tagData, 1); | |
| if ($AVC_PacketType == AVC_SEQUENCE_HEADER) | |
| { | |
| if ($this->AVC_HeaderWritten) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| break; | |
| } | |
| else | |
| { | |
| LogDebug("Writing AVC sequence header", $debug); | |
| $this->AVC_HeaderWritten = true; | |
| } | |
| } | |
| else if (!$this->AVC_HeaderWritten) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Discarding video packet received before AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| break; | |
| } | |
| } | |
| if ($packetSize > 0) | |
| { | |
| $pts = $packetTS; | |
| if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_NALU)) | |
| { | |
| $cts = ReadInt24($tagData, 2); | |
| $cts = ($cts + 0xff800000) ^ 0xff800000; | |
| $pts = $packetTS + $cts; | |
| if ($cts != 0) | |
| LogDebug("DTS: $packetTS CTS: $cts PTS: $pts", $debug); | |
| } | |
| // Check for packets with non-monotonic video timestamps and fix them | |
| if (!(($CodecID == CODEC_ID_AVC) and (($AVC_PacketType == AVC_SEQUENCE_HEADER) or ($AVC_PacketType == AVC_SEQUENCE_END) or $this->prevAVC_Header))) | |
| if (($this->prevVideoTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevVideoTS)) | |
| { | |
| LogDebug(sprintf("%s\n" . $this->format, "Fixing video timestamp", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevVideoTS - $packetTS); | |
| WriteFlvTimestamp($tagHeader, 0, $packetTS); | |
| } | |
| $flvTag = $tagHeader . $tagData; | |
| $flvTagLen = strlen($flvTag); | |
| WriteInt32($flvTag, $flvTagLen, $flvTagLen); | |
| $flvTagLen = strlen($flvTag); | |
| if ($flvWrite and is_resource($flvFile)) | |
| { | |
| $this->pVideoTagPos = ftell($flvFile); | |
| $status = fwrite($flvFile, $flvTag, $flvTagLen); | |
| if (!$status) | |
| LogError("Failed to write flv data to file"); | |
| if ($debug) | |
| LogDebug(sprintf($this->format . "%-16s", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize, $this->pVideoTagPos)); | |
| } | |
| else | |
| { | |
| $flvData .= $flvTag; | |
| if ($debug) | |
| LogDebug(sprintf($this->format, "VIDEO", $packetTS, $this->prevVideoTS, $packetSize)); | |
| } | |
| if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_SEQUENCE_HEADER)) | |
| $this->prevAVC_Header = true; | |
| else | |
| $this->prevAVC_Header = false; | |
| $this->prevVideoTS = $packetTS; | |
| $this->pVideoTagLen = $flvTagLen; | |
| } | |
| else | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized video packet", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| } | |
| else | |
| LogDebug(sprintf("%s\n" . $this->format, "Skipping video packet in fragment " . $fragNum, "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
| if (!$this->video) | |
| $this->video = true; | |
| break; | |
| case SCRIPT_DATA: | |
| break; | |
| default: | |
| if (($packetType == 40) or ($packetType == 41)) | |
| LogError("This stream is encrypted with FlashAccess DRM. Decryption of such streams isn't currently possible with this script.", 2); | |
| else | |
| { | |
| LogInfo("Unknown packet type " . $packetType . " encountered! Unable to process fragment " . $fragNum); | |
| break 2; | |
| } | |
| } | |
| $fragPos += $totalTagLen; | |
| } | |
| $this->duration = round($packetTS / 1000, 0); | |
| if ($flvWrite and is_resource($flvFile)) | |
| { | |
| $this->filesize = ftell($flvFile) / (1024 * 1024); | |
| return true; | |
| } | |
| else | |
| return $flvData; | |
| } | |
| function WriteFragment($download, &$opt) | |
| { | |
| $this->frags[$download['id']] = $download; | |
| if (!isset($opt['flvWrite'])) | |
| $opt['flvWrite'] = true; | |
| if ($this->play) | |
| $opt['flvWrite'] = false; | |
| $available = count($this->frags); | |
| for ($i = 0; $i < $available; $i++) | |
| { | |
| if (isset($this->frags[$this->lastFrag + 1])) | |
| { | |
| $frag = $this->frags[$this->lastFrag + 1]; | |
| if ($frag['response'] !== false) | |
| { | |
| LogDebug("Writing fragment " . $frag['id'] . " to flv file"); | |
| if (!isset($opt['flvFile'])) | |
| { | |
| $this->decoderTest = true; | |
| if ($this->play) | |
| $outFile = STDOUT; | |
| else if ($this->outFile) | |
| { | |
| if ($opt['filesize']) | |
| $outFile = JoinUrl($this->outDir, $this->outFile . '-' . $this->fileCount++ . ".flv"); | |
| else | |
| $outFile = JoinUrl($this->outDir, $this->outFile . ".flv"); | |
| } | |
| else | |
| { | |
| if ($opt['filesize']) | |
| $outFile = JoinUrl($this->outDir, $this->baseFilename . '-' . $this->fileCount++ . ".flv"); | |
| else | |
| $outFile = JoinUrl($this->outDir, $this->baseFilename . ".flv"); | |
| } | |
| $this->InitDecoder(); | |
| $this->DecodeFragment($frag['response'], $frag['id'], $opt); | |
| $opt['flvFile'] = WriteFlvFile($outFile, $this->audio, $this->video); | |
| if ($this->metadata) | |
| WriteMetadata($this, $opt['flvFile']); | |
| $this->decoderTest = false; | |
| $this->InitDecoder(); | |
| } | |
| if ($opt['flvWrite']) | |
| $this->DecodeFragment($frag['response'], $frag['id'], $opt); | |
| else | |
| { | |
| $flvData = $this->DecodeFragment($frag['response'], $frag['id'], $opt); | |
| if (strlen($flvData) > 0) | |
| { | |
| $status = fwrite($opt['flvFile'], $flvData, strlen($flvData)); | |
| if (!$status) | |
| LogError("Failed to write flv data"); | |
| } | |
| } | |
| $this->lastFrag = $frag['id']; | |
| } | |
| else | |
| { | |
| $this->lastFrag += 1; | |
| LogDebug("Skipping failed fragment " . $this->lastFrag); | |
| } | |
| unset($this->frags[$this->lastFrag]); | |
| } | |
| else | |
| break; | |
| $recDuration = $opt['duration'] + $this->duration; | |
| if ($opt['tDuration'] and ($recDuration >= $opt['tDuration'])) | |
| { | |
| LogInfo(""); | |
| LogInfo($recDuration . " seconds of content has been recorded successfully."); | |
| return STOP_PROCESSING; | |
| } | |
| if ($opt['filesize'] and ($this->filesize >= $opt['filesize'])) | |
| { | |
| $this->filesize = 0; | |
| $opt['duration'] += $this->duration; | |
| fclose($opt['flvFile']); | |
| unset($opt['flvFile']); | |
| } | |
| } | |
| if (!count($this->frags)) | |
| unset($this->frags); | |
| return true; | |
| } | |
| } | |
| function ReadByte($str, $pos) | |
| { | |
| $int = unpack('C', $str[$pos]); | |
| return $int[1]; | |
| } | |
| function ReadInt16($str, $pos) | |
| { | |
| $int32 = unpack('N', "\x00\x00" . substr($str, $pos, 2)); | |
| return $int32[1]; | |
| } | |
| function ReadInt24($str, $pos) | |
| { | |
| $int32 = unpack('N', "\x00" . substr($str, $pos, 3)); | |
| return $int32[1]; | |
| } | |
| function ReadInt32($str, $pos) | |
| { | |
| $int32 = unpack('N', substr($str, $pos, 4)); | |
| return $int32[1]; | |
| } | |
| function ReadInt64($str, $pos) | |
| { | |
| $hi = sprintf("%u", ReadInt32($str, $pos)); | |
| $lo = sprintf("%u", ReadInt32($str, $pos + 4)); | |
| $int64 = bcadd(bcmul($hi, "4294967296"), $lo); | |
| return $int64; | |
| } | |
| function ReadDouble($str, $pos) | |
| { | |
| $double = unpack('d', strrev(substr($str, $pos, 8))); | |
| return $double[1]; | |
| } | |
| function ReadString($str, &$pos) | |
| { | |
| $len = 0; | |
| while ($str[$pos + $len] != "\x00") | |
| $len++; | |
| $str = substr($str, $pos, $len); | |
| $pos += $len + 1; | |
| return $str; | |
| } | |
| function ReadBoxHeader($str, &$pos, &$boxType, &$boxSize) | |
| { | |
| if (!isset($pos)) | |
| $pos = 0; | |
| $boxSize = ReadInt32($str, $pos); | |
| $boxType = substr($str, $pos + 4, 4); | |
| if ($boxSize == 1) | |
| { | |
| $boxSize = ReadInt64($str, $pos + 8) - 16; | |
| $pos += 16; | |
| } | |
| else | |
| { | |
| $boxSize -= 8; | |
| $pos += 8; | |
| } | |
| if ($boxSize <= 0) | |
| $boxSize = 0; | |
| } | |
| function WriteByte(&$str, $pos, $int) | |
| { | |
| $str[$pos] = pack('C', $int); | |
| } | |
| function WriteInt24(&$str, $pos, $int) | |
| { | |
| $str[$pos] = pack('C', ($int & 0xFF0000) >> 16); | |
| $str[$pos + 1] = pack('C', ($int & 0xFF00) >> 8); | |
| $str[$pos + 2] = pack('C', $int & 0xFF); | |
| } | |
| function WriteInt32(&$str, $pos, $int) | |
| { | |
| $str[$pos] = pack('C', ($int & 0xFF000000) >> 24); | |
| $str[$pos + 1] = pack('C', ($int & 0xFF0000) >> 16); | |
| $str[$pos + 2] = pack('C', ($int & 0xFF00) >> 8); | |
| $str[$pos + 3] = pack('C', $int & 0xFF); | |
| } | |
| function WriteBoxSize(&$str, $pos, $type, $size) | |
| { | |
| if (substr($str, $pos - 4, 4) == $type) | |
| WriteInt32($str, $pos - 8, $size); | |
| else | |
| { | |
| WriteInt32($str, $pos - 8, 0); | |
| WriteInt32($str, $pos - 4, $size); | |
| } | |
| } | |
| function WriteFlvTimestamp(&$frag, $fragPos, $packetTS) | |
| { | |
| WriteInt24($frag, $fragPos + 4, ($packetTS & 0x00FFFFFF)); | |
| WriteByte($frag, $fragPos + 7, ($packetTS & 0xFF000000) >> 24); | |
| } | |
| function AbsoluteUrl($baseUrl, $url) | |
| { | |
| if (!isHttpUrl($url)) | |
| $url = JoinUrl($baseUrl, $url); | |
| return NormalizePath($url); | |
| } | |
| function DecodeUrl($url) | |
| { | |
| $queryPart = strpos($url, '?'); | |
| if ($queryPart) | |
| { | |
| $query = substr($url, $queryPart); | |
| $url = rawurldecode(substr($url, 0, $queryPart)) . $query; | |
| } | |
| else | |
| $url = rawurldecode($url); | |
| return $url; | |
| } | |
| function GetFragmentList($baseFilename, $fragStart, $fileExt) | |
| { | |
| $files = array(); | |
| $retries = 0; | |
| $count = $fragStart; | |
| while (true) | |
| { | |
| if ($retries >= 50) | |
| break; | |
| $file = $baseFilename . ++$count; | |
| if (file_exists($file)) | |
| { | |
| $files[$count] = $file; | |
| $retries = 0; | |
| } | |
| else if (file_exists($file . $fileExt)) | |
| { | |
| $files[$count] = $file . $fileExt; | |
| $retries = 0; | |
| } | |
| else | |
| $retries++; | |
| } | |
| return $files; | |
| } | |
| function GetString($object) | |
| { | |
| return trim(strval($object)); | |
| } | |
| function isHttpUrl($url) | |
| { | |
| return (strncasecmp($url, "http", 4) == 0) ? true : false; | |
| } | |
| function isRtmpUrl($url) | |
| { | |
| return (preg_match('/^rtm(p|pe|pt|pte|ps|pts|fp):\/\//i', $url)) ? true : false; | |
| } | |
| function JoinUrl($firstUrl, $secondUrl) | |
| { | |
| if ($firstUrl and $secondUrl) | |
| { | |
| if (substr($firstUrl, -1) == '/') | |
| $firstUrl = substr($firstUrl, 0, -1); | |
| if (substr($secondUrl, 0, 1) == '/') | |
| $secondUrl = substr($secondUrl, 1); | |
| return $firstUrl . '/' . $secondUrl; | |
| } | |
| else if ($firstUrl) | |
| return $firstUrl; | |
| else | |
| return $secondUrl; | |
| } | |
| function KeyName(array $a, $pos) | |
| { | |
| $temp = array_slice($a, $pos, 1, true); | |
| return key($temp); | |
| } | |
| function LogDebug($msg, $display = true) | |
| { | |
| global $debug, $showHeader; | |
| if ($showHeader) | |
| { | |
| ShowHeader(); | |
| $showHeader = false; | |
| } | |
| if ($display and $debug) | |
| fwrite(STDERR, $msg . "\n"); | |
| } | |
| function LogError($msg, $code = 1) | |
| { | |
| LogInfo($msg); | |
| exit($code); | |
| } | |
| function LogInfo($msg, $progress = false) | |
| { | |
| global $quiet, $showHeader; | |
| if ($showHeader) | |
| { | |
| ShowHeader(); | |
| $showHeader = false; | |
| } | |
| if (!$quiet) | |
| PrintLine($msg, $progress); | |
| } | |
| function NormalizePath($path) | |
| { | |
| $inSegs = preg_split('/(?<!\/)\/(?!\/)/u', $path); | |
| $outSegs = array(); | |
| foreach ($inSegs as $seg) | |
| { | |
| if ($seg == '' or $seg == '.') | |
| continue; | |
| if ($seg == '..') | |
| array_pop($outSegs); | |
| else | |
| array_push($outSegs, $seg); | |
| } | |
| $outPath = implode('/', $outSegs); | |
| if (substr($path, 0, 1) == '/') | |
| $outPath = '/' . $outPath; | |
| if (substr($path, -1) == '/') | |
| $outPath .= '/'; | |
| return $outPath; | |
| } | |
| function PrintLine($msg, $progress = false) | |
| { | |
| if ($msg) | |
| { | |
| printf("\r%-79s\r", ""); | |
| if ($progress) | |
| printf("%s\r", $msg); | |
| else | |
| printf("%s\n", $msg); | |
| } | |
| else | |
| printf("\n"); | |
| } | |
| function RemoveExtension($outFile) | |
| { | |
| preg_match("/\.\w{1,4}$/i", $outFile, $extension); | |
| if (isset($extension[0])) | |
| { | |
| $extension = $extension[0]; | |
| $outFile = substr($outFile, 0, -strlen($extension)); | |
| return $outFile; | |
| } | |
| return $outFile; | |
| } | |
| function RenameFragments($oldFiles, $baseFilename) | |
| { | |
| $newFiles = array(); | |
| $count = 0; | |
| foreach ($oldFiles as $oldFile) | |
| { | |
| $count++; | |
| $newFile = $baseFilename . $count; | |
| if (!rename($oldFile, $newFile)) | |
| LogError("Failed to rename fragment from '" . $oldFile . "' to '" . $newFile . "'"); | |
| $newFiles[$count] = $newFile; | |
| } | |
| return $newFiles; | |
| } | |
| function ShowHeader() | |
| { | |
| $header = "KSV Adobe HDS Downloader"; | |
| $len = strlen($header); | |
| $width = floor((80 - $len) / 2) + $len; | |
| $format = "\n%" . $width . "s\n\n"; | |
| printf($format, $header); | |
| } | |
| function WriteFlvFile($outFile, $audio = true, $video = true) | |
| { | |
| $flvHeader = unhexlify("464c5601050000000900000000"); | |
| $flvHeaderLen = strlen($flvHeader); | |
| // Set proper Audio/Video marker | |
| WriteByte($flvHeader, 4, $audio << 2 | $video); | |
| if (is_resource($outFile)) | |
| $flv = $outFile; | |
| else | |
| $flv = fopen($outFile, "w+b"); | |
| if (!$flv) | |
| LogError("Failed to open " . $outFile); | |
| fwrite($flv, $flvHeader, $flvHeaderLen); | |
| return $flv; | |
| } | |
| function WriteMetadata($f4f, $flv) | |
| { | |
| if (isset($f4f->media) and $f4f->media['metadata']) | |
| { | |
| $metadataSize = strlen($f4f->media['metadata']); | |
| WriteByte($metadata, 0, SCRIPT_DATA); | |
| WriteInt24($metadata, 1, $metadataSize); | |
| WriteInt24($metadata, 4, 0); | |
| WriteInt32($metadata, 7, 0); | |
| $metadata = implode("", $metadata) . $f4f->media['metadata']; | |
| WriteByte($metadata, $f4f->tagHeaderLen + $metadataSize - 1, 0x09); | |
| WriteInt32($metadata, $f4f->tagHeaderLen + $metadataSize, $f4f->tagHeaderLen + $metadataSize); | |
| if (is_resource($flv)) | |
| { | |
| fwrite($flv, $metadata, $f4f->tagHeaderLen + $metadataSize + $f4f->prevTagSize); | |
| return true; | |
| } | |
| else | |
| return $metadata; | |
| } | |
| return false; | |
| } | |
| function hexlify($str) | |
| { | |
| $str = unpack("H*", $str); | |
| return $str[1]; | |
| } | |
| function in_array_field($needle, $needle_field, $haystack, $strict = false) | |
| { | |
| if ($strict) | |
| { | |
| foreach ($haystack as $item) | |
| if (isset($item[$needle_field]) and $item[$needle_field] === $needle) | |
| return true; | |
| } | |
| else | |
| { | |
| foreach ($haystack as $item) | |
| if (isset($item[$needle_field]) and $item[$needle_field] == $needle) | |
| return true; | |
| } | |
| return false; | |
| } | |
| function unhexlify($str) | |
| { | |
| return pack("H*", $str); | |
| } | |
| function value_in_array_field($needle, $needle_field, $value_field, $haystack, $strict = false) | |
| { | |
| if ($strict) | |
| { | |
| foreach ($haystack as $item) | |
| if (isset($item[$needle_field]) and $item[$needle_field] === $needle) | |
| return $item[$value_field]; | |
| } | |
| else | |
| { | |
| foreach ($haystack as $item) | |
| if (isset($item[$needle_field]) and $item[$needle_field] == $needle) | |
| return $item[$value_field]; | |
| } | |
| return false; | |
| } | |
| // Global code starts here | |
| $format = " %-8s%-16s%-16s%-8s"; | |
| $baseFilename = ""; | |
| $debug = false; | |
| $duration = 0; | |
| $delete = false; | |
| $fileCount = 1; | |
| $fileExt = ".f4f"; | |
| $filesize = 0; | |
| $fragCount = 0; | |
| $fragStart = 0; | |
| $manifest = ""; | |
| $maxSpeed = 0; | |
| $metadata = true; | |
| $outDir = ""; | |
| $outFile = ""; | |
| $play = false; | |
| $quiet = false; | |
| $referrer = ""; | |
| $rename = false; | |
| $showHeader = true; | |
| $start = 0; | |
| $update = false; | |
| // Set large enough memory limit | |
| ini_set("memory_limit", "1024M"); | |
| // Initialize command line processing | |
| $options = array( | |
| 0 => array( | |
| 'help' => 'displays this help', | |
| 'debug' => 'show debug output', | |
| 'delete' => 'delete fragments after processing', | |
| 'fproxy' => 'force proxy for downloading of fragments', | |
| 'play' => 'dump stream to stdout for piping to media player', | |
| 'rename' => 'rename fragments sequentially before processing', | |
| 'update' => 'update the script to current git version' | |
| ), | |
| 1 => array( | |
| 'adkey' => 'akamai session decryption key', | |
| 'auth' => 'authentication string for fragment requests', | |
| 'duration' => 'stop recording after specified number of seconds', | |
| 'filesize' => 'split output file in chunks of specified size (MB)', | |
| 'fragments' => 'base filename for fragments', | |
| 'fixwindow' => 'timestamp gap between frames to consider as timeshift', | |
| 'manifest' => 'manifest file for downloading of fragments', | |
| 'maxspeed' => 'maximum bandwidth consumption (KB) for fragment downloading', | |
| 'outdir' => 'destination folder for output file', | |
| 'outfile' => 'filename to use for output file', | |
| 'parallel' => 'number of fragments to download simultaneously', | |
| 'proxy' => 'proxy for downloading of manifest', | |
| 'quality' => 'selected quality level (low|medium|high) or exact bitrate', | |
| 'referrer' => 'Referer to use for emulation of browser requests', | |
| 'start' => 'start from specified fragment', | |
| 'useragent' => 'User-Agent to use for emulation of browser requests' | |
| ) | |
| ); | |
| $cli = new CLI($options, true); | |
| // Check if STDOUT is available | |
| if ($cli->getParam('play')) | |
| { | |
| $play = true; | |
| $quiet = true; | |
| $showHeader = false; | |
| } | |
| // Check for required extensions | |
| $required_extensions = array( | |
| "bcmath", | |
| "curl", | |
| "SimpleXML" | |
| ); | |
| $missing_extensions = array_diff($required_extensions, get_loaded_extensions()); | |
| if ($missing_extensions) | |
| { | |
| $msg = "You have to install and enable the following extension(s) to continue: '" . implode("', '", $missing_extensions) . "'"; | |
| LogError($msg); | |
| } | |
| // Display help | |
| if ($cli->getParam('help')) | |
| { | |
| $cli->displayHelp(); | |
| exit(0); | |
| } | |
| // Initialize classes | |
| $cc = new cURL(); | |
| $ad = new AkamaiDecryptor(); | |
| $f4f = new F4F(); | |
| // Process command line options | |
| if (isset($cli->params['unknown'])) | |
| $baseFilename = $cli->params['unknown'][0]; | |
| if ($cli->getParam('debug')) | |
| $debug = true; | |
| if ($cli->getParam('delete')) | |
| $delete = true; | |
| if ($cli->getParam('fproxy')) | |
| $cc->fragProxy = true; | |
| if ($cli->getParam('rename')) | |
| $rename = $cli->getParam('rename'); | |
| if ($cli->getParam('update')) | |
| $update = true; | |
| if ($cli->getParam('adkey')) | |
| $ad->sessionKey = unhexlify($cli->getParam('adkey')); | |
| if ($cli->getParam('auth')) | |
| $f4f->auth = '?' . $cli->getParam('auth'); | |
| if ($cli->getParam('duration')) | |
| $duration = $cli->getParam('duration'); | |
| if ($cli->getParam('filesize')) | |
| $filesize = $cli->getParam('filesize'); | |
| if ($cli->getParam('fixwindow')) | |
| $f4f->fixWindow = $cli->getParam('fixwindow'); | |
| if ($cli->getParam('fragments')) | |
| $baseFilename = $cli->getParam('fragments'); | |
| if ($cli->getParam('manifest')) | |
| $manifest = $cli->getParam('manifest'); | |
| if ($cli->getParam('maxspeed')) | |
| $maxSpeed = $cli->getParam('maxspeed'); | |
| if ($cli->getParam('outdir')) | |
| $outDir = $cli->getParam('outdir'); | |
| if ($cli->getParam('outfile')) | |
| $outFile = $cli->getParam('outfile'); | |
| if ($cli->getParam('parallel')) | |
| $f4f->parallel = $cli->getParam('parallel'); | |
| if ($cli->getParam('proxy')) | |
| $cc->proxy = $cli->getParam('proxy'); | |
| if ($cli->getParam('quality')) | |
| $f4f->quality = $cli->getParam('quality'); | |
| if ($cli->getParam('referrer')) | |
| $referrer = $cli->getParam('referrer'); | |
| if ($cli->getParam('start')) | |
| $start = $cli->getParam('start'); | |
| if ($cli->getParam('useragent')) | |
| $cc->user_agent = $cli->getParam('useragent'); | |
| // Use custom referrer | |
| if ($referrer) | |
| $cc->headers[] = "Referer: " . $referrer; | |
| // Update the script | |
| if ($update) | |
| { | |
| LogInfo("Updating script...."); | |
| $status = $cc->get("https://raw.github.com/K-S-V/Scripts/master/AdobeHDS.php"); | |
| if ($status == 200) | |
| { | |
| if (md5($cc->response) == md5(file_get_contents($argv[0]))) | |
| LogError("You are already using the latest version of this script.", 0); | |
| $status = file_put_contents($argv[0], $cc->response); | |
| if (!$status) | |
| LogError("Failed to write script file"); | |
| LogError("Script has been updated successfully.", 0); | |
| } | |
| else | |
| LogError("Failed to update script"); | |
| } | |
| // Set overall maximum bandwidth for fragment downloading | |
| if ($maxSpeed > 0) | |
| { | |
| $cc->maxSpeed = ($maxSpeed * 1024) / $f4f->parallel; | |
| LogDebug(sprintf("Setting maximum speed to %.2f KB per fragment (overall %d KB)", $cc->maxSpeed / 1024, $maxSpeed)); | |
| } | |
| // Create output directory | |
| if ($outDir) | |
| { | |
| $outDir = rtrim(str_replace('\\', '/', $outDir)); | |
| if (!file_exists($outDir)) | |
| { | |
| LogDebug("Creating destination directory " . $outDir); | |
| if (!mkdir($outDir, 0777, true)) | |
| LogError("Failed to create destination directory " . $outDir); | |
| } | |
| } | |
| // Remove existing file extension | |
| if ($outFile) | |
| $outFile = RemoveExtension($outFile); | |
| // Disable filesize when piping | |
| if ($play) | |
| $filesize = 0; | |
| // Disable metadata if it invalidates the stream duration | |
| if ($start or $duration or $filesize) | |
| $metadata = false; | |
| // Set f4f options | |
| $f4f->baseFilename = $baseFilename; | |
| $f4f->debug = $debug; | |
| $f4f->format = $format; | |
| $f4f->metadata = $metadata; | |
| $f4f->outDir = $outDir; | |
| $f4f->outFile = $outFile; | |
| $f4f->play = $play; | |
| $f4f->rename = $rename; | |
| // Download fragments when manifest is available | |
| $opt = array( | |
| 'ad' => $ad, | |
| 'cc' => $cc, | |
| 'duration' => 0, | |
| 'filesize' => $filesize, | |
| 'start' => $start, | |
| 'tDuration' => $duration | |
| ); | |
| if ($manifest) | |
| { | |
| $manifest = AbsoluteUrl("http://", $manifest); | |
| $f4f->DownloadFragments($manifest, $opt); | |
| $baseFilename = $f4f->baseFilename; | |
| $rename = $f4f->rename; | |
| } | |
| // Determine output filename | |
| if (!$outFile) | |
| { | |
| $baseFilename = str_replace('\\', '/', $baseFilename); | |
| $lastChar = substr($baseFilename, -1); | |
| if ($baseFilename and !(($lastChar == '/') or ($lastChar == ':'))) | |
| { | |
| $lastSlash = strrpos($baseFilename, '/'); | |
| if ($lastSlash) | |
| $outFile = substr($baseFilename, $lastSlash + 1); | |
| else | |
| $outFile = $baseFilename; | |
| } | |
| else | |
| $outFile = "Joined"; | |
| $outFile = RemoveExtension($outFile); | |
| } | |
| // Check for available fragments and rename if required | |
| if ($f4f->fragStart) | |
| $fragStart = $f4f->fragStart; | |
| else if ($start) | |
| $fragStart = $start - 1; | |
| $files = GetFragmentList($baseFilename, $fragStart, $fileExt); | |
| if ($rename) | |
| { | |
| $files = RenameFragments($files, $baseFilename); | |
| $fragStart = 0; | |
| } | |
| $fragCount = count($files); | |
| if (!($f4f->live or $f4f->play)) | |
| LogInfo("Found " . $fragCount . " fragments"); | |
| // Process available fragments | |
| if (!$f4f->processed) | |
| { | |
| if ($fragCount < 1) | |
| exit(1); | |
| $f4f->lastFrag = $fragStart; | |
| $f4f->outFile = $outFile; | |
| $timeStart = microtime(true); | |
| LogDebug("Joining Fragments:"); | |
| $count = 0; | |
| foreach ($files as $id => $file) | |
| { | |
| $frag['id'] = $id; | |
| $frag['response'] = file_get_contents($file); | |
| LogInfo("Processed " . (++$count) . " fragments", true); | |
| if ($f4f->WriteFragment($frag, $opt) === STOP_PROCESSING) | |
| break; | |
| } | |
| unset($id, $file); | |
| if (isset($opt['flvFile'])) | |
| fclose($opt['flvFile']); | |
| $timeEnd = microtime(true); | |
| $timeTaken = sprintf("%.2f", $timeEnd - $timeStart); | |
| LogInfo("Joined " . $count . " fragments in " . $timeTaken . " seconds"); | |
| } | |
| // Delete fragments after processing | |
| if ($delete) | |
| { | |
| foreach ($files as $file) | |
| { | |
| if (!unlink($file)) | |
| LogInfo("Failed to delete file '" . $file . "'"); | |
| } | |
| } | |
| LogInfo("Finished"); | |
| ?> |