api.recorder.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <?php
  2. /**
  3. * Camera streams capture/recording implementation
  4. */
  5. class Recorder {
  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 stardust process manager instance
  26. *
  27. * @var object
  28. */
  29. protected $stardust = '';
  30. /**
  31. * Contains full cameras data as
  32. *
  33. * @var array
  34. */
  35. protected $allCamerasData = array();
  36. /**
  37. * Storages instance placeholder.
  38. *
  39. * @var object
  40. */
  41. protected $storages = '';
  42. /**
  43. * Contains ffmpeg binary path
  44. *
  45. * @var string
  46. */
  47. protected $ffmpgPath = '';
  48. /**
  49. * System messages helper instance placeholder
  50. *
  51. * @var object
  52. */
  53. protected $messages = '';
  54. /**
  55. * Debugging mode flag
  56. *
  57. * @var bool
  58. */
  59. protected $debugFlag = false;
  60. /**
  61. * CliFF instance placeholder
  62. *
  63. * @var object
  64. */
  65. protected $cliff = '';
  66. /**
  67. * some creepy params here
  68. */
  69. protected $transportTemplate = '';
  70. protected $recordOpts = '';
  71. protected $audioCapture = '';
  72. protected $supressOutput = '';
  73. /**
  74. * Some predefined stuff
  75. */
  76. const PID_PREFIX = 'RECORD_';
  77. const CAPTURE_PID = 'CAPTURE';
  78. const WRAPPER = '/bin/wrapi';
  79. const CHUNKS_MASK = '%s';
  80. const CHUNKS_EXT = '.mp4';
  81. const DEBUG_LOG = 'exports/recorder_debug.log';
  82. /**
  83. * Dinosaurs are my best friends
  84. * Through thick and thin, until the very end
  85. * People tell me, do not pretend
  86. * Stop living in your made up world again
  87. * But the dinosaurs, they're real to me
  88. * They bring me up and make me happy
  89. * Hold on now, I think I see
  90. * A dinosaur wants to play with me
  91. */
  92. public function __construct() {
  93. $this->initMessages();
  94. $this->loadConfigs();
  95. $this->initCliff();
  96. $this->setOptions();
  97. $this->initStardust();
  98. $this->initStorages();
  99. $this->initCameras();
  100. }
  101. /**
  102. * Loads some required configs
  103. *
  104. * @global $ubillingConfig
  105. *
  106. * @return void
  107. */
  108. protected function loadConfigs() {
  109. global $ubillingConfig;
  110. $this->binPaths = $ubillingConfig->getBinpaths();
  111. $this->altCfg = $ubillingConfig->getAlter();
  112. }
  113. /**
  114. * Sets required properties depends on config options
  115. *
  116. * @return void
  117. */
  118. protected function setOptions() {
  119. $this->ffmpgPath = $this->binPaths['FFMPG_PATH'];
  120. $this->transportTemplate = $this->cliff->getTransportTemplate();
  121. $this->recordOpts = $this->cliff->getRecordOpts();
  122. $this->audioCapture = $this->cliff->getAudioCapture();
  123. $this->supressOutput = '';
  124. if (isset($this->altCfg['RECORDER_DEBUG'])) {
  125. if ($this->altCfg['RECORDER_DEBUG']) {
  126. $this->debugFlag = true;
  127. }
  128. }
  129. }
  130. /**
  131. * Inits ffmpeg CLI wrapper
  132. *
  133. * @return void
  134. */
  135. protected function initCliff() {
  136. $this->cliff = new CliFF();
  137. }
  138. /**
  139. * Inits system messages helper
  140. *
  141. * @return void
  142. */
  143. protected function initMessages() {
  144. $this->messages = new UbillingMessageHelper();
  145. }
  146. /**
  147. * Inits cameras into protected prop and loads its full data
  148. *
  149. * @return void
  150. */
  151. protected function initCameras() {
  152. $this->cameras = new Cameras();
  153. $this->allCamerasData = $this->cameras->getAllCamerasFullData();
  154. }
  155. /**
  156. * Inits storages into protected prop for further usage
  157. *
  158. * @return void
  159. */
  160. protected function initStorages() {
  161. $this->storages = new Storages();
  162. }
  163. /**
  164. * Inits stardust process manager
  165. *
  166. * @return void
  167. */
  168. protected function initStardust() {
  169. $this->stardust = new StarDust();
  170. }
  171. /**
  172. * Runs recording process of some camera
  173. *
  174. * @param int $cameraId
  175. *
  176. * @return void
  177. */
  178. public function runRecord($cameraId) {
  179. $cameraId = ubRouting::filters($cameraId, 'int');
  180. if (isset($this->allCamerasData[$cameraId])) {
  181. $cameraData = $this->allCamerasData[$cameraId];
  182. if ($cameraData['CAMERA']['active']) {
  183. $allRunningRecorders = $this->getRunningRecorders();
  184. if (!isset($allRunningRecorders[$cameraId])) {
  185. $pid = self::PID_PREFIX . $cameraId;
  186. $this->stardust->setProcess($pid);
  187. if ($this->stardust->notRunning()) {
  188. if (zb_PingICMP($cameraData['CAMERA']['ip'])) {
  189. $storageId = $cameraData['CAMERA']['storageid'];
  190. $channel = $cameraData['CAMERA']['channel'];
  191. $channelPath = $this->storages->initChannel($storageId, $channel);
  192. if ($channelPath) {
  193. if ($cameraData['TEMPLATE']['MAIN_STREAM']) {
  194. //rtsp proto capture
  195. if ($cameraData['TEMPLATE']['PROTO'] == 'rtsp') {
  196. //custom rtsp port is here?
  197. $rtspPort = $cameraData['TEMPLATE']['RTSP_PORT'];
  198. if (isset($cameraData['OPTS'])) {
  199. if (!empty($cameraData['OPTS']['rtspport'])) {
  200. $rtspPort = $cameraData['OPTS']['rtspport'];
  201. }
  202. }
  203. $authString = $cameraData['CAMERA']['login'] . ':' . $cameraData['CAMERA']['password'] . '@';
  204. $streamUrl = $cameraData['CAMERA']['ip'] . ':' . $rtspPort . $cameraData['TEMPLATE']['MAIN_STREAM'];
  205. $audioOpts = ($cameraData['TEMPLATE']['SOUND']) ? $this->audioCapture : '';
  206. $captureFullUrl = "'rtsp://" . $authString . $streamUrl . "' " . $audioOpts . $this->recordOpts . ' ' . self::CHUNKS_MASK . self::CHUNKS_EXT;
  207. $captureCommand = $this->ffmpgPath . ' ' . $this->transportTemplate . ' ' . $captureFullUrl . ' ' . $this->supressOutput;
  208. $fullCommand = 'cd ' . $channelPath . ' && ' . $captureCommand;
  209. $this->stardust->start();
  210. log_register('RECORDER STARTED [' . $cameraId . ']');
  211. //optional logging there
  212. if ($this->debugFlag) {
  213. file_put_contents(self::DEBUG_LOG, curdatetime() . ' START: ' . $fullCommand . PHP_EOL, FILE_APPEND);
  214. }
  215. //locks process till it finishes
  216. if ($this->debugFlag) {
  217. $fullCommand .= ' 2>> /tmp/recorder_' . $cameraId . '.log';
  218. shell_exec($fullCommand);
  219. } else {
  220. shell_exec($fullCommand);
  221. }
  222. $this->stardust->stop();
  223. }
  224. } else {
  225. log_register('RECORDER FAILED [' . $cameraId . '] NO MAINSTREAM');
  226. }
  227. } else {
  228. log_register('RECORDER FAILED [' . $cameraId . '] CHANNEL NOT EXISTS');
  229. }
  230. } else {
  231. if ($this->debugFlag) {
  232. log_register('RECORDER NOTSTARTED [' . $cameraId . '] CAMERA NOT ACCESSIBLE');
  233. }
  234. }
  235. } else {
  236. log_register('RECORDER NOTSTARTED [' . $cameraId . '] ALREADY RUNNING STARDUST');
  237. }
  238. } else {
  239. log_register('RECORDER NOTSTARTED [' . $cameraId . '] ALREADY RUNNING REALPROCESS');
  240. }
  241. } else {
  242. log_register('RECORDER NOTSTARTED [' . $cameraId . '] CAMERA DISABLED');
  243. }
  244. } else {
  245. log_register('RECORDER FAILED [' . $cameraId . '] CAMERA NOT EXISTS');
  246. }
  247. }
  248. /**
  249. * Returns all running recorders real process PID-s array as pid=>processString
  250. *
  251. * @return array
  252. */
  253. protected function getRecordersPids() {
  254. $result = array();
  255. $command = $this->binPaths['PS'] . ' ax | ' . $this->binPaths['GREP'] . ' ' . $this->ffmpgPath . ' | ' . $this->binPaths['GREP'] . ' -v grep';
  256. $rawResult = shell_exec($command);
  257. if (!empty($rawResult)) {
  258. $rawResult = explodeRows($rawResult);
  259. foreach ($rawResult as $io => $eachLine) {
  260. $eachLine = trim($eachLine);
  261. $rawLine = $eachLine;
  262. $eachLine = explode(' ', $eachLine);
  263. if (isset($eachLine[0])) {
  264. $eachPid = $eachLine[0];
  265. if (is_numeric($eachPid)) {
  266. //is this really capture process?
  267. if (ispos($rawLine, $this->recordOpts)) {
  268. $result[$eachPid] = $rawLine;
  269. }
  270. }
  271. }
  272. }
  273. }
  274. return ($result);
  275. }
  276. /**
  277. * Returns running cameras recording processes as cameraId=>realPid
  278. *
  279. * @return array
  280. */
  281. public function getRunningRecorders() {
  282. $result = array();
  283. if (!empty($this->allCamerasData)) {
  284. $recorderPids = $this->getRecordersPids();
  285. if (!empty($recorderPids)) {
  286. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  287. foreach ($recorderPids as $eachPid => $eachProcess) {
  288. $camIp = $eachCameraData['CAMERA']['ip'];
  289. $camLogin = $eachCameraData['CAMERA']['login'];
  290. $camPass = $eachCameraData['CAMERA']['password'];
  291. $camPort = $eachCameraData['TEMPLATE']['RTSP_PORT'];
  292. if (isset($eachCameraData['OPTS'])) {
  293. if (!empty($eachCameraData['OPTS']['rtspport'])) {
  294. $camPort = $eachCameraData['OPTS']['rtspport'];
  295. }
  296. }
  297. //looks familiar?
  298. if (ispos($eachProcess, $camIp) and ispos($eachProcess, $camLogin) and ispos($eachProcess, $camPass) and ispos($eachProcess, ':' . $camPort)) {
  299. $result[$eachCameraId] = $eachPid;
  300. }
  301. }
  302. }
  303. }
  304. }
  305. return ($result);
  306. }
  307. /**
  308. * Shutdowns recorder process if its running
  309. *
  310. * @param int $cameraId
  311. *
  312. * @return void
  313. */
  314. public function stopRecord($cameraId) {
  315. $cameraId = ubRouting::filters($cameraId, 'int');
  316. $allRunningRecorders = $this->getRunningRecorders();
  317. if (isset($allRunningRecorders[$cameraId])) {
  318. $count = 0;
  319. while (isset($allRunningRecorders[$cameraId])) {
  320. $count++;
  321. $command = $this->binPaths['SUDO'] . ' ' . $this->binPaths['KILL'] . ' ' . $allRunningRecorders[$cameraId];
  322. shell_exec($command);
  323. if ($this->debugFlag) {
  324. file_put_contents(self::DEBUG_LOG, curdatetime() . ' STOP: ' . $command . PHP_EOL, FILE_APPEND);
  325. }
  326. $allRunningRecorders = $this->getRunningRecorders();
  327. }
  328. log_register('RECORDER STOPPED [' . $cameraId . '] ATTEMPT `' . $count . '`');
  329. }
  330. }
  331. /**
  332. * Runs recorder for selected camera in background
  333. *
  334. * @param int $cameraId
  335. *
  336. * @return void
  337. */
  338. public function runRecordBackground($cameraId) {
  339. $cameraId = ubRouting::filters($cameraId, 'int');
  340. if (isset($this->allCamerasData[$cameraId])) {
  341. $cameraData = $this->allCamerasData[$cameraId];
  342. if ($cameraData['CAMERA']['active']) {
  343. $recordingProcess = new StarDust(self::PID_PREFIX . $cameraId);
  344. $allRunningRecorders = $this->getRunningRecorders();
  345. if (!isset($allRunningRecorders[$cameraId])) {
  346. $recordingProcess->runBackgroundProcess(self::WRAPPER . ' "recherd&cameraid=' . $cameraId . '"', 1);
  347. }
  348. }
  349. }
  350. }
  351. /**
  352. * Runs all recorders for active cameras in background
  353. *
  354. * @return void
  355. */
  356. public function captureAll() {
  357. $captureProcess = new StarDust(self::CAPTURE_PID);
  358. if ($captureProcess->notRunning()) {
  359. $captureProcess->start();
  360. if (!empty($this->allCamerasData)) {
  361. foreach ($this->allCamerasData as $io => $eachCamera) {
  362. if ($eachCamera['CAMERA']['active']) {
  363. $cameraId = $eachCamera['CAMERA']['id'];
  364. $this->runRecordBackground($cameraId);
  365. }
  366. }
  367. }
  368. $captureProcess->stop();
  369. }
  370. }
  371. }