api.livecams.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. <?php
  2. /**
  3. * Live cams implementation
  4. */
  5. class LiveCams {
  6. /**
  7. * Contains alter config as key=>value
  8. *
  9. * @var array
  10. */
  11. protected $altCfg = array();
  12. /**
  13. * Chanshots instance placeholder
  14. *
  15. * @var object
  16. */
  17. protected $chanshots = '';
  18. /**
  19. * Cameras instance placeholder
  20. *
  21. * @var object
  22. */
  23. protected $cameras = '';
  24. /**
  25. * Contains all available cameras data as id=>camFullData
  26. *
  27. * @var array
  28. */
  29. protected $allCamerasData = array();
  30. /**
  31. * ACL instance placeholder
  32. *
  33. * @var object
  34. */
  35. protected $acl = '';
  36. /**
  37. * Contains binpaths.ini config as key=>value
  38. *
  39. * @var array
  40. */
  41. protected $binPaths = array();
  42. /**
  43. * Contains ffmpeg binary path
  44. *
  45. * @var string
  46. */
  47. protected $ffmpgPath = '';
  48. /**
  49. * Contains player width by default
  50. *
  51. * @var string
  52. */
  53. protected $playerWidth = '80%';
  54. /**
  55. * Contains system messages helper instance
  56. *
  57. * @var object
  58. */
  59. protected $messages = '';
  60. /**
  61. * Contains stardust process manager instance
  62. *
  63. * @var object
  64. */
  65. protected $stardust = '';
  66. /**
  67. * Contains streams base path for each channel
  68. *
  69. * @var string
  70. */
  71. protected $streamsPath = '';
  72. /**
  73. * Contains sub-streams base path for each channel
  74. *
  75. * @var string
  76. */
  77. protected $subStreamsPath = '';
  78. /**
  79. * Live stream basic options
  80. *
  81. * @var string
  82. */
  83. protected $liveOptsPrefix = '';
  84. /**
  85. * Live stream basic options
  86. *
  87. * @var string
  88. */
  89. protected $liveOptsSuffix = '';
  90. /**
  91. * CliFF instance placeholder
  92. *
  93. * @var object
  94. */
  95. protected $cliff = '';
  96. /**
  97. * Is live-wall enabled flag?
  98. *
  99. * @var bool
  100. */
  101. protected $wallFlag = false;
  102. /**
  103. * other predefined stuff like routes
  104. */
  105. const PID_PREFIX = 'LIVE_';
  106. const SUB_PREFIX = 'LQ_';
  107. const STREAMS_SUBDIR = 'livestreams/';
  108. const SUBSTREAMS_SUBDIR = 'livelq/';
  109. const STREAM_PLAYLIST = 'stream.m3u8';
  110. const SUBSTREAM_PLAYLIST = 'livesub.m3u8';
  111. const LIVECAMSDL_PLAYLIST = 'livecams.m3u8';
  112. const URL_ME = '?module=livecams';
  113. const URL_PSEUDOSTREAM = '?module=pseudostream';
  114. const ROUTE_VIEW = 'livechannel';
  115. const ROUTE_PSEUDOLIVE = 'live';
  116. const ROUTE_PSEUDOSUB = 'sublq';
  117. const ROUTE_LIVEWALL = 'wall';
  118. const ROUTE_DL_PLAYLIST = 'downloadplaylist';
  119. const WRAPPER = '/bin/wrapi';
  120. const OPTION_WALL = 'LIVE_WALL';
  121. public function __construct() {
  122. $this->initMessages();
  123. $this->loadConfigs();
  124. $this->initCliff();
  125. $this->setOptions();
  126. $this->initCameras();
  127. $this->initChanshots();
  128. $this->initAcl();
  129. $this->initStardust();
  130. }
  131. /**
  132. * Inits system messages helper
  133. *
  134. * @return void
  135. */
  136. protected function initMessages() {
  137. $this->messages = new UbillingMessageHelper();
  138. }
  139. /**
  140. * Inits ffmpeg CLI wrapper
  141. *
  142. * @return void
  143. */
  144. protected function initCliff() {
  145. $this->cliff = new CliFF();
  146. }
  147. /**
  148. * Loads some required configs
  149. *
  150. * @global $ubillingConfig
  151. *
  152. * @return void
  153. */
  154. protected function loadConfigs() {
  155. global $ubillingConfig;
  156. $this->binPaths = $ubillingConfig->getBinpaths();
  157. $this->altCfg = $ubillingConfig->getAlter();
  158. }
  159. /**
  160. * Sets required properties depends on config options
  161. *
  162. * @return void
  163. */
  164. protected function setOptions() {
  165. $this->ffmpgPath = $this->binPaths['FFMPG_PATH'];
  166. $this->liveOptsPrefix = $this->cliff->getLiveOptsPrefix();
  167. $this->liveOptsSuffix = $this->cliff->getLiveOptsSuffix();
  168. $this->streamsPath = Storages::PATH_HOWL . self::STREAMS_SUBDIR;
  169. $this->subStreamsPath = Storages::PATH_HOWL . self::SUBSTREAMS_SUBDIR;
  170. if (isset($this->altCfg[self::OPTION_WALL])) {
  171. if ($this->altCfg[self::OPTION_WALL]) {
  172. $this->wallFlag = true;
  173. }
  174. }
  175. }
  176. /**
  177. * Inits chanshots instance for further usage
  178. *
  179. * @return void
  180. */
  181. protected function initChanshots() {
  182. $this->chanshots = new ChanShots();
  183. }
  184. /**
  185. * Inits StarDust process manager
  186. *
  187. * @return void
  188. */
  189. protected function initStardust() {
  190. $this->stardust = new StarDust();
  191. }
  192. /**
  193. * Inits ACL instance
  194. *
  195. * @return void
  196. */
  197. protected function initAcl() {
  198. $this->acl = new ACL();
  199. }
  200. /**
  201. * Inits cameras instance and loads camera full data
  202. *
  203. * @return void
  204. */
  205. protected function initCameras() {
  206. $this->cameras = new Cameras();
  207. $this->allCamerasData = $this->cameras->getAllCamerasFullData();
  208. }
  209. /**
  210. * Lists available cameras as channels shots preview
  211. *
  212. * @return string
  213. */
  214. public function renderList() {
  215. $result = '';
  216. if ($this->acl->haveCamsAssigned()) {
  217. if (!empty($this->allCamerasData)) {
  218. $style = 'style="float: left; margin: 5px;"';
  219. $result .= wf_tag('div');
  220. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  221. if ($this->acl->isMyCamera($eachCameraId)) {
  222. $cameraChannel = $eachCameraData['CAMERA']['channel'];
  223. $channelScreenshot = $this->chanshots->getChannelScreenShot($cameraChannel);
  224. $cameraLabel = $this->cameras->getCameraComment($cameraChannel);
  225. if (empty($channelScreenshot)) {
  226. $channelScreenshot = $this->chanshots::ERR_NOSIG;
  227. } else {
  228. $chanshotValid = $this->chanshots->isChannelScreenshotValid($channelScreenshot);
  229. if (!$chanshotValid) {
  230. $channelScreenshot = $this->chanshots::ERR_CORRUPT;
  231. }
  232. }
  233. if (!$eachCameraData['CAMERA']['active']) {
  234. $channelScreenshot = $this->chanshots::ERR_DISABLD;
  235. }
  236. $result .= wf_tag('div', false, '', $style);
  237. $channelUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $cameraChannel;
  238. $channelImage = wf_img($channelScreenshot, $cameraLabel, 'width: 480px; height: 270px; object-fit: cover;');
  239. $channelLink = wf_Link($channelUrl, $channelImage);
  240. $result .= $channelLink;
  241. $result .= wf_tag('div', true);
  242. }
  243. }
  244. $result .= wf_tag('div', true);
  245. } else {
  246. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  247. }
  248. } else {
  249. $result .= $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  250. }
  251. return ($result);
  252. }
  253. /**
  254. * Retrives user assigned cameras pseudo-streams playlist
  255. *
  256. * @return void
  257. */
  258. public function getLiveCamerasPlayList() {
  259. $playList = '';
  260. $camCount = 0;
  261. if ($this->acl->haveCamsAssigned()) {
  262. if (!empty($this->allCamerasData)) {
  263. $playList .= '#EXTM3U' . PHP_EOL;
  264. $baseUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];
  265. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  266. if ($this->acl->isMyCamera($eachCameraId)) {
  267. if ($eachCameraData['CAMERA']['active']) {
  268. $channelId = $eachCameraData['CAMERA']['channel'];
  269. $channelName = $eachCameraData['CAMERA']['comment'];
  270. $psUrl = $baseUrl . self::URL_PSEUDOSTREAM . '&' . self::ROUTE_PSEUDOLIVE . '=' . $channelId . '&file=' . self::STREAM_PLAYLIST;
  271. $playList .= '#EXTINF:0,' . $channelName . PHP_EOL;
  272. $playList .= $psUrl . PHP_EOL;
  273. $camCount++;
  274. }
  275. }
  276. }
  277. }
  278. }
  279. if ($camCount > 0) {
  280. header("Cache-Control: no-cache, must-revalidate");
  281. header('Content-Type: application/vnd.apple.mpegurl');
  282. header("Content-Transfer-Encoding: Binary");
  283. header("Content-disposition: attachment; filename=\"" . self::LIVECAMSDL_PLAYLIST . "\"");
  284. header("Content-Description: File Transfer");
  285. die($playList);
  286. } else {
  287. $result = $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  288. $result .= wf_delimiter(0);
  289. $result .= wf_BackLink(self::URL_ME);
  290. show_window(__('Oh no'), $result);
  291. }
  292. }
  293. /**
  294. * Lists available cameras live-wall with low-qual substreams
  295. *
  296. * @return string
  297. */
  298. public function renderLiveWall() {
  299. $result = '';
  300. if ($this->acl->haveCamsAssigned()) {
  301. if (!empty($this->allCamerasData)) {
  302. $style = 'style="float: left; margin: 5px; min-height:240px; min-width:320px; border: 0px solid red;"';
  303. $style .= ' id="livewallelement"';
  304. $result .= wf_tag('div');
  305. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  306. if ($this->acl->isMyCamera($eachCameraId)) {
  307. $viewableFlag = true;
  308. $cameraChannel = $eachCameraData['CAMERA']['channel'];
  309. $cameraId = $eachCameraData['CAMERA']['id'];
  310. $channelScreenshot = $this->chanshots->getChannelScreenShot($cameraChannel);
  311. $cameraLabel = $this->cameras->getCameraComment($cameraChannel);
  312. if (empty($channelScreenshot)) {
  313. $channelScreenshot = $this->chanshots::ERR_NOSIG;
  314. $viewableFlag = false;
  315. }
  316. if (!$eachCameraData['CAMERA']['active']) {
  317. $channelScreenshot = $this->chanshots::ERR_DISABLD;
  318. $viewableFlag = false;
  319. }
  320. $result .= wf_tag('div', false, '', $style);
  321. if ($viewableFlag) {
  322. $streamUrl = $this->getSubStreamUrl($cameraChannel);
  323. if ($streamUrl) {
  324. //seems live stream now live
  325. $playerId = 'lqplayer_' . $cameraChannel;
  326. $player = new Player('350px', true);
  327. $player->setPlayerLib('w5');
  328. $result .= $player->renderLivePlayer($streamUrl, $playerId);
  329. $result .= $this->renderSubKeepAliveCallback($cameraId);
  330. } else {
  331. /**
  332. * У твоїх очах був цілий світ
  333. * У твоїх очах тепер пустота
  334. * У твоїх очах була неба синь
  335. * А тепер лише моря печаль
  336. */
  337. $viewableFlag = false;
  338. $result .= wf_img('skins/error.gif', $cameraLabel, 'width: 320px; height: 200px; object-fit: cover;');
  339. }
  340. } else {
  341. $channelUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $cameraChannel;
  342. $channelImage = wf_img($channelScreenshot, $cameraLabel, 'width: 320px; height: 200px; object-fit: cover;');
  343. $channelLink = wf_Link($channelUrl, $channelImage);
  344. $result .= $channelLink;
  345. }
  346. $result .= wf_tag('div', true);
  347. }
  348. }
  349. $result .= wf_tag('div', true);
  350. } else {
  351. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  352. }
  353. } else {
  354. $result .= $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  355. }
  356. return ($result);
  357. }
  358. /**
  359. * Returns all running livestreams real process PID-s array as pid=>processString
  360. *
  361. * @return array
  362. */
  363. protected function getLiveStreamsPids() {
  364. $result = array();
  365. $command = $this->binPaths['PS'] . ' ax | ' . $this->binPaths['GREP'] . ' ' . $this->ffmpgPath . ' | ' . $this->binPaths['GREP'] . ' -v grep';
  366. $rawResult = shell_exec($command);
  367. if (!empty($rawResult)) {
  368. $rawResult = explodeRows($rawResult);
  369. foreach ($rawResult as $io => $eachLine) {
  370. $eachLine = trim($eachLine);
  371. $rawLine = $eachLine;
  372. $eachLine = explode(' ', $eachLine);
  373. if (isset($eachLine[0])) {
  374. $eachPid = $eachLine[0];
  375. if (is_numeric($eachPid)) {
  376. //is this really live stream process?
  377. if (ispos($rawLine, $this->liveOptsSuffix) and ispos($rawLine, self::STREAM_PLAYLIST)) {
  378. $result[$eachPid] = $rawLine;
  379. }
  380. }
  381. }
  382. }
  383. }
  384. return ($result);
  385. }
  386. /**
  387. * Returns all running live sub-streams real process PID-s array as pid=>processString
  388. *
  389. * @return array
  390. */
  391. protected function getLiveSubStreamsPids() {
  392. $result = array();
  393. $command = $this->binPaths['PS'] . ' ax | ' . $this->binPaths['GREP'] . ' ' . $this->ffmpgPath . ' | ' . $this->binPaths['GREP'] . ' -v grep';
  394. $rawResult = shell_exec($command);
  395. if (!empty($rawResult)) {
  396. $rawResult = explodeRows($rawResult);
  397. foreach ($rawResult as $io => $eachLine) {
  398. $eachLine = trim($eachLine);
  399. $rawLine = $eachLine;
  400. $eachLine = explode(' ', $eachLine);
  401. if (isset($eachLine[0])) {
  402. $eachPid = $eachLine[0];
  403. if (is_numeric($eachPid)) {
  404. //is this really live stream process?
  405. if (ispos($rawLine, $this->liveOptsSuffix) and ispos($rawLine, self::SUBSTREAM_PLAYLIST)) {
  406. $result[$eachPid] = $rawLine;
  407. }
  408. }
  409. }
  410. }
  411. }
  412. return ($result);
  413. }
  414. /**
  415. * Returns running cameras live sub-stream processes as cameraId=>realPid
  416. *
  417. * @return array
  418. */
  419. public function getRunningSubStreams() {
  420. $result = array();
  421. if (!empty($this->allCamerasData)) {
  422. $liveStreamPids = $this->getLiveSubStreamsPids();
  423. if (!empty($liveStreamPids)) {
  424. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  425. foreach ($liveStreamPids as $eachPid => $eachProcess) {
  426. $camIp = $eachCameraData['CAMERA']['ip'];
  427. $camLogin = $eachCameraData['CAMERA']['login'];
  428. $camPass = $eachCameraData['CAMERA']['password'];
  429. $camPort = $eachCameraData['TEMPLATE']['RTSP_PORT'];
  430. if (isset($eachCameraData['OPTS'])) {
  431. if (!empty($eachCameraData['OPTS']['rtspport'])) {
  432. $camPort = $eachCameraData['OPTS']['rtspport'];
  433. }
  434. }
  435. //looks familiar?
  436. if (ispos($eachProcess, $camIp) and ispos($eachProcess, $camLogin) and ispos($eachProcess, $camPass) and ispos($eachProcess, ':' . $camPort)) {
  437. $result[$eachCameraId] = $eachPid;
  438. }
  439. }
  440. }
  441. }
  442. }
  443. return ($result);
  444. }
  445. /**
  446. * Returns running cameras live stream processes as cameraId=>realPid
  447. *
  448. * @return array
  449. */
  450. public function getRunningStreams() {
  451. $result = array();
  452. if (!empty($this->allCamerasData)) {
  453. $liveStreamPids = $this->getLiveStreamsPids();
  454. if (!empty($liveStreamPids)) {
  455. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  456. foreach ($liveStreamPids as $eachPid => $eachProcess) {
  457. $camIp = $eachCameraData['CAMERA']['ip'];
  458. $camLogin = $eachCameraData['CAMERA']['login'];
  459. $camPass = $eachCameraData['CAMERA']['password'];
  460. $camPort = $eachCameraData['TEMPLATE']['RTSP_PORT'];
  461. if (isset($eachCameraData['OPTS'])) {
  462. if (!empty($eachCameraData['OPTS']['rtspport'])) {
  463. $camPort = $eachCameraData['OPTS']['rtspport'];
  464. }
  465. }
  466. //looks familiar?
  467. if (ispos($eachProcess, $camIp) and ispos($eachProcess, $camLogin) and ispos($eachProcess, $camPass) and ispos($eachProcess, ':' . $camPort)) {
  468. $result[$eachCameraId] = $eachPid;
  469. }
  470. }
  471. }
  472. }
  473. }
  474. return ($result);
  475. }
  476. /**
  477. * Returns some channel human-readable comment
  478. *
  479. * @param string $channelId
  480. *
  481. * @return string
  482. */
  483. public function getCameraComment($channelId) {
  484. $result = '';
  485. if ($channelId) {
  486. $result .= $this->cameras->getCameraComment($channelId);
  487. }
  488. return ($result);
  489. }
  490. /**
  491. * Allocates streams path, returns it if its writable
  492. *
  493. * @return string|void on error
  494. */
  495. protected function allocateStreamPath($channelId) {
  496. $result = '';
  497. if (!file_exists($this->streamsPath)) {
  498. mkdir($this->streamsPath, 0777);
  499. chmod($this->streamsPath, 0777);
  500. log_register('LIVECAMS ALLOCATED `' . $this->streamsPath . '`');
  501. }
  502. if (file_exists($this->streamsPath)) {
  503. if (is_writable($this->streamsPath)) {
  504. $livePath = $this->streamsPath . $channelId;
  505. if (!file_exists($livePath)) {
  506. mkdir($livePath, 0777);
  507. chmod($livePath, 0777);
  508. log_register('LIVECAMS ALLOCATED `' . $livePath . '`');
  509. }
  510. $result = $livePath . '/';
  511. }
  512. }
  513. return ($result);
  514. }
  515. /**
  516. * Allocates sub-streams path, returns it if its writable
  517. *
  518. * @return string|void on error
  519. */
  520. protected function allocateSubStreamPath($channelId) {
  521. $result = '';
  522. if (!file_exists($this->subStreamsPath)) {
  523. mkdir($this->subStreamsPath, 0777);
  524. chmod($this->subStreamsPath, 0777);
  525. log_register('SUBLIVE ALLOCATED `' . $this->subStreamsPath . '`');
  526. }
  527. if (file_exists($this->subStreamsPath)) {
  528. if (is_writable($this->subStreamsPath)) {
  529. $livePath = $this->subStreamsPath . $channelId;
  530. if (!file_exists($livePath)) {
  531. mkdir($livePath, 0777);
  532. chmod($livePath, 0777);
  533. log_register('SUBLIVE ALLOCATED `' . $livePath . '`');
  534. }
  535. $result = $livePath . '/';
  536. }
  537. }
  538. return ($result);
  539. }
  540. /**
  541. * Starts live stream capture
  542. *
  543. * @return void
  544. */
  545. public function runStream($cameraId) {
  546. $this->stardust->setProcess(self::PID_PREFIX . $cameraId);
  547. if ($this->stardust->notRunning()) {
  548. $this->stardust->start();
  549. if (isset($this->allCamerasData[$cameraId])) {
  550. $cameraData = $this->allCamerasData[$cameraId];
  551. if ($cameraData['CAMERA']['active']) {
  552. $allRunningStreams = $this->getRunningStreams();
  553. if (!isset($allRunningStreams[$cameraId])) {
  554. if (zb_PingICMP($cameraData['CAMERA']['ip'])) {
  555. $channelId = $cameraData['CAMERA']['channel'];
  556. $streamPath = $this->allocateStreamPath($channelId);
  557. if ($cameraData['TEMPLATE']['PROTO'] == 'rtsp') {
  558. //set stream as alive
  559. $streamDog = new StreamDog();
  560. $streamDog->keepAlive($cameraId);
  561. //custom rtsp port is here?
  562. $rtspPort = $cameraData['TEMPLATE']['RTSP_PORT'];
  563. if (isset($cameraData['OPTS'])) {
  564. if (!empty($cameraData['OPTS']['rtspport'])) {
  565. $rtspPort = $cameraData['OPTS']['rtspport'];
  566. }
  567. }
  568. //run live stream capture
  569. $authString = $cameraData['CAMERA']['login'] . ':' . $cameraData['CAMERA']['password'] . '@';
  570. $streamType = $cameraData['TEMPLATE']['MAIN_STREAM'];
  571. $streamUrl = $cameraData['CAMERA']['ip'] . ':' . $rtspPort . $streamType;
  572. $captureFullUrl = "'rtsp://" . $authString . $streamUrl . "'";
  573. $liveCommand = $this->ffmpgPath . ' ' . $this->liveOptsPrefix . ' ' . $captureFullUrl . ' ' . $this->liveOptsSuffix . ' ' . self::STREAM_PLAYLIST;
  574. $fullCommand = 'cd ' . $streamPath . ' && ' . $liveCommand;
  575. shell_exec($fullCommand);
  576. }
  577. } else {
  578. log_register('LIVECAMS NOTSTARTED [' . $cameraId . '] CAMERA NOT ACCESSIBLE');
  579. }
  580. }
  581. } else {
  582. log_register('LIVECAMS NOTSTARTED [' . $cameraId . '] CAMERA DISABLED');
  583. }
  584. }
  585. $this->stardust->stop();
  586. }
  587. }
  588. /**
  589. * Starts live sub-stream capture
  590. *
  591. * @return void
  592. */
  593. public function runSubStream($cameraId) {
  594. $this->stardust->setProcess(self::SUB_PREFIX . $cameraId);
  595. if ($this->stardust->notRunning()) {
  596. $this->stardust->start();
  597. if (isset($this->allCamerasData[$cameraId])) {
  598. $cameraData = $this->allCamerasData[$cameraId];
  599. if ($cameraData['CAMERA']['active']) {
  600. $allRunningStreams = $this->getRunningSubStreams();
  601. if (!isset($allRunningStreams[$cameraId])) {
  602. if (zb_PingICMP($cameraData['CAMERA']['ip'])) {
  603. $channelId = $cameraData['CAMERA']['channel'];
  604. $streamPath = $this->allocateSubStreamPath($channelId);
  605. if ($cameraData['TEMPLATE']['PROTO'] == 'rtsp') {
  606. if ($cameraData['TEMPLATE']['SUB_STREAM']) {
  607. //set stream as alive
  608. $streamDog = new StreamDog();
  609. $streamDog->keepSubAlive($cameraId);
  610. //custom rtsp port is here?
  611. $rtspPort = $cameraData['TEMPLATE']['RTSP_PORT'];
  612. if (isset($cameraData['OPTS'])) {
  613. if (!empty($cameraData['OPTS']['rtspport'])) {
  614. $rtspPort = $cameraData['OPTS']['rtspport'];
  615. }
  616. }
  617. //run live stream capture
  618. $authString = $cameraData['CAMERA']['login'] . ':' . $cameraData['CAMERA']['password'] . '@';
  619. $streamType = $cameraData['TEMPLATE']['SUB_STREAM'];
  620. $streamUrl = $cameraData['CAMERA']['ip'] . ':' . $rtspPort . $streamType;
  621. $captureFullUrl = "'rtsp://" . $authString . $streamUrl . "'";
  622. $liveCommand = $this->ffmpgPath . ' ' . $this->liveOptsPrefix . ' ' . $captureFullUrl . ' ' . $this->liveOptsSuffix . ' ' . self::SUBSTREAM_PLAYLIST;
  623. $fullCommand = 'cd ' . $streamPath . ' && ' . $liveCommand;
  624. shell_exec($fullCommand);
  625. } else {
  626. log_register('LIVESUB NOTSTARTED [' . $cameraId . '] SUBSTREAM NOT SPECIFIED');
  627. }
  628. }
  629. } else {
  630. log_register('LIVESUB NOTSTARTED [' . $cameraId . '] CAMERA NOT ACCESSIBLE');
  631. }
  632. }
  633. } else {
  634. log_register('LIVESUB NOTSTARTED [' . $cameraId . '] CAMERA DISABLED');
  635. }
  636. }
  637. $this->stardust->stop();
  638. }
  639. }
  640. /**
  641. * Destroys live stream. Returns true if stream was alive.
  642. *
  643. * @return bool
  644. */
  645. public function stopStream($cameraId) {
  646. $result = false;
  647. $cameraId = ubRouting::filters($cameraId, 'int');
  648. $allRunningStreams = $this->getRunningStreams();
  649. //is camera live stream running?
  650. if (isset($allRunningStreams[$cameraId])) {
  651. //killing stream process
  652. $streamPid = $allRunningStreams[$cameraId];
  653. $command = $this->binPaths['SUDO'] . ' ' . $this->binPaths['KILL'] . ' -9 ' . $streamPid;
  654. shell_exec($command);
  655. //livestream location cleanup
  656. if (isset($this->allCamerasData[$cameraId])) {
  657. $cameraData = $this->allCamerasData[$cameraId];
  658. $channelId = $cameraData['CAMERA']['channel'];
  659. $streamPath = $this->allocateStreamPath($channelId);
  660. if (file_exists($streamPath)) {
  661. $playListPath = $streamPath . self::STREAM_PLAYLIST;
  662. if (file_exists($playListPath)) {
  663. unlink($playListPath);
  664. }
  665. }
  666. }
  667. $result = true;
  668. }
  669. return ($result);
  670. }
  671. /**
  672. * Destroys live sub-stream. Returns true if stream was alive.
  673. *
  674. * @return bool
  675. */
  676. public function stopSubStream($cameraId) {
  677. $result = false;
  678. $cameraId = ubRouting::filters($cameraId, 'int');
  679. $allRunningStreams = $this->getRunningSubStreams();
  680. //is camera live stream running?
  681. if (isset($allRunningStreams[$cameraId])) {
  682. //killing stream process
  683. $streamPid = $allRunningStreams[$cameraId];
  684. $command = $this->binPaths['SUDO'] . ' ' . $this->binPaths['KILL'] . ' -9 ' . $streamPid;
  685. shell_exec($command);
  686. //livestream location cleanup
  687. if (isset($this->allCamerasData[$cameraId])) {
  688. $cameraData = $this->allCamerasData[$cameraId];
  689. $channelId = $cameraData['CAMERA']['channel'];
  690. $streamPath = $this->allocateSubStreamPath($channelId);
  691. if (file_exists($streamPath)) {
  692. $playListPath = $streamPath . self::SUBSTREAM_PLAYLIST;
  693. if (file_exists($playListPath)) {
  694. unlink($playListPath);
  695. }
  696. }
  697. }
  698. $result = true;
  699. }
  700. return ($result);
  701. }
  702. /**
  703. * Returns live stream full URL
  704. *
  705. * @param string $channelId
  706. *
  707. * @return string
  708. */
  709. public function getStreamUrl($channelId) {
  710. $result = '';
  711. $streamPath = $this->allocateStreamPath($channelId);
  712. if ($streamPath) {
  713. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  714. if ($cameraId) {
  715. $this->stardust->setProcess(self::PID_PREFIX . $cameraId);
  716. if ($this->stardust->notRunning()) {
  717. $this->stardust->runBackgroundProcess(self::WRAPPER . ' "liveswarm&cameraid=' . $cameraId . '"', 1);
  718. }
  719. $fullStreamUrl = $streamPath . self::STREAM_PLAYLIST;
  720. if (file_exists($fullStreamUrl)) {
  721. $result = $fullStreamUrl;
  722. } else {
  723. $retries = 5;
  724. for ($i = 0; $i < $retries; $i++) {
  725. sleep(1);
  726. if (file_exists($fullStreamUrl)) {
  727. $result = $fullStreamUrl;
  728. break;
  729. }
  730. }
  731. }
  732. }
  733. }
  734. return ($result);
  735. }
  736. /**
  737. * Returns live sub-stream full URL
  738. *
  739. * @param string $channelId
  740. *
  741. * @return string
  742. */
  743. public function getSubStreamUrl($channelId) {
  744. $result = '';
  745. $streamPath = $this->allocateSubStreamPath($channelId);
  746. if ($streamPath) {
  747. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  748. if ($cameraId) {
  749. $this->stardust->setProcess(self::SUB_PREFIX . $cameraId);
  750. if ($this->stardust->notRunning()) {
  751. $this->stardust->runBackgroundProcess(self::WRAPPER . ' "subswarm&cameraid=' . $cameraId . '"', 1);
  752. }
  753. $fullStreamUrl = $streamPath . self::SUBSTREAM_PLAYLIST;
  754. if (file_exists($fullStreamUrl)) {
  755. $result = $fullStreamUrl;
  756. } else {
  757. $retries = 5;
  758. for ($i = 0; $i < $retries; $i++) {
  759. sleep(1);
  760. if (file_exists($fullStreamUrl)) {
  761. $result = $fullStreamUrl;
  762. break;
  763. }
  764. }
  765. }
  766. }
  767. }
  768. return ($result);
  769. }
  770. /**
  771. * Renders camera keep alive container
  772. *
  773. * @param int $cameraId
  774. *
  775. * @return string
  776. */
  777. protected function renderKeepAliveCallback($cameraId) {
  778. $result = '';
  779. $streamDog = new StreamDog();
  780. $timeout = 10000; // in ms
  781. $keepAliveLink = self::URL_ME . '&' . StreamDog::ROUTE_KEEPALIVE . '=' . $cameraId;
  782. //preventing stream destroy before first callback
  783. $streamDog->keepAlive($cameraId);
  784. //appending periodic requests code
  785. $result .= $streamDog->getKeepAliveCallback($keepAliveLink, $timeout);
  786. return ($result);
  787. }
  788. /**
  789. * Renders camera sub-stream keep alive container
  790. *
  791. * @param int $cameraId
  792. *
  793. * @return string
  794. */
  795. protected function renderSubKeepAliveCallback($cameraId) {
  796. $result = '';
  797. $streamDog = new StreamDog();
  798. $timeout = 10000; // in ms
  799. $keepAliveLink = self::URL_ME . '&' . StreamDog::ROUTE_KEEPSUBALIVE . '=' . $cameraId;
  800. //preventing stream destroy before first callback
  801. $streamDog->keepAlive($cameraId);
  802. //appending periodic requests code
  803. $result .= $streamDog->getKeepAliveCallback($keepAliveLink, $timeout);
  804. return ($result);
  805. }
  806. /**
  807. * Returns channel live stream preview
  808. *
  809. * @param string $channelId
  810. *
  811. * @return string
  812. */
  813. public function renderLive($channelId) {
  814. $result = '';
  815. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  816. $cameraControls = wf_BackLink(self::URL_ME);
  817. if ($cameraId) {
  818. $cameraData = $this->allCamerasData[$cameraId];
  819. if ($cameraData['CAMERA']['active']) {
  820. $streamUrl = $this->getStreamUrl($channelId);
  821. if ($streamUrl) {
  822. //seems live stream now live
  823. $playerId = 'liveplayer_' . $channelId;
  824. $player = new Player($this->playerWidth, true);
  825. $result .= $player->renderLivePlayer($streamUrl, $playerId);
  826. $result .= $this->renderKeepAliveCallback($cameraId);
  827. } else {
  828. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('No such live stream'), 'error');
  829. }
  830. } else {
  831. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('Camera disabled now'), 'error');
  832. }
  833. if (cfr('CAMERAS')) {
  834. $cameraControls .= wf_Link(Cameras::URL_ME . '&' . Cameras::ROUTE_EDIT . '=' . $cameraData['CAMERA']['id'], wf_img('skins/icon_camera_small.png') . ' ' . __('Camera'), false, 'ubButton');
  835. }
  836. if (cfr('ARCHIVE')) {
  837. $cameraControls .= wf_Link(Archive::URL_ME . '&' . Archive::ROUTE_VIEW . '=' . $cameraData['CAMERA']['channel'], wf_img('skins/icon_archive_small.png') . ' ' . __('Video from camera'), false, 'ubButton');
  838. }
  839. if (cfr('EXPORT')) {
  840. $cameraControls .= wf_Link(Export::URL_ME . '&' . Export::ROUTE_CHANNEL . '=' . $cameraData['CAMERA']['channel'], wf_img('skins/icon_export.png') . ' ' . __('Save record'), false, 'ubButton');
  841. }
  842. } else {
  843. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('No such camera'), 'error');
  844. }
  845. $result .= wf_delimiter();
  846. $result .= $cameraControls;
  847. return ($result);
  848. }
  849. /**
  850. * Returns pseudo-live stream HLS playlist
  851. *
  852. * @param string $channelId
  853. *
  854. * @return string
  855. */
  856. public function getPseudoStream($channelId) {
  857. $result = '';
  858. $streamUrl = $this->getStreamUrl($channelId);
  859. if (!empty($streamUrl)) {
  860. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  861. $playlistBody = file_get_contents($streamUrl);
  862. $prefix = Storages::PATH_HOWL . self::STREAMS_SUBDIR . $channelId . '/';
  863. if (!empty($playlistBody)) {
  864. $playlistBody = explodeRows($playlistBody);
  865. foreach ($playlistBody as $io => $eachLine) {
  866. if (!empty($eachLine)) {
  867. if (!ispos($eachLine, '#')) {
  868. $eachLine = $prefix . $eachLine;
  869. }
  870. $result .= $eachLine . PHP_EOL;
  871. }
  872. }
  873. //keeping stream alive
  874. if ($cameraId) {
  875. $streamDog = new StreamDog();
  876. $streamDog->keepAlive($cameraId);
  877. }
  878. }
  879. }
  880. return ($result);
  881. }
  882. /**
  883. * Returns the title controls based on the livewall flag.
  884. *
  885. * @return string The title controls.
  886. */
  887. public function getTitleControls() {
  888. $result = '';
  889. if ($this->wallFlag) {
  890. if (ubRouting::checkGet(self::ROUTE_LIVEWALL)) {
  891. $result .= wf_Link(self::URL_ME, wf_img('skins/surveillance2_32.png', __('List')));
  892. } else {
  893. $result .= wf_Link(self::URL_ME . '&' . self::ROUTE_LIVEWALL . '=true', wf_img('skins/surveillance3_32.png', __('Live')));
  894. }
  895. $result .= wf_Link(self::URL_ME . '&' . self::ROUTE_DL_PLAYLIST . '=true', wf_img('skins/list32.png', __('Playlist')));
  896. }
  897. return ($result);
  898. }
  899. }