Browse Source

Motion video-surveillance

master
Yves G 6 months ago
parent
commit
6247e9c521
14 changed files with 540 additions and 0 deletions
  1. +29
    -0
      group_vars/all
  2. +10
    -0
      roles/dmz_motion_front/handlers/main.yml
  3. +7
    -0
      roles/dmz_motion_front/meta.OK/main.yml
  4. +56
    -0
      roles/dmz_motion_front/tasks/main.yml
  5. +103
    -0
      roles/dmz_motion_front/templates/index.html.j2
  6. +1
    -0
      roles/front_run/meta.OK/main.yml
  7. BIN
      roles/motion_back/files/example_mask_640_360.pgm
  8. +22
    -0
      roles/motion_back/handlers/main.yml
  9. +8
    -0
      roles/motion_back/meta.OK/main.yml
  10. +251
    -0
      roles/motion_back/tasks/main.yml
  11. +9
    -0
      roles/motion_back/templates/email.sh.j2
  12. +40
    -0
      roles/motion_back/templates/upload.sh.j2
  13. +2
    -0
      roles/ssowat/templates/conf.json.j2
  14. +2
    -0
      site.yml

+ 29
- 0
group_vars/all View File

@ -124,6 +124,9 @@ http_pfx_gitea: /git
# URL prefix of LDAP-Account-Manager (web UI for LDAP).
http_pfx_lam: /account
# URL prefix of Motion (video surveillance).
http_pfx_motion: /netcam
# URL prefix of Movim (XMPP web client).
http_pfx_movim: /social
@ -308,6 +311,32 @@ media_minidlna_conf: |
root_container=B
friendly_name=HomeMedia
# Motion data directory
motion_data: /var/lib/motion
motion_cloud_url: 'https://www.mediafire.com/'
motion_cloud_login: login
motion_cloud_password: password
motion_cloud_id: app_id_xxxxx
motion_cloud_key: xxxxxxxxxx…xxxxxxxxxx
motion_email_recipient: hostmaster@localhost
motion_cameras: '[
{
"id": 1, "name": "street door",
"url": "rtsp://user:password@street.example.org:554/videoMain",
"width": 640, "height": 360,
"mask_file": "example_mask_640_360.pgm",
"framerate": 5
},
{
"id": 2, "name": "garden door",
"url": "rtsp://user:password@garden.example.org:554/videoMain",
"width": 640, "height": 360,
"mask_file": null,
"framerate": 5
}
]'
motion_web_title: "Video surveillance"
# Name of the Movim database in PostgreSQL.
movim_db: movim


+ 10
- 0
roles/dmz_motion_front/handlers/main.yml View File

@ -0,0 +1,10 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
- name: restart nginx.service
systemd:
daemon_reload: true
name: nginx.service
state: restarted

+ 7
- 0
roles/dmz_motion_front/meta.OK/main.yml View File

@ -0,0 +1,7 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
dependencies:
- role: dmz_nginx

+ 56
- 0
roles/dmz_motion_front/tasks/main.yml View File

