Working NVR plugin

This commit is contained in:
Ziver Koc 2022-05-20 13:04:23 +02:00
parent 3a278b0ca6
commit 2501831a59
15 changed files with 268 additions and 57 deletions

Binary file not shown.

View file

@ -82,9 +82,9 @@
<div class="form-group">
<label class="control-label">Type:</label>
<select class="form-control" name="type">
{{#availableCameras}}
{{#availableCameraConfigClasses}}
<option>{{.getName()}}</option>
{{/availableCameras}}
{{/availableCameraConfigClasses}}
</select>
</div>
@ -102,7 +102,7 @@
</div>
</div>
<div id="camera-data-conf-template" class="hidden">
{{#cameraConf}}
{{#availableCameraObjectConfig}}
<div id="{{.clazz.getName()}}">
{{#.params}}
<div class="form-group">
@ -118,6 +118,6 @@
</div>
{{/.params}}
</div>
{{/cameraConf}}
{{/availableCameraObjectConfig}}
</div>

View file

@ -1,9 +1,19 @@
<h1 class="page-header">Details for <a href="#">{{camera.getName()}}</a></h1>
<style>
#view-main {
width: 100%;
height: 480px;
}
</style>
<div class="col-md-12">
<video width="100%" height="480" controls>
<source src="rtsp://admin:xxxx@192.168.10.223:554/H.264">
Your browser does not support the video tag.
<video id="view-main" width="100%" height="480" class="video-js vjs-fill" preload="auto" data-setup='{}' controls autoplay muted >
<source src="{{camera.getPlaylistRelativeUrl()}}">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video.</a>
</p>
</video>
</div>
@ -24,7 +34,7 @@
</thead>
<tr>
<th class="text-right">Type:</th>
<td>{{camera.getDeviceData().getClass().getSimpleName()}}</td>
<td>{{camera.getDeviceConfig().getClass().getSimpleName()}}</td>
</tr>
<tr>
<th class="text-right">Owner:</th>
@ -40,3 +50,7 @@
</div>
</div>
</div>
<link href="css/video-js.min.css" rel="stylesheet">
<script src="js/video.min.js"></script>
<script src="js/videojs-http-streaming.min.js"></script>

View file

@ -1,9 +1,7 @@
<h1 class="page-header">Camera Monitor</h1>
<style>
.monitor-view {
width: 100%;
height: 600px;
height: 740px;
}
</style>
@ -11,7 +9,7 @@
<tr>
<td>
<video id="view1" class="video-js vjs-fill" preload="auto" data-setup='{}' controls autoplay muted >
<source src="{{stream1}}">
<source src="{{camera1.getPlaylistRelativeUrl()}}">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video.</a>
@ -21,7 +19,7 @@
<td>
<video id="view2" class="video-js vjs-fill" preload="auto" data-setup='{}' controls autoplay muted >
<source src="{{stream2}}">
<source src="{{camera2.getPlaylistRelativeUrl()}}">
</video>
</td>
</tr>
@ -29,13 +27,13 @@
<tr>
<td>
<video id="view3" class="video-js vjs-fill" preload="auto" data-setup='{}' controls autoplay muted >
<source src="{{stream3}}">
<source src="{{camera3.getPlaylistRelativeUrl()}}">
</video>
</td>
<td>
<video id="view4" class="video-js vjs-fill" preload="auto" data-setup='{}' controls autoplay muted >
<source src="{{stream4}}">
<source src="{{camera4.getPlaylistRelativeUrl()}}">
</video>
</td>
</tr>

View file

@ -9,15 +9,11 @@
<thead>
<th class="col-md-4">Name</th>
<th class="col-md-3">Type</th>
<th class="col-md-2">Data</th>
<th class="col-md-2">Last Update</th>
</thead>
{{#cameras}}
<tr>
<td><a href="?id={{.getId()}}">{{.getName()}}</a></td>
<td>{{.getDeviceConfig().getClass().getSimpleName()}}</td>
<td>{{.getDeviceData()}}</td>
<td><span class="timestamp">{{.getDeviceData().getTimestamp()}}</span></td>
</tr>
{{/cameras}}
</table>

View file

@ -1,16 +1,22 @@
package se.hal.plugin.nvr;
import se.hal.HalContext;
import se.hal.intf.HalAbstractControllerManager;
import se.hal.intf.HalEventController;
import se.hal.plugin.nvr.intf.HalCameraConfig;
import se.hal.plugin.nvr.intf.HalCameraController;
import se.hal.plugin.nvr.struct.Camera;
import se.hal.struct.Sensor;
import se.hal.util.HalDeviceUtil;
import zutil.db.DBConnection;
import zutil.log.LogUtil;
import zutil.plugin.PluginManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -21,14 +27,66 @@ public class CameraControllerManager extends HalAbstractControllerManager<HalCam
/** List of all registered cameras **/
private List<Camera> registeredCameras = Collections.synchronizedList(new ArrayList<>());
@Override
public void register(Camera device) {
public void initialize(PluginManager pluginManager){
super.initialize(pluginManager);
instance = this;
// Read in existing devices
try {
DBConnection db = HalContext.getDB();
logger.info("Reading in existing cameras.");
for (Camera camera : Camera.getCameras(db)) {
register(camera);
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Unable to read in existing cameras.", e);
}
}
@Override
public void register(Camera camera) {
if (camera.getDeviceConfig() == null) {
logger.warning("Camera config is null: " + camera);
return;
}
if (!getAvailableDeviceConfigs().contains(camera.getDeviceConfig().getClass())) {
logger.warning("Camera data plugin not available: " + camera.getDeviceConfig().getClass());
return;
}
logger.info("Registering new camera(id: " + camera.getId() + "): " + camera.getDeviceConfig().getClass());
Class<? extends HalCameraController> controllerClass = (Class<? extends HalCameraController>) camera.getControllerClass();
HalCameraController controller = getControllerInstance(controllerClass);
if (controller != null)
controller.register(camera.getDeviceConfig());
registeredCameras.add(camera);
//detectedCameras.remove(HalDeviceUtil.findDevice(camera.getDeviceConfig(), detectedCameras)); // Remove if this device was detected
}
@Override
public void deregister(Camera device) {
public void deregister(Camera camera) {
if (camera.getDeviceConfig() == null) {
logger.warning("Camera config is null: " + camera);
return;
}
Class<? extends HalCameraController> controllerClass = (Class<? extends HalCameraController>) camera.getControllerClass();
HalCameraController controller = (HalCameraController) controllerMap.get(controllerClass);
if (controller != null) {
logger.info("Deregistering camera(id: " + camera.getId() + "): " + camera.getDeviceConfig().getClass());
controller.deregister(camera.getDeviceConfig());
registeredCameras.remove(camera);
removeControllerIfEmpty(controller);
} else {
logger.warning("Controller not instantiated: " + camera.getControllerClass());
}
}
@Override
@ -47,13 +105,6 @@ public class CameraControllerManager extends HalAbstractControllerManager<HalCam
}
public void initialize(PluginManager pluginManager){
super.initialize(pluginManager);
instance = this;
}
public static CameraControllerManager getInstance(){
return instance;
}

View file

@ -0,0 +1,18 @@
package se.hal.plugin.nvr;
import se.hal.HalContext;
import se.hal.intf.HalDatabaseUpgrader;
/**
* The DB upgrade class for Hal-NVR plugin
*/
public class NVRDatabaseUpgrader extends HalDatabaseUpgrader {
private static final int REFERENCE_DB_VERSION = 1;
private static final String REFERENCE_DB_PATH = HalContext.RESOURCE_ROOT + "/resource/hal-nvr-reference.db";
public NVRDatabaseUpgrader() {
super(REFERENCE_DB_VERSION, REFERENCE_DB_PATH);
}
}

View file

@ -24,6 +24,7 @@
package se.hal.plugin.nvr.page;
import se.hal.EventControllerManager;
import se.hal.HalContext;
import se.hal.intf.HalWebPage;
import se.hal.page.HalAlertManager;
@ -83,7 +84,7 @@ public class CameraConfigWebPage extends HalWebPage {
camera.setUser(localUser);
camera.getDeviceConfigurator().setValues(request).applyConfiguration();
camera.save(db);
//ControllerManager.getInstance().register(camera);
CameraControllerManager.getInstance().register(camera);
HalAlertManager.getInstance().addAlert(new UserMessage(
MessageLevel.SUCCESS, "Successfully created new camera: " + camera.getName(), MessageTTL.ONE_VIEW));
@ -100,7 +101,7 @@ public class CameraConfigWebPage extends HalWebPage {
camera.save(db);
HalAlertManager.getInstance().addAlert(new UserMessage(
MessageLevel.SUCCESS, "Successfully saved camera: "+camera.getName(), MessageTTL.ONE_VIEW));
MessageLevel.SUCCESS, "Successfully saved camera: " + camera.getName(), MessageTTL.ONE_VIEW));
} else {
logger.warning("Unknown camera id: " + id);
HalAlertManager.getInstance().addAlert(new UserMessage(
@ -112,15 +113,15 @@ public class CameraConfigWebPage extends HalWebPage {
camera = Camera.getCamera(db, id);
if (camera != null) {
logger.info("Removing camera: " + camera.getName());
//ControllerManager.getInstance().deregister(camera);
CameraControllerManager.getInstance().deregister(camera);
camera.delete(db);
HalAlertManager.getInstance().addAlert(new UserMessage(
MessageLevel.SUCCESS, "Successfully removed camera: "+camera.getName(), MessageTTL.ONE_VIEW));
MessageLevel.SUCCESS, "Successfully removed camera: " + camera.getName(), MessageTTL.ONE_VIEW));
} else {
logger.warning("Unknown camera id: " + id);
HalAlertManager.getInstance().addAlert(new UserMessage(
MessageLevel.ERROR, "Unknown camera id: "+id, MessageTTL.ONE_VIEW));
MessageLevel.ERROR, "Unknown camera id: " + id, MessageTTL.ONE_VIEW));
}
break;
}
@ -129,6 +130,8 @@ public class CameraConfigWebPage extends HalWebPage {
// Output
Templator tmpl = new Templator(FileUtil.find(TEMPLATE));
tmpl.set("cameras", Camera.getCameras(db));
tmpl.set("availableCameraConfigClasses", CameraControllerManager.getInstance().getAvailableDeviceConfigs());
tmpl.set("availableCameraObjectConfig", cameraConfigurations);
return tmpl;

View file

@ -72,11 +72,11 @@ public class CameraOverviewWebPage extends HalWebPage {
return tmpl;
}
else {
List<Event> events = Event.getLocalEvents(db);
Collections.sort(events, DeviceNameComparator.getInstance());
List<Camera> cameras = Camera.getCameras(db);
Collections.sort(cameras, DeviceNameComparator.getInstance());
Templator tmpl = new Templator(FileUtil.find(OVERVIEW_TEMPLATE));
tmpl.set("events", events);
tmpl.set("cameras", cameras);
return tmpl;
}
}

View file

@ -26,10 +26,12 @@ package se.hal.plugin.nvr.page;
import se.hal.HalContext;
import se.hal.intf.HalWebPage;
import se.hal.plugin.nvr.struct.Camera;
import zutil.db.DBConnection;
import zutil.io.file.FileUtil;
import zutil.parser.Templator;
import java.util.List;
import java.util.Map;
public class MonitorWebPage extends HalWebPage {
@ -45,10 +47,11 @@ public class MonitorWebPage extends HalWebPage {
DBConnection db = HalContext.getDB();
Templator tmpl = new Templator(FileUtil.find(TEMPLATE));
tmpl.set("stream1", "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8");
tmpl.set("stream2", "http://vjs.zencdn.net/v/oceans.mp4");
tmpl.set("stream3", "http://vjs.zencdn.net/v/oceans.mp4");
tmpl.set("stream4", "http://vjs.zencdn.net/v/oceans.mp4");
List<Camera> cameras = Camera.getCameras(db);
for (int i = 0; i < cameras.size(); i++) {
tmpl.set("camera" + (i+1), cameras.get(i));
}
return tmpl;
}

View file

@ -3,8 +3,11 @@
"name": "Hal-NVR",
"description": "A Network Video Recorder plugin for recording network streams.",
"interfaces": [
{"se.hal.intf.HalDatabaseUpgrader": "se.hal.plugin.nvr.NVRDatabaseUpgrader"},
{"se.hal.intf.HalAbstractControllerManager": "se.hal.plugin.nvr.CameraControllerManager"},
{"se.hal.plugin.nvr.intf.HalCameraConfig": "se.hal.plugin.nvr.rtsp.RTSPCameraConfig"},
{"se.hal.intf.HalWebPage": "se.hal.plugin.nvr.page.CameraConfigWebPage"},
{"se.hal.intf.HalWebPage": "se.hal.plugin.nvr.page.CameraOverviewWebPage"},
{"se.hal.intf.HalWebPage": "se.hal.plugin.nvr.page.MonitorWebPage"}

View file

@ -27,9 +27,11 @@ package se.hal.plugin.nvr.rtsp;
import se.hal.intf.HalDeviceData;
import se.hal.plugin.nvr.intf.HalCameraConfig;
import se.hal.plugin.nvr.intf.HalCameraController;
import zutil.ui.conf.Configurator;
public class RTSPCameraConfig implements HalCameraConfig {
@Configurator.Configurable(value = "RTSP URL", description = "Url to the RTSP stream of the camera. (Should start with rtsp://)")
private String rtspUrl;
@ -54,10 +56,24 @@ public class RTSPCameraConfig implements HalCameraConfig {
return null; // TODO:
}
@Override
public boolean equals(Object obj) {
if (obj instanceof RTSPCameraConfig)
return rtspUrl.equals(((RTSPCameraConfig) obj).rtspUrl);
return false;
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RTSPCameraConfig)) return false;
RTSPCameraConfig that = (RTSPCameraConfig) o;
return rtspUrl != null ? rtspUrl.equals(that.rtspUrl) : that.rtspUrl == null;
}
@Override
public int hashCode() {
return rtspUrl != null ? rtspUrl.hashCode() : 0;
}
@Override
public String toString() {
return "URL: " + rtspUrl;
}
}

View file

@ -1,7 +1,17 @@
package se.hal.plugin.nvr.rtsp;
import se.hal.HalContext;
import zutil.io.file.FileUtil;
import zutil.log.LogUtil;
import zutil.osal.OSALBinaryManager;
import zutil.osal.app.ffmpeg.FFmpeg;
import zutil.osal.app.ffmpeg.FFmpegConstants;
import zutil.osal.app.ffmpeg.FFmpegInput;
import zutil.osal.app.ffmpeg.FFmpegOutput;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -11,11 +21,21 @@ import java.util.logging.Logger;
public class RTSPCameraRecorder implements Runnable {
private static final Logger logger = LogUtil.getLogger();
private static final File FFMPEG_BINARY_PATH = FileUtil.find(HalContext.RESOURCE_ROOT + "/resource/bin/");
private RTSPCameraConfig camera;
private String storagePath;
private Process process;
public RTSPCameraRecorder(RTSPCameraConfig camera) {
public RTSPCameraRecorder(RTSPCameraConfig camera, String storagePath) {
this.camera = camera;
this.storagePath = storagePath;
}
public RTSPCameraConfig getCamera() {
return camera;
}
@ -24,7 +44,70 @@ public class RTSPCameraRecorder implements Runnable {
logger.info("Starting up RTSP Stream recording thread for: " + camera.getRtspUrl());
try {
new File(storagePath).mkdirs();
// ----------------------------------
// Setup commandline
// ----------------------------------
FFmpegInput ffmpegInput = new FFmpegInput(camera.getRtspUrl());
//FFmpegOutput ffmpegOutput = new FFmpegOutput(storagePath + File.separator + "stream.mp4");
FFmpegOutput ffmpegOutput = new FFmpegOutput(new File(storagePath, "stream_%v/stream.m3u8").getPath());
/*ffmpegOutput.addAdditionalArg("-filter_complex \"[0:v]split=3[v1][v2][v3]; [v1]copy[v1out]; [v2]scale=w=1280:h=720[v2out]; [v3]scale=w=640:h=360[v3out]\"",
"-map [v1out] -c:v:0 libx264 -x264-params \"nal-hrd=cbr:force-cfr=1\" -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset veryfast -g 25 -sc_threshold 0",
"-map [v2out] -c:v:1 libx264 -x264-params \"nal-hrd=cbr:force-cfr=1\" -b:v:1 3M -maxrate:v:1 3M -minrate:v:1 3M -bufsize:v:1 3M -preset veryfast -g 25 -sc_threshold 0",
"-map [v3out] -c:v:2 libx264 -x264-params \"nal-hrd=cbr:force-cfr=1\" -b:v:2 1M -maxrate:v:2 1M -minrate:v:2 1M -bufsize:v:2 1M -preset veryfast -g 25 -sc_threshold 0",
"-map a:0 -c:a:0 aac -b:a:0 96k -ac 2",
"-map a:0 -c:a:1 aac -b:a:1 96k -ac 2",
"-map a:0 -c:a:2 aac -b:a:2 48k -ac 2",
"-var_stream_map \"v:0,a:0,name:Source v:1,a:1,name:720p v:2,a:2,name:360p\""
);*/
ffmpegOutput.addAdditionalArg(
"-c:v:0 libx264 -x264-params \"nal-hrd=cbr:force-cfr=1\" -b:v:0 5M -maxrate:v:0 5M -minrate:v:0 5M -bufsize:v:0 10M -preset veryfast -g 25 -sc_threshold 0"
);
ffmpegOutput.addAdditionalArg("-f hls",
"-hls_time 2", // segment length in seconds
//"-hls_playlist_type event", // Do not delete old segments
"-hls_flags independent_segments+delete_segments",
"-hls_segment_type mpegts",
"-hls_segment_filename \"" + new File(storagePath, "stream_%v/data%02d.ts").getPath() + "\"",
"-master_pl_name \"playlist.m3u8\""
);
FFmpeg ffmpeg = new FFmpeg();
ffmpeg.setLogLevel(FFmpegConstants.FFmpegLogLevel.ERROR);
ffmpeg.addInput(ffmpegInput);
ffmpeg.addOutput(ffmpegOutput);
String cmdParams = ffmpeg.buildCommand();
// ----------------------------------
// Execute command
// ----------------------------------
File cmdPath = OSALBinaryManager.getPath(FFMPEG_BINARY_PATH, "ffmpeg");
String cmd = cmdPath.getParent() + File.separator + cmdParams;
logger.finest("Executing ffmpeg: " + cmd);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
if (process != null) process.destroyForcibly();
}
});
process = Runtime.getRuntime().exec(cmd);
BufferedReader output = new BufferedReader(new InputStreamReader(process.getErrorStream()));
while (process.isAlive()) {
String line;
while ((line = output.readLine()) != null) {
logger.finest("[Cam: " + camera.getRtspUrl() + "] " + line);
}
Thread.sleep(1000);
}
output.close();
} catch (Exception e) {
logger.log(Level.SEVERE, "RTSP Stream recording thread has crashed for: " + camera.getRtspUrl(), e);
} finally {
@ -33,6 +116,9 @@ public class RTSPCameraRecorder implements Runnable {
}
public void close() {
camera = null;
if (process != null) {
logger.info("Killing ffmpeg instance.");
process.destroy();
}
}
}

View file

@ -24,6 +24,7 @@
package se.hal.plugin.nvr.rtsp;
import se.hal.HalContext;
import se.hal.intf.HalDeviceConfig;
import se.hal.intf.HalDeviceReportListener;
import se.hal.plugin.nvr.intf.HalCameraController;
@ -31,7 +32,7 @@ import zutil.log.LogUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.*;
import java.util.logging.Logger;
@ -40,7 +41,8 @@ public class RTSPController implements HalCameraController {
private static final String CONFIG_RECORDING_PATH = "nvr.recording_path";
private List<RTSPCameraConfig> cameras = new ArrayList<>();
private static ExecutorService threadPool = Executors.newCachedThreadPool();
private List<RTSPCameraRecorder> recorders = new ArrayList<>();
private List<HalDeviceReportListener> deviceListeners = new CopyOnWriteArrayList<>();
@ -67,18 +69,28 @@ public class RTSPController implements HalCameraController {
@Override
public void register(HalDeviceConfig deviceConfig) {
if (deviceConfig instanceof RTSPCameraConfig)
cameras.add((RTSPCameraConfig) deviceConfig);
if (deviceConfig instanceof RTSPCameraConfig) {
RTSPCameraConfig rtspCam = (RTSPCameraConfig) deviceConfig;
RTSPCameraRecorder recorder = new RTSPCameraRecorder(rtspCam,
HalContext.getStringProperty(CONFIG_RECORDING_PATH, HalContext.RESOURCE_WEB_ROOT + "/recordings/" + deviceConfig.hashCode()));
recorders.add(recorder);
threadPool.execute(recorder);
}
}
@Override
public void deregister(HalDeviceConfig deviceConfig) {
cameras.remove(deviceConfig);
RTSPCameraRecorder recorder = getRecorder((RTSPCameraConfig) deviceConfig);
if (recorder != null) {
recorder.close();
recorders.remove(recorder);
}
}
@Override
public int size() {
return cameras.size();
return recorders.size();
}
@Override
@ -87,4 +99,12 @@ public class RTSPController implements HalCameraController {
deviceListeners.add(listener);
}
private RTSPCameraRecorder getRecorder(RTSPCameraConfig cameraConfig) {
for (RTSPCameraRecorder recorder : recorders) {
if (recorder.getCamera().equals(cameraConfig))
return recorder;
}
return null;
}
}

View file

@ -24,7 +24,6 @@
package se.hal.plugin.nvr.struct;
import se.hal.intf.HalAbstractController;
import se.hal.intf.HalAbstractDevice;
import se.hal.plugin.nvr.intf.HalCameraConfig;
import se.hal.plugin.nvr.intf.HalCameraData;
@ -34,7 +33,7 @@ import zutil.db.bean.DBBean;
import java.sql.SQLException;
import java.util.List;
@DBBean.DBTable(value="camera", superBean=true)
public class Camera extends HalAbstractDevice<Camera, HalCameraConfig, HalCameraData> {
public static List<Camera> getCameras(DBConnection db) throws SQLException{
@ -46,6 +45,10 @@ public class Camera extends HalAbstractDevice<Camera, HalCameraConfig, HalCamera
}
public String getPlaylistRelativeUrl() {
return "recordings/" + getDeviceConfig().hashCode() + "/playlist.m3u8";
}
@Override
protected HalCameraData getLatestDeviceData(DBConnection db) {
return null;