api.archive.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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 = '100%';
  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. $result.=wf_tag('div',false,'cameraslist');
  147. foreach ($this->allCamerasData as $io => $each) {
  148. $eachCamId = $each['CAMERA']['id'];
  149. if ($this->acl->isMyCamera($eachCamId)) {
  150. $eachCamDesc = $each['CAMERA']['comment'];
  151. $eachCamChannel = $each['CAMERA']['channel'];
  152. $eachCamUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $eachCamChannel;
  153. $camPreview = '';
  154. $chanShot = $screenshots->getChannelScreenShot($eachCamChannel);
  155. if (empty($chanShot)) {
  156. $chanShot = $screenshots::ERR_NOSIG;
  157. } else {
  158. $chanshotValid = $screenshots->isChannelScreenshotValid($chanShot);
  159. if (!$chanshotValid) {
  160. $chanShot = $screenshots::ERR_CORRUPT;
  161. } else {
  162. //replacing chanshot url with base64 encoded image
  163. $embedData = $screenshots->getLastCheckedShot();
  164. if (!empty($embedData)) {
  165. $chanShot = $embedData;
  166. }
  167. }
  168. }
  169. if (!$each['CAMERA']['active']) {
  170. $chanShot = $screenshots::ERR_DISABLD;
  171. }
  172. $camPreview = wf_img($chanShot,$eachCamDesc);
  173. $containerId='wrcamcont_'.$eachCamDesc;
  174. $result.=wf_tag('div',false,'','id="'.$containerId.'"');
  175. $camInfo=wf_tag('div',false,'camera-info').$eachCamDesc.wf_tag('div',true);
  176. $result.=wf_Link($eachCamUrl, $camPreview . $camInfo, false, 'camera-item');
  177. $result.=wf_tag('div',true);
  178. }
  179. }
  180. $result.=wf_tag('div',true);
  181. $result.=wf_AjaxContainer('wrqsstatus','','');
  182. } else {
  183. $result .= $this->messages->getStyledMessage(__('Cameras') . ': ' . __('Nothing to show'), 'warning');
  184. }
  185. } else {
  186. $result .= $this->messages->getStyledMessage(__('Storages') . ': ' . __('Nothing found'), 'warning');
  187. }
  188. } else {
  189. $result .= $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  190. }
  191. return ($result);
  192. }
  193. /**
  194. * Renders howl player for previously generated playlist
  195. *
  196. * @param string $playlistPath - full playlist path
  197. * @param bool $autoPlay - start playback right now?
  198. * @param string $playerId - must be equal to channel name to access playlist in DOM
  199. *
  200. * @return string
  201. */
  202. protected function renderArchivePlayer($playlistPath, $autoPlay = false, $playerId = '') {
  203. $plStart = '';
  204. if (!ubRouting::checkGet(self::ROUTE_SHOWDATE)) {
  205. $fewMinsAgo = strtotime("-5 minute", time());
  206. $fewMinsAgo = date("H:i", $fewMinsAgo);
  207. $plStart = ', plstart:"s_' . $fewMinsAgo . '"';
  208. }
  209. //explict time segment setup
  210. if (ubRouting::checkGet(self::ROUTE_TIMESEGMENT)) {
  211. $plStart = ', plstart:"s_' . ubRouting::get(self::ROUTE_TIMESEGMENT, 'mres') . '"';
  212. }
  213. $player = new Player($this->playerWidth, $autoPlay);
  214. $result = $player->renderPlaylistPlayer($playlistPath, $plStart, $playerId);
  215. return ($result);
  216. }
  217. /**
  218. * Allocates array with full timeline as hh:mm=>0
  219. *
  220. * @return array
  221. */
  222. public function allocDayTimeline() {
  223. $result = array();
  224. for ($h = 0; $h <= 23; $h++) {
  225. for ($m = 0; $m < 60; $m++) {
  226. $hLabel = ($h > 9) ? $h : '0' . $h;
  227. $mLabel = ($m > 9) ? $m : '0' . $m;
  228. $timeLabel = $hLabel . ':' . $mLabel;
  229. $result[$timeLabel] = 0;
  230. }
  231. }
  232. return ($result);
  233. }
  234. /**
  235. * Renders recordings availability due some day of month
  236. *
  237. * @return string
  238. */
  239. protected function renderDayRecordsAvailTimeline($chunksList, $date) {
  240. $result = '';
  241. if (!empty($chunksList)) {
  242. $dayMinAlloc = $this->allocDayTimeline();
  243. $chunksByDay = 0;
  244. $curDate = curdate();
  245. $fewMinAgo = strtotime("-5 minute", time());
  246. $fewMinLater = strtotime("+1 minute", time());
  247. foreach ($chunksList as $timeStamp => $eachChunk) {
  248. $dayOfMonth = date("Y-m-d", $timeStamp);
  249. if ($dayOfMonth == $date) {
  250. $timeOfDay = date("H:i", $timeStamp);
  251. if (isset($dayMinAlloc[$timeOfDay])) {
  252. $dayMinAlloc[$timeOfDay] = 1;
  253. $chunksByDay++;
  254. }
  255. }
  256. }
  257. //any records here?
  258. if ($chunksByDay) {
  259. if ($chunksByDay > 3) {
  260. $barWidth = 0.064;
  261. $barStyle = 'width:' . $barWidth . '%;';
  262. $result = wf_tag('div', false, 'rectimeline', '');
  263. foreach ($dayMinAlloc as $eachMin => $recAvail) {
  264. $recAvailBar = ($recAvail) ? 'skins/rec_avail.png' : 'skins/rec_unavail.png';
  265. if ($curDate == $date) {
  266. $eachMinTs = strtotime($date . ' ' . $eachMin . ':00');
  267. if (zb_isTimeStampBetween($fewMinAgo, $fewMinLater, $eachMinTs)) {
  268. $recAvailBar = 'skins/rec_now.png';
  269. }
  270. }
  271. $recAvailTitle = ($recAvail) ? $eachMin : $eachMin . ' - ' . __('No record');
  272. $timeBarLabel = wf_img($recAvailBar, $recAvailTitle, $barStyle);
  273. if ($recAvail) {
  274. $timeSeg = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . ubRouting::get(self::ROUTE_VIEW) . '&' . self::ROUTE_SHOWDATE . '=' . $date . '&' . self::ROUTE_TIMESEGMENT . '=' . $eachMin;
  275. $result .= trim(wf_Link($timeSeg, $timeBarLabel));
  276. } else {
  277. $result .= $timeBarLabel;
  278. }
  279. }
  280. $result .= wf_tag('div', true);
  281. }
  282. $result .= wf_delimiter(0);
  283. }
  284. }
  285. return ($result);
  286. }
  287. /**
  288. * Renders basic timeline for some chunks list
  289. *
  290. * @param string $channelId
  291. * @param array $chunksList
  292. *
  293. * @return string
  294. */
  295. protected function renderDaysTimeline($channelId, $chunksList) {
  296. $result = '';
  297. $channelId = ubRouting::filters($channelId, 'mres');
  298. $dayPointer = ubRouting::checkGet(self::ROUTE_SHOWDATE) ? ubRouting::get(self::ROUTE_SHOWDATE) : curdate();
  299. if (!empty($chunksList)) {
  300. $datesTmp = array();
  301. foreach ($chunksList as $timeStamp => $chunkName) {
  302. $chunkDate = date("Y-m-d", $timeStamp);
  303. if (!isset($datesTmp[$chunkDate])) {
  304. $datesTmp[$chunkDate] = 1;
  305. } else {
  306. $datesTmp[$chunkDate]++;
  307. }
  308. }
  309. if (!empty($datesTmp)) {
  310. //day timeline here
  311. $result .= $this->renderDayRecordsAvailTimeline($chunksList, $dayPointer);
  312. //optional neural fast objects search
  313. if ($this->altCfg['NEURAL_ENABLED']) {
  314. $nobjSearch = new NeuralObjSearch();
  315. $result .= $nobjSearch->renderContainer();
  316. }
  317. $chunkTime = $this->altCfg['RECORDER_CHUNK_TIME'];
  318. foreach ($datesTmp as $eachDate => $chunksCount) {
  319. $justDay = date("d", strtotime($eachDate));
  320. $baseUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $channelId . '&' . self::ROUTE_SHOWDATE . '=' . $eachDate;
  321. $recordsTime = wr_formatTimeArchive($chunksCount * $chunkTime);
  322. $buttonIcon = ($eachDate == $dayPointer) ? 'skins/icon_play_small.png' : 'skins/icon_calendar.gif';
  323. $result .= wf_Link($baseUrl, wf_img($buttonIcon, $eachDate . ' - ' . $recordsTime) . ' ' . $justDay, false, 'ubButton') . ' ';
  324. }
  325. $result .= wf_CleanDiv();
  326. }
  327. }
  328. return ($result);
  329. }
  330. /**
  331. * Saves playlist and returns its path in howl ready for player rendering
  332. *
  333. * @param int $cameraId
  334. * @param string $dateFrom
  335. * @param string $dateTo
  336. * @param array $chunksList
  337. *
  338. * @return string/void on error
  339. */
  340. protected function generateArchivePlaylist($cameraId, $dateFrom, $dateTo, $chunksList = array()) {
  341. $result = '';
  342. $cameraId = ubRouting::filters($cameraId, 'int');
  343. if (isset($this->allCamerasData[$cameraId])) {
  344. $curDate = curdate();
  345. $dateFromTs = strtotime($dateFrom . ' 00:00:00');
  346. $dateToTs = strtotime($dateFrom . ' 23:59:59');
  347. $minuteBetweenNow = strtotime('-1 minute', time());
  348. $cameraData = $this->allCamerasData[$cameraId]['CAMERA'];
  349. $cameraStorageData = $this->allCamerasData[$cameraId]['STORAGE'];
  350. $storagePath = $cameraStorageData['path'];
  351. $storagePathLastChar = substr($storagePath, 0, -1);
  352. if ($storagePathLastChar != '/') {
  353. $storagePath = $storagePath . '/';
  354. }
  355. $howlChunkPath = Storages::PATH_HOWL . '/';
  356. if (empty($chunksList)) {
  357. $chunksList = $this->storages->getChannelChunks($cameraData['storageid'], $cameraData['channel']);
  358. }
  359. $filteredChunks = array();
  360. if (!empty($chunksList)) {
  361. foreach ($chunksList as $chunkTimeStamp => $chunkPath) {
  362. if (zb_isTimeStampBetween($dateFromTs, $dateToTs, $chunkTimeStamp)) {
  363. $howlChunkFullPath = str_replace($storagePath, $howlChunkPath, $chunkPath);
  364. $howlChunkFullPath = str_replace('//', '/', $howlChunkFullPath);
  365. //excluding last minute chunk - it may be unfinished now
  366. if ($chunkTimeStamp < $minuteBetweenNow) {
  367. $filteredChunks[$chunkTimeStamp] = $howlChunkFullPath;
  368. }
  369. }
  370. }
  371. }
  372. //generating playlist
  373. if (!empty($filteredChunks)) {
  374. $playListPath = Storages::PATH_HOWL . $cameraData['channel'] . self::PLAYLIST_MASK;
  375. $segmentsCount = sizeof($filteredChunks);
  376. $playlistContent = '[' . PHP_EOL;
  377. $i = 0;
  378. foreach ($filteredChunks as $chunkTimeStamp => $chunkFile) {
  379. $i++;
  380. $chunkTitle = date("Y-m-d H:i:s", $chunkTimeStamp);
  381. $segmentId = 's' . '_' . date("H:i", $chunkTimeStamp);
  382. $playlistContent .= '{"title":"' . $chunkTitle . '","file":"' . $chunkFile . '","id":"' . $segmentId . '"}';
  383. if ($i < $segmentsCount) {
  384. $playlistContent .= ',';
  385. }
  386. $playlistContent .= PHP_EOL;
  387. }
  388. $playlistContent .= ']' . PHP_EOL;
  389. file_put_contents($playListPath, $playlistContent);
  390. $result = $playListPath;
  391. }
  392. }
  393. return ($result);
  394. }
  395. /**
  396. * Renders basic archive lookup interface
  397. *
  398. * @param string $channelId
  399. *
  400. * @return string
  401. */
  402. public function renderLookup($channelId) {
  403. $result = '';
  404. $channelId = ubRouting::filters($channelId, 'mres');
  405. //camera ID lookup by channel
  406. $allCamerasChannels = $this->cameras->getAllCamerasChannels();
  407. $cameraId = (isset($allCamerasChannels[$channelId])) ? $allCamerasChannels[$channelId] : 0;
  408. if ($cameraId) {
  409. if (isset($this->allCamerasData[$cameraId])) {
  410. $cameraData = $this->allCamerasData[$cameraId]['CAMERA'];
  411. $showDate = (ubRouting::checkGet(self::ROUTE_SHOWDATE)) ? ubRouting::get(self::ROUTE_SHOWDATE, 'mres') : curdate();
  412. $chunksList = $this->storages->getChannelChunks($cameraData['storageid'], $cameraData['channel']);
  413. if (!empty($chunksList)) {
  414. $archivePlayList = $this->generateArchivePlaylist($cameraId, $showDate, $showDate, $chunksList);
  415. if ($archivePlayList) {
  416. $result .= $this->renderArchivePlayer($archivePlayList, true, $cameraData['channel']);
  417. } else {
  418. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  419. $result .= wf_delimiter(0);
  420. }
  421. //some timeline here
  422. $result .= $this->renderDaysTimeline($channelId, $chunksList);
  423. } else {
  424. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  425. }
  426. } else {
  427. $result .= $this->messages->getStyledMessage(__('Camera') . ' [' . $cameraId . '] ' . __('not exists'), 'error');
  428. }
  429. } else {
  430. $result .= $this->messages->getStyledMessage(__('Camera') . ' ' . __('with channel') . ' `' . $channelId . '` ' . __('not exists'), 'error');
  431. }
  432. $result .= wf_delimiter(1);
  433. $result .= wf_BackLink(self::URL_ME);
  434. if (cfr('CAMERAS')) {
  435. if ($cameraId) {
  436. $result .= wf_Link(Cameras::URL_ME . '&' . Cameras::ROUTE_EDIT . '=' . $cameraId, wf_img('skins/icon_camera_small.png') . ' ' . __('Camera'), false, 'ubButton');
  437. }
  438. }
  439. if (cfr('LIVECAMS')) {
  440. $result .= wf_Link(LiveCams::URL_ME . '&' . LiveCams::ROUTE_VIEW . '=' . $channelId, wf_img('skins/icon_live_small.png') . ' ' . __('Live'), false, 'ubButton');
  441. }
  442. if (cfr('EXPORT')) {
  443. $result .= wf_Link(Export::URL_ME . '&' . Export::ROUTE_CHANNEL . '=' . $channelId, wf_img('skins/icon_export.png') . ' ' . __('Save record'), false, 'ubButton');
  444. }
  445. if ($this->altCfg['NEURAL_ENABLED']) {
  446. $neurSearchUrl = self::URL_ME . '&' . NeuralObjSearch::ROUTE_CHAN_DETECT . '=' . $channelId . '&' . NeuralObjSearch::ROUTE_DATE . '=' . $showDate;
  447. $result .= wf_AjaxLink($neurSearchUrl, web_icon_search() . ' ' . __('Objects search'), NeuralObjSearch::AJAX_CONTAINER, false, 'ubButton') . ' ';
  448. }
  449. return ($result);
  450. }
  451. /**
  452. * Returns some channel human-readable comment
  453. *
  454. * @param string $channelId
  455. *
  456. * @return string
  457. */
  458. public function getCameraComment($channelId) {
  459. $result = '';
  460. if ($channelId) {
  461. $result .= $this->cameras->getCameraComment($channelId);
  462. }
  463. return ($result);
  464. }
  465. }