summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xAdobeHDS.php2041
-rwxr-xr-xfb-img-resize12
-rw-r--r--woof620
-rwxr-xr-xximkeys75
4 files changed, 2748 insertions, 0 deletions
diff --git a/AdobeHDS.php b/AdobeHDS.php
new file mode 100755
index 0000000..508935d
--- /dev/null
+++ b/AdobeHDS.php
@@ -0,0 +1,2041 @@
+#!/usr/bin/php
+<?php
+ define('AUDIO', 0x08);
+ define('VIDEO', 0x09);
+ define('SCRIPT_DATA', 0x12);
+ define('FRAME_TYPE_INFO', 0x05);
+ define('CODEC_ID_AVC', 0x07);
+ define('CODEC_ID_AAC', 0x0A);
+ define('AVC_SEQUENCE_HEADER', 0x00);
+ define('AAC_SEQUENCE_HEADER', 0x00);
+ define('AVC_NALU', 0x01);
+ define('AVC_SEQUENCE_END', 0x02);
+ define('FRAMEFIX_STEP', 40);
+ define('INVALID_TIMESTAMP', -1);
+ define('STOP_PROCESSING', 2);
+
+ class CLI
+ {
+ protected static $ACCEPTED = array();
+ var $params = array();
+
+ function __construct($options = array(), $handleUnknown = false)
+ {
+ global $argc, $argv;
+
+ if (count($options))
+ self::$ACCEPTED = $options;
+
+ // Parse params
+ if ($argc > 1)
+ {
+ $paramSwitch = false;
+ for ($i = 1; $i < $argc; $i++)
+ {
+ $arg = $argv[$i];
+ $isSwitch = preg_match('/^-+/', $arg);
+
+ if ($isSwitch)
+ $arg = preg_replace('/^-+/', '', $arg);
+
+ if ($paramSwitch and $isSwitch)
+ $this->error("[param] expected after '$paramSwitch' switch (" . self::$ACCEPTED[1][$paramSwitch] . ')');
+ else if (!$paramSwitch and !$isSwitch)
+ {
+ if ($handleUnknown)
+ $this->params['unknown'][] = $arg;
+ else
+ $this->error("'$arg' is an invalid option, use --help to display valid switches.");
+ }
+ else if (!$paramSwitch and $isSwitch)
+ {
+ if (isset($this->params[$arg]))
+ $this->error("'$arg' switch can't occur more than once");
+
+ $this->params[$arg] = true;
+ if (isset(self::$ACCEPTED[1][$arg]))
+ $paramSwitch = $arg;
+ else if (!isset(self::$ACCEPTED[0][$arg]))
+ $this->error("there's no '$arg' switch, use --help to display all switches.");
+ }
+ else if ($paramSwitch and !$isSwitch)
+ {
+ $this->params[$paramSwitch] = $arg;
+ $paramSwitch = false;
+ }
+ }
+ }
+
+ // Final check
+ foreach ($this->params as $k => $v)
+ if (isset(self::$ACCEPTED[1][$k]) and $v === true)
+ $this->error("[param] expected after '$k' switch (" . self::$ACCEPTED[1][$k] . ')');
+ }
+
+ function displayHelp()
+ {
+ LogInfo("You can use script with following switches:\n");
+ foreach (self::$ACCEPTED[0] as $key => $value)
+ LogInfo(sprintf(" --%-17s %s", $key, $value));
+ foreach (self::$ACCEPTED[1] as $key => $value)
+ LogInfo(sprintf(" --%-9s%-8s %s", $key, " [param]", $value));
+ }
+
+ function error($msg)
+ {
+ LogError($msg);
+ }
+
+ function getParam($name)
+ {
+ if (isset($this->params[$name]))
+ return $this->params[$name];
+ else
+ return false;
+ }
+ }
+
+ class cURL
+ {
+ var $headers, $user_agent, $compression, $cookie_file;
+ var $active, $cert_check, $fragProxy, $maxSpeed, $proxy, $response;
+ var $mh, $ch, $mrc;
+ static $ref = 0;
+
+ function __construct($cookies = true, $cookie = 'Cookies.txt', $compression = 'gzip', $proxy = '')
+ {
+ $this->headers = $this->headers();
+ $this->user_agent = 'Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0';
+ $this->compression = $compression;
+ $this->cookies = $cookies;
+ if ($this->cookies == true)
+ $this->cookie($cookie);
+ $this->cert_check = false;
+ $this->fragProxy = false;
+ $this->maxSpeed = 0;
+ $this->proxy = $proxy;
+ self::$ref++;
+ }
+
+ function __destruct()
+ {
+ $this->stopDownloads();
+ if ((self::$ref <= 1) and file_exists($this->cookie_file))
+ unlink($this->cookie_file);
+ self::$ref--;
+ }
+
+ function headers()
+ {
+ $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
+ $headers[] = 'Connection: Keep-Alive';
+ return $headers;
+ }
+
+ function cookie($cookie_file)
+ {
+ if (file_exists($cookie_file))
+ $this->cookie_file = $cookie_file;
+ else
+ {
+ $file = fopen($cookie_file, 'w') or $this->error('The cookie file could not be opened. Make sure this directory has the correct permissions.');
+ $this->cookie_file = $cookie_file;
+ fclose($file);
+ }
+ }
+
+ function get($url)
+ {
+ $process = curl_init($url);
+ $options = array(
+ CURLOPT_HTTPHEADER => $this->headers,
+ CURLOPT_HEADER => 0,
+ CURLOPT_USERAGENT => $this->user_agent,
+ CURLOPT_ENCODING => $this->compression,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_FOLLOWLOCATION => 1
+ );
+ curl_setopt_array($process, $options);
+ if (!$this->cert_check)
+ curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false);
+ if ($this->cookies == true)
+ {
+ curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file);
+ curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file);
+ }
+ if ($this->proxy)
+ $this->setProxy($process, $this->proxy);
+ $this->response = curl_exec($process);
+ if ($this->response !== false)
+ $status = curl_getinfo($process, CURLINFO_HTTP_CODE);
+ curl_close($process);
+ if (isset($status))
+ return $status;
+ else
+ return false;
+ }
+
+ function post($url, $data)
+ {
+ $process = curl_init($url);
+ $headers = $this->headers;
+ $headers[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8';
+ $options = array(
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_HEADER => 1,
+ CURLOPT_USERAGENT => $this->user_agent,
+ CURLOPT_ENCODING => $this->compression,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_FOLLOWLOCATION => 1,
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => $data
+ );
+ curl_setopt_array($process, $options);
+ if (!$this->cert_check)
+ curl_setopt($process, CURLOPT_SSL_VERIFYPEER, false);
+ if ($this->cookies == true)
+ {
+ curl_setopt($process, CURLOPT_COOKIEFILE, $this->cookie_file);
+ curl_setopt($process, CURLOPT_COOKIEJAR, $this->cookie_file);
+ }
+ if ($this->proxy)
+ $this->setProxy($process, $this->proxy);
+ $return = curl_exec($process);
+ curl_close($process);
+ return $return;
+ }
+
+ function setProxy(&$process, $proxy)
+ {
+ $type = "";
+ $separator = strpos($proxy, "://");
+ if ($separator !== false)
+ {
+ $type = strtolower(substr($proxy, 0, $separator));
+ $proxy = substr($proxy, $separator + 3);
+ }
+ switch ($type)
+ {
+ case "socks4":
+ $type = CURLPROXY_SOCKS4;
+ break;
+ case "socks5":
+ $type = CURLPROXY_SOCKS5;
+ break;
+ default:
+ $type = CURLPROXY_HTTP;
+ }
+ curl_setopt($process, CURLOPT_PROXY, $proxy);
+ curl_setopt($process, CURLOPT_PROXYTYPE, $type);
+ }
+
+ function addDownload($url, $id)
+ {
+ if (!isset($this->mh))
+ $this->mh = curl_multi_init();
+ if (isset($this->ch[$id]))
+ return false;
+ $download =& $this->ch[$id];
+ $download['id'] = $id;
+ $download['url'] = $url;
+ $download['ch'] = curl_init($url);
+ $options = array(
+ CURLOPT_HTTPHEADER => $this->headers,
+ CURLOPT_HEADER => 0,
+ CURLOPT_USERAGENT => $this->user_agent,
+ CURLOPT_ENCODING => $this->compression,
+ CURLOPT_LOW_SPEED_LIMIT => 1024,
+ CURLOPT_LOW_SPEED_TIME => 10,
+ CURLOPT_BINARYTRANSFER => 1,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_FOLLOWLOCATION => 1
+ );
+ curl_setopt_array($download['ch'], $options);
+ if (!$this->cert_check)
+ curl_setopt($download['ch'], CURLOPT_SSL_VERIFYPEER, false);
+ if ($this->cookies == true)
+ {
+ curl_setopt($download['ch'], CURLOPT_COOKIEFILE, $this->cookie_file);
+ curl_setopt($download['ch'], CURLOPT_COOKIEJAR, $this->cookie_file);
+ }
+ if ($this->fragProxy and $this->proxy)
+ $this->setProxy($download['ch'], $this->proxy);
+ if ($this->maxSpeed > 0)
+ curl_setopt($download['ch'], CURLOPT_MAX_RECV_SPEED_LARGE, $this->maxSpeed);
+ curl_multi_add_handle($this->mh, $download['ch']);
+ do
+ {
+ $this->mrc = curl_multi_exec($this->mh, $this->active);
+ } while ($this->mrc == CURLM_CALL_MULTI_PERFORM);
+ return true;
+ }
+
+ function checkDownloads()
+ {
+ if (isset($this->mh))
+ {
+ curl_multi_select($this->mh);
+ $this->mrc = curl_multi_exec($this->mh, $this->active);
+ if ($this->mrc != CURLM_OK)
+ return false;
+ while ($info = curl_multi_info_read($this->mh))
+ {
+ foreach ($this->ch as $download)
+ if ($download['ch'] == $info['handle'])
+ break;
+ $array['id'] = $download['id'];
+ $array['url'] = $download['url'];
+ $info = curl_getinfo($download['ch']);
+ if ($info['http_code'] == 0)
+ {
+ /* if curl fails due to network connectivity issues or some other reason it's *
+ * better to add some delay before next try to avoid busy loop. */
+ LogDebug("Fragment " . $download['id'] . ": " . curl_error($download['ch']));
+ usleep(1000000);
+ $array['status'] = false;
+ $array['response'] = "";
+ }
+ else if ($info['http_code'] == 200)
+ {
+ if ($info['size_download'] >= $info['download_content_length'])
+ {
+ $array['status'] = $info['http_code'];
+ $array['response'] = curl_multi_getcontent($download['ch']);
+ }
+ else
+ {
+ $array['status'] = false;
+ $array['response'] = "";
+ }
+ }
+ else
+ {
+ $array['status'] = $info['http_code'];
+ $array['response'] = curl_multi_getcontent($download['ch']);
+ }
+ $downloads[] = $array;
+ curl_multi_remove_handle($this->mh, $download['ch']);
+ curl_close($download['ch']);
+ unset($this->ch[$download['id']]);
+ }
+ if (isset($downloads) and (count($downloads) > 0))
+ return $downloads;
+ }
+ return false;
+ }
+
+ function stopDownloads()
+ {
+ if (isset($this->mh))
+ {
+ if (isset($this->ch))
+ {
+ foreach ($this->ch as $download)
+ {
+ curl_multi_remove_handle($this->mh, $download['ch']);
+ curl_close($download['ch']);
+ }
+ unset($this->ch);
+ }
+ curl_multi_close($this->mh);
+ unset($this->mh);
+ }
+ }
+
+ function error($error)
+ {
+ LogError("cURL Error : $error");
+ }
+ }
+
+ class F4F
+ {
+ var $audio, $auth, $baseFilename, $baseTS, $bootstrapUrl, $baseUrl, $debug, $duration, $fileCount, $filesize, $fixWindow;
+ var $format, $live, $media, $metadata, $outDir, $outFile, $parallel, $play, $processed, $quality, $rename, $video;
+ var $prevTagSize, $tagHeaderLen;
+ var $segTable, $fragTable, $segNum, $fragNum, $frags, $fragCount, $lastFrag, $fragUrl, $discontinuity;
+ var $negTS, $prevAudioTS, $prevVideoTS, $pAudioTagLen, $pVideoTagLen, $pAudioTagPos, $pVideoTagPos;
+ var $prevAVC_Header, $prevAAC_Header, $AVC_HeaderWritten, $AAC_HeaderWritten;
+
+ function __construct()
+ {
+ $this->auth = "";
+ $this->baseFilename = "";
+ $this->bootstrapUrl = "";
+ $this->debug = false;
+ $this->duration = 0;
+ $this->fileCount = 1;
+ $this->fixWindow = 1000;
+ $this->format = "";
+ $this->live = false;
+ $this->metadata = true;
+ $this->outDir = "";
+ $this->outFile = "";
+ $this->parallel = 8;
+ $this->play = false;
+ $this->processed = false;
+ $this->quality = "high";
+ $this->rename = false;
+ $this->segTable = array();
+ $this->fragTable = array();
+ $this->segStart = false;
+ $this->fragStart = false;
+ $this->frags = array();
+ $this->fragCount = 0;
+ $this->lastFrag = 0;
+ $this->discontinuity = "";
+ $this->InitDecoder();
+ }
+
+ function InitDecoder()
+ {
+ $this->audio = false;
+ $this->filesize = 0;
+ $this->video = false;
+ $this->prevTagSize = 4;
+ $this->tagHeaderLen = 11;
+ $this->baseTS = INVALID_TIMESTAMP;
+ $this->negTS = INVALID_TIMESTAMP;
+ $this->prevAudioTS = INVALID_TIMESTAMP;
+ $this->prevVideoTS = INVALID_TIMESTAMP;
+ $this->pAudioTagLen = 0;
+ $this->pVideoTagLen = 0;
+ $this->pAudioTagPos = 0;
+ $this->pVideoTagPos = 0;
+ $this->prevAVC_Header = false;
+ $this->prevAAC_Header = false;
+ $this->AVC_HeaderWritten = false;
+ $this->AAC_HeaderWritten = false;
+ }
+
+ function GetManifest(cURL $cc, $manifest)
+ {
+ $status = $cc->get($manifest);
+ if ($status == 403)
+ LogError("Access Denied! Unable to download the manifest.");
+ else if ($status != 200)
+ LogError("Unable to download the manifest");
+ $xml = simplexml_load_string(trim($cc->response));
+ if (!$xml)
+ LogError("Failed to load xml");
+ $namespace = $xml->getDocNamespaces();
+ $namespace = $namespace[''];
+ $xml->registerXPathNamespace("ns", $namespace);
+ return $xml;
+ }
+
+ function ParseManifest($cc, $parentManifest)
+ {
+ LogInfo("Processing manifest info....");
+ $xml = $this->GetManifest($cc, $parentManifest);
+
+ // Extract baseUrl from manifest url
+ $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL");
+ if (isset($baseUrl[0]))
+ $baseUrl = GetString($baseUrl[0]);
+ else
+ {
+ $baseUrl = $parentManifest;
+ if (strpos($baseUrl, '?') !== false)
+ $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?'));
+ $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/'));
+ }
+
+ $childManifests = array();
+ $url = $xml->xpath("/ns:manifest/ns:media[@*]");
+ if (isset($url[0]['href']))
+ {
+ $count = 1;
+ foreach ($url as $childManifest)
+ {
+ if (isset($childManifest['bitrate']))
+ $bitrate = floor(GetString($childManifest['bitrate']));
+ else
+ $bitrate = $count++;
+ $entry =& $childManifests[$bitrate];
+ $entry['bitrate'] = $bitrate;
+ $entry['url'] = AbsoluteUrl($baseUrl, GetString($childManifest['href']));
+ $entry['xml'] = $this->GetManifest($cc, $entry['url']);
+ }
+ unset($entry, $childManifest);
+ }
+ else
+ {
+ $childManifests[0]['bitrate'] = 0;
+ $childManifests[0]['url'] = $parentManifest;
+ $childManifests[0]['xml'] = $xml;
+ }
+
+ $count = 1;
+ foreach ($childManifests as $childManifest)
+ {
+ $xml = $childManifest['xml'];
+
+ // Extract baseUrl from manifest url
+ $baseUrl = $xml->xpath("/ns:manifest/ns:baseURL");
+ if (isset($baseUrl[0]))
+ $baseUrl = GetString($baseUrl[0]);
+ else
+ {
+ $baseUrl = $childManifest['url'];
+ if (strpos($baseUrl, '?') !== false)
+ $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?'));
+ $baseUrl = substr($baseUrl, 0, strrpos($baseUrl, '/'));
+ }
+
+ $streams = $xml->xpath("/ns:manifest/ns:media");
+ foreach ($streams as $stream)
+ {
+ $array = array();
+ foreach ($stream->attributes() as $k => $v)
+ $array[strtolower($k)] = GetString($v);
+ $array['metadata'] = GetString($stream->{'metadata'});
+ $stream = $array;
+
+ if (isset($stream['bitrate']))
+ $bitrate = floor($stream['bitrate']);
+ else if ($childManifest['bitrate'] > 0)
+ $bitrate = $childManifest['bitrate'];
+ else
+ $bitrate = $count++;
+ while (isset($this->media[$bitrate]))
+ $bitrate++;
+ $streamId = isset($stream[strtolower('streamId')]) ? $stream[strtolower('streamId')] : "";
+ $mediaEntry =& $this->media[$bitrate];
+
+ $mediaEntry['baseUrl'] = $baseUrl;
+ $mediaEntry['url'] = $stream['url'];
+ if (isRtmpUrl($mediaEntry['baseUrl']) or isRtmpUrl($mediaEntry['url']))
+ LogError("Provided manifest is not a valid HDS manifest");
+
+ // Use embedded auth information when available
+ $idx = strpos($mediaEntry['url'], '?');
+ if ($idx !== false)
+ {
+ $mediaEntry['queryString'] = substr($mediaEntry['url'], $idx);
+ $mediaEntry['url'] = substr($mediaEntry['url'], 0, $idx);
+ if (strlen($this->auth) != 0 and strcmp($this->auth, $mediaEntry['queryString']) != 0)
+ LogDebug("Manifest overrides 'auth': " . $mediaEntry['queryString']);
+ }
+ else
+ $mediaEntry['queryString'] = $this->auth;
+
+ if (isset($stream[strtolower('bootstrapInfoId')]))
+ $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo[@id='" . $stream[strtolower('bootstrapInfoId')] . "']");
+ else
+ $bootstrap = $xml->xpath("/ns:manifest/ns:bootstrapInfo");
+ if (isset($bootstrap[0]['url']))
+ {
+ $mediaEntry['bootstrapUrl'] = AbsoluteUrl($mediaEntry['baseUrl'], GetString($bootstrap[0]['url']));
+ if (strpos($mediaEntry['bootstrapUrl'], '?') === false)
+ $mediaEntry['bootstrapUrl'] .= $this->auth;
+ }
+ else
+ $mediaEntry['bootstrap'] = base64_decode(GetString($bootstrap[0]));
+ if (isset($stream['metadata']))
+ $mediaEntry['metadata'] = base64_decode($stream['metadata']);
+ else
+ $mediaEntry['metadata'] = "";
+ }
+ unset($mediaEntry, $childManifest);
+ }
+
+ // Available qualities
+ $bitrates = array();
+ if (!count($this->media))
+ LogError("No media entry found");
+ krsort($this->media, SORT_NUMERIC);
+ LogDebug("Manifest Entries:\n");
+ LogDebug(sprintf(" %-8s%s", "Bitrate", "URL"));
+ for ($i = 0; $i < count($this->media); $i++)
+ {
+ $key = KeyName($this->media, $i);
+ $bitrates[] = $key;
+ LogDebug(sprintf(" %-8d%s", $key, $this->media[$key]['url']));
+ }
+ LogDebug("");
+ LogInfo("Quality Selection:\n Available: " . implode(' ', $bitrates));
+
+ // Quality selection
+ $key = $this->quality;
+ if (is_numeric($key) and isset($this->media[$key]))
+ $this->media = $this->media[$key];
+ else
+ {
+ $this->quality = strtolower($this->quality);
+ switch ($this->quality)
+ {
+ case "low":
+ $this->quality = 2;
+ break;
+ case "medium":
+ $this->quality = 1;
+ break;
+ default:
+ $this->quality = 0;
+ }
+ while ($this->quality >= 0)
+ {
+ $key = KeyName($this->media, $this->quality);
+ if ($key !== NULL)
+ {
+ $this->media = $this->media[$key];
+ break;
+ }
+ else
+ $this->quality -= 1;
+ }
+ }
+ LogInfo(" Selected : " . $key);
+
+ // Parse initial bootstrap info
+ $this->baseUrl = $this->media['baseUrl'];
+ if (isset($this->media['bootstrapUrl']))
+ {
+ $this->bootstrapUrl = $this->media['bootstrapUrl'];
+ $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl);
+ }
+ else
+ {
+ $bootstrapInfo = $this->media['bootstrap'];
+ ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
+ if ($boxType == "abst")
+ $this->ParseBootstrapBox($bootstrapInfo, $pos);
+ else
+ LogError("Failed to parse bootstrap info");
+ }
+ }
+
+ function UpdateBootstrapInfo(cURL $cc, $bootstrapUrl)
+ {
+ $fragNum = $this->fragCount;
+ $retries = 0;
+
+ // Backup original headers and add no-cache directive for fresh bootstrap info
+ $headers = $cc->headers;
+ $cc->headers[] = "Cache-Control: no-cache";
+ $cc->headers[] = "Pragma: no-cache";
+
+ while (($fragNum == $this->fragCount) and ($retries < 30))
+ {
+ $bootstrapPos = 0;
+ LogDebug("Updating bootstrap info, Available fragments: " . $this->fragCount);
+ $status = $cc->get($bootstrapUrl);
+ if ($status != 200)
+ LogError("Failed to refresh bootstrap info, Status: " . $status);
+ $bootstrapInfo = $cc->response;
+ ReadBoxHeader($bootstrapInfo, $bootstrapPos, $boxType, $boxSize);
+ if ($boxType == "abst")
+ $this->ParseBootstrapBox($bootstrapInfo, $bootstrapPos);
+ else
+ LogError("Failed to parse bootstrap info");
+ LogDebug("Update complete, Available fragments: " . $this->fragCount);
+ if ($fragNum == $this->fragCount)
+ {
+ LogInfo("Updating bootstrap info, Retries: " . ++$retries, true);
+ usleep(4000000);
+ }
+ }
+
+ // Restore original headers
+ $cc->headers = $headers;
+ }
+
+ function ParseBootstrapBox($bootstrapInfo, $pos)
+ {
+ $version = ReadByte($bootstrapInfo, $pos);
+ $flags = ReadInt24($bootstrapInfo, $pos + 1);
+ $bootstrapVersion = ReadInt32($bootstrapInfo, $pos + 4);
+ $byte = ReadByte($bootstrapInfo, $pos + 8);
+ $profile = ($byte & 0xC0) >> 6;
+ if (($byte & 0x20) >> 5)
+ {
+ $this->live = true;
+ $this->metadata = false;
+ }
+ $update = ($byte & 0x10) >> 4;
+ if (!$update)
+ {
+ $this->segTable = array();
+ $this->fragTable = array();
+ }
+ $timescale = ReadInt32($bootstrapInfo, $pos + 9);
+ $currentMediaTime = ReadInt64($bootstrapInfo, $pos + 13);
+ $smpteTimeCodeOffset = ReadInt64($bootstrapInfo, $pos + 21);
+ $pos += 29;
+ $movieIdentifier = ReadString($bootstrapInfo, $pos);
+ $serverEntryCount = ReadByte($bootstrapInfo, $pos++);
+ for ($i = 0; $i < $serverEntryCount; $i++)
+ $serverEntryTable[$i] = ReadString($bootstrapInfo, $pos);
+ $qualityEntryCount = ReadByte($bootstrapInfo, $pos++);
+ for ($i = 0; $i < $qualityEntryCount; $i++)
+ $qualityEntryTable[$i] = ReadString($bootstrapInfo, $pos);
+ $drmData = ReadString($bootstrapInfo, $pos);
+ $metadata = ReadString($bootstrapInfo, $pos);
+ $segRunTableCount = ReadByte($bootstrapInfo, $pos++);
+ $segTable = array();
+ LogDebug(sprintf("%s:", "Segment Tables"));
+ for ($i = 0; $i < $segRunTableCount; $i++)
+ {
+ LogDebug(sprintf("\nTable %d:", $i + 1));
+ ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
+ if ($boxType == "asrt")
+ $segTable[$i] = $this->ParseAsrtBox($bootstrapInfo, $pos);
+ $pos += $boxSize;
+ }
+ $fragRunTableCount = ReadByte($bootstrapInfo, $pos++);
+ $fragTable = array();
+ LogDebug(sprintf("%s:", "Fragment Tables"));
+ for ($i = 0; $i < $fragRunTableCount; $i++)
+ {
+ LogDebug(sprintf("\nTable %d:", $i + 1));
+ ReadBoxHeader($bootstrapInfo, $pos, $boxType, $boxSize);
+ if ($boxType == "afrt")
+ $fragTable[$i] = $this->ParseAfrtBox($bootstrapInfo, $pos);
+ $pos += $boxSize;
+ }
+ $this->segTable = array_replace($this->segTable, $segTable[0]);
+ $this->fragTable = array_replace($this->fragTable, $fragTable[0]);
+ $this->ParseSegAndFragTable();
+ }
+
+ function ParseAsrtBox($asrt, $pos)
+ {
+ $segTable = array();
+ $version = ReadByte($asrt, $pos);
+ $flags = ReadInt24($asrt, $pos + 1);
+ $qualityEntryCount = ReadByte($asrt, $pos + 4);
+ $pos += 5;
+ for ($i = 0; $i < $qualityEntryCount; $i++)
+ $qualitySegmentUrlModifiers[$i] = ReadString($asrt, $pos);
+ $segCount = ReadInt32($asrt, $pos);
+ $pos += 4;
+ LogDebug(sprintf(" %-8s%-10s", "Number", "Fragments"));
+ for ($i = 0; $i < $segCount; $i++)
+ {
+ $firstSegment = ReadInt32($asrt, $pos);
+ $segEntry =& $segTable[$firstSegment];
+ $segEntry['firstSegment'] = $firstSegment;
+ $segEntry['fragmentsPerSegment'] = ReadInt32($asrt, $pos + 4);
+ if ($segEntry['fragmentsPerSegment'] & 0x80000000)
+ $segEntry['fragmentsPerSegment'] = 0;
+ $pos += 8;
+ }
+ unset($segEntry);
+ foreach ($segTable as $segEntry)
+ LogDebug(sprintf(" %-8s%-10s", $segEntry['firstSegment'], $segEntry['fragmentsPerSegment']));
+ LogDebug("");
+ return $segTable;
+ }
+
+ function ParseAfrtBox($afrt, $pos)
+ {
+ $fragTable = array();
+ $version = ReadByte($afrt, $pos);
+ $flags = ReadInt24($afrt, $pos + 1);
+ $timescale = ReadInt32($afrt, $pos + 4);
+ $qualityEntryCount = ReadByte($afrt, $pos + 8);
+ $pos += 9;
+ for ($i = 0; $i < $qualityEntryCount; $i++)
+ $qualitySegmentUrlModifiers[$i] = ReadString($afrt, $pos);
+ $fragEntries = ReadInt32($afrt, $pos);
+ $pos += 4;
+ LogDebug(sprintf(" %-12s%-16s%-16s%-16s", "Number", "Timestamp", "Duration", "Discontinuity"));
+ for ($i = 0; $i < $fragEntries; $i++)
+ {
+ $firstFragment = ReadInt32($afrt, $pos);
+ $fragEntry =& $fragTable[$firstFragment];
+ $fragEntry['firstFragment'] = $firstFragment;
+ $fragEntry['firstFragmentTimestamp'] = ReadInt64($afrt, $pos + 4);
+ $fragEntry['fragmentDuration'] = ReadInt32($afrt, $pos + 12);
+ $fragEntry['discontinuityIndicator'] = "";
+ $pos += 16;
+ if ($fragEntry['fragmentDuration'] == 0)
+ $fragEntry['discontinuityIndicator'] = ReadByte($afrt, $pos++);
+ }
+ unset($fragEntry);
+ foreach ($fragTable as $fragEntry)
+ LogDebug(sprintf(" %-12s%-16s%-16s%-16s", $fragEntry['firstFragment'], $fragEntry['firstFragmentTimestamp'], $fragEntry['fragmentDuration'], $fragEntry['discontinuityIndicator']));
+ LogDebug("");
+ return $fragTable;
+ }
+
+ function ParseSegAndFragTable()
+ {
+ $firstSegment = reset($this->segTable);
+ $lastSegment = end($this->segTable);
+ $firstFragment = reset($this->fragTable);
+ $lastFragment = end($this->fragTable);
+
+ // Check if live stream is still live
+ if (($lastFragment['fragmentDuration'] == 0) and ($lastFragment['discontinuityIndicator'] == 0))
+ {
+ $this->live = false;
+ array_pop($this->fragTable);
+ $lastFragment = end($this->fragTable);
+ }
+
+ // Count total fragments by adding all entries in compactly coded segment table
+ $invalidFragCount = false;
+ $prev = reset($this->segTable);
+ $this->fragCount = $prev['fragmentsPerSegment'];
+ while ($current = next($this->segTable))
+ {
+ $this->fragCount += ($current['firstSegment'] - $prev['firstSegment'] - 1) * $prev['fragmentsPerSegment'];
+ $this->fragCount += $current['fragmentsPerSegment'];
+ $prev = $current;
+ }
+ if (!($this->fragCount & 0x80000000))
+ $this->fragCount += $firstFragment['firstFragment'] - 1;
+ if ($this->fragCount & 0x80000000)
+ {
+ $this->fragCount = 0;
+ $invalidFragCount = true;
+ }
+ if ($this->fragCount < $lastFragment['firstFragment'])
+ $this->fragCount = $lastFragment['firstFragment'];
+
+ // Determine starting segment and fragment
+ if ($this->segStart === false)
+ {
+ if ($this->live)
+ $this->segStart = $lastSegment['firstSegment'];
+ else
+ $this->segStart = $firstSegment['firstSegment'];
+ if ($this->segStart < 1)
+ $this->segStart = 1;
+ }
+ if ($this->fragStart === false)
+ {
+ if ($this->live and !$invalidFragCount)
+ $this->fragStart = $this->fragCount - 2;
+ else
+ $this->fragStart = $firstFragment['firstFragment'] - 1;
+ if ($this->fragStart < 0)
+ $this->fragStart = 0;
+ }
+ }
+
+ function GetSegmentFromFragment($fragNum)
+ {
+ $firstSegment = reset($this->segTable);
+ $lastSegment = end($this->segTable);
+ $firstFragment = reset($this->fragTable);
+ $lastFragment = end($this->fragTable);
+
+ if (count($this->segTable) == 1)
+ return $firstSegment['firstSegment'];
+ else
+ {
+ $prev = $firstSegment['firstSegment'];
+ $start = $firstFragment['firstFragment'];
+ for ($i = $firstSegment['firstSegment']; $i <= $lastSegment['firstSegment']; $i++)
+ {
+ if (isset($this->segTable[$i]))
+ $seg = $this->segTable[$i];
+ else
+ $seg = $prev;
+ $end = $start + $seg['fragmentsPerSegment'];
+ if (($fragNum >= $start) and ($fragNum < $end))
+ return $i;
+ $prev = $seg;
+ $start = $end;
+ }
+ }
+ return $lastSegment['firstSegment'];
+ }
+
+ function DownloadFragments(cURL $cc, $manifest, $opt = array())
+ {
+ $start = 0;
+ extract($opt, EXTR_IF_EXISTS);
+
+ $this->ParseManifest($cc, $manifest);
+ $segNum = $this->segStart;
+ $fragNum = $this->fragStart;
+ if ($start)
+ {
+ $segNum = $this->GetSegmentFromFragment($start);
+ $fragNum = $start - 1;
+ $this->segStart = $segNum;
+ $this->fragStart = $fragNum;
+ }
+ $this->lastFrag = $fragNum;
+ $opt['cc'] = $cc;
+ $opt['duration'] = 0;
+ $firstFragment = reset($this->fragTable);
+ LogInfo(sprintf("Fragments Total: %s, First: %s, Start: %s, Parallel: %s", $this->fragCount, $firstFragment['firstFragment'], $fragNum + 1, $this->parallel));
+
+ // Extract baseFilename
+ $this->baseFilename = $this->media['url'];
+ if (substr($this->baseFilename, -1) == '/')
+ $this->baseFilename = substr($this->baseFilename, 0, -1);
+ $this->baseFilename = RemoveExtension($this->baseFilename);
+ $lastSlash = strrpos($this->baseFilename, '/');
+ if ($lastSlash !== false)
+ $this->baseFilename = substr($this->baseFilename, $lastSlash + 1);
+ if (strpos($manifest, '?'))
+ $manifestHash = md5(substr($manifest, 0, strpos($manifest, '?')));
+ else
+ $manifestHash = md5($manifest);
+ if (strlen($this->baseFilename) > 32)
+ $this->baseFilename = md5($this->baseFilename);
+ $this->baseFilename = $manifestHash . "_" . $this->baseFilename . "_Seg" . $segNum . "-Frag";
+
+ if ($fragNum >= $this->fragCount)
+ LogError("No fragment available for downloading");
+
+ $this->fragUrl = AbsoluteUrl($this->baseUrl, $this->media['url']);
+ LogDebug("Base Fragment Url:\n" . $this->fragUrl . "\n");
+ LogDebug("Downloading Fragments:\n");
+
+ while (($fragNum < $this->fragCount) or $cc->active)
+ {
+ while ((count($cc->ch) < $this->parallel) and ($fragNum < $this->fragCount))
+ {
+ $frag = array();
+ $fragNum = $fragNum + 1;
+ $frag['id'] = $fragNum;
+ LogInfo("Downloading $fragNum/$this->fragCount fragments", true);
+ if (in_array_field($fragNum, "firstFragment", $this->fragTable, true))
+ $this->discontinuity = value_in_array_field($fragNum, "firstFragment", "discontinuityIndicator", $this->fragTable, true);
+ else
+ {
+ $closest = reset($this->fragTable);
+ $closest = $closest['firstFragment'];
+ while ($current = next($this->fragTable))
+ {
+ if ($current['firstFragment'] < $fragNum)
+ $closest = $current['firstFragment'];
+ else
+ break;
+ }
+ $this->discontinuity = value_in_array_field($closest, "firstFragment", "discontinuityIndicator", $this->fragTable, true);
+ }
+ if ($this->discontinuity !== "")
+ {
+ LogDebug("Skipping fragment $fragNum due to discontinuity, Type: " . $this->discontinuity);
+ $frag['response'] = false;
+ $this->rename = true;
+ }
+ else if (file_exists($this->baseFilename . $fragNum))
+ {
+ LogDebug("Fragment $fragNum is already downloaded");
+ $frag['response'] = file_get_contents($this->baseFilename . $fragNum);
+ }
+ if (isset($frag['response']))
+ {
+ if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING)
+ break 2;
+ else
+ continue;
+ }
+
+ LogDebug("Adding fragment $fragNum to download queue");
+ $segNum = $this->GetSegmentFromFragment($fragNum);
+ $cc->addDownload($this->fragUrl . "Seg" . $segNum . "-Frag" . $fragNum . $this->media['queryString'], $fragNum);
+ }
+
+ $downloads = $cc->checkDownloads();
+ if ($downloads !== false)
+ {
+ for ($i = 0; $i < count($downloads); $i++)
+ {
+ $frag = array();
+ $download = $downloads[$i];
+ $frag['id'] = $download['id'];
+ if ($download['status'] == 200)
+ {
+ if ($this->VerifyFragment($download['response']))
+ {
+ LogDebug("Fragment " . $this->baseFilename . $download['id'] . " successfully downloaded");
+ if (!($this->live or $this->play))
+ file_put_contents($this->baseFilename . $download['id'], $download['response']);
+ $frag['response'] = $download['response'];
+ }
+ else
+ {
+ LogDebug("Fragment " . $download['id'] . " failed to verify");
+ LogDebug("Adding fragment " . $download['id'] . " to download queue");
+ $cc->addDownload($download['url'], $download['id']);
+ }
+ }
+ else if ($download['status'] === false)
+ {
+ LogDebug("Fragment " . $download['id'] . " failed to download");
+ LogDebug("Adding fragment " . $download['id'] . " to download queue");
+ $cc->addDownload($download['url'], $download['id']);
+ }
+ else if ($download['status'] == 403)
+ LogError("Access Denied! Unable to download fragments.");
+ else if ($download['status'] == 503)
+ {
+ LogDebug("Fragment " . $download['id'] . " seems temporary unavailable");
+ LogDebug("Adding fragment " . $download['id'] . " to download queue");
+ $cc->addDownload($download['url'], $download['id']);
+ }
+ else
+ {
+ LogDebug("Fragment " . $download['id'] . " doesn't exist, Status: " . $download['status']);
+ $frag['response'] = false;
+ $this->rename = true;
+
+ /* Resync with latest available fragment when we are left behind due to slow *
+ * connection and short live window on streaming server. make sure to reset *
+ * the last written fragment. */
+ if ($this->live and ($fragNum >= $this->fragCount) and ($i + 1 == count($downloads)) and !$cc->active)
+ {
+ LogDebug("Trying to resync with latest available fragment");
+ if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING)
+ break 2;
+ unset($frag['response']);
+ $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl);
+ $fragNum = $this->fragCount - 1;
+ $this->lastFrag = $fragNum;
+ }
+ }
+ if (isset($frag['response']))
+ if ($this->WriteFragment($frag, $opt) === STOP_PROCESSING)
+ break 2;
+ }
+ unset($downloads, $download);
+ }
+ if ($this->live and ($fragNum >= $this->fragCount) and !$cc->active)
+ $this->UpdateBootstrapInfo($cc, $this->bootstrapUrl);
+ }
+
+ LogInfo("");
+ LogDebug("\nAll fragments downloaded successfully\n");
+ $cc->stopDownloads();
+ $this->processed = true;
+ }
+
+ function VerifyFragment(&$frag)
+ {
+ $fragPos = 0;
+ $fragLen = strlen($frag);
+
+ /* Some moronic servers add wrong boxSize in header causing fragment verification *
+ * to fail so we have to fix the boxSize before processing the fragment. */
+ while ($fragPos < $fragLen)
+ {
+ ReadBoxHeader($frag, $fragPos, $boxType, $boxSize);
+ if ($boxType == "mdat")
+ {
+ $len = strlen(substr($frag, $fragPos, $boxSize));
+ if ($boxSize and ($len == $boxSize))
+ return true;
+ else
+ {
+ $boxSize = $fragLen - $fragPos;
+ WriteBoxSize($frag, $fragPos, $boxType, $boxSize);
+ return true;
+ }
+ }
+ $fragPos += $boxSize;
+ }
+ return false;
+ }
+
+ function DecodeFragment($frag, $fragNum, $opt = array())
+ {
+ $debug = $this->debug;
+ $flv = false;
+ $test = false;
+ extract($opt, EXTR_IF_EXISTS);
+ if ($test)
+ $debug = false;
+
+ $flvData = "";
+ $fragPos = 0;
+ $packetTS = 0;
+ $fragLen = strlen($frag);
+
+ if (!$this->VerifyFragment($frag))
+ {
+ LogInfo("Skipping fragment number $fragNum");
+ return false;
+ }
+
+ while ($fragPos < $fragLen)
+ {
+ ReadBoxHeader($frag, $fragPos, $boxType, $boxSize);
+ if ($boxType == "mdat")
+ {
+ $fragLen = $fragPos + $boxSize;
+ break;
+ }
+ $fragPos += $boxSize;
+ }
+
+ LogDebug(sprintf("\nFragment %d:\n" . $this->format . "%-16s", $fragNum, "Type", "CurrentTS", "PreviousTS", "Size", "Position"), $debug);
+ while ($fragPos < $fragLen)
+ {
+ $packetType = ReadByte($frag, $fragPos);
+ $packetSize = ReadInt24($frag, $fragPos + 1);
+ $packetTS = ReadInt24($frag, $fragPos + 4);
+ $packetTS = $packetTS | (ReadByte($frag, $fragPos + 7) << 24);
+ if ($packetTS & 0x80000000)
+ $packetTS &= 0x7FFFFFFF;
+ $totalTagLen = $this->tagHeaderLen + $packetSize + $this->prevTagSize;
+
+ // Try to fix the odd timestamps and make them zero based
+ $currentTS = $packetTS;
+ $lastTS = $this->prevVideoTS >= $this->prevAudioTS ? $this->prevVideoTS : $this->prevAudioTS;
+ $fixedTS = $lastTS + FRAMEFIX_STEP;
+ if (($this->baseTS == INVALID_TIMESTAMP) and (($packetType == AUDIO) or ($packetType == VIDEO)))
+ $this->baseTS = $packetTS;
+ if (($this->baseTS > 1000) and ($packetTS >= $this->baseTS))
+ $packetTS -= $this->baseTS;
+ if ($lastTS != INVALID_TIMESTAMP)
+ {
+ $timeShift = $packetTS - $lastTS;
+ if ($timeShift > $this->fixWindow)
+ {
+ LogDebug("Timestamp gap detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " Timeshift=" . $timeShift, $debug);
+ if ($this->baseTS < $packetTS)
+ $this->baseTS += $timeShift - FRAMEFIX_STEP;
+ else
+ $this->baseTS = $timeShift - FRAMEFIX_STEP;
+ $packetTS = $fixedTS;
+ }
+ else
+ {
+ $lastTS = $packetType == VIDEO ? $this->prevVideoTS : $this->prevAudioTS;
+ if ($packetTS < ($lastTS - $this->fixWindow))
+ {
+ if (($this->negTS != INVALID_TIMESTAMP) and (($packetTS + $this->negTS) < ($lastTS - $this->fixWindow)))
+ $this->negTS = INVALID_TIMESTAMP;
+ if ($this->negTS == INVALID_TIMESTAMP)
+ {
+ $this->negTS = $fixedTS - $packetTS;
+ LogDebug("Negative timestamp detected: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug);
+ $packetTS = $fixedTS;
+ }
+ else
+ {
+ if (($packetTS + $this->negTS) <= ($lastTS + $this->fixWindow))
+ $packetTS += $this->negTS;
+ else
+ {
+ $this->negTS = $fixedTS - $packetTS;
+ LogDebug("Negative timestamp override: PacketTS=" . $packetTS . " LastTS=" . $lastTS . " NegativeTS=" . $this->negTS, $debug);
+ $packetTS = $fixedTS;
+ }
+ }
+ }
+ }
+ }
+ if ($packetTS != $currentTS)
+ WriteFlvTimestamp($frag, $fragPos, $packetTS);
+
+ switch ($packetType)
+ {
+ case AUDIO:
+ if ($packetTS > $this->prevAudioTS - $this->fixWindow)
+ {
+ $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen);
+ $CodecID = ($FrameInfo & 0xF0) >> 4;
+ if ($CodecID == CODEC_ID_AAC)
+ {
+ $AAC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1);
+ if ($AAC_PacketType == AAC_SEQUENCE_HEADER)
+ {
+ if ($this->AAC_HeaderWritten)
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
+ break;
+ }
+ else
+ {
+ LogDebug("Writing AAC sequence header", $debug);
+ $this->AAC_HeaderWritten = true;
+ }
+ }
+ else if (!$this->AAC_HeaderWritten)
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Discarding audio packet received before AAC sequence header", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
+ break;
+ }
+ }
+ if ($packetSize > 0)
+ {
+ // Check for packets with non-monotonic audio timestamps and fix them
+ if (!(($CodecID == CODEC_ID_AAC) and (($AAC_PacketType == AAC_SEQUENCE_HEADER) or $this->prevAAC_Header)))
+ if (($this->prevAudioTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevAudioTS))
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Fixing audio timestamp", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
+ $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevAudioTS - $packetTS);
+ WriteFlvTimestamp($frag, $fragPos, $packetTS);
+ }
+ if (is_resource($flv))
+ {
+ $this->pAudioTagPos = ftell($flv);
+ $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen);
+ if (!$status)
+ LogError("Failed to write flv data to file");
+ if ($debug)
+ LogDebug(sprintf($this->format . "%-16s", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize, $this->pAudioTagPos));
+ }
+ else
+ {
+ $flvData .= substr($frag, $fragPos, $totalTagLen);
+ if ($debug)
+ LogDebug(sprintf($this->format, "AUDIO", $packetTS, $this->prevAudioTS, $packetSize));
+ }
+ if (($CodecID == CODEC_ID_AAC) and ($AAC_PacketType == AAC_SEQUENCE_HEADER))
+ $this->prevAAC_Header = true;
+ else
+ $this->prevAAC_Header = false;
+ $this->prevAudioTS = $packetTS;
+ $this->pAudioTagLen = $totalTagLen;
+ }
+ else
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized audio packet", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
+ }
+ else
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping audio packet in fragment $fragNum", "AUDIO", $packetTS, $this->prevAudioTS, $packetSize), $debug);
+ if (!$this->audio)
+ $this->audio = true;
+ break;
+ case VIDEO:
+ if ($packetTS > $this->prevVideoTS - $this->fixWindow)
+ {
+ $FrameInfo = ReadByte($frag, $fragPos + $this->tagHeaderLen);
+ $FrameType = ($FrameInfo & 0xF0) >> 4;
+ $CodecID = $FrameInfo & 0x0F;
+ if ($FrameType == FRAME_TYPE_INFO)
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping video info frame", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ break;
+ }
+ if ($CodecID == CODEC_ID_AVC)
+ {
+ $AVC_PacketType = ReadByte($frag, $fragPos + $this->tagHeaderLen + 1);
+ if ($AVC_PacketType == AVC_SEQUENCE_HEADER)
+ {
+ if ($this->AVC_HeaderWritten)
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ break;
+ }
+ else
+ {
+ LogDebug("Writing AVC sequence header", $debug);
+ $this->AVC_HeaderWritten = true;
+ }
+ }
+ else if (!$this->AVC_HeaderWritten)
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Discarding video packet received before AVC sequence header", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ break;
+ }
+ }
+ if ($packetSize > 0)
+ {
+ $pts = $packetTS;
+ if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_NALU))
+ {
+ $cts = ReadInt24($frag, $fragPos + $this->tagHeaderLen + 2);
+ $cts = ($cts + 0xff800000) ^ 0xff800000;
+ $pts = $packetTS + $cts;
+ if ($cts != 0)
+ LogDebug("DTS: $packetTS CTS: $cts PTS: $pts", $debug);
+ }
+
+ // Check for packets with non-monotonic video timestamps and fix them
+ if (!(($CodecID == CODEC_ID_AVC) and (($AVC_PacketType == AVC_SEQUENCE_HEADER) or ($AVC_PacketType == AVC_SEQUENCE_END) or $this->prevAVC_Header)))
+ if (($this->prevVideoTS != INVALID_TIMESTAMP) and ($packetTS <= $this->prevVideoTS))
+ {
+ LogDebug(sprintf("%s\n" . $this->format, "Fixing video timestamp", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ $packetTS += (FRAMEFIX_STEP / 5) + ($this->prevVideoTS - $packetTS);
+ WriteFlvTimestamp($frag, $fragPos, $packetTS);
+ }
+ if (is_resource($flv))
+ {
+ $this->pVideoTagPos = ftell($flv);
+ $status = fwrite($flv, substr($frag, $fragPos, $totalTagLen), $totalTagLen);
+ if (!$status)
+ LogError("Failed to write flv data to file");
+ if ($debug)
+ LogDebug(sprintf($this->format . "%-16s", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize, $this->pVideoTagPos));
+ }
+ else
+ {
+ $flvData .= substr($frag, $fragPos, $totalTagLen);
+ if ($debug)
+ LogDebug(sprintf($this->format, "VIDEO", $packetTS, $this->prevVideoTS, $packetSize));
+ }
+ if (($CodecID == CODEC_ID_AVC) and ($AVC_PacketType == AVC_SEQUENCE_HEADER))
+ $this->prevAVC_Header = true;
+ else
+ $this->prevAVC_Header = false;
+ $this->prevVideoTS = $packetTS;
+ $this->pVideoTagLen = $totalTagLen;
+ }
+ else
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping small sized video packet", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ }
+ else
+ LogDebug(sprintf("%s\n" . $this->format, "Skipping video packet in fragment $fragNum", "VIDEO", $packetTS, $this->prevVideoTS, $packetSize), $debug);
+ if (!$this->video)
+ $this->video = true;
+ break;
+ case SCRIPT_DATA:
+ break;
+ default:
+ if (($packetType == 10) or ($packetType == 11))
+ LogError("This stream is encrypted with Akamai DRM. Decryption of such streams isn't currently possible with this script.", 2);
+ else if (($packetType == 40) or ($packetType == 41))
+ LogError("This stream is encrypted with FlashAccess DRM. Decryption of such streams isn't currently possible with this script.", 2);
+ else
+ {
+ LogInfo("Unknown packet type " . $packetType . " encountered! Unable to process fragment $fragNum");
+ break 2;
+ }
+ }
+ $fragPos += $totalTagLen;
+ }
+ $this->duration = round($packetTS / 1000, 0);
+ if (is_resource($flv))
+ {
+ $this->filesize = ftell($flv) / (1024 * 1024);
+ return true;
+ }
+ else
+ return $flvData;
+ }
+
+ function WriteFragment($download, &$opt)
+ {
+ $this->frags[$download['id']] = $download;
+
+ $available = count($this->frags);
+ for ($i = 0; $i < $available; $i++)
+ {
+ if (isset($this->frags[$this->lastFrag + 1]))
+ {
+ $frag = $this->frags[$this->lastFrag + 1];
+ if ($frag['response'] !== false)
+ {
+ LogDebug("Writing fragment " . $frag['id'] . " to flv file");
+ if (!isset($opt['file']))
+ {
+ $opt['test'] = true;
+ if ($this->play)
+ $outFile = STDOUT;
+ else if ($this->outFile)
+ {
+ if ($opt['filesize'])
+ $outFile = JoinUrl($this->outDir, $this->outFile . '-' . $this->fileCount++ . ".flv");
+ else
+ $outFile = JoinUrl($this->outDir, $this->outFile . ".flv");
+ }
+ else
+ {
+ if ($opt['filesize'])
+ $outFile = JoinUrl($this->outDir, $this->baseFilename . '-' . $this->fileCount++ . ".flv");
+ else
+ $outFile = JoinUrl($this->outDir, $this->baseFilename . ".flv");
+ }
+ $this->InitDecoder();
+ $this->DecodeFragment($frag['response'], $frag['id'], $opt);
+ $opt['file'] = WriteFlvFile($outFile, $this->audio, $this->video);
+ if ($this->metadata)
+ WriteMetadata($this, $opt['file']);
+
+ $opt['test'] = false;
+ $this->InitDecoder();
+ }
+ $flvData = $this->DecodeFragment($frag['response'], $frag['id'], $opt);
+ if (strlen($flvData))
+ {
+ $status = fwrite($opt['file'], $flvData, strlen($flvData));
+ if (!$status)
+ LogError("Failed to write flv data");
+ if (!$this->play)
+ $this->filesize = ftell($opt['file']) / (1024 * 1024);
+ }
+ $this->lastFrag = $frag['id'];
+ }
+ else
+ {
+ $this->lastFrag += 1;
+ LogDebug("Skipping failed fragment " . $this->lastFrag);
+ }
+ unset($this->frags[$this->lastFrag]);
+ }
+ else
+ break;
+
+ if ($opt['tDuration'] and (($opt['duration'] + $this->duration) >= $opt['tDuration']))
+ {
+ LogInfo("");
+ LogInfo(($opt['duration'] + $this->duration) . " seconds of content has been recorded successfully.", true);
+ return STOP_PROCESSING;
+ }
+ if ($opt['filesize'] and ($this->filesize >= $opt['filesize']))
+ {
+ $this->filesize = 0;
+ $opt['duration'] += $this->duration;
+ fclose($opt['file']);
+ unset($opt['file']);
+ }
+ }
+
+ if (!count($this->frags))
+ unset($this->frags);
+ return true;
+ }
+ }
+
+ function ReadByte($str, $pos)
+ {
+ $int = unpack('C', $str[$pos]);
+ return $int[1];
+ }
+
+ function ReadInt16($str, $pos)
+ {
+ $int32 = unpack('N', "\x00\x00" . substr($str, $pos, 2));
+ return $int32[1];
+ }
+
+ function ReadInt24($str, $pos)
+ {
+ $int32 = unpack('N', "\x00" . substr($str, $pos, 3));
+ return $int32[1];
+ }
+
+ function ReadInt32($str, $pos)
+ {
+ $int32 = unpack('N', substr($str, $pos, 4));
+ return $int32[1];
+ }
+
+ function ReadInt64($str, $pos)
+ {
+ $hi = sprintf("%u", ReadInt32($str, $pos));
+ $lo = sprintf("%u", ReadInt32($str, $pos + 4));
+ $int64 = bcadd(bcmul($hi, "4294967296"), $lo);
+ return $int64;
+ }
+
+ function ReadDouble($str, $pos)
+ {
+ $double = unpack('d', strrev(substr($str, $pos, 8)));
+ return $double[1];
+ }
+
+ function ReadString($str, &$pos)
+ {
+ $len = 0;
+ while ($str[$pos + $len] != "\x00")
+ $len++;
+ $str = substr($str, $pos, $len);
+ $pos += $len + 1;
+ return $str;
+ }
+
+ function ReadBoxHeader($str, &$pos, &$boxType, &$boxSize)
+ {
+ if (!isset($pos))
+ $pos = 0;
+ $boxSize = ReadInt32($str, $pos);
+ $boxType = substr($str, $pos + 4, 4);
+ if ($boxSize == 1)
+ {
+ $boxSize = ReadInt64($str, $pos + 8) - 16;
+ $pos += 16;
+ }
+ else
+ {
+ $boxSize -= 8;
+ $pos += 8;
+ }
+ if ($boxSize <= 0)
+ $boxSize = 0;
+ }
+
+ function WriteByte(&$str, $pos, $int)
+ {
+ $str[$pos] = pack('C', $int);
+ }
+
+ function WriteInt24(&$str, $pos, $int)
+ {
+ $str[$pos] = pack('C', ($int & 0xFF0000) >> 16);
+ $str[$pos + 1] = pack('C', ($int & 0xFF00) >> 8);
+ $str[$pos + 2] = pack('C', $int & 0xFF);
+ }
+
+ function WriteInt32(&$str, $pos, $int)
+ {
+ $str[$pos] = pack('C', ($int & 0xFF000000) >> 24);
+ $str[$pos + 1] = pack('C', ($int & 0xFF0000) >> 16);
+ $str[$pos + 2] = pack('C', ($int & 0xFF00) >> 8);
+ $str[$pos + 3] = pack('C', $int & 0xFF);
+ }
+
+ function WriteBoxSize(&$str, $pos, $type, $size)
+ {
+ if (substr($str, $pos - 4, 4) == $type)
+ WriteInt32($str, $pos - 8, $size);
+ else
+ {
+ WriteInt32($str, $pos - 8, 0);
+ WriteInt32($str, $pos - 4, $size);
+ }
+ }
+
+ function WriteFlvTimestamp(&$frag, $fragPos, $packetTS)
+ {
+ WriteInt24($frag, $fragPos + 4, ($packetTS & 0x00FFFFFF));
+ WriteByte($frag, $fragPos + 7, ($packetTS & 0xFF000000) >> 24);
+ }
+
+ function AbsoluteUrl($baseUrl, $url)
+ {
+ if (!isHttpUrl($url))
+ $url = JoinUrl($baseUrl, $url);
+ return NormalizePath($url);
+ }
+
+ function GetString($object)
+ {
+ return trim(strval($object));
+ }
+
+ function isHttpUrl($url)
+ {
+ return (strncasecmp($url, "http", 4) == 0) ? true : false;
+ }
+
+ function isRtmpUrl($url)
+ {
+ return (preg_match('/^rtm(p|pe|pt|pte|ps|pts|fp):\/\//i', $url)) ? true : false;
+ }
+
+ function JoinUrl($firstUrl, $secondUrl)
+ {
+ if ($firstUrl and $secondUrl)
+ {
+ if (substr($firstUrl, -1) == '/')
+ $firstUrl = substr($firstUrl, 0, -1);
+ if (substr($secondUrl, 0, 1) == '/')
+ $secondUrl = substr($secondUrl, 1);
+ return $firstUrl . '/' . $secondUrl;
+ }
+ else if ($firstUrl)
+ return $firstUrl;
+ else
+ return $secondUrl;
+ }
+
+ function KeyName(array $a, $pos)
+ {
+ $temp = array_slice($a, $pos, 1, true);
+ return key($temp);
+ }
+
+ function LogDebug($msg, $display = true)
+ {
+ global $debug, $showHeader;
+ if ($showHeader)
+ {
+ ShowHeader();
+ $showHeader = false;
+ }
+ if ($display and $debug)
+ fwrite(STDERR, $msg . "\n");
+ }
+
+ function LogError($msg, $code = 1)
+ {
+ LogInfo($msg);
+ exit($code);
+ }
+
+ function LogInfo($msg, $progress = false)
+ {
+ global $quiet, $showHeader;
+ if ($showHeader)
+ {
+ ShowHeader();
+ $showHeader = false;
+ }
+ if (!$quiet)
+ PrintLine($msg, $progress);
+ }
+
+ function NormalizePath($path)
+ {
+ $inSegs = preg_split('/(?<!\/)\/(?!\/)/u', $path);
+ $outSegs = array();
+
+ foreach ($inSegs as $seg)
+ {
+ if ($seg == '' or $seg == '.')
+ continue;
+ if ($seg == '..')
+ array_pop($outSegs);
+ else
+ array_push($outSegs, $seg);
+ }
+ $outPath = implode('/', $outSegs);
+
+ if (substr($path, 0, 1) == '/')
+ $outPath = '/' . $outPath;
+ if (substr($path, -1) == '/')
+ $outPath .= '/';
+ return $outPath;
+ }
+
+ function PrintLine($msg, $progress = false)
+ {
+ if ($msg)
+ {
+ printf("\r%-79s\r", "");
+ if ($progress)
+ printf("%s\r", $msg);
+ else
+ printf("%s\n", $msg);
+ }
+ else
+ printf("\n");
+ }
+
+ function RemoveExtension($outFile)
+ {
+ preg_match("/\.\w{1,4}$/i", $outFile, $extension);
+ if (isset($extension[0]))
+ {
+ $extension = $extension[0];
+ $outFile = substr($outFile, 0, -strlen($extension));
+ return $outFile;
+ }
+ return $outFile;
+ }
+
+ function RenameFragments($baseFilename, $fragNum, $fileExt)
+ {
+ $files = array();
+ $retries = 0;
+
+ while (true)
+ {
+ if ($retries >= 50)
+ break;
+ $file = $baseFilename . ++$fragNum;
+ if (file_exists($file))
+ {
+ $files[] = $file;
+ $retries = 0;
+ }
+ else if (file_exists($file . $fileExt))
+ {
+ $files[] = $file . $fileExt;
+ $retries = 0;
+ }
+ else
+ $retries++;
+ }
+
+ $fragCount = count($files);
+ natsort($files);
+ for ($i = 0; $i < $fragCount; $i++)
+ rename($files[$i], $baseFilename . ($i + 1));
+ }
+
+ function ShowHeader()
+ {
+ $header = "KSV Adobe HDS Downloader";
+ $len = strlen($header);
+ $width = floor((80 - $len) / 2) + $len;
+ $format = "\n%" . $width . "s\n\n";
+ printf($format, $header);
+ }
+
+ function WriteFlvFile($outFile, $audio = true, $video = true)
+ {
+ $flvHeader = pack("H*", "464c5601050000000900000000");
+ $flvHeaderLen = strlen($flvHeader);
+
+ // Set proper Audio/Video marker
+ WriteByte($flvHeader, 4, $audio << 2 | $video);
+
+ if (is_resource($outFile))
+ $flv = $outFile;
+ else
+ $flv = fopen($outFile, "w+b");
+ if (!$flv)
+ LogError("Failed to open " . $outFile);
+ fwrite($flv, $flvHeader, $flvHeaderLen);
+ return $flv;
+ }
+
+ function WriteMetadata($f4f, $flv)
+ {
+ if (isset($f4f->media) and $f4f->media['metadata'])
+ {
+ $metadataSize = strlen($f4f->media['metadata']);
+ WriteByte($metadata, 0, SCRIPT_DATA);
+ WriteInt24($metadata, 1, $metadataSize);
+ WriteInt24($metadata, 4, 0);
+ WriteInt32($metadata, 7, 0);
+ $metadata = implode("", $metadata) . $f4f->media['metadata'];
+ WriteByte($metadata, $f4f->tagHeaderLen + $metadataSize - 1, 0x09);
+ WriteInt32($metadata, $f4f->tagHeaderLen + $metadataSize, $f4f->tagHeaderLen + $metadataSize);
+ if (is_resource($flv))
+ {
+ fwrite($flv, $metadata, $f4f->tagHeaderLen + $metadataSize + $f4f->prevTagSize);
+ return true;
+ }
+ else
+ return $metadata;
+ }
+ return false;
+ }
+
+ function in_array_field($needle, $needle_field, $haystack, $strict = false)
+ {
+ if ($strict)
+ {
+ foreach ($haystack as $item)
+ if (isset($item[$needle_field]) and $item[$needle_field] === $needle)
+ return true;
+ }
+ else
+ {
+ foreach ($haystack as $item)
+ if (isset($item[$needle_field]) and $item[$needle_field] == $needle)
+ return true;
+ }
+ return false;
+ }
+
+ function value_in_array_field($needle, $needle_field, $value_field, $haystack, $strict = false)
+ {
+ if ($strict)
+ {
+ foreach ($haystack as $item)
+ if (isset($item[$needle_field]) and $item[$needle_field] === $needle)
+ return $item[$value_field];
+ }
+ else
+ {
+ foreach ($haystack as $item)
+ if (isset($item[$needle_field]) and $item[$needle_field] == $needle)
+ return $item[$value_field];
+ }
+ return false;
+ }
+
+ // Global code starts here
+ $format = " %-8s%-16s%-16s%-8s";
+ $baseFilename = "";
+ $debug = false;
+ $duration = 0;
+ $delete = false;
+ $fileExt = ".f4f";
+ $fileCount = 1;
+ $filesize = 0;
+ $fixWindow = 1000;
+ $fragCount = 0;
+ $fragNum = 0;
+ $manifest = "";
+ $maxSpeed = 0;
+ $metadata = true;
+ $outDir = "";
+ $outFile = "";
+ $play = false;
+ $quiet = false;
+ $referrer = "";
+ $rename = false;
+ $showHeader = true;
+ $start = 0;
+ $update = false;
+
+ $options = array(
+ 0 => array(
+ 'help' => 'displays this help',
+ 'debug' => 'show debug output',
+ 'delete' => 'delete fragments after processing',
+ 'fproxy' => 'force proxy for downloading of fragments',
+ 'play' => 'dump stream to stdout for piping to media player',
+ 'rename' => 'rename fragments sequentially before processing',
+ 'update' => 'update the script to current git version'
+ ),
+ 1 => array(
+ 'auth' => 'authentication string for fragment requests',
+ 'duration' => 'stop recording after specified number of seconds',
+ 'filesize' => 'split output file in chunks of specified size (MB)',
+ 'fragments' => 'base filename for fragments',
+ 'fixwindow' => 'timestamp gap between frames to consider as timeshift',
+ 'manifest' => 'manifest file for downloading of fragments',
+ 'maxspeed' => 'maximum bandwidth consumption (KB) for fragment downloading',
+ 'outdir' => 'destination folder for output file',
+ 'outfile' => 'filename to use for output file',
+ 'parallel' => 'number of fragments to download simultaneously',
+ 'proxy' => 'proxy for downloading of manifest',
+ 'quality' => 'selected quality level (low|medium|high) or exact bitrate',
+ 'referrer' => 'Referer to use for emulation of browser requests',
+ 'start' => 'start from specified fragment',
+ 'useragent' => 'User-Agent to use for emulation of browser requests'
+ )
+ );
+ $cli = new CLI($options, true);
+
+ // Set large enough memory limit
+ ini_set("memory_limit", "512M");
+
+ // Check if STDOUT is available
+ if ($cli->getParam('play'))
+ {
+ $play = true;
+ $quiet = true;
+ $showHeader = false;
+ }
+ if ($cli->getParam('help'))
+ {
+ $cli->displayHelp();
+ exit(0);
+ }
+
+ // Check for required extensions
+ $required_extensions = array(
+ "bcmath",
+ "curl",
+ "SimpleXML"
+ );
+ $missing_extensions = array_diff($required_extensions, get_loaded_extensions());
+ if ($missing_extensions)
+ {
+ $msg = "You have to install the following extension(s) to continue: '" . implode("', '", $missing_extensions) . "'";
+ LogError($msg);
+ }
+
+ // Initialize classes
+ $cc = new cURL();
+ $f4f = new F4F();
+
+ $f4f->baseFilename =& $baseFilename;
+ $f4f->debug =& $debug;
+ $f4f->fixWindow =& $fixWindow;
+ $f4f->format =& $format;
+ $f4f->metadata =& $metadata;
+ $f4f->outDir =& $outDir;
+ $f4f->outFile =& $outFile;
+ $f4f->play =& $play;
+ $f4f->rename =& $rename;
+
+ // Process command line options
+ if (isset($cli->params['unknown']))
+ $baseFilename = $cli->params['unknown'][0];
+ if ($cli->getParam('debug'))
+ $debug = true;
+ if ($cli->getParam('delete'))
+ $delete = true;
+ if ($cli->getParam('fproxy'))
+ $cc->fragProxy = true;
+ if ($cli->getParam('rename'))
+ $rename = $cli->getParam('rename');
+ if ($cli->getParam('update'))
+ $update = true;
+ if ($cli->getParam('auth'))
+ $f4f->auth = '?' . $cli->getParam('auth');
+ if ($cli->getParam('duration'))
+ $duration = $cli->getParam('duration');
+ if ($cli->getParam('filesize'))
+ $filesize = $cli->getParam('filesize');
+ if ($cli->getParam('fixwindow'))
+ $fixWindow = $cli->getParam('fixwindow');
+ if ($cli->getParam('fragments'))
+ $baseFilename = $cli->getParam('fragments');
+ if ($cli->getParam('manifest'))
+ $manifest = $cli->getParam('manifest');
+ if ($cli->getParam('maxspeed'))
+ $maxSpeed = $cli->getParam('maxspeed');
+ if ($cli->getParam('outdir'))
+ $outDir = $cli->getParam('outdir');
+ if ($cli->getParam('outfile'))
+ $outFile = $cli->getParam('outfile');
+ if ($cli->getParam('parallel'))
+ $f4f->parallel = $cli->getParam('parallel');
+ if ($cli->getParam('proxy'))
+ $cc->proxy = $cli->getParam('proxy');
+ if ($cli->getParam('quality'))
+ $f4f->quality = $cli->getParam('quality');
+ if ($cli->getParam('referrer'))
+ $referrer = $cli->getParam('referrer');
+ if ($cli->getParam('start'))
+ $start = $cli->getParam('start');
+ if ($cli->getParam('useragent'))
+ $cc->user_agent = $cli->getParam('useragent');
+
+ // Use custom referrer
+ if ($referrer)
+ $cc->headers[] = "Referer: " . $referrer;
+
+ // Update the script
+ if ($update)
+ {
+ LogInfo("Updating script....");
+ $status = $cc->get("https://raw.github.com/K-S-V/Scripts/master/AdobeHDS.php");
+ if ($status == 200)
+ {
+ if (md5($cc->response) == md5(file_get_contents($argv[0])))
+ LogError("You are already using the latest version of this script.", 0);
+ $status = file_put_contents($argv[0], $cc->response);
+ if (!$status)
+ LogError("Failed to write script file");
+ LogError("Script has been updated successfully.", 0);
+ }
+ else
+ LogError("Failed to update script");
+ }
+
+ // Set overall maximum bandwidth for fragment downloading
+ if ($maxSpeed > 0)
+ {
+ $cc->maxSpeed = ($maxSpeed * 1024) / $f4f->parallel;
+ LogDebug(sprintf("Setting maximum speed to %.2f KB per fragment (overall $maxSpeed KB)", $cc->maxSpeed / 1024));
+ }
+
+ // Create output directory
+ if ($outDir)
+ {
+ $outDir = rtrim(str_replace('\\', '/', $outDir));
+ if (!file_exists($outDir))
+ {
+ LogDebug("Creating destination directory " . $outDir);
+ if (!mkdir($outDir, 0777, true))
+ LogError("Failed to create destination directory " . $outDir);
+ }
+ }
+
+ // Remove existing file extension
+ if ($outFile)
+ $outFile = RemoveExtension($outFile);
+
+ // Disable filesize when piping
+ if ($play)
+ $filesize = 0;
+
+ // Disable metadata if it invalidates the stream duration
+ if ($start or $duration or $filesize)
+ $metadata = false;
+
+ // Download fragments when manifest is available
+ if ($manifest)
+ {
+ $manifest = AbsoluteUrl("http://", $manifest);
+ $opt = array(
+ 'start' => $start,
+ 'tDuration' => $duration,
+ 'filesize' => $filesize
+ );
+ $f4f->DownloadFragments($cc, $manifest, $opt);
+ }
+
+ // Determine output filename
+ if (!$outFile)
+ {
+ $baseFilename = str_replace('\\', '/', $baseFilename);
+ $lastChar = substr($baseFilename, -1);
+ if ($baseFilename and !(($lastChar == '/') or ($lastChar == ':')))
+ {
+ $lastSlash = strrpos($baseFilename, '/');
+ if ($lastSlash)
+ $outFile = substr($baseFilename, $lastSlash + 1);
+ else
+ $outFile = $baseFilename;
+ }
+ else
+ $outFile = "Joined";
+ $outFile = RemoveExtension($outFile);
+ }
+
+ // Check for available fragments and rename if required
+ if ($f4f->fragNum)
+ $fragNum = $f4f->fragNum;
+ else if ($start)
+ $fragNum = $start - 1;
+ if ($rename)
+ {
+ RenameFragments($baseFilename, $fragNum, $fileExt);
+ $fragNum = 0;
+ }
+ $count = $fragNum + 1;
+ while (true)
+ {
+ if (file_exists($baseFilename . $count) or file_exists($baseFilename . $count . $fileExt))
+ $fragCount++;
+ else
+ break;
+ $count++;
+ }
+ LogInfo("Found $fragCount fragments");
+
+ if (!$f4f->processed)
+ {
+ // Process available fragments
+ if ($fragCount < 1)
+ exit(1);
+ $timeStart = microtime(true);
+ LogDebug("Joining Fragments:");
+ for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++)
+ {
+ $file = $baseFilename . $i;
+ if (file_exists($file))
+ $frag = file_get_contents($file);
+ else if (file_exists($file . $fileExt))
+ $frag = file_get_contents($file . $fileExt);
+ if (!isset($opt['flv']))
+ {
+ $opt['test'] = true;
+ $f4f->InitDecoder();
+ $f4f->DecodeFragment($frag, $i, $opt);
+ if ($filesize)
+ $opt['flv'] = WriteFlvFile(JoinUrl($outDir, $outFile . '-' . $fileCount++ . ".flv"), $f4f->audio, $f4f->video);
+ else
+ $opt['flv'] = WriteFlvFile(JoinUrl($outDir, $outFile . ".flv"), $f4f->audio, $f4f->video);
+ if ($metadata)
+ WriteMetadata($f4f, $opt['flv']);
+
+ $opt['test'] = false;
+ $f4f->InitDecoder();
+ }
+ $f4f->DecodeFragment($frag, $i, $opt);
+ if ($filesize and ($f4f->filesize >= $filesize))
+ {
+ $f4f->filesize = 0;
+ fclose($opt['flv']);
+ unset($opt['flv']);
+ }
+ LogInfo("Processed " . ($i - $fragNum) . " fragments", true);
+ }
+ if (isset($opt['flv']))
+ fclose($opt['flv']);
+ $timeEnd = microtime(true);
+ $timeTaken = sprintf("%.2f", $timeEnd - $timeStart);
+ LogInfo("Joined $fragCount fragments in $timeTaken seconds");
+ }
+
+ // Delete fragments after processing
+ if ($delete)
+ {
+ for ($i = $fragNum + 1; $i <= $fragNum + $fragCount; $i++)
+ {
+ $file = $baseFilename . $i;
+ if (file_exists($file))
+ unlink($file);
+ else if (file_exists($file . $fileExt))
+ unlink($file . $fileExt);
+ }
+ }
+
+ LogInfo("Finished");
+?>
diff --git a/fb-img-resize b/fb-img-resize
new file mode 100755
index 0000000..8825b5b
--- /dev/null
+++ b/fb-img-resize
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+TMPDIR="$(mktemp -d "/tmp/${0##*/}.XXXXXX")"
+trap "rm -rf '${TMPDIR}'" EXIT TERM
+
+for i in "$@"; do
+ convert "$i" -resize 1500x1500 "$TMPDIR/${i##*/}"
+done
+
+fb -m "$TMPDIR"/*
diff --git a/woof b/woof
new file mode 100644
index 0000000..cf128dc
--- /dev/null
+++ b/woof
@@ -0,0 +1,620 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+#
+# woof -- an ad-hoc single file webserver
+# Copyright (C) 2004-2009 Simon Budig <simon@budig.de>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# A copy of the GNU General Public License is available at
+# http://www.fsf.org/licenses/gpl.txt, you can also write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+# Darwin support with the help from Mat Caughron, <mat@phpconsulting.com>
+# Solaris support by Colin Marquardt, <colin.marquardt@zmd.de>
+# FreeBSD support with the help from Andy Gimblett, <A.M.Gimblett@swansea.ac.uk>
+# Cygwin support by Stefan Reichör <stefan@xsteve.at>
+# tarfile usage suggested by Morgan Lefieux <comete@geekandfree.org>
+# File upload support loosely based on code from Stephen English <steve@secomputing.co.uk>
+
+import sys, os, errno, socket, getopt, commands, tempfile
+import cgi, urllib, urlparse, BaseHTTPServer
+import readline
+import ConfigParser
+import shutil, tarfile, zipfile
+import struct
+
+maxdownloads = 1
+TM = object
+cpid = -1
+compressed = 'gz'
+upload = False
+
+
+class EvilZipStreamWrapper(TM):
+ def __init__ (self, victim):
+ self.victim_fd = victim
+ self.position = 0
+ self.tells = []
+ self.in_file_data = 0
+
+ def tell (self):
+ self.tells.append (self.position)
+ return self.position
+
+ def seek (self, offset, whence = 0):
+ if offset != 0:
+ if offset == self.tells[0] + 14:
+ # the zipfile module tries to fix up the file header.
+ # write Data descriptor header instead,
+ # the next write from zipfile
+ # is CRC, compressed_size and file_size (as required)
+ self.write ("PK\007\010")
+ elif offset == self.tells[1]:
+ # the zipfile module goes to the end of the file. The next
+ # data written definitely is infrastructure (in_file_data = 0)
+ self.tells = []
+ self.in_file_data = 0
+ else:
+ raise "unexpected seek for EvilZipStreamWrapper"
+
+ def write (self, data):
+ # only test for headers if we know that we're not writing
+ # (potentially compressed) data.
+ if self.in_file_data == 0:
+ if data[:4] == zipfile.stringFileHeader:
+ # fix the file header for extra Data descriptor
+ hdr = list (struct.unpack (zipfile.structFileHeader, data[:30]))
+ hdr[3] |= (1 << 3)
+ data = struct.pack (zipfile.structFileHeader, *hdr) + data[30:]
+ self.in_file_data = 1
+ elif data[:4] == zipfile.stringCentralDir:
+ # fix the directory entry to match file header.
+ hdr = list (struct.unpack (zipfile.structCentralDir, data[:46]))
+ hdr[5] |= (1 << 3)
+ data = struct.pack (zipfile.structCentralDir, *hdr) + data[46:]
+
+ self.position += len (data)
+ self.victim_fd.write (data)
+
+ def __getattr__ (self, name):
+ return getattr (self.victim_fd, name)
+
+
+# Utility function to guess the IP (as a string) where the server can be
+# reached from the outside. Quite nasty problem actually.
+
+def find_ip ():
+ # we get a UDP-socket for the TEST-networks reserved by IANA.
+ # It is highly unlikely, that there is special routing used
+ # for these networks, hence the socket later should give us
+ # the ip address of the default route.
+ # We're doing multiple tests, to guard against the computer being
+ # part of a test installation.
+
+ candidates = []
+ for test_ip in ["192.0.2.0", "198.51.100.0", "203.0.113.0"]:
+ s = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
+ s.connect ((test_ip, 80))
+ ip_addr = s.getsockname ()[0]
+ s.close ()
+ if ip_addr in candidates:
+ return ip_addr
+ candidates.append (ip_addr)
+
+ return candidates[0]
+
+
+# our own HTTP server class, fixing up a change in python 2.7
+# since we do our fork() in the request handler
+# the server must not shutdown() the socket.
+
+class ForkingHTTPServer (BaseHTTPServer.HTTPServer):
+ def process_request(self, request, client_address):
+ self.finish_request (request, client_address)
+ self.close_request (request)
+
+
+# Main class implementing an HTTP-Requesthandler, that serves just a single
+# file and redirects all other requests to this file (this passes the actual
+# filename to the client).
+# Currently it is impossible to serve different files with different
+# instances of this class.
+
+class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler):
+ server_version = "Simons FileServer"
+ protocol_version = "HTTP/1.0"
+
+ filename = "."
+
+ def log_request (self, code='-', size='-'):
+ if code == 200:
+ BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size)
+
+
+ def do_POST (self):
+ global maxdownloads, upload
+
+ if not upload:
+ self.send_error (501, "Unsupported method (POST)")
+ return
+
+ # taken from
+ # http://mail.python.org/pipermail/python-list/2006-September/402441.html
+
+ ctype, pdict = cgi.parse_header (self.headers.getheader ('Content-Type'))
+ form = cgi.FieldStorage (fp = self.rfile,
+ headers = self.headers,
+ environ = {'REQUEST_METHOD' : 'POST'},
+ keep_blank_values = 1,
+ strict_parsing = 1)
+ if not form.has_key ("upfile"):
+ self.send_error (403, "No upload provided")
+ return
+
+ upfile = form["upfile"]
+
+ if not upfile.file or not upfile.filename:
+ self.send_error (403, "No upload provided")
+ return
+
+ upfilename = upfile.filename
+
+ if "\\" in upfilename:
+ upfilename = upfilename.split ("\\")[-1]
+
+ upfilename = os.path.basename (upfile.filename)
+
+ destfile = None
+ for suffix in ["", ".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]:
+ destfilename = os.path.join (".", upfilename + suffix)
+ try:
+ destfile = os.open (destfilename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644)
+ break
+ except OSError, e:
+ if e.errno == errno.EEXIST:
+ continue
+ raise
+
+ if not destfile:
+ upfilename += "."
+ destfile, destfilename = tempfile.mkstemp (prefix = upfilename, dir = ".")
+
+ print >>sys.stderr, "accepting uploaded file: %s -> %s" % (upfilename, destfilename)
+
+ shutil.copyfileobj (upfile.file, os.fdopen (destfile, "w"))
+
+ if upfile.done == -1:
+ self.send_error (408, "upload interrupted")
+
+ txt = """\
+ <html>
+ <head><title>Woof Upload</title></head>
+ <body>
+ <h1>Woof Upload complete</title></h1>
+ <p>Thanks a lot!</p>
+ </body>
+ </html>
+ """
+ self.send_response (200)
+ self.send_header ("Content-Type", "text/html")
+ self.send_header ("Content-Length", str (len (txt)))
+ self.end_headers ()
+ self.wfile.write (txt)
+
+ maxdownloads -= 1
+
+ return
+
+
+ def do_GET (self):
+ global maxdownloads, cpid, compressed, upload
+
+ # Form for uploading a file
+ if upload:
+ txt = """\
+ <html>
+ <head><title>Woof Upload</title></head>
+ <body>
+ <h1>Woof Upload</title></h1>
+ <form name="upload" method="POST" enctype="multipart/form-data">
+ <p><input type="file" name="upfile" /></p>
+ <p><input type="submit" value="Upload!" /></p>
+ </form>
+ </body>
+ </html>
+ """
+ self.send_response (200)
+ self.send_header ("Content-Type", "text/html")
+ self.send_header ("Content-Length", str (len (txt)))
+ self.end_headers ()
+ self.wfile.write (txt)
+ return
+
+ # Redirect any request to the filename of the file to serve.
+ # This hands over the filename to the client.
+
+ self.path = urllib.quote (urllib.unquote (self.path))
+ location = "/" + urllib.quote (os.path.basename (self.filename))
+ if os.path.isdir (self.filename):
+ if compressed == 'gz':
+ location += ".tar.gz"
+ elif compressed == 'bz2':
+ location += ".tar.bz2"
+ elif compressed == 'zip':
+ location += ".zip"
+ else:
+ location += ".tar"
+
+ if self.path != location:
+ txt = """\
+ <html>
+ <head><title>302 Found</title></head>
+ <body>302 Found <a href="%s">here</a>.</body>
+ </html>\n""" % location
+ self.send_response (302)
+ self.send_header ("Location", location)
+ self.send_header ("Content-Type", "text/html")
+ self.send_header ("Content-Length", str (len (txt)))
+ self.end_headers ()
+ self.wfile.write (txt)
+ return
+
+ maxdownloads -= 1
+
+ # let a separate process handle the actual download, so that
+ # multiple downloads can happen simultaneously.
+
+ cpid = os.fork ()
+
+ if cpid == 0:
+ # Child process
+ child = None
+ type = None
+
+ if os.path.isfile (self.filename):
+ type = "file"
+ elif os.path.isdir (self.filename):
+ type = "dir"
+
+ if not type:
+ print >> sys.stderr, "can only serve files or directories. Aborting."
+ sys.exit (1)
+
+ self.send_response (200)
+ self.send_header ("Content-Type", "application/octet-stream")
+ self.send_header ("Content-Disposition", "attachment;filename=%s" % urllib.quote (os.path.basename (self.filename)))
+ if os.path.isfile (self.filename):
+ self.send_header ("Content-Length",
+ os.path.getsize (self.filename))
+ self.end_headers ()
+
+ try:
+ if type == "file":
+ datafile = file (self.filename)
+ shutil.copyfileobj (datafile, self.wfile)
+ datafile.close ()
+ elif type == "dir":
+ if compressed == 'zip':
+ ezfile = EvilZipStreamWrapper (self.wfile)
+ zfile = zipfile.ZipFile (ezfile, 'w', zipfile.ZIP_DEFLATED)
+ stripoff = os.path.dirname (self.filename) + os.sep
+
+ for root, dirs, files in os.walk (self.filename):
+ for f in files:
+ filename = os.path.join (root, f)
+ if filename[:len (stripoff)] != stripoff:
+ raise RuntimeException, "invalid filename assumptions, please report!"
+ zfile.write (filename, filename[len (stripoff):])
+ zfile.close ()
+ else:
+ tfile = tarfile.open (mode=('w|' + compressed),
+ fileobj=self.wfile)
+ tfile.add (self.filename,
+ arcname=os.path.basename (self.filename))
+ tfile.close ()
+ except Exception, e:
+ print e
+ print >>sys.stderr, "Connection broke. Aborting"
+
+
+def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
+ global maxdownloads
+
+ maxdownloads = maxdown
+
+ # We have to somehow push the filename of the file to serve to the
+ # class handling the requests. This is an evil way to do this...
+
+ FileServHTTPRequestHandler.filename = filename
+
+ try:
+ httpd = ForkingHTTPServer ((ip_addr, port), FileServHTTPRequestHandler)
+ except socket.error:
+ print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port)
+ sys.exit (1)
+
+ if not ip_addr:
+ ip_addr = find_ip ()
+ if ip_addr:
+ if filename:
+ location = "http://%s:%s/%s" % (ip_addr, httpd.server_port,
+ urllib.quote (os.path.basename (filename)))
+ if os.path.isdir (filename):
+ if compressed == 'gz':
+ location += ".tar.gz"
+ elif compressed == 'bz2':
+ location += ".tar.bz2"
+ elif compressed == 'zip':
+ location += ".zip"
+ else:
+ location += ".tar"
+ else:
+ location = "http://%s:%s/" % (ip_addr, httpd.server_port)
+
+ print "Now serving on %s" % location
+
+ while cpid != 0 and maxdownloads > 0:
+ httpd.handle_request ()
+
+
+
+def usage (defport, defmaxdown, errmsg = None):
+ name = os.path.basename (sys.argv[0])
+ print >>sys.stderr, """
+ Usage: %s [-i <ip_addr>] [-p <port>] [-c <count>] <file>
+ %s [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir>
+ %s [-i <ip_addr>] [-p <port>] [-c <count>] -s
+ %s [-i <ip_addr>] [-p <port>] [-c <count>] -U
+
+ %s <url>
+
+ Serves a single file <count> times via http on port <port> on IP
+ address <ip_addr>.
+ When a directory is specified, an tar archive gets served. By default
+ it is gzip compressed. You can specify -z for gzip compression,
+ -j for bzip2 compression, -Z for ZIP compression or -u for no compression.
+ You can configure your default compression method in the configuration
+ file described below.
+
+ When -s is specified instead of a filename, %s distributes itself.
+
+ When -U is specified, woof provides an upload form, allowing file uploads.
+
+ defaults: count = %d, port = %d
+
+ If started with an url as an argument, woof acts as a client,
+ downloading the file and saving it in the current directory.
+
+ You can specify different defaults in two locations: /etc/woofrc
+ and ~/.woofrc can be INI-style config files containing the default
+ port and the default count. The file in the home directory takes
+ precedence. The compression methods are "off", "gz", "bz2" or "zip".
+
+ Sample file:
+
+ [main]
+ port = 8008
+ count = 2
+ ip = 127.0.0.1
+ compressed = gz
+ """ % (name, name, name, name, name, name, defmaxdown, defport)
+
+ if errmsg:
+ print >>sys.stderr, errmsg
+ print >>sys.stderr
+ sys.exit (1)
+
+
+
+def woof_client (url):
+ urlparts = urlparse.urlparse (url, "http")
+ if urlparts[0] not in [ "http", "https" ] or urlparts[1] == '':
+ return None
+
+ fname = None
+
+ f = urllib.urlopen (url)
+
+ f_meta = f.info ()
+ disp = f_meta.getheader ("Content-Disposition")
+
+ if disp:
+ disp = disp.split (";")
+
+ if disp and disp[0].lower () == 'attachment':
+ fname = [x[9:] for x in disp[1:] if x[:9].lower () == "filename="]
+ if len (fname):
+ fname = fname[0]
+ else:
+ fname = None
+
+ if fname == None:
+ url = f.geturl ()
+ urlparts = urlparse.urlparse (url)
+ fname = urlparts[2]
+
+ if not fname:
+ fname = "woof-out.bin"
+
+ if fname:
+ fname = urllib.unquote (fname)
+ fname = os.path.basename (fname)
+
+ readline.set_startup_hook (lambda: readline.insert_text (fname))
+ fname = raw_input ("Enter target filename: ")
+ readline.set_startup_hook (None)
+
+ override = False
+
+ destfile = None
+ destfilename = os.path.join (".", fname)
+ try:
+ destfile = os.open (destfilename,
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644)
+ except OSError, e:
+ if e.errno == errno.EEXIST:
+ override = raw_input ("File exists. Overwrite (y/n)? ")
+ override = override.lower () in [ "y", "yes" ]
+ else:
+ raise
+
+ if destfile == None:
+ if override == True:
+ destfile = os.open (destfilename, os.O_WRONLY | os.O_CREAT, 0644)
+ else:
+ for suffix in [".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]:
+ destfilename = os.path.join (".", fname + suffix)
+ try:
+ destfile = os.open (destfilename,
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644)
+ break
+ except OSError, e:
+ if e.errno == errno.EEXIST:
+ continue
+ raise
+
+ if not destfile:
+ destfile, destfilename = tempfile.mkstemp (prefix = fname + ".",
+ dir = ".")
+ print "alternate filename is:", destfilename
+
+ print "downloading file: %s -> %s" % (fname, destfilename)
+
+ shutil.copyfileobj (f, os.fdopen (destfile, "w"))
+
+ return 1;
+
+
+
+def main ():
+ global cpid, upload, compressed
+
+ maxdown = 1
+ port = 8080
+ ip_addr = ''
+
+ config = ConfigParser.ConfigParser ()
+ config.read (['/etc/woofrc', os.path.expanduser ('~/.woofrc')])
+
+ if config.has_option ('main', 'port'):
+ port = config.getint ('main', 'port')
+
+ if config.has_option ('main', 'count'):
+ maxdown = config.getint ('main', 'count')
+
+ if config.has_option ('main', 'ip'):
+ ip_addr = config.get ('main', 'ip')
+
+ if config.has_option ('main', 'compressed'):
+ formats = { 'gz' : 'gz',
+ 'true' : 'gz',
+ 'bz' : 'bz2',
+ 'bz2' : 'bz2',
+ 'zip' : 'zip',
+ 'off' : '',
+ 'false' : '' }
+ compressed = config.get ('main', 'compressed')
+ compressed = formats.get (compressed, 'gz')
+
+ defaultport = port
+ defaultmaxdown = maxdown
+
+ try:
+ options, filenames = getopt.getopt (sys.argv[1:], "hUszjZui:c:p:")
+ except getopt.GetoptError, desc:
+ usage (defaultport, defaultmaxdown, desc)
+
+ for option, val in options:
+ if option == '-c':
+ try:
+ maxdown = int (val)
+ if maxdown <= 0:
+ raise ValueError
+ except ValueError:
+ usage (defaultport, defaultmaxdown,
+ "invalid download count: %r. "
+ "Please specify an integer >= 0." % val)
+
+ elif option == '-i':
+ ip_addr = val
+
+ elif option == '-p':
+ try:
+ port = int (val)
+ except ValueError:
+ usage (defaultport, defaultmaxdown,
+ "invalid port number: %r. Please specify an integer" % val)
+
+ elif option == '-s':
+ filenames.append (__file__)
+
+ elif option == '-h':
+ usage (defaultport, defaultmaxdown)
+
+ elif option == '-U':
+ upload = True
+
+ elif option == '-z':
+ compressed = 'gz'
+ elif option == '-j':
+ compressed = 'bz2'
+ elif option == '-Z':
+ compressed = 'zip'
+ elif option == '-u':
+ compressed = ''
+
+ else:
+ usage (defaultport, defaultmaxdown, "Unknown option: %r" % option)
+
+ if upload:
+ if len (filenames) > 0:
+ usage (defaultport, defaultmaxdown,
+ "Conflicting usage: simultaneous up- and download not supported.")
+ filename = None
+
+ else:
+ if len (filenames) == 1:
+ if woof_client (filenames[0]) != None:
+ sys.exit (0)
+
+ filename = os.path.abspath (filenames[0])
+ else:
+ usage (defaultport, defaultmaxdown,
+ "Can only serve single files/directories.")
+
+ if not os.path.exists (filename):
+ usage (defaultport, defaultmaxdown,
+ "%s: No such file or directory" % filenames[0])
+
+ if not (os.path.isfile (filename) or os.path.isdir (filename)):
+ usage (defaultport, defaultmaxdown,
+ "%s: Neither file nor directory" % filenames[0])
+
+ serve_files (filename, maxdown, ip_addr, port)
+
+ # wait for child processes to terminate
+ if cpid != 0:
+ try:
+ while 1:
+ os.wait ()
+ except OSError:
+ pass
+
+
+
+if __name__=='__main__':
+ try:
+ main ()
+ except KeyboardInterrupt:
+ print
+
diff --git a/ximkeys b/ximkeys
new file mode 100755
index 0000000..6a0a5d8
--- /dev/null
+++ b/ximkeys
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+# List the X compose sequences available to generate the specified character.
+# I.E. the keyboard key sequence to enter after the compose (multi) key or
+# a dead key is pressed.
+#
+# This version has been heavily modified by me (David the H.). It is now
+# bash-specific, reduces the need for external tools (only grep is needed),
+# and can handle multiple inputs.
+#
+# Original script info follows. For the original version, go here:
+# http://www.pixelbeat.org/docs/xkeyboard/
+#
+# Author:
+# P@draigBrady.com
+# Notes:
+# GTK+ apps use a different but broadly similar input method
+# to X by default. Personally I tell GTK+ to use the X one by
+# adding `export GTK_IM_MODULE=xim` to /etc/profile
+# Changes:
+# V0.1, 09 Sep 2005, Initial release
+# V0.2, 04 May 2007, Added support for ubuntu
+#
+
+if [[ -z $* ]]; then
+ echo "Usage: ${0##*/} 'character(s)'" >&2
+ echo "Multiple characters are supported." >&2
+ echo "They don't need to be space-separated." >&2
+ exit 1
+fi
+
+if [[ $LANG =~ (.*)[.]UTF.*8 ]]; then
+
+ lang="${BASH_REMATCH[1]}"
+ codeset=UTF-8
+
+else
+
+ echo "Sorry, only UTF-8 is supported at present" >&2
+ exit 1
+ #could try and normalise codeset, and get char with printf %q
+ #but would not be general enough I think.
+
+fi
+
+dir=/usr/share/X11/locale #ubuntu
+
+if [[ ! -d "$dir" ]]; then
+
+ dir=/usr/X11R6/lib/X11/locale #redhat/debian
+
+fi
+
+if [[ ! -f "$dir/locale.dir" ]]; then
+
+ echo "Sorry, couldn't find your X windows locale data" >&2
+ exit 1
+
+fi
+
+page="$( grep -m1 "${lang}.${codeset}$" <$dir/locale.dir )"
+page=${page%%/*}
+
+file="$dir/$page/Compose"
+
+while read -n 1 character; do
+
+ [[ -z $character ]] && continue
+ echo "combinations found for [$character]"
+ grep -F "\"$character\"" "$file"
+ echo
+
+done <<<"$@"
+
+exit 0 \ No newline at end of file