forked from Github/frigate
Compare commits
105 Commits
v0.10.0
...
v0.10.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adbc54bcfe | ||
|
|
4deb365758 | ||
|
|
1171770447 | ||
|
|
54d1a223a5 | ||
|
|
62c1a61ed0 | ||
|
|
9ecc7920dd | ||
|
|
45b56bdce5 | ||
|
|
54b88fb4a9 | ||
|
|
a3fa3cb716 | ||
|
|
64f80a4732 | ||
|
|
0b02f20b26 | ||
|
|
8670a3d808 | ||
|
|
3617a625d3 | ||
|
|
ad4929c621 | ||
|
|
9a0d276761 | ||
|
|
24f9937009 | ||
|
|
4e23967442 | ||
|
|
acc1022998 | ||
|
|
02c91d4c51 | ||
|
|
5e156f8151 | ||
|
|
47e0e1d221 | ||
|
|
f57501d033 | ||
|
|
1a3f21e5c1 | ||
|
|
5a2076fcab | ||
|
|
2d5ec25dca | ||
|
|
499f75e165 | ||
|
|
3600ebca39 | ||
|
|
50b5d40c10 | ||
|
|
21f1a98da4 | ||
|
|
21cc29be6f | ||
|
|
794a9ff162 | ||
|
|
7b4cb95825 | ||
|
|
b1e84ca7fe | ||
|
|
e6ec5cb097 | ||
|
|
23c70acd51 | ||
|
|
091648187f | ||
|
|
2b7d38f947 | ||
|
|
f801930588 | ||
|
|
955c2779d9 | ||
|
|
037f8667a6 | ||
|
|
307068a61f | ||
|
|
077d900b44 | ||
|
|
92f9195075 | ||
|
|
82c60093d1 | ||
|
|
944b9181e0 | ||
|
|
326b368e82 | ||
|
|
040d8c9778 | ||
|
|
273f803c7c | ||
|
|
bd8e23833c | ||
|
|
9edf38347c | ||
|
|
1569ce7cf6 | ||
|
|
db1255aa7f | ||
|
|
609b436ed8 | ||
|
|
95bdf9fe34 | ||
|
|
251d29aa38 | ||
|
|
156e1a4dc2 | ||
|
|
a5c13e7455 | ||
|
|
fcb4aaef0d | ||
|
|
589432bc89 | ||
|
|
b19a02888a | ||
|
|
18fd50dfce | ||
|
|
df0246aed8 | ||
|
|
cbb2882123 | ||
|
|
9f18629df3 | ||
|
|
63f8034e46 | ||
|
|
f3efc0667f | ||
|
|
af001321a8 | ||
|
|
92e08b92f5 | ||
|
|
26241b0877 | ||
|
|
c1155af169 | ||
|
|
77c1f1bb1b | ||
|
|
ae3c01fe2d | ||
|
|
7a2a85d253 | ||
|
|
77c66d4e49 | ||
|
|
494e5ac4ec | ||
|
|
63b7465452 | ||
|
|
e6d2df5661 | ||
|
|
a3301e0347 | ||
|
|
3d556cc2cb | ||
|
|
585efe1a0f | ||
|
|
c7d47439dd | ||
|
|
19a6978228 | ||
|
|
1ebb8a54bf | ||
|
|
ae968044d6 | ||
|
|
b912851e49 | ||
|
|
14c74e4361 | ||
|
|
51fb532e1a | ||
|
|
3541f966e3 | ||
|
|
c7faef8faa | ||
|
|
cdd3000315 | ||
|
|
1c1c28d0e5 | ||
|
|
4422e86907 | ||
|
|
8f43a2d109 | ||
|
|
bd7755fdd3 | ||
|
|
d554175631 | ||
|
|
ff667b019a | ||
|
|
57dcb29f8b | ||
|
|
9dc6c423b7 | ||
|
|
58117e2a3e | ||
|
|
5bec438f9c | ||
|
|
24cc63d6d3 | ||
|
|
d17bd74c9a | ||
|
|
8f101ccca8 | ||
|
|
b63c56d810 | ||
|
|
61c62d4685 |
@@ -43,11 +43,6 @@ If you are storing your database on a network share (SMB, NFS, etc), you may get
|
||||
|
||||
This may need to be in a custom location if network storage is used for the media folder.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /path/to/frigate.db
|
||||
```
|
||||
|
||||
### `model`
|
||||
|
||||
If using a custom model, the width and height will need to be specified.
|
||||
|
||||
@@ -19,34 +19,6 @@ output_args:
|
||||
rtmp: -c:v libx264 -an -f flv
|
||||
```
|
||||
|
||||
### JPEG Stream Cameras
|
||||
|
||||
Cameras using a live changing jpeg image will need input parameters as below
|
||||
|
||||
```yaml
|
||||
input_args:
|
||||
- -r
|
||||
- 5 # << enter FPS here
|
||||
- -stream_loop
|
||||
- -1
|
||||
- -f
|
||||
- image2
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- 1
|
||||
```
|
||||
|
||||
Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras)
|
||||
|
||||
### RTMP Cameras
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
@@ -159,23 +159,11 @@ detect:
|
||||
enabled: True
|
||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
|
||||
max_disappeared: 25
|
||||
# Optional: Configuration for stationary object tracking
|
||||
stationary:
|
||||
# Optional: Frequency for running detection on stationary objects (default: shown below)
|
||||
# When set to 0, object detection will never be run on stationary objects. If set to 10, it will be run on every 10th frame.
|
||||
interval: 0
|
||||
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
|
||||
threshold: 50
|
||||
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
|
||||
# This can help with false positives for objects that should only be stationary for a limited amount of time.
|
||||
# It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave
|
||||
# car at the default.
|
||||
max_frames:
|
||||
# Optional: Default for all object types (default: not set, track forever)
|
||||
default: 3000
|
||||
# Optional: Object specific values
|
||||
objects:
|
||||
person: 1000
|
||||
# Optional: Frequency for running detection on stationary objects (default: shown below)
|
||||
# When set to 0, object detection will never be run on stationary objects. If set to 10, it will be run on every 10th frame.
|
||||
stationary_interval: 0
|
||||
# Optional: Number of frames without a position change for an object to be considered stationary (default: shown below)
|
||||
stationary_threshold: 10
|
||||
|
||||
# Optional: Object configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
@@ -238,11 +226,6 @@ motion:
|
||||
# NOTE: Can be overridden at the camera level
|
||||
record:
|
||||
# Optional: Enable recording (default: shown below)
|
||||
# WARNING: Frigate does not currently support limiting recordings based
|
||||
# on available disk space automatically. If using recordings,
|
||||
# you must specify retention settings for a number of days that
|
||||
# will fit within the available disk space of your drive or Frigate
|
||||
# will crash.
|
||||
enabled: False
|
||||
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
|
||||
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
|
||||
|
||||
@@ -5,11 +5,7 @@ title: Objects
|
||||
|
||||
import labels from "../../../labelmap.txt";
|
||||
|
||||
Frigate includes the object models listed below from the Google Coral test data.
|
||||
|
||||
Please note:
|
||||
- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
- `person` is the only tracked object by default. See the [full configuration reference](https://docs.frigate.video/configuration/index#full-configuration-reference) for an example of expanding the list of tracked objects.
|
||||
By default, Frigate includes the following object models from the Google Coral test data. Note that `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
|
||||
<ul>
|
||||
{labels.split("\n").map((label) => (
|
||||
|
||||
@@ -5,4 +5,4 @@ title: RTMP
|
||||
|
||||
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||
|
||||
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](../faqs#audio-in-recordings).
|
||||
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization.
|
||||
|
||||
@@ -11,24 +11,9 @@ This error message is due to a shm-size that is too small. Try updating your shm
|
||||
|
||||
A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
|
||||
|
||||
### How can I get sound or audio in my recordings? {#audio-in-recordings}
|
||||
### How can I get sound or audio in my recordings?
|
||||
|
||||
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/configuration/index/#full-configuration-reference).
|
||||
|
||||
:::tip
|
||||
|
||||
When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example:
|
||||
|
||||
```diff title="frigate.yml"
|
||||
ffmpeg:
|
||||
output_args:
|
||||
- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac
|
||||
```
|
||||
|
||||
This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`.
|
||||
|
||||
:::
|
||||
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
|
||||
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
|
||||
|
||||
@@ -167,17 +167,13 @@ cameras:
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- path: rtsp://10.0.10.10:554/high_res_stream # <----- Add high res stream
|
||||
roles:
|
||||
- record
|
||||
- record # <----- Add role
|
||||
detect: ...
|
||||
record: # <----- Enable recording
|
||||
enabled: True
|
||||
motion: ...
|
||||
```
|
||||
|
||||
If you don't have separate streams for detect and record, you would just add the record role to the list on the first input.
|
||||
|
||||
By default, Frigate will retain video of all events for 10 days. The full set of options for recording can be found [here](/configuration/index#full-configuration-reference).
|
||||
|
||||
### Step 8: Enable snapshots (optional)
|
||||
|
||||
@@ -21,12 +21,6 @@ Windows is not officially supported, but some users have had success getting it
|
||||
|
||||
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
|
||||
|
||||
:::caution
|
||||
|
||||
Note that Frigate does not currently support limiting recordings based on available disk space automatically. If using recordings, you must specify retention settings for a number of days that will fit within the available disk space of your drive or Frigate will crash.
|
||||
|
||||
:::
|
||||
|
||||
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
|
||||
@@ -124,7 +118,6 @@ services:
|
||||
shm_size: "64mb" # update for your cameras based on calculation above
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
|
||||
- /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
|
||||
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
@@ -56,7 +56,6 @@ Message published for each changed event. The first message is published when th
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false,
|
||||
"stationary": false, // whether or not the object is considered stationary
|
||||
"motionless_count": 0, // number of frames the object has been motionless
|
||||
"position_changes": 2 // number of times the object has moved from a stationary position
|
||||
},
|
||||
@@ -79,7 +78,6 @@ Message published for each changed event. The first message is published when th
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false,
|
||||
"stationary": false, // whether or not the object is considered stationary
|
||||
"motionless_count": 0, // number of frames the object has been motionless
|
||||
"position_changes": 2 // number of times the object has changed position
|
||||
}
|
||||
|
||||
@@ -162,29 +162,6 @@ class RuntimeMotionConfig(MotionConfig):
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
class StationaryMaxFramesConfig(FrigateBaseModel):
|
||||
default: Optional[int] = Field(title="Default max frames.", ge=1)
|
||||
objects: Dict[str, int] = Field(
|
||||
default_factory=dict, title="Object specific max frames."
|
||||
)
|
||||
|
||||
|
||||
class StationaryConfig(FrigateBaseModel):
|
||||
interval: Optional[int] = Field(
|
||||
default=0,
|
||||
title="Frame interval for checking stationary objects.",
|
||||
ge=0,
|
||||
)
|
||||
threshold: Optional[int] = Field(
|
||||
title="Number of frames without a position change for an object to be considered stationary",
|
||||
ge=1,
|
||||
)
|
||||
max_frames: StationaryMaxFramesConfig = Field(
|
||||
default_factory=StationaryMaxFramesConfig,
|
||||
title="Max frames for stationary objects.",
|
||||
)
|
||||
|
||||
|
||||
class DetectConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Height of the stream for the detect role.")
|
||||
width: int = Field(default=1280, title="Width of the stream for the detect role.")
|
||||
@@ -195,9 +172,15 @@ class DetectConfig(FrigateBaseModel):
|
||||
max_disappeared: Optional[int] = Field(
|
||||
title="Maximum number of frames the object can dissapear before detection ends."
|
||||
)
|
||||
stationary: StationaryConfig = Field(
|
||||
default_factory=StationaryConfig,
|
||||
title="Stationary objects config.",
|
||||
stationary_interval: Optional[int] = Field(
|
||||
default=0,
|
||||
title="Frame interval for checking stationary objects.",
|
||||
ge=0,
|
||||
)
|
||||
stationary_threshold: Optional[int] = Field(
|
||||
default=10,
|
||||
title="Number of frames without a position change for an object to be considered stationary",
|
||||
ge=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -788,11 +771,6 @@ class FrigateConfig(FrigateBaseModel):
|
||||
if camera_config.detect.max_disappeared is None:
|
||||
camera_config.detect.max_disappeared = max_disappeared
|
||||
|
||||
# Default stationary_threshold configuration
|
||||
stationary_threshold = camera_config.detect.fps * 10
|
||||
if camera_config.detect.stationary.threshold is None:
|
||||
camera_config.detect.stationary.threshold = stationary_threshold
|
||||
|
||||
# FFMPEG input substitution
|
||||
for input in camera_config.ffmpeg.inputs:
|
||||
input.path = input.path.format(**FRIGATE_ENV_VARS)
|
||||
|
||||
@@ -161,7 +161,7 @@ class TrackedObject:
|
||||
# if the motionless_count reaches the stationary threshold
|
||||
if (
|
||||
self.obj_data["motionless_count"]
|
||||
== self.camera_config.detect.stationary.threshold
|
||||
== self.camera_config.detect.stationary_threshold
|
||||
):
|
||||
significant_change = True
|
||||
|
||||
@@ -193,8 +193,6 @@ class TrackedObject:
|
||||
"box": self.obj_data["box"],
|
||||
"area": self.obj_data["area"],
|
||||
"region": self.obj_data["region"],
|
||||
"stationary": self.obj_data["motionless_count"]
|
||||
> self.camera_config.detect.stationary.threshold,
|
||||
"motionless_count": self.obj_data["motionless_count"],
|
||||
"position_changes": self.obj_data["position_changes"],
|
||||
"current_zones": self.current_zones.copy(),
|
||||
|
||||
@@ -78,9 +78,9 @@ class ObjectTracker:
|
||||
}
|
||||
return False
|
||||
|
||||
# if there are less than 10 entries for the position, add the bounding box
|
||||
# if there are less than stationary_threshold entries for the position, add the bounding box
|
||||
# and recompute the position box
|
||||
if len(position["xmins"]) < 10:
|
||||
if len(position["xmins"]) < self.detect_config.stationary_threshold:
|
||||
position["xmins"].append(xmin)
|
||||
position["ymins"].append(ymin)
|
||||
position["xmaxs"].append(xmax)
|
||||
@@ -93,40 +93,18 @@ class ObjectTracker:
|
||||
|
||||
return True
|
||||
|
||||
def is_expired(self, id):
|
||||
obj = self.tracked_objects[id]
|
||||
# get the max frames for this label type or the default
|
||||
max_frames = self.detect_config.stationary.max_frames.objects.get(
|
||||
obj["label"], self.detect_config.stationary.max_frames.default
|
||||
)
|
||||
|
||||
# if there is no max_frames for this label type, continue
|
||||
if max_frames is None:
|
||||
return False
|
||||
|
||||
# if the object has exceeded the max_frames setting, deregister
|
||||
if (
|
||||
obj["motionless_count"] - self.detect_config.stationary.threshold
|
||||
> max_frames
|
||||
):
|
||||
print(f"expired: {obj['motionless_count']}")
|
||||
return True
|
||||
|
||||
def update(self, id, new_obj):
|
||||
self.disappeared[id] = 0
|
||||
# update the motionless count if the object has not moved to a new position
|
||||
if self.update_position(id, new_obj["box"]):
|
||||
self.tracked_objects[id]["motionless_count"] += 1
|
||||
if self.is_expired(id):
|
||||
self.deregister(id)
|
||||
return
|
||||
else:
|
||||
# register the first position change and then only increment if
|
||||
# the object was previously stationary
|
||||
if (
|
||||
self.tracked_objects[id]["position_changes"] == 0
|
||||
or self.tracked_objects[id]["motionless_count"]
|
||||
>= self.detect_config.stationary.threshold
|
||||
>= self.detect_config.stationary_threshold
|
||||
):
|
||||
self.tracked_objects[id]["position_changes"] += 1
|
||||
self.tracked_objects[id]["motionless_count"] = 0
|
||||
@@ -134,11 +112,9 @@ class ObjectTracker:
|
||||
self.tracked_objects[id].update(new_obj)
|
||||
|
||||
def update_frame_times(self, frame_time):
|
||||
for id in list(self.tracked_objects.keys()):
|
||||
for id in self.tracked_objects.keys():
|
||||
self.tracked_objects[id]["frame_time"] = frame_time
|
||||
self.tracked_objects[id]["motionless_count"] += 1
|
||||
if self.is_expired(id):
|
||||
self.deregister(id)
|
||||
|
||||
def match_and_update(self, frame_time, new_objects):
|
||||
# group by name
|
||||
|
||||
@@ -184,7 +184,10 @@ class BirdsEyeFrameManager:
|
||||
if self.mode == BirdseyeModeEnum.continuous:
|
||||
return True
|
||||
|
||||
if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0:
|
||||
if (
|
||||
self.mode == BirdseyeModeEnum.motion
|
||||
and object_box_count + motion_box_count > 0
|
||||
):
|
||||
return True
|
||||
|
||||
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0:
|
||||
@@ -415,7 +418,7 @@ def output_frames(config: FrigateConfig, video_output_queue):
|
||||
):
|
||||
if birdseye_manager.update(
|
||||
camera,
|
||||
len([o for o in current_tracked_objects if not o["stationary"]]),
|
||||
len(current_tracked_objects),
|
||||
len(motion_boxes),
|
||||
frame_time,
|
||||
frame,
|
||||
|
||||
@@ -507,12 +507,12 @@ def process_frames(
|
||||
stationary_object_ids = [
|
||||
obj["id"]
|
||||
for obj in object_tracker.tracked_objects.values()
|
||||
# if there hasn't been motion for 10 frames
|
||||
if obj["motionless_count"] >= 10
|
||||
# if there hasn't been motion for N frames
|
||||
if obj["motionless_count"] >= detect_config.stationary_threshold
|
||||
# and it isn't due for a periodic check
|
||||
and (
|
||||
detect_config.stationary.interval == 0
|
||||
or obj["motionless_count"] % detect_config.stationary.interval != 0
|
||||
detect_config.stationary_interval == 0
|
||||
or obj["motionless_count"] % detect_config.stationary_interval != 0
|
||||
)
|
||||
# and it hasn't disappeared
|
||||
and object_tracker.disappeared[obj["id"]] == 0
|
||||
|
||||
@@ -29,8 +29,12 @@ function Camera({ name, conf }) {
|
||||
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
|
||||
const href = `/cameras/${name}`;
|
||||
const buttons = useMemo(() => {
|
||||
return [{ name: 'Events', href: `/events?camera=${name}` }, { name: 'Recordings', href: `/recording/${name}` }];
|
||||
}, [name]);
|
||||
const result = [{ name: 'Events', href: `/events?camera=${name}` }];
|
||||
if (conf.record.enabled) {
|
||||
result.push({ name: 'Recordings', href: `/recording/${name}` });
|
||||
}
|
||||
return result;
|
||||
}, [name, conf.record.enabled]);
|
||||
const icons = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('Cameras Route', () => {
|
||||
|
||||
expect(screen.queryByLabelText('Loading…')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByText('Recordings')).toHaveLength(2);
|
||||
expect(screen.queryAllByText('Recordings')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('buttons toggle detect, clips, and snapshots', async () => {
|
||||
|
||||
Reference in New Issue
Block a user