From f213540a8b64a88d75cd18c8efdff447ebc737e5 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Wed, 21 Feb 2024 21:06:42 +0100 Subject: [PATCH 01/12] add manifest file parsing functions to exnvr.utils --- apps/ex_nvr/lib/ex_nvr/utils.ex | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/apps/ex_nvr/lib/ex_nvr/utils.ex b/apps/ex_nvr/lib/ex_nvr/utils.ex index 626aa634..27265205 100644 --- a/apps/ex_nvr/lib/ex_nvr/utils.ex +++ b/apps/ex_nvr/lib/ex_nvr/utils.ex @@ -5,6 +5,8 @@ defmodule ExNVR.Utils do @unix_socket_dir "/tmp/sockets" + @regex ~r/EXT-X-STREAM-INF:BANDWIDTH=(\d+),AVERAGE-BANDWIDTH=(\d+),RESOLUTION=(\d+x\d+),CODECS="([^"]+)"/ + @spec hls_dir(Device.id() | nil) :: Path.t() def hls_dir(device_id \\ nil) do dir = Application.get_env(:ex_nvr, :hls_directory) @@ -19,6 +21,45 @@ defmodule ExNVR.Utils do Path.join(@unix_socket_dir, "ex_nvr.#{device_id}.sock") end + def parse_manifest_file(file_path) do + case File.read(file_path) do + {:ok, content} -> + parse_content(content) + + _error -> + %{ + bandwidth: nil, + average_bandwidth: nil, + resolution: nil, + codecs: nil + } + end + end + + defp parse_content(content) do + content + |> String.split("\n", trim: true) + |> Enum.reduce([], fn line, acc -> + case Regex.run(@regex, line) do + [_, bandwidth, average_bandwidth, resolution, codecs] -> + [ + %{ + bandwidth: bandwidth, + average_bandwidth: average_bandwidth, + resolution: resolution, + codecs: codecs + } + | acc + ] + + _ -> + acc + end + end) + |> List.first() + |> IO.inspect() + end + @spec pipeline_name(Device.t()) :: atom() def pipeline_name(device), do: :"pipeline_#{device.id}" From 8a75ab42ea5934240fea4e9ce3b394b39c42bc1c Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Wed, 21 Feb 2024 21:07:18 +0100 Subject: [PATCH 02/12] add stream info to dashboard liveview --- .../lib/ex_nvr_web/live/dashboard_live.ex | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index 859ca757..0303c724 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -6,6 +6,7 @@ defmodule ExNVRWeb.DashboardLive do alias ExNVR.Recordings alias ExNVR.Model.Device alias ExNVRWeb.TimelineComponent + alias ExNVR.Utils @durations [ {"2 Minutes", "120"}, @@ -94,6 +95,15 @@ defmodule ExNVRWeb.DashboardLive do > <.icon name="hero-camera" /> +
+

Bandwidth: <%= @stream_info.bandwidth || "N/A" %>

+

Average Bandwidth: <%= @stream_info.average_bandwidth || "N/A" %>

+

Resolution: <%= @stream_info.resolution || "N/A" %>

+

Codecs: <%= @stream_info.codecs || "N/A" %>

