'a','à'=>'a','ã'=>'a','â'=>'a','é'=>'e','ê'=>'e','í'=>'i','ó'=>'o','ô'=>'o','õ'=>'o','ú'=>'u','ç'=>'c', 'Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A','É'=>'E','Ê'=>'E','Í'=>'I','Ó'=>'O','Ô'=>'O','Õ'=>'O','Ú'=>'U','Ç'=>'C' ); $nome_limpo = strtr($_POST['nome_camera'], $mapa_acentos); // Remove tudo que não for letra ou número e deixa minúsculo (Remove traços, espaços, pontos...) $nome_limpo = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($nome_limpo)); // Se a pessoa tiver digitado só símbolos e o nome ficar vazio, a gente bota um padrão if (empty($nome_limpo)) $nome_limpo = "cam"; // Colamos 4 letrinhas randômicas no final pra evitar a catástrofe de IDs repetidos no Shinobi $monitor_id = "cam_" . $nome_limpo . "_" . substr(md5(uniqid()), 0, 4); } // ========================================================================= // O DESMEMBRADOR DE RTSP // ========================================================================= $partes_url = parse_url($rtsp_link); $rtsp_host = $partes_url['host'] ?? $rtsp_link; $rtsp_port = $partes_url['port'] ?? '554'; $rtsp_user = $partes_url['user'] ?? ''; $rtsp_pass = $partes_url['pass'] ?? ''; $rtsp_path = $partes_url['path'] ?? ''; if (isset($partes_url['query'])) { $rtsp_path .= '?' . $partes_url['query']; } // ========================================================================= // A TÉCNICA DO CLONE COM OS ZEROS CORRIGIDOS // ========================================================================= $json_molde = '{"mode":"start","mid":"","name":"","type":"h264","protocol":"rtsp","host":"","port":"554","path":"","height":"480","width":"640","ext":"mp4","fps":"1","details":{"max_keep_days":"","notes":"","dir":"","rtmp_key":"","auto_host_enable":"1","auto_host":"","rtsp_transport":"tcp","muser":"","mpass":"","port_force":"0","fatal_max":"0","skip_ping":null,"is_onvif":null,"onvif_non_standard":null,"onvif_port":"","primary_input":"0:0","aduration":"1000000","probesize":"1000000","stream_loop":"0","sfps":"","wall_clock_timestamp_ignore":null,"accelerator":"0","hwaccel":"auto","hwaccel_vcodec":"","hwaccel_device":"","stream_type":"hls","stream_flv_type":"ws","stream_flv_maxLatency":"","stream_mjpeg_clients":"","stream_vcodec":"copy","stream_acodec":"no","hls_time":"2","hls_list_size":"3","preset_stream":"ultrafast","stream_quality":"15","stream_fps":"2","stream_scale_x":"","stream_scale_y":"","stream_rotate":null,"signal_check":"10","signal_check_log":"0","stream_vf":"","tv_channel":"0","tv_channel_id":"","tv_channel_group_title":"","stream_timestamp":"0","stream_timestamp_font":"","stream_timestamp_font_size":"","stream_timestamp_color":"","stream_timestamp_box_color":"","stream_timestamp_x":"","stream_timestamp_y":"","stream_watermark":"0","stream_watermark_location":"","stream_watermark_position":"tr","snap":"0","snap_fps":"","snap_scale_x":"","snap_scale_y":"","snap_vf":"","vcodec":"copy","crf":"1","preset_record":"","acodec":"no","record_scale_y":"","record_scale_x":"","cutoff":"15","rotate":null,"vf":"","timestamp":"0","timestamp_font":"","timestamp_font_size":"10","timestamp_color":"white","timestamp_box_color":"0x00000000@1","timestamp_x":"(w-tw)/2","timestamp_y":"0","watermark":"0","watermark_location":"","watermark_position":"tr","record_timelapse":null,"record_timelapse_mp4":null,"record_timelapse_fps":null,"record_timelapse_scale_x":"","record_timelapse_scale_y":"","record_timelapse_vf":"","record_timelapse_watermark":null,"record_timelapse_watermark_location":"","record_timelapse_watermark_position":null,"cust_input":"","cust_stream":"","cust_snap":"","cust_record":"","cust_detect":"","cust_detect_object":"","cust_sip_record":"","custom_output":"","detector":"0","detector_http_api":null,"detector_send_frames":"1","detector_fps":"","detector_scale_x":"640","detector_scale_y":"480","detector_lock_timeout":"","detector_save":"0","detector_record_method":"sip","detector_trigger":"1","detector_trigger_record_fps":"","detector_timeout":"5","detector_send_video_length":"","watchdog_reset":"0","detector_delete_motionless_videos":"0","det_multi_trig":null,"group_detector_multi":"","detector_webhook":"0","detector_webhook_timeout":"","detector_webhook_url":"","detector_webhook_method":null,"detector_command_enable":"0","detector_command":"","detector_command_timeout":"","snap_seconds_inward":"","detector_mail":"0","detector_mail_timeout":"","use_detector_filters":null,"use_detector_filters_object":null,"cords":"[]","detector_filters":"","detector_pam":"1","detector_sensitivity":"","detector_max_sensitivity":"","detector_threshold":"1","detector_color_threshold":"","inverse_trigger":null,"detector_frame":"0","detector_noise_filter":null,"detector_noise_filter_range":"","detector_notrigger":"0","detector_notrigger_mail":"0","detector_notrigger_discord":null,"detector_notrigger_timeout":"","detector_notrigger_webhook":null,"detector_notrigger_webhook_url":"","detector_notrigger_webhook_method":null,"detector_notrigger_command_enable":null,"detector_notrigger_command":"","detector_notrigger_command_timeout":"","detector_audio":null,"detector_audio_min_db":"","detector_audio_max_db":"","detector_use_detect_object":"0","detector_send_frames_object":null,"detector_obj_count_in_region":null,"detector_obj_region":null,"detector_use_motion":"1","detector_fps_object":"","detector_scale_x_object":"","detector_scale_y_object":"","detector_lisence_plate":"0","detector_lisence_plate_country":"us","detector_buffer_vcodec":"auto","detector_buffer_acodec":null,"detector_buffer_fps":"","event_record_scale_x":"","event_record_scale_y":"","detector_buffer_hls_time":"","detector_buffer_hls_list_size":"","detector_buffer_start_number":"","detector_buffer_live_start_index":"","control":"0","control_base_url":"","control_url_method":null,"control_digest_auth":null,"control_stop":"0","control_url_stop_timeout":"","control_turn_speed":"","detector_ptz_follow":null,"detector_ptz_follow_target":"","detector_obj_count":null,"control_url_center":"","control_url_left":"","control_url_left_stop":"","control_url_right":"","control_url_right_stop":"","control_url_up":"","control_url_up_stop":"","control_url_down":"","control_url_down_stop":"","control_url_enable_nv":"","control_url_disable_nv":"","control_url_zoom_out":"","control_url_zoom_out_stop":"","control_url_zoom_in":"","control_url_zoom_in_stop":"","control_invert_y":null,"groups":"[]","notify_email":null,"notify_onUnexpectedExit":null,"loglevel":"warning","sqllog":"0","detector_cascades":"","stream_channels":"","input_maps":"","input_map_choices":""},"shto":"[]","shfr":"[]"}'; // Transforma o molde num Array $config_shinobi = json_decode($json_molde, true); // ========================================================================= // INJETANDO OS DADOS BÁSICOS E O FILTRO MÁGICO // ========================================================================= $config_shinobi['mid'] = $monitor_id; $config_shinobi['name'] = $nome_camera; $config_shinobi['host'] = $rtsp_host; $config_shinobi['port'] = $rtsp_port; $config_shinobi['path'] = $rtsp_path; $config_shinobi['details']['auto_host'] = $rtsp_link; $config_shinobi['details']['muser'] = $rtsp_user; $config_shinobi['details']['mpass'] = $rtsp_pass; $config_shinobi['details']['max_keep_days'] = $dias_retencao; // ========================================================================= // INJEÇÃO DA CHAVE SELETORA (COPY vs AUTO) - PADRÃO DATACENTER HÍBRIDO // ========================================================================= if ($codec_video === 'auto') { $config_shinobi['details']['stream_vcodec'] = "no"; $config_shinobi['details']['preset_stream'] = "ultrafast"; $config_shinobi['details']['stream_quality'] = "28"; $config_shinobi['details']['stream_fps'] = "5"; $config_shinobi['details']['snap'] = "1"; $config_shinobi['details']['vcodec'] = "libx264"; $config_shinobi['details']['preset_record'] = "ultrafast"; $config_shinobi['details']['crf'] = "28"; } else { $config_shinobi['details']['stream_vcodec'] = "copy"; $config_shinobi['details']['preset_stream'] = "ultrafast"; $config_shinobi['details']['stream_quality'] = "15"; $config_shinobi['details']['stream_fps'] = "2"; $config_shinobi['details']['snap'] = "0"; $config_shinobi['details']['vcodec'] = "copy"; } // ========================================================================= // 🚨 A MÁGICA DO ÁUDIO + BLINDAGEM ANTI-PICOTE 🚨 // ========================================================================= if ($captura_audio == 1) { $config_shinobi['details']['acodec'] = "aac"; $config_shinobi['details']['stream_acodec'] = "aac"; $flag_audio = ""; // Se tem áudio, não cortamos na entrada! } else { $config_shinobi['details']['acodec'] = "no"; $config_shinobi['details']['stream_acodec'] = "no"; $flag_audio = "-an "; // Se não tem áudio, corta na raiz pra poupar CPU } // A CURA DEFINITIVA PARA INTELBRAS PICOTANDO EM CENA DE MOVIMENTO // Removido o 'nobuffer' que rasgava a imagem e adicionado correção de timestamps. $config_shinobi['details']['cust_input'] = $flag_audio . "-rtsp_transport tcp -fflags +genpts+discardcorrupt"; $config_shinobi['details']['detector_webhook'] = "1"; $config_shinobi['details']['detector_webhook_url'] = "https://vision.consultja.net.br/webhook_telegram.php"; // ========================================================================= // O VOCABULÁRIO PERFEITO DOS MODOS E TIMEOUT // ========================================================================= if ($modo_gravacao === 'motion') { $config_shinobi['mode'] = 'start'; $config_shinobi['details']['detector'] = "1"; $config_shinobi['details']['detector_send_frames'] = "1"; $config_shinobi['details']['detector_trigger'] = "1"; $config_shinobi['details']['detector_timeout'] = $timeout_gravacao; $config_shinobi['details']['record_timeout'] = $timeout_gravacao; $config_shinobi['details']['detector_record_method'] = "sip"; $comando_boot = "start"; } elseif ($modo_gravacao === 'watch') { $config_shinobi['mode'] = 'start'; $config_shinobi['details']['detector'] = "0"; $config_shinobi['details']['detector_send_frames'] = "0"; $config_shinobi['details']['detector_trigger'] = "0"; $comando_boot = "start"; } else { $config_shinobi['mode'] = 'record'; $config_shinobi['details']['detector'] = "0"; $config_shinobi['details']['detector_send_frames'] = "0"; $config_shinobi['details']['detector_trigger'] = "0"; $comando_boot = "record"; } // ========================================================================= // O BOTÃO DE PÂNICO: FORÇA A PARADA TOTAL SE DESATIVADA // ========================================================================= if ($status_camera == 0) { $config_shinobi['mode'] = 'stop'; $comando_boot = 'stop'; // Isso vai fazer o Shinobi cortar a conexão na raiz } // ========================================================================= // CONFIGURANDO NO SHINOBI // ========================================================================= $url_api_shinobi = "http://127.0.0.1:8080/{$api_key}/configureMonitor/{$group_key}/{$monitor_id}"; $payload = http_build_query(['data' => json_encode($config_shinobi)]); $ch = curl_init($url_api_shinobi); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); $resposta_shinobi = curl_exec($ch); $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_status == 200 || strpos(strtolower($resposta_shinobi), 'ok') !== false) { // Passo 1: Desliga o monitor $url_stop = "http://127.0.0.1:8080/{$api_key}/monitor/{$group_key}/{$monitor_id}/stop"; $ch_stop = curl_init($url_stop); curl_setopt($ch_stop, CURLOPT_RETURNTRANSFER, true); curl_exec($ch_stop); curl_close($ch_stop); sleep(2); // Respira // Passo 2: Liga o monitor respeitando a vontade do usuário (Ou mantém em STOP) $url_start = "http://127.0.0.1:8080/{$api_key}/monitor/{$group_key}/{$monitor_id}/{$comando_boot}"; $ch_start = curl_init($url_start); curl_setopt($ch_start, CURLOPT_RETURNTRANSFER, true); curl_exec($ch_start); curl_close($ch_start); try { if (!empty($id_camera)) { $stmt = $pdo->prepare("UPDATE cameras SET id_loja=?, nome_camera=?, api_key=?, group_key=?, monitor_id=?, visivel_cliente=?, status_camera=?, rtsp_link=?, modo_gravacao=?, dias_retencao=?, timeout_gravacao=?, codec_video=?, modo_hibrido=?, captura_audio=? WHERE id=?"); $stmt->execute([$id_loja, $nome_camera, $api_key, $group_key, $monitor_id, $visivel_cliente, $status_camera, $rtsp_link, $modo_gravacao, $dias_retencao, $timeout_gravacao, $codec_video, $modo_hibrido_post, $captura_audio, $id_camera]); registrar_log($pdo, $_SESSION['usuario_id'], $id_empresa_sessao, 'CONFIG_CAMERA', "Câmera $nome_camera atualizada e sincronizada."); $mensagem = " Configuração salva e sincronizada com sucesso!"; } else { $stmt = $pdo->prepare("INSERT INTO cameras (id_loja, nome_camera, api_key, group_key, monitor_id, visivel_cliente, status_camera, rtsp_link, modo_gravacao, dias_retencao, timeout_gravacao, codec_video, modo_hibrido, captura_audio) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([$id_loja, $nome_camera, $api_key, $group_key, $monitor_id, $visivel_cliente, $status_camera, $rtsp_link, $modo_gravacao, $dias_retencao, $timeout_gravacao, $codec_video, $modo_hibrido_post, $captura_audio]); registrar_log($pdo, $_SESSION['usuario_id'], $id_empresa_sessao, 'ADD_CAMERA', "Nova câmera integrada via Zero-Touch: $nome_camera"); $mensagem = " Câmera provisionada no servidor!"; } } catch (PDOException $e) { $mensagem = " Erro no banco: " . $e->getMessage() . ""; } } else { $mensagem = " Erro de Comunicação (Status: $http_status). Resposta: $resposta_shinobi"; } } // ========================================================================= // 2. Processa a Exclusão // ========================================================================= if (isset($_GET['excluir'])) { $id_excluir = $_GET['excluir']; try { $stmt_info = $pdo->prepare("SELECT nome_camera, api_key, group_key, monitor_id FROM cameras WHERE id = ?"); $stmt_info->execute([$id_excluir]); $cam_info = $stmt_info->fetch(PDO::FETCH_ASSOC); if ($cam_info) { $url_del = "http://127.0.0.1:8080/{$cam_info['api_key']}/configureMonitor/{$cam_info['group_key']}/{$cam_info['monitor_id']}/delete"; $ch_del = curl_init($url_del); curl_setopt($ch_del, CURLOPT_RETURNTRANSFER, true); curl_exec($ch_del); curl_close($ch_del); $stmt = $pdo->prepare("DELETE FROM cameras WHERE id = ?"); $stmt->execute([$id_excluir]); registrar_log($pdo, $_SESSION['usuario_id'], $id_empresa_sessao, 'DELETE_CAMERA', "Câmera excluída: {$cam_info['nome_camera']}"); $mensagem = " Câmera e Gravações removidas de toda a infraestrutura!"; } } catch (PDOException $e) { $mensagem = " Erro ao excluir: " . $e->getMessage() . ""; } } // ========================================================================= // 3. Carrega Dados para Edição OU Cópia // ========================================================================= $cam_edit = null; $is_copy = false; if (isset($_GET['editar'])) { $stmt = $pdo->prepare("SELECT * FROM cameras WHERE id = ?"); $stmt->execute([$_GET['editar']]); $cam_edit = $stmt->fetch(PDO::FETCH_ASSOC); } elseif (isset($_GET['copiar'])) { $stmt = $pdo->prepare("SELECT * FROM cameras WHERE id = ?"); $stmt->execute([$_GET['copiar']]); $cam_edit = $stmt->fetch(PDO::FETCH_ASSOC); if ($cam_edit) { $is_copy = true; // Coloca um sufixo para você saber que é a cópia $cam_edit['nome_camera'] .= ' (Cópia)'; } } // 4. Buscas com Filtro SaaS if ($id_empresa_sessao == 1) { $lojas = $pdo->query("SELECT * FROM lojas ORDER BY nome_loja")->fetchAll(PDO::FETCH_ASSOC); $cameras = $pdo->query("SELECT c.*, l.nome_loja FROM cameras c JOIN lojas l ON c.id_loja = l.id ORDER BY l.nome_loja, c.nome_camera")->fetchAll(PDO::FETCH_ASSOC); } else { $stmt_l = $pdo->prepare("SELECT * FROM lojas WHERE id_empresa = ? ORDER BY nome_loja"); $stmt_l->execute([$id_empresa_sessao]); $lojas = $stmt_l->fetchAll(PDO::FETCH_ASSOC); $stmt_c = $pdo->prepare("SELECT c.*, l.nome_loja FROM cameras c JOIN lojas l ON c.id_loja = l.id WHERE l.id_empresa = ? ORDER BY l.nome_loja, c.nome_camera"); $stmt_c->execute([$id_empresa_sessao]); $cameras = $stmt_c->fetchAll(PDO::FETCH_ASSOC); } // 5. Agrupando para a Sanfona $cameras_agrupadas = []; foreach ($cameras as $c) { $cameras_agrupadas[$c['nome_loja']][] = $c; } ?>