PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/AdobeHDS.php

https://github.com/tejastank/Scripts
PHP | 1845 lines | 1103 code | 48 blank | 694 comment | 111 complexity | 29c511aca00c937e24ca77c6bd2651ba MD5 | raw file
Possible License(s): GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. define('AUDIO', 0x08);
  3. define('VIDEO', 0x09);
  4. define('SCRIPT_DATA', 0x12);
  5. define('FRAME_TYPE_INFO', 0x05);
  6. define('CODEC_ID_AVC', 0x07);
  7. define('CODEC_ID_AAC', 0x0A);
  8. define('AVC_SEQUENCE_HEADER', 0x00);
  9. define('AAC_SEQUENCE_HEADER', 0x00);
  10. define('AVC_SEQUENCE_END', 0x02);
  11. define('FRAMEGAP_DURATION', 8);
  12. define('INVALID_TIMESTAMP', -1);
  13. class CLI
  14. {
  15. protected static $ACCEPTED = array(
  16. 0 => array(
  17. 'help' => 'displays this help',
  18. 'debug' => 'show debug output',
  19. 'delete' => 'delete fragments after processing',
  20. 'fproxy' => 'force proxy for downloading of fragments',
  21. 'play' => 'dump stream to stdout for piping to media player',
  22. 'rename' => 'rename fragments sequentially before processing',
  23. 'update' => 'update the script to current git version'
  24. ),
  25. 1 => array(
  26. 'auth' => 'authentication string for fragment requests',
  27. 'duration' => 'stop recording after specified number of seconds',
  28. 'filesize' => 'split output file in chunks of specified size (MB)',
  29. 'fragments' => 'base filename for fragments',
  30. 'fixwindow' => 'timestamp gap between frames to consider as timeshift',
  31. 'manifest' => 'manifest file for downloading of fragments',
  32. 'outdir' => 'destination folder for output file',
  33. 'outfile' => 'filename to use for output file',
  34. 'parallel' => 'number of fragments to download simultaneously',
  35. 'proxy' => 'proxy for downloading of manifest',
  36. 'quality' => 'selected quality level (low|medium|high) or exact bitrate',
  37. 'referrer' => 'Referer to use for emulation of browser requests',
  38. 'start' => 'start from specified fragment',
  39. 'useragent' => 'User-Agent to use for emulation of browser requests'
  40. )
  41. );
  42. var $params = array();
  43. function __construct()
  44. {
  45. global $argc, $argv;
  46. // Parse params
  47. if ($argc > 1)
  48. {
  49. $paramSwitch = false;
  50. for ($i = 1; $i < $argc; $i++)
  51. {
  52. $arg = $argv[$i];
  53. $isSwitch = preg_match('/^--/', $arg);
  54. if ($isSwitch)
  55. $arg = preg_replace('/^--/', '', $arg);
  56. if ($paramSwitch && $isSwitch)
  57. LogError("[param] expected after '$paramSwitch' switch (" . self::$ACCEPTED[1][$paramSwitch] . ")");
  58. else if (!$paramSwitch && !$isSwitch)
  59. {
  60. if (isset($GLOBALS['baseFilename']) and (!$GLOBALS['baseFilename']))
  61. $GLOBALS['baseFilename'] = $arg;
  62. else
  63. LogError("'$arg' is an invalid switch, use --help to display valid switches.");
  64. }
  65. else if (!$paramSwitch && $isSwitch)
  66. {
  67. if (isset($this->params[$arg]))
  68. LogError("'$arg' switch cannot occur more than once");
  69. $this->params[$arg] = true;
  70. if (isset(self::$ACCEPTED[1][$arg]))
  71. $paramSwitch = $arg;
  72. else if (!isset(self::$ACCEPTED[0][$arg]))
  73. LogError("there's no '$arg' switch, use --help to display all switches.");
  74. }
  75. else if ($paramSwitch && !$isSwitch)
  76. {
  77. $this->params[$paramSwitch] = $arg;
  78. $paramSwitch = false;
  79. }
  80. }
  81. }
  82. // Final check
  83. foreach ($this->params as $k => $v)
  84. if (isset(self::$ACCEPTED[1][$k]) && $v === true)
  85. LogError("[param] expected after '$k' switch (" . self::$ACCEPTED[1][$k] . ")");
  86. }
  87. function getParam($name)
  88. {
  89. if (isset($this->params[$name]))
  90. return $this->params[$name];
  91. else
  92. return "";
  93. }
  94. function displayHelp()
  95. {
  96. LogInfo("You can use script with following switches: \n");
  97. foreach (self::$ACCEPTED[0] as $key => $value)
  98. LogInfo(sprintf(" --%-18s%s", $key, $value));
  99. foreach (self::$ACCEPTED[1] as $key => $value)
  100. LogInfo(sprintf(" --%-9s%-9s%s", $key, " [param]", $value));
  101. }
  102. }
  103. class cURL
  104. {
  105. var $headers, $user_agent, $compression, $cookie_file;
  106. var $active, $cert_check, $fragProxy, $proxy, $response;
  107. var $mh, $ch, $mrc;
  108. static $ref = 0;
  109. function cURL($cookies = true, $cookie = 'Cookies.txt', $compression = 'gzip', $proxy = '')
  110. {
  111. $this->headers = $this->headers();
  112. $this->user_agent = 'Mozilla/5.0 (Windows NT 5.1; rv:17.0) Gecko/20100101 Firefox/17.0';
  113. $this->compression = $compression;
  114. $this->cookies = $cookies;
  115. if ($this->cookies == true)
  116. $this->cookie($cookie);
  117. $this->cert_check = true;
  118. $this->fragProxy = false;
  119. $this->proxy = $proxy;
  120. self::$ref++;
  121. }
  122. function __destruct()
  123. {
  124. $this->stopDownloads();
  125. if ((self::$ref <= 1) and file_exists($this->cookie_file))
  126. unlink($this->cookie_file);
  127. self::$ref--;
  128. }
  129. function headers()
  130. {
  131. $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
  132. $headers[] = 'Connection: Keep-Alive';
  133. return $headers;
  134. }
  135. function cookie($cookie_file)
  136. {
  137. if (file_exists($cookie_file))
  138. $this->cookie_file = $cookie_file;
  139. else
  140. {
  141. $file = fopen($cookie_file, 'w') or $this->error('The cookie file could not be opened. Make sure this directory has the correct permissions.');
  142. $this->cookie_file = $cookie_file;
  143. fclose($file);
  144. }
  145. }
  146. function get($url)
  147. {
  148. $process = curl_init($url);
  149. curl_setopt($process, CURLOPT_HTTPHEADER, $this->headers);
  150. curl_setopt($process, CURLOPT_HEADER, 0);
  151. curl_setopt($process, CURLOPT_USERAGENT, $this->user_agent);
  152. if ($this->cookies == true)
  153. {
  154. curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file);
  155. curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file);
  156. }
  157. curl_setopt($process, CURLOPT_ENCODING, $this->compression);
  158. curl_setopt($process, CURLOPT_TIMEOUT, 60);
  159. if ($this->proxy)
  160. $this->setProxy($process, $this->proxy);
  161. curl_setopt($process, CURLOPT_RETURNTRANSFER, 1);
  162. curl_setopt($process, CURLOPT_FOLLOWLOCATION, 1);
  163. if (!$this->cert_check)
  164. curl_setopt($process, CURLOPT_SSL_VERIFYPEER, 0);
  165. $this->response = curl_exec($process);
  166. if ($this->response !== false)
  167. $status = curl_getinfo($process, CURLINFO_HTTP_CODE);
  168. curl_close($process);
  169. if (isset($status))
  170. return $status;
  171. else
  172. return false;
  173. }
  174. function post($url, $data)
  175. {
  176. $process = curl_init($url);
  177. $headers = $this->headers;
  178. $headers[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8';
  179. curl_setopt($process, CURLOPT_HTTPHEADER, $headers);
  180. curl_setopt($process, CURLOPT_HEADER, 1);
  181. curl_setopt($process, CURLOPT_USERAGENT, $this->user_agent);
  182. if ($this->cookies == true)
  183. {
  184. curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file);
  185. curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file);
  186. }
  187. curl_setopt($process, CURLOPT_ENCODING, $this->compression);
  188. curl_setopt($process, CURLOPT_TIMEOUT, 60);
  189. if ($this->proxy)
  190. $this->setProxy($process, $this->proxy);
  191. curl_setopt($process, CURLOPT_POSTFIELDS, $data);
  192. curl_setopt($process, CURLOPT_RETURNTRANSFER, 1);
  193. curl_setopt($process, CURLOPT_FOLLOWLOCATION, 1);
  194. curl_setopt($process, CURLOPT_POST, 1);
  195. if (!$this->cert_check)
  196. curl_setopt($process, CURLOPT_SSL_VERIFYPEER, 0);
  197. $return = curl_exec($process);
  198. curl_close($process);
  199. return $return;
  200. }
  201. function setProxy(&$process, $proxy)
  202. {
  203. $type = substr($proxy, 0, stripos($proxy, "://"));
  204. if ($type)
  205. {
  206. $type = strtolower($type);
  207. $proxy = substr($proxy, stripos($proxy, "://") + 3);
  208. }
  209. switch ($type)
  210. {
  211. case "socks4":
  212. $type = CURLPROXY_SOCKS4;
  213. break;
  214. case "socks5":
  215. $type = CURLPROXY_SOCKS5;
  216. break;
  217. default:
  218. $type = CURLPROXY_HTTP;
  219. }
  220. curl_setopt($process, CURLOPT_PROXY, $proxy);
  221. curl_setopt($process, CURLOPT_PROXYTYPE, $type);
  222. }
  223. function addDownload($url, $id)
  224. {
  225. if (!isset($this->mh))
  226. $this->mh = curl_multi_init();
  227. if (isset($this->ch[$id]))
  228. return;
  229. else
  230. $download =& $this->ch[$id];
  231. $download['id'] = $id;
  232. $download['url'] = $url;
  233. $download['ch'] = curl_init($url);
  234. curl_setopt($download['ch'], CURLOPT_HTTPHEADER, $this->headers);
  235. curl_setopt($download['ch'], CURLOPT_HEADER, 0);
  236. curl_setopt($download['ch'], CURLOPT_USERAGENT, $this->user_agent);
  237. if ($this->cookies == true)
  238. {
  239. curl_setopt($download['ch'], CURLOPT_COOKIEFILE, $this->cookie_file);
  240. curl_setopt($download['ch'], CURLOPT_COOKIEJAR, $this->cookie_file);
  241. }
  242. curl_setopt($download['ch'], CURLOPT_ENCODING, $this->compression);
  243. curl_setopt($download['ch'], CURLOPT_TIMEOUT, 300);
  244. if ($this->fragProxy and $this->proxy)
  245. $this->setProxy($download['ch'], $this->proxy);
  246. curl_setopt($download['ch'], CURLOPT_BINARYTRANSFER, 1);
  247. curl_setopt($download['ch'], CURLOPT_RETURNTRANSFER, 1);
  248. curl_setopt($download['ch'], CURLOPT_FOLLOWLOCATION, 1);
  249. if (!$this->cert_check)
  250. curl_setopt($download['ch'], CURLOPT_SSL_VERIFYPEER, 0);
  251. curl_multi_add_handle($this->mh, $download['ch']);
  252. do
  253. {
  254. $this->mrc = curl_multi_exec($this->mh, $this->active);
  255. } while ($this->mrc == CURLM_CALL_MULTI_PERFORM);
  256. }
  257. function checkDownloads()
  258. {
  259. if (isset($this->mh))
  260. {
  261. curl_multi_select($this->mh);
  262. $this->mrc = curl_multi_exec($this->mh, $this->active);
  263. if ($this->mrc != CURLM_OK)
  264. return false;
  265. while ($info = curl_multi_info_read($this->mh))
  266. {
  267. foreach ($this->ch as $download)
  268. if ($download['ch'] == $info['handle'])
  269. break;
  270. $info = curl_getinfo($download['ch']);
  271. $array['id'] = $download['id'];
  272. $array['url'] = $download['url'];
  273. if ($info['http_code'] == 200)
  274. {
  275. if ($info['size_download'] >= $info['download_content_length'])
  276. {
  277. $array['status'] = $info['http_code'];
  278. $array['response'] = curl_multi_getcontent($download['ch']);
  279. }
  280. else
  281. {
  282. $array['status'] = false;
  283. $array['response'] = "";
  284. }
  285. }
  286. else
  287. {
  288. $array['status'] = $info['http_code'];
  289. $array['response'] = curl_multi_getcontent($download['ch']);
  290. }
  291. $downloads[] = $array;
  292. curl_multi_remove_handle($this->mh, $download['ch']);
  293. curl_close($download['ch']);
  294. unset($this->ch[$download['id']]);
  295. }
  296. if (isset($downloads) and (count($downloads) > 0))
  297. return $downloads;
  298. }
  299. return false;
  300. }
  301. function stopDownloads()
  302. {
  303. if (isset($this->mh))
  304. {
  305. if (isset($this->ch))
  306. {
  307. foreach ($this->ch as $download)
  308. {
  309. curl_multi_remove_handle($this->mh, $download['ch']);
  310. curl_close($download['ch']);
  311. }
  312. unset($this->ch);
  313. }
  314. curl_multi_close($this->mh);
  315. unset($this->mh);
  316. }
  317. }
  318. function error($error)
  319. {
  320. LogError("cURL Error : $error");
  321. }
  322. }
  323. class F4F
  324. {
  325. var $audio, $auth, $baseFilename, $baseTS, $bootstrapUrl, $baseUrl, $debug, $duration, $fileCount, $filesize;
  326. var $fixWindow, $format, $live, $media, $outDir, $outFile, $parallel, $play, $processed, $quality, $rename, $video;
  327. var $prevTagSize, $tagHeaderLen;
  328. var $segTable, $fragTable, $segNum, $fragNum, $frags, $fragCount, $fragsPerSeg, $lastFrag, $fragUrl, $discontinuity;
  329. var $prevAudioTS, $prevVideoTS, $pAudioTagLen, $pVideoTagLen, $pAudioTagPos, $pVideoTagPos;
  330. var $prevAVC_Header, $prevAAC_Header, $AVC_HeaderWritten, $AAC_HeaderWritten;
  331. function __construct()
  332. {
  333. $this->auth = "";
  334. $this->baseFilename = "";
  335. $this->bootstrapUrl = "";
  336. $this->debug = false;
  337. $this->duration = 0;
  338. $this->fileCount = 1;
  339. $this->fixWindow = 1000;
  340. $this->format = "";
  341. $this->live = false;
  342. $this->outDir = "";
  343. $this->outFile = "";
  344. $this->parallel = 8;
  345. $this->play = false;
  346. $this->processed = false;
  347. $this->quality = "high";
  348. $this->rename = false;
  349. $this->segTable = array();
  350. $this->fragTable = array();
  351. $this->segStart = false;
  352. $this->fragStart = false;
  353. $this->frags = array();
  354. $this->fragCount = 0;
  355. $this->fragsPerSeg = 0;
  356. $this->lastFrag = 0;
  357. $this->discontinuity = "";
  358. $this->InitDecoder();
  359. }
  360. function InitDecoder()
  361. {
  362. $this->audio = false;
  363. $this->baseAudioTS = INVALID_TIMESTAMP;
  364. $this->baseVideoTS = INVALID_TIMESTAMP;
  365. $this->filesize = 0;
  366. $this->video = false;
  367. $this->prevTagSize = 4;
  368. $this->tagHeaderLen = 11;
  369. $this->prevAudioTS = INVALID_TIMESTAMP;
  370. $this->prevVideoTS = INVALID_TIMESTAMP;
  371. $this->pAudioTagLen = 0;
  372. $this->pVideoTagLen = 0;
  373. $this->pAudioTagPos = 0;
  374. $this->pVideoTagPos = 0;
  375. $this->prevAVC_Header = false;
  376. $this->prevAAC_Header = false;
  377. $this->AVC_HeaderWritten = false;
  378. $this->AAC_HeaderWritten = false;
  379. }
  380. function GetManifest($cc, $manifest)
  381. {
  382. $status = $cc->get($manifest);
  383. if ($status == 403)
  384. LogError("Access Denied! Unable to download the manifest.");
  385. else if ($status != 200)
  386. LogError("Unable to download the manifest");
  387. $xml = simplexml_load_string(trim($cc->response));
  388. if (!$xml)
  389. LogError("Failed to load xml");
  390. $namespace = $xml->getDocNamespaces();
  391. $namespace = $namespace[''];
  392. $xml->registerXPathNamespace("ns", $namespace);
  393. return $xml;
  394. }
  395. function ParseManifest($cc, $manifest)
  396. {
  397. LogInfo("Processing manifest info....");
  398. $xml = $this->GetManifest($cc, $manifest);
  399. $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL");
  400. if (isset($baseUrl[0]))
  401. {
  402. $baseUrl = GetString($baseUrl[0]);
  403. if (substr($baseUrl, -1) != "/")
  404. $baseUrl .= "/";
  405. }
  406. else
  407. $baseUrl = "";
  408. $url = $xml->xpath("/ns:manifest/ns:media[@*]");
  409. if (isset($url[0]['href']))
  410. {
  411. foreach ($url as $manifest)
  412. {
  413. $bitrate = (int) $manifest['bitrate'];
  414. $entry =& $manifests[$bitrate];
  415. $entry['bitrate'] = $bitrate;
  416. $href = GetString($manifest['href']);
  417. if (substr($href, 0, 1) == "/")
  418. $href = substr($href, 1);
  419. $entry['url'] = NormalizePath($baseUrl . $href);
  420. $entry['xml'] = $this->GetManifest($cc, $entry['url']);
  421. }
  422. }
  423. else
  424. {
  425. $manifests[0]['bitrate'] = 0;
  426. $manifests[0]['url'] = $manifest;
  427. $manifests[0]['xml'] = $xml;
  428. }
  429. foreach ($manifests as $manifest)
  430. {
  431. $xml = $manifest['xml'];
  432. // Extract baseUrl from manifest url
  433. $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL");
  434. if (isset($baseUrl[0]))
  435. {
  436. $baseUrl = GetString($baseUrl[0]);
  437. if (substr($baseUrl, -1) == "/")
  438. $baseUrl = substr($baseUrl, 0, -1);
  439. }
  440. else
  441. {
  442. $baseUrl = $manifest['url'];
  443. if (strpos($baseUrl, '?') !== false)
  444. $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?'));
  445. $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/'));
  446. }
  447. if (!isHttpUrl($baseUrl))
  448. LogError("Provided manifest is not a valid HDS manifest");
  449. $streams = $xml->xpath("/ns:manifest/ns:media");
  450. foreach ($streams as $stream)
  451. {
  452. $array = array();
  453. foreach ($stream->attributes() as $k => $v)
  454. $array[strtolower($k)] = GetString($v);
  455. $array['metadata'] = GetString($stream->{'metadata'});
  456. $stream = $array;
  457. $bitrate = isset($stream['bitrate']) ? (int) $stream['bitrate'] : $manifest['bitrate'];
  458. $streamId = isset($stream[strtolower('streamId')]) ? $stream[strtolower('streamId')] : "";
  459. $mediaEntry =& $this->media[$bitrate];
  460. $mediaEntry['baseUrl'] = $baseUrl;
  461. if (substr($stream['url'], 0, 1) == "/")
  462. $mediaEntry['url'] = substr($stream['url'], 1);
  463. else
  464. $mediaEntry['url'] = $stream['url'];
  465. if (isset($stream[strtolower('bootstrapInfoId')]))
  466. $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo[@id='" . $stream[strtolower('bootstrapInfoId')] . "']");
  467. else
  468. $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo");
  469. if (isset($bootstrap[0]['url']))
  470. {
  471. $bootstrapUrl = GetString($bootstrap[0]['url']);
  472. if (!isHttpUrl($bootstrapUrl))
  473. $bootstrapUrl = $mediaEntry['baseUrl'] . "/$bootstrapUrl";
  474. $mediaEntry['bootstrapUrl'] = NormalizePath($bootstrapUrl);
  475. if ($cc->get($mediaEntry['bootstrapUrl']) != 200)
  476. LogError("Failed to get bootstrap info");
  477. $mediaEntry['bootstrap'] = $cc->response;
  478. }
  479. else
  480. $mediaEntry['bootstrap'] = base64_decode(GetString($bootstrap[0]));
  481. if (isset($stream['metadata']))
  482. $mediaEntry['metadata'] = base64_decode($stream['metadata']);
  483. else
  484. $mediaEntry['metadata'] = "";
  485. }
  486. }
  487. // Available qualities
  488. $bitrates = array();
  489. if (!count($this->media))
  490. LogError("No media entry found");
  491. krsort($this->media, SORT_NUMERIC);
  492. LogDebug("Manifest Entries:\n");
  493. LogDebug(sprintf(" %-8s%s", "Bitrate", "URL"));
  494. for ($i = 0; $i < count($this->media); $i++)
  495. {
  496. $key = KeyName($this->media, $i);
  497. $bitrates[] = $key;
  498. LogDebug(sprintf(" %-8d%s", $key, $this->media[$key]['url']));
  499. }
  500. LogDebug("");
  501. LogInfo("Quality Selection:\n Available: " . implode(' ', $bitrates));
  502. // Quality selection
  503. if (is_numeric($this->quality) and isset($this->media[$this->quality]))
  504. {
  505. $key = $this->quality;
  506. $this->media = $this->media[$key];
  507. }
  508. else
  509. {
  510. $this->quality = strtolower($this->quality);
  511. switch ($this->quality)
  512. {
  513. case "low":
  514. $this->quality = 2;
  515. break;
  516. case "medium":
  517. $this->quality = 1;
  518. break;
  519. default:
  520. $this->quality = 0;
  521. }
  522. while ($this->quality >= 0)
  523. {
  524. $key = KeyName($this->media, $this->quality);
  525. if ($key !== NULL)
  526. {
  527. $this->media = $this->media[$key];
  528. break;
  529. }
  530. else
  531. $this->quality -= 1;
  532. }
  533. }
  534. LogInfo(" Selected : " . $key);
  535. $this->baseUrl = $this->media['baseUrl'];
  536. if (isset($this->media['bootstrapUrl']))
  537. $this->bootstrapUrl = $this->media['bootstrapUrl'];
  538. $bootstrapInfo = $this->media['bootstrap'];
  539. ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
  540. if ($boxType == "abst")
  541. $this->ParseBootstrapBox($bootstrapInfo, $pos);
  542. else
  543. LogError("Failed to parse bootstrap info");
  544. }
  545. function UpdateBootstrapInfo($cc, $bootstrapUrl)
  546. {
  547. $fragNum = $this->fragCount;
  548. $retries = 0;
  549. while (($fragNum == $this->fragCount) and ($retries < 30))
  550. {
  551. $bootstrapPos = 0;
  552. LogDebug("Updating bootstrap info, Available fragments: " . $this->fragCount);
  553. if ($cc->get($bootstrapUrl) != 200)
  554. LogError("Failed to refresh bootstrap info");
  555. $bootstrapInfo = $cc->response;
  556. ReadBoxHeader($bootstrapInfo, $bootstrapPos, $boxType, $boxSize);
  557. if ($boxType == "abst")
  558. $this->ParseBootstrapBox($bootstrapInfo, $bootstrapPos);
  559. else
  560. LogError("Failed to parse bootstrap info");
  561. LogDebug("Update complete, Available fragments: " . $this->fragCount);
  562. if ($fragNum == $this->fragCount)
  563. {
  564. LogInfo("Updating bootstrap info, Retries: " . ++$retries, true);
  565. usleep(4000000);
  566. }
  567. }
  568. }
  569. function ParseBootstrapBox($bootstrapInfo, $pos)
  570. {
  571. $version = ReadByte($bootstrapInfo, $pos);
  572. $flags = ReadInt24($bootstrapInfo, $pos + 1);
  573. $bootstrapVersion = ReadInt32($bootstrapInfo, $pos + 4);
  574. $byte = ReadByte($bootstrapInfo, $pos + 8);
  575. $profile = ($byte & 0xC0) >> 6;
  576. if (($byte & 0x20) >> 5)
  577. $this->live = true;
  578. $update = ($byte & 0x10) >> 4;
  579. $timescale = ReadInt32($bootstrapInfo, $pos + 9);
  580. $currentMediaTime = ReadInt64($bootstrapInfo, 13);
  581. $smpteTimeCodeOffset = ReadInt64($bootstrapInfo, 21);
  582. $pos += 29;
  583. $movieIdentifier = ReadString($bootstrapInfo, $pos);
  584. $serverEntryCount = ReadByte($bootstrapInfo, $pos++);
  585. for ($i = 0; $i < $serverEntryCount; $i++)
  586. $serverEntryTable[$i] = ReadString($bootstrapInfo, $pos);
  587. $qualityEntryCount = ReadByte($bootstrapInfo, $pos++);
  588. for ($i = 0; $i < $qualityEntryCount; $i++)
  589. $qualityEntryTable[$i] = ReadString($bootstrapInfo, $pos);
  590. $drmData = ReadString($bootstrapInfo, $pos);
  591. $metadata = ReadString($bootstrapInfo, $pos);
  592. $segRunTableCount = ReadByte($bootstrapInfo, $pos++);
  593. for ($i = 0; $i < $segRunTableCount; $i++)
  594. {
  595. ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
  596. if ($boxType == "asrt")
  597. $this->ParseAsrtBox($bootstrapInfo, $pos);
  598. $pos += $boxSize;
  599. }
  600. $fragRunTableCount = ReadByte($bootstrapInfo, $pos++);
  601. for ($i = 0; $i < $fragRunTableCount; $i++)
  602. {
  603. ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
  604. if ($boxType == "afrt")
  605. $this->ParseAfrtBox($bootstrapInfo, $pos);
  606. $pos += $boxSize;
  607. }
  608. }
  609. function ParseAsrtBox($asrt, $pos)
  610. {
  611. $version = ReadByte($asrt, $pos);
  612. $flags = ReadInt24($asrt, $pos + 1);
  613. $qualityEntryCount = ReadByte($asrt, $pos + 4);
  614. $pos += 5;
  615. for ($i = 0; $i < $qualityEntryCount; $i++)
  616. $qualitySegmentUrlModifiers[$i] = ReadString($asrt, $pos);
  617. $segCount = ReadInt32($asrt, $pos);
  618. $pos += 4;
  619. LogDebug(sprintf("%s:\n\n %-8s%-10s", "Segment Entries", "Number", "Fragments"));
  620. for ($i = 0; $i < $segCount; $i++)
  621. {
  622. $firstSegment = ReadInt32($asrt, $pos);
  623. $segEntry =& $this->segTable[$firstSegment];
  624. $segEntry['firstSegment'] = $firstSegment;
  625. $segEntry['fragmentsPerSegment'] = ReadInt32($asrt, $pos + 4);
  626. if ($segEntry['fragmentsPerSegment'] & 0x80000000)
  627. $segEntry['fragmentsPerSegment'] = 0;
  628. $pos += 8;
  629. }
  630. unset($segEntry);
  631. foreach ($this->segTable as $segEntry)
  632. LogDebug(sprintf(" %-8s%-10s", $segEntry['firstSegment'], $segEntry['fragmentsPerSegment']));
  633. LogDebug("");
  634. $lastSegment = end($this->segTable);
  635. if ($this->segStart === false)
  636. $this->segStart = $lastSegment['firstSegment'];
  637. $this->fragCount = $lastSegment['fragmentsPerSegment'];
  638. // Use segment table in case of multiple segments
  639. if (count($this->segTable) > 1)
  640. {
  641. $secondLastSegment = prev($this->segTable);
  642. if ($this->fragStart === false)
  643. {
  644. $this->fragsPerSeg = $secondLastSegment['fragmentsPerSegment'];
  645. $this->fragStart = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount - 2;
  646. $this->fragCount = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount;
  647. }
  648. else
  649. $this->fragCount = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount;
  650. }
  651. }
  652. function ParseAfrtBox($afrt, $pos)
  653. {
  654. $version = ReadByte($afrt, $pos);
  655. $flags = ReadInt24($afrt, $pos + 1);
  656. $timescale = ReadInt32($afrt, $pos + 4);
  657. $qualityEntryCount = ReadByte($afrt, $pos + 8);
  658. $pos += 9;
  659. for ($i = 0; $i < $qualityEntryCount; $i++)
  660. $qualitySegmentUrlModifiers[$i] = ReadString($afrt, $pos);
  661. $fragEntries = ReadInt32($afrt, $pos);
  662. $pos += 4;
  663. LogDebug(sprintf("%s:\n\n %-8s%-16s%-16s%-16s", "Fragment Entries", "Number", "Timestamp", "Duration", "Discontinuity"));
  664. for ($i = 0; $i < $fragEntries; $i++)
  665. {
  666. $firstFragment = ReadInt32($afrt, $pos);
  667. $fragEntry =& $this->fragTable[$firstFragment];
  668. $fragEntry['firstFragment'] = $firstFragment;
  669. $fragEntry['firstFragmentTimestamp'] = ReadInt64($afrt, $pos + 4);
  670. $fragEntry['fragmentDuration'] = ReadInt32($afrt, $pos + 12);
  671. $fragEntry['discontinuityIndicator'] = "";
  672. $pos += 16;
  673. if ($fragEntry['fragmentDuration'] == 0)
  674. $fragEntry['discontinuityIndicator'] = ReadByte($afrt, $pos++);
  675. }
  676. unset($fragEntry);
  677. foreach ($this->fragTable as $fragEntry)
  678. LogDebug(sprintf(" %-8s%-16s%-16s%-16s", $fragEntry['firstFragment'], $fragEntry['firstFragmentTimestamp'], $fragEntry['fragmentDuration'], $fragEntry['discontinuityIndicator']));
  679. LogDebug("");
  680. // Use fragment table in case of single segment
  681. if (count($this->segTable) == 1)
  682. {
  683. $firstFragment = reset($this->fragTable);
  684. $lastFragment = end($this->fragTable);
  685. if ($this->fragStart === false)
  686. {
  687. if ($this->live)
  688. $this->fragStart = $lastFragment['firstFragment'] - 2;
  689. else
  690. $this->fragStart = $firstFragment['firstFragment'] - 1;
  691. if ($this->fragStart < 0)
  692. $this->fragStart = 0;
  693. }
  694. if ($this->fragCount > 0)
  695. $this->fragCount += $firstFragment['firstFragment'] - 1;
  696. if ($this->fragCount < $lastFragment['firstFragment'])
  697. $this->fragCount = $lastFragment['firstFragment'];
  698. }
  699. }
  700. function DownloadFragments($cc, $manifest, $opt = array())
  701. {
  702. $start = 0;
  703. extract($opt, EXTR_IF_EXISTS);
  704. $this->ParseManifest($cc, $manifest);
  705. $segNum = $this->segStart;
  706. $fragNum = $this->fragStart;
  707. if ($start)
  708. {
  709. if ($segNum > 1)
  710. if ($start % $this->fragsPerSeg)
  711. $segNum = (int) ($start / $this->fragsPerSeg + 1);
  712. else
  713. $segNum = (int) ($start / $this->fragsPerSeg);
  714. $fragNum = $start - 1;
  715. $this->segStart = $segNum;
  716. $this->fragStart = $fragNum;
  717. }
  718. $this->lastFrag = $fragNum;
  719. $opt['cc'] = $cc;
  720. $opt['duration'] = 0;
  721. // Extract baseFilename
  722. $this->baseFilename = $this->media['url'];
  723. if (substr($this->baseFilename, -1) == '/')
  724. $this->baseFilename = substr($this->baseFilename, 0, -1);
  725. $this->baseFilename = RemoveExtension($this->baseFilename);
  726. if (strrpos($this->baseFilename, '/'))
  727. $this->baseFilename = substr($this->baseFilename, strrpos($this->baseFilename, '/') + 1);
  728. if (strpos($manifest, "?"))
  729. $this->baseFilename = md5(substr($manifest, 0, strpos($manifest, "?"))) . "_" . $this->baseFilename;
  730. else
  731. $this->baseFilename = md5($manifest) . "_" . $this->baseFilename;
  732. $this->baseFilename .= "Seg" . $segNum . "-Frag";
  733. if ($fragNum >= $this->fragCount)
  734. LogError("No fragment available for downloading");
  735. if (isHttpUrl($this->media['url']))
  736. $this->fragUrl = $this->media['url'];
  737. else
  738. $this->fragUrl = $this->baseUrl . "/" . $this->media['url'];
  739. $this->fragUrl = NormalizePath($this->fragUrl);
  740. LogDebug("Base Fragment Url:\n" . $this->fragUrl . "\n");
  741. LogDebug("Downloading Fragments:\n");
  742. while (($fragNum < $this->fragCount) or $cc->active)
  743. {
  744. while ((count($cc->ch) < $this->parallel) and ($fragNum < $this->fragCount))
  745. {
  746. $frag = array();
  747. $fragNum = $fragNum + 1;
  748. $frag['id'] = $fragNum;
  749. LogInfo("Downloading $fragNum/$this->fragCount fragments", true);
  750. if (in_array_field($fragNum, "firstFragment", $this->fragTable, true))
  751. $this->discontinuity = value_in_array_field($fragNum, "firstFragment", "discontinuityIndicator", $this->fragTable, true);
  752. else
  753. {
  754. $closest = 1;
  755. foreach ($this->fragTable as $item)
  756. {
  757. if ($item['firstFragment'] < $fragNum)
  758. $closest = $item['firstFragment'];
  759. else
  760. break;
  761. }
  762. $this->discontinuity = value_in_array_field($closest, "firstFragment", "discontinuityIndicator", $this->fragTable, true);
  763. }
  764. if (($this->discontinuity == 1) or ($this->discontinuity == 3))
  765. {
  766. LogDebug("Skipping fragment $fragNum due to discontinuity");
  767. $frag['response'] = false;
  768. $this->rename = true;
  769. }
  770. else if (file_exists($this->baseFilename . $fragNum))
  771. {
  772. LogDebug("Fragment $fragNum is already downloaded");
  773. $frag['response'] = file_get_contents($this->baseFilename . $fragNum);
  774. }
  775. if (isset($frag['response']))
  776. {
  777. if ($this->WriteFragment($frag, $opt) === 2)
  778. break 2;
  779. else
  780. continue;
  781. }
  782. /* Increase or decrease segment number if current fragment is not available */
  783. /* in selected segment range */
  784. if (count($this->segTable) > 1)
  785. {
  786. if ($fragNum > ($segNum * $this->fragsPerSeg))
  787. $segNum++;
  788. else if ($fragNum <= (($segNum - 1) * $this->fragsPerSeg))
  789. $segNum--;
  790. }
  791. LogDebug("Adding fragment $fragNum to download queue");
  792. $cc->addDownload($this->fragUrl . "Seg" . $segNum . "-Frag" . $fragNum . $this->auth, $fragNum);
  793. }
  794. $downloads = $cc->checkDownloads();
  795. if ($downloads !== false)
  796. {
  797. for ($i = 0; $i < count($downloads); $i++)
  798. {
  799. $frag = array();
  800. $download = $downloads[$i];
  801. $frag['id'] = $download['id'];
  802. if ($download['status'] == 200)
  803. {
  804. if ($this->VerifyFragment($download['response']))
  805. {
  806. LogDebug("Fragment " . $this->baseFilename . $download['id'] . " successfully downloaded");
  807. if (!($this->live or $this->play))
  808. file_put_contents($this->baseFilename . $download['id'], $download['response']);
  809. $frag['response'] = $download['response'];
  810. }
  811. else
  812. {
  813. LogDebug("Fragment " . $download['id'] . " failed to verify");
  814. LogDebug("Adding fragment " . $download['id'] . " to download queue");
  815. $cc->addDownload($download['url'], $download['id']);
  816. }
  817. }
  818. else if ($download['status'] === false)
  819. {
  820. LogDebug("Fragment " . $download['id'] . " failed to download");
  821. LogDebug("Adding fragment " . $download['id'] . " to download queue");
  822. $cc->addDownload($download['url'], $download['id']);
  823. }
  824. else if ($download['status'] == 403)
  825. LogError("Access Denied! Unable to download fragments.");
  826. else
  827. {
  828. LogDebug("Fragment " . $download['id'] . " doesn't exist, Status: " . $download['status']);
  829. $frag['response'] = false;
  830. $this->rename = true;
  831. /* Resync with latest available fragment when we are left behind due to */
  832. /* slow connection and short live window on streaming server. make sure */
  833. /* to reset the last written fragment. */
  834. if ($this->live and ($i + 1 == count($downloads)) and !$cc->active)
  835. {
  836. LogDebug("Trying to resync with latest available fragment");
  837. if ($this->WriteFragment($frag, $opt) === 2)
  838. break 2;
  839. unset($frag['response']);
  840. $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl);
  841. $fragNum = $this->fragCount - 1;
  842. $this->lastFrag = $fragNum;
  843. }
  844. }
  845. if (isset($frag['response']))
  846. if ($this->WriteFragment($frag, $opt) === 2)
  847. break 2;
  848. }
  849. unset($downloads, $download);
  850. }
  851. if ($this->live and ($fragNum >= $this->fragCount) and !$cc->active)
  852. $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl);
  853. }
  854. LogInfo("");
  855. LogDebug("\nAll fragments downloaded successfully\n");
  856. $cc->stopDownloads();
  857. $this->processed = true;
  858. }
  859. function VerifyFragment(&$frag)
  860. {
  861. $fragPos = 0;
  862. $fragLen = strlen($frag);
  863. /* Some moronic servers add wrong boxSize in header causing fragment verification *
  864. * to fail so we have to fix the boxSize before processing the fragment. */
  865. while ($fragPos < $fragLen)
  866. {
  867. ReadBoxHeader($frag, $fragPos, $boxType, $boxSize);
  868. if ($boxType == "mdat")
  869. {
  870. $len = strlen(substr($frag, $fragPos, $boxSize));
  871. if ($boxSize and ($len == $boxSize))
  872. return true;
  873. else
  874. {
  875. $boxSize = $fragLen - $fragPos;
  876. WriteBoxSize($frag, $boxType, $boxSize);
  877. return true;
  878. }
  879. }
  880. $fragPos += $boxSize;
  881. }
  882. return false;
  883. }
  884. function RenameFragments($baseFilename, $fragNum, $fileExt)
  885. {
  886. $files = array();
  887. $retries = 0;
  888. while (true)
  889. {
  890. if ($retries >= 50)
  891. break;
  892. $file = $baseFilename . ++$fragNum;
  893. if (file_exists($file))
  894. {
  895. $files[] = $file;
  896. $retries = 0;
  897. }
  898. else if (file_exists($file . $fileExt))
  899. {
  900. $files[] = $file;
  901. $retries = 0;
  902. }
  903. else
  904. $retries++;
  905. }
  906. $fragCount = count($files);
  907. natsort($files);
  908. for ($i = 0; $i < $fragCount; $i++)
  909. rename($files[$i], $baseFilename . ($i + 1));
  910. }
  911. function WriteMetadata($flv = false)
  912. {
  913. if (isset($this->media) and $this->media['metadata'])
  914. {
  915. $metadataSize = strlen($this->media['metadata']);
  916. WriteByte($metadata, 0, SCRIPT_DATA);
  917. WriteInt24($metadata, 1, $metadataSize);
  918. WriteInt24($metadata, 4, 0);
  919. WriteInt32($metadata, 7, 0);
  920. $metadata = implode("", $metadata) . $this->media['metadata'];
  921. WriteByte($metadata, $this->tagHeaderLen + $metadataSize - 1, 0x09);
  922. WriteInt32($metadata, $this->tagHeaderLen + $metadataSize, $this->tagHeaderLen + $metadataSize);
  923. if (is_resource($flv))
  924. {
  925. fwrite($flv, $metadata, $this->tagHeaderLen + $metadataSize + $this->prevTagSize);
  926. return true;
  927. }
  928. else
  929. return $metadata;
  930. }
  931. return false;
  932. }
  933. function WriteFlvTimestamp(&$frag, $fragPos, $packetTS)
  934. {
  935. WriteInt24($frag, $fragPos + 4, ($packetTS & 0x00FFFFFF));
  936. WriteByte($frag, $fragPos + 7, ($packetTS & 0xFF000000) >> 24);
  937. }
  938. function DecodeFragment($frag, $fragNum, $opt = array())
  939. {
  940. $debug = $this->debug;
  941. $flv = false;
  942. extract($opt, EXTR_IF_EXISTS);
  943. $flvData = "";
  944. $fragPos = 0;
  945. $packetTS = 0;
  946. $fragLen = strlen($frag);
  947. if (!$this->VerifyFragment($frag))
  948. {
  949. LogInfo("Skipping fragment number $fragNum");
  950. return false;
  951. }
  952. while ($fragPos < $fragLen)
  953. {
  954. ReadBoxHeader($frag, $fragPos, $boxType, $boxSize);
  955. if ($boxType == "mdat")
  956. {
  957. $fragLen = $fragPos + $boxSize;
  958. break;
  959. }
  960. $fragPos += $boxSize;
  961. }
  962. LogDebug(sprintf("\nFragment %d:\n" . $this->format . "%-16s", $fragNum, "Type", "CurrentTS", "PreviousTS", "Size", "Position"), $debug);
  963. while ($fragPos < $fragLen)
  964. {
  965. $packetType = ReadByte($frag, $fragPos);
  966. $packetSize = ReadInt24($frag, $fragPos + 1);
  967. $packetTS = ReadInt24($frag, $fragPos + 4);
  968. $packetTS = $packetTS | (ReadByte($frag, $fragPos + 7) << 24);
  969. if ($packetTS & 0x80000000)
  970. $packetTS &= 0x7FFFFFFF;
  971. $totalTagLen = $this->tagHeaderLen + $packetSize + $this->prevTagSize;
  972. // Try to fix the odd timestamps and make them zero based
  973. switch ($packetType)
  974. {
  975. case AUDIO:
  976. if ($this->baseAudioTS == INVALID_TIMESTAMP)
  977. $this->baseAudioTS = $packetTS;
  978. if ($this->baseAudioTS > 1000)
  979. {
  980. if ($packetTS >= $this->baseAudioTS)
  981. $packetTS -= $this->baseAudioTS;
  982. else
  983. $packetTS = $this->prevAudioTS + FRAMEGAP_DURATION * 5;
  984. }
  985. if ($this->prevAudioTS != INVALID_TIMESTAMP)
  986. {
  987. $timeShift = $packetTS - $this->prevAudioTS;
  988. if ($timeShift > $this->fixWindow)
  989. {
  990. $this->baseAudioTS += $timeShift - FRAMEGAP_DURATION * 5;
  991. $packetTS = $this->prevAudioTS + FRAMEGAP_DURATION * 5;
  992. }
  993. }
  994. $this->WriteFlvTimestamp($frag, $fragPos, $packetTS);
  995. break;
  996. case VIDEO:
  997. if ($this->baseVideoTS == INVALID_TIMESTAMP)
  998. $this->baseVideoTS = $packetTS;
  999. if ($this->baseVideoTS > 1000)
  1000. {
  1001. if ($packetTS >= $this->baseVideoTS)
  1002. $packetTS -= $this->baseVideoTS;
  1003. else
  1004. $packetTS = $this->prevVideoTS + FRAMEGAP_DURATION * 5;
  1005. }
  1006. if ($this->prevVideoTS != INVALID_TIMESTAMP)
  1007. {
  1008. $timeShift = $packetTS - $this->prevVideoTS;
  1009. if ($timeShift > $this->fixWindow)
  1010. {
  1011. $this->baseVideoTS += $timeShift - FRAMEGAP_DURATION * 5;
  1012. $packetTS = $this->prevVideoTS + FRAMEGAP_DURATION * 5;
  1013. }
  1014. }
  1015. $this->WriteFlvTimestamp($frag, $fragPos, $packetTS);
  1016. break;
  1017. }
  1018. switch ($packetType)
  1019. {
  1020. case AUDIO:
  1021. if ($packetTS > $this->prevAudioTS - $this->fixWindow)
  1022. {
  1023. $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen);
  1024. $CodecID = ($FrameInfo & 0xF0) >> 4;
  1025. if ($CodecID == CODEC_ID_AAC)
  1026. {
  1027. $AAC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1);
  1028. if ($AAC_PacketType == AAC_SEQUENCE_HEADER)
  1029. {
  1030. if ($this->AAC_HeaderWritten)
  1031. {
  1032. LogDebug(sprintf("%s\n" . $this->format, "Skipping AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
  1033. break;
  1034. }
  1035. else
  1036. {
  1037. LogDebug("Writing AAC sequence header", $debug);
  1038. $this->AAC_HeaderWritten = true;
  1039. }
  1040. }
  1041. else if (!$this->AAC_HeaderWritten)
  1042. {
  1043. LogDebug(sprintf("%s\n" . $this->format, "Discarding audio packet received before AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
  1044. break;
  1045. }
  1046. }
  1047. if ($packetSize > 0)
  1048. {
  1049. // Check for packets with non-monotonic audio timestamps and fix them
  1050. if (!(($CodecID == CODEC_ID_AAC) and (($AAC_PacketType == AAC_SEQUENCE_HEADER) or $this->prevAAC_Header)))
  1051. if (($this->prevAudioTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevAudioTS))
  1052. {
  1053. LogDebug(sprintf("%s\n" . $this->format, "Fixing audio timestamp", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
  1054. $packetTS += FRAMEGAP_DURATION + ($this->prevAudioTS - $packetTS);
  1055. $this->WriteFlvTimestamp($frag, $fragPos, $packetTS);
  1056. }
  1057. if (is_resource($flv))
  1058. {
  1059. $this->pAudioTagPos = ftell($flv);
  1060. $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen);
  1061. if

Large files files are truncated, but you can click here to view the full file