+
assign_devices() |> assign_current_device() |> assign_streams() + |> assign_stream_info() |> assign_form(nil) |> assign_footage_form(%{}) |> live_view_enabled?() @@ -203,6 +214,7 @@ defmodule ExNVRWeb.DashboardLive do socket |> assign_current_device(device) |> assign_streams() + |> assign_stream_info() |> assign_form(nil) |> assign_footage_form(%{}) |> assign(start_date: nil) @@ -218,6 +230,7 @@ defmodule ExNVRWeb.DashboardLive do socket = socket |> assign_form(%{"stream" => stream, "device" => socket.assigns.current_device.id}) + |> assign_stream_info() |> live_view_enabled?() |> maybe_push_stream_event(socket.assigns.start_date) @@ -270,6 +283,19 @@ defmodule ExNVRWeb.DashboardLive do end end + defp assign_stream_info(socket) do + device = socket.assigns.current_device + + # manifest_path = "path/to/your/manifest.m3u8" + hls_dir = Path.join(Utils.hls_dir(device.id), "live") + manifest_path = Path.join(hls_dir, "index.m3u8") |> IO.inspect() + + stream_info = Utils.parse_manifest_file(manifest_path) + + # Update the socket's assigns with the new stream info + assign(socket, stream_info: stream_info) + end + defp assign_devices(socket) do assign(socket, devices: Devices.list()) end From 850b3b6d6c3e77f11cd03ae8b86bdb4ecbebfa14 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Wed, 21 Feb 2024 21:07:39 +0100 Subject: [PATCH 03/12] add a periodic event check to update stream info --- .../ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index 0303c724..a90c7048 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -190,6 +190,13 @@ defmodule ExNVRWeb.DashboardLive do """ end + @impl true + def on_mount(:default, _params, _session, socket) do + # Schedule periodic manifest updates, e.g., every 3 seconds + :timer.send_interval(3_000, self(), :update_manifest) + {:cont, socket} + end + def mount(_params, _session, socket) do socket = socket @@ -283,6 +290,11 @@ defmodule ExNVRWeb.DashboardLive do end end + @impl true + def handle_info(:update_manifest, socket) do + assign_stream_info(socket) + end + defp assign_stream_info(socket) do device = socket.assigns.current_device From 2ac1c9fc63ad14e277a9e9503aef6a5184b9b956 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Fri, 23 Feb 2024 16:37:02 +0100 Subject: [PATCH 04/12] change adding stream info from backend to frontend using hls.js --- apps/ex_nvr/lib/ex_nvr/utils.ex | 39 ------------------ apps/ex_nvr_web/assets/js/app.js | 15 +++++++ .../lib/ex_nvr_web/live/dashboard_live.ex | 41 ++++--------------- 3 files changed, 23 insertions(+), 72 deletions(-) diff --git a/apps/ex_nvr/lib/ex_nvr/utils.ex b/apps/ex_nvr/lib/ex_nvr/utils.ex index 27265205..85a43f76 100644 --- a/apps/ex_nvr/lib/ex_nvr/utils.ex +++ b/apps/ex_nvr/lib/ex_nvr/utils.ex @@ -21,45 +21,6 @@ defmodule ExNVR.Utils do Path.join(@unix_socket_dir, "ex_nvr.#{device_id}.sock") end - def parse_manifest_file(file_path) do - case File.read(file_path) do - {:ok, content} -> - parse_content(content) - - _error -> - %{ - bandwidth: nil, - average_bandwidth: nil, - resolution: nil, - codecs: nil - } - end - end - - defp parse_content(content) do - content - |> String.split("\n", trim: true) - |> Enum.reduce([], fn line, acc -> - case Regex.run(@regex, line) do - [_, bandwidth, average_bandwidth, resolution, codecs] -> - [ - %{ - bandwidth: bandwidth, - average_bandwidth: average_bandwidth, - resolution: resolution, - codecs: codecs - } - | acc - ] - - _ -> - acc - end - end) - |> List.first() - |> IO.inspect() - end - @spec pipeline_name(Device.t()) :: atom() def pipeline_name(device), do: :"pipeline_#{device.id}" diff --git a/apps/ex_nvr_web/assets/js/app.js b/apps/ex_nvr_web/assets/js/app.js index 1e4a7b51..5fdb6a83 100644 --- a/apps/ex_nvr_web/assets/js/app.js +++ b/apps/ex_nvr_web/assets/js/app.js @@ -116,6 +116,8 @@ function initDarkMode() { startStreaming = (src, poster_url) => { var video = document.getElementById("live-video") + var infoBox = document.getElementById("stream-info"); + if (video != null && Hls.isSupported()) { if (window.hls) { window.hls.destroy() @@ -130,6 +132,19 @@ startStreaming = (src, poster_url) => { }) window.hls.loadSource(src) window.hls.attachMedia(video) + + window.hls.on(Hls.Events.LEVEL_LOADED, (event, data) => { + const { level } = data; + const levelInfo = window.hls.levels[level]; + + infoBox.innerHTML = ` +

Bandwith: ${levelInfo.bitrate}

+

Avg.Bandwith: ${levelInfo.averageBitrate}

+

