Skip to content

Commit

Permalink
add query params to dashboard and release v0.10.0 (#389)
Browse files Browse the repository at this point in the history
* Add query params to dashboard

* Fix onvif discovery

* Bump version

* Update deps

* Copy ffmpeg deps to external libs

* Create external libs folder

* Update docker file
  • Loading branch information
gBillal authored Feb 29, 2024
1 parent bd52c16 commit c552190
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 159 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ RUN \

ARG VERSION
ENV VERSION=${VERSION}
ENV DOCKER_BUILD=true

ARG ERL_FLAGS
ENV ERL_FLAGS=$ERL_FLAGS
Expand Down
20 changes: 16 additions & 4 deletions apps/ex_nvr/lib/ex_nvr/onvif/discovery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,32 @@ defmodule ExNVR.Onvif.Discovery do
<e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope"
xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
xmlns:dn="http://www.onvif.org/ver10/network/wsdl">
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<e:Header>
<w:MessageID>uuid:edf6ea85-3a68-46f2-9d02-0b15ef52a787</w:MessageID>
<w:MessageID>uuid:$id</w:MessageID>
<w:To e:mustUnderstand="true">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>
<w:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>
</e:Header>
<e:Body>
<d:Probe>
<d:Types>dn:NetworkVideoTransmitter</d:Types>
<d:Types>tds:Device</d:Types>
</d:Probe>
</e:Body>
</e:Envelope>
"""
@scope_regex ~r[^onvif://www.onvif.org/(name|hardware)/(.*)]

def probe(timeout) do
msg = String.replace(@probe_message, "$id", UUID.uuid4())

with {:ok, socket} <- mockable(:gen_udp).open(0, [:binary, active: false]),
:ok <- mockable(:gen_udp).send(socket, @multicast_addr, @multicast_port, @probe_message) do
:ok <- mockable(:gen_udp).send(socket, @multicast_addr, @multicast_port, msg) do
socket
|> recv(timeout)
|> Map.values()
|> Enum.map(&String.replace(&1, ["\r\n", "\r", "\n"], ""))
|> Enum.filter(&String.starts_with?(&1, "<?xml"))
|> Enum.map(&deduplicate/1)
|> Enum.map(&%Soap.Response{body: &1, status_code: 200})
|> Enum.map(&Soap.Response.parse/1)
|> Enum.map(&delete_namespaces/1)
Expand All @@ -52,6 +56,14 @@ defmodule ExNVR.Onvif.Discovery do
end
end

# Some cameras (Axis) returns duplicate responses to the probe message
defp deduplicate(response) do
case :binary.matches(response, "<?xml") do
[_no_duplicates] -> response
[_first_match, {pos, _len} | _rest] -> :binary.part(response, 0, pos)
end
end

defp format_response(response) do
probe_match = get_in(response, [:ProbeMatches, :ProbeMatch])
scopes = probe_match[:Scopes] |> String.split()
Expand Down
4 changes: 2 additions & 2 deletions apps/ex_nvr/lib/ex_nvr/recordings/snapshooter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ defmodule ExNVR.Recordings.Snapshooter do
end

defp get_parameter_sets(%H264{stream_structure: {_avc, dcr}}) do
%{spss: spss, ppss: ppss} = H264.DecoderConfigurationRecord.parse(dcr)
%{spss: spss, ppss: ppss} = H264.Parser.DecoderConfigurationRecord.parse(dcr)
Enum.map_join(spss ++ ppss, &(<<0, 0, 0, 1>> <> &1))
end

defp get_parameter_sets(%H265{stream_structure: {_hevc, dcr}}) do
%{vpss: vpss, spss: spss, ppss: ppss} = H265.DecoderConfigurationRecord.parse(dcr)
%{vpss: vpss, spss: spss, ppss: ppss} = H265.Parser.DecoderConfigurationRecord.parse(dcr)
Enum.map_join(vpss ++ spss ++ ppss, &(<<0, 0, 0, 1>> <> &1))
end

Expand Down
8 changes: 5 additions & 3 deletions apps/ex_nvr/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExNVR.MixProject do
def project do
[
app: :ex_nvr,
version: "0.9.1",
version: "0.10.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down Expand Up @@ -40,6 +40,7 @@ defmodule ExNVR.MixProject do
{:phoenix_pubsub, "~> 2.1"},
{:ecto_sql, "~> 3.6"},
{:ecto_sqlite3, ">= 0.0.0"},
{:ecto_sqlite3_extras, "~> 1.2.0"},
{:jason, "~> 1.2"},
{:swoosh, "~> 1.15"},
{:finch, "~> 0.13"},
Expand All @@ -49,11 +50,12 @@ defmodule ExNVR.MixProject do
{:membrane_rtp_plugin, "~> 0.24.0"},
{:membrane_rtp_h264_plugin, "~> 0.19.0"},
{:membrane_rtp_h265_plugin, "~> 0.5.0"},
{:membrane_h26x_plugin, "~> 0.10.0"},
{:membrane_h264_plugin, "~> 0.9.0"},
{:membrane_h265_plugin, "~> 0.4.0"},
{:membrane_mp4_plugin, "~> 0.33.0", override: true},
{:membrane_file_plugin, "~> 0.16.0"},
{:membrane_http_adaptive_stream_plugin,
github: "gBillal/membrane_http_adaptive_stream_plugin", ref: "2a690bf1bf"},
github: "gBillal/membrane_http_adaptive_stream_plugin", ref: "b283625"},
{:membrane_h264_ffmpeg_plugin, "~> 0.31.0"},
{:membrane_h265_ffmpeg_plugin, "~> 0.4.0"},
{:membrane_ffmpeg_swscale_plugin, "~> 0.15.0"},
Expand Down
3 changes: 2 additions & 1 deletion apps/ex_nvr/test/ex_nvr/onvif_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule ExNVR.OnvifTest do
alias ExNVR.Onvif

@response """
<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope">
<Header>
<wsa:MessageID xmlns:wsa="http://www.w3.org/2005/08/addressing">uuid:de305d54-75b4-431b-adb2-eb6b9e546014</wsa:MessageID>
Expand Down Expand Up @@ -121,7 +122,7 @@ defmodule ExNVR.OnvifTest do
mock(:gen_udp, :open, Agent.start(fn -> 0 end))

mock(:gen_udp, [send: 4], fn _socket, {239, 255, 255, 250}, 3702, request ->
assert request =~ "dn:NetworkVideoTransmitter"
assert request =~ "tds:Device"
:ok
end)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule ExNVRWeb.API.RemoteStorageController do
end
end

def remote_storage_plug(%Conn{} = conn, opts) do
def remote_storage_plug(%Conn{} = conn, _opts) do
remote_storage_id = conn.path_params["id"]

case RemoteStorages.get(remote_storage_id) do
Expand Down
98 changes: 47 additions & 51 deletions apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule ExNVRWeb.DashboardLive do
alias ExNVR.Devices
alias ExNVR.Recordings
alias ExNVR.Model.Device
alias ExNVRWeb.Router.Helpers, as: Routes
alias ExNVRWeb.TimelineComponent

@durations [
Expand Down Expand Up @@ -181,47 +182,51 @@ defmodule ExNVRWeb.DashboardLive do
end

def mount(_params, _session, socket) do
socket =
socket
|> assign_devices()
|> assign_current_device()
|> assign_streams()
|> assign_form(nil)
|> assign_footage_form(%{})
|> live_view_enabled?()
|> assign_runs()
|> assign_timezone()
|> maybe_push_stream_event(nil)

{:ok, assign(socket, start_date: nil, custom_duration: false)}
socket
|> assign_devices()
|> assign(
start_date: nil,
custom_duration: false
)
|> then(&{:ok, &1})
end

def handle_event("switch_device", %{"device" => device_id}, socket) do
device = Enum.find(socket.assigns.devices, &(&1.id == device_id))
def handle_params(params, _uri, socket) do
devices = socket.assigns.devices

socket =
socket
|> assign_current_device(device)
|> assign_streams()
|> assign_form(nil)
|> assign_footage_form(%{})
|> assign(start_date: nil)
|> live_view_enabled?()
|> assign_runs()
|> assign_timezone()
|> maybe_push_stream_event(socket.assigns.start_date)
device =
Enum.find(socket.assigns.devices, List.first(devices), &(&1.id == params["device_id"]))

stream = Map.get(params, "stream", socket.assigns[:stream]) || "sub_stream"

socket
|> assign(current_device: device)
|> assign(stream: stream, start_date: nil)
|> assign_streams()
|> assign_form()
|> assign_footage_form(%{"device_id" => device && device.id})
|> live_view_enabled?()
|> assign_runs()
|> assign_timezone()
|> maybe_push_stream_event(socket.assigns.start_date)
|> then(&{:noreply, &1})
end

{:noreply, socket}
def handle_event("switch_device", %{"device" => device_id}, socket) do
route =
Routes.dashboard_path(socket, :new, %{device_id: device_id, stream: socket.assigns.stream})

{:noreply, push_patch(socket, to: route, replace: true)}
end

def handle_event("switch_stream", %{"stream" => stream}, socket) do
socket =
socket
|> assign_form(%{"stream" => stream, "device" => socket.assigns.current_device.id})
|> live_view_enabled?()
|> maybe_push_stream_event(socket.assigns.start_date)
route =
Routes.dashboard_path(socket, :new, %{
device_id: socket.assigns.current_device.id,
stream: stream
})

{:noreply, socket}
{:noreply, push_patch(socket, to: route, replace: true)}
end

def handle_event("datetime", %{"value" => value}, socket) do
Expand Down Expand Up @@ -274,24 +279,19 @@ defmodule ExNVRWeb.DashboardLive do
assign(socket, devices: Devices.list())
end

defp assign_current_device(socket, device \\ nil) do
devices = socket.assigns.devices
assign(socket, current_device: device || List.first(devices))
end

defp assign_streams(%{assigns: %{current_device: nil}} = socket), do: socket

defp assign_streams(socket) do
device = socket.assigns.current_device
%{current_device: device, stream: stream} = socket.assigns

supported_streams =
{supported_streams, stream} =
if Device.has_sub_stream(device) do
[{"Main Stream", "main_stream"}, {"Sub Stream", "sub_stream"}]
{[{"Main Stream", "main_stream"}, {"Sub Stream", "sub_stream"}], stream}
else
[{"Main Stream", "main_stream"}]
{[{"Main Stream", "main_stream"}], "main_stream"}
end

assign(socket, supported_streams: supported_streams)
assign(socket, supported_streams: supported_streams, stream: stream)
end

defp assign_runs(%{assigns: %{current_device: nil}} = socket), do: socket
Expand Down Expand Up @@ -323,15 +323,11 @@ defmodule ExNVRWeb.DashboardLive do
assign(socket, timezone: socket.assigns.current_device.timezone)
end

defp assign_form(%{assigns: %{current_device: nil}} = socket, _params), do: socket

defp assign_form(socket, nil) do
device = socket.assigns.current_device
assign(socket, form: to_form(%{"device" => device.id, "stream" => "main_stream"}))
end
defp assign_form(%{assigns: %{current_device: nil}} = socket), do: socket

defp assign_form(socket, params) do
assign(socket, form: to_form(params))
defp assign_form(socket) do
%{current_device: device, stream: stream} = socket.assigns
assign(socket, form: to_form(%{"device" => device.id, "stream" => stream}))
end

defp assign_footage_form(socket, params) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,6 @@ defmodule ExNVRWeb.RecordingListLive do
defp format_date(date, timezone) do
date
|> DateTime.shift_zone!(timezone)
|> Calendar.strftime("%b %d, %Y %H:%M:%S")
|> Calendar.strftime("%b %d, %Y %H:%M:%S %Z")
end
end
5 changes: 3 additions & 2 deletions apps/ex_nvr_web/lib/ex_nvr_web/user_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ defmodule ExNVRWeb.UserAuth do
Used for routes that require a webhook token.
"""
def require_webhook_token(conn, _opts) do
if (token = fetch_token_from_headers_or_query_params(conn, "token")) &&
Accounts.verify_webhook_token(token) do
token = fetch_token_from_headers_or_query_params(conn, "token")

if token && Accounts.verify_webhook_token(token) do
conn
else
unauthorized(conn)
Expand Down
2 changes: 1 addition & 1 deletion apps/ex_nvr_web/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExNVRWeb.MixProject do
def project do
[
app: :ex_nvr_web,
version: "0.9.1",
version: "0.10.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down
4 changes: 2 additions & 2 deletions apps/ex_nvr_web/priv/static/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.0
info:
title: ExNVR API
description: Manage ExNVR via API endpoints
version: 0.9.1
version: 0.10.0
servers:
- url: '{protocol}://{host}:{port}'
variables:
Expand Down Expand Up @@ -1035,7 +1035,7 @@ components:
maximum: 1.0
minItems: 4
maxItems: 4
desription: The normalized coordinates of the plate.
description: The normalized coordinates of the plate.
plate_color:
type: string
description: The color of the vehicle plate.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule ExNVRWeb.API.RemoteStorageTest do
|> get("/api/remote-storages/")
|> json_response(200)

assert length(response) == 0
assert Enum.empty?(response)
end

test "get all remote_storages (remote_storages)", %{conn: conn} do
Expand Down
36 changes: 8 additions & 28 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@ In this guide, we'll deploy `ex_nvr` using `docker compose`. We provide a `docke

Copy the two provided files to your target host and update the files to fit your needs.

## Quick deploy

We provide a script to quickly deploy `ex_nvr` using `docker compose`. It'll try to install `docker` if it's not installed on the target machine (currently only `ubuntu` is supported).

Before running the script, make sure that a `cert` folder with `certificate.key` and `certificate.crt` files exists in the folder where you'll run the following command (For `Evercam` we already have certificates on `zoho vault`).

```bash
bash <(wget -qO- https://evercam-public-assets.s3.eu-west-1.amazonaws.com/ex_nvr/docker-deploy.sh)
```

The script will prompt you for some configuration with default values.

In the following steps, we'll explain in details what the script does

## Dependencies

Make sure that `docker` and `docker compose plugin` are installed on your target machine.
Expand All @@ -32,27 +18,21 @@ The docker images are saved in Github container registry `ghcr.io` and publicly
docker pull ghcr.io/evercam/ex_nvr:latest
```

## Which image to use

Currently there's two different tags available for `ex_nvr` images, `v*` and `v*-armv7` where `*` represent a version number (e.g. `0.1.1`), the first is for `arm64/v8` and `amd64` machines and the latter for `arm/v7`.

Update `docker-compose.yml` file to use the appropriate image.

## HTTPS

If `https` is enabled and it should be, we'll need a private key and a certificate. Generating this files is out of the scope of this guide. However, there's many ways to generate this certificates, like self signed certificates (not recommended for production) or using a tool like [`let's encrypt`](https://letsencrypt.org/).
If `https` is desired, we need to provide an SSL certificate: a public and private key. Generating this files is out of the scope of this guide. However, there's many ways to generate this certificates, like self signed certificates (not recommended for production) or using a tool like [`let's encrypt`](https://letsencrypt.org/).

In this guide, we assume the files are called `certificate.key` for the key, and `certificate.crt` for the certificate.

## Prepare volumes

By default any data written to a docker container will be lost when the container is stopped, to preserve the data you need volumes. In this guide we suppose that we have a hard drive mounted at `/data`.
By default any data written to a docker container will be lost when the container is stopped, to preserve the data you need volumes. We suppose that we have a hard drive mounted at `/media/hdd`.

* Create the needed folders
* Create the database and cert folders
```bash
sudo mkdir /data/ex_nvr
cd /data/ex_nvr
sudo mkdir database recordings cert
sudo mkdir /media/hdd
cd /media/hdd/ex_nvr
sudo mkdir database cert
```

* Copy the certificates (needed for https) to the `cert` folder
Expand All @@ -61,9 +41,9 @@ By default any data written to a docker container will be lost when the containe
```
If you don't plan to use `https`, you can skip this step

* Make the `ex_nvr` user the owner of the *ex_nvr* folder
* Make the `ex_nvr` the owner of the */data* folder
```bash
sudo chown -R nobody:nogroup /data/ex_nvr
sudo chown -R nobody:nogroup /media/hdd/ex_nvr
```

## Environment variables
Expand Down
Loading

0 comments on commit c552190

Please sign in to comment.