DVR-Scan-Headless 1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dvr_scan/LICENSE +47 -0
- dvr_scan/__init__.py +55 -0
- dvr_scan/__main__.py +80 -0
- dvr_scan/app/__init__.py +10 -0
- dvr_scan/app/__main__.py +218 -0
- dvr_scan/app/about_window.py +137 -0
- dvr_scan/app/application.py +1700 -0
- dvr_scan/app/common.py +31 -0
- dvr_scan/app/region_editor.py +1416 -0
- dvr_scan/app/scan_window.py +258 -0
- dvr_scan/app/widgets.py +213 -0
- dvr_scan/cli.py +511 -0
- dvr_scan/config.py +607 -0
- dvr_scan/controller.py +215 -0
- dvr_scan/detector.py +124 -0
- dvr_scan/docs/404.html +1 -0
- dvr_scan/docs/assets/javascripts/bundle.88dd0f4e.min.js +15 -0
- dvr_scan/docs/assets/stylesheets/main.6f8fc17f.min.css +1 -0
- dvr_scan/docs/assets/stylesheets/palette.06af60db.min.css +1 -0
- dvr_scan/docs/changelog.html +1 -0
- dvr_scan/docs/docs.html +73 -0
- dvr_scan/docs/faq.html +13 -0
- dvr_scan/docs/guide.html +1 -0
- dvr_scan/docs/index.html +5 -0
- dvr_scan/docs/resources.html +30 -0
- dvr_scan/docs/style.min.css +1 -0
- dvr_scan/dvr-scan-logo.png +0 -0
- dvr_scan/dvr-scan.ico +0 -0
- dvr_scan/dvr-scan.png +0 -0
- dvr_scan/opencv_loader.py +37 -0
- dvr_scan/overlays.py +251 -0
- dvr_scan/platform.py +236 -0
- dvr_scan/region.py +65 -0
- dvr_scan/scanner.py +1303 -0
- dvr_scan/shared/__init__.py +287 -0
- dvr_scan/shared/cli.py +291 -0
- dvr_scan/shared/settings.py +72 -0
- dvr_scan/subtractor.py +163 -0
- dvr_scan/video_joiner.py +188 -0
- dvr_scan_headless-1.8.dist-info/METADATA +119 -0
- dvr_scan_headless-1.8.dist-info/RECORD +45 -0
- dvr_scan_headless-1.8.dist-info/WHEEL +5 -0
- dvr_scan_headless-1.8.dist-info/entry_points.txt +2 -0
- dvr_scan_headless-1.8.dist-info/licenses/LICENSE +47 -0
- dvr_scan_headless-1.8.dist-info/top_level.txt +1 -0
dvr_scan/controller.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
"""``dvr_scan.controller`` Module
|
|
12
|
+
|
|
13
|
+
This module manages the DVR-Scan program control flow, starting with `run_dvr_scan()`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import typing as ty
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from scenedetect import FrameTimecode
|
|
22
|
+
|
|
23
|
+
import dvr_scan
|
|
24
|
+
from dvr_scan.cli import get_cli_parser
|
|
25
|
+
from dvr_scan.config import ConfigLoadFailure, ConfigRegistry, RegionValueDeprecated
|
|
26
|
+
from dvr_scan.scanner import DetectorType, OutputMode
|
|
27
|
+
from dvr_scan.shared import ScanSettings, init_logging, init_scanner, logfile_path, setup_logger
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("dvr_scan")
|
|
30
|
+
|
|
31
|
+
LOGFILE_PATH = logfile_path(name_prefix="dvr-scan")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _preprocess_args(args):
|
|
35
|
+
"""Perform some preprocessing and validation of command line arguments, returning a
|
|
36
|
+
boolean indicating if the validation succeeded, and the set of validated arguments."""
|
|
37
|
+
# -i/--input
|
|
38
|
+
# Each entry in args.input is a list of paths or globs we need to expand and combine.
|
|
39
|
+
input_files = []
|
|
40
|
+
for files in args.input:
|
|
41
|
+
for file in files:
|
|
42
|
+
path = Path(file)
|
|
43
|
+
expanded = list(Path.cwd().glob(file)) if not path.is_absolute() else [path]
|
|
44
|
+
if not expanded:
|
|
45
|
+
logger.error("Error: Input file does not exist:\n %s", file)
|
|
46
|
+
return False, None
|
|
47
|
+
input_files += expanded
|
|
48
|
+
args.input = input_files
|
|
49
|
+
# -o/--output
|
|
50
|
+
if hasattr(args, "output") and "." not in args.output:
|
|
51
|
+
args.output += ".avi"
|
|
52
|
+
# -roi/--region-of-interest
|
|
53
|
+
if hasattr(args, "region_of_interest") and args.region_of_interest:
|
|
54
|
+
original_roi = args.region_of_interest
|
|
55
|
+
try:
|
|
56
|
+
args.region_of_interest = RegionValueDeprecated(
|
|
57
|
+
value=" ".join(original_roi), allow_size=True
|
|
58
|
+
).value
|
|
59
|
+
except ValueError:
|
|
60
|
+
logger.error(
|
|
61
|
+
"Error: Invalid value for ROI: %s. ROI must be specified as a rectangle of"
|
|
62
|
+
" the form `x,y,w,h` or the max window size `w,h` (commas/spaces are ignored)."
|
|
63
|
+
" For example: -roi 200,250 50,100",
|
|
64
|
+
" ".join(original_roi),
|
|
65
|
+
)
|
|
66
|
+
return False, None
|
|
67
|
+
return True, args
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_settings() -> ty.Optional[ScanSettings]:
|
|
71
|
+
"""Parse command line options and load config file settings."""
|
|
72
|
+
# We defer printing the debug log until we know where to put it.
|
|
73
|
+
init_log = []
|
|
74
|
+
failed_to_load_config = True
|
|
75
|
+
debug_mode = False
|
|
76
|
+
config = ConfigRegistry()
|
|
77
|
+
config_load_error = None
|
|
78
|
+
# Try to load config from user settings folder.
|
|
79
|
+
try:
|
|
80
|
+
user_config = ConfigRegistry()
|
|
81
|
+
user_config.load()
|
|
82
|
+
config = user_config
|
|
83
|
+
init_logging(args=None, config=config)
|
|
84
|
+
except ConfigLoadFailure as ex:
|
|
85
|
+
config_load_error = ex
|
|
86
|
+
# Parse CLI args, override config if an override was specified on the command line.
|
|
87
|
+
try:
|
|
88
|
+
args = get_cli_parser(config).parse_args()
|
|
89
|
+
debug_mode = args.debug
|
|
90
|
+
init_logging(args=args, config=config)
|
|
91
|
+
init_log += [(logging.INFO, "DVR-Scan %s" % dvr_scan.__version__)]
|
|
92
|
+
if config_load_error and not hasattr(args, "config"):
|
|
93
|
+
raise config_load_error
|
|
94
|
+
if debug_mode:
|
|
95
|
+
init_log += config.consume_init_log()
|
|
96
|
+
if hasattr(args, "config"):
|
|
97
|
+
config_setting = ConfigRegistry()
|
|
98
|
+
config_setting.load(Path(args.config))
|
|
99
|
+
init_logging(args, config_setting)
|
|
100
|
+
config = config_setting
|
|
101
|
+
init_log += config.consume_init_log()
|
|
102
|
+
if config.get("save-log"):
|
|
103
|
+
setup_logger(
|
|
104
|
+
logfile_path=LOGFILE_PATH,
|
|
105
|
+
max_log_files=config.get("max-log-files"),
|
|
106
|
+
name_prefix="dvr-scan",
|
|
107
|
+
)
|
|
108
|
+
failed_to_load_config = False
|
|
109
|
+
except ConfigLoadFailure as ex:
|
|
110
|
+
failed_to_load_config = True
|
|
111
|
+
config_load_error = ex
|
|
112
|
+
init_log += ex.init_log
|
|
113
|
+
if ex.reason is not None:
|
|
114
|
+
init_log += [(logging.ERROR, "Error: %s" % str(ex.reason).replace("\t", " "))]
|
|
115
|
+
finally:
|
|
116
|
+
for log_level, log_str in init_log:
|
|
117
|
+
logger.log(log_level, log_str)
|
|
118
|
+
|
|
119
|
+
if failed_to_load_config:
|
|
120
|
+
logger.critical("Failed to load config file.")
|
|
121
|
+
logger.debug("Error loading config file:", exc_info=config_load_error)
|
|
122
|
+
if debug_mode:
|
|
123
|
+
raise config_load_error
|
|
124
|
+
# Intentionally suppress the exception in release mode since we've already logged the
|
|
125
|
+
# failure reason to the user above. We can now exit with an error code.
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
if config.config_dict:
|
|
129
|
+
logger.debug("Loaded configuration:\n%s", str(config.config_dict))
|
|
130
|
+
|
|
131
|
+
validated, args = _preprocess_args(args)
|
|
132
|
+
if not validated:
|
|
133
|
+
return None
|
|
134
|
+
logger.debug("Program arguments:\n%s", str(args))
|
|
135
|
+
settings = ScanSettings(args=args, config=config)
|
|
136
|
+
|
|
137
|
+
# Validate that the specified motion detector is available on this system.
|
|
138
|
+
try:
|
|
139
|
+
detector_type = settings.get("bg-subtractor")
|
|
140
|
+
bg_subtractor = DetectorType[detector_type.upper()]
|
|
141
|
+
except KeyError:
|
|
142
|
+
logger.error("Error: Unknown background subtraction type: %s", detector_type)
|
|
143
|
+
return None
|
|
144
|
+
if not bg_subtractor.value.is_available():
|
|
145
|
+
if bg_subtractor == DetectorType.MOG2_CUDA:
|
|
146
|
+
logger.error("MOG2_CUDA requires a CUDA-enabled build of the `opencv-python` package!")
|
|
147
|
+
else:
|
|
148
|
+
logger.error(
|
|
149
|
+
f"Method {bg_subtractor.name} is not available, you may need to run: "
|
|
150
|
+
"`pip install opencv-contrib-python`"
|
|
151
|
+
)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
return settings
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def run_dvr_scan(
|
|
158
|
+
settings: ScanSettings,
|
|
159
|
+
) -> ty.List[ty.Tuple[FrameTimecode, FrameTimecode]]:
|
|
160
|
+
"""Run DVR-Scan scanning logic using validated `settings` from `parse_settings()`."""
|
|
161
|
+
|
|
162
|
+
scanner = init_scanner(settings)
|
|
163
|
+
|
|
164
|
+
# Scan video for motion with specified parameters.
|
|
165
|
+
processing_start = time.time()
|
|
166
|
+
result = scanner.scan()
|
|
167
|
+
if result is None:
|
|
168
|
+
logger.debug("Exiting early, scan() returned None.")
|
|
169
|
+
return
|
|
170
|
+
processing_time = time.time() - processing_start
|
|
171
|
+
|
|
172
|
+
# Display results and performance.
|
|
173
|
+
processing_rate = float(result.num_frames) / processing_time
|
|
174
|
+
logger.info(
|
|
175
|
+
"Processed %d frames read in %3.1f secs (avg %3.1f FPS).",
|
|
176
|
+
result.num_frames,
|
|
177
|
+
processing_time,
|
|
178
|
+
processing_rate,
|
|
179
|
+
)
|
|
180
|
+
if not result.event_list:
|
|
181
|
+
logger.info("No motion events detected in input.")
|
|
182
|
+
return
|
|
183
|
+
logger.info("Detected %d motion events in input.", len(result.event_list))
|
|
184
|
+
if result.event_list:
|
|
185
|
+
output_strs = [
|
|
186
|
+
"-------------------------------------------------------------",
|
|
187
|
+
"| Event # | Start Time | Duration | End Time |",
|
|
188
|
+
"-------------------------------------------------------------",
|
|
189
|
+
]
|
|
190
|
+
output_strs += [
|
|
191
|
+
"| Event %4d | %s | %s | %s |"
|
|
192
|
+
% (
|
|
193
|
+
i + 1,
|
|
194
|
+
event.start.get_timecode(precision=1),
|
|
195
|
+
(event.end - event.start).get_timecode(precision=1),
|
|
196
|
+
event.end.get_timecode(precision=1),
|
|
197
|
+
)
|
|
198
|
+
for i, event in enumerate(result.event_list)
|
|
199
|
+
]
|
|
200
|
+
output_strs += ["-------------------------------------------------------------"]
|
|
201
|
+
logger.info("List of motion events:\n%s", "\n".join(output_strs))
|
|
202
|
+
timecode_list = []
|
|
203
|
+
for event in result.event_list:
|
|
204
|
+
timecode_list.append(event.start.get_timecode())
|
|
205
|
+
timecode_list.append(event.end.get_timecode())
|
|
206
|
+
logger.info("Comma-separated timecode values:")
|
|
207
|
+
# Print values regardless of quiet mode or not.
|
|
208
|
+
# TODO: This should not print in quiet mode, it should go through the logger.
|
|
209
|
+
# TODO(#78): Fix this output format to be more usable, in the form:
|
|
210
|
+
# start1-end1[,[+]start2-end2[,[+]start3-end3...]]
|
|
211
|
+
print(",".join(timecode_list))
|
|
212
|
+
|
|
213
|
+
# TODO: Fix private variable access.
|
|
214
|
+
if scanner._output_mode != OutputMode.SCAN_ONLY:
|
|
215
|
+
logger.info("Motion events written to disk.")
|
dvr_scan/detector.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
"""``dvr_scan.detector`` Module
|
|
12
|
+
|
|
13
|
+
Contains the motion detection algorithm (`MotionDetector`) for DVR-Scan. It calculates a score that
|
|
14
|
+
represents the relative amount of movement of consecutive frames in a video.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import typing as ty
|
|
19
|
+
from collections import namedtuple
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import cv2
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from dvr_scan.region import Point
|
|
26
|
+
from dvr_scan.subtractor import Subtractor
|
|
27
|
+
|
|
28
|
+
Rectangle = namedtuple("Rectangle", ["x", "y", "w", "h"])
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("dvr_scan")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ProcessedFrame:
|
|
35
|
+
"""Result of processing a frame through the `MotionDetector`."""
|
|
36
|
+
|
|
37
|
+
subtracted: np.ndarray
|
|
38
|
+
"""Mask representing areas of cropped input frame that have motion."""
|
|
39
|
+
masked: ty.Union[np.ndarray, np.ma.MaskedArray]
|
|
40
|
+
"""The background mask with the specified ROIs applied."""
|
|
41
|
+
score: float
|
|
42
|
+
"""Score representing relative amount of motion in this frame inside the specified ROIs."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MotionDetector:
|
|
46
|
+
"""Detects motion on the input provided by the associated MotionScanner."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
subtractor: Subtractor,
|
|
51
|
+
frame_size: ty.Tuple[int, int],
|
|
52
|
+
downscale: int,
|
|
53
|
+
regions: ty.Optional[ty.Iterable[ty.Iterable[Rectangle]]],
|
|
54
|
+
):
|
|
55
|
+
self._subtractor = subtractor
|
|
56
|
+
self._frame_size = frame_size
|
|
57
|
+
self._downscale = downscale
|
|
58
|
+
self._regions = list(regions) if regions is not None else []
|
|
59
|
+
self._mask: np.ndarray = np.ones((0, 0))
|
|
60
|
+
self._area: ty.Tuple[Point, Point] = (
|
|
61
|
+
Point(0, 0),
|
|
62
|
+
Point(self._frame_size[0] - 1, self._frame_size[1] - 1),
|
|
63
|
+
)
|
|
64
|
+
if self._regions:
|
|
65
|
+
# TODO: See if this can be done using a single color channel or in a bitmap
|
|
66
|
+
mask = np.zeros((frame_size[1], frame_size[0], 3), dtype=np.uint8)
|
|
67
|
+
for shape in self._regions:
|
|
68
|
+
points = np.array([shape], np.int32)
|
|
69
|
+
mask = cv2.fillPoly(mask, points, color=(255, 255, 255), lineType=cv2.LINE_4)
|
|
70
|
+
mask = mask[:, :, 0].astype(bool)
|
|
71
|
+
active_pixels = mask.sum()
|
|
72
|
+
# False marks unmasked elements (those inside the active region), so we invert the mask.
|
|
73
|
+
mask = np.logical_not(mask)
|
|
74
|
+
# Calculate subset of frame to use to speed up calculations.
|
|
75
|
+
min_x, min_y, max_x, max_y = self._frame_size[0], self._frame_size[1], 0, 0
|
|
76
|
+
for shape in self._regions:
|
|
77
|
+
for point in shape:
|
|
78
|
+
min_x, min_y = min(min_x, point.x), min(min_y, point.y)
|
|
79
|
+
max_x, max_y = max(max_x, point.x), max(max_y, point.y)
|
|
80
|
+
self._area = (Point(min_x, min_y), Point(max_x, max_y))
|
|
81
|
+
coverage = 100.0 * (active_pixels / float(frame_size[0] * frame_size[1]))
|
|
82
|
+
mask = mask[self._area[0].y : self._area[1].y, self._area[0].x : self._area[1].x]
|
|
83
|
+
logger.debug(
|
|
84
|
+
"Region Mask: area = ("
|
|
85
|
+
f"{self._area[0].x},{self._area[0].y}),({self._area[1].x},{self._area[1].y}"
|
|
86
|
+
f"), coverage = {coverage:.2f}%"
|
|
87
|
+
)
|
|
88
|
+
if self._downscale > 1:
|
|
89
|
+
mask = mask[:: self._downscale, :: self._downscale]
|
|
90
|
+
logger.debug(f"Mask Downscaled: size = {mask.shape[0]}, {mask.shape[1]}")
|
|
91
|
+
self._mask = mask
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def area(self) -> Rectangle:
|
|
95
|
+
"""Area the region of interest covers in the original frame."""
|
|
96
|
+
return self._area
|
|
97
|
+
|
|
98
|
+
def _preprocess(self, frame: np.ndarray) -> np.ndarray:
|
|
99
|
+
cropped = None
|
|
100
|
+
if not self._regions:
|
|
101
|
+
cropped = frame
|
|
102
|
+
else:
|
|
103
|
+
cropped = frame[
|
|
104
|
+
self._area[0].y : self._area[1].y,
|
|
105
|
+
self._area[0].x : self._area[1].x,
|
|
106
|
+
]
|
|
107
|
+
if self._downscale > 1:
|
|
108
|
+
return cropped[:: self._downscale, :: self._downscale, :]
|
|
109
|
+
|
|
110
|
+
return cropped
|
|
111
|
+
|
|
112
|
+
def update(self, frame: np.ndarray) -> ProcessedFrame:
|
|
113
|
+
frame = self._preprocess(frame)
|
|
114
|
+
subtracted = self._subtractor.apply(frame)
|
|
115
|
+
if not self._regions:
|
|
116
|
+
return ProcessedFrame(
|
|
117
|
+
subtracted=subtracted, masked=subtracted, score=np.average(subtracted)
|
|
118
|
+
)
|
|
119
|
+
motion_mask = np.ma.array(subtracted, mask=self._mask)
|
|
120
|
+
return ProcessedFrame(
|
|
121
|
+
subtracted=subtracted,
|
|
122
|
+
masked=motion_mask,
|
|
123
|
+
score=np.ma.sum(motion_mask) / float(np.ma.count(motion_mask)),
|
|
124
|
+
)
|
dvr_scan/docs/404.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html lang=en class=no-js> <head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=author content="Brandon Castellano"><link rel=icon href=/https://www.dvr-scan.com/assets/dvr-scan.ico><meta name=generator content="mkdocs-1.6.1, mkdocs-material-9.5.49"><title>DVR-Scan</title><link rel=stylesheet href=/assets/stylesheets/main.6f8fc17f.min.css><link rel=stylesheet href=/assets/stylesheets/palette.06af60db.min.css><style>:root{--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 7 9 19l-5.5-5.5 1.41-1.41L9 16.17 19.59 5.59z"/></svg>');}</style><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Rubik:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback"><style>:root{--md-text-font:"Rubik";--md-code-font:"JetBrains Mono"}</style><link rel=stylesheet href=/style.min.css><script>__md_scope=new URL("/",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script></head> <body dir=ltr data-md-color-scheme=default data-md-color-primary=custom data-md-color-accent=indigo> <input class=md-toggle data-md-toggle=drawer type=checkbox id=__drawer autocomplete=off> <input class=md-toggle data-md-toggle=search type=checkbox id=__search autocomplete=off> <label class=md-overlay for=__drawer></label> <div data-md-component=skip> </div> <div data-md-component=announce> </div> <header class="md-header md-header--shadow md-header--lifted" data-md-component=header> <nav class="md-header__inner md-grid" aria-label=Header> <a href=/. title=DVR-Scan class="md-header__button md-logo" aria-label=DVR-Scan data-md-component=logo> <img src=/https://www.dvr-scan.com/assets/dvr-scan.png alt=logo> </a> <label class="md-header__button md-icon" for=__drawer> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg> </label> <div class=md-header__title data-md-component=header-title> <div class=md-header__ellipsis> <div class=md-header__topic> <span class=md-ellipsis> DVR-Scan </span> </div> <div class=md-header__topic data-md-component=header-topic> <span class=md-ellipsis> </span> </div> </div> </div> <form class=md-header__option data-md-component=palette> <input class=md-option data-md-color-media data-md-color-scheme=default data-md-color-primary=custom data-md-color-accent=indigo aria-label="Switch to dark mode" type=radio name=__palette id=__palette_0> <label class="md-header__button md-icon" title="Switch to dark mode" for=__palette_0 hidden> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 24 24"><path d="M12 8a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 10a6 6 0 0 1-6-6 6 6 0 0 1 6-6 6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12z"/></svg> </label> </form> <script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script> <div class=md-header__source> <a href=https://github.com/Breakthrough/DVR-Scan title="Go to repository" class=md-source data-md-component=source> <div class="md-source__icon md-icon"> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 496 512"><!-- Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg> </div> <div class=md-source__repository> DVR-Scan on Github </div> </a> </div> </nav> <nav class=md-tabs aria-label=Tabs data-md-component=tabs> <div class=md-grid> <ul class=md-tabs__list> <li class=md-tabs__item> <a href=/guide.html class=md-tabs__link> User Guide </a> </li> <li class=md-tabs__item> <a href=/docs.html class=md-tabs__link> Documentation </a> </li> <li class=md-tabs__item> <a href=/faq.html class=md-tabs__link> FAQ </a> </li> <li class=md-tabs__item> <a href=/changelog.html class=md-tabs__link> Changelog </a> </li> <li class=md-tabs__item> <a href=/resources.html class=md-tabs__link> Resources </a> </li> </ul> </div> </nav> </header> <div class=md-container data-md-component=container> <main class=md-main data-md-component=main> <div class="md-main__inner md-grid"> <div class="md-sidebar md-sidebar--primary" data-md-component=sidebar data-md-type=navigation> <div class=md-sidebar__scrollwrap> <div class=md-sidebar__inner> <nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label=Navigation data-md-level=0> <label class=md-nav__title for=__drawer> <a href=/. title=DVR-Scan class="md-nav__button md-logo" aria-label=DVR-Scan data-md-component=logo> <img src=/https://www.dvr-scan.com/assets/dvr-scan.png alt=logo> </a> DVR-Scan </label> <div class=md-nav__source> <a href=https://github.com/Breakthrough/DVR-Scan title="Go to repository" class=md-source data-md-component=source> <div class="md-source__icon md-icon"> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 496 512"><!-- Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg> </div> <div class=md-source__repository> DVR-Scan on Github </div> </a> </div> <ul class=md-nav__list data-md-scrollfix> <li class=md-nav__item> <a href=/guide.html class=md-nav__link> <span class=md-ellipsis> User Guide </span> </a> </li> <li class=md-nav__item> <a href=/docs.html class=md-nav__link> <span class=md-ellipsis> Documentation </span> </a> </li> <li class=md-nav__item> <a href=/faq.html class=md-nav__link> <span class=md-ellipsis> FAQ </span> </a> </li> <li class=md-nav__item> <a href=/changelog.html class=md-nav__link> <span class=md-ellipsis> Changelog </span> </a> </li> <li class=md-nav__item> <a href=/resources.html class=md-nav__link> <span class=md-ellipsis> Resources </span> </a> </li> </ul> </nav> </div> </div> </div> <div class=md-content data-md-component=content> <article class="md-content__inner md-typeset"> <h1>404 - Not found</h1> </article> </div> <script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script> </div> </main> <footer class=md-footer> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class=md-copyright> <div class=md-copyright__highlight> Copyright © 2016 Brandon Castellano. All rights reserved.<br><a href=https://github.com/Breakthrough/DVR-Scan/blob/main/LICENSE alt="link to LICENSE on Github">Licensed under BSD 2-Clause license. </div> </div> </div> </div> </footer> </div> <div class=md-dialog data-md-component=dialog> <div class="md-dialog__inner md-typeset"></div> </div> <script id=__config type=application/json>{"base": "/", "features": ["navigation.tabs", "navigation.sections", "navigation.tabs.sticky", "toc.follow", "toc.integrate", "content.code.copy"], "search": "/assets/javascripts/workers/search.6ce7567c.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script> <script src=/assets/javascripts/bundle.88dd0f4e.min.js></script> </body> </html>
|