@ -0,0 +1,56 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
- name: create a directory for the Motion web page
file:
name: /srv/http/motion
state: directory
mode: 0750
owner: root
group: http
- name: send the feedback and control web page for Motion
template:
src: templates/index.html.j2
dest: /srv/http/motion/index.html
mode: 0640
owner: root
group: http
- name: configure nginx for Motion
copy:
content: |
location {{http_pfx_motion}}/ {
alias /srv/http/motion/;
index index.html;
}
location {{http_pfx_motion}}/control/ {
proxy_pass http://unix:/run/shared_sockets/motion_control.socket:/;
proxy_buffering off;
proxy_cache off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location {{http_pfx_motion}}/camera/ {
proxy_pass http://unix:/run/shared_sockets/motion_stream.socket:/;
proxy_buffering off;
proxy_cache off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
dest: /etc/nginx/inc.d/motion.https.inc
mode: 0440
owner: http
group: http
notify:
- restart nginx.service
### LOCAL COMMIT ⇒ ###
- name: commit local changes
include_role: name=etckeeper.inc allow_duplicates=true tasks_from=local.yml
vars:
msg: Gitea
### ⇐ LOCAL COMMIT ###
- meta: flush_handlers

+ 103
- 0
roles/dmz_motion_front/templates/index.html.j2 View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{motion_web_title}}</title>
<script type="text/javascript">
var motionControl = {
Camera: function(section) {
this.number = section.dataset.number;
section.innerHTML = document.getElementById('camera-template').innerHTML;
this.statusFrame = section.getElementsByClassName('status-iframe')[0];
this.offLink = section.getElementsByClassName('status-off')[0];
this.onLink = section.getElementsByClassName('status-on')[0];
this.motionImg = section.getElementsByClassName('camera-motion')[0];
this.statusFrame.src = 'control/' + this.number + '/detection/status';
this.offLink['data-href'] = 'control/' + this.number + '/detection/pause';
this.onLink['data-href'] = 'control/' + this.number + '/detection/start';
this.motionImgEventListener = () => { this.resizeMotion(); };
this.motionImg.addEventListener('load', this.motionImgEventListener);
this.motionImg.src = 'camera/' + this.number + '/motion';
section.getElementsByClassName('camera-stream')[0].src = 'camera/' + this.number + '/stream';
section.getElementsByTagName('h1')[0].innerHTML = section.dataset.title;
this.offLink.addEventListener('click', event => { this.handleStatusControl(event); });
this.onLink.addEventListener('click', event => { this.handleStatusControl(event); });
},
init: function() {
const cameras = document.getElementsByClassName('camera');
for (let i = 0; i < cameras.length; i++) {
new motionControl.Camera(cameras[i]);
}
}
};
motionControl.Camera.prototype.handleStatusControl = function(event) {
event.preventDefault();
let url = event.target['data-href'];
let request = new XMLHttpRequest();
request.addEventListener('load', () => { this.reloadStatus(); }, false);
request.addEventListener('error', () => { this.showError(); }, false);
request.open('GET', url);
request.send();
};
motionControl.Camera.prototype.reloadStatus = function() {
this.statusFrame.contentWindow.location.reload(true);
};
motionControl.Camera.prototype.showError = function(event) {
console.log(event);
alert('ERROR (type ' + event.type + '); see the browser console for details.');
};
motionControl.Camera.prototype.resizeMotion = function() {
this.motionImg.removeEventListener('load', this.motionImgEventListener);
this.motionImg.width /= 2; // height automatic
}
window.addEventListener('load', motionControl.init);
</script>
<style type="text/css">
#camera-template {
display: none;
}
.status-control {
float: left;
clear: left;
margin: 0.1em 0.5em;
padding: 0.25em;
width: 3em;
height: 1em;
box-sizing: content-box;
/* border-width: 0.5em 1em; assumed for native look */
}
.status-iframe {
box-sizing: border-box;
width: calc(100% - 6.5em);
min-width: 30ex;
height: 4.3em;
}
.camera-output {
clear: both;
}
</style>
</head>
<body>
<h1>{{motion_web_title}}</h1>
{% for camera in (motion_cameras | from_json) %}
<section class="camera" data-number={{camera.id}} data-title="{{camera.name}}"></section>
{% endfor %}
<div id="camera-template">
<h1></h1>
<section class="camera-control">
<button class="status-control status-off">OFF</button>
<button class="status-control status-on">ON</button>
<iframe class="status-iframe" type="text/plain; charset=UTF-8" src=""></iframe>
</section>
<section class="camera-output">
<img class="camera-stream" src="">
<img class="camera-motion" src="">
</section>
</div>
</body>
</html>

+ 1
- 0
roles/front_run/meta.OK/main.yml View File

@ -8,6 +8,7 @@ dependencies:
- role: dovecot
- role: front
- role: ihmgit_back
- role: motion_back
- role: movim_back
- role: nextcloud_back
- role: postgresql


BIN
roles/motion_back/files/example_mask_640_360.pgm View File


+ 22
- 0
roles/motion_back/handlers/main.yml View File

@ -0,0 +1,22 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
- name: restart motion.service
systemd:
daemon_reload: true
name: motion.service
state: restarted
- name: restart socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service
systemd:
daemon_reload: true
name: socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service
state: restarted
- name: restart socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service
systemd:
daemon_reload: true
name: socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service
state: restarted

+ 8
- 0
roles/motion_back/meta.OK/main.yml View File

@ -0,0 +1,8 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
dependencies:
- role: cleanupdate
- role: sockets

+ 251
- 0
roles/motion_back/tasks/main.yml View File

