├── .gitattributes ├── .gitignore ├── README.md ├── acePHP.php ├── doc └── screenshot │ ├── 11 clients.jpg │ ├── b6e502243e.jpg │ ├── b7a71c45e9.jpg │ ├── css3d_scr5.jpg │ ├── live_2clients.png │ ├── live_4clients_uptime.png │ ├── live_5clients_pointers.png │ ├── prebuf.png │ └── streaming.png └── res ├── .acePHProxy.settings ├── autoload.php ├── core ├── class.core.acephproxy.php ├── class.core.aceplugin.php ├── class.core.client_pool.php ├── class.core.clientrequest.php ├── class.core.clientresponse.php ├── class.core.exceptions.php ├── class.core.stream_client.php ├── class.core.stream_manager.php └── class.core.stream_unit.php ├── init.php ├── interface ├── interface.plugin.php ├── interface.stream_resource.php └── interface.ui.php ├── modules ├── class.lib.ace.php ├── class.lib.acemgr.php ├── class.lib.bdecode.php ├── class.lib.bencode.php ├── class.lib.file.php ├── class.lib.ws.php ├── class.plugin.torrent.php ├── class.plugin.ttv.php ├── class.plugin.websrv.php ├── class.ui.common.php ├── class.ui.ncurses.php ├── class.ui.text.php └── class.ui.websocket.php └── websrv ├── css3dui.html ├── img ├── kodi.png ├── nebula.jpg ├── vlc.ico ├── wmp.png └── xbmc.png └── js ├── CSS3DRenderer.js ├── TrackballControls.js ├── css3d.js ├── jquery-1.11.3.js ├── socket.js ├── three.min.js └── tween.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AcePHProxy 2 | PHP proxy daemon for Ace Stream. Allows to play torrent channel by http link on any device. 3 | Uses nice ncurses interface, support multiclient connections to one stream. 4 | Doesn't require VLC. 5 | Have some XBMC-related workarounds, so it works more stable and response faster 6 | 7 | [![Web UI](https://raw.githubusercontent.com/mexxval/AcePHProxy/master/doc/screenshot/css3d_scr5.jpg)](https://raw.githubusercontent.com/mexxval/AcePHProxy/master/doc/screenshot/css3d_scr5.jpg) 8 | 9 | -------------------------------------------------------------------------------- /acePHP.php: -------------------------------------------------------------------------------- 1 | :8000/pid// 5 | * Например ссылка трансляции канала 2x2 будет выглядеть так 6 | * http://127.0.0.1:8000/pid/0d2137fc5d44fa9283b6820973f4c0e017898a09/2x2 7 | * нужен для отображения в ncurses интерфейсе 8 | * 9 | * Для работы требует PHP и сервер AceStream и 10 | * опционально pecl-расширение ncurses 11 | * Поддерживает подключение множества клиентов к одной трансляции. 12 | * Поддерживает воспроизведение .torrent файлов с возможностью перемотки и 13 | * просмотра с заданного места 14 | * Рекомендованные опции запуска AceStream 15 | * --client-console --live-cache-size 200000000 --upload-limit 1000 --max-upload-slots 10 --live-buffer 45 16 | * 17 | * @author mexxval 18 | * @link http://blog.sci-smart.ru 19 | */ 20 | 21 | 22 | # еще одно торрент-тв, открывать через анонимайзер 23 | # http://torrentstream.tv/browse-znanie-videos-1-date.html 24 | 25 | require_once dirname(__FILE__) . '/res/init.php'; 26 | 27 | $App = AcePHProxy::getInstance(); 28 | 29 | // мониторим новых клиентов, запускаем для них трансляцию или, если такая запущена, копируем данные из нее 30 | // мониторим дисконнекты и убиваем трансляцию, если клиентов больше нет (пока можно сделать ее вечноживой) 31 | // мониторим проблемы с трансляцией и делаем попытку ее перезапустить в случае чего 32 | while (!$App->isCtrlC_Occured()) { 33 | $App->tick(); 34 | // увеличение с 20 до 100мс улучшило ситуацию с переполнением клиентских сокетов 35 | usleep(20000); 36 | } 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /doc/screenshot/11 clients.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/11 clients.jpg -------------------------------------------------------------------------------- /doc/screenshot/b6e502243e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/b6e502243e.jpg -------------------------------------------------------------------------------- /doc/screenshot/b7a71c45e9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/b7a71c45e9.jpg -------------------------------------------------------------------------------- /doc/screenshot/css3d_scr5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/css3d_scr5.jpg -------------------------------------------------------------------------------- /doc/screenshot/live_2clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/live_2clients.png -------------------------------------------------------------------------------- /doc/screenshot/live_4clients_uptime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/live_4clients_uptime.png -------------------------------------------------------------------------------- /doc/screenshot/live_5clients_pointers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/live_5clients_pointers.png -------------------------------------------------------------------------------- /doc/screenshot/prebuf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/prebuf.png -------------------------------------------------------------------------------- /doc/screenshot/streaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/doc/screenshot/streaming.png -------------------------------------------------------------------------------- /res/.acePHProxy.settings: -------------------------------------------------------------------------------- 1 | { 2 | "buffers": 3 | { 4 | "223":256000 5 | }, 6 | "listen_ip": "0.0.0.0", 7 | "listen_port": "8001", 8 | "stream_keepalive_sec": "5", 9 | "ui": ["ncurses", "websocket"], 10 | 11 | "ttv": 12 | { 13 | "acestreamkey": "n51LvQoTlJzNGaFxseRK-uvnvX-sD4Vm5Axwmc4UcoD-jruxmKsuJaH0eVgE", 14 | "ttv_login":"", 15 | "ttv_psw":"" 16 | }, 17 | "torrent": 18 | { 19 | "acestreamkey": "n51LvQoTlJzNGaFxseRK-uvnvX-sD4Vm5Axwmc4UcoD-jruxmKsuJaH0eVgE", 20 | "basedir": "/STORAGE/FILES/" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /res/autoload.php: -------------------------------------------------------------------------------- 1 | init(); 26 | } 27 | 28 | private function init() { 29 | $this->startts = time(); 30 | $this->initSettings(); 31 | $this->initCtrlC(); 32 | } 33 | private function initSettings() { 34 | $setup_file = __DIR__ . '/../.acePHProxy.settings'; 35 | $savedcfg = json_decode(file_get_contents($setup_file), true); 36 | $defaultcfg = array( 37 | 'buffers' => array(), 38 | 'listen_ip' => '0.0.0.0', 39 | 'listen_port' => '8001', 40 | 'stream_keepalive_sec' => 5, 41 | 'ui' => array('ncurses'), 42 | ); 43 | 44 | // такое выражение действует так: 45 | // в результирующий массив попадают все ключи из savedcfg в неизменном виде, 46 | // плюс те ключи из defaultcfg, которых нет в savedcfg 47 | $this->config = $savedcfg + $defaultcfg; 48 | } 49 | 50 | private function initCtrlC() { 51 | if (!function_exists('pcntl_signal')) { 52 | $this->error('pcntl function not found. Ctrl+C will not work properly'); 53 | } 54 | else { 55 | $this->success('Setting up Ctrl+C'); 56 | declare(ticks=1000); 57 | pcntl_signal(SIGINT, array($this, '_ctrlC_Handler')); 58 | } 59 | } 60 | public function _ctrlC_Handler() { 61 | $this->ctrlC = true; 62 | $this->error('Ctrl+C caught. Exiting'); 63 | } 64 | public function isCtrlC_Occured() { 65 | return $this->ctrlC; 66 | } 67 | 68 | public function __call($m, $a) { 69 | if (in_array($m, array('log', 'error', 'success'))) { 70 | if ($m == 'error') { 71 | error_log($a[0]); 72 | } 73 | foreach ($this->getUI() as $one) { 74 | call_user_func_array(array($one, $m), $a); 75 | } 76 | } 77 | } 78 | public function getPlugin($type, $fullClassName = false) { 79 | // название класса плагина, регистр не важен 80 | $plugin = $fullClassName ? $type : ('AcePlugin_' . $type); 81 | if (!class_exists($plugin)) { 82 | throw new CoreException('Plugin type not found', 0, $type); 83 | } 84 | $config = isset($this->config[$type]) ? $this->config[$type] : array(); 85 | if (is_callable($cb = array($plugin, 'getInstance'))) { 86 | return call_user_func_array($cb, array($this, $config)); 87 | } 88 | return new $plugin($this, $config); 89 | } 90 | 91 | public function tick() { 92 | $check_inet = (time() - $this->last_check) > 10; // every N sec 93 | if ($check_inet) { 94 | $this->checkInternet(); 95 | } 96 | 97 | $pool = $this->getClientPool(); 98 | $streams = $this->getStreamManager(); 99 | 100 | try { 101 | // получаем статистику по новым клиентам, отвалившимся клиентам и запросам контента 102 | if ($new = $pool->track4new()) { 103 | foreach ($new['start'] as $peer => $req) { 104 | $streams->start2($req); 105 | } 106 | 107 | foreach ($new['new'] as $peer => $_) { 108 | } 109 | foreach ($new['done'] as $peer => $_) { 110 | } 111 | 112 | // быстренько валим на новый цикл (main) 113 | if ($new['recheck']) { 114 | return; 115 | } 116 | } 117 | 118 | 119 | // раскидываем контент по клиентам 120 | $streams->closeWaitingStreams(); 121 | $streams->copyContents(); 122 | 123 | // дергаем метод перерисовки всех UI 124 | foreach ($this->getUI() as $one) { 125 | $one->draw(); 126 | } 127 | } 128 | catch (Exception $e) { 129 | $this->error($e->getMessage()); 130 | } 131 | } 132 | 133 | // метод выдачи некоторой вспомогательной и статистической инфы для вывода в UI 134 | public function getUIAdditionalInfo() { 135 | // выведем аптайм и потребляемую память 136 | $allsec = time() - $this->startts; 137 | $secs = sprintf('%02d', $allsec % 60); 138 | $mins = sprintf('%02d', floor($allsec / 60 % 60)); 139 | $hours = sprintf('%02d', floor($allsec / 3600)); 140 | $mem = memory_get_usage(); // bytes 141 | $mem = round($mem / (1024 * 1024), 1); // MBytes 142 | 143 | $pool = $this->getClientPool(); 144 | $addinfo = array( 145 | 'ram' => $mem, 146 | 'uptime' => "$hours:$mins:$secs", 147 | 'title' => ' AcePHProxy v.' . ACEPHPROXY_VERSION . ' ', 148 | 'port' => $pool->getPort(), 149 | 'wwwok' => $this->wwwstate 150 | ); 151 | return $addinfo; 152 | } 153 | 154 | 155 | // в соответствии с конфигом инстанцируем нужные модули UI 156 | private function getUI() { 157 | if (is_null(self::$ui)) { 158 | self::$ui = array(); 159 | 160 | // ищем все загруженные модули интерфейсов и инстанцируем 161 | $list = get_declared_classes(); 162 | $cfgui = $this->config['ui']; // интерфейсы из конфига, произвольный регистр 163 | $regexp = '~(' . implode('|', $cfgui) . ')~i'; 164 | 165 | foreach ($list as $className) { 166 | if (preg_match($regexp, $className)) { 167 | try { 168 | // try-catch, ибо ncurses например может взбрыкнуть, 169 | // если расширение в пхп не загружено 170 | // все UI - синглтоны 171 | if (is_a($className, 'AppUserInterface', true)) { 172 | $tmp = $this->getPlugin($className, true); 173 | } else { 174 | throw new CoreException($className . ' is not a AppUserInterface class', 0); 175 | } 176 | } catch (Exception $e) { 177 | $this->error($e->getMessage()); 178 | continue; 179 | } 180 | self::$ui[] = $tmp; 181 | $tmp->init2($this); 182 | } 183 | } 184 | } 185 | return self::$ui; 186 | } 187 | 188 | 189 | 190 | public function __destruct() { 191 | // тормозим все трансляции, закрываем сокеты Ace 192 | $this->getStreamManager()->closeAll(); 193 | } 194 | 195 | // а еще чтобы не долбить коннектами, можно его куда нить открыть и держать. 196 | // опыт xbmc клиента правда говорит, что это мб весьма ненадежно.. зато реалтайм 197 | private function checkInternet() { 198 | $tmp = $this->wwwstate; // для определения смены состояния 199 | 200 | // делаем 2-3 попытки коннекта для проверки инета 201 | $cyc = 3; 202 | while (!($fp = @stream_socket_client('tcp://8.8.8.8:53', $e, $e, 0.15, STREAM_CLIENT_CONNECT)) and $cyc-- > 0); 203 | $res = (bool) $fp; 204 | $fp and fclose($fp); 205 | 206 | $this->last_check = time(); 207 | $wwwChanged = $tmp != $this->wwwstate; 208 | if ($wwwChanged) { 209 | $this->getClientPool()->notify($this->wwwstate ? 'Интернет восстановлен' : 'Интернет упал'); 210 | } 211 | return $res; 212 | } 213 | 214 | private function getClientPool() { 215 | // создает сокет сервера трансляций и управляет коннектами клиентов к демону 216 | if (!self::$pool) { 217 | self::$pool = new ClientPool($this->config['listen_ip'], $this->config['listen_port']); 218 | } 219 | return self::$pool; 220 | } 221 | 222 | // пришлось сделать public для AcePlugin_common::getStreams() 223 | // для AppUserInterface::init() тоже пригодилось 224 | public function getStreamManager() { 225 | // управляет трансляциями. заказывает их у Ace и раздает клиентам из pool 226 | if (!self::$mgr) { 227 | self::$mgr = new StreamsManager($this); 228 | self::$mgr->setKeepaliveTime($this->config['stream_keepalive_sec']); 229 | } 230 | return self::$mgr; 231 | } 232 | } 233 | 234 | 235 | -------------------------------------------------------------------------------- /res/core/class.core.aceplugin.php: -------------------------------------------------------------------------------- 1 | app = $app; 12 | $this->config = $config; 13 | $this->init(); 14 | } 15 | 16 | abstract protected function init(); 17 | 18 | public function __get($prop) { 19 | return isset($this->config[$prop]) ? $this->config[$prop] : null; 20 | } 21 | 22 | protected function getStreams() { 23 | return $this->getApp()->getStreamManager()->getStreams(); 24 | } 25 | protected function getApp() { 26 | return $this->app; 27 | } 28 | 29 | // должен вернуть объект ClientResponse, содержащий данные для отправки на клиента 30 | // в виде plaintext (отправка разом) или в виде объекта-потока (регистрация в Stream Manager) 31 | // содержимое ресурса будет выдаваться клиенту до тех пор, пока не кончится (eof) 32 | abstract public function process(ClientRequest $req); 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /res/core/class.core.client_pool.php: -------------------------------------------------------------------------------- 1 | port = $port; 12 | $this->socket = stream_socket_server(sprintf("tcp://%s:%d", $ip, $this->port), $errno, $errstr); 13 | if (!$this->socket) { 14 | throw new Exception("Failed to create socket server. $errstr", $errno); 15 | } 16 | } 17 | 18 | public function getPort() { 19 | return $this->port; 20 | } 21 | 22 | public function getClients() { 23 | return $this->clients; 24 | } 25 | 26 | public function notify() { 27 | $args = func_get_args(); 28 | foreach ($this->getClients() as $one) { 29 | call_user_func_array(array($one, 'notify'), $args); 30 | } 31 | } 32 | 33 | public function track4new() { 34 | $read = array($this->socket); // опрашиваем только мастер-сокет (сервер) 35 | $_ = array(); 36 | $mod_fd = stream_select($read, $_, $_, 0, 20000); 37 | if ($mod_fd === FALSE) { 38 | return false; 39 | } 40 | 41 | // сюда собираем статистику для дальнейшего реагирования 42 | $newclients = array(); // newly connected 43 | $doneclients = array(); // disconnected 44 | $startreq = array(); // client->pid for start 45 | 46 | // флаг необходимости срочно опросить сокеты еще раз 47 | $recheck = false; 48 | 49 | // теперь надо по всем клиентам пройти и спросить. нет ли у них на сокетах событий 50 | foreach ($this->clients as $peer => $one) { 51 | try { 52 | if ($one->isFinished()) { 53 | throw new Exception('Close finished client'); 54 | } 55 | 56 | // тут такая логика: xbmc при открытии m3u по каждому элементу коннектится и 57 | // спрашивает инфу, отдельно. а в главном цикле демона задержка около 30мс 58 | // из-за нее плейлист из 10 строк открывается секунду. 59 | // потому делаем так: если поймали исключение HEAD или empty-range - опрашиваем сокет еще 60 | // но блин проблема в том, что на каждый эл-т плейлиста - отдельный коннект, так что 61 | // компактненько тут не обойдешься... TODO 62 | $result = $one->track4new(); 63 | if ($result) { 64 | $startreq[$peer] = $result; 65 | } 66 | } 67 | catch (Exception $e) { 68 | // если в возвращаемых методом массивах будут объекты (главный цикл, new), 69 | // то __destruct не срабатывает, хотя new там перезаписывается каждый цикл, непонятно 70 | // флаг finished выставляется в методе close(), так что вызывать его еще раз ни к чему 71 | // upd: нет, надо! клиент может не только по флагу finished сам себя закрыть. 72 | // но еще и отменить запрос (прервать закачку файла) и отвалиться 73 | $one->close(); 74 | unset($this->clients[$peer]); 75 | $doneclients[$peer] = null; 76 | // ассоциированные трансляции должны удалиться через __destruct клиента 77 | // тут можно разве что в лог написать 78 | // error_log('disconnected ' . $peer); 79 | 80 | // чо это такое? и нафиг надо 81 | // кажется чтобы в логе connected и disconnected были по порядку 82 | if (in_array($e->getCode(), array(3, 4))) { 83 | $recheck = true; 84 | } 85 | } 86 | } 87 | 88 | if ($read) { // есть событие на сокете сервера - Новый клиент 89 | $conn = stream_socket_accept($this->socket, 1, $peer); // peer заполняется по ссылке 90 | // также при желании пишем в лог о новом коннекте 91 | // и желательно сразу, а то пока до главного цикла дойдет, окажется что клиент уже и данные прислал 92 | // а мы после этого только пишем, что он приконнектился 93 | // еще одна мелкая эстетическая проблемка. когда клиент рвет соединение и тут же создает новое 94 | // в логе пишется сначала connected для нового. затем disconnected для старого коннекта 95 | // error_log('connected ' . $peer); 96 | $this->clients[$peer] = new StreamClient($peer, $conn); 97 | $newclients[$peer] = null; 98 | } 99 | 100 | return array( 101 | 'recheck' => $recheck, 102 | 'new' => $newclients, 103 | 'done' => $doneclients, 104 | 'start' => $startreq 105 | ); 106 | } 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /res/core/class.core.clientrequest.php: -------------------------------------------------------------------------------- 1 | req = $data; 14 | $this->client = $client; 15 | $this->start = $this->parse($this->req); 16 | // error_log('construct request ' . spl_object_hash ($this)); 17 | } 18 | // переопределенное имя потока или индекс видеофайла (нехорошо такое смешивать) 19 | public function getName() { 20 | return $this->start['uriName']; 21 | } 22 | public function getPluginCode() { 23 | return $this->start['plugin']; 24 | } 25 | // или или имя торрент-файла 26 | public function getPid() { 27 | return $this->start['uriAddr']; 28 | } 29 | // возвращает тип запрошенного контента, acelive, trid, pid, torrent, file 30 | public function getType() { 31 | return $this->start['uriType']; 32 | } 33 | // raw post data 34 | public function getContent() { 35 | return $this->start['reqContent']; 36 | } 37 | public function getClient() { 38 | return $this->client; 39 | } 40 | // в каком виде? явно строка, но включен ли двойной перенос в конце? 41 | public function getHeaders() { 42 | return $this->req; 43 | } 44 | // все после GET и до HTTP/1.x, request uri в общем 45 | public function getUri() { 46 | return $this->start['reqUri']; 47 | } 48 | public function getUserAgent() { 49 | return $this->start['UA']; 50 | } 51 | // GET POST HEAD OPTIONS SUBSCRIBE etc 52 | public function getReqType() { 53 | return $this->start['reqType']; 54 | } 55 | // есть ли заголовок Range: bytes=x-x 56 | public function isRanged() { 57 | return !is_null($this->start['range']); 58 | } 59 | public function isEmptyRanged() { 60 | return $this->start['range'] === array('from' => 0, 'to' => 0); 61 | } 62 | public function getReqRange() { 63 | return $this->start['range']; 64 | } 65 | public function getHttpHost($withPort = true) { 66 | $host = $this->start['reqHost']; 67 | if (!$withPort) { 68 | $host = explode(':', $host); 69 | $host = reset($host); 70 | } 71 | return $host; 72 | } 73 | // на какой интерфейс пришло обращение 74 | // (заголовки можно и подделать, но как определить на сервере не нашел) 75 | // кстати там может быть как dns-имя, так и ip 76 | public function getServerHost() { 77 | return $this->getHttpHost(false); 78 | } 79 | 80 | public function addData() { 81 | error_log('TODO ' . __METHOD__); 82 | } 83 | 84 | // немного упрощает написание выдачи ответов в плагинах 85 | public function response($contents = null, $streamid = null) { 86 | return new ClientResponse($this, $contents, $streamid); 87 | } 88 | 89 | 90 | // TODO тут не дб логики обработки и кидания исключений 91 | // только разбор заголовков и выдача их в удобном виде 92 | // а кто чего попросил запустить это в acePHP.php стоит решать 93 | protected function parse($sock_data) { 94 | $firstLine = trim(substr($sock_data, 0, strpos($sock_data, "\n"))); 95 | preg_match('~HTTP/1\..*\r?\n\r?\n(.*)~sm', $sock_data, $content); 96 | $result = array( 97 | 'reqType' => substr($firstLine, 0, $space = strpos($firstLine, ' ')), // от начала до первого пробела (GET/HEAD/etc) 98 | 'reqUri' => substr($firstLine, $space + 1, ($rspace = strrpos($firstLine, ' ')) - $space - 1), // /ttv/trid/123/title 99 | 'reqProto' => substr($firstLine, $rspace + 1), // HTTP/1.x 100 | 'range' => preg_match('~Range: bytes=(\d+)-(\d+)?~sm', $sock_data, $m) ? 101 | array('from' => $m[1], 'to' => @$m[2]) : null, 102 | 'reqHost' => preg_match('~host: ([^\s]*)~smi', $sock_data, $m) ? $m[1] : null, 103 | 'UA' => preg_match('~user-agent: ([^\n]+)~smi', $sock_data, $m) ? $m[1] : null, 104 | 'reqContent' => empty($content[1]) ? null : $content[1] // тело запроса (для POST) 105 | ); 106 | 107 | // немного дополним инфо о запросе, разобрав reqUri 108 | // обычно запрос состоит из 3 частей: тип, адрес и название. /pid/blablabla/name 109 | // UPD: теперь запрос состоит из указателя на плагин, типа контента, id и названия 110 | // /ttv/trid/390/2x2 111 | // /torrent/seriesname_s01.torrent/2/Episode name 112 | $uriInfo = array(); 113 | $uri = $result['reqUri']; 114 | $tmp = explode('/', $uri); 115 | // @ расставлены, чтобы кривые урлы в лог ошибки не генерили 116 | $uriInfo['plugin'] = @$tmp[1]; // getPluginCode() между первым и вторым слешами 117 | $uriInfo['uriType'] = urldecode(@$tmp[2]); // getType() между вторым и третьим слешами 118 | $uriInfo['uriAddr'] = urldecode(@$tmp[3]); // getPid() decode, торрент файл может содержать спецсимволы ([ ] пробелы и т.д.) 119 | // getName() название торрента - необязательный параметр. скоро через LOADASYNC получать будем 120 | $uriInfo['uriName'] = isset($tmp[4]) ? urldecode($tmp[4]) : ''; 121 | 122 | // types: 123 | // pid - start by PID 124 | // acelive, trid - start by translation ID (http://torrent-tv.ru/torrent-online.php?translation=?) 125 | // torrent - start torrent file 126 | 127 | // такая штука. если клиент медленный, он может выдать GET запрос за первый проход, 128 | // а остальные хедеры за последующие. в связи с чем по первому принятому запросу 129 | // запускаем поток, а по следующему кидаем этот эксепшен и клиента кикает 130 | // upd: жду от клиента полных заголовков, и только потом обрабатываю. вернул исключенеи 131 | #throw new Exception('Unknown request', 15); 132 | return $result + $uriInfo; 133 | } 134 | public function __destruct() { 135 | // error_log(' destruct request ' . spl_object_hash ($this)); 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /res/core/class.core.clientresponse.php: -------------------------------------------------------------------------------- 1 | req = $req; 13 | $this->contents = $contents; 14 | $this->streamid = $streamid; 15 | # error_log(' construct response ' . spl_object_hash ($this)); 16 | } 17 | 18 | public function getPluginCode() { 19 | return $this->req->getPluginCode(); 20 | } 21 | public function getName() { 22 | // return $this->req->getName(); // это не катит никак 23 | return $this->getStreamId(); 24 | } 25 | public function isStream() { 26 | return (bool) $this->getStreamId(); 27 | } 28 | public function getStreamId() { 29 | return $this->streamid; 30 | } 31 | 32 | // метод выдает ресурс, полученный на вход, 33 | // но только если это инстанс AppStreamResource 34 | public function getStreamResource() { 35 | return is_a($this->contents, 'AppStreamResource') ? $this->contents : null; 36 | } 37 | // то же, но выдает только plaintext 38 | public function getContents() { 39 | return is_scalar($this->contents) ? $this->contents : null; 40 | } 41 | 42 | public function __destruct() { 43 | # error_log(' destruct response ' . spl_object_hash ($this)); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /res/core/class.core.exceptions.php: -------------------------------------------------------------------------------- 1 | addinfo = $addinfo; 14 | } 15 | public function getAdditionalInfo() { 16 | return $this->addinfo; 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /res/core/class.core.stream_client.php: -------------------------------------------------------------------------------- 1 | peer = $peer; 25 | $this->socket = $socket; 26 | stream_set_blocking($this->socket, 0); 27 | stream_set_timeout($this->socket, 0, 20000); 28 | $this->tsconnected = time(); 29 | // error_log('construct client ' . spl_object_hash ($this) . "\t" . $peer); 30 | } 31 | 32 | public function registerEventListener($cb) { 33 | $this->listener = $cb; 34 | } 35 | protected function notifyListener($event) { 36 | is_callable($this->listener) and call_user_func_array($this->listener, array($this, $event)); 37 | } 38 | public function getIp() { 39 | return implode('', array_slice(explode(':', $this->peer), 0, 1)); 40 | } 41 | 42 | public function getName() { 43 | return $this->peer; 44 | } 45 | public function getPointerPosition() { 46 | return $this->pointerPos; 47 | } 48 | public function getUptimeSeconds() { 49 | return time() - $this->tsconnected; 50 | } 51 | public function getUptime() { 52 | $allsec = $this->getUptimeSeconds(); 53 | // собираем строку времени 54 | $secs = sprintf('%02ds', $allsec % 60); 55 | $hours = $mins = ''; 56 | if ($tmp = floor($allsec / 3600)) { 57 | $hours = sprintf('%dh ', $tmp); 58 | } 59 | if ($tmp = floor(($allsec - intval($hours) * 3600) / 60)) { 60 | $mins = sprintf('%02dm ', $tmp); 61 | } 62 | return $hours . $mins . $secs; 63 | } 64 | public function getTraffic() { 65 | $bytes = $this->bytesgot; 66 | if ($bytes < 1024) { 67 | $units = 'B'; 68 | } else if (($bytes /= 1024) < 1024) { 69 | $units = 'kB'; 70 | } else if (($bytes /= 1024) < 1024) { 71 | $units = 'MB'; 72 | } else if (($bytes /= 1024) < 1024) { 73 | $units = 'GB'; 74 | } else { 75 | $units = 'TB'; 76 | } 77 | return sprintf('%d %s', $bytes, $units); 78 | } 79 | // возвращает известные типы клиентов. например название плеера, браузера 80 | public function getType() { 81 | $req = $this->getLastRequest(); 82 | // коннект уже может быть, но оформленного реквеста может не быть 83 | if (!$req) { 84 | return false; 85 | } 86 | 87 | $ua = $req->getUserAgent(); 88 | $map = array( 89 | 'chrome' => 'chrome', 90 | 'vlc' => 'vlc', 91 | 'kodi' => 'kodi', 92 | 'xbmc' => 'xbmc', 93 | 'wmp' => array('wmfsdk', 'Windows-Media-Player'), 94 | ); 95 | $pattern = array(); 96 | foreach ($map as $name => $subptrn) { 97 | is_array($subptrn) or $subptrn = array($subptrn); 98 | $pattern[] = sprintf('(?<%s>(%s))', $name, implode('|', $subptrn)); 99 | } 100 | $pattern = '~(' . implode('|', $pattern) . ')~sUi'; 101 | 102 | if (!preg_match($pattern, $ua, $m)) { 103 | return false; 104 | } 105 | $res = array_filter(array_intersect_key($m, $map), 'strlen'); 106 | if (!$res) { 107 | return false; 108 | } 109 | reset($res); 110 | return key($res); 111 | } 112 | 113 | public function isFinished() { 114 | return $this->finished; 115 | } 116 | public function isActiveStream() { 117 | return $this->stream and $this->stream->isActive(); 118 | } 119 | public function setEcoMode($bool) { 120 | //error_log('Setting eco mode to ' . ($bool ? 'TRUE' : 'false')); 121 | $this->ecoModeEnabled = (bool) $bool; 122 | } 123 | // включена ли функция экорежима вообще 124 | public function isEcoMode() { 125 | return $this->ecoModeEnabled; 126 | } 127 | // задействована ли функция экорежима в данный момент (буфер опустел?) 128 | public function isEcoModeRunning() { 129 | return $this->ecoModeRunning; 130 | } 131 | 132 | // вызывается при регистрации клиента в потоке, они связываются перекрестными ссылками друг на друга 133 | public function associateStream(StreamUnit $stream) { 134 | $this->stream = $stream; 135 | // новая связка - возможно новая регистрация того же клиента 136 | // значит будет новый вызов accept() 137 | $this->isAccepted = false; 138 | } 139 | 140 | 141 | public function accept($headers, $pointer = 0, $pointerPos = 0) { 142 | if ($this->isAccepted()) { // клиент уже получил заголовки, пропускаем 143 | error_log(sprintf('already accepted %s!! why again???', $this->getName())); 144 | return; 145 | } 146 | # error_log(sprintf('ACCEPT %s with %s ptr %d', $this->getName(), json_encode($headers), $pointer)); 147 | $this->pointer = 0; 148 | $this->pointerPos = 0; 149 | $this->isAccepted = true; 150 | // отправляем заголовки, тут pointer дб равен 0 151 | $res = $this->put($headers); 152 | // затем выставляем указатель в требуемое положение 153 | $this->pointer = (int) $pointer; 154 | $this->pointerPos = (int) $pointerPos; 155 | return $res; 156 | } 157 | 158 | public function isAccepted() { 159 | return $this->isAccepted; 160 | } 161 | 162 | // затея: сюда передается большой буфер из StreamUnit. мы храним указатель и самостоятельно берем нужную часть 163 | // bufSize - сколько клиент должен себе забрать 164 | public function put(&$data, $bufSize = null) { 165 | if (!$this->socket) { 166 | throw new Exception('inactive client socket', 10); 167 | } 168 | 169 | // пробуем использовать stream_select() 170 | // а проблема в том, что XBMC набрал себе буфера секунд 5-8, и больше не лезет, 171 | // а Ace транслирует и читать это приходится, разве что излишки в памяти хранить 172 | // upd: это только для трансляций. для фильмов надо переставать читать по какому то условию 173 | $write = array($this->socket); 174 | $_ = null; 175 | $mod_fd = stream_select($_, $write, $_, 0, 20000); 176 | if ($mod_fd === FALSE) { 177 | return false; 178 | } 179 | // когда клиент тупо вырубается (по питанию, инет упал и т.д.) - он застревает тут 180 | // вообще то клиент и при нормальной работе достаточно часто тут оказывается 181 | // для VLC http://joxi.ru/48Ang31hopYNrO 182 | // для XBMC бывает и так http://joxi.ru/dp27Dgpcn4M5A7 от 16 до 92 была сплошь неготовность 183 | // вопрос по детекту отвалившегося сокета. правда без ответа 184 | // https://stackoverflow.com/questions/16715313/how-can-i-detect-when-a-stream-client-is-no-longer-available-in-php-eg-network 185 | if (!$write) { 186 | return null; 187 | } 188 | $sock = reset($write); 189 | 190 | // fwrite отличается тем, что не врет, что записал весь буфер в неактивный сокет 191 | // но с ней другая проблема, картинка периодически разваливается, затем снова восстанавливается 192 | // можно юзать .._sendto, а ошибки мониторить через error_get_last, 193 | // к тому же реальное число записанных байт не пригодилось 194 | #$res = @fwrite($this->socket, $data); // @ чтоб ошибки в лог не сыпались 195 | #$res = @stream_socket_sendto($this->socket, $data); 196 | 197 | // если запись не удалась, надо бы как то попытаться еще раз.. может в буфер себе сохранить 198 | // вот еще по ошибке 11 199 | // http://stackoverflow.com/questions/14370489/what-can-cause-a-resource-temporarily-unavailable-on-sock-send-command 200 | 201 | // Типафича. если буфер Null, пишем всю data, полезно при записи заголовков на клиент 202 | $writeWhole = is_null($bufSize); 203 | // $data это большой общий разделяемый буфер. передается сюда по ссылке 204 | $dataLen = strlen($data); 205 | // решение "выдавать по одному" было проблемой для отправки хедеров из accept() 206 | 207 | // XBMC при попытке остановить поток во время Ace-буферизации вис до окончания буферизации 208 | // причина была в том, что весь буфер был уже записан, и флаг ecoMode по сути не работал 209 | // put был пуст и мы выходили из метода 210 | // поэтому ecoMode определяем сами как последние 10кБ буфера, думаю этого достаточно, 211 | // чтобы по 5 байт выдавать до таймаута самого XBMC 212 | // работает! 213 | // ecoMode - выдача данных по 1 байту, т.к. XBMC при отсутствии данных вешается нахер, 214 | // не реагирует ни на какие раздражители, пока не отвалится по таймауту 215 | // upd: чтобы это работало, нужно подкорректировать bufSize, чтобы тот не вылез за пределы dataLen 216 | // например: в этот проход разница данные-указатель > 50000, а bufSize = 256000, 217 | // и в след.проход на клиента пишутся остатки буфера и ничего для ecoMode не остается 218 | $ecoModeTailLength = 1000; 219 | 220 | $correctPointer = true; 221 | if ($writeWhole) { 222 | $bufSize = $dataLen; 223 | $correctPointer = false; 224 | // error_log('put on client ' . $data); 225 | } else { 226 | // иначе проверим, сколько осталось до конца буфера 227 | // надо оставить хвост для выдачи по 1 байту 228 | // например для ТВ потока при буфере 512к и остатке данных 510к 229 | // ecoMode включится очень рано, задолго до заданных 1кБ 230 | // нужно подрезать буфер 231 | // отличный эффект, клиенты держатся чуть не на последнем байте, 232 | // стабильность улучшена (xbmc не буферизует) 233 | if ( 234 | ($this->pointer + $bufSize) > $dataLen and 235 | ($this->pointer + $ecoModeTailLength) < $dataLen // защита от ухода в минус 236 | ) { 237 | $bufSize = $dataLen - $this->pointer - $ecoModeTailLength; 238 | // error_log('buffer trimmed to ' . $bufSize); 239 | } 240 | } 241 | 242 | // ecoMode представляет проблему для режима просмотра фильма, 243 | // но очень и очень полезен для режима ТВ: 244 | // дает стабильность и XBMC быстрее отрабатывает остановку потока 245 | $this->ecoModeRunning = ($this->isEcoMode() and !$writeWhole and 246 | ($this->pointer + $bufSize) > $dataLen); 247 | if ($this->ecoModeRunning) { 248 | $bufSize = 1; 249 | } 250 | 251 | 252 | // сразу обновим указатель в % 253 | $this->pointerPos = $dataLen ? round($this->pointer / $dataLen * 100) : 0; // и его позиции в % 254 | 255 | $put = substr($data, $this->pointer, $bufSize); 256 | if (!$put) { 257 | # error_log('No data for client got from buffer'); 258 | return; 259 | } 260 | // а вот так работает. хотя функция "Returns a result code, as an integer" 261 | // проверка же показала, что выдается число байт 262 | $b = stream_socket_sendto($this->socket, $put); 263 | if ($writeWhole and $b < strlen($put)) { 264 | error_log('Some data failed to write on client!'); 265 | } 266 | // это явно ошибка и корректировать буфер на -1 совсем ни к чему 267 | if ($b == -1) { 268 | return $b; 269 | } 270 | # error_log($b . ' bytes was written on client'); 271 | // а $b может быть false или другим не-числом? 272 | if ($correctPointer) { 273 | $this->pointer += $b; // корректировка указателя 274 | $this->pointerPos = $dataLen ? round($this->pointer / $dataLen * 100) : 0; // и его позиции в % 275 | } 276 | // обновим статистику записанных на клиента байт 277 | $this->bytesgot += $b; 278 | 279 | // если сокет полон и дальше не лезет - выдаем сколько байт НЕ записалось 280 | return $bufSize - $b; 281 | } 282 | 283 | // когда буфер триммируется, все клиенты получают команду на смещение указателей 284 | public function correctBufferPointer($bytes, &$buffer) { 285 | if ($bytes <= 0) { 286 | return; 287 | } 288 | 289 | $this->pointer -= $bytes; 290 | // видимо буфер уже очищается, а мы все еще не прочитали его. может мы мертвы? 291 | if ($this->pointer < 0) { 292 | $this->pointer = 0; 293 | // проблема подключения нового клиента, его сразу кикает в половине случаев 294 | // будем кикать только если клиент хотя бы пару секунд был на связи 295 | if ($this->getUptimeSeconds() > 20) { 296 | error_log('Close on negative pointer'); 297 | $this->close(); 298 | } 299 | return; 300 | } 301 | $dataLen = strlen($buffer); 302 | $this->pointerPos = $dataLen ? round($this->pointer / $dataLen * 100) : 0; // и его позиции в % 303 | } 304 | 305 | public function getLastRequest() { 306 | return $this->last_request; 307 | } 308 | 309 | public function track4new() { 310 | $read = array($this->socket); 311 | $_ = null; 312 | $mod_fd = stream_select($read, $_, $_, 0, 20000); 313 | if ($mod_fd === FALSE) { 314 | return false; 315 | } 316 | if (!$read) { 317 | return null; 318 | } 319 | $sock = reset($read); 320 | 321 | $sock_data = stream_socket_recvfrom($sock, 1024); 322 | if (strlen($sock_data) === 0) { // connection closed, works 323 | throw new Exception('Disconnect', 1); 324 | } else if ($sock_data === FALSE) { 325 | throw new Exception('Something bad happened', 2); 326 | } 327 | 328 | // TODO тут надо читать HTTP запрос целиком, до тех пор, пока не встретится пустая строка 329 | // таймаут? хз, может клиент сам отвалится если что.. ну или вводить тут конечный автомат 330 | // если клиент начал что-то похожее на GET / передавать, 331 | // значит читаем заголовки и ждем пустой строки 332 | 333 | // хоть логика разбора клиентского запроса и лежит в ClientRequest, но все же 334 | // мы явно работаем по HTTP, что означает несколько вещей: 335 | // - клиент отправляет только 1 запрос в самом начале за все время коннекта 336 | // - прежде чем обрабатывать запрос, нужно дождаться пустой строки, как того требует HTTP 337 | // Следовательно, часть логики поместим тут, а именно, собираем запрос от клиента 338 | // до пустой строки, и только потом из полученного делаем объект запроса 339 | 340 | $this->raw_read = $sock_data; 341 | 342 | // TODO требуется такая логика: первые данные при коннекте нового клиента должны обрабатываться через 343 | // clientPool как и сейчас. а дальнейшие данные уже в определенном плагине 344 | // т.е. прочитали первую строку GET /websrv/... HTTP/1.0, выдали как объект ClientRequest 345 | // дальше в случае гет-запроса данных не будет. 346 | // А вот в случае POST, как раз вместо того, что реализовано ниже (проверка длины пост-данных), 347 | // можно было бы это делать уже в недрах соответствующего модуля 348 | // В случае вебсокетов тоже - handshake читается тут, дальше работает модуль: все остальные 349 | // данные с клиента и на него обрабатываются в плагине 350 | // А тут логика упрощается донельзя. И нечего тут развесистые условия разводить. 351 | 352 | // для DLNA нужно уметь обрабатывать POST-запросы. если запрос POST, ждем появления двойного переноса строк и 353 | // контента длиной Content-Length байт. значение смотрим в заголовке 354 | if (!preg_match('~HTTP/1\..*\r?\n\r?\n(.*)~sm', $this->raw_read, $content)) { 355 | // одно из двух. либо клиент прислал реально какую-то чушь. 356 | // либо это доп.данные, через вебсокеты например 357 | // error_log('wrong request from client: ' . $this->raw_read); 358 | if ($this->last_request) { // запрос был создан, значит доп.данные 359 | // добавляем данные в запрос, но сам его не возвращаем из метода, 360 | // иначе он будет повторно обработан, а это совсем ни к чему. 361 | // возврат объекта ClientRequest отсюда равносилен старту какого-то потока! 362 | // TODO решить с этим -> $this->last_request->addData($sock_data); 363 | $this->notifyListener(array('moredata' => $sock_data)); 364 | } 365 | return false; 366 | } 367 | 368 | // РЕАЛИЗАЦИЯ: если мы тут, значит начало данных верное (HTTP протокол, GET-POST) 369 | // создаем объект запроса сразу 370 | $this->last_request = new ClientRequest($this->raw_read, $this); 371 | 372 | // TODO это уже надо переносить в модуль, т.к. raw_read уже не наращивается через .= 373 | $isPost = substr($this->raw_read, 0, 4) == 'POST'; 374 | // проконтролируем длину полученного контента 375 | if ($isPost) { 376 | $contentLength = null; 377 | preg_match('~Content-Length: (\d+)~i', $this->raw_read, $m); 378 | $contentLength = $m[1]; 379 | $content = $content[1]; 380 | $allDataGot = (!$contentLength or strlen($content) == $contentLength); 381 | if (!$allDataGot) { 382 | error_log('PARTIAL POST DATA ' . strlen($content) . ' of ' . $contentLength); 383 | return false; 384 | } 385 | } 386 | 387 | // далее предстоит реализация перемотки кино 388 | // оно хоть и касается только кино, но по сути это есть правильная работа с 389 | // HTTP/1.1 206 Partial Content и Range: bytes=.. 390 | // Плеер (на примере VLC) отправляет отправляет исходный запрос с Range: bytes=0- 391 | // мы отвечаем ему HTTP/1.1 206 Partial Content, Content-Range: bytes 0-2200333187/2200333188 392 | // при перемотке плеер закрывает текущий коннект и открывает новый, 393 | // с новым значением Range: bytes= 394 | // думаю при этом логика работы с потоком дб такая: закрываем поток от AceServer, 395 | // открываем заново, передаем туда заголовки от клиента, читаем заголовки из потока, 396 | // выдаем их клиенту. по идее на этом все 397 | // показывать кино параллельно на несколько устройств было бы можно, 398 | // если бы это поддерживал AceServer. Подключиться к потоку можно только в 1 поток :) 399 | // соответственно, размножить видео в принципе можно, но перематывать сможет только кто-то один 400 | // пока буду исходить из того, что одно и то же кино смотрит один клиент! 401 | 402 | return $this->last_request; 403 | } 404 | 405 | // новая фича, пробуем уведомить XBMC-клиента об ошибке (popup уведомление) 406 | // работает! :) 407 | // notify all уведомляет всех клиентов, хз чем фича мб полезна 408 | // {"id":2,"jsonrpc":"2.0","method":"JSONRPC.NotifyAll","params":{"sender":"me","message":"he","data":"testdata"}} 409 | // тут же получаю уведомление 410 | // {"jsonrpc":"2.0","method":"Other.he","params":{"data":"testdata","sender":"me"}} 411 | // и отчет о выполнении команды {"id":2,"jsonrpc":"2.0","result":"OK"} 412 | public function notify($note, $type = 'info') { 413 | $ip = $this->getIp(); 414 | error_log('NOTE on ' . $ip . ':' . $note); 415 | 416 | $conn = @stream_socket_client('tcp://' . $ip . ':9090', $e, $e, 0.01, STREAM_CLIENT_CONNECT); 417 | if ($conn) { 418 | switch ($type) { 419 | case 'info': 420 | $dtime = 1500; 421 | break; 422 | case 'warning': 423 | $dtime = 3000; 424 | break; 425 | default: 426 | $dtime = 4000; 427 | } 428 | 429 | $json = array( 430 | 'jsonrpc' => '2.0', 431 | 'id' => 1, 432 | 'method' => 'GUI.ShowNotification', 433 | 'params' => array( 434 | 'title' => 'AcePHP ' . $type, 435 | 'message' => $note, 436 | 'image' => 'http://kodi.wiki/images/c/c9/Logo.png', 437 | 'displaytime' => $dtime 438 | ) 439 | ); 440 | $json = json_encode($json); 441 | $res = @stream_socket_sendto($conn, $json); 442 | fclose($conn); 443 | } 444 | } 445 | 446 | public function close() { 447 | // без этого не уничтожались объекты клиентов после их дисконнекта 448 | unset($this->last_request); 449 | 450 | if (!empty($this->stream)) { 451 | $this->stream->unregisterClientByName($this->getName()); 452 | unset($this->stream); // без этого лишняя ссылка оставалась в памяти и объект потока не уничтожался 453 | } 454 | is_resource($this->socket) and fclose($this->socket); 455 | $this->finished = true; 456 | $this->notifyListener(array('event' => 'close')); 457 | // error_log(' client closed'); 458 | } 459 | 460 | public function __destruct() { 461 | // error_log(' destruct client ' . spl_object_hash ($this) . "\t" . $this->getName()); 462 | } 463 | } 464 | 465 | 466 | 467 | 468 | -------------------------------------------------------------------------------- /res/core/class.core.stream_manager.php: -------------------------------------------------------------------------------- 1 | StreamUnit 6 | protected $closeStreams = array(); // закрывать будем не сразу, а через время (10sec). pid => puttime 7 | protected $app; 8 | 9 | // app для логирования в основном 10 | public function __construct(AcePHProxy $parent) { 11 | $this->app = $parent; 12 | } 13 | public function getStreams() { 14 | return $this->streams; 15 | } 16 | public function isExists($pid) { 17 | return isset($this->streams[$pid]); 18 | } 19 | 20 | public function setKeepaliveTime($sec) { 21 | $this->keepalive_time = $sec; 22 | } 23 | 24 | 25 | // метод обработки запроса пользователя. основная точка входа! 26 | public function start2(ClientRequest $req) { 27 | // получаем первую часть request uri (/pid, /torrent, /trid, /acelive...) 28 | // она будет указывать на плагин, необходимый для обработки запроса 29 | $pcode = $req->getPluginCode(); 30 | // скармливаем плагину запрос юзера 31 | // на выходе ожидаем ответ ClientResponse, из которого будет ясно, быстрый это запрос или медленный 32 | // плагин при процессинге определяет, надо ли запускать поток 33 | // инфо получаем по ссылке 34 | try { 35 | $client = $req->getClient(); 36 | $plugin = $this->app->getPlugin($pcode); 37 | $response = $plugin->process($req); 38 | 39 | // плагины мб разной степени кривизны, в т.ч. могут вернуть неверный объект или null/false 40 | if (!is_a($response, 'ClientResponse')) { 41 | throw new CoreException('Wrong response from plugin ' . $pcode, 0); 42 | } 43 | 44 | // если требуется запуск, смотрим по id, не запущено ли уже 45 | if ($response->isStream()) { 46 | $streamid = $response->getStreamId(); 47 | // если трансляции нет, создаем экземпляр 48 | if (!$this->isExists($streamid)) { 49 | $this->app->log(sprintf('Start new %s-stream ', $response->getPluginCode())); 50 | // просим плагин запустить поток. вся логика ace, file, torrent, web, soap - на стороне плагинов 51 | // но создавать самостоятельно новые потоки плагин не может. надо тут проверить, не запущен ли уже такой streamid 52 | $this->streams[$streamid] = new StreamUnit($response->getStreamResource()); 53 | } 54 | else { // уже есть и запущено 55 | #error_log('Stream exists'); 56 | $this->streams[$streamid]->unfinish(); 57 | } 58 | // если мы тут, значит поток либо успешно создан, либо уже был создан ранее 59 | 60 | // удалим из очереди на закрытие 61 | if (isset($this->closeStreams[$streamid])) { 62 | $this->app->log('Cancel stop ' . $this->streams[$streamid]->getName()); 63 | unset($this->closeStreams[$streamid]); 64 | } 65 | 66 | // регистрируем клиента в потоке 67 | #error_log('Register client in stream'); 68 | $this->streams[$streamid]->registerClient($client); 69 | // на клиента ничего не пишем, ответные заголовки будут потом 70 | } else { 71 | $respContents = $response->getContents(); 72 | $client->put($respContents); 73 | $client->close(); 74 | } 75 | } 76 | catch (Exception $e) { 77 | $client->close(); 78 | if (!empty($streamid)) { 79 | $this->closeStream($streamid); 80 | $client->notify('Start error: ' . $e->getMessage(), 'error'); 81 | } 82 | $this->app->error($e->getMessage()); 83 | return false; 84 | } 85 | 86 | return ; 87 | } 88 | 89 | // вызывается около 33 раз в сек, зависит от usleep в главном цикле 90 | public function copyContents() { 91 | // по каждой активной трансляции читаем контент и раскидываем его по ассоциированным клиентам 92 | // по каждому ace-коннекту читаем его лог, чтобы буфер не заполнял, да и полезно бывает 93 | foreach ($this->streams as $pid => $one) { 94 | try { 95 | $one->copyChunk(); 96 | } 97 | catch (Exception $e) { 98 | // TODO какого рода тут ошибка и почему раньше стрим закрывался только по ошибке ace_connection_broken - ХЗ 99 | error_log($e->getMessage()); 100 | $this->app->error('StrMgr [E] ' . $e->getMessage()); 101 | #if ($e->getMessage() == 'ace_connection_broken') { 102 | $this->closeStream($pid); 103 | #} 104 | } 105 | } 106 | } 107 | 108 | 109 | 110 | public function closeAll() { 111 | foreach ($this->closeStreams as $pid => $time) { 112 | $this->closeStream($pid); 113 | } 114 | foreach ($this->streams as $pid => $peers) { 115 | $this->closeStream($pid); 116 | } 117 | } 118 | 119 | // xbmc странно делает, при зависании закрывает коннект и открывает новый с offset-ом, 120 | // при этом я успеваю отдать хедеры, но в итоге xbmc останавливает поток 121 | // может ему что то в хедерах не нравится 122 | protected function markStream4Close($pid) { 123 | $this->closeStreams[$pid] = time(); 124 | } 125 | 126 | public function closeWaitingStreams() { 127 | // mark finished streams 128 | foreach ($this->streams as $pid => $one) { 129 | if ($one->isFinished() and !isset($this->closeStreams[$pid])) { 130 | $this->app->log('Prepare to stop ' . $one->getName()); 131 | $this->markStream4Close($pid); 132 | } 133 | } 134 | 135 | // потоки, помеченные для закрытия, закрываем по достижении таймаута 136 | foreach ($this->closeStreams as $pid => $time) { 137 | if (time() - $time > $this->keepalive_time) { // 15 sec to close 138 | $this->closeStream($pid); 139 | unset($this->closeStreams[$pid]); 140 | } 141 | } 142 | } 143 | 144 | protected function closeStream($pid) { 145 | if (!$this->isExists($pid)) { 146 | return false; // o_O 147 | } 148 | $name = $this->streams[$pid]->getName(); 149 | $this->streams[$pid]->close(); 150 | unset($this->streams[$pid]); 151 | $this->app->log('Closed stream ' . $name); 152 | } 153 | } 154 | 155 | 156 | -------------------------------------------------------------------------------- /res/core/class.core.stream_unit.php: -------------------------------------------------------------------------------- 1 | cur_conn = $conn; // КЛАСС источника потока, у него там тоже конечный автомат есть 63 | $this->cur_conn->registerEventListener(array($this, 'connectionListener')); 64 | $this->state = self::STATE_IDLE; 65 | 66 | $this->init(); 67 | 68 | // ace-related code! TODO 69 | // в принципе все эти ace-параметры можно и к другим источникам применить 70 | $this->statistics = array( 71 | 'dl_total' => 0, // сколько байт вообще было прочитано из источника за все время 72 | 'bufpercent' => null, 73 | 'acestate' => null, 74 | 'speed_dn' => null, 75 | 'speed_up' => null, 76 | 'peers' => null, 77 | 'dl_bytes' => null, 78 | 'ul_bytes' => null, 79 | ) + $this->buf_adjusted; 80 | # error_log('construct stream ' . spl_object_hash ($this)); 81 | 82 | $this->start2(); 83 | } 84 | 85 | protected function init() { 86 | $this->bufferSize = self::BUF_READ; 87 | // инициализируем и сопутствующие массивы 88 | $this->buf_adjusted = array( 89 | 'lastcheck' => null, 90 | 'state' => null, // есть данные или нет 91 | 'over' => false, // adaptiveBuffer: слишком долго читаем поток без буферизации 92 | 'changed' => null, // unixts последнего перехода нет-есть/есть-нет данных 93 | 'state1time' => null, // adaptiveBuffer: время наличия данных 94 | 'state0time' => null, // adaptiveBuffer: время отсутствия данных 95 | 'emptydata' => false, // считаны ли данные из источника 96 | 'shortdata' => false, // true, если данных меньше, чем размер буфера 97 | ); 98 | } 99 | 100 | private function start2() { 101 | $this->state = self::STATE_STARTING; 102 | $this->startTime = time(); 103 | $this->waitSec = 30; // cycles ~ seconds 104 | $this->cur_conn->open(); 105 | } 106 | 107 | public function __destruct() { 108 | # error_log(' destruct stream ' . spl_object_hash ($this)); 109 | } 110 | 111 | // первый компонент request_uri, т.е. плагин (torrent, ttv, websocket, etc) 112 | // достать его здесь - не совсем просто, т.к. это содержится в ClientRequest 113 | // делаем так, при регистрации в потоке первого клиента, берем его lastRequest, 114 | // и спрашиваем у него тип 115 | public function getType() { 116 | return $this->streamType; 117 | } 118 | 119 | public function close() { 120 | // вообще по идее при уничтожении объекта будут вызваны __destruct и всех вложенных 121 | foreach ($this->clients as $peer => $one) { 122 | $this->dropClientByPeer($peer); 123 | } 124 | $this->closeStream(); 125 | $this->finished = true; 126 | $this->state = self::STATE_IDLE; 127 | } 128 | private function dropClientByPeer($peer) { 129 | // почему было закомментировано закрытие клиентов? 130 | $this->clients[$peer]->close(); 131 | unset($this->clients[$peer]); // может из-за unset? 132 | // в __destruct у клиента нет кода самозакрытия, так что раскомментировал 133 | // не работал сброс клиента при Failed to get link, помогло 134 | } 135 | 136 | public function unfinish() { 137 | $this->finished = false; 138 | } 139 | 140 | public function isFinished() { 141 | return $this->finished; 142 | } 143 | 144 | public function isRestarting() { 145 | return $this->cur_conn->isRestarting(); 146 | } 147 | 148 | protected function closeStream() { 149 | isset($this->cur_conn) and $this->cur_conn->close(); 150 | // например для плагина вебсервера объект файла закрывался, но не удалялся. 151 | // а в нем ссылка на этот объект StreamUnit (через registerEventListener) 152 | unset($this->cur_conn); 153 | } 154 | 155 | public function getStatistics() { 156 | return $this->statistics; 157 | } 158 | 159 | // текущий объем данных в разделяемом буфере 160 | public function getBufferedLength() { 161 | return strlen($this->buffer); 162 | } 163 | 164 | // размер порции данных, читаемых из источника за раз 165 | public function getBufferSize() { 166 | return $this->bufferSize; 167 | } 168 | 169 | // максимальный объем разделяемого буфера 170 | public function getBufferLength() { 171 | return self::BUFFER_LENGTH; 172 | } 173 | 174 | public function getState() { 175 | $set = array( 176 | iconv('cp866', 'utf8', chr(0x27)), // ' (апостроф, точки вверху не нашел) 177 | iconv('cp866', 'utf8', chr(0xf9)), // точка в центре 178 | '.' // точка внизу 179 | ); 180 | $sign = $set[time() % count($set)]; 181 | $perc = $this->statistics['bufpercent']; 182 | $state = $this->statistics['acestate']; 183 | 184 | if ($state == 'buf') { 185 | $state = $sign . ' ' . $perc . '%'; 186 | } 187 | else if ($state == 'check') { 188 | $state = 'chk ' . $perc . '%'; 189 | } 190 | else if ($state == 'prebuf') { 191 | $state = 'pre ' . $perc . '%'; 192 | } 193 | else if ($state == 'dl') { 194 | $state = 'PLAY'; 195 | // для кина рисуем другую картинку 196 | if (!$this->isLive) { 197 | $s = ')'; // различные варианты значков 198 | #$s = iconv('cp866', 'utf8', chr(186)); 199 | #$s = iconv('cp866', 'utf8', chr(249)); 200 | $list = array("$s ", "$s$s ", "$s$s$s ", " $s$s ", " $s ", " "); 201 | // * 4 регулирует скорость. больше множитель - выше скорость 202 | $symbolidx = round(microtime(1) * 3) % count($list); 203 | $symbol = $list[$symbolidx]; 204 | $state = ($perc == 100 ? $perc : ($symbol . $perc)) . '%'; 205 | } 206 | } 207 | else if ($this->state == self::STATE_STARTED) { 208 | $state = 'READ'; 209 | } 210 | else if ($this->state == self::STATE_STARTING) { 211 | $state = 'START'; 212 | } 213 | else { 214 | #$state = 'unk'; 215 | } 216 | return $state; 217 | } 218 | public function isLive() { 219 | return $this->isLive; 220 | } 221 | 222 | public function getName() { 223 | // в случае failed to start stream объекта потока может не быть 224 | // и тогда тут будет фатал 225 | return isset($this->cur_conn) ? $this->cur_conn->getName() : null; 226 | } 227 | 228 | public function getClients() { // alias 229 | return $this->getPeers(); 230 | } 231 | public function getPeers() { 232 | return $this->clients; 233 | } 234 | 235 | public function unregisterClientByName($peer) { 236 | unset($this->clients[$peer]); 237 | if (empty($this->clients)) { // пора сворачивать кино 238 | // выставим флаг, а StreamsManager по нему поставит нас в очередь на остановку 239 | $this->finished = true; 240 | } 241 | } 242 | 243 | protected function notify() { 244 | $args = func_get_args(); 245 | foreach ($this->clients as $one) { 246 | call_user_func_array(array($one, 'notify'), $args); 247 | } 248 | } 249 | // метод отвечает, запущена ли уже выдача видео, т.е. в основном ли рабочем состоянии находится объект 250 | protected function isRunning() { 251 | return $this->state == self::STATE_STARTED; 252 | } 253 | 254 | 255 | 256 | // любой ответ от движка в plaintext поступает сюда 257 | public function connectionListener($stats) { 258 | $this->statistics = array_merge($this->statistics, $stats); 259 | 260 | // движок говорит, что поток остановлен (бывает при ошибке Cannot load transport file) 261 | if (!empty($stats['eof'])) { 262 | # $this->close(); 263 | $this->finished = true; // через флаг лучше. parent-объект нас потом сам закроет 264 | # error_log('Event: stream resource eof'); 265 | } 266 | // вообще этот метод дергается только при наличии ответа от Ace, а если тот будет молчать, можем застрять 267 | else if ($this->state == self::STATE_STARTING and !empty($stats['started'])) { 268 | $this->state = self::STATE_STARTED; 269 | $this->isLive = $this->cur_conn->isLive(); 270 | $this->startedTime = time(); 271 | foreach ($this->getClients() as $c) { 272 | // перевыставляем режим ecoMode, см.коммент к методу registerClient 273 | $c->setEcoMode($this->isLive); 274 | } 275 | #error_log('Event: stream resource ready, isLive=' . ($this->isLive ? 'true' : 'false')); 276 | } 277 | 278 | if (!empty($stats['headers'])) { // готовы хедеры в ответ на запрос пользователя, отдаем 279 | // поток открыт, пора всех клиентов оповестить и раздать им заголовки 280 | #error_log('Event: Accepting all clients on stream start'); 281 | foreach ($this->getClients() as $c) { 282 | // отправляем хттп заголовки ОК 283 | $c->accept($stats['headers']); 284 | } 285 | } 286 | } 287 | 288 | // TODO рефаккттоориииить 289 | // TODO еще косяк.. касается долгооткрывающегося ace контента. 290 | // флаг isLive в момент регистрации клиента мб определен неверно, 291 | // т.к. регистрация происходит раньше, чем поток открывается и 292 | // отчитывается событием 'headers' 293 | public function registerClient(StreamClient $client) { 294 | // это тут немного не к месту. просто нужен тип открываемого потока 295 | // файл, торрент, вебсокет, тв и т.д. 296 | if (!$this->streamType) { 297 | $req = $client->getLastRequest(); 298 | $this->streamType = $req->getPluginCode(); 299 | } 300 | 301 | if (!$this->isLive) { // типа кино 302 | // предыдущих клиентов надо скинуть, иначе новый диапазон байт будет при 303 | // прочтении записан на них тоже. надо только на последнего подключившегося 304 | // может это логичнее при openStream делать? 305 | // error_log('Drop all clients except new one'); 306 | // если клиенту не отправлялись заголовки Connection: close, он вполне 307 | // может возыметь наглось отправить еще запрос по тому же каналу 308 | // и этот запрос будет обработан как и предыдущий. и приведет нас сюда 309 | // и в случае, если это кино (isLive=false), то все клиенты будут сброшены 310 | // включая последнего, и это косяк. см.ниже про getLastRequest() 311 | foreach ($this->getClients() as $peer => $one) { 312 | // поэтому дополнительно проверяем peer 313 | if ($peer == $client->getName()) { 314 | // сделаем unregister, чтобы далее register нормально отработал 315 | $this->unregisterClientByName($peer); 316 | error_log(' unregister ' . $peer . ' instead of kick'); 317 | } else { 318 | $this->dropClientByPeer($peer); 319 | } 320 | } 321 | // на случай если поток уже был запущен и последний клиент отключился, 322 | // идет обратный отсчет до полной остановки. и тут подключается новый 323 | // клиент - надо отменить остановку 324 | $this->unfinish(); 325 | $this->buffer = ''; 326 | $this->statistics['dl_total'] = 0; 327 | } 328 | 329 | // для режима кино обязательно вырубаем ecoMode, иначе просто не будет работать 330 | // т.к. для работы перемотки плееры делают несколько мелких запросов, а ecoMode 331 | // из-за этого отдает данные по 1 байту 332 | // см. коммент к методу про косяк с isLive 333 | $client->setEcoMode($this->isLive); 334 | 335 | $peer = $client->getName(); 336 | $this->clients[$peer] = $client; 337 | $client->associateStream($this); 338 | 339 | 340 | 341 | 342 | // что идет ниже - мне не нравится 343 | // заголовки должны быть с правильным range и content-length 344 | if ($this->isLive) { 345 | // если поток уже открыт и воспроизводится, то похоже это дополнительные клиенты 346 | // надо им отослать заголовки! а то внезапно оказалось, что более 1 клиента на одну 347 | // трансляцию перестало обслуживаться 348 | // для Live-режима заголовки те же самые, одинаковые для всех 349 | // для просмотра торрентов отдельная песня. там разные Range: bytes должны быть 350 | if ($this->isRunning()) { 351 | $headers = $this->cur_conn->getStreamHeaders(true); 352 | // попробуем решить проблему отвала VLC по negative counter таким способом: 353 | // нового клиента цепляем на середину буфера 354 | list($pointerPos, $pointer) = $this->getMiddlePointerPosition(); 355 | # error_log('Accept client on ' . $pointerPos . '%'); 356 | $client->accept($headers, $pointer, $pointerPos); 357 | } 358 | return; 359 | } 360 | 361 | // далее идет логика для режима Кино (не лайв поток) 362 | // сбросим метку начала старта, чтобы не кикнуло раньше времени 363 | $this->startTime = time(); 364 | // итак, если у нас не поток (кино), то нужно закрыть источник 365 | // и открыть его с новыми клиентскими заголовками 366 | // НО только если поток уже запущен и воспроизводится 367 | if (!$this->isRunning()) { 368 | return; 369 | } 370 | 371 | // вебсокеты открываются быстро, и сразу отправляют уведомление с headers, 372 | // но клиент еще не ассоциирован и не получает его. правильнее каждому новому клиенту 373 | // при регистрации сразу давать заголовки, если они готовы 374 | // upd: снова косяк. теперь кино глючит. первый коннект получает хедеры, 375 | // затем идет следующий коннект с новым range, но хедеры еще не обновились (поток не переоткрылся) 376 | // и клиент тут принимается со старыми заголовками.. 377 | // upd: другой косяк. WMPlayer имеет наглость иногда отправлять через один сокет 2 GET запроса. 378 | // и если это кино, то при обработке второго запроса кикаются все клиенты (см.выше), 379 | // в т.ч. и сам клиент от второго запроса, т.к. он тот же, что и для первого. 380 | // last_request в клиенте очищается и получаем тут при обращении req->isRanged() фатал! 381 | $req = $client->getLastRequest(); 382 | // DONE хотелось бы все же, чтобы у каждого клиента был определен минимум 1 запрос, 383 | // с которым он пришел. иначе нефига ему в этом методе делать 384 | if ($req->isRanged()) { 385 | $range = $req->getReqRange(); 386 | $this->cur_conn->seek($range['from']); 387 | // error_log('Seek to ' . $range['from']); 388 | } 389 | 390 | $headers = $this->cur_conn->getStreamHeaders(true); 391 | if ($headers) { 392 | $client->accept($headers); 393 | } 394 | } 395 | 396 | // если транслируем неразобранный chunked-поток, надо позицию искать так, 397 | // чтобы данные для клиента начинались с длины чанка, как положено 398 | // иначе пофиг, просто берем 50% 399 | private function getMiddlePointerPosition() { 400 | $isChunkedStream = $this->isChunkedStream(); 401 | if ($isChunkedStream) { 402 | $offset = strlen($this->buffer) / 2; 403 | $found = preg_match('~(?:\r?\n|^)([0-9a-f]{3,8})\r?\n~smU', $this->buffer, $m, PREG_OFFSET_CAPTURE, $offset); 404 | if (!$found or !isset($m[1][1])) { 405 | error_log('Failed to get middle position in chunked stream'); 406 | $pointerPos = 0; 407 | $pointer = 0; 408 | } else { 409 | $pointer = $m[1][1]; 410 | $pointerPos = round(100 * $pointer / strlen($this->buffer)); 411 | } 412 | } else { 413 | $pointerPos = 50; 414 | $pointer = round(strlen($this->buffer) * $pointerPos / 100); 415 | } 416 | return array($pointerPos, $pointer); 417 | } 418 | 419 | private function isChunkedStream() { 420 | return stripos($this->cur_conn->getStreamHeaders(true), 'chunked') !== false; 421 | } 422 | 423 | 424 | // читаем часть трансляции и раздаем зарегенным клиентам 425 | // вызывается около 33 раз в сек, зависит от usleep в главном цикле 426 | // наверное где то тут надо отслеживать коннект ace и рестартить поток в случае падения 427 | public function copyChunk() { 428 | // таймаут ожидания открытия потока. если источник ничего не выдал - забиваем 429 | if ($this->state == self::STATE_STARTING) { 430 | $secPassed = (time() - $this->startTime); 431 | if ($secPassed > $this->waitSec) { 432 | // может close+exception заменить одним методом, например error(msg) 433 | $this->close(); 434 | throw new CoreException('Failed to start stream', 0); 435 | } 436 | // return; 437 | } 438 | 439 | // на данном этапе надо открыть полученную ссылку, и сконнектить общение клиента и Ace, 440 | // т.е. пробрасывать все запросы клиента в поток, ну и само собой из потока все данные тупо на клиента выдавать 441 | // клиент запросит несколько различных частей потока и тогда перемотка работает!! 442 | // класть ли ответные заголовки в буфер или отсеивать? 443 | 444 | $data = null; 445 | // если режим остановки и буфер похудел - продолжаем чтение 446 | // TODO эту фигню надо рефакторить и тестировать! глючит 447 | // и для лайв-режима неактуальна совершенно 448 | // КОСЯК: проблема была в том, что ТВ поток стопорился внезапно, 449 | // и ace-статистика по нему не обновлялась, т.к. не дергался aceconn::readsocket(), 450 | // т.к. не вызывался getStreamChunk(), т.к. был режим остановки чтения! 451 | if (!$this->isLive and $this->stopReading) { 452 | if ($this->getBufferedLength() <= $this->getBufferLength()) { 453 | $this->stopReading = false; 454 | } 455 | } 456 | // считываем часть контента из источника 457 | else { 458 | $data = $this->cur_conn->getStreamChunk($this->getBufferSize()); 459 | } 460 | 461 | // тут собирается некоторая статистика и флаги для вывода в UI 462 | $this->adjustBuffer($data); 463 | // добавляем считанные данные к буферу 464 | // если считанных данных нет. а до этого была частичная запись, то в буфере остается кусок, 465 | // который пишется бесконечно, пока не будут прочитаны данные из потока - косяк 466 | $this->appendBuffer($data); 467 | 468 | // TODO 469 | // если данные пусты, надо выдавать по несколько байт из последнего элемента буфера, 470 | // чтобы XBMC дал нормально остановить при желании поток. 471 | // а то он пока байта не прочитает будет висеть (или до таймаута своего) 472 | 473 | $this->statistics = array_merge($this->statistics, $this->buf_adjusted); 474 | 475 | // походу тут и проблема. эта строка писалась для старта потока 476 | // однако она же сработает и при окончании потока от Ace 477 | // и записывать на клиент по 1 байтику не даст 478 | // upd: емое,я с указателем перепутал, закэшированные данные в размере 479 | // могут только вырасти, с 0 до 15-30Мб 480 | $gotLinkTime = (time() - $this->startedTime); 481 | if ( 482 | $this->isLive and 483 | ($gotLinkTime < self::INIT_SECONDS or 484 | $this->getBufferedLength() < self::INIT_LENGTH) 485 | ) { // подкопим немного для начала 486 | return; // // убрал до ввода доп.флага различия старта и финиша 487 | } 488 | 489 | // на каждого клиента есть указатель на буфер 490 | // буфер потока один на всех клиентов 491 | // при старте потока пишем все в буфер, держим его размер постоянным 492 | foreach ($this->clients as $peer => $client) { 493 | // на клиента всегда пытаемся писать все, что есть, т.к. максимальными кусками 494 | $result = $client->put($this->buffer, self::BUF_MAX); 495 | // TODO это что, такой метод определения eof? 496 | if ($this->isFinished() and $client->getPointerPosition() == 100) { 497 | $this->dropClientByPeer($peer); 498 | } 499 | } 500 | 501 | $this->trimBuffer(); 502 | 503 | return ; 504 | } 505 | 506 | protected function appendBuffer($data) { 507 | if ($data) { 508 | $this->buffer .= $data; 509 | $this->statistics['dl_total'] += strlen($data); 510 | } 511 | } 512 | 513 | // задача метода - держать размер буфера 15-30Мб 514 | // уведомлять клиентов о необходимости скорректировать указатели 515 | // кикать зазевавшихся или мертвых клиентов (upd: клиент сам себя кикнет) 516 | protected function trimBuffer() { 517 | $len = $this->getBufferedLength(); 518 | $delta = $len - $this->getBufferLength(); 519 | if ($delta > 0) { 520 | $this->buffer = substr($this->buffer, $delta); 521 | } 522 | // а если delta 0 или вдруг < 0? 523 | $tmp = true; 524 | // если хоть один клиент уже приближается к концу буфера, надо бы его пополнить 525 | // т.е. прочитать еще кусок из источника. а если не переставать читать, то 526 | // клиентов кикнет по достижении начала буфера. типа они отстали от остальных 527 | foreach ($this->clients as $peer => $client) { 528 | if ($client->getPointerPosition() > 80) { 529 | $tmp = false; 530 | } 531 | $client->correctBufferPointer($delta, $this->buffer); 532 | } 533 | // здесь определяется только остановка, не запуск 534 | if ($tmp != $this->stopReading and $this->getBufferedLength() > 1000000) { 535 | $this->stopReading = true; 536 | } 537 | } 538 | 539 | // data на входе только для контроля ситуации, идет ли считывание из ace 540 | protected function adjustBuffer($data, &$adjusted = null) { 541 | // логика адаптивной подстройки буфера выключена, фигня 542 | $adaptiveBuffer = false; 543 | 544 | $adjusted = null; // не используется в общем то 545 | 546 | $this->buf_adjusted['emptydata'] = empty($data); 547 | $this->buf_adjusted['shortdata'] = strlen($data) < $this->bufferSize; 548 | 549 | // хочется добиться равномерного считывания потока и записи на клиент 550 | // причем с учетом, что у потоков мб разный битрейт 551 | // если данные не получены, значит вычитали весь буфер источника 552 | // (при нормальной работе, факапы в расчет не берем сейчас) 553 | // значит прекращаем повышать размер буфера для потока 554 | // иначе повышаем его постепенно (на 100-1000 байт при каждом пустом $data) 555 | 556 | // время считывания контента должно быть секунд 30, подстраиваем буфер под это 557 | // upd: буфер выставлен фиксированно, не меняем его размер 558 | // только собираем доп.данные 559 | 560 | $statechange = false; // факт перехода есть данные - нет данных и обратно 561 | if ($data and !$this->buf_adjusted['state']) { // переход "нет данных - есть данные" 562 | $this->buf_adjusted['state'] = true; 563 | // если есть время, когда пропали данные, высчитаем период их отсутствия 564 | if ($this->buf_adjusted['changed']) { 565 | $this->buf_adjusted['state0time'] = time() - $this->buf_adjusted['changed']; 566 | } 567 | $this->buf_adjusted['changed'] = time(); 568 | $statechange = true; 569 | } 570 | else if (!$data and $this->buf_adjusted['state']) { // переход "есть - нет" 571 | $this->buf_adjusted['state'] = false; 572 | // если есть время, когда появились данные, высчитаем период их наличия 573 | if ($this->buf_adjusted['changed']) { 574 | $this->buf_adjusted['state1time'] = time() - $this->buf_adjusted['changed']; 575 | } 576 | $this->buf_adjusted['changed'] = time(); 577 | $statechange = true; 578 | } 579 | 580 | $check = ( 581 | empty($this->buf_adjusted['lastcheck']) or 582 | time() - $this->buf_adjusted['lastcheck'] >= 1 583 | ); 584 | 585 | $changeTime = time() - $this->buf_adjusted['changed']; 586 | if ($data and $changeTime > (1.0 * self::BUF_SECONDS) and $check) { // too long reading 587 | $this->buf_adjusted['over'] = true; 588 | // адаптивная подстройка буфера 589 | $adaptiveBuffer and $this->bufferSize += 150; 590 | } 591 | if ($check) { 592 | $this->buf_adjusted['lastcheck'] = time(); 593 | } 594 | 595 | // итого, имея период наличия данных и их отсутствия, 596 | if ($adaptiveBuffer and $this->buf_adjusted['state1time']) { 597 | $coeff = $this->buf_adjusted['state1time'] / self::BUF_SECONDS; 598 | if (!$data) { 599 | // здесь принимается решение подпихнуть символ вместо данных 600 | // развитие алгоритма: вместо случайного символа - расходуем буфер FIFO 601 | // если конечно он есть 602 | if ($check and !$this->buf_adjusted['state'] and $changeTime > 20) { 603 | // тут была подстановка в data фейкового байта 604 | } 605 | } 606 | // буфер правим только при переходе "есть - нет" 607 | if ($statechange and !$this->buf_adjusted['state'] and 608 | !$this->buf_adjusted['over']) { 609 | // внимание со степенью: должна быть нечетной, чтобы не потерять знак 610 | $delta = round(-($this->bufferSize * self::BUF_DELTA_PRC / 100) 611 | * pow(2 * (1 - $coeff), 3)); 612 | $this->bufferSize += $delta; 613 | if ($this->bufferSize > self::BUF_MAX) { 614 | $this->bufferSize = self::BUF_MAX; 615 | } 616 | else if ($this->bufferSize < self::BUF_MIN) { 617 | $this->bufferSize = self::BUF_MIN; 618 | } 619 | $adjusted = array('delta' => $delta, 'buf' => $this->bufferSize); 620 | } 621 | } 622 | if ($statechange) { 623 | $this->buf_adjusted['over'] = false; 624 | } 625 | // HACK нафиг всю эту подстройку буфера 626 | $adaptiveBuffer or $this->bufferSize = self::BUF_READ; 627 | 628 | if (!isset($this->statistics['speed_dn'])) { 629 | return $this->bufferSize; 630 | } 631 | // такая мысля. смотрим скорость загрузки и выставляем буфер относительно нее 632 | // чтобы не обогнать наполнение буфера ace своим жадным чтением из него, ибо 633 | // это провоцирует его уходить в глухой режим буферизации 634 | // но вот засада - скорость указана в секунду, 635 | // а мы читаем данные из источника неизвестно сколько раз за секунду, так что 636 | // даже установление нужного размера буфера не поможет просто так. 637 | // нужно ограничить чтение данных этим объемом за секунду 638 | // а еще у нас есть данные по объему скачанных ace данных 639 | // если отслеживать кол-во прочитанного, то с оглядкой на ту цифру можно 640 | // и буфер выставить 641 | 642 | // пока данных мало - используем простое определение буфера по скорости 643 | // имеет смысл только для isLive 644 | $dl_bytes = $this->statistics['dl_bytes']; 645 | $dl_total = $this->statistics['dl_total']; 646 | // когда кино скачалось, скорость падает до 0 и размер буфера вместе с ней 647 | $dlspeed = $this->statistics['speed_dn'] ? $this->statistics['speed_dn'] : 1; 648 | // скорость - кбайт/с, буфер - байт, 0.9 - коэфф-т 649 | $dl_bytes2 = $dl_bytes - self::INIT_LENGTH; // пытаемся отставать ровно на N Мб 650 | $coeff2 = ($dl_bytes2 - $dl_total) / self::INIT_LENGTH; // пытаемся отставать ровно на N Мб 651 | // коэф-т надо немного ослабить, а то слишком быстро нагоняет разницу 652 | // делим на 2 653 | $this->bufferSize = round($coeff2 * 1024 * $dlspeed / 3); 654 | 655 | if ($this->bufferSize > self::BUF_MAX) { 656 | $this->bufferSize = self::BUF_MAX; 657 | } 658 | else if ($this->bufferSize < self::BUF_MIN) { 659 | $this->bufferSize = self::BUF_MIN; 660 | } 661 | 662 | #error_log(sprintf("%.1f\tAce dlb: %d\twe got: %d\tBufSize: %d", 663 | # $coeff2, $dl_bytes, $dl_total, $this->bufferSize)); 664 | } 665 | 666 | } 667 | 668 | -------------------------------------------------------------------------------- /res/init.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 39 | $this->pid = $pid; 40 | $this->parent = $parent; 41 | // error_log('construct aceconn ' . spl_object_hash($this)); 42 | } 43 | public function __destruct() { 44 | $this->disconnect(); 45 | // error_log(' destruct aceconn ' . spl_object_hash($this)); 46 | } 47 | 48 | public function startraw($base64, $fileidx = 0) { 49 | $this->fileidx = $fileidx; 50 | $this->cid = $base64; 51 | $this->startMode = 'raw'; 52 | } 53 | public function starttorrent($url, $fileidx = 0) { 54 | $this->fileidx = $fileidx; 55 | $this->cid = $url; 56 | $this->startMode = 'torrent'; 57 | } 58 | public function startpid($pid) { 59 | $this->fileidx = null; 60 | $this->cid = $pid; 61 | $this->startMode = 'pid'; 62 | } 63 | 64 | public function open() { 65 | switch ($this->startMode) { 66 | case 'raw': 67 | $idx = rand(10, 99); 68 | $this->send('LOADASYNC ' . $idx . ' RAW ' . $this->cid . ' 0 0 0', 0); 69 | $this->send('START RAW ' . $this->cid . ' ' . $this->fileidx . ' 0 0 0', 10); 70 | break; 71 | case 'torrent': 72 | $idx = rand(10, 99); 73 | $this->send('LOADASYNC ' . $idx . ' TORRENT ' . $this->cid . ' 0 0 0', 0); 74 | $this->send('START TORRENT ' . $this->cid . ' 0 0 0 0 0', 15); 75 | break; 76 | case 'pid': 77 | $this->send('START PID ' . $this->cid . ' 0', 15); 78 | break; 79 | default: 80 | throw new CoreException('Unknown start mode', 0); 81 | } 82 | } 83 | 84 | public function auth($prodkey) { 85 | // HELLOBG, get inkey 86 | $ans = $this->send('HELLOBG version=3'); // << HELLOTS ... key=... 87 | if (!preg_match('~key=([0-9a-f]{10})~', $ans, $m)) { 88 | throw new CoreException('No answer with HELLOBG. ' . $ans, CoreException::EXC_CONN_FAIL); 89 | } 90 | $inkey = $m[1]; 91 | if (!$inkey) { 92 | throw new Exception('Key not get with HELLOBG'); 93 | } 94 | 95 | $ready_key = $this->makeKey($prodkey, $inkey); 96 | 97 | // AUTH with ready_key 98 | $ans = $this->send(sprintf('READY key=%s', $ready_key)); // << AUTH 1 99 | 100 | return $this->auth = true; //$ans == 'AUTH 1'; 101 | } 102 | 103 | public function isLive() { // START http://... >>> stream=1 104 | return $this->isLive; 105 | } 106 | public function isAuthorized() { 107 | return $this->auth; 108 | } 109 | 110 | protected function makeKey($prodkey, $inkey) { 111 | $shakey = sha1($inkey . $prodkey); 112 | $part = explode('-', $prodkey); 113 | $prod_part = reset($part); 114 | return $prod_part . '-' . $shakey; 115 | } 116 | 117 | public function send($string, $sec = 1, $usec = 0) { 118 | stream_socket_sendto($this->conn, $string . "\r\n"); 119 | // error_log('Ace send: ' . $string); 120 | $line = $this->readsocket($sec, $usec); 121 | return $line; 122 | } 123 | 124 | public function registerEventListener($cb) { 125 | $this->listener = $cb; 126 | } 127 | protected function notifyListener($event) { 128 | is_callable($this->listener) and call_user_func_array($this->listener, array($event)); 129 | } 130 | 131 | public function close() { 132 | // тут сохранялся объект StreamUnit, отчего даже при закрытии потока объект не уничтожался 133 | $this->listener = null; 134 | // вызываем через parent, Он управляет массивом коннектов 135 | // дисконнект будет вызван через destruct 136 | // TODO черезжопия какая то, можно бы и getPID выпилить заодно 137 | $this->parent->_closeConn($this->getPID()); 138 | } 139 | private function getPID() { 140 | return $this->pid; 141 | } 142 | public function getName() { 143 | return $this->name; 144 | } 145 | 146 | 147 | // public только для aceCLI.php 148 | public function readsocket($sec = 0, $usec = 300000) { 149 | stream_set_timeout($this->conn, $sec, $usec); 150 | 151 | // при падении ace engine моментально выставляется eof в true 152 | $s = socket_get_status($this->conn); 153 | if ($s['eof']) { 154 | $this->eof = true; 155 | // тут ничего не делаем, задумка такая, что Listener получит флаг eof и сам все остановит 156 | // $this->disconnect(); // решение примем уровнями выше 157 | // throw new Exception('ace_connection_broken'); 158 | } 159 | 160 | $dlstat = array(); 161 | $line = trim(fgets($this->conn)); 162 | if ($line) { 163 | $dlstat['line'] = $line; 164 | if ($line == 'EVENT getuserdata') { 165 | $this->send('USERDATA [{"gender": 1}, {"age": 4}]'); 166 | // error_log('Send userdata'); 167 | } 168 | 169 | // error_log('Ace line: ' . $line); 170 | $pattern = '~^STATUS\smain:(?buf|prebuf|dl|check);(?\d+)(;(\d+;\d+;)?\d+;' . 171 | '(?\d+);\d+;(?\d+);(?\d+);\d+;(?\d+);\d+;(?\d+))?$~s'; 172 | if (preg_match($pattern, $line, $m)) { 173 | $dlstat = array( 174 | 'acestate' => $m['state'], 175 | 'bufpercent' => isset($m['percent']) ? $m['percent'] : null, 176 | 'speed_dn' => @$m['spdn'], 177 | 'speed_up' => @$m['spup'], 178 | 'peers' => @$m['peers'], 179 | 'dl_bytes' => @$m['dlb'], 180 | 'ul_bytes' => @$m['ulb'], 181 | ); 182 | } else if (substr($line, 0, 5) == 'EVENT') { 183 | } else if (substr($line, 0, 5) == 'STATE') { 184 | // error_log('Ace: ' . $line); 185 | } else { 186 | // error_log('Ace line not matched: ' . $line); 187 | } 188 | 189 | // несколько косвенно. можно смотреть на окончание данных по ссылке 190 | if (!$this->eof and $line == 'STATE 0') { 191 | $this->eof = true; 192 | } 193 | // при состоянии STARTING ожидаем ссылки на поток 194 | // ждем START http://127.0.0.1:6878/content/aa1ad7963f4dabed7899367c9b6b33c77447abad/0.784118134089 195 | if (strpos($line, 'START http') !== false) { 196 | $tmp = explode(' ', $line); 197 | $this->link = $tmp[1]; 198 | $this->isLive = (isset($tmp[2]) and $tmp[2] == 'stream=1'); 199 | // error_log('Got link ' . $this->link . ' ' . ($this->isLive ? 'is live' : 'is NOT live')); 200 | } 201 | 202 | // TODO можно и красивше сделать 203 | if (strpos($line, 'LOADRESP') !== false) { 204 | // поскольку idx двузначный, можем отрезать с известной позиции в строке 205 | // длина "LOADRESP NN " = 12 206 | $answer = explode(' ', $line, 3); 207 | if (isset($answer[2])) { 208 | $answer = json_decode($answer[2], true); 209 | $fileidx = $this->fileidx; 210 | // первый попавшийся filename берем как название ресурса 211 | if (isset($answer['files'], $answer['files'][$fileidx], $answer['files'][$fileidx][0])) { 212 | $this->name = urldecode($answer['files'][$fileidx][0]); 213 | } 214 | } 215 | } 216 | } 217 | 218 | $dlstat['eof'] = $this->eof; 219 | $dlstat['started'] = $this->started; 220 | 221 | $this->notifyListener($dlstat); 222 | return $line; 223 | } 224 | 225 | 226 | public function getStreamHeaders($implode = true) { 227 | if (!$this->headers) { 228 | return $implode ? '' : array(); 229 | } 230 | return $implode ? 231 | implode("\r\n", $this->headers) . "\r\n\r\n" : 232 | $this->headers; 233 | } 234 | 235 | protected function disconnect() { 236 | $this->send('STOP'); 237 | fclose($this->conn); 238 | } 239 | 240 | // с какими заголовками клиент запросил поток. их запишем при открытии ссылки от ace 241 | public function setRequestHeaders($headers) { 242 | $this->reqheaders = $headers; 243 | } 244 | 245 | // основной метод получения данных. дергается в цикле. тут работает конечный автомат 246 | // сначала ждем сыслки от ace, потом заголовков, потом только начинаем выдавать данные 247 | public function getStreamChunk($bufSize) { 248 | $this->readsocket(0, 20000); // читаем лог понемногу, сигналы сервера можно отслеживать 249 | 250 | // ссылки на поток нет - ловить нечего 251 | if (!$this->link) { 252 | return; 253 | } 254 | 255 | // далее открываем ссылку, пытаемся прочитать заголовки 256 | if (!$this->resource) { 257 | $chunk = $this->initiateStream($this->link, $this->reqheaders, $bufSize); 258 | } else { 259 | $chunk = $this->readStreamChunk($this->resource, $bufSize); 260 | } 261 | 262 | return $chunk; 263 | } 264 | 265 | // headers - http request headers to open link with 266 | private function initiateStream($link, $headers, $bufSize) { 267 | $this->resource = $this->openStream($link, $headers); 268 | // ну и сразу надо скопировать из потока первую часть данных 269 | // это для режима кина в основном, иначе проблема следующая 270 | // XBMC делает при старте много запросов подряд, при неблокирующем чтении можно просто 271 | // не успеть прочитать и отдать данные, получится пустой ответ (при следующем коннекте предыдущий кикается) 272 | // и перемотка видео работать не будет. 273 | // TODO с этим что то надо сделать! может просто таймаут побольше? секунд 5 например. а то софт вешается бывает 274 | # stream_set_blocking($this->resource, 0); 275 | # stream_set_timeout($this->resource, 5, 0); // не особо действенный способ вышел 276 | $chunk = $this->readStreamChunk($this->resource, $bufSize); 277 | 278 | // теперь переводим поток в неблокирующий режим, он помогает от зависаний в желтом состоянии буфера 279 | stream_set_blocking($this->resource, 0); // чет картинка сыпется. но похоже не из-за этого 280 | stream_set_timeout($this->resource, 0, 20000); // неизвестно, работает или нет 281 | 282 | return $chunk; 283 | } 284 | 285 | protected function readStreamChunk($res, $bufferSize) { 286 | $tmp = ''; 287 | 288 | if ($this->state == self::STATE_CHUNKWAIT) { 289 | // $tmp = fgets($res); 290 | $tmp = stream_get_line($res, 16, "\r\n"); 291 | $len = trim($tmp); 292 | if (!$len) { 293 | return ''; 294 | } 295 | if (!preg_match('~^[0-9a-f]{1,8}$~', $len)) { 296 | throw new Exception('Chunk read failed "' . json_encode($len) . '"'); 297 | } 298 | else { 299 | // +2 на \r\n, длина которых в чанке не учитывается 300 | $this->currentChunkSize = hexdec($len) + 2; 301 | } 302 | $this->state = self::STATE_CHUNKREAD; 303 | } 304 | if ($this->state == self::STATE_CHUNKREAD or $this->state == self::STATE_CHUNKWAIT) { 305 | $bufferSize = $bufferSize > $this->currentChunkSize ? $this->currentChunkSize : $bufferSize; 306 | } 307 | 308 | // замена fread на stream_socket_recvfrom решила проблему тормозов при просмотре torrent-файлов! 309 | // потому как fread читает только по 8192 байт, хз как увеличить. второй параметр не работает 310 | // stream_socket_recvfrom не работает как надо. 253871 - первая длина буфера после 3ffa0, бывало и 264к вместо 262к 311 | // зато с fread функция чтения chunked работает. fgets вообще не але 312 | // новая напасть: при запросе конца файла, stream_get_line выдает пустую $data, 313 | // если читать осталось меньше, чем размер буфера! 314 | #$data = stream_get_line($res, $bufferSize); 315 | // только stream_socket_recvfrom отработала как нужно! 316 | // Значит для режима live юзаем stream_get_line, а для кина - stream_socket_recvfrom 317 | // а лучше так, если get_line ничего не дало, попробуем recvfrom 318 | // upd: не работает!! попытка использовать stream_socket_recvfrom сразу после stream_get_line 319 | // не дает данных на выходе. т.е. вариант - только раздельное использование 320 | // но есть подозрение, что для кина это иногда становится причиной вывода только звука без видео 321 | if ($this->isLive()) { 322 | $data = stream_get_line($res, $bufferSize); 323 | } 324 | else { 325 | $data = stream_socket_recvfrom($res, $bufferSize); 326 | } 327 | $datalen = strlen($data); 328 | // error_log('got stream ' . $datalen . ' bytes'); 329 | 330 | // контролируем, весь ли буфер прочитан 331 | if ($this->state == self::STATE_CHUNKREAD) { 332 | if ($datalen < $this->currentChunkSize) { 333 | $this->currentChunkSize -= $datalen; 334 | } 335 | else { 336 | $this->state = self::STATE_CHUNKWAIT; 337 | $data = substr($data, 0, -2); // откусываем последние \r\n 338 | } 339 | } 340 | 341 | return $data; 342 | } 343 | 344 | protected function readStreamHeaders($res) { 345 | $headers = array(); 346 | $this->isChunked = false; // лишняя строка 347 | while ($line = trim(fgets($res))) { 348 | if (is_null($this->state)) { // только начали, первый шаг 349 | if (strpos($line, 'HTTP/1.') === false) { 350 | throw new Exception('HTTP header expected. Got ' . $line); 351 | } 352 | $this->state = self::STATE_HDRREAD; 353 | } 354 | if (strpos($line, ':') !== false) { 355 | list ($name, $value) = array_map('trim', explode(':', $line)); 356 | if ($name == 'Transfer-Encoding' and $value == 'chunked') { 357 | $this->isChunked = true; 358 | } 359 | // вот этот хедер здорово мешал, по сути препятствовал запуску потока 360 | // я же chunked-поток разбираю (кстати можно вернуть этот хедер и не заниматься разбором) 361 | // а раз уж разбираю, то и хедер естессно надо убирать 362 | if (strpos($line, 'Trans') !== false) { 363 | continue; 364 | } 365 | } 366 | // обработка ошибок. бывает и такое 367 | // "HTTP/1.1 500 Internal Server Error", "Content-Type: text/plain", "Content-Length: 45" 368 | if (strpos($line, '500 Internal') !== false) { 369 | $this->state = self::STATE_ERROR; 370 | } 371 | if (strpos($line, 'Connection:') !== false) { 372 | //$line = 'Connection: close'; 373 | } 374 | if ($line == 'Content-Type: None') { 375 | // error_log('Content-Type = None, rewrite to video/x-msvideo'); 376 | // $line = 'Content-Type: video/x-msvideo'; 377 | } 378 | $headers[] = $line; 379 | } 380 | 381 | // если ответ был ошибкой - прочитаем ее содержание и кинем исключение 382 | if ($this->state == self::STATE_ERROR) { 383 | $err = fgets($res); 384 | throw new Exception('Headers contains error: ' . $err); 385 | } 386 | 387 | // устанавливается состояние Потоковая передача. 388 | $this->state = $this->isChunked ? self::STATE_CHUNKWAIT : self::STATE_HDRSENT; 389 | return $headers; 390 | } 391 | 392 | /** 393 | * значит так. для режима Live открываем поток 1 раз, остальным клиентам выдаем 394 | * заголовки $this->headers от потока 395 | * для режима кина предполагаем, что клиент один (иначе перемотка работать не будет) 396 | * соответственно, каждый раз закрываем поток и открываем снова, 397 | * передавая последние заголовки клиента в поток, и выдавая ему ответные от потока 398 | 399 | * надо бы для начала написать простейший скрипт, выступающий как веб-прокси, 400 | * сервящий один видеофайл с поддержкой перемотки, а там и видно будет 401 | * проблема, что ace-поток позволяет только 1 коннект за раз, а некоторые плееры 402 | * пробивают разными значениями Range в несколько потоков. VLC вон вообще жестит 403 | * скрипт написан: serve_video_test.php 404 | 405 | * план работы с выдачей кина: 406 | * предыстория: XBMC делает около 4-6 запросов для старта видео. 407 | * сначала HEAD запрос, чтобы получить опции сервера и длину контента 408 | * коннект закрывается сразу после ответа 409 | * думаю на поддержку перемотки результат ответа на HEAD особо не влияет 410 | * Далее - запрос #1 GET с range 0- (т.е. файл целиком, но этот поток будет сброшен) 411 | * затем #2 немного с конца файла range [многобайт]- (около 1Мб до конца файла), #1 активен 412 | * затем сброс #1 и запрос #3 "почти сначала" range 4108-, #2 еще активен 413 | * затем сброс #2 и запрос #4 опять с конца [многобайт]-, почти с того же места, #3 активен 414 | * сброс #3, активен только #4 (чтение хвоста) 415 | * закончено чтение хвоста, открыт новый коннект #5 "почти сначала", bytes 4108-, 416 | * далее весь файл сливается по последнему коннекту #5 417 | * поскольку вся эта канитель происходит очень быстро, в первые секунды, 418 | * и данных на каждый такой запрос в итоге передается немного, то можно установить размер 419 | * буфера чтения ace-потока где-нить в 256-512кБ 420 | * итак, видно, что кроме того, что делаются несколько запросов с разных концов потока, 421 | * они еще и параллельные, хорошо, что не все сразу, а только 2 за раз. 422 | * вероятный план: 423 | * клиент подключился (запрос #1), открываем ресурс (ace-поток), пихаем туда заголовки, читаем ответные 424 | * после, читаем один буфер, пишем на клиента. а точнее надо usleep немного повышенный поставить 425 | * и читать поток пока он есть. логика будет ясна позже 426 | * клиент делает запрос #2. первый еще активен, его, наверное, попробуем закрыть принудительно. 427 | * если будет брыкаться, заморим голодом, т.е. писать данные на него не будем, сам отвалится. 428 | * данные пишутся только на последний коннект. 429 | * для каждого нового коннекта ресурс закрывается, если открыт, и открывается заново с заголовками 430 | * последнего коннекта, там будет нужный Range: bytes=NNN-. 431 | * Это должно работать, т.к. на каждый из серии начальных коннектов приходится совсем немного данных. 432 | * клиент делает запрос #3, сбрасывая #1. #2 еще активен, туда записан минимум 1 буфер, но тут.. 433 | * ..клиент сбрасывает #2 и делает #4.. ну и т.д. 434 | * TODO волшебный функционал: для поддержки нескольких клиентов на 1 фильм, да еще и с 435 | * индивидуальной перемоткой для каждого, можно быстро метаться между кусками ресурса, 436 | * считывая на клиент, пока тот не подавится 437 | * ДЕЛАЕМ! 438 | */ 439 | protected function openStream($link, $headers) { 440 | // нужно поправить заголовки, воткнуть туда ссылку к ace, вместо клиентского запроса к проксе 441 | // GET /trid/407 HTTP/1.1 надо отрезать и заменить другой ссылкой 442 | $parsed = parse_url($link); 443 | $get = sprintf('GET %s HTTP/1.1', $parsed['path']); 444 | $headers = explode("\r\n", $headers); 445 | array_shift($headers); // снимаем GET с шапки массива 446 | array_shift($headers); // снимаем Host с шапки массива 447 | 448 | // поищем заголовок range, если задан оффсет 449 | if ($this->seek) { 450 | foreach ($headers as $idx => $line) { 451 | if (stripos($line, 'range') === 0) { 452 | unset($headers[$idx]); 453 | break; 454 | } 455 | } 456 | array_unshift($headers, 'Range: bytes=' . $this->seek . '-'); 457 | } 458 | array_unshift($headers, 'Host: 127.0.0.1:6878'); // кладем сверху свой Host 459 | array_unshift($headers, $get); // кладем сверху свой GET 460 | 461 | // пытался сделать trim() до explode, а сюда добавить \r\n, зависает софт. че за ХХ 462 | $headers = implode("\r\n", $headers); 463 | 464 | // готовы открывать коннект к видеоданным 465 | $res = sprintf('tcp://%s:%d', $parsed['host'], $parsed['port']); 466 | $link_src = stream_socket_client($res, $errno, $errstr, $tmout = 1); 467 | if (!$link_src) { 468 | throw new Exception('Failed to open stream link'); 469 | } 470 | // пишем заголовки запроса 471 | // error_log('open stream request ' . $headers); 472 | // почему использована именно эта функция? при записи нужен блокирующий режим 473 | // stream_socket_sendto($link_src, $headers); 474 | fwrite($link_src, $headers); 475 | 476 | // теперь ждем заголовков ответа, сохраним их отдельно 477 | // при этом режим дб блокирующий, иначе нам не успеют ответить 478 | // TODO замерить время ожидания 479 | $this->headers = $this->readStreamHeaders($link_src); 480 | error_log('open stream response ' . json_encode($this->headers)); 481 | 482 | // флаг started устанавливается из false в true один раз - при прочтении заголовков по ссылке 483 | if (!$this->started) { // хедеры прочитаны и данные пошли 484 | $this->started = true; 485 | } 486 | 487 | $this->notifyListener(array('headers' => $this->getStreamHeaders(true))); 488 | return $link_src; 489 | } 490 | 491 | // перемотка 492 | public function seek($offsetBytes) { 493 | $this->seek = $offsetBytes; 494 | // хитрый ход. закрываем ресурс, ставим метку оффсета, при запросе данных поток откроется с заданного места 495 | is_resource($this->resource) and fclose($this->resource); 496 | $this->resource = null; 497 | $this->headers = array(); 498 | // из-за несброшенной ссылки с концами вешался весь софт 499 | // XBMC, как известно, делает несколько запросов для поддержки перемотки. 500 | // на каждый следующий запрос Ace выдавал немного другую ссылку. infohash был тот же, но rand() другой 501 | // http://127.0.0.1:6878/content/6aa8418581a92db20cf588aa0f651cdd7a7834a8/0.370222724338 502 | // менялась последняя часть 0.370... 503 | // и при следующем вызове initiateStream() открывалась старая ссылка и ожидались данные, а их нет 504 | // и быть не будет, а режим там блокирующий.. вот и виселица 505 | // ХЕР, все вообще не так было, я зря новый START отправлял на Ace Server 506 | # $this->link = null; 507 | } 508 | 509 | 510 | 511 | 512 | // НИЖЕ НЕРАЗОБРАННЫЙ ШЛАК 513 | protected $restarting = 0; // счетчик с обратным отсчетом, выставляется когда поток пытается перезапуститься 514 | 515 | public function isRestarting() { 516 | return $this->restarting > 0; 517 | } 518 | /* 519 | // эта ветка срабатывает только при запуске потока, по идее на этот момент только 1 клиент в массиве 520 | if ($this->state == self::STATE_STARTED) { 521 | $this->resource = $this->openStream($this->cur_link); 522 | } 523 | 524 | if ($this->isRestarting()) { 525 | $this->restart(); // дальнейшие попытки 526 | } 527 | try { 528 | // проверяем, жив ли сокет до ace server, он мог упасть. метод годный, быстрый 529 | if (!is_null($this->cur_conn)) { 530 | $this->cur_conn->ping(); 531 | $this->cur_conn->readsocket(0, 20000); // читаем лог понемногу, сигналы сервера можно отслеживать 532 | } 533 | } 534 | catch (Exception $e) { 535 | $msg = $e->getMessage(); 536 | if ($msg == 'ace_connection_broken' or strpos($msg, 'Cannot connect') !== false) { 537 | // ace бывает падает, надо попробовать перезапустить 538 | // если не получится, будет исключение и поток остановится 539 | $this->restart(); // первая попытка 540 | return; 541 | } 542 | } 543 | 544 | 545 | // такие 2 метода есть выше 546 | public function close() { 547 | return $this->isActive() and fclose($this->resource); 548 | } 549 | public function isActive() { 550 | return is_resource($this->resource); 551 | } 552 | // найти, что использует 553 | private function isActive() { 554 | return $this->state !== 0; 555 | } 556 | 557 | protected function restart() { 558 | usleep(250000); 559 | if (!$this->isRestarting()) { // первая попытка 560 | $this->notify('Ace connect broken. Restarting', 'warning'); 561 | // закрываем поток видео и коннект к ace, но оставляем всех клиентов активными. потом запускаем заново 562 | $this->cur_conn->close(); 563 | $this->ace->stoppid($this->cur_pid); 564 | $this->restarting = self::RESTART_COUNT; 565 | $this->resource = null; 566 | } 567 | 568 | // очень плохое решение. ace перезапускается не сразу, сек через 5. делаем N попыток с интервалом в 0.25секунды 569 | // а остальное приложение все это время ждет.. gui не обновляется 570 | $pid = $this->cur_pid; 571 | $name = $this->cur_name; 572 | try { 573 | $res = $this->start($pid, $name, false); 574 | $this->restarting = 0; 575 | return $res; 576 | } 577 | catch (Exception $e) { 578 | if (strpos($e->getMessage(), 'Cannot connect') === false) { 579 | $this->restarting = 0; 580 | throw $e; // не наш случай. мы ждем ошибки коннекта 581 | } 582 | } 583 | $this->restarting--; 584 | 585 | if ($this->restarting == 0) { // так и не дождались 586 | error_log('Ace Server not reachable'); 587 | throw $e; 588 | } 589 | } 590 | 591 | */ 592 | 593 | 594 | } 595 | 596 | 597 | 598 | 599 | 600 | -------------------------------------------------------------------------------- /res/modules/class.lib.acemgr.php: -------------------------------------------------------------------------------- 1 | connect 17 | protected $host = '127.0.0.1'; 18 | protected $port = 62062; 19 | 20 | static public function getInstance($key) { 21 | if (!self::$instance) { 22 | self::$instance = new AceConnect($key); 23 | } 24 | return self::$instance; 25 | } 26 | 27 | private function __construct($key = null) { 28 | $this->key = $key ? $key : $this->key; 29 | } 30 | 31 | // для каждой трансляции новый коннект к Ace 32 | public function getConnection($pid) { 33 | if (!isset($this->conn[$pid])) { 34 | $this->conn[$pid] = $this->connect($this->key, $this->host, $this->port, $pid); 35 | } 36 | return $this->conn[$pid]; 37 | } 38 | 39 | protected function connect($key, $host, $port, $pid) { 40 | $tmout = 1; // seconds 41 | $conn = @stream_socket_client(sprintf('tcp://%s:%d', $host, $port), $errno, $errstr, $tmout); 42 | if (!$conn) { 43 | throw new Exception('Cannot connect to AceServer. ' . $errstr, $errno); 44 | } 45 | # stream_set_blocking($conn, 0); // с этим херня полная 46 | stream_set_timeout($conn, 1, 0); // нужно ли 47 | $conn = new AceConn($conn, $pid, $this); 48 | try { 49 | $res = $conn->auth($key); 50 | } catch (CoreException $e) { 51 | if ($e->getCode() == CoreException::EXC_CONN_FAIL) { 52 | // acestream hellobg not answered 53 | $this->restartAceServer(); 54 | } 55 | throw $e; 56 | } 57 | return $conn; 58 | } 59 | 60 | public function _closeConn($pid) { 61 | if (!isset($this->conn[$pid])) { // O_o 62 | return false; 63 | } 64 | // $this->conn[$pid]->close(); // закроется через destruct 65 | unset($this->conn[$pid]); 66 | return; 67 | } 68 | 69 | public function restartAceServer() { 70 | $cmd = 'killall acestreamengine'; 71 | return `$cmd`; 72 | } 73 | 74 | 75 | 76 | // TODO сильно рефакторить 77 | public function startraw($pid, $fileidx = 0) { 78 | $conn = $this->getConnection($pid . $fileidx); 79 | if (!$conn->isAuthorized()) { 80 | throw new Exception('Ace connection not authorized'); 81 | } 82 | 83 | // вероятно $pid это имя торрент файла, поищем в папке /STORAGE/FILES 84 | // оно может быть и урлом 85 | $url = parse_url($pid); 86 | if (!isset($url['scheme'])) { // видимо файл 87 | if (!is_file($file = ('/STORAGE/FILES/' . $pid))) { 88 | throw new Exception('Torrent file not found ' . $file); 89 | } 90 | } else { 91 | $file = $pid; 92 | } 93 | 94 | $base64 = file_get_contents($file); 95 | $base64 = base64_encode($base64); 96 | 97 | $conn->startraw($base64, $fileidx); 98 | return $conn; 99 | } 100 | 101 | public function starttorrent($url, $fileidx = 0) { 102 | $conn = $this->getConnection($url . $fileidx); 103 | if (!$conn->isAuthorized()) { 104 | throw new Exception('Ace connection not authorized'); 105 | } 106 | $conn->starttorrent($url, $fileidx); 107 | return $conn; 108 | } 109 | 110 | public function startpid($pid) { 111 | $conn = $this->getConnection($pid); 112 | if (!$conn->isAuthorized()) { 113 | throw new Exception('Ace connection not authorized'); 114 | } 115 | $conn->startpid($pid); 116 | return $conn; 117 | } 118 | } 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /res/modules/class.lib.bdecode.php: -------------------------------------------------------------------------------- 1 | result['my_value_name'] 28 | * 29 | * -- Here is a list of some of the most used properties: 30 | * $torrent->result['announce'] // string 31 | * $torrent->result['announce-list'] // array 32 | * $torrent->result['comment'] // string 33 | * $torrent->result['created by'] // string 34 | * $torrent->result['creation date'] // unix timestamp 35 | * $torrent->result['encoding'] // string 36 | * $torrent->result['info']['files'] // array 37 | * $torrent->result['info']['files'][?]['length'] // integer 38 | * $torrent->result['info']['files'][?]['path'] // string 39 | * $torrent->result['info']['name'] // string 40 | * $torrent->result['info']['piece length'] // integer 41 | * $torrent->result['info']['pieces'] // string 42 | * $torrent->result['info']['private'] // integer 43 | * $torrent->result['modified-by'] // array 44 | * 45 | * See http://wiki.theory.org/BitTorrentSpecification for bittorrent specification 46 | */ 47 | 48 | 49 | final class BDecode { 50 | private $content; // string containing contents of file 51 | private $pointer = 0; // current position pointer in content 52 | public $result = array(); // result array containing all decoded elements 53 | 54 | 55 | /************************************************************************** 56 | * Info: Parses bencoded file into array. 57 | * Args: {string} filepath: full or relative path to bencoded file 58 | **************************************************************************/ 59 | function __construct($filepath) { 60 | $this->content = @file_get_contents($filepath); 61 | 62 | if (!$this->content) { 63 | $this->throwException('File does not exist!'); 64 | } else { 65 | if (!isset($this->content)) { 66 | $this->throwException('Error opening file!'); 67 | } else { 68 | $this->result = $this->processElement(); 69 | } 70 | } 71 | unset($this->content); 72 | } 73 | 74 | public function getInfoHash() { 75 | $enc = new BEncodeLib; 76 | return sha1($enc->bencode($this->result['info'])); 77 | } 78 | 79 | /************************************************************************** 80 | * Info: Clear class variables. 81 | * Args: none 82 | **************************************************************************/ 83 | function __destruct() { 84 | unset($this->content); 85 | unset($this->result); 86 | } 87 | 88 | 89 | /************************************************************************** 90 | * Info: Terminates decoding process and returns error. 91 | * Args: {string} error [optional] - error description 92 | **************************************************************************/ 93 | private function throwException($error = 'error parsing file') { 94 | $this->result = array(); 95 | $this->result['error'] = $error; 96 | } 97 | 98 | /************************************************************************** 99 | * Info: Processes element depending on its type. 100 | * Results in error if no valid identifier is found. 101 | * Args: none 102 | **************************************************************************/ 103 | private function processElement() { 104 | switch($this->content[$this->pointer]) { 105 | case 'd': 106 | return $this->processDictionary(); 107 | break; 108 | case 'l': 109 | return $this->processList(); 110 | break; 111 | case 'i': 112 | return $this->processInteger(); 113 | break; 114 | default: 115 | if (is_numeric($this->content[$this->pointer])) { 116 | return $this->processString(); 117 | } else { 118 | $this->throwException('Unknown BEncode element'); 119 | } 120 | break; 121 | } 122 | } 123 | 124 | /************************************************************************** 125 | * Info: Processes dictionary entries. 126 | * Returns array of dictionary entries. 127 | * Args: none 128 | **************************************************************************/ 129 | private function processDictionary() { 130 | if (!$this->isOfType('d')) 131 | $this->throwException(); 132 | 133 | $res = array(); 134 | $this->pointer++; 135 | 136 | while (!$this->isOfType('e')) { 137 | $elemkey = $this->processString(); 138 | 139 | switch($this->content[$this->pointer]) { 140 | case 'd': 141 | $res[$elemkey] = $this->processDictionary(); 142 | break; 143 | case 'l': 144 | $res[$elemkey] = $this->processList(); 145 | break; 146 | case 'i': 147 | $res[$elemkey] = $this->processInteger(); 148 | break; 149 | default: 150 | if (is_numeric($this->content[$this->pointer])) { 151 | $res[$elemkey] = $this->processString(); 152 | } else { 153 | $this->throwException('Unknown BEncode element!'); 154 | } 155 | break; 156 | } 157 | } 158 | 159 | $this->pointer++; 160 | return $res; 161 | } 162 | 163 | /************************************************************************** 164 | * Info: Processes list entries. 165 | * Returns array of list entries found between 'l' and 'e' identifiers. 166 | * Args: none 167 | **************************************************************************/ 168 | private function processList() { 169 | if (!$this->isOfType('l')) 170 | $this->throwException(); 171 | 172 | $res = array(); 173 | $this->pointer++; 174 | 175 | while (!$this->isOfType('e')) 176 | $res[] = $this->processElement(); 177 | 178 | $this->pointer++; 179 | return $res; 180 | } 181 | 182 | /************************************************************************** 183 | * Info: Processes integer value. 184 | * Returns integer value found between 'i' and 'e' identifiers. 185 | * Args: none 186 | **************************************************************************/ 187 | private function processInteger() { 188 | if (!$this->isOfType('e')) 189 | $this->throwException(); 190 | 191 | $this->pointer++; 192 | 193 | $delim_pos = strpos($this->content, 'e', $this->pointer); 194 | $integer = substr($this->content, $this->pointer, $delim_pos - $this->pointer); 195 | if (($integer == '-0') || ((substr($integer, 0, 1) == '0') && (strlen($integer) > 1))) 196 | $this->throwException(); 197 | 198 | $integer = abs(floatval($integer)); 199 | $this->pointer = $delim_pos + 1; 200 | return $integer; 201 | } 202 | 203 | /************************************************************************** 204 | * Info: Processes string value. 205 | * Returns string value found after '%:' identifier, where '%' is any 206 | * valid integer. 207 | * Args: none 208 | **************************************************************************/ 209 | private function processString() { 210 | if (!is_numeric($this->content[$this->pointer])) { 211 | $this->throwException(); 212 | } 213 | 214 | $delim_pos = strpos($this->content, ':', $this->pointer); 215 | $elem_len = intval(substr($this->content, $this->pointer, $delim_pos - $this->pointer)); 216 | $this->pointer = $delim_pos + 1; 217 | 218 | $elem_name = substr($this->content, $this->pointer, $elem_len); 219 | 220 | $this->pointer += $elem_len; 221 | return $elem_name; 222 | } 223 | 224 | /************************************************************************** 225 | * Info: Checks if identifier at current pointer is of supplied type. 226 | * Args: {char} type - character denoting required type. 227 | * Usually one of [d,l,i,e]. 228 | **************************************************************************/ 229 | private function isOfType($type) { 230 | return ($this->content[$this->pointer] == $type); 231 | } 232 | } 233 | ?> 234 | -------------------------------------------------------------------------------- /res/modules/class.lib.bencode.php: -------------------------------------------------------------------------------- 1 | bdecode_rec($x, $f); 35 | array_push($r,$v['r']); 36 | $f = $v['l']; 37 | } 38 | $result['r'] = $r; 39 | $result['l'] = $f + 1; 40 | return $result; 41 | } 42 | 43 | public function decode_dict(&$x, $f) 44 | { 45 | $r = array(); 46 | while($x[$f] != 'e') 47 | { 48 | $k = $this->decode_string($x, $f); 49 | $f = $k['l']; 50 | $v = $this->bdecode_rec($x, $f); 51 | $r[$k['r']] = $v['r']; 52 | $f = $v['l']; 53 | } 54 | $result['r'] = $r; 55 | $result['l'] = $f + 1; 56 | return $result; 57 | } 58 | 59 | public function bdecode_rec(&$x, $f) 60 | { 61 | $t = $x[$f]; 62 | if ($t == 'i') 63 | return $this->decode_int($x, $f + 1); 64 | elseif ($t == 'l') 65 | return $this->decode_list($x, $f + 1); 66 | elseif ($t == 'd') 67 | return $this->decode_dict($x, $f + 1); 68 | else 69 | return $this->decode_string($x, $f); 70 | } 71 | 72 | public function bdecode($x) 73 | { 74 | $result = $this->bdecode_rec($x, 0); 75 | return $result['r']; 76 | } 77 | 78 | public function bencode_rec($x, &$b) 79 | { 80 | if (is_numeric($x)) 81 | $b .= 'i'.round($x).'e'; 82 | elseif (is_string($x)) 83 | $b .= strlen($x).':'.$x; 84 | elseif (is_array($x)) 85 | { 86 | // Unlike Python, PHP does not have a "tuple", "list" or "dict" type 87 | // This code assumes arrays with purely integer indexes are lists, 88 | // arrays which use string indexes assumed to be dictionaries. 89 | $keys = array_keys($x); 90 | $listtype = true; 91 | while(list($k,$v) = each($keys)) 92 | if (!is_integer($v)) $listtype = false; 93 | if ($listtype) 94 | { 95 | // List 96 | $b .= 'l'; 97 | while(list($k,$v) = each($x)) 98 | $this->bencode_rec($v, $b); 99 | $b .= 'e'; 100 | } 101 | else 102 | { 103 | // Dictionary 104 | $b .= 'd'; 105 | ksort($x); 106 | while(list($k,$v) = each($x)) 107 | { 108 | settype($k,"string"); 109 | $this->bencode_rec($k, $b); 110 | $this->bencode_rec($v, $b); 111 | } 112 | $b .= 'e'; 113 | } 114 | } 115 | } 116 | 117 | public function bencode($x) 118 | { 119 | $b = ''; 120 | $this->bencode_rec($x, $b); 121 | return $b; 122 | } 123 | } 124 | ?> -------------------------------------------------------------------------------- /res/modules/class.lib.file.php: -------------------------------------------------------------------------------- 1 | file = $file; 14 | # error_log('construct file ' . spl_object_hash($this)); 15 | } 16 | public function __destruct() { 17 | # error_log(' destruct file ' . spl_object_hash($this)); 18 | } 19 | 20 | // TODO это рефакторить! циклические ссылки на StreamUnit 21 | public function registerEventListener($cb) { 22 | $this->listener = $cb; 23 | } 24 | protected function notifyListener($event) { 25 | is_callable($this->listener) and call_user_func_array($this->listener, array($event)); 26 | } 27 | public function isRestarting() { 28 | return false; 29 | } 30 | public function open() { 31 | // открываем ссылку, пытаемся прочитать заголовки 32 | $this->fp = fopen($this->file, 'r'); 33 | $this->notifyListener(array( 34 | 'headers' => $this->getStreamHeaders(), 35 | 'started' => true 36 | )); 37 | } 38 | public function close() { 39 | return is_resource($this->fp) and fclose($this->fp); 40 | } 41 | public function getName() { 42 | $name = str_replace(realpath(__DIR__ . '/../../') . '/', '', $this->file); 43 | return $name; 44 | } 45 | public function isLive() { 46 | return false; 47 | } 48 | public function getStreamHeaders($implode = true) { 49 | $mimetype = mime_content_type($this->file); 50 | 51 | strpos($this->file, '.mp4') and $mimetype = 'video/x-msvideo'; 52 | strpos($this->file, '.ts') and $mimetype = 'video/MP2T'; 53 | 54 | $headers = array( 55 | 'HTTP/1.0 200 OK', 56 | 'Content-Type: ' . $mimetype, 57 | 'Content-Length: ' . filesize($this->file), 58 | ); 59 | return $implode ? 60 | implode("\r\n", $headers) . "\r\n\r\n" : 61 | $headers; 62 | } 63 | 64 | public function seek($offsetBytes) {} 65 | 66 | public function getStreamChunk($bufSize) { 67 | if (!is_resource($this->fp)) { 68 | return false; 69 | } 70 | $data = fread($this->fp, $bufSize); // с этим работало 71 | 72 | if (!$data) { 73 | $this->notifyListener(array('eof' => true)); 74 | return ; 75 | } 76 | 77 | return $data; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /res/modules/class.lib.ws.php: -------------------------------------------------------------------------------- 1 | key = $key; 13 | $this->client = $client; 14 | // error_log('construct ws ' . spl_object_hash($this)); 15 | } 16 | public function __destruct() { 17 | // error_log(' destruct ws ' . spl_object_hash($this)); 18 | } 19 | 20 | // TODO это рефакторить! циклические ссылки на StreamUnit 21 | public function registerEventListener($cb) { 22 | $this->listener = $cb; 23 | } 24 | protected function notifyListener($event) { 25 | is_callable($this->listener) and call_user_func_array($this->listener, array($event)); 26 | } 27 | public function isRestarting() { 28 | return false; 29 | } 30 | 31 | // по сути и открывать-то нечего 32 | public function open() { 33 | // проблема в том, что когда в конструктор StreamUnit передается этот объект ws и 34 | // StreamUnit сразу в конструкторе вызывает open() и мы тут же уведомляем ОК и отдаем хедеры 35 | // - клиентов еще нет. клиент ассоциируется чуть позже, и увведомление он просирает 36 | $this->notifyListener(array( 37 | 'headers' => $this->getStreamHeaders(), 38 | 'started' => true 39 | )); 40 | } 41 | // и закрывать тоже 42 | public function close() { 43 | $this->notifyListener(array('eof' => true)); 44 | return ; 45 | } 46 | public function getName() { 47 | return sprintf('WebSocket %s', $this->key); 48 | } 49 | public function isLive() { 50 | return false; 51 | } 52 | public function getStreamHeaders() { 53 | return $this->handshake($this->key); 54 | } 55 | 56 | public function seek($offsetBytes) { 57 | } 58 | 59 | // снимаем с массива 1 элемент и выдаем 60 | public function getStreamChunk($bufSize) { 61 | $data = array_shift($this->clientData); 62 | return $data ? $this->encode($data) : null; 63 | } 64 | 65 | // кладем в массив элемент 66 | public function put($data) { 67 | if ($this->client->isFinished()) { 68 | // клиент отвалился, расходимся 69 | $this->notifyListener(array('eof' => true)); 70 | return; 71 | } 72 | $this->clientData[] = json_encode($data); 73 | } 74 | 75 | // used code http://petukhovsky.com/simple-web-socket-on-php-from-very-start/ 76 | private function handshake($key) { 77 | //отправляем заголовок согласно протоколу вебсокета 78 | $SecWebSocketAccept = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); 79 | $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . 80 | "Upgrade: websocket\r\n" . 81 | "Connection: Upgrade\r\n" . 82 | "Sec-WebSocket-Accept:".$SecWebSocketAccept."\r\n\r\n"; 83 | return $upgrade; 84 | } 85 | 86 | private function encode($payload, $type = 'text', $masked = false) { 87 | $frameHead = array(); 88 | $payloadLength = strlen($payload); 89 | 90 | switch ($type) { 91 | case 'text': 92 | // first byte indicates FIN, Text-Frame (10000001): 93 | $frameHead[0] = 129; 94 | break; 95 | 96 | case 'close': 97 | // first byte indicates FIN, Close Frame(10001000): 98 | $frameHead[0] = 136; 99 | break; 100 | 101 | case 'ping': 102 | // first byte indicates FIN, Ping frame (10001001): 103 | $frameHead[0] = 137; 104 | break; 105 | 106 | case 'pong': 107 | // first byte indicates FIN, Pong frame (10001010): 108 | $frameHead[0] = 138; 109 | break; 110 | } 111 | 112 | // set mask and payload length (using 1, 3 or 9 bytes) 113 | if ($payloadLength > 65535) { 114 | $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); 115 | $frameHead[1] = ($masked === true) ? 255 : 127; 116 | for ($i = 0; $i < 8; $i++) { 117 | $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); 118 | } 119 | // most significant bit MUST be 0 120 | if ($frameHead[2] > 127) { 121 | return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)'); 122 | } 123 | } elseif ($payloadLength > 125) { 124 | $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); 125 | $frameHead[1] = ($masked === true) ? 254 : 126; 126 | $frameHead[2] = bindec($payloadLengthBin[0]); 127 | $frameHead[3] = bindec($payloadLengthBin[1]); 128 | } else { 129 | $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; 130 | } 131 | 132 | // convert frame-head to string: 133 | foreach (array_keys($frameHead) as $i) { 134 | $frameHead[$i] = chr($frameHead[$i]); 135 | } 136 | if ($masked === true) { 137 | // generate a random mask: 138 | $mask = array(); 139 | for ($i = 0; $i < 4; $i++) { 140 | $mask[$i] = chr(rand(0, 255)); 141 | } 142 | 143 | $frameHead = array_merge($frameHead, $mask); 144 | } 145 | $frame = implode('', $frameHead); 146 | 147 | // append payload to frame: 148 | for ($i = 0; $i < $payloadLength; $i++) { 149 | $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; 150 | } 151 | 152 | return $frame; 153 | } 154 | // TODO сделать private 155 | public function decode($data) { 156 | $unmaskedPayload = ''; 157 | $decodedData = array(); 158 | 159 | // estimate frame type: 160 | $firstByteBinary = sprintf('%08b', ord($data[0])); 161 | $secondByteBinary = sprintf('%08b', ord($data[1])); 162 | $opcode = bindec(substr($firstByteBinary, 4, 4)); 163 | $isMasked = ($secondByteBinary[0] == '1') ? true : false; 164 | $payloadLength = ord($data[1]) & 127; 165 | 166 | // unmasked frame is received: 167 | if (!$isMasked) { 168 | return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)'); 169 | } 170 | 171 | switch ($opcode) { 172 | // text frame: 173 | case 1: 174 | $decodedData['type'] = 'text'; 175 | break; 176 | 177 | case 2: 178 | $decodedData['type'] = 'binary'; 179 | break; 180 | 181 | // connection close frame: 182 | case 8: 183 | $decodedData['type'] = 'close'; 184 | break; 185 | 186 | // ping frame: 187 | case 9: 188 | $decodedData['type'] = 'ping'; 189 | break; 190 | 191 | // pong frame: 192 | case 10: 193 | $decodedData['type'] = 'pong'; 194 | break; 195 | 196 | default: 197 | return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)'); 198 | } 199 | 200 | if ($payloadLength === 126) { 201 | $mask = substr($data, 4, 4); 202 | $payloadOffset = 8; 203 | $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; 204 | } elseif ($payloadLength === 127) { 205 | $mask = substr($data, 10, 4); 206 | $payloadOffset = 14; 207 | $tmp = ''; 208 | for ($i = 0; $i < 8; $i++) { 209 | $tmp .= sprintf('%08b', ord($data[$i + 2])); 210 | } 211 | $dataLength = bindec($tmp) + $payloadOffset; 212 | unset($tmp); 213 | } else { 214 | $mask = substr($data, 2, 4); 215 | $payloadOffset = 6; 216 | $dataLength = $payloadLength + $payloadOffset; 217 | } 218 | 219 | /** 220 | * We have to check for large frames here. socket_recv cuts at 1024 bytes 221 | * so if websocket-frame is > 1024 bytes we have to wait until whole 222 | * data is transferd. 223 | */ 224 | if (strlen($data) < $dataLength) { 225 | return false; 226 | } 227 | 228 | if ($isMasked) { 229 | for ($i = $payloadOffset; $i < $dataLength; $i++) { 230 | $j = $i - $payloadOffset; 231 | if (isset($data[$i])) { 232 | $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; 233 | } 234 | } 235 | $decodedData['payload'] = $unmaskedPayload; 236 | } else { 237 | $payloadOffset = $payloadOffset - 4; 238 | $decodedData['payload'] = substr($data, $payloadOffset); 239 | } 240 | 241 | return $decodedData; 242 | } 243 | } 244 | 245 | -------------------------------------------------------------------------------- /res/modules/class.plugin.torrent.php: -------------------------------------------------------------------------------- 1 | ace = AceConnect::getInstance($this->acestreamkey); 18 | } 19 | 20 | // косяк был такой! при старте видео XBMC, как известно, делает несколько разных коннектов 21 | // и тут на каждый запрос вызывается startraw! что хреново! т.к. поток уже запущен и 22 | // надо просто переоткрыть ссылку от ace с новым Range: bytes= 23 | // а я думаю, что-то в логе не то, START http:// лишние лезут откуда-то 24 | public function process(ClientRequest $req) { 25 | // для пробивочного запроса выдаем заголовки и закрываем коннект 26 | if ($req->getReqType() == 'HEAD' or ($req->isRanged() and $req->isEmptyRanged())) { 27 | return $req->response( 28 | 'HTTP/1.1 200 OK' . "\r\n" . 29 | 'Content-Length: 14324133' . "\r\n" . // TODO хедеры от балды, поправить 30 | 'Connection: close' . "\r\n" . 31 | 'Accept-Ranges: bytes' . "\r\n\r\n" 32 | ); 33 | } 34 | 35 | // определяем уникальный идентификатор контента 36 | $pid = $req->getType(); 37 | $fileidx = (int) $req->getPid(); // для мультиторрента это индекс видеофайла 38 | 39 | if (substr($req->getUri(), -4) == '.m3u') { 40 | $playlist = $this->playlist($req); 41 | $response = 'HTTP/1.1 200 OK' . "\r\n" . 42 | 'Connection: close' . "\r\n" . 43 | 'Content-Type: text/plain' . "\r\n" . 44 | 'Content-Length: ' . strlen($playlist) . "\r\n" . 45 | 'Accept-Ranges: bytes' . "\r\n\r\n" . 46 | $playlist; 47 | return $req->response($response); 48 | } 49 | 50 | $streamid = $pid; 51 | // для многосерийных торрентов. торрент-файл один, но разные серии надо показывать как разные потоки 52 | if (is_numeric($fileidx)) { 53 | $streamid .= $fileidx; // а вот фиг. 54 | // попытка быстро переключиться на другую серию приводит к уже известной проблеме - 55 | // отсутствию данных из-за открытия одного и того же контента 2 раза, неважно, 56 | // что коннекты к AceStream при этом разные, и даже что ссылка START http выдается - 57 | // данных в ней нет! 58 | // все-таки не фиг: потоки (объекты StreamUnit) должны быть разные, а вышеописанная 59 | // проблема была из-за того, что коннект к Ace был один. подмешал к id коннекта еще и fileidx 60 | } 61 | 62 | $conn = $this->ace->startraw($pid, $fileidx); // TODO refactor, it is NOT name for series 63 | $conn->setRequestHeaders($req->getHeaders()); 64 | return $req->response($conn, $streamid); 65 | } 66 | 67 | 68 | private function playlist($req) { 69 | $playlist = array(); 70 | 71 | $curFile = $req->getType(); 72 | // вторым параметром в ссылке может быть либо playlist.m3u либо мультифайловый торрент 73 | // во втором случае откусываем имя торрент файла и выдаем его содержание как плейлист 74 | if (substr($curFile, -12) == '.torrent.m3u') { 75 | // xbmc не воспринимает содержимое как плейлист без расширения m3u 76 | // может еще удастся поиграть и настроить через хедеры или mime 77 | $curFile = substr($curFile, 0, -4); 78 | } else { 79 | // иначе плейлист формируем из списка торрент-файлов в корне basedir 80 | $curFile = null; 81 | } 82 | 83 | $basedir = $this->basedir; 84 | $hostport = $req->getHttpHost(true); // true - с портом через двоеточие, если тот есть 85 | $lib_loaded = class_exists('BDecode'); 86 | 87 | // это запрос на чтение содержимого торрент-файла 88 | if ($lib_loaded and is_file($path = ($basedir . $curFile))) { 89 | $torrent = new BDecode($path); 90 | $files = $torrent->result['info']['files']; 91 | foreach ($files as $idx => $one) { 92 | $name = implode('/', $one['path']); 93 | // TODO hostname брать из запроса 94 | $playlist[$name] = '#EXTINF:-1,' . $name . "\r\n" . 95 | 'http://' . $hostport . '/torrent/' . $curFile . '/' . $idx . "\r\n"; 96 | } 97 | } else { 98 | $torList = glob($basedir . '*.torrent'); 99 | foreach ($torList as $one) { 100 | $basename = basename($one); 101 | $name = str_replace('.torrent', '', $basename); 102 | 103 | $isMultifiled = false; 104 | // попробуем декодировать торрент и получить некоторое инфо 105 | if ($lib_loaded) { 106 | $torrent = new BDecode($one); 107 | if (isset($torrent->result['info']['name'])) { 108 | $name = $torrent->result['info']['name']; 109 | } 110 | $files = isset($torrent->result['info']['files']) ? 111 | $torrent->result['info']['files'] : array(); 112 | $count = count($files); 113 | foreach ($files as $f) { 114 | // отсеем всякие сопутствующие фильмам файлы 115 | $tmp = implode('/', $f['path']); 116 | if (in_array(substr($tmp, -4), array('.srt', '.ac3'))) { 117 | $count--; 118 | } 119 | } 120 | if ($count > 1) { 121 | $isMultifiled = true; 122 | } 123 | } 124 | 125 | // принимаем решение, запускать файл или выдавать как плейлист 126 | if ($isMultifiled) { 127 | $playlist[$name] = '#EXTINF:-1,' . $name . "\r\n" . 128 | 'http://' . $hostport . '/torrent/' . $basename . '.m3u' . "\r\n"; 129 | } else { 130 | $playlist[$name] = '#EXTINF:-1,' . $name . "\r\n" . 131 | 'http://' . $hostport . '/torrent/' . $basename . "\r\n"; 132 | } 133 | } 134 | } 135 | 136 | ksort($playlist); 137 | $playlist = '#EXTM3U' . "\r\n" . implode("\r\n", $playlist); 138 | 139 | return $playlist; 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /res/modules/class.plugin.ttv.php: -------------------------------------------------------------------------------- 1 | ace = AceConnect::getInstance($this->acestreamkey); 13 | } 14 | 15 | // метод должен вернуть инфу по запросу и объект ответа, если запрос не предполагает запуска потока 16 | public function process(ClientRequest $req) { 17 | // для пробивочного запроса выдаем заголовки и закрываем коннект 18 | if ($req->getReqType() == 'HEAD' or ($req->isRanged() and $req->isEmptyRanged())) { 19 | return $req->response( 20 | 'HTTP/1.1 200 OK' . "\r\n" . 21 | 'Content-Length: 14324133' . "\r\n" . // TODO хедеры от балды, поправить 22 | 'Accept-Ranges: bytes' . "\r\n\r\n" 23 | ); 24 | } 25 | 26 | 27 | $type = $req->getType(); 28 | $pid = $req->getPid(); 29 | $tmp = null; 30 | 31 | // передаем также request headers клиента 32 | switch ($type) { 33 | case 'pid': 34 | $conn = $this->ace->startpid($pid); 35 | break; 36 | case 'trid': 37 | try { 38 | $tmp = $this->parse4PID($pid); 39 | } 40 | catch (Exception $e) { 41 | // рефакторить на нормальные классы, коды ошибок и убрать копипаст parse4PID!! 42 | // error_log($e->getMessage()); 43 | if (stripos($e->getMessage(), 'curl') === 0) { 44 | throw new Exception('Torrent tv timed out'); 45 | } 46 | $this->torrentAuth(); 47 | $tmp = $this->parse4PID($pid); 48 | } 49 | // tmp использовалась, чтобы pid не попортить раньше времени 50 | $pid = $tmp; 51 | case 'acelive': 52 | if ($type == 'acelive') { 53 | $pid = sprintf('http://content.asplaylist.net/cdn/%d_all.acelive', $pid); 54 | } 55 | default: 56 | $conn = $this->ace->starttorrent($pid); 57 | } 58 | $conn->setRequestHeaders($req->getHeaders()); 59 | 60 | // определяем уникальный идентификатор контента 61 | $streamid = $req->getPid(); 62 | return $req->response($conn, $streamid); 63 | } 64 | 65 | 66 | protected function torrentAuth() { 67 | error_log('Authorizing on torrent'); 68 | $url = "http://torrent-tv.ru/auth.php"; 69 | $opts = array( 70 | CURLOPT_POST => true, 71 | CURLOPT_POSTFIELDS => 'email=' . urlencode($this->ttv_login) . '&password=' . 72 | urlencode($this->ttv_psw) . '&enter=' . urlencode('Войти'), 73 | ); 74 | $res = $this->makeRequest($url, $opts); 75 | 76 | if (!preg_match('~"Refresh".*URL="cabinet\.php"~', $res)) { 77 | throw new Exception('Login failed'); 78 | } 79 | return true; 80 | } 81 | protected function parse4PID($trid) { 82 | $url = "http://torrent-tv.ru/torrent-online.php?translation=" . $trid; 83 | $res = $this->makeRequest($url); 84 | $isLoggedIn = preg_match('~/exit\.php~', $res); 85 | // PID на сайте больше нет. http://content.torrent-tv.ru/cdn/31_all.acelive 86 | // this.loadTorrent("http://content.torrent-tv.ru/cdn/31_all.acelive",{autoplay: true}); 87 | $pattern = '~loadPlayer\("([a-f0-9]{40})"~smU'; 88 | $pattern = '~loadTorrent\("([^"]+)",~sm'; 89 | if (!preg_match($pattern, $res, $m)) { 90 | throw new Exception('Stream ID not matched. ' . ($isLoggedIn ? 'Is' : 'Not') . ' logged in'); 91 | } 92 | 93 | return $m[1]; 94 | } 95 | private function makeRequest($url, $addCurlOptions = array(), $tryAntiban = true) { 96 | $rnd = rand(1000, 10000); 97 | $httpHeaders = array( 98 | 'Origin: http://torrent-tv.ru', 99 | 'Referer: http://torrent-tv.ru', 100 | 'User-Agent: Mozilla/5.0' 101 | ); 102 | 103 | // base init 104 | $curl = curl_init(); 105 | curl_setopt_array($curl, array( 106 | CURLOPT_URL => $url, 107 | CURLOPT_VERBOSE => false, 108 | CURLOPT_HEADER => false, 109 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0, 110 | CURLOPT_COOKIEFILE => __DIR__ . '/../cookie_ttv.txt', 111 | CURLOPT_COOKIEJAR => __DIR__ . '/../cookie_ttv.txt', 112 | #CURLOPT_STDERR => fopen('/tmp/ttvcurl_' . $rnd, 'w'), 113 | CURLOPT_HTTPHEADER => $httpHeaders, 114 | CURLOPT_RETURNTRANSFER => true, 115 | CURLOPT_FOLLOWLOCATION => true, 116 | CURLOPT_CONNECTTIMEOUT => 3, 117 | CURLOPT_TIMEOUT => 10 // чем больше тут секунд, тем дольше висит UI при факапе на стороне ttv.ru. форкаться чтоль.. 118 | )); 119 | curl_setopt_array($curl, $addCurlOptions); 120 | $res = curl_exec($curl); 121 | 122 | $isTimeout = ($curlErrno = curl_errno($curl)) == 28; 123 | if ($isTimeout) { 124 | throw new Exception('Curl timeout'); 125 | } else if ($curlErrno !== 0) { 126 | throw new Exception('Curl error ' . $curlErrno); 127 | } 128 | 129 | // torrent-tv внедрил защиту от ботов, но мы ее обойдем 130 | if ($tryAntiban and strpos($res, 'banhammer/pid') !== false) { // защита активировалась 131 | error_log('Enabling antiban...'); 132 | // надо запросить у банхаммера заголовок X-BH-Token 133 | // для чего просто отправляем на /banhammer/pid get-запрос 134 | $addHeader = array(CURLOPT_HEADER => true); 135 | $res = $this->makeRequest('http://torrent-tv.ru/banhammer/pid', $addHeader, false); 136 | // ищем header X-BH-Token со значением вроде IUMjlVubjDlQJZVIctmuLmVuPIU=_22142957038 137 | if (preg_match('~X\-BH\-Token:\s?([^\s]+)[\s$]~smU', $res, $m)) { 138 | $token = $m[1]; 139 | error_log('Found antiban token'); 140 | // нашли токен, повторяем изначальный запрос, но с установкой куки 141 | $addCurlOptions[CURLOPT_COOKIE] = 'BHC=' . urlencode($token); 142 | $res = $this->makeRequest($url, $addCurlOptions, false); 143 | } 144 | else { 145 | throw new Exception('Banhammer crack failed'); 146 | } 147 | } 148 | return $res; 149 | } 150 | } 151 | 152 | -------------------------------------------------------------------------------- /res/modules/class.plugin.websrv.php: -------------------------------------------------------------------------------- 1 | servicedir = realpath($path = __DIR__ . '/../websrv/'); 16 | if (!is_dir($this->servicedir)) { 17 | $this->getApp()->error('Path not exists: ' . $path); 18 | } 19 | } 20 | 21 | 22 | // метод возвращает текст ответа клиенту (с хедерами), а по ссылке массив инфы о потоке/запросе 23 | public function process(ClientRequest $req) { 24 | try { 25 | $filepath = $this->getFilePath($req); 26 | } catch (Exception $e) { 27 | $this->getApp()->error($e->getCode() . ': ' . $req->getUri()); 28 | return $req->response($this->returnCode($e->getCode())); 29 | } 30 | 31 | // если GET - выдаем статичный файл 32 | if ($req->getReqType() == 'GET') { 33 | $tpl = $this->getFilePath($req); 34 | // если файл html или php - подключаем его как шаблон, в нем допустимы php-тэги 35 | $ext = substr($tpl, strrpos($tpl, '.') + 1); 36 | if (in_array($ext, array('html', 'php'))) { 37 | $TPLDATA = $this->getApp()->getUIAdditionalInfo(); 38 | $TPLDATA['ipport'] = sprintf('%s:%s', $req->getServerHost(), $TPLDATA['port']); 39 | $STREAMS = $this->getStreams(); 40 | ob_start(); 41 | include $tpl; // это может быть и 1Мб-ный minified-JS 42 | $tmpfname = tempnam(sys_get_temp_dir(), "AcePHProxyTemp_"); 43 | file_put_contents($tmpfname, ob_get_clean()); 44 | $tpl = $tmpfname; 45 | } 46 | 47 | $streamid = md5($filepath . uniqid()); 48 | return $req->response(new StreamResource_file($tpl), $streamid); 49 | } 50 | 51 | // === TODO ==== implement 52 | $return = ''; 53 | if ($req->getReqType() == 'POST' and strpos($file, '.php') !== false) { // POST запрос к файлу делаем 54 | $rawpost = $req->getContent(); 55 | } 56 | 57 | 58 | error_log($req->getReqType()); 59 | $response = 'HTTP/1.1 200 OK' . "\r\n" . 60 | $return; 61 | error_log('RESPONSE ' . $response); 62 | return $req->response($response); 63 | } 64 | 65 | 66 | 67 | private function getFilePath(ClientRequest $req) { 68 | $file = $req->getUri(); // полный путь, начиная с /websrv, раз уж обрабатывается этим плагином 69 | // /websrv надо откусить 70 | $file = substr($file, 7); 71 | // query string тоже откусить 72 | $pos = strpos($file, '?') and $file = substr($file, 0, $pos); 73 | // а вот дальше надо путь обработать методом-антихакером 74 | $filepath = realpath($this->servicedir . $file); 75 | // если в получившемся пути нет root-папки - возможно нас пытались хакнуть через ../../ 76 | if (strpos($filepath, $this->servicedir) !== 0) { 77 | throw new Exception('Intrusion attempt', 403); 78 | } 79 | if (!is_file($filepath)) { 80 | throw new Exception('File not found', 404); 81 | } 82 | return $filepath; 83 | } 84 | 85 | private function returnCode($code) { 86 | switch ($code) { 87 | case 403: 88 | $response = 'HTTP/1.1 403 Forbidden' . "\r\n" . 89 | 'Connection: close' . "\r\n" . 90 | "\r\n"; 91 | break; 92 | case 404: 93 | $response = 'HTTP/1.1 404 Not Found' . "\r\n" . 94 | 'Connection: close' . "\r\n" . 95 | "\r\n"; 96 | break; 97 | default: 98 | $response = 'HTTP/1.1 204 No Content' . "\r\n" . 99 | 'Connection: close' . "\r\n" . 100 | "\r\n"; 101 | break; 102 | } 103 | return $response; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /res/modules/class.ui.common.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs($args); 20 | } 21 | return self::$instance[$cn]; 22 | } 23 | 24 | // private сделать нельзя из-за public parent конструктора 25 | // но могу взбрыкнуть хотя бы 26 | final public function __construct() { 27 | $cn = get_called_class(); 28 | if (isset(self::$instance[$cn])) { 29 | throw new CoreException('cannot instance again, use getInstance()', 0); 30 | } 31 | $args = func_get_args(); 32 | call_user_func_array(array('parent', __FUNCTION__), $args); 33 | } 34 | 35 | public function init2(AcePHProxy $app) { 36 | // getApp есть в plugin 37 | } 38 | 39 | abstract public function draw(); 40 | 41 | abstract public function log($msg, $color = self::CLR_DEFAULT); 42 | 43 | public function error($msg) { 44 | return $this->log($msg, self::CLR_ERROR); 45 | } 46 | public function success($msg) { 47 | return $this->log($msg, self::CLR_GREEN); 48 | } 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /res/modules/class.ui.ncurses.php: -------------------------------------------------------------------------------- 1 | closeClean(); 14 | } 15 | 16 | // только ради требований базового класса, обращений к модулю извне не предполагается 17 | public function process(ClientRequest $req) { 18 | } 19 | 20 | public function init() { 21 | if (!function_exists('ncurses_init')) { 22 | throw new Exception('NCurses UI not available. check ncurses PHP extension installed'); 23 | } 24 | // конфиг раскладки по колонкам 25 | $this->colwid = array( 26 | 0 => 24, // channel (variable!) 27 | 8, // Buffer 28 | 9, // State 29 | 6, // peers 30 | 15, // up/down bytes 31 | 25, // Client list 32 | 12, // download/upload speed 33 | ); 34 | $this->initWindows(); 35 | } 36 | 37 | // вызывать каждый цикл. выводим массив трансляций 38 | public function draw() { 39 | $addinfo = $this->getApp()->getUIAdditionalInfo(); 40 | $streams = $this->makePlainStreamsArray($this->getStreams()); 41 | 42 | ncurses_werase ($this->windows['stat']); 43 | ncurses_wborder($this->windows['stat'], 0,0, 0,0, 0,0, 0,0); 44 | 45 | $this->listen4resize(); 46 | 47 | if (isset($addinfo['title'])) { 48 | $this->output('stat', 0, 2, $addinfo['title']); 49 | } 50 | if (isset($addinfo['port'])) { 51 | $this->output('stat', 0, 25, sprintf(' Port %d ', $addinfo['port'])); 52 | } 53 | if (isset($addinfo['ram'])) { 54 | $this->output('stat', 0, 38, sprintf(' RAM: %s MB ', $addinfo['ram'])); 55 | } 56 | if (isset($addinfo['uptime'])) { 57 | $this->output('stat', 0, 54, sprintf(' Uptime %s ', $addinfo['uptime'])); 58 | } 59 | 60 | $i = 1; 61 | $map = $this->map; 62 | 63 | // выводим все коннекты и трансляции 64 | $this->outputCol('stat', $i, 0, ""); 65 | $this->outputCol('stat', $i, 1, "Buffer"); 66 | $this->outputCol('stat', $i, 2, "State"); 67 | $this->outputCol('stat', $i, 3, "Peers"); 68 | $this->outputCol('stat', $i, 4, "Up (MB) Down"); 69 | $this->outputCol('stat', $i, 5, "Client"); 70 | $this->outputCol('stat', $i, 6, "DL (kbps) UL"); 71 | $i++; 72 | 73 | foreach ($streams as $row) { 74 | $i++; 75 | foreach ($row as $colidx => $str) { 76 | $this->outputCol('stat', $i, $colidx, $str); 77 | } 78 | } 79 | 80 | // состояние инета 81 | // ascii table http://www.linuxivr.com/c/week6/ascii_window.jpg 82 | #iconv('cp866', 'utf8', chr(0xb4)), 83 | #iconv('cp866', 'utf8', chr(0xc3)) 84 | $str = array( 85 | 0 => ($wwwok = !empty($addinfo['wwwok'])) ? self::CLR_GREEN : self::CLR_ERROR, 86 | 1 => sprintf(' %s ', $wwwok ? 'online' : 'offline') 87 | ); 88 | $this->outputCol('stat', 0, 6, $str); 89 | 90 | ncurses_wrefresh($this->windows['stat']); 91 | 92 | // перерисуем окно лога, при ресайзе оно не обновляется 93 | // TODO один хрен косяк 94 | ncurses_wborder($this->windows['log'], 0,0, 0,0, 0,0, 0,0); 95 | ncurses_wrefresh($this->windows['log']); 96 | } 97 | 98 | public function log($msg, $color = self::CLR_DEFAULT) { 99 | ncurses_getmaxyx ($this->windows['log'], $y, $x); 100 | ncurses_getyx ($this->windows['log'], $cy, $cx); // cursor xy 101 | if ($cy > $y - 3) { 102 | ncurses_werase ($this->windows['log']); 103 | ncurses_wborder($this->windows['log'], 0,0, 0,0, 0,0, 0,0); 104 | $cy = 0; 105 | } 106 | $msg = mb_substr($msg, 0, $x - 2); 107 | 108 | $color and ncurses_wcolor_set($this->windows['log'], $color); 109 | ncurses_mvwaddstr ($this->windows['log'], $cy + 1, 1, $msg); 110 | ncurses_clrtoeol (); 111 | $color and ncurses_wcolor_set($this->windows['log'], self::CLR_DEFAULT); 112 | 113 | // никак скроллить не выходит 114 | #ncurses_insdelln (1); 115 | #ncurses_scrl (-2); // вообще 0 реакции 116 | #ncurses_insertln (); 117 | ncurses_wrefresh($this->windows['log']); 118 | } 119 | 120 | // закрывает сессию ncurses 121 | protected function closeClean() { 122 | ncurses_end(); // выходим из режима ncurses, чистим экран 123 | } 124 | 125 | private function initWindows() { 126 | // начинаем с инициализации библиотеки 127 | $ncurse = ncurses_init(); 128 | // используем весь экран 129 | $this->windows['main'] = ncurses_newwin ( 0, 0, 0, 0); 130 | // рисуем рамку вокруг окна 131 | ncurses_border(0,0, 0,0, 0,0, 0,0); 132 | ncurses_getmaxyx ($this->windows['main'], $y, $x); 133 | // save current main window size 134 | $this->cur_x = $x; 135 | $this->cur_y = $y; 136 | 137 | // создаём второе окно для лога 138 | $rows = floor($y / 2); $cols = $x; $sy = $y - $rows; $sx = 0; 139 | $this->windows['log'] = ncurses_newwin($rows, $cols, $sy, $sx); 140 | 141 | // и окно для статистики (остальное пространство) 142 | $rows = $y - $rows; $cols = $x; $sy = 0; $sx = 0; 143 | $this->windows['stat'] = ncurses_newwin($rows, $cols, $sy, $sx); 144 | 145 | if (ncurses_has_colors()) { 146 | ncurses_start_color(); 147 | // colors http://php.net/manual/en/ncurses.colorconsts.php 148 | ncurses_init_pair(self::CLR_ERROR, NCURSES_COLOR_RED, NCURSES_COLOR_BLACK); 149 | ncurses_init_pair(self::CLR_GREEN, NCURSES_COLOR_GREEN, NCURSES_COLOR_BLACK); 150 | ncurses_init_pair(self::CLR_YELLOW, NCURSES_COLOR_YELLOW, NCURSES_COLOR_BLACK); 151 | ncurses_init_pair(self::CLR_SPEC1, NCURSES_COLOR_RED, NCURSES_COLOR_WHITE); 152 | ncurses_init_pair(5, NCURSES_COLOR_MAGENTA, NCURSES_COLOR_BLACK); 153 | ncurses_init_pair(6, NCURSES_COLOR_CYAN, NCURSES_COLOR_BLACK); 154 | ncurses_init_pair(self::CLR_DEFAULT, NCURSES_COLOR_WHITE, NCURSES_COLOR_BLACK); 155 | $this->log('Init colors', self::CLR_GREEN); 156 | } 157 | 158 | // рамка для него 159 | ncurses_wborder($this->windows['log'], 0,0, 0,0, 0,0, 0,0); 160 | ncurses_wborder($this->windows['stat'], 0,0, 0,0, 0,0, 0,0); 161 | 162 | ncurses_nl (); 163 | ncurses_curs_set (0); // visibility 164 | 165 | ncurses_refresh(); // рисуем окна 166 | 167 | // обновляем маленькое окно для вывода строки 168 | ncurses_wrefresh($this->windows['log']); 169 | } 170 | 171 | protected function listen4resize() { 172 | ncurses_getmaxyx ($this->windows['main'], $y, $x); 173 | if ($x != $this->cur_x or $y != $this->cur_y) { 174 | // restart ncurses session, redraw all 175 | $this->closeClean(); 176 | $this->initWindows(); 177 | } 178 | 179 | // save current main window size 180 | $this->cur_x = $x; 181 | $this->cur_y = $y; 182 | 183 | $startoffset = 2; // небольшой отступ на отрисовку границ, чтобы на них не налезал контент 184 | // ширина первого столбца определяется как разность ширины окна и всех столбцов, кроме первого 185 | $colsum = array_sum($this->colwid) - $this->colwid[0]; 186 | $this->colwid[0] = $this->cur_x - $colsum - ($startoffset * 2); // *2 ибо с 2 сторон 187 | 188 | // renew map 189 | $col = 0; 190 | $this->map = array( 191 | 0 => $col += $startoffset, // channel 192 | $col += $this->colwid[0], // Buffer, but 25 is Channel width! 193 | $col += $this->colwid[1], // State 194 | $col += $this->colwid[2], // up/down bytes 195 | $col += $this->colwid[3], // peers 196 | $col += $this->colwid[4], // Client list 197 | $col += $this->colwid[5], // download/upload speed 198 | ); 199 | } 200 | 201 | protected function outputTitle($title) { 202 | ncurses_attron(NCURSES_A_REVERSE); 203 | ncurses_mvaddstr(0, 1, $title); 204 | ncurses_attroff(NCURSES_A_REVERSE); 205 | ncurses_refresh(); // рисуем окна 206 | } 207 | 208 | protected function outputCol($wcode, $y, $col, $str) { 209 | $x = $this->map[$col]; 210 | $maxwid = $this->colwid[$col]; 211 | // -1 чтобы не сливалось со след.столбцом, но для последнего столбца неактуально 212 | if ($col < count($this->colwid) - 1) { 213 | $maxwid -= 1; 214 | } 215 | return $this->output($wcode, $y, $x, $str, $maxwid); 216 | } 217 | protected function output($wcode, $y, $x, $str, $maxwid = null) { 218 | $w = $this->windows[$wcode]; 219 | $color = null; 220 | if (is_array($str)) { 221 | $color = $str[0]; 222 | $str = $str[1]; 223 | } 224 | if (!is_null($maxwid) and mb_strlen($str) > $maxwid) { 225 | $str = mb_substr($str, 0, $maxwid); 226 | } 227 | 228 | $color and ncurses_wcolor_set($w, $color); 229 | ncurses_mvwaddstr($w, $y, $x, $str); 230 | $color and ncurses_wcolor_set($w, self::CLR_DEFAULT); 231 | } 232 | 233 | private function makePlainStreamsArray($allStreams) { 234 | // задача - собрать массив трансляций для вывода в UI 235 | $channels = array(); 236 | foreach ($allStreams as $pid => $one) { 237 | $stats = $one->getStatistics(); 238 | $isRest = $one->isRestarting(); 239 | $bufColor = self::CLR_GREEN; 240 | $titleColor = self::CLR_DEFAULT; 241 | if ($isRest) { 242 | $bufColor = self::CLR_SPEC1; 243 | $titleColor = self::CLR_ERROR; 244 | } 245 | else if (@$stats['emptydata']) { 246 | $bufColor = self::CLR_ERROR; 247 | } 248 | else if (@$stats['shortdata']) { 249 | $bufColor = self::CLR_YELLOW; 250 | } 251 | 252 | $bufLen = round($one->getBufferedLength() / 1024 / 1024) . ' Mb'; 253 | // показываем поочередно размер буфера чтения и размер прочитанного внутреннего буфера 254 | $buf = time() % 2 ? $one->getBufferSize() : $bufLen; 255 | $s = iconv('cp866', 'utf8', chr(249)); // значок заполнитель 256 | $tmp = array( 257 | // если вместо строки массив: 0 - цвет, 1 - выводимая строка 258 | 0 => array(0 => $titleColor, 1 => $one->getName()), 259 | 1 => array(0 => $bufColor, 1 => $buf), 260 | 2 => $one->getState(), 261 | 3 => @$stats['peers'], 262 | 4 => sprintf('%\'.-7d%\'.6d', @$stats['ul_bytes']/1024/1024, @$stats['dl_bytes']/1024/1024), 263 | 6 => sprintf('%\'.-6d%\'.6d', @$stats['speed_dn'], @$stats['speed_up']) 264 | ); 265 | $peers = $one->getPeers(); 266 | if (empty($peers)) { 267 | $tmp[2] = 'close'; 268 | $channels[] = $tmp; 269 | } 270 | else { 271 | foreach ($peers as $peer => $client) { 272 | $peercolor = self::CLR_DEFAULT; 273 | if ($client->isEcoMode()) { // если экорежим на клиенте разрешен 274 | $peercolor = $client->isEcoModeRunning() ? self::CLR_ERROR : self::CLR_GREEN; 275 | } 276 | // выводим поочередно то клиента, то его статистику 277 | // это поле размером 24 символа 278 | $peerline = round(time() / 0.6) % 2 ? 279 | sprintf('%s %d%%', $client->getName(), $client->getPointerPosition()) : 280 | sprintf('%-13s %8s', $client->getUptime(), $client->getTraffic()) ; 281 | $tmp[5] = array(0 => $peercolor, 1 => $peerline); 282 | $channels[] = $tmp; 283 | $tmp = array(0 => '', '', '', '', '', '', ''); 284 | } 285 | } 286 | } 287 | 288 | return $channels; 289 | } 290 | 291 | } 292 | 293 | -------------------------------------------------------------------------------- /res/modules/class.ui.text.php: -------------------------------------------------------------------------------- 1 | getStreams() as $one) { 16 | $clients += count($one->getPeers()); 17 | } 18 | echo 'Active streams: ' . count($streams) . "\t" . 'Active clients: ' . $clients . "\r"; 19 | } 20 | 21 | public function log($msg, $color = null) { 22 | echo $msg . "\n"; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /res/modules/class.ui.websocket.php: -------------------------------------------------------------------------------- 1 | getHeaders()))); 14 | $key = null; 15 | foreach ($headers as $line) { 16 | if (strpos($line, 'Sec-WebSocket-Key') === false) { 17 | continue; 18 | } 19 | $key = trim(substr($line, 18)); 20 | break; 21 | } 22 | if (empty($key)) { 23 | return false; 24 | } 25 | 26 | // создаем сокет/поток, куда мы можем писать данные, 27 | // которые в итоге пойдут на вебстраницу клиента и будут разобраны в JS 28 | $client = $req->getClient(); 29 | $client->registerEventListener(array($this, 'clientEventListener')); 30 | $socket = new StreamResource_ws($client, $key); 31 | self::$sockets[$client->getName()] = $socket; 32 | return $req->response($socket, $key); 33 | } 34 | 35 | public function clientEventListener($client, $event) { 36 | if (isset($event['event'])) { 37 | switch ($event['event']) { 38 | case 'close': // client disconnected 39 | $this->closeClient($client); 40 | break; 41 | } 42 | } 43 | if (isset($event['moredata'])) { 44 | $socket = self::$sockets[$client->getName()]; 45 | if ($socket) { 46 | $gotpacket = $socket->decode($event['moredata']); 47 | // error_log('WS got: ' . json_encode($gotpacket)); 48 | if (isset($gotpacket['type']) and $gotpacket['type'] == 'close') { 49 | $this->closeClient($client); 50 | } 51 | } 52 | } 53 | } 54 | 55 | private function closeClient(StreamClient $client) { 56 | if (isset(self::$sockets[$client->getName()])) { 57 | // error_log('closing WS for client ' . $client->getName()); 58 | self::$sockets[$client->getName()]->close(); 59 | unset(self::$sockets[$client->getName()]); 60 | } 61 | } 62 | 63 | public function init() { 64 | } 65 | 66 | // по всем клиентам вебсокета надо раздать данные (потоки, статистика) 67 | public function draw() { 68 | //error_log('draw ' . microtime(1)); 69 | if (rand(0, 10) < 10) { // do not redraw so often 70 | # return; 71 | } 72 | // сделаем так.. будем замерять время и обновлять интерфейс раз в 100мс 73 | if ((microtime(1) - $this->lastupdated) < 0.3) { 74 | return; 75 | } 76 | $this->lastupdated = microtime(1); 77 | 78 | $uidata = array( 79 | 'stats' => $this->getApp()->getUIAdditionalInfo(), 80 | 'streams' => $this->makePlainStreamsArray($this->getStreams()) 81 | ); 82 | $this->put($uidata); 83 | } 84 | 85 | private function put($uidata) { 86 | // уведомляем все сокеты об обновлениях UI 87 | // заодно проверим и выкинем отвалившихся клиентов 88 | // здесь проверяем каждого на isFinished, 89 | // но вообще callback под это надо завести 90 | // см. метод put 91 | foreach (self::$sockets as $socket) { 92 | $socket->put($uidata); 93 | } 94 | } 95 | 96 | public function log($msg, $color = null) { 97 | $this->put($msg); 98 | } 99 | 100 | 101 | 102 | private function makePlainStreamsArray($allStreams) { 103 | // задача - собрать массив трансляций для вывода в UI 104 | $channels = array(); 105 | foreach ($allStreams as $pid => $one) { 106 | $channels[$pid] = array( 107 | 'streamid' => $pid, 108 | 'isLive' => $one->isLive(), 109 | 'type' => $one->getType(), 110 | 'statistics' => $one->getStatistics(), 111 | 'isRestarting' => $one->isRestarting(), 112 | 'title' => $one->getName(), 113 | 'buffer' => $one->getBufferSize(), 114 | 'state' => $one->getState(), 115 | 'bufferedLength' => $one->getBufferedLength(), 116 | 'bufferMaxLength' => $one->getBufferLength(), 117 | 'clients' => array(), 118 | ); 119 | 120 | $peers = $one->getPeers(); 121 | if (empty($peers)) { 122 | $channels[$pid]['state'] = 'close'; 123 | } 124 | 125 | foreach ($peers as $peer => $client) { 126 | $channels[$pid]['clients'][$peer] = array( 127 | 'isEcoMode' => $client->isEcoMode(), 128 | 'isEcoModeRunning' => $client->isEcoModeRunning(), 129 | 'ptrPosition' => $client->getPointerPosition(), 130 | 'uptime' => $client->getUptime(), 131 | 'traffic' => $client->getTraffic(), 132 | 'clienttype' => strtolower($client->getType()), 133 | ); 134 | } 135 | } 136 | 137 | return $channels; 138 | } 139 | 140 | 141 | } 142 | 143 | 144 | -------------------------------------------------------------------------------- /res/websrv/css3dui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?=$TPLDATA['title']?> 5 | 6 | 7 | 8 | 9 | 129 | 130 | 131 | 132 | 133 |
134 |
135 |   136 | 137 | 138 | LIVE 139 | 140 | starting 141 | stopping 142 | playing 143 | buffering 144 | 145 | 146 | prebuffering 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
155 |
156 |
158 |
159 | 160 | 161 | 162 |
163 |
164 | 165 |
166 | 167 |
168 | 169 |
170 |
disconnected
171 | 172 |

