api.recorder.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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. //custom transport protocol?
  204. $transTemplate = $this->transportTemplate;
  205. if (!empty($cameraData['TEMPLATE']['UDP_TRANSPORT'])) {
  206. $transTemplate = str_replace('-rtsp_transport tcp', '-rtsp_transport udp', $transTemplate);
  207. }
  208. $authString = $cameraData['CAMERA']['login'] . ':' . $cameraData['CAMERA']['password'] . '@';
  209. $streamUrl = $cameraData['CAMERA']['ip'] . ':' . $rtspPort . $cameraData['TEMPLATE']['MAIN_STREAM'];
  210. $audioOpts = ($cameraData['TEMPLATE']['SOUND']) ? $this->audioCapture : '';
  211. $captureFullUrl = "'rtsp://" . $authString . $streamUrl . "' " . $audioOpts . $this->recordOpts . ' ' . self::CHUNKS_MASK . self::CHUNKS_EXT;
  212. $captureCommand = $this->ffmpgPath . ' ' . $transTemplate . ' ' . $captureFullUrl . ' ' . $this->supressOutput;
  213. $fullCommand = 'cd ' . $channelPath . ' && ' . $captureCommand;
  214. $this->stardust->start();
  215. log_register('RECORDER STARTED [' . $cameraId . ']');
  216. //optional logging there
  217. if ($this->debugFlag) {
  218. file_put_contents(self::DEBUG_LOG, curdatetime() . ' START: ' . $fullCommand . PHP_EOL, FILE_APPEND);
  219. }
  220. //locks process till it finishes
  221. if ($this->debugFlag) {
  222. $fullCommand .= ' 2>> /tmp/recorder_' . $cameraId . '.log';
  223. shell_exec($fullCommand);
  224. } else {
  225. shell_exec($fullCommand);
  226. }
  227. $this->stardust->stop();
  228. }
  229. } else {
  230. log_register('RECORDER FAILED [' . $cameraId . '] NO MAINSTREAM');
  231. }
  232. } else {
  233. log_register('RECORDER FAILED [' . $cameraId . '] CHANNEL NOT EXISTS');
  234. }
  235. } else {
  236. if ($this->debugFlag) {
  237. log_register('RECORDER NOTSTARTED [' . $cameraId . '] CAMERA NOT ACCESSIBLE');
  238. }
  239. }
  240. } else {
  241. log_register('RECORDER NOTSTARTED [' . $cameraId . '] ALREADY RUNNING STARDUST');
  242. }
  243. } else {
  244. log_register('RECORDER NOTSTARTED [' . $cameraId . '] ALREADY RUNNING REALPROCESS');
  245. }
  246. } else {
  247. log_register('RECORDER NOTSTARTED [' . $cameraId . '] CAMERA DISABLED');
  248. }
  249. } else {
  250. log_register('RECORDER FAILED [' . $cameraId . '] CAMERA NOT EXISTS');
  251. }
  252. }
  253. /**
  254. * Returns all running recorders real process PID-s array as pid=>processString
  255. *
  256. * @return array
  257. */
  258. protected function getRecordersPids() {
  259. $result = array();
  260. $command = $this->binPaths['PS'] . ' ax | ' . $this->binPaths['GREP'] . ' ' . $this->ffmpgPath . ' | ' . $this->binPaths['GREP'] . ' -v grep';
  261. $rawResult = shell_exec($command);
  262. if (!empty($rawResult)) {
  263. $rawResult = explodeRows($rawResult);
  264. foreach ($rawResult as $io => $eachLine) {
  265. $eachLine = trim($eachLine);
  266. $rawLine = $eachLine;
  267. $eachLine = explode(' ', $eachLine);
  268. if (isset($eachLine[0])) {
  269. $eachPid = $eachLine[0];
  270. if (is_numeric($eachPid)) {
  271. //is this really capture process?
  272. if (ispos($rawLine, $this->recordOpts)) {
  273. $result[$eachPid] = $rawLine;
  274. }
  275. }
  276. }
  277. }
  278. }
  279. return ($result);
  280. }
  281. /**
  282. * Returns running cameras recording processes as cameraId=>realPid
  283. *
  284. * @return array
  285. */
  286. public function getRunningRecorders() {
  287. $result = array();
  288. if (!empty($this->allCamerasData)) {
  289. $recorderPids = $this->getRecordersPids();
  290. if (!empty($recorderPids)) {
  291. foreach ($this->allCamerasData as $eachCameraId => $eachCameraData) {
  292. foreach ($recorderPids as $eachPid => $eachProcess) {
  293. $camIp = $eachCameraData['CAMERA']['ip'];
  294. $camLogin = $eachCameraData['CAMERA']['login'];
  295. $camPass = $eachCameraData['CAMERA']['password'];
  296. $camPort = $eachCameraData['TEMPLATE']['RTSP_PORT'];
  297. if (isset($eachCameraData['OPTS'])) {
  298. if (!empty($eachCameraData['OPTS']['rtspport'])) {
  299. $camPort = $eachCameraData['OPTS']['rtspport'];
  300. }
  301. }
  302. //looks familiar?
  303. if (ispos($eachProcess, $camIp) and ispos($eachProcess, $camLogin) and ispos($eachProcess, $camPass) and ispos($eachProcess, ':' . $camPort)) {
  304. $result[$eachCameraId] = $eachPid;
  305. }
  306. }
  307. }
  308. }
  309. }
  310. return ($result);
  311. }
  312. /**
  313. * Shutdowns recorder process if its running
  314. *
  315. * @param int $cameraId
  316. *
  317. * @return void
  318. */
  319. public function stopRecord($cameraId) {
  320. $cameraId = ubRouting::filters($cameraId, 'int');
  321. $allRunningRecorders = $this->getRunningRecorders();
  322. if (isset($allRunningRecorders[$cameraId])) {
  323. $count = 0;
  324. while (isset($allRunningRecorders[$cameraId])) {
  325. $count++;
  326. $command = $this->binPaths['SUDO'] . ' ' . $this->binPaths['KILL'] . ' ' . $allRunningRecorders[$cameraId];
  327. shell_exec($command);
  328. if ($this->debugFlag) {
  329. file_put_contents(self::DEBUG_LOG, curdatetime() . ' STOP: ' . $command . PHP_EOL, FILE_APPEND);
  330. }
  331. $allRunningRecorders = $this->getRunningRecorders();
  332. }
  333. log_register('RECORDER STOPPED [' . $cameraId . '] ATTEMPT `' . $count . '`');
  334. }
  335. }
  336. /**
  337. * Runs recorder for selected camera in background
  338. *
  339. * @param int $cameraId
  340. *
  341. * @return void
  342. */
  343. public function runRecordBackground($cameraId) {
  344. $cameraId = ubRouting::filters($cameraId, 'int');
  345. if (isset($this->allCamerasData[$cameraId])) {
  346. $cameraData = $this->allCamerasData[$cameraId];
  347. if ($cameraData['CAMERA']['active']) {
  348. $recordingProcess = new StarDust(self::PID_PREFIX . $cameraId);
  349. $allRunningRecorders = $this->getRunningRecorders();
  350. if (!isset($allRunningRecorders[$cameraId])) {
  351. $recordingProcess->runBackgroundProcess(self::WRAPPER . ' "recherd&cameraid=' . $cameraId . '"', 1);
  352. }
  353. }
  354. }
  355. }
  356. /**
  357. * Runs all recorders for active cameras in background
  358. *
  359. * @return void
  360. */
  361. public function captureAll() {
  362. $captureProcess = new StarDust(self::CAPTURE_PID);
  363. if ($captureProcess->notRunning()) {
  364. $captureProcess->start();
  365. if (!empty($this->allCamerasData)) {
  366. foreach ($this->allCamerasData as $io => $eachCamera) {
  367. if ($eachCamera['CAMERA']['active']) {
  368. $cameraId = $eachCamera['CAMERA']['id'];
  369. $this->runRecordBackground($cameraId);
  370. }
  371. }
  372. }
  373. $captureProcess->stop();
  374. }
  375. }
  376. }