@ -0,0 +1,251 @@
---
# The home-server project produces a multi-purpose setup using Ansible.
# Copyright © 2018 Y. Gablin, under the GPL-3.0-or-later license.
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
### UPSTREAM BEGIN ⇒ ###
- name: pull prerequisites from upstream
include_role: name=etckeeper.inc allow_duplicates=true tasks_from=upstream.yml
vars:
msg: motion
### ⇐ UPSTREAM BEGIN ###
- name: install software
package:
name: "{{item}}"
state: present
with_items:
- curl
- motion
- 's-nail'
- socat
### UPSTREAM END ⇒ ###
- name: merge upstream
include_role: name=etckeeper.inc allow_duplicates=true tasks_from=merge.yml
vars:
msg: motion
### ⇐ UPSTREAM END ###
- name: send the script for Motion to send emails
template:
src: templates/email.sh.j2
dest: /etc/motion/email.sh
owner: root
group: motion
mode: 0750
notify:
- restart motion.service
- name: send the script for Motion to upload files
template:
src: templates/upload.sh.j2
dest: /etc/motion/upload.sh
owner: root
group: motion
mode: 0750
notify:
- restart motion.service
- name: send main Motion configuration
copy:
content: |
target_dir {{motion_data}}
on_event_end /etc/motion/email.sh %$ %v %Y-%m-%d %H:%M:%S
on_picture_save /etc/motion/upload.sh "%f"
minimum_motion_frames 5
event_gap 10
picture_output on
picture_quality 50
picture_filename %$-%v-%Y%m%d%H%M%S-%q@%K_%L
movie_output off
webcontrol_port 1080
webcontrol_localhost on
webcontrol_interface 1
webcontrol_parms 1
stream_port 1081
stream_localhost on
stream_preview_method 4
stream_quality 20
camera_dir /etc/motion/camera.d
dest: /etc/motion/motion.conf
owner: root
group: motion
mode: 0640
notify:
- restart motion.service
- name: create the directory for Motion cameras
file:
name: /etc/motion/camera.d
state: directory
owner: root
group: motion
mode: 0750
- name: send mask-files for Motion cameras
copy:
src: files/{{item.mask_file}}
dest: /etc/motion/camera.d/{{item.mask_file}}
owner: root
group: motion
mode: 0640
with_items: "{{motion_cameras}}"
when:
- (item.mask_file != None)
notify:
- restart motion.service
- name: send Motion cameras configuration
copy:
content: |
camera_id {{item.id}}
camera_name {{item.name}}
netcam_url {{item.url}}
{{ ('mask_file /etc/motion/camera.d/' + item.mask_file) if item.mask_file != None else '' }}
width {{item.width}}
height {{item.height}}
framerate {{item.framerate}}
text_right %q (%ix%J+%K+%L)
auto_brightness 0
noise_tune on
lightswitch_percent 40
lightswitch_frames 15
dest: /etc/motion/camera.d/camera_{{item.id}}.conf
owner: root
group: motion
mode: 0640
with_items: "{{motion_cameras}}"
notify:
- restart motion.service
- name: identify all Motion cameras configured on the server
find:
paths: [ '/etc/motion/camera.d' ]
patterns: [ 'camera_*.conf' ]
register: existing_cameras
changed_when: false
- name: only keep basenames of configured Motion cameras
set_fact:
existing_cameras: "{{ existing_cameras.files | map(attribute='path') | map('basename') | list }}"
changed_when: false
- name: filter out up-to-date Motion cameras
set_fact:
existing_cameras: "{{ existing_cameras | reject('contains', 'camera_' + (item.id | string) + '.conf') | list }}"
with_items: "{{ motion_cameras }}"
changed_when: false
- name: delete old Motion cameras
file:
path: /etc/motion/camera.d/{{item}}
state: absent
with_items: "{{ existing_cameras }}"
notify:
- restart motion.service
- name: ensure ownership of the Motion data directory
file:
path: "{{motion_data}}"
state: directory
owner: motion
recurse: true
- name: prepare override of Motion launch parameters
file:
name: /etc/systemd/system/motion.service.d
state: directory
- name: override Motion launch parameters
copy:
content: |
[Unit]
Description=Motion daemon, paused
[Service]
ExecStart=
ExecStart=/usr/bin/motion -n -m
dest: /etc/systemd/system/motion.service.d/paused-mode.conf
mode: 0644
notify:
- restart motion.service
- name: create a generic service for socat-based port-forwarding
copy:
content: |
[Unit]
Description=socat-based Unix domain socket to IPv4/TCP forwarding
After=network-online.target
Wants=network-online.target
[Service]
ExecStartPre=/usr/bin/sh -c 'rm -f "$${0%%%%:*}"' "%I"
ExecStart=/usr/bin/sh -c 'exec /usr/bin/socat -d UNIX-LISTEN:"$${0%%%%:*}",fork,mode=0666 TCP4:$${0#*:}' "%I"
PrivateDevices=yes
ProtectSystem=full
NoNewPrivileges=yes
ReadWritePaths=/run /tmp
dest: /etc/systemd/system/socat-unix-to-tcp4@.service
mode: 0644
notify:
- restart socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service
- restart socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service
- name: prepare instanciation of socat-based port-forwarding for Motion control
file:
name: /etc/systemd/system/socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service.d
state: directory
- name: instanciate socat-based port-forwarding for Motion control
copy:
content: |
[Unit]
Description=socat-based Unix–TCP forwarding of Motion control
After=motion.service
Wants=motion.service
dest: /etc/systemd/system/socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service.d/dependency.conf
mode: 0644
notify:
- restart socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service
- name: prepare instanciation of socat-based port-forwarding for Motion stream
file:
name: /etc/systemd/system/socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service.d
state: directory
- name: instanciate socat-based port-forwarding for Motion stream
copy:
content: |
[Unit]
Description=socat-based Unix–TCP forwarding of Motion stream
After=motion.service
Wants=motion.service
dest: /etc/systemd/system/socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service.d/dependency.conf
mode: 0644
notify:
- restart socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service
- name: enable Motion
systemd:
daemon_reload: true
name: motion.service
enabled: true
- name: enable unix-to-tcp forwarding for Motion control
systemd:
daemon_reload: true
name: socat-unix-to-tcp4@-run-shared_sockets-motion_control.socket\x3alocalhost\x3a1080.service
enabled: true
- name: enable unix-to-tcp forwarding for Motion stream
systemd:
daemon_reload: true
name: socat-unix-to-tcp4@-run-shared_sockets-motion_stream.socket\x3alocalhost\x3a1081.service
enabled: true
### LOCAL COMMIT ⇒ ###
- name: commit local changes
include_role: name=etckeeper.inc allow_duplicates=true tasks_from=local.yml
vars:
msg: motion
### ⇐ LOCAL COMMIT ###
- meta: flush_handlers

