api.livecams.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <?php
  2. /**
  3. * Live cams implementation
  4. */
  5. class LiveCams {
  6. /**
  7. * Chanshots instance placeholder
  8. *
  9. * @var object
  10. */
  11. protected $chanshots = '';
  12. /**
  13. * Cameras instance placeholder
  14. *
  15. * @var object
  16. */
  17. protected $cameras = '';
  18. /**
  19. * Contains all available cameras data as id=>camFullData
  20. *
  21. * @var array
  22. */
  23. protected $allCamerasData = array();
  24. /**
  25. * ACL instance placeholder
  26. *
  27. * @var object
  28. */
  29. protected $acl = '';
  30. /**
  31. * Contains binpaths.ini config as key=>value
  32. *
  33. * @var array
  34. */
  35. protected $binPaths = array();
  36. /**
  37. * Contains ffmpeg binary path
  38. *
  39. * @var string
  40. */
  41. protected $ffmpgPath = '';
  42. /**
  43. * Contains player width by default
  44. *
  45. * @var string
  46. */
  47. protected $playerWidth = '80%';
  48. /**
  49. * Contains system messages helper instance
  50. *
  51. * @var object
  52. */
  53. protected $messages = '';
  54. /**
  55. * Contains stardust process manager instance
  56. *
  57. * @var object
  58. */
  59. protected $stardust = '';
  60. /**
  61. * Contains streams base path for each channel
  62. *
  63. * @var string
  64. */
  65. protected $streamsPath = '';
  66. /**
  67. * Live stream basic options
  68. *
  69. * @var string
  70. */
  71. protected $liveOptsPrefix = '';
  72. /**
  73. * Live stream basic options
  74. *
  75. * @var string
  76. */
  77. protected $liveOptsSuffix = '';
  78. /**
  79. * CliFF instance placeholder
  80. *
  81. * @var object
  82. */
  83. protected $cliff = '';
  84. /**
  85. * other predefined stuff like routes
  86. */
  87. const PID_PREFIX = 'LIVE_';
  88. const STREAMS_SUBDIR = 'livestreams/';
  89. const STREAM_PLAYLIST = 'stream.m3u8';
  90. const URL_ME = '?module=livecams';
  91. const URL_PSEUDOSTREAM = '?module=pseudostream';
  92. const ROUTE_VIEW = 'livechannel';
  93. const ROUTE_PSEUDOLIVE = 'live';
  94. const WRAPPER = '/bin/wrapi';
  95. public function __construct() {
  96. $this->initMessages();
  97. $this->loadConfigs();
  98. $this->initCliff();
  99. $this->setOptions();
  100. $this->initCameras();
  101. $this->initChanshots();
  102. $this->initAcl();
  103. $this->initStardust();
  104. }
  105. /**
  106. * Inits system messages helper
  107. *
  108. * @return void
  109. */
  110. protected function initMessages() {
  111. $this->messages = new UbillingMessageHelper();
  112. }
  113. /**
  114. * Inits ffmpeg CLI wrapper
  115. *
  116. * @return void
  117. */
  118. protected function initCliff() {
  119. $this->cliff = new CliFF();
  120. }
  121. /**
  122. * Loads some required configs
  123. *
  124. * @global $ubillingConfig
  125. *
  126. * @return void
  127. */
  128. protected function loadConfigs() {
  129. global $ubillingConfig;
  130. $this->binPaths = $ubillingConfig->getBinpaths();
  131. }
  132. /**
  133. * Sets required properties depends on config options
  134. *
  135. * @return void
  136. */
  137. protected function setOptions() {
  138. $this->ffmpgPath = $this->binPaths['FFMPG_PATH'];
  139. $this->liveOptsPrefix = $this->cliff->getLiveOptsPrefix();
  140. $this->liveOptsSuffix = $this->cliff->getLiveOptsSuffix();
  141. $this->streamsPath = Storages::PATH_HOWL . self::STREAMS_SUBDIR;
  142. }
  143. /**
  144. * Inits chanshots instance for further usage
  145. *
  146. * @return void
  147. */
  148. protected function initChanshots() {
  149. $this->chanshots = new ChanShots();
  150. }
  151. /**
  152. * Inits StarDust process manager
  153. *
  154. * @return void
  155. */
  156. protected function initStardust() {
  157. $this->stardust = new StarDust();
  158. }
  159. /**
  160. * Inits ACL instance
  161. *
  162. * @return void
  163. */
  164. protected function initAcl() {
  165. $this->acl = new ACL();
  166. }
  167. /**
  168. * Inits cameras instance and loads camera full data
  169. *
  170. * @return void
  171. */
  172. protected function initCameras() {
  173. $this->cameras = new Cameras();
  174. $this->allCamerasData = $this->cameras->getAllCamerasFullData();
  175. }
  176. /**
  177. * Lists available cameras
  178. *
  179. * @return string
  180. */
  181. public function renderList() {
  182. $result = '';
  183. if ($this->acl->haveCamsAssigned()) {
  184. if (!empty($this->allCamerasData)) {
  185. $style = 'style="float: left; margin: 5px;"';
  186. $result .= wf_tag('div');
  187. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  188. if ($this->acl->isMyCamera($eachCameraId)) {
  189. $cameraChannel = $eachCameraData['CAMERA']['channel'];
  190. $channelScreenshot = $this->chanshots->getChannelScreenShot($cameraChannel);
  191. $cameraLabel = $this->cameras->getCameraComment($cameraChannel);
  192. if (empty($channelScreenshot)) {
  193. $channelScreenshot = 'skins/nosignal.gif';
  194. }
  195. if (!$eachCameraData['CAMERA']['active']) {
  196. $channelScreenshot = 'skins/chanblock.gif';
  197. }
  198. $result .= wf_tag('div', false, '', $style);
  199. $channelUrl = self::URL_ME . '&' . self::ROUTE_VIEW . '=' . $cameraChannel;
  200. $channelImage = wf_img($channelScreenshot, $cameraLabel, 'width: 480px; height: 270px; object-fit: cover;');
  201. $channelLink = wf_Link($channelUrl, $channelImage);
  202. $result .= $channelLink;
  203. $result .= wf_tag('div', true);
  204. }
  205. }
  206. $result .= wf_tag('div', true);
  207. } else {
  208. $result .= $this->messages->getStyledMessage(__('Nothing to show'), 'warning');
  209. }
  210. } else {
  211. $result .= $this->messages->getStyledMessage(__('No assigned cameras to show'), 'warning');
  212. }
  213. return($result);
  214. }
  215. /**
  216. * Returns all running livestreams real process PID-s array as pid=>processString
  217. *
  218. * @return array
  219. */
  220. protected function getLiveStreamsPids() {
  221. $result = array();
  222. $command = $this->binPaths['PS'] . ' ax | ' . $this->binPaths['GREP'] . ' ' . $this->ffmpgPath . ' | ' . $this->binPaths['GREP'] . ' -v grep';
  223. $rawResult = shell_exec($command);
  224. if (!empty($rawResult)) {
  225. $rawResult = explodeRows($rawResult);
  226. foreach ($rawResult as $io => $eachLine) {
  227. $eachLine = trim($eachLine);
  228. $rawLine = $eachLine;
  229. $eachLine = explode(' ', $eachLine);
  230. if (isset($eachLine[0])) {
  231. $eachPid = $eachLine[0];
  232. if (is_numeric($eachPid)) {
  233. //is this really live stream process?
  234. if (ispos($rawLine, $this->liveOptsSuffix) AND ispos($rawLine, self::STREAM_PLAYLIST)) {
  235. $result[$eachPid] = $rawLine;
  236. }
  237. }
  238. }
  239. }
  240. }
  241. return($result);
  242. }
  243. /**
  244. * Returns running cameras live stream processes as cameraId=>realPid
  245. *
  246. * @return array
  247. */
  248. public function getRunningStreams() {
  249. $result = array();
  250. if (!empty($this->allCamerasData)) {
  251. $liveStreamPids = $this->getLiveStreamsPids();
  252. if (!empty($liveStreamPids)) {
  253. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  254. foreach ($liveStreamPids as $eachPid => $eachProcess) {
  255. //looks familiar?
  256. if (ispos($eachProcess, $eachCameraData['CAMERA']['ip']) AND ispos($eachProcess, $eachCameraData['CAMERA']['login'])) {
  257. $result[$eachCameraId] = $eachPid;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. return($result);
  264. }
  265. /**
  266. * Returns some channel human-readable comment
  267. *
  268. * @param string $channelId
  269. *
  270. * @return string
  271. */
  272. public function getCameraComment($channelId) {
  273. $result = '';
  274. if ($channelId) {
  275. $result .= $this->cameras->getCameraComment($channelId);
  276. }
  277. return($result);
  278. }
  279. /**
  280. * Allocates streams path, returns it if its writable
  281. *
  282. * @return string/void on error
  283. */
  284. protected function allocateStreamPath($channelId) {
  285. $result = '';
  286. if (!file_exists($this->streamsPath)) {
  287. mkdir($this->streamsPath, 0777);
  288. chmod($this->streamsPath, 0777);
  289. log_register('LIVECAMS ALLOCATED `' . $this->streamsPath . '`');
  290. }
  291. if (file_exists($this->streamsPath)) {
  292. if (is_writable($this->streamsPath)) {
  293. $livePath = $this->streamsPath . $channelId;
  294. if (!file_exists($livePath)) {
  295. mkdir($livePath, 0777);
  296. chmod($livePath, 0777);
  297. log_register('LIVECAMS ALLOCATED `' . $livePath . '`');
  298. }
  299. $result = $livePath . '/';
  300. }
  301. }
  302. return($result);
  303. }
  304. /**
  305. * Starts live stream capture
  306. *
  307. * @return void
  308. */
  309. public function runStream($cameraId) {
  310. $this->stardust->setProcess(self::PID_PREFIX . $cameraId);
  311. if ($this->stardust->notRunning()) {
  312. $this->stardust->start();
  313. if (isset($this->allCamerasData[$cameraId])) {
  314. $cameraData = $this->allCamerasData[$cameraId];
  315. if ($cameraData['CAMERA']['active']) {
  316. $allRunningStreams = $this->getRunningStreams();
  317. if (!isset($allRunningStreams[$cameraId])) {
  318. if (zb_PingICMP($cameraData['CAMERA']['ip'])) {
  319. $channelId = $cameraData['CAMERA']['channel'];
  320. $streamPath = $this->allocateStreamPath($channelId);
  321. if ($cameraData['TEMPLATE']['PROTO'] == 'rtsp') {
  322. //set stream as alive
  323. $streamDog = new StreamDog();
  324. $streamDog->keepAlive($cameraId);
  325. //run live stream capture
  326. $authString = $cameraData['CAMERA']['login'] . ':' . $cameraData['CAMERA']['password'] . '@';
  327. $streamType = $cameraData['TEMPLATE']['MAIN_STREAM']; //TODO: may be configurable in future?
  328. $streamUrl = $cameraData['CAMERA']['ip'] . ':' . $cameraData['TEMPLATE']['RTSP_PORT'] . $streamType;
  329. $captureFullUrl = "'rtsp://" . $authString . $streamUrl . "'";
  330. $liveCommand = $this->ffmpgPath . ' ' . $this->liveOptsPrefix . ' ' . $captureFullUrl . ' ' . $this->liveOptsSuffix . ' ' . self::STREAM_PLAYLIST;
  331. $fullCommand = 'cd ' . $streamPath . ' && ' . $liveCommand;
  332. shell_exec($fullCommand);
  333. }
  334. } else {
  335. log_register('LIVECAMS NOTSTARTED [' . $cameraId . '] CAMERA NOT ACCESSIBLE');
  336. }
  337. }
  338. } else {
  339. log_register('LIVECAMS NOTSTARTED [' . $cameraId . '] CAMERA DISABLED');
  340. }
  341. }
  342. $this->stardust->stop();
  343. }
  344. }
  345. /**
  346. * Destroys live stream. Returns true if stream was alive.
  347. *
  348. * @return bool
  349. */
  350. public function stopStream($cameraId) {
  351. $result = false;
  352. $cameraId = ubRouting::filters($cameraId, 'int');
  353. $allRunningStreams = $this->getRunningStreams();
  354. //is camera live stream running?
  355. if (isset($allRunningStreams[$cameraId])) {
  356. //killing stream process
  357. $streamPid = $allRunningStreams[$cameraId];
  358. $command = $this->binPaths['SUDO'] . ' ' . $this->binPaths['KILL'] . ' -9 ' . $streamPid;
  359. shell_exec($command);
  360. //livestream location cleanup
  361. if (isset($this->allCamerasData[$cameraId])) {
  362. $cameraData = $this->allCamerasData[$cameraId];
  363. $channelId = $cameraData['CAMERA']['channel'];
  364. $streamPath = $this->allocateStreamPath($channelId);
  365. if (file_exists($streamPath)) {
  366. $playListPath = $streamPath . self::STREAM_PLAYLIST;
  367. if (file_exists($playListPath)) {
  368. unlink($playListPath);
  369. }
  370. }
  371. }
  372. $result = true;
  373. }
  374. return($result);
  375. }
  376. /**
  377. * Returns live stream full URL
  378. *
  379. * @param string $channelId
  380. *
  381. * @return string
  382. */
  383. public function getStreamUrl($channelId) {
  384. $result = '';
  385. $streamPath = $this->allocateStreamPath($channelId);
  386. if ($streamPath) {
  387. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  388. if ($cameraId) {
  389. $this->stardust->setProcess(self::PID_PREFIX . $cameraId);
  390. if ($this->stardust->notRunning()) {
  391. $this->stardust->runBackgroundProcess(self::WRAPPER . ' "liveswarm&cameraid=' . $cameraId . '"', 1);
  392. }
  393. $fullStreamUrl = $streamPath . self::STREAM_PLAYLIST;
  394. if (file_exists($fullStreamUrl)) {
  395. $result = $fullStreamUrl;
  396. } else {
  397. $retries = 5;
  398. for ($i = 0; $i < $retries; $i++) {
  399. sleep(1);
  400. if (file_exists($fullStreamUrl)) {
  401. $result = $fullStreamUrl;
  402. break;
  403. }
  404. }
  405. }
  406. }
  407. }
  408. return($result);
  409. }
  410. /**
  411. * Renders camera keep alive container
  412. *
  413. * @param int $cameraId
  414. *
  415. * @return string
  416. */
  417. protected function renderKeepAliveCallback($cameraId) {
  418. $result = '';
  419. $streamDog = new StreamDog();
  420. $timeout = 10000; // in ms
  421. $keepAliveLink = self::URL_ME . '&' . StreamDog::ROUTE_KEEPALIVE . '=' . $cameraId;
  422. //preventing stream destroy before first callback
  423. $streamDog->keepAlive($cameraId);
  424. //appending periodic requests code
  425. $result .= $streamDog->getKeepAliveCallback($keepAliveLink, $timeout);
  426. return($result);
  427. }
  428. /**
  429. * Returns channel live stream preview
  430. *
  431. * @param string $channelId
  432. *
  433. * @return string
  434. */
  435. public function renderLive($channelId) {
  436. $result = '';
  437. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  438. $cameraControls = wf_BackLink(self::URL_ME);
  439. if ($cameraId) {
  440. $cameraData = $this->allCamerasData[$cameraId];
  441. if ($cameraData['CAMERA']['active']) {
  442. $streamUrl = $this->getStreamUrl($channelId);
  443. if ($streamUrl) {
  444. //seems live stream now live
  445. $playerId = 'liveplayer_' . $channelId;
  446. $player = new Player($this->playerWidth, true);
  447. $result .= $player->renderLivePlayer($streamUrl, $playerId);
  448. $result .= $this->renderKeepAliveCallback($cameraId);
  449. } else {
  450. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('No such live stream'), 'error');
  451. }
  452. } else {
  453. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('Camera disabled now'), 'error');
  454. }
  455. if (cfr('CAMERAS')) {
  456. $cameraControls .= wf_Link(Cameras::URL_ME . '&' . Cameras::ROUTE_EDIT . '=' . $cameraData['CAMERA']['id'], wf_img('skins/icon_camera_small.png') . ' ' . __('Camera'), false, 'ubButton');
  457. }
  458. if (cfr('ARCHIVE')) {
  459. $cameraControls .= wf_Link(Archive::URL_ME . '&' . Archive::ROUTE_VIEW . '=' . $cameraData['CAMERA']['channel'], wf_img('skins/icon_archive_small.png') . ' ' . __('Video from camera'), false, 'ubButton');
  460. }
  461. if (cfr('EXPORT')) {
  462. $cameraControls .= wf_Link(Export::URL_ME . '&' . Export::ROUTE_CHANNEL . '=' . $cameraData['CAMERA']['channel'], wf_img('skins/icon_export.png') . ' ' . __('Save record'), false, 'ubButton');
  463. }
  464. } else {
  465. $result .= $this->messages->getStyledMessage(__('Oh no') . ': ' . __('No such camera'), 'error');
  466. }
  467. $result .= wf_delimiter();
  468. $result .= $cameraControls;
  469. return($result);
  470. }
  471. /**
  472. * Returns pseudo-live stream HLS playlist
  473. *
  474. * @param string $channelId
  475. *
  476. * @return string
  477. */
  478. public function getPseudoStream($channelId) {
  479. $result = '';
  480. $streamUrl = $this->getStreamUrl($channelId);
  481. if (!empty($streamUrl)) {
  482. $cameraId = $this->cameras->getCameraIdByChannel($channelId);
  483. $playlistBody = file_get_contents($streamUrl);
  484. $prefix = Storages::PATH_HOWL . self::STREAMS_SUBDIR . $channelId . '/';
  485. if (!empty($playlistBody)) {
  486. $playlistBody = explodeRows($playlistBody);
  487. foreach ($playlistBody as $io => $eachLine) {
  488. if (!empty($eachLine)) {
  489. if (!ispos($eachLine, '#')) {
  490. $eachLine = $prefix . $eachLine;
  491. }
  492. $result .= $eachLine . PHP_EOL;
  493. }
  494. }
  495. //keeping stream alive
  496. if ($cameraId) {
  497. $streamDog = new StreamDog();
  498. $streamDog->keepAlive($cameraId);
  499. }
  500. }
  501. }
  502. return($result);
  503. }
  504. }