173 | 174 |
175 |
    176 |
  • up and ready time
  • 177 |
  • Used RAM: Mb
  • 178 |
  • Listen on port
  • 179 |
180 | 181 |
182 |
183 |
184 | 185 |
186 |
187 |
188 | 189 |
190 |
191 |
192 |
193 | 194 |
195 | 196 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /res/websrv/img/kodi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/res/websrv/img/kodi.png -------------------------------------------------------------------------------- /res/websrv/img/nebula.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/res/websrv/img/nebula.jpg -------------------------------------------------------------------------------- /res/websrv/img/vlc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/res/websrv/img/vlc.ico -------------------------------------------------------------------------------- /res/websrv/img/wmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/res/websrv/img/wmp.png -------------------------------------------------------------------------------- /res/websrv/img/xbmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mexxval/AcePHProxy/a932434dec8c78b48664f6062c3396b9df654edb/res/websrv/img/xbmc.png -------------------------------------------------------------------------------- /res/websrv/js/CSS3DRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs 3 | * @author mrdoob / http://mrdoob.com/ 4 | */ 5 | 6 | THREE.CSS3DObject = function ( element ) { 7 | 8 | THREE.Object3D.call( this ); 9 | 10 | this.element = element; 11 | this.element.style.position = 'absolute'; 12 | 13 | this.addEventListener( 'removed', function ( event ) { 14 | 15 | if ( this.element.parentNode !== null ) { 16 | 17 | this.element.parentNode.removeChild( this.element ); 18 | 19 | } 20 | 21 | } ); 22 | 23 | }; 24 | 25 | THREE.CSS3DObject.prototype = Object.create( THREE.Object3D.prototype ); 26 | THREE.CSS3DObject.prototype.constructor = THREE.CSS3DObject; 27 | 28 | THREE.CSS3DSprite = function ( element ) { 29 | 30 | THREE.CSS3DObject.call( this, element ); 31 | 32 | }; 33 | 34 | THREE.CSS3DSprite.prototype = Object.create( THREE.CSS3DObject.prototype ); 35 | THREE.CSS3DSprite.prototype.constructor = THREE.CSS3DSprite; 36 | 37 | // 38 | 39 | THREE.CSS3DRenderer = function () { 40 | 41 | console.log( 'THREE.CSS3DRenderer', THREE.REVISION ); 42 | 43 | var _width, _height; 44 | var _widthHalf, _heightHalf; 45 | 46 | var matrix = new THREE.Matrix4(); 47 | 48 | var cache = { 49 | camera: { fov: 0, style: '' }, 50 | objects: {} 51 | }; 52 | 53 | var domElement = document.createElement( 'div' ); 54 | domElement.style.overflow = 'hidden'; 55 | 56 | domElement.style.WebkitTransformStyle = 'preserve-3d'; 57 | domElement.style.MozTransformStyle = 'preserve-3d'; 58 | domElement.style.oTransformStyle = 'preserve-3d'; 59 | domElement.style.transformStyle = 'preserve-3d'; 60 | 61 | this.domElement = domElement; 62 | 63 | var cameraElement = document.createElement( 'div' ); 64 | 65 | cameraElement.style.WebkitTransformStyle = 'preserve-3d'; 66 | cameraElement.style.MozTransformStyle = 'preserve-3d'; 67 | cameraElement.style.oTransformStyle = 'preserve-3d'; 68 | cameraElement.style.transformStyle = 'preserve-3d'; 69 | 70 | domElement.appendChild( cameraElement ); 71 | 72 | this.setClearColor = function () {}; 73 | 74 | this.getSize = function() { 75 | 76 | return { 77 | width: _width, 78 | height: _height 79 | }; 80 | 81 | }; 82 | 83 | this.setSize = function ( width, height ) { 84 | 85 | _width = width; 86 | _height = height; 87 | 88 | _widthHalf = _width / 2; 89 | _heightHalf = _height / 2; 90 | 91 | domElement.style.width = width + 'px'; 92 | domElement.style.height = height + 'px'; 93 | 94 | cameraElement.style.width = width + 'px'; 95 | cameraElement.style.height = height + 'px'; 96 | 97 | }; 98 | 99 | var epsilon = function ( value ) { 100 | 101 | return Math.abs( value ) < Number.EPSILON ? 0 : value; 102 | 103 | }; 104 | 105 | var getCameraCSSMatrix = function ( matrix ) { 106 | 107 | var elements = matrix.elements; 108 | 109 | return 'matrix3d(' + 110 | epsilon( elements[ 0 ] ) + ',' + 111 | epsilon( - elements[ 1 ] ) + ',' + 112 | epsilon( elements[ 2 ] ) + ',' + 113 | epsilon( elements[ 3 ] ) + ',' + 114 | epsilon( elements[ 4 ] ) + ',' + 115 | epsilon( - elements[ 5 ] ) + ',' + 116 | epsilon( elements[ 6 ] ) + ',' + 117 | epsilon( elements[ 7 ] ) + ',' + 118 | epsilon( elements[ 8 ] ) + ',' + 119 | epsilon( - elements[ 9 ] ) + ',' + 120 | epsilon( elements[ 10 ] ) + ',' + 121 | epsilon( elements[ 11 ] ) + ',' + 122 | epsilon( elements[ 12 ] ) + ',' + 123 | epsilon( - elements[ 13 ] ) + ',' + 124 | epsilon( elements[ 14 ] ) + ',' + 125 | epsilon( elements[ 15 ] ) + 126 | ')'; 127 | 128 | }; 129 | 130 | var getObjectCSSMatrix = function ( matrix ) { 131 | 132 | var elements = matrix.elements; 133 | 134 | return 'translate3d(-50%,-50%,0) matrix3d(' + 135 | epsilon( elements[ 0 ] ) + ',' + 136 | epsilon( elements[ 1 ] ) + ',' + 137 | epsilon( elements[ 2 ] ) + ',' + 138 | epsilon( elements[ 3 ] ) + ',' + 139 | epsilon( - elements[ 4 ] ) + ',' + 140 | epsilon( - elements[ 5 ] ) + ',' + 141 | epsilon( - elements[ 6 ] ) + ',' + 142 | epsilon( - elements[ 7 ] ) + ',' + 143 | epsilon( elements[ 8 ] ) + ',' + 144 | epsilon( elements[ 9 ] ) + ',' + 145 | epsilon( elements[ 10 ] ) + ',' + 146 | epsilon( elements[ 11 ] ) + ',' + 147 | epsilon( elements[ 12 ] ) + ',' + 148 | epsilon( elements[ 13 ] ) + ',' + 149 | epsilon( elements[ 14 ] ) + ',' + 150 | epsilon( elements[ 15 ] ) + 151 | ')'; 152 | 153 | }; 154 | 155 | var renderObject = function ( object, camera ) { 156 | 157 | if ( object instanceof THREE.CSS3DObject ) { 158 | 159 | var style; 160 | 161 | if ( object instanceof THREE.CSS3DSprite ) { 162 | 163 | // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/ 164 | 165 | matrix.copy( camera.matrixWorldInverse ); 166 | matrix.transpose(); 167 | matrix.copyPosition( object.matrixWorld ); 168 | matrix.scale( object.scale ); 169 | 170 | matrix.elements[ 3 ] = 0; 171 | matrix.elements[ 7 ] = 0; 172 | matrix.elements[ 11 ] = 0; 173 | matrix.elements[ 15 ] = 1; 174 | 175 | style = getObjectCSSMatrix( matrix ); 176 | 177 | } else { 178 | 179 | style = getObjectCSSMatrix( object.matrixWorld ); 180 | 181 | } 182 | 183 | var element = object.element; 184 | var cachedStyle = cache.objects[ object.id ]; 185 | 186 | if ( cachedStyle === undefined || cachedStyle !== style ) { 187 | 188 | element.style.WebkitTransform = style; 189 | element.style.MozTransform = style; 190 | element.style.oTransform = style; 191 | element.style.transform = style; 192 | 193 | cache.objects[ object.id ] = style; 194 | 195 | } 196 | 197 | if ( element.parentNode !== cameraElement ) { 198 | 199 | cameraElement.appendChild( element ); 200 | 201 | } 202 | 203 | } 204 | 205 | for ( var i = 0, l = object.children.length; i < l; i ++ ) { 206 | 207 | renderObject( object.children[ i ], camera ); 208 | 209 | } 210 | 211 | }; 212 | 213 | this.render = function ( scene, camera ) { 214 | 215 | var fov = 0.5 / Math.tan( THREE.Math.degToRad( camera.getEffectiveFOV() * 0.5 ) ) * _height; 216 | 217 | if ( cache.camera.fov !== fov ) { 218 | 219 | domElement.style.WebkitPerspective = fov + "px"; 220 | domElement.style.MozPerspective = fov + "px"; 221 | domElement.style.oPerspective = fov + "px"; 222 | domElement.style.perspective = fov + "px"; 223 | 224 | cache.camera.fov = fov; 225 | 226 | } 227 | 228 | scene.updateMatrixWorld(); 229 | 230 | if ( camera.parent === null ) camera.updateMatrixWorld(); 231 | 232 | camera.matrixWorldInverse.getInverse( camera.matrixWorld ); 233 | 234 | var style = "translate3d(0,0," + fov + "px)" + getCameraCSSMatrix( camera.matrixWorldInverse ) + 235 | " translate3d(" + _widthHalf + "px," + _heightHalf + "px, 0)"; 236 | 237 | if ( cache.camera.style !== style ) { 238 | 239 | cameraElement.style.WebkitTransform = style; 240 | cameraElement.style.MozTransform = style; 241 | cameraElement.style.oTransform = style; 242 | cameraElement.style.transform = style; 243 | 244 | cache.camera.style = style; 245 | 246 | } 247 | 248 | renderObject( scene, camera ); 249 | 250 | }; 251 | 252 | }; 253 | -------------------------------------------------------------------------------- /res/websrv/js/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | 8 | THREE.TrackballControls = function ( object, domElement ) { 9 | 10 | var _this = this; 11 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 | 22 | this.rotateSpeed = 1.0; 23 | this.zoomSpeed = 1.2; 24 | this.panSpeed = 0.3; 25 | 26 | this.noRotate = false; 27 | this.noZoom = false; 28 | this.noPan = false; 29 | 30 | this.staticMoving = false; 31 | this.dynamicDampingFactor = 0.2; 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; 35 | 36 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 | 38 | // internals 39 | 40 | this.target = new THREE.Vector3(); 41 | 42 | var EPS = 0.000001; 43 | 44 | var lastPosition = new THREE.Vector3(); 45 | 46 | var _state = STATE.NONE, 47 | _prevState = STATE.NONE, 48 | 49 | _eye = new THREE.Vector3(), 50 | 51 | _movePrev = new THREE.Vector2(), 52 | _moveCurr = new THREE.Vector2(), 53 | 54 | _lastAxis = new THREE.Vector3(), 55 | _lastAngle = 0, 56 | 57 | _zoomStart = new THREE.Vector2(), 58 | _zoomEnd = new THREE.Vector2(), 59 | 60 | _touchZoomDistanceStart = 0, 61 | _touchZoomDistanceEnd = 0, 62 | 63 | _panStart = new THREE.Vector2(), 64 | _panEnd = new THREE.Vector2(); 65 | 66 | // for reset 67 | 68 | this.target0 = this.target.clone(); 69 | this.position0 = this.object.position.clone(); 70 | this.up0 = this.object.up.clone(); 71 | 72 | // events 73 | 74 | var changeEvent = { type: 'change' }; 75 | var startEvent = { type: 'start' }; 76 | var endEvent = { type: 'end' }; 77 | 78 | 79 | // methods 80 | 81 | this.handleResize = function () { 82 | 83 | if ( this.domElement === document ) { 84 | 85 | this.screen.left = 0; 86 | this.screen.top = 0; 87 | this.screen.width = window.innerWidth; 88 | this.screen.height = window.innerHeight; 89 | 90 | } else { 91 | 92 | var box = this.domElement.getBoundingClientRect(); 93 | // adjustments come from similar code in the jquery offset() function 94 | var d = this.domElement.ownerDocument.documentElement; 95 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 | this.screen.width = box.width; 98 | this.screen.height = box.height; 99 | 100 | } 101 | 102 | }; 103 | 104 | this.handleEvent = function ( event ) { 105 | 106 | if ( typeof this[ event.type ] == 'function' ) { 107 | 108 | this[ event.type ]( event ); 109 | 110 | } 111 | 112 | }; 113 | 114 | var getMouseOnScreen = ( function () { 115 | 116 | var vector = new THREE.Vector2(); 117 | 118 | return function getMouseOnScreen( pageX, pageY ) { 119 | 120 | vector.set( 121 | ( pageX - _this.screen.left ) / _this.screen.width, 122 | ( pageY - _this.screen.top ) / _this.screen.height 123 | ); 124 | 125 | return vector; 126 | 127 | }; 128 | 129 | }() ); 130 | 131 | var getMouseOnCircle = ( function () { 132 | 133 | var vector = new THREE.Vector2(); 134 | 135 | return function getMouseOnCircle( pageX, pageY ) { 136 | 137 | vector.set( 138 | ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 139 | ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 140 | ); 141 | 142 | return vector; 143 | 144 | }; 145 | 146 | }() ); 147 | 148 | this.rotateCamera = ( function() { 149 | 150 | var axis = new THREE.Vector3(), 151 | quaternion = new THREE.Quaternion(), 152 | eyeDirection = new THREE.Vector3(), 153 | objectUpDirection = new THREE.Vector3(), 154 | objectSidewaysDirection = new THREE.Vector3(), 155 | moveDirection = new THREE.Vector3(), 156 | angle; 157 | 158 | return function rotateCamera() { 159 | 160 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 161 | angle = moveDirection.length(); 162 | 163 | if ( angle ) { 164 | 165 | _eye.copy( _this.object.position ).sub( _this.target ); 166 | 167 | eyeDirection.copy( _eye ).normalize(); 168 | objectUpDirection.copy( _this.object.up ).normalize(); 169 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 170 | 171 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 172 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 173 | 174 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 175 | 176 | axis.crossVectors( moveDirection, _eye ).normalize(); 177 | 178 | angle *= _this.rotateSpeed; 179 | quaternion.setFromAxisAngle( axis, angle ); 180 | 181 | _eye.applyQuaternion( quaternion ); 182 | _this.object.up.applyQuaternion( quaternion ); 183 | 184 | _lastAxis.copy( axis ); 185 | _lastAngle = angle; 186 | 187 | } else if ( ! _this.staticMoving && _lastAngle ) { 188 | 189 | _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 190 | _eye.copy( _this.object.position ).sub( _this.target ); 191 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 192 | _eye.applyQuaternion( quaternion ); 193 | _this.object.up.applyQuaternion( quaternion ); 194 | 195 | } 196 | 197 | _movePrev.copy( _moveCurr ); 198 | 199 | }; 200 | 201 | }() ); 202 | 203 | 204 | this.zoomCamera = function () { 205 | 206 | var factor; 207 | 208 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 209 | 210 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 211 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 212 | _eye.multiplyScalar( factor ); 213 | 214 | } else { 215 | 216 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 217 | 218 | if ( factor !== 1.0 && factor > 0.0 ) { 219 | 220 | _eye.multiplyScalar( factor ); 221 | 222 | if ( _this.staticMoving ) { 223 | 224 | _zoomStart.copy( _zoomEnd ); 225 | 226 | } else { 227 | 228 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 229 | 230 | } 231 | 232 | } 233 | 234 | } 235 | 236 | }; 237 | 238 | this.panCamera = ( function() { 239 | 240 | var mouseChange = new THREE.Vector2(), 241 | objectUp = new THREE.Vector3(), 242 | pan = new THREE.Vector3(); 243 | 244 | return function panCamera() { 245 | 246 | mouseChange.copy( _panEnd ).sub( _panStart ); 247 | 248 | if ( mouseChange.lengthSq() ) { 249 | 250 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 251 | 252 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 253 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 254 | 255 | _this.object.position.add( pan ); 256 | _this.target.add( pan ); 257 | 258 | if ( _this.staticMoving ) { 259 | 260 | _panStart.copy( _panEnd ); 261 | 262 | } else { 263 | 264 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 265 | 266 | } 267 | 268 | } 269 | 270 | }; 271 | 272 | }() ); 273 | 274 | this.checkDistances = function () { 275 | 276 | if ( ! _this.noZoom || ! _this.noPan ) { 277 | 278 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 279 | 280 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 281 | _zoomStart.copy( _zoomEnd ); 282 | 283 | } 284 | 285 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 286 | 287 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 288 | _zoomStart.copy( _zoomEnd ); 289 | 290 | } 291 | 292 | } 293 | 294 | }; 295 | 296 | this.update = function () { 297 | 298 | _eye.subVectors( _this.object.position, _this.target ); 299 | 300 | if ( ! _this.noRotate ) { 301 | 302 | _this.rotateCamera(); 303 | 304 | } 305 | 306 | if ( ! _this.noZoom ) { 307 | 308 | _this.zoomCamera(); 309 | 310 | } 311 | 312 | if ( ! _this.noPan ) { 313 | 314 | _this.panCamera(); 315 | 316 | } 317 | 318 | _this.object.position.addVectors( _this.target, _eye ); 319 | 320 | _this.checkDistances(); 321 | 322 | _this.object.lookAt( _this.target ); 323 | 324 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 325 | 326 | _this.dispatchEvent( changeEvent ); 327 | 328 | lastPosition.copy( _this.object.position ); 329 | 330 | } 331 | 332 | }; 333 | 334 | this.reset = function () { 335 | 336 | _state = STATE.NONE; 337 | _prevState = STATE.NONE; 338 | 339 | _this.target.copy( _this.target0 ); 340 | _this.object.position.copy( _this.position0 ); 341 | _this.object.up.copy( _this.up0 ); 342 | 343 | _eye.subVectors( _this.object.position, _this.target ); 344 | 345 | _this.object.lookAt( _this.target ); 346 | 347 | _this.dispatchEvent( changeEvent ); 348 | 349 | lastPosition.copy( _this.object.position ); 350 | 351 | }; 352 | 353 | // listeners 354 | 355 | function keydown( event ) { 356 | 357 | if ( _this.enabled === false ) return; 358 | 359 | window.removeEventListener( 'keydown', keydown ); 360 | 361 | _prevState = _state; 362 | 363 | if ( _state !== STATE.NONE ) { 364 | 365 | return; 366 | 367 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { 368 | 369 | _state = STATE.ROTATE; 370 | 371 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { 372 | 373 | _state = STATE.ZOOM; 374 | 375 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { 376 | 377 | _state = STATE.PAN; 378 | 379 | } 380 | 381 | } 382 | 383 | function keyup( event ) { 384 | 385 | if ( _this.enabled === false ) return; 386 | 387 | _state = _prevState; 388 | 389 | window.addEventListener( 'keydown', keydown, false ); 390 | 391 | } 392 | 393 | function mousedown( event ) { 394 | 395 | if ( _this.enabled === false ) return; 396 | 397 | event.preventDefault(); 398 | event.stopPropagation(); 399 | 400 | if ( _state === STATE.NONE ) { 401 | 402 | _state = event.button; 403 | 404 | } 405 | 406 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 407 | 408 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 409 | _movePrev.copy( _moveCurr ); 410 | 411 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 412 | 413 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 414 | _zoomEnd.copy( _zoomStart ); 415 | 416 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 417 | 418 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 419 | _panEnd.copy( _panStart ); 420 | 421 | } 422 | 423 | document.addEventListener( 'mousemove', mousemove, false ); 424 | document.addEventListener( 'mouseup', mouseup, false ); 425 | 426 | _this.dispatchEvent( startEvent ); 427 | 428 | } 429 | 430 | function mousemove( event ) { 431 | 432 | if ( _this.enabled === false ) return; 433 | 434 | event.preventDefault(); 435 | event.stopPropagation(); 436 | 437 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 438 | 439 | _movePrev.copy( _moveCurr ); 440 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 441 | 442 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 443 | 444 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 445 | 446 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 447 | 448 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 449 | 450 | } 451 | 452 | } 453 | 454 | function mouseup( event ) { 455 | 456 | if ( _this.enabled === false ) return; 457 | 458 | event.preventDefault(); 459 | event.stopPropagation(); 460 | 461 | _state = STATE.NONE; 462 | 463 | document.removeEventListener( 'mousemove', mousemove ); 464 | document.removeEventListener( 'mouseup', mouseup ); 465 | _this.dispatchEvent( endEvent ); 466 | 467 | } 468 | 469 | function mousewheel( event ) { 470 | 471 | if ( _this.enabled === false ) return; 472 | 473 | event.preventDefault(); 474 | event.stopPropagation(); 475 | 476 | _zoomStart.y -= event.deltaY * 0.01; 477 | 478 | _this.dispatchEvent( startEvent ); 479 | _this.dispatchEvent( endEvent ); 480 | 481 | } 482 | 483 | function touchstart( event ) { 484 | 485 | if ( _this.enabled === false ) return; 486 | 487 | switch ( event.touches.length ) { 488 | 489 | case 1: 490 | _state = STATE.TOUCH_ROTATE; 491 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 492 | _movePrev.copy( _moveCurr ); 493 | break; 494 | 495 | default: // 2 or more 496 | _state = STATE.TOUCH_ZOOM_PAN; 497 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 498 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 499 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 500 | 501 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 502 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 503 | _panStart.copy( getMouseOnScreen( x, y ) ); 504 | _panEnd.copy( _panStart ); 505 | break; 506 | 507 | } 508 | 509 | _this.dispatchEvent( startEvent ); 510 | 511 | } 512 | 513 | function touchmove( event ) { 514 | 515 | if ( _this.enabled === false ) return; 516 | 517 | event.preventDefault(); 518 | event.stopPropagation(); 519 | 520 | switch ( event.touches.length ) { 521 | 522 | case 1: 523 | _movePrev.copy( _moveCurr ); 524 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 525 | break; 526 | 527 | default: // 2 or more 528 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 529 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 530 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 531 | 532 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 533 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 534 | _panEnd.copy( getMouseOnScreen( x, y ) ); 535 | break; 536 | 537 | } 538 | 539 | } 540 | 541 | function touchend( event ) { 542 | 543 | if ( _this.enabled === false ) return; 544 | 545 | switch ( event.touches.length ) { 546 | 547 | case 0: 548 | _state = STATE.NONE; 549 | break; 550 | 551 | case 1: 552 | _state = STATE.TOUCH_ROTATE; 553 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 554 | _movePrev.copy( _moveCurr ); 555 | break; 556 | 557 | } 558 | 559 | _this.dispatchEvent( endEvent ); 560 | 561 | } 562 | 563 | function contextmenu( event ) { 564 | 565 | event.preventDefault(); 566 | 567 | } 568 | 569 | this.dispose = function() { 570 | 571 | this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); 572 | this.domElement.removeEventListener( 'mousedown', mousedown, false ); 573 | this.domElement.removeEventListener( 'wheel', mousewheel, false ); 574 | 575 | this.domElement.removeEventListener( 'touchstart', touchstart, false ); 576 | this.domElement.removeEventListener( 'touchend', touchend, false ); 577 | this.domElement.removeEventListener( 'touchmove', touchmove, false ); 578 | 579 | document.removeEventListener( 'mousemove', mousemove, false ); 580 | document.removeEventListener( 'mouseup', mouseup, false ); 581 | 582 | window.removeEventListener( 'keydown', keydown, false ); 583 | window.removeEventListener( 'keyup', keyup, false ); 584 | 585 | }; 586 | 587 | this.domElement.addEventListener( 'contextmenu', contextmenu, false ); 588 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 589 | this.domElement.addEventListener( 'wheel', mousewheel, false ); 590 | 591 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 592 | this.domElement.addEventListener( 'touchend', touchend, false ); 593 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 594 | 595 | window.addEventListener( 'keydown', keydown, false ); 596 | window.addEventListener( 'keyup', keyup, false ); 597 | 598 | this.handleResize(); 599 | 600 | // force an update at start 601 | this.update(); 602 | 603 | }; 604 | 605 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 606 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; 607 | -------------------------------------------------------------------------------- /res/websrv/js/css3d.js: -------------------------------------------------------------------------------- 1 | 2 | $(function () { 3 | var camera, scene, renderer; 4 | var controls; 5 | 6 | init(); 7 | animate(); 8 | 9 | function init() { 10 | 11 | camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); 12 | camera.position.z = 500; 13 | //camera.position.x = 500; // прикольно 14 | //camera.position.y = 500; 15 | 16 | scene = new THREE.Scene(); 17 | 18 | var element = document.getElementById('stats'); 19 | var object = new THREE.CSS3DObject( element ); 20 | object.position.x = -500; 21 | object.position.y = 0; 22 | object.position.z = 0; 23 | object.rotation.y = 0.5; 24 | scene.add( object ); 25 | 26 | // 27 | var element = document.getElementById('maincontent'); 28 | var object = new THREE.CSS3DObject( element ); 29 | object.position.x = 0; 30 | object.position.y = 0; 31 | object.position.z = 0; 32 | scene.add( object ); 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | renderer = new THREE.CSS3DRenderer(); 41 | renderer.setSize( window.innerWidth, window.innerHeight ); 42 | //renderer.domElement.style.position = 'absolute'; 43 | document.body.appendChild( renderer.domElement ); 44 | 45 | // 46 | 47 | controls = new THREE.TrackballControls( camera, renderer.domElement ); 48 | controls.rotateSpeed = 0.0; 49 | controls.zoomSpeed = 0.01; 50 | controls.minDistance = 400; 51 | controls.maxDistance = 700; 52 | 53 | 54 | 55 | window.addEventListener( 'resize', onWindowResize, false ); 56 | 57 | } 58 | 59 | function onWindowResize() { 60 | camera.aspect = window.innerWidth / window.innerHeight; 61 | camera.updateProjectionMatrix(); 62 | 63 | renderer.setSize( window.innerWidth, window.innerHeight ); 64 | } 65 | 66 | function animate() { 67 | requestAnimationFrame( animate ); 68 | TWEEN.update(); 69 | render(); 70 | controls.update(); 71 | } 72 | 73 | function render() { 74 | 75 | renderer.render( scene, camera ); 76 | 77 | } 78 | 79 | }); 80 | 81 | /* 82 | function transform( targets, duration ) { 83 | 84 | TWEEN.removeAll(); 85 | 86 | for ( var i = 0; i < objects.length; i ++ ) { 87 | 88 | var object = objects[ i ]; 89 | var target = targets[ i ]; 90 | 91 | new TWEEN.Tween( object.position ) 92 | .to( { x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration ) 93 | .easing( TWEEN.Easing.Exponential.InOut ) 94 | .start(); 95 | 96 | new TWEEN.Tween( object.rotation ) 97 | .to( { x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, Math.random() * duration + duration ) 98 | .easing( TWEEN.Easing.Exponential.InOut ) 99 | .start(); 100 | 101 | } 102 | 103 | new TWEEN.Tween( this ) 104 | .to( {}, duration * 2 ) 105 | .start(); 106 | 107 | } 108 | 109 | */ 110 | 111 | -------------------------------------------------------------------------------- /res/websrv/js/socket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var STOP = false; 4 | 5 | (function () { 6 | var ipport = document.body.getAttribute('ipport'); 7 | if (!ipport) { 8 | $('#error').html('IP:PORT not found').show(); 9 | return false; 10 | } 11 | 12 | var socket; 13 | var init = function () { 14 | socket = new WebSocket('ws://' + ipport + '/websocket/'); 15 | socket.onopen = onOpen; 16 | socket.onmessage = onMessage; 17 | socket.onclose = function() { 18 | $('[data-connected]').attr('data-connected', 0); 19 | setTimeout(init, 1000); 20 | } 21 | socket.onerror = function() { 22 | $('[data-connected]').attr('data-connected', 0); 23 | $('#error').html('WebSocket error').show(); 24 | }; 25 | }; 26 | 27 | function onOpen() { 28 | $('[data-connected]').attr('data-connected', 1); 29 | $('#error').html('').hide(); 30 | } 31 | 32 | function onMessage(e) { 33 | var data = JSON.parse(e.data); 34 | // console.log("Got data: ", data); 35 | if (STOP) { 36 | return; 37 | } 38 | 39 | var stats = $('#stats'); 40 | if (data && data.stats) { 41 | var tmp = data.stats.uptime.split(':'); 42 | var d = Math.floor(tmp[0] / 24); // days 43 | var h = tmp[0] - d * 24; // hours (always < 24) 44 | stats.find('[data-content="uptime"]').html(data.stats.uptime); 45 | stats.find('[data-content="uptime_h"]').html((d ? (d + 'd ') : '') + h + 'h'); // human friendly 46 | stats.find('[data-content="memory"]').html(data.stats.ram); 47 | stats.find('[data-content="port"]').html(data.stats.port); 48 | $('[data-wwwok]').attr('data-wwwok', data.stats.wwwok ? '1' : '0'); 49 | $('[data-content="maintitle"]').html(data.stats.title); 50 | } 51 | 52 | if (data && data.streams) { 53 | var peersOnPage = {}; // все, имеющиеся на странице 54 | var streamsOnPage = {}; // все, имеющиеся на странице 55 | $('#streams [data-peer]').each(function() { 56 | peersOnPage[$(this).attr('data-peer')] = false; 57 | }); 58 | $('#streams [data-streamid]').each(function() { 59 | streamsOnPage[$(this).attr('data-streamid')] = false; 60 | }); 61 | 62 | var example = $('#examples .app__stream'); 63 | for (var streamid in data.streams) { 64 | var stream = data.streams[streamid]; 65 | var existing = $('#streams [data-streamid="' + streamid + '"]'); 66 | var newrow = existing.length == 1 ? existing : example.clone(); 67 | // определим ширину прогрессбара, причем только активной его части! 68 | var pbWidth = newrow.find('[ui-element="progressbar"] .bar').width(); 69 | streamsOnPage[streamid] = true; 70 | 71 | // fill with data 72 | for (var key in stream) { 73 | // если атрибут имеет не скалярное значение - пока пропускаем 74 | if (['number', 'string', 'boolean'].indexOf($.type(stream[key])) < 0) { 75 | continue; 76 | } 77 | // ставим атрибуты всем. кто их имеет 78 | // этот прием не работает! addBack()+filter().. не ищет атрибуты вложенных эл-в 79 | newrow.addBack().filter('[data-' + key + ']').attr('data-' + key, stream[key]); 80 | // так что добавляем отдельный проход по вложенным эл-м 81 | newrow.find('[data-' + key + ']').attr('data-' + key, stream[key]); 82 | // также заполняем текстовые ноды по атрибутам data-content="..." 83 | newrow.find('[data-content="' + key + '"]').html(stream[key]); 84 | } 85 | 86 | // клиенты 87 | for (var peer in stream.clients) { 88 | var clex = $('#examples .app__stream__client'); 89 | var clexist = newrow.find('[data-peer="' + peer + '"]'); 90 | var clrow = clexist.length == 1 ? clexist : clex.clone(); 91 | var client = stream.clients[peer]; 92 | client.peer = peer; 93 | // уберем клиента из массива для удаления 94 | peersOnPage[peer] = true; 95 | 96 | for (var key in client) { 97 | // ставим атрибуты всем. кто их имеет 98 | clrow.addBack().filter('[data-' + key + ']').attr('data-' + key, client[key]); 99 | clrow.find('[data-' + key + ']').attr('data-' + key, client[key]); 100 | // также заполняем текстовые ноды по атрибутам data-content="..." 101 | clrow.find('[data-content="' + key + '"]').html(client[key]); 102 | } 103 | // client position on progressbar 104 | var leftOffset = Math.round(pbWidth * client.ptrPosition / 100); 105 | clrow.find('div').stop().animate({left: (leftOffset + 'px')}, 150); 106 | clexist.length == 1 || 107 | clrow.appendTo(newrow.find('.app__clients')); 108 | } 109 | 110 | var state = stream.state.toLowerCase(); 111 | if (['close', 'start'].indexOf(state) < 0) { 112 | state = stream.statistics.acestate; 113 | } 114 | // заполнение буфера 115 | var buf_fill_prc = Math.round( stream.bufferedLength / stream.bufferMaxLength * 100); 116 | newrow.find('[ui-element="progressbar"] .bar') 117 | .stop().animate({width: (buf_fill_prc + '%')}); 118 | 119 | var buf_fill_prc = stream.statistics.bufpercent; // прогресс буферизации 120 | var bar2 = newrow.find('[ui-element="progressbar"] .bar2') 121 | .stop().animate({width: (buf_fill_prc + '%')}).show(); 122 | state == 'dl' && bar2.hide(); 123 | 124 | newrow.find('[ui-element="statistics-acestate"][data-acestate]').attr('data-acestate', '' + state); 125 | newrow.find('[ui-element="statistics-acestate"] [data-content="bufpercent"]').html(stream.statistics.bufpercent); 126 | newrow.find('[data-content="bufLenMb"]').html(Math.round(stream.bufferedLength / 1024 / 1024, 1) + 'Mb'); 127 | newrow.attr('data-started', stream.statistics.started); 128 | 129 | // Добавляем в контейнер подходящего типа 130 | if (existing.length == 0) { 131 | newrow.appendTo($('[data-filter-type="' + stream.type + '"]')); 132 | } 133 | } 134 | 135 | // лишних клиентов (отвалившихся) и потоки (закрытые) удаляем 136 | for (var peer in peersOnPage) { 137 | if (peersOnPage[peer]) { 138 | continue; 139 | } 140 | $('[data-peer="' + peer + '"]').remove(); 141 | } 142 | for (var streamid in streamsOnPage) { 143 | if (streamsOnPage[streamid]) { 144 | continue; 145 | } 146 | $('[data-streamid="' + streamid + '"]').remove(); 147 | } 148 | } 149 | if (typeof(data) == 'string') { // log 150 | var example = $('#examples .logline'); 151 | var newrow = example.clone(); 152 | newrow.html(data); 153 | newrow.prependTo($('.logcontainer')); 154 | } 155 | } 156 | 157 | window.addEventListener('load', function () { 158 | init(); 159 | }, false); 160 | })(); 161 | 162 | -------------------------------------------------------------------------------- /res/websrv/js/tween.min.js: -------------------------------------------------------------------------------- 1 | // tween.js - http://github.com/sole/tween.js 2 | 'use strict';var TWEEN=TWEEN||function(){var a=[];return{REVISION:"7",getAll:function(){return a},removeAll:function(){a=[]},add:function(c){a.push(c)},remove:function(c){c=a.indexOf(c);-1!==c&&a.splice(c,1)},update:function(c){if(0===a.length)return!1;for(var b=0,d=a.length,c=void 0!==c?c:Date.now();b(a*=2)?0.5*a*a:-0.5*(--a*(a-2)-1)}},Cubic:{In:function(a){return a*a*a},Out:function(a){return--a*a*a+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*a:0.5*((a-=2)*a*a+2)}},Quartic:{In:function(a){return a*a*a*a},Out:function(a){return 1- --a*a*a*a},InOut:function(a){return 1>(a*=2)?0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)}},Quintic:{In:function(a){return a*a*a* 7 | a*a},Out:function(a){return--a*a*a*a*a+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)}},Sinusoidal:{In:function(a){return 1-Math.cos(a*Math.PI/2)},Out:function(a){return Math.sin(a*Math.PI/2)},InOut:function(a){return 0.5*(1-Math.cos(Math.PI*a))}},Exponential:{In:function(a){return 0===a?0:Math.pow(1024,a-1)},Out:function(a){return 1===a?1:1-Math.pow(2,-10*a)},InOut:function(a){return 0===a?0:1===a?1:1>(a*=2)?0.5*Math.pow(1024,a-1):0.5*(-Math.pow(2,-10*(a-1))+2)}},Circular:{In:function(a){return 1- 8 | Math.sqrt(1-a*a)},Out:function(a){return Math.sqrt(1- --a*a)},InOut:function(a){return 1>(a*=2)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)}},Elastic:{In:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return-(b*Math.pow(2,10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4))},Out:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return b*Math.pow(2,-10*a)*Math.sin((a-c)* 9 | 2*Math.PI/0.4)+1},InOut:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return 1>(a*=2)?-0.5*b*Math.pow(2,10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4):0.5*b*Math.pow(2,-10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4)+1}},Back:{In:function(a){return a*a*(2.70158*a-1.70158)},Out:function(a){return--a*a*(2.70158*a+1.70158)+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*(3.5949095*a-2.5949095):0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)}},Bounce:{In:function(a){return 1- 10 | TWEEN.Easing.Bounce.Out(1-a)},Out:function(a){return a<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375},InOut:function(a){return 0.5>a?0.5*TWEEN.Easing.Bounce.In(2*a):0.5*TWEEN.Easing.Bounce.Out(2*a-1)+0.5}}}; 11 | TWEEN.Interpolation={Linear:function(a,c){var b=a.length-1,d=b*c,e=Math.floor(d),f=TWEEN.Interpolation.Utils.Linear;return 0>c?f(a[0],a[1],d):1b?b:e+1],d-e)},Bezier:function(a,c){var b=0,d=a.length-1,e=Math.pow,f=TWEEN.Interpolation.Utils.Bernstein,h;for(h=0;h<=d;h++)b+=e(1-c,d-h)*e(c,h)*a[h]*f(d,h);return b},CatmullRom:function(a,c){var b=a.length-1,d=b*c,e=Math.floor(d),f=TWEEN.Interpolation.Utils.CatmullRom;return a[0]===a[b]?(0>c&&(e=Math.floor(d=b*(1+c))),f(a[(e- 12 | 1+b)%b],a[e],a[(e+1)%b],a[(e+2)%b],d-e)):0>c?a[0]-(f(a[0],a[0],a[1],a[1],-d)-a[0]):1