SHOW:
|
|
- or go back to the newest paste.
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('FRAMEFIX_STEP', 40); | |
12 | define('INVALID_TIMESTAMP', -1); | |
13 | ||
14 | class CLI | |
15 | { | |
16 | protected static $ACCEPTED = array( | |
17 | 0 => array( | |
18 | 'help' => 'displays this help', | |
19 | 'debug' => 'show debug output', | |
20 | 'delete' => 'delete fragments after processing', | |
21 | 'fproxy' => 'force proxy for downloading of fragments', | |
22 | 'play' => 'dump stream to stdout for piping to media player', | |
23 | 'rename' => 'rename fragments sequentially before processing', | |
24 | 'update' => 'update the script to current git version' | |
25 | ), | |
26 | 1 => array( | |
27 | 'auth' => 'authentication string for fragment requests', | |
28 | 'duration' => 'stop recording after specified number of seconds', | |
29 | 'filesize' => 'split output file in chunks of specified size (MB)', | |
30 | 'fragments' => 'base filename for fragments', | |
31 | 'fixwindow' => 'timestamp gap between frames to consider as timeshift', | |
32 | 'manifest' => 'manifest file for downloading of fragments', | |
33 | 'outdir' => 'destination folder for output file', | |
34 | 'outfile' => 'filename to use for output file', | |
35 | 'parallel' => 'number of fragments to download simultaneously', | |
36 | 'proxy' => 'proxy for downloading of manifest', | |
37 | 'quality' => 'selected quality level (low|medium|high) or exact bitrate', | |
38 | 'referrer' => 'Referer to use for emulation of browser requests', | |
39 | 'start' => 'start from specified fragment', | |
40 | 'useragent' => 'User-Agent to use for emulation of browser requests' | |
41 | ) | |
42 | ); | |
43 | var $params = array(); | |
44 | ||
45 | function __construct() | |
46 | { | |
47 | global $argc, $argv; | |
48 | ||
49 | // Parse params | |
50 | if ($argc > 1) | |
51 | { | |
52 | $paramSwitch = false; | |
53 | for ($i = 1; $i < $argc; $i++) | |
54 | { | |
55 | $arg = $argv[$i]; | |
56 | $isSwitch = preg_match('/^--/', $arg); | |
57 | ||
58 | if ($isSwitch) | |
59 | $arg = preg_replace('/^--/', '', $arg); | |
60 | ||
61 | if ($paramSwitch && $isSwitch) | |
62 | LogError("[param] expected after '$paramSwitch' switch (" . self::$ACCEPTED[1][$paramSwitch] . ")"); | |
63 | else if (!$paramSwitch && !$isSwitch) | |
64 | { | |
65 | if (isset($GLOBALS['baseFilename']) and (!$GLOBALS['baseFilename'])) | |
66 | $GLOBALS['baseFilename'] = $arg; | |
67 | else | |
68 | LogError("'$arg' is an invalid switch, use --help to display valid switches."); | |
69 | } | |
70 | else if (!$paramSwitch && $isSwitch) | |
71 | { | |
72 | if (isset($this->params[$arg])) | |
73 | LogError("'$arg' switch cannot occur more than once"); | |
74 | ||
75 | $this->params[$arg] = true; | |
76 | if (isset(self::$ACCEPTED[1][$arg])) | |
77 | $paramSwitch = $arg; | |
78 | else if (!isset(self::$ACCEPTED[0][$arg])) | |
79 | LogError("there's no '$arg' switch, use --help to display all switches."); | |
80 | } | |
81 | else if ($paramSwitch && !$isSwitch) | |
82 | { | |
83 | $this->params[$paramSwitch] = $arg; | |
84 | $paramSwitch = false; | |
85 | } | |
86 | } | |
87 | } | |
88 | ||
89 | // Final check | |
90 | foreach ($this->params as $k => $v) | |
91 | if (isset(self::$ACCEPTED[1][$k]) && $v === true) | |
92 | LogError("[param] expected after '$k' switch (" . self::$ACCEPTED[1][$k] . ")"); | |
93 | } | |
94 | ||
95 | function getParam($name) | |
96 | { | |
97 | if (isset($this->params[$name])) | |
98 | return $this->params[$name]; | |
99 | else | |
100 | return ""; | |
101 | } | |
102 | ||
103 | function displayHelp() | |
104 | { | |
105 | LogInfo("You can use script with following switches: \n"); | |
106 | foreach (self::$ACCEPTED[0] as $key => $value) | |
107 | LogInfo(sprintf(" --%-18s%s", $key, $value)); | |
108 | foreach (self::$ACCEPTED[1] as $key => $value) | |
109 | LogInfo(sprintf(" --%-9s%-9s%s", $key, " [param]", $value)); | |
110 | } | |
111 | } | |
112 | ||
113 | class cURL | |
114 | { | |
115 | var $headers, $user_agent, $compression, $cookie_file; | |
116 | var $active, $cert_check, $fragProxy, $proxy, $response; | |
117 | var $mh, $ch, $mrc; | |
118 | static $ref = 0; | |
119 | ||
120 | function cURL($cookies = true, $cookie = 'Cookies.txt', $compression = 'gzip', $proxy = '') | |
121 | { | |
122 | $this->headers = $this->headers(); | |
123 | $this->user_agent = 'Mozilla/5.0 (Windows NT 5.1; rv:18.0) Gecko/20100101 Firefox/18.0'; | |
124 | $this->compression = $compression; | |
125 | $this->cookies = $cookies; | |
126 | if ($this->cookies == true) | |
127 | $this->cookie($cookie); | |
128 | $this->cert_check = true; | |
129 | $this->fragProxy = false; | |
130 | $this->proxy = $proxy; | |
131 | self::$ref++; | |
132 | } | |
133 | ||
134 | function __destruct() | |
135 | { | |
136 | $this->stopDownloads(); | |
137 | if ((self::$ref <= 1) and file_exists($this->cookie_file)) | |
138 | unlink($this->cookie_file); | |
139 | self::$ref--; | |
140 | } | |
141 | ||
142 | function headers() | |
143 | { | |
144 | $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; | |
145 | $headers[] = 'Connection: Keep-Alive'; | |
146 | return $headers; | |
147 | } | |
148 | ||
149 | function cookie($cookie_file) | |
150 | { | |
151 | if (file_exists($cookie_file)) | |
152 | $this->cookie_file = $cookie_file; | |
153 | else | |
154 | { | |
155 | $file = fopen($cookie_file, 'w') or $this->error('The cookie file could not be opened. Make sure this directory has the correct permissions.'); | |
156 | $this->cookie_file = $cookie_file; | |
157 | fclose($file); | |
158 | } | |
159 | } | |
160 | ||
161 | function get($url) | |
162 | { | |
163 | $process = curl_init($url); | |
164 | curl_setopt($process, CURLOPT_HTTPHEADER, $this->headers); | |
165 | curl_setopt($process, CURLOPT_HEADER, 0); | |
166 | curl_setopt($process, CURLOPT_USERAGENT, $this->user_agent); | |
167 | if ($this->cookies == true) | |
168 | { | |
169 | curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); | |
170 | curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); | |
171 | } | |
172 | curl_setopt($process, CURLOPT_ENCODING, $this->compression); | |
173 | curl_setopt($process, CURLOPT_TIMEOUT, 60); | |
174 | if ($this->proxy) | |
175 | $this->setProxy($process, $this->proxy); | |
176 | curl_setopt($process, CURLOPT_RETURNTRANSFER, 1); | |
177 | curl_setopt($process, CURLOPT_FOLLOWLOCATION, 1); | |
178 | if (!$this->cert_check) | |
179 | curl_setopt($process, CURLOPT_SSL_VERIFYPEER, 0); | |
180 | $this->response = curl_exec($process); | |
181 | if ($this->response !== false) | |
182 | $status = curl_getinfo($process, CURLINFO_HTTP_CODE); | |
183 | curl_close($process); | |
184 | if (isset($status)) | |
185 | return $status; | |
186 | else | |
187 | return false; | |
188 | } | |
189 | ||
190 | function post($url, $data) | |
191 | { | |
192 | $process = curl_init($url); | |
193 | $headers = $this->headers; | |
194 | $headers[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'; | |
195 | curl_setopt($process, CURLOPT_HTTPHEADER, $headers); | |
196 | curl_setopt($process, CURLOPT_HEADER, 1); | |
197 | curl_setopt($process, CURLOPT_USERAGENT, $this->user_agent); | |
198 | if ($this->cookies == true) | |
199 | { | |
200 | curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file); | |
201 | curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file); | |
202 | } | |
203 | curl_setopt($process, CURLOPT_ENCODING, $this->compression); | |
204 | curl_setopt($process, CURLOPT_TIMEOUT, 60); | |
205 | if ($this->proxy) | |
206 | $this->setProxy($process, $this->proxy); | |
207 | curl_setopt($process, CURLOPT_POSTFIELDS, $data); | |
208 | curl_setopt($process, CURLOPT_RETURNTRANSFER, 1); | |
209 | curl_setopt($process, CURLOPT_FOLLOWLOCATION, 1); | |
210 | curl_setopt($process, CURLOPT_POST, 1); | |
211 | if (!$this->cert_check) | |
212 | curl_setopt($process, CURLOPT_SSL_VERIFYPEER, 0); | |
213 | $return = curl_exec($process); | |
214 | curl_close($process); | |
215 | return $return; | |
216 | } | |
217 | ||
218 | function setProxy(&$process, $proxy) | |
219 | { | |
220 | $type = substr($proxy, 0, stripos($proxy, "://")); | |
221 | if ($type) | |
222 | { | |
223 | $type = strtolower($type); | |
224 | $proxy = substr($proxy, stripos($proxy, "://") + 3); | |
225 | } | |
226 | switch ($type) | |
227 | { | |
228 | case "socks4": | |
229 | $type = CURLPROXY_SOCKS4; | |
230 | break; | |
231 | case "socks5": | |
232 | $type = CURLPROXY_SOCKS5; | |
233 | break; | |
234 | default: | |
235 | $type = CURLPROXY_HTTP; | |
236 | } | |
237 | curl_setopt($process, CURLOPT_PROXY, $proxy); | |
238 | curl_setopt($process, CURLOPT_PROXYTYPE, $type); | |
239 | } | |
240 | ||
241 | function addDownload($url, $id) | |
242 | { | |
243 | if (!isset($this->mh)) | |
244 | $this->mh = curl_multi_init(); | |
245 | if (isset($this->ch[$id])) | |
246 | return; | |
247 | else | |
248 | $download =& $this->ch[$id]; | |
249 | $download['id'] = $id; | |
250 | $download['url'] = $url; | |
251 | $download['ch'] = curl_init($url); | |
252 | curl_setopt($download['ch'], CURLOPT_HTTPHEADER, $this->headers); | |
253 | curl_setopt($download['ch'], CURLOPT_HEADER, 0); | |
254 | curl_setopt($download['ch'], CURLOPT_USERAGENT, $this->user_agent); | |
255 | if ($this->cookies == true) | |
256 | { | |
257 | curl_setopt($download['ch'], CURLOPT_COOKIEFILE, $this->cookie_file); | |
258 | curl_setopt($download['ch'], CURLOPT_COOKIEJAR, $this->cookie_file); | |
259 | } | |
260 | curl_setopt($download['ch'], CURLOPT_ENCODING, $this->compression); | |
261 | curl_setopt($download['ch'], CURLOPT_TIMEOUT, 300); | |
262 | if ($this->fragProxy and $this->proxy) | |
263 | $this->setProxy($download['ch'], $this->proxy); | |
264 | curl_setopt($download['ch'], CURLOPT_BINARYTRANSFER, 1); | |
265 | curl_setopt($download['ch'], CURLOPT_RETURNTRANSFER, 1); | |
266 | curl_setopt($download['ch'], CURLOPT_FOLLOWLOCATION, 1); | |
267 | if (!$this->cert_check) | |
268 | curl_setopt($download['ch'], CURLOPT_SSL_VERIFYPEER, 0); | |
269 | curl_multi_add_handle($this->mh, $download['ch']); | |
270 | do | |
271 | { | |
272 | $this->mrc = curl_multi_exec($this->mh, $this->active); | |
273 | } while ($this->mrc == CURLM_CALL_MULTI_PERFORM); | |
274 | } | |
275 | ||
276 | function checkDownloads() | |
277 | { | |
278 | if (isset($this->mh)) | |
279 | { | |
280 | curl_multi_select($this->mh); | |
281 | $this->mrc = curl_multi_exec($this->mh, $this->active); | |
282 | if ($this->mrc != CURLM_OK) | |
283 | return false; | |
284 | while ($info = curl_multi_info_read($this->mh)) | |
285 | { | |
286 | foreach ($this->ch as $download) | |
287 | if ($download['ch'] == $info['handle']) | |
288 | break; | |
289 | $info = curl_getinfo($download['ch']); | |
290 | $array['id'] = $download['id']; | |
291 | $array['url'] = $download['url']; | |
292 | if ($info['http_code'] == 200) | |
293 | { | |
294 | if ($info['size_download'] >= $info['download_content_length']) | |
295 | { | |
296 | $array['status'] = $info['http_code']; | |
297 | $array['response'] = curl_multi_getcontent($download['ch']); | |
298 | } | |
299 | else | |
300 | { | |
301 | $array['status'] = false; | |
302 | $array['response'] = ""; | |
303 | } | |
304 | } | |
305 | else | |
306 | { | |
307 | $array['status'] = $info['http_code']; | |
308 | $array['response'] = curl_multi_getcontent($download['ch']); | |
309 | } | |
310 | $downloads[] = $array; | |
311 | curl_multi_remove_handle($this->mh, $download['ch']); | |
312 | curl_close($download['ch']); | |
313 | unset($this->ch[$download['id']]); | |
314 | } | |
315 | if (isset($downloads) and (count($downloads) > 0)) | |
316 | return $downloads; | |
317 | } | |
318 | return false; | |
319 | } | |
320 | ||
321 | function stopDownloads() | |
322 | { | |
323 | if (isset($this->mh)) | |
324 | { | |
325 | if (isset($this->ch)) | |
326 | { | |
327 | foreach ($this->ch as $download) | |
328 | { | |
329 | curl_multi_remove_handle($this->mh, $download['ch']); | |
330 | curl_close($download['ch']); | |
331 | } | |
332 | unset($this->ch); | |
333 | } | |
334 | curl_multi_close($this->mh); | |
335 | unset($this->mh); | |
336 | } | |
337 | } | |
338 | ||
339 | function error($error) | |
340 | { | |
341 | LogError("cURL Error : $error"); | |
342 | } | |
343 | } | |
344 | ||
345 | class F4F | |
346 | { | |
347 | var $audio, $auth, $baseFilename, $baseTS, $bootstrapUrl, $baseUrl, $debug, $duration, $fileCount, $filesize; | |
348 | var $fixWindow, $format, $live, $media, $outDir, $outFile, $parallel, $play, $processed, $quality, $rename, $video; | |
349 | var $prevTagSize, $tagHeaderLen; | |
350 | var $segTable, $fragTable, $segNum, $fragNum, $frags, $fragCount, $fragsPerSeg, $lastFrag, $fragUrl, $discontinuity; | |
351 | var $prevAudioTS, $prevVideoTS, $pAudioTagLen, $pVideoTagLen, $pAudioTagPos, $pVideoTagPos; | |
352 | var $prevAVC_Header, $prevAAC_Header, $AVC_HeaderWritten, $AAC_HeaderWritten; | |
353 | ||
354 | function __construct() | |
355 | { | |
356 | $this->auth = ""; | |
357 | $this->baseFilename = ""; | |
358 | $this->bootstrapUrl = ""; | |
359 | $this->debug = false; | |
360 | $this->duration = 0; | |
361 | $this->fileCount = 1; | |
362 | $this->fixWindow = 1000; | |
363 | $this->format = ""; | |
364 | $this->live = false; | |
365 | $this->outDir = ""; | |
366 | $this->outFile = ""; | |
367 | $this->parallel = 8; | |
368 | $this->play = false; | |
369 | $this->processed = false; | |
370 | $this->quality = "high"; | |
371 | $this->rename = false; | |
372 | $this->segTable = array(); | |
373 | $this->fragTable = array(); | |
374 | $this->segStart = false; | |
375 | $this->fragStart = false; | |
376 | $this->frags = array(); | |
377 | $this->fragCount = 0; | |
378 | $this->fragsPerSeg = 0; | |
379 | $this->lastFrag = 0; | |
380 | $this->discontinuity = ""; | |
381 | $this->InitDecoder(); | |
382 | } | |
383 | ||
384 | function InitDecoder() | |
385 | { | |
386 | $this->audio = false; | |
387 | $this->baseTS = INVALID_TIMESTAMP; | |
388 | $this->filesize = 0; | |
389 | $this->video = false; | |
390 | $this->prevTagSize = 4; | |
391 | $this->tagHeaderLen = 11; | |
392 | $this->prevAudioTS = INVALID_TIMESTAMP; | |
393 | $this->prevVideoTS = INVALID_TIMESTAMP; | |
394 | $this->pAudioTagLen = 0; | |
395 | $this->pVideoTagLen = 0; | |
396 | $this->pAudioTagPos = 0; | |
397 | $this->pVideoTagPos = 0; | |
398 | $this->prevAVC_Header = false; | |
399 | $this->prevAAC_Header = false; | |
400 | $this->AVC_HeaderWritten = false; | |
401 | $this->AAC_HeaderWritten = false; | |
402 | } | |
403 | ||
404 | function GetManifest($cc, $manifest) | |
405 | { | |
406 | $status = $cc->get($manifest); | |
407 | if ($status == 403) | |
408 | LogError("Access Denied! Unable to download the manifest."); | |
409 | else if ($status != 200) | |
410 | LogError("Unable to download the manifest"); | |
411 | $xml = simplexml_load_string(trim($cc->response)); | |
412 | if (!$xml) | |
413 | LogError("Failed to load xml"); | |
414 | $namespace = $xml->getDocNamespaces(); | |
415 | $namespace = $namespace['']; | |
416 | $xml->registerXPathNamespace("ns", $namespace); | |
417 | return $xml; | |
418 | } | |
419 | ||
420 | function ParseManifest($cc, $manifest) | |
421 | { | |
422 | LogInfo("Processing manifest info...."); | |
423 | $xml = $this->GetManifest($cc, $manifest); | |
424 | $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); | |
425 | if (isset($baseUrl[0])) | |
426 | $baseUrl = GetString($baseUrl[0]); | |
427 | else | |
428 | $baseUrl = ""; | |
429 | $url = $xml->xpath("/ns:manifest/ns:media[@*]"); | |
430 | if (isset($url[0]['href'])) | |
431 | { | |
432 | foreach ($url as $manifest) | |
433 | { | |
434 | $bitrate = (int) $manifest['bitrate']; | |
435 | $entry =& $manifests[$bitrate]; | |
436 | $entry['bitrate'] = $bitrate; | |
437 | $href = GetString($manifest['href']); | |
438 | $entry['url'] = NormalizePath(JoinUrl($baseUrl, $href)); | |
439 | $entry['xml'] = $this->GetManifest($cc, $entry['url']); | |
440 | } | |
441 | } | |
442 | else | |
443 | { | |
444 | $manifests[0]['bitrate'] = 0; | |
445 | $manifests[0]['url'] = $manifest; | |
446 | $manifests[0]['xml'] = $xml; | |
447 | } | |
448 | ||
449 | foreach ($manifests as $manifest) | |
450 | { | |
451 | $xml = $manifest['xml']; | |
452 | ||
453 | // Extract baseUrl from manifest url | |
454 | $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL"); | |
455 | if (isset($baseUrl[0])) | |
456 | $baseUrl = GetString($baseUrl[0]); | |
457 | else | |
458 | { | |
459 | $baseUrl = $manifest['url']; | |
460 | if (strpos($baseUrl, '?') !== false) | |
461 | $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')); | |
462 | $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/')); | |
463 | } | |
464 | if (!isHttpUrl($baseUrl)) | |
465 | LogError("Provided manifest is not a valid HDS manifest"); | |
466 | ||
467 | $streams = $xml->xpath("/ns:manifest/ns:media"); | |
468 | foreach ($streams as $stream) | |
469 | { | |
470 | $array = array(); | |
471 | foreach ($stream->attributes() as $k => $v) | |
472 | $array[strtolower($k)] = GetString($v); | |
473 | $array['metadata'] = GetString($stream->{'metadata'}); | |
474 | $stream = $array; | |
475 | ||
476 | $bitrate = isset($stream['bitrate']) ? (int) $stream['bitrate'] : $manifest['bitrate']; | |
477 | $streamId = isset($stream[strtolower('streamId')]) ? $stream[strtolower('streamId')] : ""; | |
478 | $mediaEntry =& $this->media[$bitrate]; | |
479 | ||
480 | $mediaEntry['baseUrl'] = $baseUrl; | |
481 | if (substr($stream['url'], 0, 1) == "/") | |
482 | $mediaEntry['url'] = substr($stream['url'], 1); | |
483 | else | |
484 | $mediaEntry['url'] = $stream['url']; | |
485 | if (isset($stream[strtolower('bootstrapInfoId')])) | |
486 | $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo[@id='" . $stream[strtolower('bootstrapInfoId')] . "']"); | |
487 | else | |
488 | $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo"); | |
489 | if (isset($bootstrap[0]['url'])) | |
490 | { | |
491 | $bootstrapUrl = GetString($bootstrap[0]['url']); | |
492 | if (!isHttpUrl($bootstrapUrl)) | |
493 | $bootstrapUrl = JoinUrl($mediaEntry['baseUrl'], $bootstrapUrl); | |
494 | $mediaEntry['bootstrapUrl'] = NormalizePath($bootstrapUrl); | |
495 | if ($cc->get($mediaEntry['bootstrapUrl']) != 200) | |
496 | LogError("Failed to get bootstrap info"); | |
497 | $mediaEntry['bootstrap'] = $cc->response; | |
498 | } | |
499 | else | |
500 | $mediaEntry['bootstrap'] = base64_decode(GetString($bootstrap[0])); | |
501 | if (isset($stream['metadata'])) | |
502 | $mediaEntry['metadata'] = base64_decode($stream['metadata']); | |
503 | else | |
504 | $mediaEntry['metadata'] = ""; | |
505 | } | |
506 | unset($mediaEntry); | |
507 | } | |
508 | ||
509 | // Available qualities | |
510 | $bitrates = array(); | |
511 | if (!count($this->media)) | |
512 | LogError("No media entry found"); | |
513 | krsort($this->media, SORT_NUMERIC); | |
514 | LogDebug("Manifest Entries:\n"); | |
515 | LogDebug(sprintf(" %-8s%s", "Bitrate", "URL")); | |
516 | for ($i = 0; $i < count($this->media); $i++) | |
517 | { | |
518 | $key = KeyName($this->media, $i); | |
519 | $bitrates[] = $key; | |
520 | LogDebug(sprintf(" %-8d%s", $key, $this->media[$key]['url'])); | |
521 | } | |
522 | LogDebug(""); | |
523 | LogInfo("Quality Selection:\n Available: " . implode(' ', $bitrates)); | |
524 | ||
525 | // Quality selection | |
526 | if (is_numeric($this->quality) and isset($this->media[$this->quality])) | |
527 | { | |
528 | $key = $this->quality; | |
529 | $this->media = $this->media[$key]; | |
530 | } | |
531 | else | |
532 | { | |
533 | $this->quality = strtolower($this->quality); | |
534 | switch ($this->quality) | |
535 | { | |
536 | case "low": | |
537 | $this->quality = 2; | |
538 | break; | |
539 | case "medium": | |
540 | $this->quality = 1; | |
541 | break; | |
542 | default: | |
543 | $this->quality = 0; | |
544 | } | |
545 | while ($this->quality >= 0) | |
546 | { | |
547 | $key = KeyName($this->media, $this->quality); | |
548 | if ($key !== NULL) | |
549 | { | |
550 | $this->media = $this->media[$key]; | |
551 | break; | |
552 | } | |
553 | else | |
554 | $this->quality -= 1; | |
555 | } | |
556 | } | |
557 | LogInfo(" Selected : " . $key); | |
558 | ||
559 | $this->baseUrl = $this->media['baseUrl']; | |
560 | if (isset($this->media['bootstrapUrl'])) | |
561 | $this->bootstrapUrl = $this->media['bootstrapUrl']; | |
562 | $bootstrapInfo = $this->media['bootstrap']; | |
563 | ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
564 | if ($boxType == "abst") | |
565 | $this->ParseBootstrapBox($bootstrapInfo, $pos); | |
566 | else | |
567 | LogError("Failed to parse bootstrap info"); | |
568 | } | |
569 | ||
570 | function UpdateBootstrapInfo($cc, $bootstrapUrl) | |
571 | { | |
572 | $fragNum = $this->fragCount; | |
573 | $retries = 0; | |
574 | while (($fragNum == $this->fragCount) and ($retries < 30)) | |
575 | { | |
576 | $bootstrapPos = 0; | |
577 | LogDebug("Updating bootstrap info, Available fragments: " . $this->fragCount); | |
578 | if ($cc->get($bootstrapUrl) != 200) | |
579 | LogError("Failed to refresh bootstrap info"); | |
580 | $bootstrapInfo = $cc->response; | |
581 | ReadBoxHeader($bootstrapInfo, $bootstrapPos, $boxType, $boxSize); | |
582 | if ($boxType == "abst") | |
583 | $this->ParseBootstrapBox($bootstrapInfo, $bootstrapPos); | |
584 | else | |
585 | LogError("Failed to parse bootstrap info"); | |
586 | LogDebug("Update complete, Available fragments: " . $this->fragCount); | |
587 | if ($fragNum == $this->fragCount) | |
588 | { | |
589 | LogInfo("Updating bootstrap info, Retries: " . ++$retries, true); | |
590 | usleep(4000000); | |
591 | } | |
592 | } | |
593 | } | |
594 | ||
595 | function ParseBootstrapBox($bootstrapInfo, $pos) | |
596 | { | |
597 | $version = ReadByte($bootstrapInfo, $pos); | |
598 | $flags = ReadInt24($bootstrapInfo, $pos + 1); | |
599 | $bootstrapVersion = ReadInt32($bootstrapInfo, $pos + 4); | |
600 | $byte = ReadByte($bootstrapInfo, $pos + 8); | |
601 | $profile = ($byte & 0xC0) >> 6; | |
602 | if (($byte & 0x20) >> 5) | |
603 | $this->live = true; | |
604 | $update = ($byte & 0x10) >> 4; | |
605 | $timescale = ReadInt32($bootstrapInfo, $pos + 9); | |
606 | $currentMediaTime = ReadInt64($bootstrapInfo, 13); | |
607 | $smpteTimeCodeOffset = ReadInt64($bootstrapInfo, 21); | |
608 | $pos += 29; | |
609 | $movieIdentifier = ReadString($bootstrapInfo, $pos); | |
610 | $serverEntryCount = ReadByte($bootstrapInfo, $pos++); | |
611 | for ($i = 0; $i < $serverEntryCount; $i++) | |
612 | $serverEntryTable[$i] = ReadString($bootstrapInfo, $pos); | |
613 | $qualityEntryCount = ReadByte($bootstrapInfo, $pos++); | |
614 | for ($i = 0; $i < $qualityEntryCount; $i++) | |
615 | $qualityEntryTable[$i] = ReadString($bootstrapInfo, $pos); | |
616 | $drmData = ReadString($bootstrapInfo, $pos); | |
617 | $metadata = ReadString($bootstrapInfo, $pos); | |
618 | $segRunTableCount = ReadByte($bootstrapInfo, $pos++); | |
619 | for ($i = 0; $i < $segRunTableCount; $i++) | |
620 | { | |
621 | ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
622 | if ($boxType == "asrt") | |
623 | $this->ParseAsrtBox($bootstrapInfo, $pos); | |
624 | $pos += $boxSize; | |
625 | } | |
626 | $fragRunTableCount = ReadByte($bootstrapInfo, $pos++); | |
627 | for ($i = 0; $i < $fragRunTableCount; $i++) | |
628 | { | |
629 | ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize); | |
630 | if ($boxType == "afrt") | |
631 | $this->ParseAfrtBox($bootstrapInfo, $pos); | |
632 | $pos += $boxSize; | |
633 | } | |
634 | } | |
635 | ||
636 | function ParseAsrtBox($asrt, $pos) | |
637 | { | |
638 | $version = ReadByte($asrt, $pos); | |
639 | $flags = ReadInt24($asrt, $pos + 1); | |
640 | $qualityEntryCount = ReadByte($asrt, $pos + 4); | |
641 | $pos += 5; | |
642 | for ($i = 0; $i < $qualityEntryCount; $i++) | |
643 | $qualitySegmentUrlModifiers[$i] = ReadString($asrt, $pos); | |
644 | $segCount = ReadInt32($asrt, $pos); | |
645 | $pos += 4; | |
646 | LogDebug(sprintf("%s:\n\n %-8s%-10s", "Segment Entries", "Number", "Fragments")); | |
647 | for ($i = 0; $i < $segCount; $i++) | |
648 | { | |
649 | $firstSegment = ReadInt32($asrt, $pos); | |
650 | $segEntry =& $this->segTable[$firstSegment]; | |
651 | $segEntry['firstSegment'] = $firstSegment; | |
652 | $segEntry['fragmentsPerSegment'] = ReadInt32($asrt, $pos + 4); | |
653 | if ($segEntry['fragmentsPerSegment'] & 0x80000000) | |
654 | $segEntry['fragmentsPerSegment'] = 0; | |
655 | $pos += 8; | |
656 | } | |
657 | unset($segEntry); | |
658 | foreach ($this->segTable as $segEntry) | |
659 | LogDebug(sprintf(" %-8s%-10s", $segEntry['firstSegment'], $segEntry['fragmentsPerSegment'])); | |
660 | LogDebug(""); | |
661 | $lastSegment = end($this->segTable); | |
662 | if ($this->segStart === false) | |
663 | $this->segStart = $lastSegment['firstSegment']; | |
664 | $this->fragCount = $lastSegment['fragmentsPerSegment']; | |
665 | ||
666 | // Use segment table in case of multiple segments | |
667 | if (count($this->segTable) > 1) | |
668 | { | |
669 | $secondLastSegment = prev($this->segTable); | |
670 | if ($this->fragStart === false) | |
671 | { | |
672 | $this->fragsPerSeg = $secondLastSegment['fragmentsPerSegment']; | |
673 | $this->fragStart = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount - 2; | |
674 | $this->fragCount = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount; | |
675 | } | |
676 | else | |
677 | $this->fragCount = $secondLastSegment['firstSegment'] * $this->fragsPerSeg + $this->fragCount; | |
678 | } | |
679 | } | |
680 | ||
681 | function ParseAfrtBox($afrt, $pos) | |
682 | { | |
683 | $version = ReadByte($afrt, $pos); | |
684 | $flags = ReadInt24($afrt, $pos + 1); | |
685 | $timescale = ReadInt32($afrt, $pos + 4); | |
686 | $qualityEntryCount = ReadByte($afrt, $pos + 8); | |
687 | $pos += 9; | |
688 | for ($i = 0; $i < $qualityEntryCount; $i++) | |
689 | $qualitySegmentUrlModifiers[$i] = ReadString($afrt, $pos); | |
690 | $fragEntries = ReadInt32($afrt, $pos); | |
691 | $pos += 4; | |
692 | LogDebug(sprintf("%s:\n\n %-8s%-16s%-16s%-16s", "Fragment Entries", "Number", "Timestamp", "Duration", "Discontinuity")); | |
693 | for ($i = 0; $i < $fragEntries; $i++) | |
694 | { | |
695 | $firstFragment = ReadInt32($afrt, $pos); | |
696 | $fragEntry =& $this->fragTable[$firstFragment]; | |
697 | $fragEntry['firstFragment'] = $firstFragment; | |
698 | $fragEntry['firstFragmentTimestamp'] = ReadInt64($afrt, $pos + 4); | |
699 | $fragEntry['fragmentDuration'] = ReadInt32($afrt, $pos + 12); | |
700 | $fragEntry['discontinuityIndicator'] = ""; | |
701 | $pos += 16; | |
702 | if ($fragEntry['fragmentDuration'] == 0) | |
703 | $fragEntry['discontinuityIndicator'] = ReadByte($afrt, $pos++); | |
704 | } | |
705 | unset($fragEntry); | |
706 | foreach ($this->fragTable as $fragEntry) | |
707 | LogDebug(sprintf(" %-8s%-16s%-16s%-16s", $fragEntry['firstFragment'], $fragEntry['firstFragmentTimestamp'], $fragEntry['fragmentDuration'], $fragEntry['discontinuityIndicator'])); | |
708 | LogDebug(""); | |
709 | ||
710 | // Use fragment table in case of single segment | |
711 | if (count($this->segTable) == 1) | |
712 | { | |
713 | $firstFragment = reset($this->fragTable); | |
714 | $lastFragment = end($this->fragTable); | |
715 | if ($this->fragStart === false) | |
716 | { | |
717 | if ($this->live) | |
718 | $this->fragStart = $lastFragment['firstFragment'] - 2; | |
719 | else | |
720 | $this->fragStart = $firstFragment['firstFragment'] - 1; | |
721 | if ($this->fragStart < 0) | |
722 | $this->fragStart = 0; | |
723 | } | |
724 | if ($this->fragCount > 0) | |
725 | $this->fragCount += $firstFragment['firstFragment'] - 1; | |
726 | if ($this->fragCount < $lastFragment['firstFragment']) | |
727 | $this->fragCount = $lastFragment['firstFragment']; | |
728 | } | |
729 | } | |
730 | ||
731 | function DownloadFragments($cc, $manifest, $opt = array()) | |
732 | { | |
733 | $start = 0; | |
734 | extract($opt, EXTR_IF_EXISTS); | |
735 | ||
736 | $this->ParseManifest($cc, $manifest); | |
737 | $segNum = $this->segStart; | |
738 | $fragNum = $this->fragStart; | |
739 | if ($start) | |
740 | { | |
741 | if ($segNum > 1) | |
742 | if ($start % $this->fragsPerSeg) | |
743 | $segNum = (int) ($start / $this->fragsPerSeg + 1); | |
744 | else | |
745 | $segNum = (int) ($start / $this->fragsPerSeg); | |
746 | $fragNum = $start - 1; | |
747 | $this->segStart = $segNum; | |
748 | $this->fragStart = $fragNum; | |
749 | } | |
750 | $this->lastFrag = $fragNum; | |
751 | $opt['cc'] = $cc; | |
752 | $opt['duration'] = 0; | |
753 | ||
754 | // Extract baseFilename | |
755 | $this->baseFilename = $this->media['url']; | |
756 | if (substr($this->baseFilename, -1) == '/') | |
757 | $this->baseFilename = substr($this->baseFilename, 0, -1); | |
758 | $this->baseFilename = RemoveExtension($this->baseFilename); | |
759 | if (strrpos($this->baseFilename, '/')) | |
760 | $this->baseFilename = substr($this->baseFilename, strrpos($this->baseFilename, '/') + 1); | |
761 | if (strpos($manifest, "?")) | |
762 | $this->baseFilename = md5(substr($manifest, 0, strpos($manifest, "?"))) . "_" . $this->baseFilename; | |
763 | else | |
764 | $this->baseFilename = md5($manifest) . "_" . $this->baseFilename; | |
765 | $this->baseFilename .= "Seg" . $segNum . "-Frag"; | |
766 | ||
767 | if ($fragNum >= $this->fragCount) | |
768 | LogError("No fragment available for downloading"); | |
769 | ||
770 | if (isHttpUrl($this->media['url'])) | |
771 | $this->fragUrl = $this->media['url']; | |
772 | else | |
773 | $this->fragUrl = JoinUrl($this->baseUrl, $this->media['url']); | |
774 | $this->fragUrl = NormalizePath($this->fragUrl); | |
775 | LogDebug("Base Fragment Url:\n" . $this->fragUrl . "\n"); | |
776 | LogDebug("Downloading Fragments:\n"); | |
777 | ||
778 | while (($fragNum < $this->fragCount) or $cc->active) | |
779 | { | |
780 | while ((count($cc->ch) < $this->parallel) and ($fragNum < $this->fragCount)) | |
781 | { | |
782 | $frag = array(); | |
783 | $fragNum = $fragNum + 1; | |
784 | $frag['id'] = $fragNum; | |
785 | LogInfo("Downloading $fragNum/$this->fragCount fragments", true); | |
786 | if (in_array_field($fragNum, "firstFragment", $this->fragTable, true)) | |
787 | $this->discontinuity = value_in_array_field($fragNum, "firstFragment", "discontinuityIndicator", $this->fragTable, true); | |
788 | else | |
789 | { | |
790 | $closest = 1; | |
791 | foreach ($this->fragTable as $item) | |
792 | { | |
793 | if ($item['firstFragment'] < $fragNum) | |
794 | $closest = $item['firstFragment']; | |
795 | else | |
796 | break; | |
797 | } | |
798 | $this->discontinuity = value_in_array_field($closest, "firstFragment", "discontinuityIndicator", $this->fragTable, true); | |
799 | } | |
800 | if (($this->discontinuity == 1) or ($this->discontinuity == 3)) | |
801 | { | |
802 | LogDebug("Skipping fragment $fragNum due to discontinuity"); | |
803 | $frag['response'] = false; | |
804 | $this->rename = true; | |
805 | } | |
806 | else if (file_exists($this->baseFilename . $fragNum)) | |
807 | { | |
808 | LogDebug("Fragment $fragNum is already downloaded"); | |
809 | $frag['response'] = file_get_contents($this->baseFilename . $fragNum); | |
810 | } | |
811 | if (isset($frag['response'])) | |
812 | { | |
813 | if ($this->WriteFragment($frag, $opt) === 2) | |
814 | break 2; | |
815 | else | |
816 | continue; | |
817 | } | |
818 | ||
819 | /* Increase or decrease segment number if current fragment is not available */ | |
820 | /* in selected segment range */ | |
821 | if (count($this->segTable) > 1) | |
822 | { | |
823 | if ($fragNum > ($segNum * $this->fragsPerSeg)) | |
824 | $segNum++; | |
825 | else if ($fragNum <= (($segNum - 1) * $this->fragsPerSeg)) | |
826 | $segNum--; | |
827 | } | |
828 | ||
829 | LogDebug("Adding fragment $fragNum to download queue"); | |
830 | $cc->addDownload($this->fragUrl . "Seg" . $segNum . "-Frag" . $fragNum . $this->auth, $fragNum); | |
831 | } | |
832 | ||
833 | $downloads = $cc->checkDownloads(); | |
834 | if ($downloads !== false) | |
835 | { | |
836 | for ($i = 0; $i < count($downloads); $i++) | |
837 | { | |
838 | $frag = array(); | |
839 | $download = $downloads[$i]; | |
840 | $frag['id'] = $download['id']; | |
841 | if ($download['status'] == 200) | |
842 | { | |
843 | if ($this->VerifyFragment($download['response'])) | |
844 | { | |
845 | LogDebug("Fragment " . $this->baseFilename . $download['id'] . " successfully downloaded"); | |
846 | if (!($this->live or $this->play)) | |
847 | file_put_contents($this->baseFilename . $download['id'], $download['response']); | |
848 | $frag['response'] = $download['response']; | |
849 | } | |
850 | else | |
851 | { | |
852 | LogDebug("Fragment " . $download['id'] . " failed to verify"); | |
853 | LogDebug("Adding fragment " . $download['id'] . " to download queue"); | |
854 | $cc->addDownload($download['url'], $download['id']); | |
855 | } | |
856 | } | |
857 | else if ($download['status'] === false) | |
858 | { | |
859 | LogDebug("Fragment " . $download['id'] . " failed to download"); | |
860 | LogDebug("Adding fragment " . $download['id'] . " to download queue"); | |
861 | $cc->addDownload($download['url'], $download['id']); | |
862 | } | |
863 | else if ($download['status'] == 403) | |
864 | LogError("Access Denied! Unable to download fragments."); | |
865 | else | |
866 | { | |
867 | LogDebug("Fragment " . $download['id'] . " doesn't exist, Status: " . $download['status']); | |
868 | $frag['response'] = false; | |
869 | $this->rename = true; | |
870 | ||
871 | /* Resync with latest available fragment when we are left behind due to */ | |
872 | /* slow connection and short live window on streaming server. make sure */ | |
873 | /* to reset the last written fragment. */ | |
874 | if ($this->live and ($i + 1 == count($downloads)) and !$cc->active) | |
875 | { | |
876 | LogDebug("Trying to resync with latest available fragment"); | |
877 | if ($this->WriteFragment($frag, $opt) === 2) | |
878 | break 2; | |
879 | unset($frag['response']); | |
880 | $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); | |
881 | $fragNum = $this->fragCount - 1; | |
882 | $this->lastFrag = $fragNum; | |
883 | } | |
884 | } | |
885 | if (isset($frag['response'])) | |
886 | if ($this->WriteFragment($frag, $opt) === 2) | |
887 | break 2; | |
888 | } | |
889 | unset($downloads, $download); | |
890 | } | |
891 | if ($this->live and ($fragNum >= $this->fragCount) and !$cc->active) | |
892 | $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl); | |
893 | } | |
894 | ||
895 | LogInfo(""); | |
896 | LogDebug("\nAll fragments downloaded successfully\n"); | |
897 | $cc->stopDownloads(); | |
898 | $this->processed = true; | |
899 | } | |
900 | ||
901 | function VerifyFragment(&$frag) | |
902 | { | |
903 | $fragPos = 0; | |
904 | $fragLen = strlen($frag); | |
905 | ||
906 | /* Some moronic servers add wrong boxSize in header causing fragment verification * | |
907 | * to fail so we have to fix the boxSize before processing the fragment. */ | |
908 | while ($fragPos < $fragLen) | |
909 | { | |
910 | ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); | |
911 | if ($boxType == "mdat") | |
912 | { | |
913 | $len = strlen(substr($frag, $fragPos, $boxSize)); | |
914 | if ($boxSize and ($len == $boxSize)) | |
915 | return true; | |
916 | else | |
917 | { | |
918 | $boxSize = $fragLen - $fragPos; | |
919 | WriteBoxSize($frag, $fragPos, $boxType, $boxSize); | |
920 | return true; | |
921 | } | |
922 | } | |
923 | $fragPos += $boxSize; | |
924 | } | |
925 | return false; | |
926 | } | |
927 | ||
928 | function RenameFragments($baseFilename, $fragNum, $fileExt) | |
929 | { | |
930 | $files = array(); | |
931 | $retries = 0; | |
932 | ||
933 | while (true) | |
934 | { | |
935 | if ($retries >= 50) | |
936 | break; | |
937 | $file = $baseFilename . ++$fragNum; | |
938 | if (file_exists($file)) | |
939 | { | |
940 | $files[] = $file; | |
941 | $retries = 0; | |
942 | } | |
943 | else if (file_exists($file . $fileExt)) | |
944 | { | |
945 | $files[] = $file; | |
946 | $retries = 0; | |
947 | } | |
948 | else | |
949 | $retries++; | |
950 | } | |
951 | ||
952 | $fragCount = count($files); | |
953 | natsort($files); | |
954 | for ($i = 0; $i < $fragCount; $i++) | |
955 | rename($files[$i], $baseFilename . ($i + 1)); | |
956 | } | |
957 | ||
958 | function WriteMetadata($flv = false) | |
959 | { | |
960 | if (isset($this->media) and $this->media['metadata']) | |
961 | { | |
962 | $metadataSize = strlen($this->media['metadata']); | |
963 | WriteByte($metadata, 0, SCRIPT_DATA); | |
964 | WriteInt24($metadata, 1, $metadataSize); | |
965 | WriteInt24($metadata, 4, 0); | |
966 | WriteInt32($metadata, 7, 0); | |
967 | $metadata = implode("", $metadata) . $this->media['metadata']; | |
968 | WriteByte($metadata, $this->tagHeaderLen + $metadataSize - 1, 0x09); | |
969 | WriteInt32($metadata, $this->tagHeaderLen + $metadataSize, $this->tagHeaderLen + $metadataSize); | |
970 | if (is_resource($flv)) | |
971 | { | |
972 | fwrite($flv, $metadata, $this->tagHeaderLen + $metadataSize + $this->prevTagSize); | |
973 | return true; | |
974 | } | |
975 | else | |
976 | return $metadata; | |
977 | } | |
978 | return false; | |
979 | } | |
980 | ||
981 | function WriteFlvTimestamp(&$frag, $fragPos, $packetTS) | |
982 | { | |
983 | WriteInt24($frag, $fragPos + 4, ($packetTS & 0x00FFFFFF)); | |
984 | WriteByte($frag, $fragPos + 7, ($packetTS & 0xFF000000) >> 24); | |
985 | } | |
986 | ||
987 | function DecodeFragment($frag, $fragNum, $opt = array()) | |
988 | { | |
989 | $debug = $this->debug; | |
990 | $flv = false; | |
991 | extract($opt, EXTR_IF_EXISTS); | |
992 | ||
993 | $flvData = ""; | |
994 | $fragPos = 0; | |
995 | $packetTS = 0; | |
996 | $fragLen = strlen($frag); | |
997 | ||
998 | if (!$this->VerifyFragment($frag)) | |
999 | { | |
1000 | LogInfo("Skipping fragment number $fragNum"); | |
1001 | return false; | |
1002 | } | |
1003 | ||
1004 | while ($fragPos < $fragLen) | |
1005 | { | |
1006 | ReadBoxHeader($frag, $fragPos, $boxType, $boxSize); | |
1007 | if ($boxType == "mdat") | |
1008 | { | |
1009 | $fragLen = $fragPos + $boxSize; | |
1010 | break; | |
1011 | } | |
1012 | $fragPos += $boxSize; | |
1013 | } | |
1014 | ||
1015 | LogDebug(sprintf("\nFragment %d:\n" . $this->format . "%-16s", $fragNum, "Type", "CurrentTS", "PreviousTS", "Size", "Position"), $debug); | |
1016 | while ($fragPos < $fragLen) | |
1017 | { | |
1018 | $packetType = ReadByte($frag, $fragPos); | |
1019 | $packetSize = ReadInt24($frag, $fragPos + 1); | |
1020 | $packetTS = ReadInt24($frag, $fragPos + 4); | |
1021 | $packetTS = $packetTS | (ReadByte($frag, $fragPos + 7) << 24); | |
1022 | if ($packetTS & 0x80000000) | |
1023 | $packetTS &= 0x7FFFFFFF; | |
1024 | $totalTagLen = $this->tagHeaderLen + $packetSize + $this->prevTagSize; | |
1025 | ||
1026 | // Try to fix the odd timestamps and make them zero based | |
1027 | $currentTS = $packetTS; | |
1028 | $lastTS = $this->prevVideoTS >= $this->prevAudioTS ? $this->prevVideoTS : $this->prevAudioTS; | |
1029 | if (($this->baseTS == INVALID_TIMESTAMP) and (($packetType == AUDIO) or ($packetType == VIDEO))) | |
1030 | $this->baseTS = $packetTS; | |
1031 | if ($this->baseTS > 1000) | |
1032 | { | |
1033 | if ($packetTS >= $this->baseTS) | |
1034 | $packetTS -= $this->baseTS; | |
1035 | else | |
1036 | $packetTS = $lastTS + FRAMEFIX_STEP; | |
1037 | } | |
1038 | if ($lastTS != INVALID_TIMESTAMP) | |
1039 | { | |
1040 | $timeShift = $packetTS - $lastTS; | |
1041 | if ($timeShift > $this->fixWindow) | |
1042 | { | |
1043 | $this->baseTS += $timeShift - FRAMEFIX_STEP; | |
1044 | $packetTS = $lastTS + FRAMEFIX_STEP; | |
1045 | } | |
1046 | } | |
1047 | if ($packetTS != $currentTS) | |
1048 | $this->WriteFlvTimestamp($frag, $fragPos, $packetTS); | |
1049 | ||
1050 | switch ($packetType) | |
1051 | { | |
1052 | case AUDIO: | |
1053 | if ($packetTS > $this->prevAudioTS - $this->fixWindow) | |
1054 | { | |
1055 | $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen); | |
1056 | $CodecID = ($FrameInfo & 0xF0) >> 4; | |
1057 | if ($CodecID == CODEC_ID_AAC) | |
1058 | { | |
1059 | $AAC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1); | |
1060 | if ($AAC_PacketType == AAC_SEQUENCE_HEADER) | |
1061 | { | |
1062 | if ($this->AAC_HeaderWritten) | |
1063 | { | |
1064 | LogDebug(sprintf("%s\n" . $this->format, "Skipping AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
1065 | break; | |
1066 | } | |
1067 | else | |
1068 | { | |
1069 | LogDebug("Writing AAC sequence header", $debug); | |
1070 | $this->AAC_HeaderWritten = true; | |
1071 | } | |
1072 | } | |
1073 | else if (!$this->AAC_HeaderWritten) | |
1074 | { | |
1075 | LogDebug(sprintf("%s\n" . $this->format, "Discarding audio packet received before AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
1076 | break; | |
1077 | } | |
1078 | } | |
1079 | if ($packetSize > 0) | |
1080 | { | |
1081 | // Check for packets with non-monotonic audio timestamps and fix them | |
1082 | if (!(($CodecID == CODEC_ID_AAC) and (($AAC_PacketType == AAC_SEQUENCE_HEADER) or $this->prevAAC_Header))) | |
1083 | if (($this->prevAudioTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevAudioTS)) | |
1084 | { | |
1085 | LogDebug(sprintf("%s\n" . $this->format, "Fixing audio timestamp", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
1086 | $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevAudioTS - $packetTS); | |
1087 | $this->WriteFlvTimestamp($frag, $fragPos, $packetTS); | |
1088 | } | |
1089 | if (is_resource($flv)) | |
1090 | { | |
1091 | $this->pAudioTagPos = ftell($flv); | |
1092 | $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen); | |
1093 | if (!$status) | |
1094 | LogError("Failed to write flv data to file"); | |
1095 | if ($debug) | |
1096 | LogDebug(sprintf($this->format . "%-16s", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize, $this->pAudioTagPos)); | |
1097 | } | |
1098 | else | |
1099 | { | |
1100 | $flvData .= substr($frag, $fragPos, $totalTagLen); | |
1101 | if ($debug) | |
1102 | LogDebug(sprintf($this->format, "AUDIO", $packetTS, $this->prevAudioTS, $packetSize)); | |
1103 | } | |
1104 | if (($CodecID == CODEC_ID_AAC) and ($AAC_PacketType == AAC_SEQUENCE_HEADER)) | |
1105 | $this->prevAAC_Header = true; | |
1106 | else | |
1107 | $this->prevAAC_Header = false; | |
1108 | $this->prevAudioTS = $packetTS; | |
1109 | $this->pAudioTagLen = $totalTagLen; | |
1110 | } | |
1111 | else | |
1112 | LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized audio packet", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
1113 | } | |
1114 | else | |
1115 | LogDebug(sprintf("%s\n" . $this->format, "Skipping audio packet in fragment $fragNum", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug); | |
1116 | if (!$this->audio) | |
1117 | $this->audio = true; | |
1118 | break; | |
1119 | case VIDEO: | |
1120 | if ($packetTS > $this->prevVideoTS - $this->fixWindow) | |
1121 | { | |
1122 | $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen); | |
1123 | $FrameType = ($FrameInfo & 0xF0) >> 4; | |
1124 | $CodecID = $FrameInfo & 0x0F; | |
1125 | if ($FrameType == FRAME_TYPE_INFO) | |
1126 | { | |
1127 | LogDebug(sprintf("%s\n" . $this->format, "Skipping video info frame", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1128 | break; | |
1129 | } | |
1130 | if ($CodecID == CODEC_ID_AVC) | |
1131 | { | |
1132 | $AVC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1); | |
1133 | if ($AVC_PacketType == AVC_SEQUENCE_HEADER) | |
1134 | { | |
1135 | if ($this->AVC_HeaderWritten) | |
1136 | { | |
1137 | LogDebug(sprintf("%s\n" . $this->format, "Skipping AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1138 | break; | |
1139 | } | |
1140 | else | |
1141 | { | |
1142 | LogDebug("Writing AVC sequence header", $debug); | |
1143 | $this->AVC_HeaderWritten = true; | |
1144 | } | |
1145 | } | |
1146 | else if (!$this->AVC_HeaderWritten) | |
1147 | { | |
1148 | LogDebug(sprintf("%s\n" . $this->format, "Discarding video packet received before AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1149 | break; | |
1150 | } | |
1151 | } | |
1152 | if ($packetSize > 0) | |
1153 | { | |
1154 | // Check for packets with non-monotonic video timestamps and fix them | |
1155 | if (!(($CodecID == CODEC_ID_AVC) and (($AVC_PacketType == AVC_SEQUENCE_HEADER) or ($AVC_PacketType == AVC_SEQUENCE_END) or $this->prevAVC_Header))) | |
1156 | if (($this->prevVideoTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevVideoTS)) | |
1157 | { | |
1158 | LogDebug(sprintf("%s\n" . $this->format, "Fixing video timestamp", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1159 | $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevVideoTS - $packetTS); | |
1160 | $this->WriteFlvTimestamp($frag, $fragPos, $packetTS); | |
1161 | } | |
1162 | if (is_resource($flv)) | |
1163 | { | |
1164 | $this->pVideoTagPos = ftell($flv); | |
1165 | $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen); | |
1166 | if (!$status) | |
1167 | LogError("Failed to write flv data to file"); | |
1168 | if ($debug) | |
1169 | LogDebug(sprintf($this->format . "%-16s", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize, $this->pVideoTagPos)); | |
1170 | } | |
1171 | else | |
1172 | { | |
1173 | $flvData .= substr($frag, $fragPos, $totalTagLen); | |
1174 | if ($debug) | |
1175 | LogDebug(sprintf($this->format, "VIDEO", $packetTS, $this->prevVideoTS, $packetSize)); | |
1176 | } | |
1177 | if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_SEQUENCE_HEADER)) | |
1178 | $this->prevAVC_Header = true; | |
1179 | else | |
1180 | $this->prevAVC_Header = false; | |
1181 | $this->prevVideoTS = $packetTS; | |
1182 | $this->pVideoTagLen = $totalTagLen; | |
1183 | } | |
1184 | else | |
1185 | LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized video packet", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1186 | } | |
1187 | else | |
1188 | LogDebug(sprintf("%s\n" . $this->format, "Skipping video packet in fragment $fragNum", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug); | |
1189 | if (!$this->video) | |
1190 | $this->video = true; | |
1191 | break; | |
1192 | case SCRIPT_DATA: | |
1193 | break; | |
1194 | default: | |
1195 | LogError("Unknown packet type " . $packetType . " encountered! Encrypted fragments can't be recovered.", 2); | |
1196 | } | |
1197 | $fragPos += $totalTagLen; | |
1198 | } | |
1199 | $this->duration = round($packetTS / 1000, 0); | |
1200 | if (is_resource($flv)) | |
1201 | { | |
1202 | $this->filesize = ftell($flv) / (1024 * 1024); | |
1203 | return true; | |
1204 | } | |
1205 | else | |
1206 | return $flvData; | |
1207 | } | |
1208 | ||
1209 | function WriteFragment($download, &$opt) | |
1210 | { | |
1211 | $this->frags[$download['id']] = $download; | |
1212 | ||
1213 | $available = count($this->frags); | |
1214 | for ($i = 0; $i < $available; $i++) | |
1215 | { | |
1216 | if (isset($this->frags[$this->lastFrag + 1])) | |
1217 | { | |
1218 | $frag = $this->frags[$this->lastFrag + 1]; | |
1219 | if ($frag['response'] !== false) | |
1220 | { | |
1221 | LogDebug("Writing fragment " . $frag['id'] . " to flv file"); | |
1222 | if (!isset($opt['file'])) | |
1223 | { | |
1224 | $opt['debug'] = false; | |
1225 | if ($this->play) | |
1226 | $outFile = STDOUT; | |
1227 | else if ($this->outFile) | |
1228 | { | |
1229 | if ($opt['filesize']) | |
1230 | $outFile = $this->outDir . $this->outFile . "-" . $this->fileCount++ . ".flv"; | |
1231 | else | |
1232 | $outFile = $this->outDir . $this->outFile . ".flv"; | |
1233 | } | |
1234 | else | |
1235 | { | |
1236 | if ($opt['filesize']) | |
1237 | $outFile = $this->outDir . $this->baseFilename . "-" . $this->fileCount++ . ".flv"; | |
1238 | else | |
1239 | $outFile = $this->outDir . $this->baseFilename . ".flv"; | |
1240 | } | |
1241 | $this->InitDecoder(); | |
1242 | $this->DecodeFragment($frag['response'], $frag['id'], $opt); | |
1243 | $opt['file'] = WriteFlvFile($outFile, $this->audio, $this->video); | |
1244 | if (!($this->live or ($this->fragStart > 0) or $this->filesize or $opt['tDuration'])) | |
1245 | $this->WriteMetadata($opt['file']); | |
1246 | ||
1247 | $opt['debug'] = $this->debug; | |
1248 | $this->InitDecoder(); | |
1249 | } | |
1250 | $flvData = $this->DecodeFragment($frag['response'], $frag['id'], $opt); | |
1251 | if (strlen($flvData)) | |
1252 | { | |
1253 | $status = fwrite($opt['file'], $flvData, strlen($flvData)); | |
1254 | if (!$status) | |
1255 | LogError("Failed to write flv data"); | |
1256 | if (!$this->play) | |
1257 | $this->filesize = ftell($opt['file']) / (1024 * 1024); | |
1258 | } | |
1259 | $this->lastFrag = $frag['id']; | |
1260 | } | |
1261 | else | |
1262 | { | |
1263 | $this->lastFrag += 1; | |
1264 | LogDebug("Skipping failed fragment " . $this->lastFrag); | |
1265 | } | |
1266 | unset($this->frags[$this->lastFrag]); | |
1267 | } | |
1268 | else | |
1269 | break; | |
1270 | ||
1271 | if ($opt['tDuration'] and (($opt['duration'] + $this->duration) >= $opt['tDuration'])) | |
1272 | { | |
1273 | LogInfo(""); | |
1274 | LogInfo(($opt['duration'] + $this->duration) . " seconds of content has been recorded successfully.", true); | |
1275 | return 2; | |
1276 | } | |
1277 | if ($opt['filesize'] and ($this->filesize >= $opt['filesize'])) | |
1278 | { | |
1279 | $this->filesize = 0; | |
1280 | $opt['duration'] += $this->duration; | |
1281 | fclose($opt['file']); | |
1282 | unset($opt['file']); | |
1283 | } | |
1284 | } | |
1285 | ||
1286 | if (!count($this->frags)) | |
1287 | unset($this->frags); | |
1288 | return true; | |
1289 | } | |
1290 | } | |
1291 | ||
1292 | function ReadByte($str, $pos) | |
1293 | { | |
1294 | $int = unpack("C", $str[$pos]); | |
1295 | return $int[1]; | |
1296 | } | |
1297 | ||
1298 | function ReadInt24($str, $pos) | |
1299 | { | |
1300 | $int32 = unpack("N", "\x00" . substr($str, $pos, 3)); | |
1301 | return $int32[1]; | |
1302 | } | |
1303 | ||
1304 | function ReadInt32($str, $pos) | |
1305 | { | |
1306 | $int32 = unpack("N", substr($str, $pos, 4)); | |
1307 | return $int32[1]; | |
1308 | } | |
1309 | ||
1310 | function ReadInt64($str, $pos) | |
1311 | { | |
1312 | $hi = sprintf("%u", ReadInt32($str, $pos)); | |
1313 | $lo = sprintf("%u", ReadInt32($str, $pos + 4)); | |
1314 | $int64 = bcadd(bcmul($hi, "4294967296"), $lo); | |
1315 | return $int64; | |
1316 | } | |
1317 | ||
1318 | function ReadString($str, &$pos) | |
1319 | { | |
1320 | $len = 0; | |
1321 | while ($str[$pos + $len] != "\x00") | |
1322 | $len++; | |
1323 | $str = substr($str, $pos, $len); | |
1324 | $pos += $len + 1; | |
1325 | return $str; | |
1326 | } | |
1327 | ||
1328 | function ReadBoxHeader($str, &$pos, &$boxType, &$boxSize) | |
1329 | { | |
1330 | if (!isset($pos)) | |
1331 | $pos = 0; | |
1332 | $boxSize = ReadInt32($str, $pos); | |
1333 | $boxType = substr($str, $pos + 4, 4); | |
1334 | if ($boxSize == 1) | |
1335 | { | |
1336 | $boxSize = ReadInt64($str, $pos + 8) - 16; | |
1337 | $pos += 16; | |
1338 | } | |
1339 | else | |
1340 | { | |
1341 | $boxSize -= 8; | |
1342 | $pos += 8; | |
1343 | } | |
1344 | if ($boxSize <= 0) | |
1345 | $boxSize = 0; | |
1346 | } | |
1347 | ||
1348 | function WriteByte(&$str, $pos, $int) | |
1349 | { | |
1350 | $str[$pos] = pack("C", $int); | |
1351 | } | |
1352 | ||
1353 | function WriteInt24(&$str, $pos, $int) | |
1354 | { | |
1355 | $str[$pos] = pack("C", ($int & 0xFF0000) >> 16); | |
1356 | $str[$pos + 1] = pack("C", ($int & 0xFF00) >> 8); | |
1357 | $str[$pos + 2] = pack("C", $int & 0xFF); | |
1358 | } | |
1359 | ||
1360 | function WriteInt32(&$str, $pos, $int) | |
1361 | { | |
1362 | $str[$pos] = pack("C", ($int & 0xFF000000) >> 24); | |
1363 | $str[$pos + 1] = pack("C", ($int & 0xFF0000) >> 16); | |
1364 | $str[$pos + 2] = pack("C", ($int & 0xFF00) >> 8); | |
1365 | $str[$pos + 3] = pack("C", $int & 0xFF); | |
1366 | } | |
1367 | ||
1368 | function WriteBoxSize(&$str, $pos, $type, $size) | |
1369 | { | |
1370 | if (substr($str, $pos - 4, 4) == $type) | |
1371 | WriteInt32($str, $pos - 8, $size); | |
1372 | else | |
1373 | { | |
1374 | WriteInt32($str, $pos - 8, 0); | |
1375 | WriteInt32($str, $pos - 4, $size); | |
1376 | } | |
1377 | } | |
1378 | ||
1379 | function GetString($xmlObject) | |
1380 | { | |
1381 | return trim((string) $xmlObject); | |
1382 | } | |
1383 | ||
1384 | function isHttpUrl($url) | |
1385 | { | |
1386 | if (strncasecmp($url, "http", 4) == 0) | |
1387 | return true; | |
1388 | else | |
1389 | return false; | |
1390 | } | |
1391 | ||
1392 | function JoinUrl($firstUrl, $secondUrl) | |
1393 | { | |
1394 | if ($firstUrl and (substr($firstUrl, -1) == '/')) | |
1395 | $firstUrl = substr($firstUrl, 0, -1); | |
1396 | if ($secondUrl and (substr($secondUrl, 0, 1) == '/')) | |
1397 | $secondUrl = substr($secondUrl, 1); | |
1398 | return $firstUrl . "/" . $secondUrl; | |
1399 | } | |
1400 | ||
1401 | function KeyName(array $a, $pos) | |
1402 | { | |
1403 | $temp = array_slice($a, $pos, 1, true); | |
1404 | return key($temp); | |
1405 | } | |
1406 | ||
1407 | function LogDebug($msg, $display = true) | |
1408 | { | |
1409 | global $debug, $logfile, $showHeader; | |
1410 | if ($showHeader) | |
1411 | { | |
1412 | ShowHeader(); | |
1413 | $showHeader = false; | |
1414 | } | |
1415 | if ($display and $debug) | |
1416 | fwrite($logfile, $msg . "\n"); | |
1417 | } | |
1418 | ||
1419 | function LogError($msg, $code = 1) | |
1420 | { | |
1421 | global $quiet; | |
1422 | if (!$quiet) | |
1423 | PrintLine($msg); | |
1424 | exit($code); | |
1425 | } | |
1426 | ||
1427 | function LogInfo($msg, $progress = false) | |
1428 | { | |
1429 | global $quiet; | |
1430 | if (!$quiet) | |
1431 | PrintLine($msg, $progress); | |
1432 | } | |
1433 | ||
1434 | function NormalizePath($path) | |
1435 | { | |
1436 | $inSegs = preg_split('/(?<!\/)\/(?!\/)/u', $path); | |
1437 | $outSegs = array(); | |
1438 | ||
1439 | foreach ($inSegs as $seg) | |
1440 | { | |
1441 | if ($seg == '' || $seg == '.') | |
1442 | continue; | |
1443 | if ($seg == '..') | |
1444 | array_pop($outSegs); | |
1445 | else | |
1446 | array_push($outSegs, $seg); | |
1447 | } | |
1448 | $outPath = implode('/', $outSegs); | |
1449 | ||
1450 | if (substr($path, 0, 1) == '/') | |
1451 | $outPath = '/' . $outPath; | |
1452 | if (substr($path, -1) == '/') | |
1453 | $outPath .= '/'; | |
1454 | return $outPath; | |
1455 | } | |
1456 | ||
1457 | function PrintLine($msg, $progress = false) | |
1458 | { | |
1459 | global $showHeader; | |
1460 | if ($showHeader) | |
1461 | { | |
1462 | ShowHeader(); | |
1463 | $showHeader = false; | |
1464 | } | |
1465 | if ($msg) | |
1466 | { | |
1467 | printf("\r%-79s\r", ""); | |
1468 | if ($progress) | |
1469 | printf("%s\r", $msg); | |
1470 | else | |
1471 | printf("%s\n", $msg); | |
1472 | } | |
1473 | else | |
1474 | printf("\n"); | |
1475 | } | |
1476 | ||
1477 | function RemoveExtension($outFile) | |
1478 | { | |
1479 | preg_match("/\.\w{1,4}$/i", $outFile, $extension); | |
1480 | if (isset($extension[0])) | |
1481 | { | |
1482 | $extension = $extension[0]; | |
1483 | $outFile = substr($outFile, 0, -strlen($extension)); | |
1484 | return $outFile; | |
1485 | } | |
1486 | return $outFile; | |
1487 | } | |
1488 | ||
1489 | function ShowHeader() | |
1490 | { | |
1491 | $header = "KSV Adobe HDS Downloader"; | |
1492 | $len = strlen($header); | |
1493 | $width = (int) ((80 - $len) / 2) + $len; | |
1494 | $format = "\n%" . $width . "s\n\n"; | |
1495 | printf($format, $header); | |
1496 | } | |
1497 | ||
1498 | function WriteFlvFile($outFile, $audio = true, $video = true) | |
1499 | { | |
1500 | $flvHeader = pack("H*", "464c5601050000000900000000"); | |
1501 | $flvHeaderLen = strlen($flvHeader); | |
1502 | ||
1503 | if (!($audio and $video)) | |
1504 | { | |
1505 | if ($audio and !$video) | |
1506 | $flvHeader[4] = "\x04"; | |
1507 | else if ($video and !$audio) | |
1508 | $flvHeader[4] = "\x01"; | |
1509 | } | |
1510 | ||
1511 | if (is_resource($outFile)) | |
1512 | $flv = $outFile; | |
1513 | else | |
1514 | $flv = fopen($outFile, "w+b"); | |
1515 | if (!$flv) | |
1516 | LogError("Failed to open " . $outFile); | |
1517 | fwrite($flv, $flvHeader, $flvHeaderLen); | |
1518 | return $flv; | |
1519 | } | |
1520 | ||
1521 | function in_array_field($needle, $needle_field, $haystack, $strict = false) | |
1522 | { | |
1523 | if ($strict) | |
1524 | { | |
1525 | foreach ($haystack as $item) | |
1526 | if (isset($item[$needle_field]) && $item[$needle_field] === $needle) | |
1527 | return true; | |
1528 | } | |
1529 | else | |
1530 | { | |
1531 | foreach ($haystack as $item) | |
1532 | if (isset($item[$needle_field]) && $item[$needle_field] == $needle) | |
1533 | return true; | |
1534 | } | |
1535 | return false; | |
1536 | } | |
1537 | ||
1538 | function value_in_array_field($needle, $needle_field, $value_field, $haystack, $strict = false) | |
1539 | { | |
1540 | if ($strict) | |
1541 | { | |
1542 | foreach ($haystack as $item) | |
1543 | if (isset($item[$needle_field]) && $item[$needle_field] === $needle) | |
1544 | return $item[$value_field]; | |
1545 | } | |
1546 | else | |
1547 | { | |
1548 | foreach ($haystack as $item) | |
1549 | if (isset($item[$needle_field]) && $item[$needle_field] == $needle) | |
1550 | return $item[$value_field]; | |
1551 | } | |
1552 | return false; | |
1553 | } | |
1554 | ||
1555 | // Global code starts here | |
1556 | $format = " %-8s%-16s%-16s%-8s"; | |
1557 | $baseFilename = ""; | |
1558 | $debug = false; | |
1559 | $duration = 0; | |
1560 | $delete = false; | |
1561 | $fileExt = ".f4f"; | |
1562 | $fileCount = 1; | |
1563 | $filesize = 0; | |
1564 | $fixWindow = 1000; | |
1565 | $fragCount = 0; | |
1566 | $fragNum = 0; | |
1567 | $logfile = STDERR; | |
1568 | $manifest = ""; | |
1569 | $outDir = ""; | |
1570 | $outFile = ""; | |
1571 | $play = false; | |
1572 | $quiet = false; | |
1573 | $referrer = ""; | |
1574 | $rename = false; | |
1575 | $showHeader = true; | |
1576 | $start = 0; | |
1577 | $update = false; | |
1578 | ||
1579 | // Set large enough memory limit | |
1580 | ini_set("memory_limit", "512M"); | |
1581 | ||
1582 | // Check if STDOUT is available | |
1583 | $cli = new CLI(); | |
1584 | if ($cli->getParam('play')) | |
1585 | { | |
1586 | $play = true; | |
1587 | $quiet = true; | |
1588 | $showHeader = false; | |
1589 | } | |
1590 | if ($cli->getParam('help')) | |
1591 | { | |
1592 | $cli->displayHelp(); | |
1593 | exit(0); | |
1594 | } | |
1595 | ||
1596 | // Check for required extensions | |
1597 | $extensions = array( | |
1598 | "bcmath", | |
1599 | "curl", | |
1600 | "SimpleXML" | |
1601 | ); | |
1602 | foreach ($extensions as $extension) | |
1603 | if (!extension_loaded($extension)) | |
1604 | LogError("You don't have '$extension' extension installed. please install it before continuing."); | |
1605 | ||
1606 | // Initialize classes | |
1607 | $cc = new cURL(); | |
1608 | $f4f = new F4F(); | |
1609 | ||
1610 | $f4f->baseFilename =& $baseFilename; | |
1611 | $f4f->debug =& $debug; | |
1612 | $f4f->fixWindow =& $fixWindow; | |
1613 | $f4f->format =& $format; | |
1614 | $f4f->outDir =& $outDir; | |
1615 | $f4f->outFile =& $outFile; | |
1616 | $f4f->play =& $play; | |
1617 | $f4f->rename =& $rename; | |
1618 | ||
1619 | // Process command line options | |
1620 | if ($cli->getParam('debug')) | |
1621 | $debug = true; | |
1622 | if ($cli->getParam('delete')) | |
1623 | $delete = true; | |
1624 | if ($cli->getParam('fproxy')) | |
1625 | $cc->fragProxy = true; | |
1626 | if ($cli->getParam('rename')) | |
1627 | $rename = $cli->getParam('rename'); | |
1628 | if ($cli->getParam('update')) | |
1629 | $update = true; | |
1630 | if ($cli->getParam('auth')) | |
1631 | $f4f->auth = "?" . $cli->getParam('auth'); | |
1632 | if ($cli->getParam('duration')) | |
1633 | $duration = $cli->getParam('duration'); | |
1634 | if ($cli->getParam('filesize')) | |
1635 | $filesize = $cli->getParam('filesize'); | |
1636 | if ($cli->getParam('fixwindow')) | |
1637 | $fixWindow = $cli->getParam('fixwindow'); | |
1638 | if ($cli->getParam('fragments')) | |
1639 | $baseFilename = $cli->getParam('fragments'); | |
1640 | if ($cli->getParam('manifest')) | |
1641 | $manifest = $cli->getParam('manifest'); | |
1642 | if ($cli->getParam('outdir')) | |
1643 | $outDir = $cli->getParam('outdir'); | |
1644 | if ($cli->getParam('outfile')) | |
1645 | $outFile = $cli->getParam('outfile'); | |
1646 | if ($cli->getParam('parallel')) | |
1647 | $f4f->parallel = $cli->getParam('parallel'); | |
1648 | if ($cli->getParam('proxy')) | |
1649 | $cc->proxy = $cli->getParam('proxy'); | |
1650 | if ($cli->getParam('quality')) | |
1651 | $f4f->quality = $cli->getParam('quality'); | |
1652 | if ($cli->getParam('referrer')) | |
1653 | $referrer = $cli->getParam('referrer'); | |
1654 | if ($cli->getParam('start')) | |
1655 | $start = $cli->getParam('start'); | |
1656 | if ($cli->getParam('useragent')) | |
1657 | $cc->user_agent = $cli->getParam('useragent'); | |
1658 | ||
1659 | // Use custom referrer | |
1660 | if ($referrer) | |
1661 | $cc->headers[] = "Referer: " . $referrer; | |
1662 | ||
1663 | // Update the script | |
1664 | if ($update) | |
1665 | { | |
1666 | $cc->cert_check = false; | |
1667 | $status = $cc->get("https://raw.github.com/K-S-V/Scripts/master/AdobeHDS.php"); | |
1668 | if ($status == 200) | |
1669 | { | |
1670 | if (md5($cc->response) == md5(file_get_contents($argv[0]))) | |
1671 | LogError("You are already using the latest version of this script.", 0); | |
1672 | $status = file_put_contents($argv[0], $cc->response); | |
1673 | if (!$status) | |
1674 | LogError("Failed to write script file"); | |
1675 | LogError("Script has been updated successfully.", 0); | |
1676 | } | |
1677 | else | |
1678 | LogError("Failed to update script"); | |
1679 | } | |
1680 | ||
1681 | // Create output directory | |
1682 | if ($outDir) | |
1683 | { | |
1684 | $outDir = rtrim(str_replace('\\', '/', $outDir)); | |
1685 | if (substr($outDir, -1) != '/') | |
1686 | $outDir = $outDir . '/'; | |
1687 | if (!file_exists($outDir)) | |
1688 | { | |
1689 | LogDebug("Creating destination directory " . $outDir); | |
1690 | if (!mkdir($outDir, 0777, true)) | |
1691 | LogError("Failed to create destination directory " . $outDir); | |
1692 | } | |
1693 | } | |
1694 | ||
1695 | // Remove existing file extension | |
1696 | if ($outFile) | |
1697 | $outFile = RemoveExtension($outFile); | |
1698 | ||
1699 | // Disable filesize when piping | |
1700 | if ($play) | |
1701 | $filesize = 0; | |
1702 | ||
1703 | // Download fragments when manifest is available | |
1704 | if ($manifest) | |
1705 | { | |
1706 | if (!isHttpUrl($manifest)) | |
1707 | $manifest = "http://" . $manifest; | |
1708 | $opt = array( | |
1709 | 'start' => $start, | |
1710 | 'tDuration' => $duration, | |
1711 | 'filesize' => $filesize | |
1712 | ); | |
1713 | $f4f->DownloadFragments($cc, $manifest, $opt); | |
1714 | } | |
1715 | ||
1716 | // Determine output filename | |
1717 | if (!$outFile) | |
1718 | { | |
1719 | $baseFilename = str_replace('\\', '/', $baseFilename); | |
1720 | $lastChar = substr($baseFilename, -1); | |
1721 | if ($baseFilename and !(($lastChar == '/') or ($lastChar == ':'))) | |
1722 | { | |
1723 | $lastSlash = strrpos($baseFilename, '/'); | |
1724 | if ($lastSlash) | |
1725 | $outFile = substr($baseFilename, $lastSlash + 1); | |
1726 | else | |
1727 | $outFile = $baseFilename; | |
1728 | } | |
1729 | else | |
1730 | $outFile = "Joined"; | |
1731 | $outFile = RemoveExtension($outFile); | |
1732 | } | |
1733 | ||
1734 | // Check for available fragments and rename if required | |
1735 | if ($f4f->fragNum) | |
1736 | $fragNum = $f4f->fragNum; | |
1737 | else if ($start) | |
1738 | $fragNum = $start - 1; | |
1739 | if ($rename) | |
1740 | { | |
1741 | $f4f->RenameFragments($baseFilename, $fragNum, $fileExt); | |
1742 | $fragNum = 0; | |
1743 | } | |
1744 | $count = $fragNum + 1; | |
1745 | while (true) | |
1746 | { | |
1747 | if (file_exists($baseFilename . $count . $fileExt)) | |
1748 | $fragCount++; | |
1749 | else if (file_exists($baseFilename . $count)) | |
1750 | { | |
1751 | $fileExt = ""; | |
1752 | $fragCount++; | |
1753 | } | |
1754 | else | |
1755 | break; | |
1756 | $count++; | |
1757 | } | |
1758 | LogInfo("Found $fragCount fragments"); | |
1759 | ||
1760 | if (!$f4f->processed) | |
1761 | { | |
1762 | // Process available fragments | |
1763 | if (!$fragCount) | |
1764 | exit(1); | |
1765 | $timeStart = microtime(true); | |
1766 | LogDebug("Joining Fragments:"); | |
1767 | for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++) | |
1768 | { | |
1769 | $frag = file_get_contents($baseFilename . $i . $fileExt); | |
1770 | if (!isset($opt['flv'])) | |
1771 | { | |
1772 | $opt['debug'] = false; | |
1773 | $f4f->InitDecoder(); | |
1774 | $f4f->DecodeFragment($frag, $i, $opt); | |
1775 | if ($filesize) | |
1776 | $opt['flv'] = WriteFlvFile($outDir . $outFile . "-" . $fileCount++ . ".flv", $f4f->audio, $f4f->video); | |
1777 | else | |
1778 | $opt['flv'] = WriteFlvFile($outDir . $outFile . ".flv", $f4f->audio, $f4f->video); | |
1779 | if (!(($fragNum > 0) or $filesize)) | |
1780 | $f4f->WriteMetadata($opt['flv']); | |
1781 | ||
1782 | $opt['debug'] = $debug; | |
1783 | $f4f->InitDecoder(); | |
1784 | } | |
1785 | $f4f->DecodeFragment($frag, $i, $opt); | |
1786 | if ($filesize and ($f4f->filesize >= $filesize)) | |
1787 | { | |
1788 | $f4f->filesize = 0; | |
1789 | fclose($opt['flv']); | |
1790 | unset($opt['flv']); | |
1791 | } | |
1792 | LogInfo("Processed " . ($i - $fragNum) . " fragments", true); | |
1793 | } | |
1794 | fclose($opt['flv']); | |
1795 | $timeEnd = microtime(true); | |
1796 | $timeTaken = sprintf("%.2f", $timeEnd - $timeStart); | |
1797 | LogInfo("Joined $fragCount fragments in $timeTaken seconds"); | |
1798 | } | |
1799 | ||
1800 | // Delete fragments after processing | |
1801 | if ($delete) | |
1802 | { | |
1803 | for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++) | |
1804 | if (file_exists($baseFilename . $i . $fileExt)) | |
1805 | unlink($baseFilename . $i . $fileExt); | |
1806 | } | |
1807 | ||
1808 | LogInfo("Finished"); | |
1809 | ?> |