diff --git a/.github/workflows/codeLintGlobal.yml b/.github/workflows/codeLintGlobal.yml deleted file mode 100644 index d10bd11..0000000 --- a/.github/workflows/codeLintGlobal.yml +++ /dev/null @@ -1,62 +0,0 @@ -on: - # Trigger the workflow on push or pull request, - # but only for the main branch - push: - branches: - - beta - pull_request: - branches: - - beta - - master - -name : 'Code Lint Global' - -jobs: - run-linters: - name: Test Linters - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "7.4" - coverage: none - tools: phpcs - - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: 14 - - # ESLint and Prettier must be in `package.json` - - name: Install Node.js dependencies - run: npm i prettier - - - name: Install Python dependencies - run: pip install flake8 black - - - name: Run linters - uses: wearerequired/lint-action@v2 - with: - php_codesniffer: false - # Optional: Ignore warnings - php_codesniffer_args: "-n --ignore-annotations ./core ./desktop ./plugin_info" - flake8: true - prettier: true - prettier_extensions: css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml - auto_fix: true - prettier_auto_fix: true - git_email: commit@jeedom.com - git_name: 'JeedomBot lint Pretty' - commit: false - black: true - black_auto_fix: true diff --git a/.github/workflows/lint_Php73.yml b/.github/workflows/lint_Php73.yml deleted file mode 100644 index e9f65cf..0000000 --- a/.github/workflows/lint_Php73.yml +++ /dev/null @@ -1,28 +0,0 @@ -name : 'Php 7.3' - -on: - push: - branches: - - beta - paths: - - "**.php" - pull_request: - branches: - - beta - - master - paths: - - "**.php" - -jobs: - run-linters: - name: Run linters - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: PHP syntax checker 7.3 - uses: prestashop/github-action-php-lint/7.3@v1 - with: - folder-to-exclude: "! -path \"./3rdparty/*\"" diff --git a/.github/workflows/lint_Php74.yml b/.github/workflows/lint_Php74.yml deleted file mode 100644 index e87f508..0000000 --- a/.github/workflows/lint_Php74.yml +++ /dev/null @@ -1,28 +0,0 @@ -name : 'Php 7.4' - -on: - push: - branches: - - beta - paths: - - "**.php" - pull_request: - branches: - - beta - - master - paths: - - "**.php" - -jobs: - run-linters: - name: Run linters - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: PHP syntax checker 7.4 - uses: prestashop/github-action-php-lint/7.4@v1 - with: - folder-to-exclude: "! -path \"./3rdparty/*\"" diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml deleted file mode 100644 index b6669dc..0000000 --- a/.github/workflows/php.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "PHP" - -on: - push: - branches: - - beta - paths: - - "**.php" - pull_request: - branches: - - beta - - master - paths: - - "**.php" - -jobs: - phpcs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: michaelw90/PHP-Lint@master - - diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 26d8687..08026e2 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -1,6 +1,5 @@ on: - # Trigger the workflow on push or pull request, - # but only for the main branch + # Create branch > prettier / this workflows create commit on branch prettier. push: branches: - prettier @@ -8,49 +7,8 @@ on: branches: - prettier -name : 'Prettier' +name : 'Prettier Plugin Jeedom' jobs: - run-linters: - name: Test Linters - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "7.4" - coverage: none - tools: phpcs - - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: 14 - - # ESLint and Prettier must be in `package.json` - - name: Install Node.js dependencies - run: npm i prettier - - - name: Install Python dependencies - run: pip install black - - - name: Run linters - uses: wearerequired/lint-action@v2 - with: - prettier: true - prettier_extensions: css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml - auto_fix: true - prettier_auto_fix: true - git_email: commit@jeedom.com - git_name: 'JeedomBot lint Pretty' - black: true - black_auto_fix: true + prettier: + uses: jeedom/workflows/.github/workflows/prettier.yml@main diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml new file mode 100644 index 0000000..d214266 --- /dev/null +++ b/.github/workflows/translations.yml @@ -0,0 +1,18 @@ +name: translate + +on: + workflow_dispatch: + push: + branches: + - beta + +jobs: + auto-translate: + runs-on: ubuntu-latest + steps: + - uses: jeedom/jeetranslate@main + with: + deepl_api_key: ${{ secrets.DEEPL_API_KEY }} + include_empty_translation: false + target_languages: "en_US,es_ES,de_DE,it_IT,pt_PT" + use_core_translations: true \ No newline at end of file diff --git a/.github/workflows/work.yml b/.github/workflows/work.yml new file mode 100644 index 0000000..345021e --- /dev/null +++ b/.github/workflows/work.yml @@ -0,0 +1,15 @@ +on: + # Workflows check plugin Jeedom + push: + branches: + - beta + pull_request: + branches: + - beta + - master + +name : 'Full Workflows Plugin Jeedom' + +jobs: + plugin: + uses: jeedom/workflows/.github/workflows/plugin.yml@main diff --git a/3rdparty/rtsp-to-hls-copy.sh b/3rdparty/rtsp-to-hls-copy.sh new file mode 100644 index 0000000..0a20aea --- /dev/null +++ b/3rdparty/rtsp-to-hls-copy.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +#[ $# -eq 0 ] && { echo "Usage: $0 rtsp://... <name>"; exit 1; } + +BASEDIR=$(dirname "$0") +SOURCE="$1" +DEST="$2" +NAME=${2:-cctv} +#ARCHIVE=${ARCHIVE:-archive} + +HLS_TIME=${HLS_TIME:-5} +HLS_LIST_SIZE=${HLS_LIST_SIZE:-5} + +ffmpeg -rtsp_transport tcp -i "$SOURCE" \ +-f hls \ +-vsync 0 -copyts -vcodec copy -acodec copy \ +-tune zerolatency \ +-movflags frag_keyframe+empty_moov \ +-hls_flags delete_segments+append_list \ +-hls_init_time 1 \ +-hls_time $HLS_TIME \ +-hls_list_size $HLS_LIST_SIZE \ +-hls_base_url "segments/" \ +-hls_segment_filename ${BASEDIR}"/../data/segments/$NAME-%d.ts" \ +${BASEDIR}"/../data/$NAME.m3u8" \ No newline at end of file diff --git a/3rdparty/rtsp-to-hls-x264.sh b/3rdparty/rtsp-to-hls-x264.sh new file mode 100644 index 0000000..c930606 --- /dev/null +++ b/3rdparty/rtsp-to-hls-x264.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +#[ $# -eq 0 ] && { echo "Usage: $0 rtsp://... <name>"; exit 1; } + +BASEDIR=$(dirname "$0") +SOURCE="$1" +DEST="$2" +NAME=${2:-cctv} +#ARCHIVE=${ARCHIVE:-archive} + +HLS_TIME=${HLS_TIME:-5} +HLS_LIST_SIZE=${HLS_LIST_SIZE:-5} + +ffmpeg -rtsp_transport tcp -i "$SOURCE" \ +-f hls \ +-vsync 0 -copyts -c:v libx264 -preset ultrafast -tune zerolatency -profile:v main \ +-c:a copy \ +-movflags frag_keyframe+empty_moov \ +-hls_flags delete_segments+append_list \ +-hls_init_time 1 \ +-hls_time $HLS_TIME \ +-hls_list_size $HLS_LIST_SIZE \ +-hls_base_url "segments/" \ +-hls_segment_filename ${BASEDIR}"/../data/segments/$NAME-%d.ts" \ +${BASEDIR}"/../data/$NAME.m3u8" \ No newline at end of file diff --git a/core/ajax/camera.ajax.php b/core/ajax/camera.ajax.php index 5310519..3f9e044 100644 --- a/core/ajax/camera.ajax.php +++ b/core/ajax/camera.ajax.php @@ -19,109 +19,108 @@ try { require_once dirname(__FILE__) . '/../../../../core/php/core.inc.php'; include_file('core', 'authentification', 'php'); - + ajax::init(); - + if (!isConnect()) { throw new Exception(__('401 - Accès non autorisé', __FILE__)); } - + if (init('action') == 'stream') { $camera = camera::byId(init('id')); - if(!is_object($camera)){ - throw new \Exception(__('Impossible de trouver la camera : ',__FILE__).init('id')); + if (!is_object($camera)) { + throw new \Exception(__('Equipement introuvable', __FILE__) . ' : ' . init('id')); } - if(count(system::ps('rtsp-to-hls.sh.*'.$camera->getConfiguration('localApiKey'))) == 0){ - shell_exec('(ps ax || ps w) | grep ffmpeg.*'.$camera->getConfiguration('localApiKey').' | awk \'{print $2}\' | xargs sudo kill -9'); + $rtspScript = dirname(__FILE__) . '/../../3rdparty/rtsp-to-hls-' . ($camera->getConfiguration('encodeX264RTSP', 0) == 1 ? 'x264' : 'copy') . '.sh '; + if (count(system::ps('rtsp-to-hls-*.sh.*' . $camera->getConfiguration('localApiKey'))) == 0) { + shell_exec('(ps ax || ps w) | grep ffmpeg.*' . $camera->getConfiguration('localApiKey') . ' | awk \'{print $2}\' | xargs sudo kill -9'); $replace = array( '#username#' => urlencode($camera->getConfiguration('username')), '#password#' => urlencode($camera->getConfiguration('password')), '#ip#' => urlencode($camera->getConfiguration('ip')), '#port#' => urlencode($camera->getConfiguration('port')), ); - $engine = config::byKey('rtsp::engine','camera','avconv'); + $engine = config::byKey('rtsp::engine', 'camera', 'avconv'); if (!file_exists(dirname(__FILE__) . '/../../data/segments')) { mkdir(dirname(__FILE__) . '/../../data/segments', 0777, true); } - log::add('camera', 'debug', 'nohup '.dirname(__FILE__) . '/../../3rdparty/rtsp-to-hls.sh ' . trim(str_replace(array_keys($replace), $replace, $camera->getConfiguration('cameraStreamAccessUrl'))).' "' . $camera->getConfiguration('localApiKey') . '" > /dev/null 2>&1 &'); - exec('nohup ' .dirname(__FILE__) . '/../../3rdparty/rtsp-to-hls.sh ' . trim(str_replace(array_keys($replace), $replace, $camera->getConfiguration('cameraStreamAccessUrl'))).' "' . $camera->getConfiguration('localApiKey') . '" > /dev/null 2>&1 &'); - $i=0; - while(!file_exists(__DIR__.'/../../data/'.$camera->getConfiguration('localApiKey').'.m3u8')){ + log::add('camera', 'debug', 'nohup ' . $rtspScript . trim(str_replace(array_keys($replace), $replace, $camera->getConfiguration('cameraStreamAccessUrl'))) . ' "' . $camera->getConfiguration('localApiKey') . '" > /dev/null 2>&1 &'); + exec('nohup ' . $rtspScript . trim(str_replace(array_keys($replace), $replace, $camera->getConfiguration('cameraStreamAccessUrl'))) . ' "' . $camera->getConfiguration('localApiKey') . '" > /dev/null 2>&1 &'); + $i = 0; + while (!file_exists(__DIR__ . '/../../data/' . $camera->getConfiguration('localApiKey') . '.m3u8')) { sleep(1); $i++; - if($i>30){ + if ($i > 30) { break; } } } - $camera->setCache('lastStreamCall',strtotime('now')); - shell_exec(system::getCmdSudo().' find '.__DIR__.'/../../data/segments/'.$camera->getConfiguration('localApiKey').'-*.ts -mmin +5 -type f -exec rm -f {} \; 2>&1 > /dev/null'); - ajax::success(); - } - - if (!isConnect('admin')) { - throw new Exception(__('401 - Accès non autorisé', __FILE__)); - } - - - - if (init('action') == 'addDiscoverCam') { - camera::addDiscoverCam(json_decode(init('config'), true)); - ajax::success(); + $camera->setCache('lastStreamCall', strtotime('now')); + shell_exec(system::getCmdSudo() . ' find ' . __DIR__ . '/../../data/segments/' . $camera->getConfiguration('localApiKey') . '-*.ts -mmin +5 -type f -exec rm -f {} \; 2>&1 > /dev/null'); + ajax::success(); + } + + if (init('action') == 'getCamera') { + if (init('object_id') == '') { + $object = jeeObject::byId($_SESSION['user']->getOptions('defaultDashboardObject')); + } else { + $object = jeeObject::byId(init('object_id')); } - - if (init('action') == 'removeRecord') { - $file = init('file'); - $file = str_replace('..', '', $file); - $record_dir = calculPath(config::byKey('recordDir', 'camera')); - shell_exec('rm -rf ' . $record_dir . '/' . $file); - ajax::success(); + if (!is_object($object)) { + $object = jeeObject::rootObject(); } - - if (init('action') == 'removeAllSnapshot') { - $camera = camera::byId(init('id')); - if (!is_object($camera)) { - throw new Exception(__('Impossible de trouver la caméra : ' . init('id'), __FILE__)); - } - $camera->removeAllSnapshot(); - ajax::success(); - } - - if (init('action') == 'getCamera') { - if (init('object_id') == '') { - $object = jeeObject::byId($_SESSION['user']->getOptions('defaultDashboardObject')); - } else { - $object = jeeObject::byId(init('object_id')); - } - if (!is_object($object)) { - $object = jeeObject::rootObject(); - } - $return = array(); - $return['eqLogics'] = array(); - if (init('object_id') == '') { - foreach (jeeObject::all() as $object) { - foreach ($object->getEqLogic(true, false, 'camera') as $camera) { - $return['eqLogics'][] = $camera->toHtml(init('version')); - } - } - } else { + $return = array(); + $return['eqLogics'] = array(); + if (init('object_id') == '') { + foreach (jeeObject::all() as $object) { foreach ($object->getEqLogic(true, false, 'camera') as $camera) { $return['eqLogics'][] = $camera->toHtml(init('version')); } - foreach (jeeObject::buildTree($object) as $child) { - $cameras = $child->getEqLogic(true, false, 'camera'); - if (count($cameras) > 0) { - foreach ($cameras as $camera) { - $return['eqLogics'][] = $camera->toHtml(init('version')); - } + } + } else { + foreach ($object->getEqLogic(true, false, 'camera') as $camera) { + $return['eqLogics'][] = $camera->toHtml(init('version')); + } + foreach (jeeObject::buildTree($object) as $child) { + $cameras = $child->getEqLogic(true, false, 'camera'); + if (count($cameras) > 0) { + foreach ($cameras as $camera) { + $return['eqLogics'][] = $camera->toHtml(init('version')); } } } - ajax::success($return); } - - throw new Exception(__('Aucune methode correspondante à : ', __FILE__) . init('action')); - /* * *********Catch exeption*************** */ - } catch (Exception $e) { - ajax::error(displayException($e), $e->getCode()); + ajax::success($return); + } + + if (!isConnect('admin')) { + throw new Exception(__('401 - Accès non autorisé', __FILE__)); + } + + if (init('action') == 'addDiscoverCam') { + camera::addDiscoverCam(json_decode(init('config'), true)); + ajax::success(); + } + + if (init('action') == 'removeRecord') { + $file = init('file'); + $file = str_replace('..', '', $file); + $record_dir = calculPath(config::byKey('recordDir', 'camera')); + shell_exec('rm -rf ' . $record_dir . '/' . $file); + ajax::success(); + } + + if (init('action') == 'removeAllSnapshot') { + $camera = camera::byId(init('id')); + if (!is_object($camera)) { + throw new Exception(__('Equipement introuvable', __FILE__) . ' : ' . init('id')); + } + $camera->removeAllSnapshot(); + ajax::success(); } + + throw new Exception(__('Aucune methode correspondante à : ', __FILE__) . init('action')); + /* * *********Catch exeption*************** */ +} catch (Exception $e) { + ajax::error(displayException($e), $e->getCode()); +} diff --git a/core/class/camera.class.php b/core/class/camera.class.php index c5654cd..7652303 100644 --- a/core/class/camera.class.php +++ b/core/class/camera.class.php @@ -78,7 +78,7 @@ public static function deamon_start() { } $cron = cron::byClassAndFunction('camera', 'pull'); if (!is_object($cron)) { - throw new Exception(__('Tache cron introuvable', __FILE__)); + throw new Exception(__('Tâche cron introuvable', __FILE__)); } $cron->run(); } @@ -86,7 +86,7 @@ public static function deamon_start() { public static function deamon_stop() { $cron = cron::byClassAndFunction('camera', 'pull'); if (!is_object($cron)) { - throw new Exception(__('Tache cron introuvable', __FILE__)); + throw new Exception(__('Tâche cron introuvable', __FILE__)); } $cron->halt(); } @@ -206,7 +206,7 @@ public static function cronHourly() { $i = 0; while ($dir_size > $max_size) { if (count($files) == 0) { - throw new Exception(__('Erreur aucun fichier trouvé à supprimer alors que le répertoire fait : ', __FILE__) . $dir_size); + throw new Exception(__('Aucun fichier à supprimer alors que le répertoire pèse', __FILE__) . ' ' . $dir_size); } shell_exec('rm -rf ' . $files[$i]['file']); $dir_size -= $files[$i]['filesize']; @@ -229,7 +229,7 @@ public static function cronHourly() { } } - public static function cronDayly() { + public static function cronDaily() { foreach (camera::byType('camera') as $camera) { try { shell_exec('(ps ax || ps w) | grep ffmpeg.*' . $camera->getConfiguration('localApiKey') . ' | awk \'{print $2}\' | xargs sudo kill -9'); @@ -338,7 +338,7 @@ public static function addDiscoverCam($_config) { /* * *********************Methode d'instance************************* */ public function configOnvif() { - try{ + try { $onvif = new Ponvif(); $onvif->setUsername($this->getConfiguration('username')); $onvif->setPassword($this->getConfiguration('password')); @@ -349,7 +349,7 @@ public function configOnvif() { $this->setConfiguration('cameraStreamProfileToken', $sources[0][0]['profiletoken']); //save profiletoken for sending onvif ptz cmd $this->setConfiguration('cameraStreamAccessUrl', $mediaUri); } catch (Exception $e) { - log::add('camera','error','[ONVIF] '.$e->getMessage()); + log::add('camera', 'error', '[ONVIF] ' . $e->getMessage()); } } @@ -367,7 +367,7 @@ public function preSave() { if ($this->getConfiguration('alertMessageCommand') != '') { $cmd = cmd::byId(str_replace('#', '', $this->getConfiguration('alertMessageCommand'))); if (is_object($cmd) && $cmd->getEqType_name() == 'camera') { - throw new Exception(__('La "Commande d\'alerte" ne peut être de type caméra', __FILE__)); + throw new Exception(__('La "Commande d\'alerte" ne doit pas être de type caméra', __FILE__)); } } if ($this->getConfiguration('localApiKey') != '') { @@ -421,7 +421,7 @@ public function preSave() { public function preUpdate() { $this->setCategory('security', 1); if ($this->getConfiguration('ip') == '') { - throw new Exception(__('L\'adresse IP de la camera ne peut être vide', __FILE__)); + throw new Exception(__("L'adresse IP de la caméra doit être renseignée", __FILE__)); } } public function postSave() { @@ -432,7 +432,7 @@ public function postSave() { if (!is_object($urlFlux)) { $urlFlux = new cameraCmd(); } - $urlFlux->setName(__('Flux video', __FILE__)); + $urlFlux->setName(__('Flux vidéo', __FILE__)); $urlFlux->setConfiguration('request', '-'); $urlFlux->setType('info'); $urlFlux->setLogicalId('urlFlux'); @@ -451,7 +451,7 @@ public function postSave() { if (!is_object($recordState)) { $recordState = new cameraCmd(); } - $recordState->setName(__('Status enregistrement', __FILE__)); + $recordState->setName(__('Statut enregistrement', __FILE__)); $recordState->setConfiguration('recordState', 1); $recordState->setConfiguration('request', '-'); $recordState->setType('info'); @@ -516,7 +516,7 @@ public function postSave() { if (!is_object($on)) { $on = new cameraCmd(); } - $on->setName(__('On', __FILE__)); + $on->setName(__('Allumer', __FILE__)); $on->setOrder(-1); $on->setType('action'); $on->setLogicalId('on'); @@ -531,7 +531,7 @@ public function postSave() { if (!is_object($off)) { $off = new cameraCmd(); } - $off->setName(__('Off', __FILE__)); + $off->setName(__('Eteindre', __FILE__)); $off->setOrder(-1); $off->setType('action'); $off->setLogicalId('off'); @@ -583,17 +583,22 @@ public function toHtml($_version = 'dashboard', $_fluxOnly = false) { '#name#' => ($cmd->getDisplay('icon') != '') ? $cmd->getDisplay('icon') : $cmd->getName(), ); $action .= template_replace($replaceCmd, getTemplate('core', $version, 'camera_action', 'camera')) . ' '; + if ($cmd->getDisplay('forceReturnLineAfter', 0) == 1) { + $action .= '
'; + } } else { if ($cmd->getType() == 'info') { $info .= $cmd->toHtml($_version); + if ($cmd->getDisplay('forceReturnLineAfter', 0) == 1) { + $info .= ''; + } } else { $action .= $cmd->toHtml($_version); + if ($cmd->getDisplay('forceReturnLineAfter', 0) == 1) { + $action .= ''; + } } } - - if ($cmd->getDisplay('forceReturnLineAfter', 0) == 1) { - $action .= '
';
+ if ($eqLogic->getConfiguration('device') != "" && camera::getImgFilePath($eqLogic->getConfiguration('device')) !== false) {
+ $img = '| + | {{Dashboard}} | +{{Mobile}} | +
|---|---|---|
| + | + | + |
| + | + | + |
| + | + | + |
| + | {{Dashboard}} | +{{Mobile}} | +
|---|---|---|
| + | + | + |
| + | + | + |
| + | + | + |