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.
Files changed (45) hide show
  1. dvr_scan/LICENSE +47 -0
  2. dvr_scan/__init__.py +55 -0
  3. dvr_scan/__main__.py +80 -0
  4. dvr_scan/app/__init__.py +10 -0
  5. dvr_scan/app/__main__.py +218 -0
  6. dvr_scan/app/about_window.py +137 -0
  7. dvr_scan/app/application.py +1700 -0
  8. dvr_scan/app/common.py +31 -0
  9. dvr_scan/app/region_editor.py +1416 -0
  10. dvr_scan/app/scan_window.py +258 -0
  11. dvr_scan/app/widgets.py +213 -0
  12. dvr_scan/cli.py +511 -0
  13. dvr_scan/config.py +607 -0
  14. dvr_scan/controller.py +215 -0
  15. dvr_scan/detector.py +124 -0
  16. dvr_scan/docs/404.html +1 -0
  17. dvr_scan/docs/assets/javascripts/bundle.88dd0f4e.min.js +15 -0
  18. dvr_scan/docs/assets/stylesheets/main.6f8fc17f.min.css +1 -0
  19. dvr_scan/docs/assets/stylesheets/palette.06af60db.min.css +1 -0
  20. dvr_scan/docs/changelog.html +1 -0
  21. dvr_scan/docs/docs.html +73 -0
  22. dvr_scan/docs/faq.html +13 -0
  23. dvr_scan/docs/guide.html +1 -0
  24. dvr_scan/docs/index.html +5 -0
  25. dvr_scan/docs/resources.html +30 -0
  26. dvr_scan/docs/style.min.css +1 -0
  27. dvr_scan/dvr-scan-logo.png +0 -0
  28. dvr_scan/dvr-scan.ico +0 -0
  29. dvr_scan/dvr-scan.png +0 -0
  30. dvr_scan/opencv_loader.py +37 -0
  31. dvr_scan/overlays.py +251 -0
  32. dvr_scan/platform.py +236 -0
  33. dvr_scan/region.py +65 -0
  34. dvr_scan/scanner.py +1303 -0
  35. dvr_scan/shared/__init__.py +287 -0
  36. dvr_scan/shared/cli.py +291 -0
  37. dvr_scan/shared/settings.py +72 -0
  38. dvr_scan/subtractor.py +163 -0
  39. dvr_scan/video_joiner.py +188 -0
  40. dvr_scan_headless-1.8.dist-info/METADATA +119 -0
  41. dvr_scan_headless-1.8.dist-info/RECORD +45 -0
  42. dvr_scan_headless-1.8.dist-info/WHEEL +5 -0
  43. dvr_scan_headless-1.8.dist-info/entry_points.txt +2 -0
  44. dvr_scan_headless-1.8.dist-info/licenses/LICENSE +47 -0
  45. 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 &copy; 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>