Resolution: ${levelInfo.width}x${levelInfo.height}

+

Codecs: ${levelInfo.attrs.CODECS}

+ `; + infoBox.innerHTML = infoHtml; + }); } } diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index a90c7048..1e55c930 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -95,14 +95,17 @@ defmodule ExNVRWeb.DashboardLive do > <.icon name="hero-camera" />
+
-

Bandwidth: <%= @stream_info.bandwidth || "N/A" %>

-

Average Bandwidth: <%= @stream_info.average_bandwidth || "N/A" %>

-

Resolution: <%= @stream_info.resolution || "N/A" %>

-

Codecs: <%= @stream_info.codecs || "N/A" %>

assign_devices() |> assign_current_device() |> assign_streams() - |> assign_stream_info() |> assign_form(nil) |> assign_footage_form(%{}) |> live_view_enabled?() @@ -221,7 +216,6 @@ defmodule ExNVRWeb.DashboardLive do socket |> assign_current_device(device) |> assign_streams() - |> assign_stream_info() |> assign_form(nil) |> assign_footage_form(%{}) |> assign(start_date: nil) @@ -237,7 +231,6 @@ defmodule ExNVRWeb.DashboardLive do socket = socket |> assign_form(%{"stream" => stream, "device" => socket.assigns.current_device.id}) - |> assign_stream_info() |> live_view_enabled?() |> maybe_push_stream_event(socket.assigns.start_date) @@ -290,24 +283,6 @@ defmodule ExNVRWeb.DashboardLive do end end - @impl true - def handle_info(:update_manifest, socket) do - assign_stream_info(socket) - end - - defp assign_stream_info(socket) do - device = socket.assigns.current_device - - # manifest_path = "path/to/your/manifest.m3u8" - hls_dir = Path.join(Utils.hls_dir(device.id), "live") - manifest_path = Path.join(hls_dir, "index.m3u8") |> IO.inspect() - - stream_info = Utils.parse_manifest_file(manifest_path) - - # Update the socket's assigns with the new stream info - assign(socket, stream_info: stream_info) - end - defp assign_devices(socket) do assign(socket, devices: Devices.list()) end From 22e36cea1c740565022eda88c3cb922ff4bd9a08 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Fri, 23 Feb 2024 16:41:54 +0100 Subject: [PATCH 05/12] fix CSS --- apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index 1e55c930..8e7e592e 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -97,7 +97,7 @@ defmodule ExNVRWeb.DashboardLive do
From b0198a797f4b40332b37efe14a1c4328d79dd6b1 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Fri, 23 Feb 2024 18:05:31 +0100 Subject: [PATCH 08/12] convert data and get actual bandwith estimate --- apps/ex_nvr_web/assets/js/app.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/ex_nvr_web/assets/js/app.js b/apps/ex_nvr_web/assets/js/app.js index 67798d0f..a6bea90b 100644 --- a/apps/ex_nvr_web/assets/js/app.js +++ b/apps/ex_nvr_web/assets/js/app.js @@ -132,14 +132,16 @@ startStreaming = (src, poster_url) => { }) window.hls.loadSource(src) window.hls.attachMedia(video) - + window.hls.on(Hls.Events.LEVEL_LOADED, (event, data) => { - const { level } = data; + const { level, stats } = data; + console.log(data) const levelInfo = window.hls.levels[level]; - + const mbpsFactor = 1024 * 1024 infoBox.innerHTML = ` -

Bitrate: ${levelInfo.bitrate}

-

Avg.Bitrate: ${levelInfo.averageBitrate}

+

Bandwith Estimate: ${(window.hls.bandwidthEstimate / mbpsFactor).toFixed(3)} (Mbps)

+

Bitrate: ${(levelInfo.bitrate / mbpsFactor).toFixed(3)} (Mbps)

+

Avg.Bitrate: ${(levelInfo.averageBitrate / mbpsFactor).toFixed(3)} (Mbps)

Resolution: ${levelInfo.width}x${levelInfo.height}

Codecs: ${levelInfo.attrs.CODECS}