+ 9
- 0
roles/motion_back/templates/email.sh.j2 View File

@ -0,0 +1,9 @@
#!/bin/bash
# $1: camera
# $2: event number
# $3: ISO date
# $4: ISO time
printf 'Camera: %s\nEvent: %s\nDate: %s %s\n\nCloud: %s' \
"$1" "$2" "$3" "$4" '{{motion_cloud_url}}' \
| mail -s 'Motion event' {{motion_email_recipient}}

+ 40
- 0
roles/motion_back/templates/upload.sh.j2 View File

@ -0,0 +1,40 @@
#!/bin/bash
# $1: file to upload
BASE_URL=https://www.mediafire.com/api
TOKEN_FILE=/var/lib/motion/token
LOGIN='{{motion_cloud_login}}'
PASSWORD='{{motion_cloud_password}}'
# Plowshare App ID & API key
APP_ID={{motion_cloud_id}}
API_KEY='{{motion_cloud_key}}'
token="$(find "$TOKEN_FILE" -mmin -9 -exec cat {} \; 2>/dev/null)"
set -e
f="$1"
file_size="$(stat -L --printf=%s "$f")"
file_name="$(basename "$f")"
if [ -z "$token" ]; then
token_json="$(curl \
-d "email=$LOGIN" --data-urlencode "password=$PASSWORD" \
-d "application_id=$APP_ID" \
-d "signature=$(printf '%s' "$LOGIN$PASSWORD$APP_ID$API_KEY" | sha1sum | head -c40)" \
-d 'version=1' -d 'response_format=json' \
"$BASE_URL/user/get_session_token.php" \
| tr -d '\r\n ')"
grep -qF '"result":"Success"' <<<"$token_json"
token="$(sed -rn 's/.*"session_token":"(([^"]|\\")*)".*/\1/p' <<<"$token_json" \
| sed 's/\\\(.\)/\1/g')"
printf '%s' "$token" >"$TOKEN_FILE"
fi
curl \
-F "Filedata=@$f;filename=$file_name" \
-H "x-filename: $file_name" -H "x-filesize: $file_size" \
"$BASE_URL/upload/simple.php?session_token=${token}&path=/Camera&action_on_duplicate=keep&response_format=json"

+ 2
- 0
roles/ssowat/templates/conf.json.j2 View File

@ -47,11 +47,13 @@
},
"you": {
"allow": {
"{{net_soa}}{{http_pfx_motion}}/": "Surveillance",
"{{net_soa}}{{http_pfx_transmission}}": "BitTorrent"
}
},
"me": {
"allow": {
"{{net_soa}}{{http_pfx_motion}}/": "Surveillance",
"{{net_soa}}{{http_pfx_movim}}/": "Social",
"{{net_soa}}{{http_pfx_transmission}}": "BitTorrent",
"{{net_soa}}{{http_pfx_wallabag}}/": "Read later"


+ 2
- 0
site.yml View File

@ -33,6 +33,7 @@
- ssh
- dovecot
- mediaplayer
- motion_back
- front_run
- acme_back
- nextcloud_davfs
@ -61,6 +62,7 @@
- dmz_dotclear_front
- dmz_ihmldap
- dmz_prosody_front
- dmz_motion_front
# - dmz_wallabag_front
- acme_front
- privatebin


Loading…
Cancel
Save