api.archive.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <?php
  2. /**
  3. * VOD archive implementation
  4. */
  5. class Archive {
  6. /**
  7. * Contains alter.ini config as key=>value
  8. *
  9. * @var array
  10. */
  11. protected $altCfg = array();
  12. /**
  13. * Contains binpaths.ini config as key=>value
  14. *
  15. * @var array
  16. */
  17. protected $binPaths = array();
  18. /**
  19. * Cameras instance placeholder
  20. *
  21. * @var object
  22. */
  23. protected $cameras = '';
  24. /**
  25. * Contains full cameras data as
  26. *
  27. * @var array
  28. */
  29. protected $allCamerasData = array();
  30. /**
  31. * Storages instance placeholder.
  32. *
  33. * @var object
  34. */
  35. protected $storages = '';
  36. /**
  37. * Contains ffmpeg binary path
  38. *
  39. * @var string
  40. */
  41. protected $ffmpgPath = '';
  42. /**
  43. * System messages helper instance placeholder
  44. *
  45. * @var object
  46. */
  47. protected $messages = '';
  48. /**
  49. * Contains chunk time in seconds
  50. */
  51. protected $chunkTime = 60;
  52. /**
  53. * Contains player width by default
  54. *
  55. * @var string
  56. */
  57. protected $playerWidth = '70%';
  58. /**
  59. * ACL instance placeholder
  60. *
  61. * @var object
  62. */
  63. protected $acl = '';
  64. /**
  65. * other predefined stuff like routes
  66. */
  67. const PLAYLIST_MASK = '_playlist.txt';
  68. const URL_ME = '?module=archive';
  69. const ROUTE_VIEW = 'viewchannel';
  70. const ROUTE_SHOWDATE = 'renderdatearchive';
  71. const ROUTE_TIMESEGMENT = 'tseg';
  72. public function __construct() {
  73. $this->initMessages();
  74. $this->loadConfigs();
  75. $this->setOptions();
  76. $this->initStorages();
  77. $this->initCameras();
  78. }
  79. /**
  80. * Loads some required configs
  81. *
  82. * @global $ubillingConfig
  83. *
  84. * @return void
  85. */
  86. protected function loadConfigs() {
  87. global $ubillingConfig;
  88. $this->binPaths = $ubillingConfig->getBinpaths();
  89. $this->altCfg = $ubillingConfig->getAlter();
  90. }
  91. /**
  92. * Sets required properties depends on config options
  93. *
  94. * @return void
  95. */
  96. protected function setOptions() {
  97. $this->ffmpgPath = $this->binPaths['FFMPG_PATH'];
  98. $this->chunkTime = $this->altCfg['RECORDER_CHUNK_TIME'];
  99. }
  100. /**
  101. * Inits system messages helper
  102. *
  103. * @return void
  104. */
  105. protected function initMessages() {
  106. $this->messages = new UbillingMessageHelper();
  107. }
  108. /**
  109. * Inits cameras into protected prop and loads its full data
  110. *
  111. * @return void
  112. */
  113. protected function initCameras() {
  114. $this->cameras = new Cameras();
  115. $this->allCamerasData = $this->cameras->getAllCamerasFullData();
  116. }
  117. /**
  118. * Inits storages into protected prop for further usage
  119. *
  120. * @return void
  121. */
  122. protected function initStorages() {
  123. $this->storages = new Storages();
  124. }
  125. /**
  126. * Inits ACL instance
  127. *
  128. * @return void
  129. */
  130. protected function initAcl() {
  131. $this->acl = new ACL();
  132. }
  133. /**
  134. * Renders available cameras list
  135. *
  136. * @return string
  137. */
  138. public function renderCamerasList() {
  139. $result = '';
  140. $this->initAcl();
  141. if ($this->acl->haveCamsAssigned()) {
  142. $allStotagesData = $this->storages->getAllStoragesData();
  143. if (!empty($allStotagesData)) {
  144. if (!empty($this->allCamerasData)) {
  145. $screenshots = new ChanShots();
  146. $cells = '';
  147. if (cfr('CAMERAS')) {
  148. $cells .= wf_TableCell(__('ID'));
  149. $cells .= wf_TableCell(__('IP'));
  150. }
  151. $cells .= wf_TableCell(__('Description'));
  152. $cells .= wf_TableCell(__('Actions'));
  153. $rows = wf_TableRow($cells, 'row1');
  154. foreach ($this->allCamerasData as $io => $each) {
  155. $eachCamId = $each['CAMERA']['id'];
  156. if ($this->acl->isMyCamera($eachCamId)) {
  157. $eachCamIp = $each['CAMERA']['ip'];
  158. $eachCamDesc = $each['CAMERA']['comment'];
  159. $eachCamChannel = $each['CAMERA']['channel'];
  160. $cells = '';
  161. if (cfr('CAMERAS')) {
  162. $cells .= wf_TableCell($eachCamId);
  163. $cells .= wf_TableCell($eachCamIp, '', '', 'sorttable_customkey="' . ip2int($eachCamIp) . '"');
  164. }
  165. $eachCamUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $eachCamChannel;
  166. $camPreview = '';
  167. $chanShot = $screenshots->getChannelScreenShot($eachCamChannel);
  168. if (empty($chanShot)) {
  169. $chanShot = $screenshots::ERR_NOSIG;
  170. } else {
  171. $chanshotValid=$screenshots->isChannelScreenshotValid($chanShot);
  172. if (!$chanshotValid) {
  173. $chanShot=$screenshots::ERR_CORRUPT;
  174. }
  175. }
  176. if (!$each['CAMERA']['active']) {
  177. $chanShot = $screenshots::ERR_DISABLD;
  178. }
  179. $camPreview = $screenshots->renderListBox($eachCamChannel, $chanShot);
  180. $cells .= wf_TableCell(wf_Link($eachCamUrl, $camPreview . $eachCamDesc, false, 'camlink', 'id="camlink' . $eachCamChannel . '"'));
  181. $actLinks = wf_Link($eachCamUrl, wf_img('skins/icon_play_small.png', __('View')));
  182. $cells .= wf_TableCell($actLinks);
  183. $rows .= wf_TableRow($cells, 'row5');
  184. }
  185. }
  186. $result .= wf_TableBody($rows, '100%', 0, 'sortable resp-table');
  187. } else {
  188. $result .= $this->messages->getStyledMessage(__('Cameras') . ': ' . __('Nothing to show'), 'warning');
  189. }
  190. } else {
  191. $result .= $this->messages->getStyledMessage(__('Storages') . ': ' . __('Nothing found'), 'warning');
  192. }
  193. } else {
  194. $result .= $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  195. }
  196. return($result);
  197. }
  198. /**
  199. * Renders howl player for previously generated playlist
  200. *
  201. * @param string $playlistPath - full playlist path
  202. * @param bool $autoPlay - start playback right now?
  203. * @param string $playerId - must be equal to channel name to access playlist in DOM
  204. *
  205. * @return string
  206. */
  207. protected function renderArchivePlayer($playlistPath, $autoPlay = false, $playerId = '') {
  208. $plStart = '';
  209. if (!ubRouting::checkGet(self::ROUTE_SHOWDATE)) {
  210. $fewMinsAgo = strtotime("-5 minute", time());
  211. $fewMinsAgo = date("H:i", $fewMinsAgo);
  212. $plStart = ', plstart:"s_' . $fewMinsAgo . '"';
  213. }
  214. //explict time segment setup
  215. if (ubRouting::checkGet(self::ROUTE_TIMESEGMENT)) {
  216. $plStart = ', plstart:"s_' . ubRouting::get(self::ROUTE_TIMESEGMENT, 'mres') . '"';
  217. }
  218. $player = new Player($this->playerWidth, $autoPlay);
  219. $result = $player->renderPlaylistPlayer($playlistPath, $plStart, $playerId);
  220. return($result);
  221. }
  222. /**
  223. * Allocates array with full timeline as hh:mm=>0
  224. *
  225. * @return array
  226. */
  227. public function allocDayTimeline() {
  228. $result = array();
  229. for ($h = 0; $h <= 23; $h++) {
  230. for ($m = 0; $m < 60; $m++) {
  231. $hLabel = ($h > 9) ? $h : '0' . $h;
  232. $mLabel = ($m > 9) ? $m : '0' . $m;
  233. $timeLabel = $hLabel . ':' . $mLabel;
  234. $result[$timeLabel] = 0;
  235. }
  236. }
  237. return($result);
  238. }
  239. /**
  240. * Renders recordings availability due some day of month
  241. *
  242. * @return string
  243. */
  244. protected function renderDayRecordsAvailTimeline($chunksList, $date) {
  245. $result = '';
  246. if (!empty($chunksList)) {
  247. $dayMinAlloc = $this->allocDayTimeline();
  248. $chunksByDay = 0;
  249. $curDate = curdate();
  250. $fewMinAgo = strtotime("-5 minute", time());
  251. $fewMinLater = strtotime("+1 minute", time());
  252. foreach ($chunksList as $timeStamp => $eachChunk) {
  253. $dayOfMonth = date("Y-m-d", $timeStamp);
  254. if ($dayOfMonth == $date) {
  255. $timeOfDay = date("H:i", $timeStamp);
  256. if (isset($dayMinAlloc[$timeOfDay])) {
  257. $dayMinAlloc[$timeOfDay] = 1;
  258. $chunksByDay++;
  259. }
  260. }
  261. }
  262. //any records here?
  263. if ($chunksByDay) {
  264. if ($chunksByDay > 3) {
  265. $barWidth = 0.064;
  266. $barStyle = 'width:' . $barWidth . '%;';
  267. $result = wf_tag('div', false, '', 'style = "width:' . $this->playerWidth . ';"');
  268. foreach ($dayMinAlloc as $eachMin => $recAvail) {
  269. $recAvailBar = ($recAvail) ? 'skins/rec_avail.png' : 'skins/rec_unavail.png';
  270. if ($curDate == $date) {
  271. $eachMinTs = strtotime($date . ' ' . $eachMin . ':00');
  272. if (zb_isTimeStampBetween($fewMinAgo, $fewMinLater, $eachMinTs)) {
  273. $recAvailBar = 'skins/rec_now.png';
  274. }
  275. }
  276. $recAvailTitle = ($recAvail) ? $eachMin : $eachMin . ' - ' . __('No record');
  277. $timeBarLabel = wf_img($recAvailBar, $recAvailTitle, $barStyle);
  278. if ($recAvail) {
  279. $timeSeg = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . ubRouting::get(self::ROUTE_VIEW) . '&' . self::ROUTE_SHOWDATE . '=' . $date . '&' . self::ROUTE_TIMESEGMENT . '=' . $eachMin;
  280. $result .= trim(wf_Link($timeSeg, $timeBarLabel));
  281. } else {
  282. $result .= $timeBarLabel;
  283. }
  284. }
  285. $result .= wf_tag('div', true);
  286. }
  287. $result .= wf_delimiter(0);
  288. }
  289. }
  290. return($result);
  291. }
  292. /**
  293. * Renders basic timeline for some chunks list
  294. *
  295. * @param string $channelId
  296. * @param array $chunksList
  297. *
  298. * @return string
  299. */
  300. protected function renderDaysTimeline($channelId, $chunksList) {
  301. $result = '';
  302. $channelId = ubRouting::filters($channelId, 'mres');
  303. $dayPointer = ubRouting::checkGet(self::ROUTE_SHOWDATE) ? ubRouting::get(self::ROUTE_SHOWDATE) : curdate();
  304. if (!empty($chunksList)) {
  305. $datesTmp = array();
  306. foreach ($chunksList as $timeStamp => $chunkName) {
  307. $chunkDate = date("Y-m-d", $timeStamp);
  308. if (!isset($datesTmp[$chunkDate])) {
  309. $datesTmp[$chunkDate] = 1;
  310. } else {
  311. $datesTmp[$chunkDate]++;
  312. }
  313. }
  314. if (!empty($datesTmp)) {
  315. //day timeline here
  316. $result .= $this->renderDayRecordsAvailTimeline($chunksList, $dayPointer);
  317. //optional neural fast objects search
  318. if ($this->altCfg['NEURAL_ENABLED']) {
  319. $nobjSearch = new NeuralObjSearch();
  320. $result .= $nobjSearch->renderContainer();
  321. }
  322. $chunkTime = $this->altCfg['RECORDER_CHUNK_TIME'];
  323. foreach ($datesTmp as $eachDate => $chunksCount) {
  324. $justDay = date("d", strtotime($eachDate));
  325. $baseUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $channelId . '&' . self::ROUTE_SHOWDATE . '=' . $eachDate;
  326. $recordsTime = wr_formatTimeArchive($chunksCount * $chunkTime);
  327. $buttonIcon = ($eachDate == $dayPointer) ? 'skins/icon_play_small.png' : 'skins/icon_calendar.gif';
  328. $result .= wf_Link($baseUrl, wf_img($buttonIcon, $eachDate . ' - ' . $recordsTime) . ' ' . $justDay, false, 'ubButton') . ' ';
  329. }
  330. $result .= wf_CleanDiv();
  331. }
  332. }
  333. return($result);
  334. }
  335. /**
  336. * Saves playlist and returns its path in howl ready for player rendering
  337. *
  338. * @param int $cameraId
  339. * @param string $dateFrom
  340. * @param string $dateTo
  341. * @param array $chunksList
  342. *
  343. * @return string/void on error
  344. */
  345. protected function generateArchivePlaylist($cameraId, $dateFrom, $dateTo, $chunksList = array()) {
  346. $result = '';
  347. $cameraId = ubRouting::filters($cameraId, 'int');
  348. if (isset($this->allCamerasData[$cameraId])) {
  349. $curDate = curdate();
  350. $dateFromTs = strtotime($dateFrom . ' 00:00:00');
  351. $dateToTs = strtotime($dateFrom . ' 23:59:59');
  352. $minuteBetweenNow = strtotime('-1 minute', time());
  353. $cameraData = $this->allCamerasData[$cameraId]['CAMERA'];
  354. $cameraStorageData = $this->allCamerasData[$cameraId]['STORAGE'];
  355. $storagePath = $cameraStorageData['path'];
  356. $storagePathLastChar = substr($storagePath, 0, -1);
  357. if ($storagePathLastChar != '/') {
  358. $storagePath = $storagePath . '/';
  359. }
  360. $howlChunkPath = Storages::PATH_HOWL . '/';
  361. if (empty($chunksList)) {
  362. $chunksList = $this->storages->getChannelChunks($cameraData['storageid'], $cameraData['channel']);
  363. }
  364. $filteredChunks = array();
  365. if (!empty($chunksList)) {
  366. foreach ($chunksList as $chunkTimeStamp => $chunkPath) {
  367. if (zb_isTimeStampBetween($dateFromTs, $dateToTs, $chunkTimeStamp)) {
  368. $howlChunkFullPath = str_replace($storagePath, $howlChunkPath, $chunkPath);
  369. $howlChunkFullPath = str_replace('//', '/', $howlChunkFullPath);
  370. //excluding last minute chunk - it may be unfinished now
  371. if ($chunkTimeStamp < $minuteBetweenNow) {
  372. $filteredChunks[$chunkTimeStamp] = $howlChunkFullPath;
  373. }
  374. }
  375. }
  376. }
  377. //generating playlist
  378. if (!empty($filteredChunks)) {
  379. $playListPath = Storages::PATH_HOWL . $cameraData['channel'] . self::PLAYLIST_MASK;
  380. $segmentsCount = sizeof($filteredChunks);
  381. $playlistContent = '[' . PHP_EOL;
  382. $i = 0;
  383. foreach ($filteredChunks as $chunkTimeStamp => $chunkFile) {
  384. $i++;
  385. $chunkTitle = date("Y-m-d H:i:s", $chunkTimeStamp);
  386. $segmentId = 's' . '_' . date("H:i", $chunkTimeStamp);
  387. $playlistContent .= '{"title":"' . $chunkTitle . '","file":"' . $chunkFile . '","id":"' . $segmentId . '"}';
  388. if ($i < $segmentsCount) {
  389. $playlistContent .= ',';
  390. }
  391. $playlistContent .= PHP_EOL;
  392. }
  393. $playlistContent .= ']' . PHP_EOL;
  394. file_put_contents($playListPath, $playlistContent);
  395. $result = $playListPath;
  396. }
  397. }
  398. return($result);
  399. }
  400. /**
  401. * Renders basic archive lookup interface
  402. *
  403. * @param string $channelId
  404. *
  405. * @return string
  406. */
  407. public function renderLookup($channelId) {
  408. $result = '';
  409. $channelId = ubRouting::filters($channelId, 'mres');
  410. //camera ID lookup by channel
  411. $allCamerasChannels = $this->cameras->getAllCamerasChannels();
  412. $cameraId = (isset($allCamerasChannels[$channelId])) ? $allCamerasChannels[$channelId] : 0;
  413. if ($cameraId) {
  414. if (isset($this->allCamerasData[$cameraId])) {
  415. $cameraData = $this->allCamerasData[$cameraId]['CAMERA'];
  416. $showDate = (ubRouting::checkGet(self::ROUTE_SHOWDATE)) ? ubRouting::get(self::ROUTE_SHOWDATE, 'mres') : curdate();
  417. $chunksList = $this->storages->getChannelChunks($cameraData['storageid'], $cameraData['channel']);
  418. if (!empty($chunksList)) {
  419. $archivePlayList = $this->generateArchivePlaylist($cameraId, $showDate, $showDate, $chunksList);
  420. if ($archivePlayList) {
  421. $result .= $this->renderArchivePlayer($archivePlayList, true, $cameraData['channel']);
  422. } else {
  423. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  424. $result .= wf_delimiter(0);
  425. }
  426. //some timeline here
  427. $result .= $this->renderDaysTimeline($channelId, $chunksList);
  428. } else {
  429. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  430. }
  431. } else {
  432. $result .= $this->messages->getStyledMessage(__('Camera') . ' [' . $cameraId . '] ' . __('not exists'), 'error');
  433. }
  434. } else {
  435. $result .= $this->messages->getStyledMessage(__('Camera') . ' ' . __('with channel') . ' `' . $channelId . '` ' . __('not exists'), 'error');
  436. }
  437. $result .= wf_delimiter(1);
  438. $result .= wf_BackLink(self::URL_ME);
  439. if (cfr('CAMERAS')) {
  440. if ($cameraId) {
  441. $result .= wf_Link(Cameras::URL_ME . '&' . Cameras::ROUTE_EDIT . '=' . $cameraId, wf_img('skins/icon_camera_small.png') . ' ' . __('Camera'), false, 'ubButton');
  442. }
  443. }
  444. if (cfr('LIVECAMS')) {
  445. $result .= wf_Link(LiveCams::URL_ME . '&' . LiveCams::ROUTE_VIEW . '=' . $channelId, wf_img('skins/icon_live_small.png') . ' ' . __('Live'), false, 'ubButton');
  446. }
  447. if (cfr('EXPORT')) {
  448. $result .= wf_Link(Export::URL_ME . '&' . Export::ROUTE_CHANNEL . '=' . $channelId, wf_img('skins/icon_export.png') . ' ' . __('Save record'), false, 'ubButton');
  449. }
  450. if ($this->altCfg['NEURAL_ENABLED']) {
  451. $neurSearchUrl = self::URL_ME . '&' . NeuralObjSearch::ROUTE_CHAN_DETECT . '=' . $channelId . '&' . NeuralObjSearch::ROUTE_DATE . '=' . $showDate;
  452. $result .= wf_AjaxLink($neurSearchUrl, web_icon_search() . ' ' . __('Objects search'), NeuralObjSearch::AJAX_CONTAINER, false, 'ubButton') . ' ';
  453. }
  454. return($result);
  455. }
  456. /**
  457. * Returns some channel human-readable comment
  458. *
  459. * @param string $channelId
  460. *
  461. * @return string
  462. */
  463. public function getCameraComment($channelId) {
  464. $result = '';
  465. if ($channelId) {
  466. $result .= $this->cameras->getCameraComment($channelId);
  467. }
  468. return($result);
  469. }
  470. }