`; From 6f35f5520e6e843436b70391bad4df0140d78ba5 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Fri, 23 Feb 2024 18:05:59 +0100 Subject: [PATCH 09/12] remove console.log --- apps/ex_nvr_web/assets/js/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ex_nvr_web/assets/js/app.js b/apps/ex_nvr_web/assets/js/app.js index a6bea90b..ef4ac634 100644 --- a/apps/ex_nvr_web/assets/js/app.js +++ b/apps/ex_nvr_web/assets/js/app.js @@ -135,7 +135,6 @@ startStreaming = (src, poster_url) => { window.hls.on(Hls.Events.LEVEL_LOADED, (event, data) => { const { level, stats } = data; - console.log(data) const levelInfo = window.hls.levels[level]; const mbpsFactor = 1024 * 1024 infoBox.innerHTML = ` From 0a9ac43b4cad01b7ea1fc3c4d0c72564d5bdf8e6 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Sat, 24 Feb 2024 14:01:35 +0100 Subject: [PATCH 10/12] change the stream info icon to information icon --- apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index c792d8ef..61f219aa 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -100,7 +100,7 @@ defmodule ExNVRWeb.DashboardLive do class="absolute top-10 right-1 rounded-sm bg-zinc-900 py-1 px-2 text-sm text-white dark:bg-gray-700 dark:bg-opacity-80 hover:cursor-pointer" phx-click={JS.toggle(to: "#stream-info")} > - <.icon name="hero-eye" /> + <.icon name="hero-information-circle" />
Date: Sat, 24 Feb 2024 14:02:06 +0100 Subject: [PATCH 11/12] move converting bitrates to a separate function --- apps/ex_nvr_web/assets/js/app.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/ex_nvr_web/assets/js/app.js b/apps/ex_nvr_web/assets/js/app.js index ef4ac634..d6246614 100644 --- a/apps/ex_nvr_web/assets/js/app.js +++ b/apps/ex_nvr_web/assets/js/app.js @@ -136,11 +136,11 @@ startStreaming = (src, poster_url) => { window.hls.on(Hls.Events.LEVEL_LOADED, (event, data) => { const { level, stats } = data; const levelInfo = window.hls.levels[level]; - const mbpsFactor = 1024 * 1024 + infoBox.innerHTML = ` -

Bandwith Estimate: ${(window.hls.bandwidthEstimate / mbpsFactor).toFixed(3)} (Mbps)

-

Bitrate: ${(levelInfo.bitrate / mbpsFactor).toFixed(3)} (Mbps)

-

Avg.Bitrate: ${(levelInfo.averageBitrate / mbpsFactor).toFixed(3)} (Mbps)

+

Bandwith Estimate: ${convertBitrate(window.hls.bandwidthEstimate)}

+

Bitrate: ${convertBitrate(levelInfo.bitrate)}

+

Avg.Bitrate: ${convertBitrate(levelInfo.averageBitrate)}

Resolution: ${levelInfo.width}x${levelInfo.height}

Codecs: ${levelInfo.attrs.CODECS}

`; @@ -149,6 +149,17 @@ startStreaming = (src, poster_url) => { } } +function convertBitrate(bitRate) { + const mbpsFactor = 1000 * 1000 + + let bitRateMbps = (bitRate / mbpsFactor) + if (bitRateMbps < 1) { + return `${(bitRateMbps * 1000).toFixed(2)} Kbps` + } else { + return `${bitRateMbps.toFixed(2)} Mbps` + } +} + window.addEventListener("phx:stream", (e) => { startStreaming(e.detail.src, e.detail.poster) }) From 7a8f8d26abdda00aea77ed52ac9d5e8be9948469 Mon Sep 17 00:00:00 2001 From: Mebareksaf Date: Tue, 27 Feb 2024 01:51:29 +0100 Subject: [PATCH 12/12] remove unused alias --- apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex index 61f219aa..663c1a0e 100644 --- a/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex +++ b/apps/ex_nvr_web/lib/ex_nvr_web/live/dashboard_live.ex @@ -6,7 +6,6 @@ defmodule ExNVRWeb.DashboardLive do alias ExNVR.Recordings alias ExNVR.Model.Device alias ExNVRWeb.TimelineComponent - alias ExNVR.Utils @durations [ {"2 Minutes", "120"},