mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-01 11:20:32 +03:00
Compare commits
4
Commits
60c062e6c8
...
b623c4bb70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b623c4bb70 | ||
|
|
c007661a71 | ||
|
|
9b02c7318d | ||
|
|
2d7aca34b6 |
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
+16
-8
@@ -254,7 +254,14 @@ rateLimiter = RateLimiter()
|
||||
|
||||
|
||||
def get_remote_addr(request: Request):
|
||||
route = list(reversed(request.headers.get("x-forwarded-for").split(",")))
|
||||
# fall back to the direct TCP peer when no proxy chain is present
|
||||
direct_addr = request.client.host if request.client else None
|
||||
|
||||
forwarded_for = request.headers.get("x-forwarded-for")
|
||||
if not forwarded_for:
|
||||
return direct_addr or "127.0.0.1"
|
||||
|
||||
route = list(reversed(forwarded_for.split(",")))
|
||||
logger.debug(f"IP Route: {[r for r in route]}")
|
||||
trusted_proxies = []
|
||||
for proxy in request.app.frigate_config.auth.trusted_proxies:
|
||||
@@ -291,13 +298,8 @@ def get_remote_addr(request: Request):
|
||||
logger.debug(f"First untrusted IP: {str(ip)}")
|
||||
return str(ip)
|
||||
|
||||
# if there wasn't anything in the route, just return the default
|
||||
remote_addr = None
|
||||
|
||||
if hasattr(request, "remote_addr"):
|
||||
remote_addr = request.remote_addr
|
||||
|
||||
return remote_addr or "127.0.0.1"
|
||||
# every hop in the route was trusted, so fall back to the direct peer
|
||||
return direct_addr or "127.0.0.1"
|
||||
|
||||
|
||||
def _cleanup_first_load_seen() -> None:
|
||||
@@ -416,6 +418,12 @@ def create_encoded_jwt(user, role, expiration, secret):
|
||||
|
||||
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
|
||||
# TODO: ideally this would set secure as well, but that requires TLS
|
||||
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
||||
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
||||
# iframes, breaking embedded views such as the Home Assistant Frigate card.
|
||||
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
|
||||
# cross-origin pages cannot set without a CORS preflight that Frigate never
|
||||
# grants (see check_csrf in api/fastapi_app.py).
|
||||
response.set_cookie(
|
||||
key=cookie_name,
|
||||
value=encoded_jwt,
|
||||
|
||||
+9
-11
@@ -269,12 +269,12 @@ async def no_recordings(
|
||||
cameras = params.cameras
|
||||
if cameras != "all":
|
||||
requested = set(unquote(cameras).split(","))
|
||||
filtered = requested.intersection(allowed_cameras)
|
||||
if not filtered:
|
||||
return JSONResponse(content=[])
|
||||
cameras = ",".join(filtered)
|
||||
camera_list = list(requested.intersection(allowed_cameras))
|
||||
else:
|
||||
cameras = allowed_cameras
|
||||
camera_list = list(allowed_cameras)
|
||||
|
||||
if not camera_list:
|
||||
return JSONResponse(content=[])
|
||||
|
||||
before = params.before or datetime.datetime.now().timestamp()
|
||||
after = (
|
||||
@@ -283,12 +283,10 @@ async def no_recordings(
|
||||
)
|
||||
scale = params.scale
|
||||
|
||||
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
|
||||
if cameras != "all":
|
||||
camera_list = cameras.split(",")
|
||||
clauses.append((Recordings.camera << camera_list))
|
||||
else:
|
||||
camera_list = allowed_cameras
|
||||
clauses = [
|
||||
(Recordings.end_time >= after) & (Recordings.start_time <= before),
|
||||
(Recordings.camera << camera_list),
|
||||
]
|
||||
|
||||
# Get recording start times
|
||||
data: list[Recordings] = (
|
||||
|
||||
@@ -13,6 +13,10 @@ from frigate.comms.event_metadata_updater import (
|
||||
EventMetadataTypeEnum,
|
||||
)
|
||||
from frigate.config import CameraConfig, FrigateConfig
|
||||
from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateEnum,
|
||||
CameraConfigUpdateSubscriber,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,6 +33,11 @@ class CameraActivityManager:
|
||||
self.zone_all_object_counts: dict[str, Counter] = {}
|
||||
self.zone_active_object_counts: dict[str, Counter] = {}
|
||||
self.all_zone_labels: dict[str, set[str]] = {}
|
||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||
config,
|
||||
config.cameras,
|
||||
[CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects],
|
||||
)
|
||||
|
||||
for camera_config in config.cameras.values():
|
||||
if not camera_config.enabled_in_config:
|
||||
@@ -56,7 +65,40 @@ class CameraActivityManager:
|
||||
else camera_config.objects.track
|
||||
)
|
||||
|
||||
def __rebuild_zone_labels(self) -> None:
|
||||
"""Rebuild zone label tracking after a runtime zones/objects change."""
|
||||
new_zone_labels: dict[str, set[str]] = {}
|
||||
|
||||
for camera_config in self.config.cameras.values():
|
||||
if not camera_config.enabled_in_config or camera_config.name is None:
|
||||
continue
|
||||
|
||||
for zone, zone_config in camera_config.zones.items():
|
||||
new_zone_labels.setdefault(zone, set()).update(
|
||||
zone_config.objects
|
||||
if zone_config.objects
|
||||
else camera_config.objects.track
|
||||
)
|
||||
|
||||
# drop counters for zones that no longer exist
|
||||
for zone in list(self.zone_all_object_counts.keys()):
|
||||
if zone not in new_zone_labels:
|
||||
self.zone_all_object_counts.pop(zone, None)
|
||||
self.zone_active_object_counts.pop(zone, None)
|
||||
|
||||
# ensure counters exist for new zones so the first count is published
|
||||
for zone in new_zone_labels:
|
||||
self.zone_all_object_counts.setdefault(zone, Counter())
|
||||
self.zone_active_object_counts.setdefault(zone, Counter())
|
||||
|
||||
self.all_zone_labels = new_zone_labels
|
||||
|
||||
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
|
||||
updated_topics = self.config_subscriber.check_for_updates()
|
||||
|
||||
if "zones" in updated_topics or "objects" in updated_topics:
|
||||
self.__rebuild_zone_labels()
|
||||
|
||||
all_objects: list[dict[str, Any]] = []
|
||||
|
||||
for camera in new_activity.keys():
|
||||
@@ -161,6 +203,9 @@ class CameraActivityManager:
|
||||
self.publish(f"{camera}/all", sum(list(all_objects.values())))
|
||||
self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
|
||||
|
||||
def stop(self) -> None:
|
||||
self.config_subscriber.stop()
|
||||
|
||||
|
||||
class AudioActivityManager:
|
||||
def __init__(
|
||||
|
||||
@@ -397,6 +397,8 @@ class Dispatcher:
|
||||
comm.publish(topic, payload, retain)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.camera_activity.stop()
|
||||
|
||||
for comm in self.comms:
|
||||
comm.stop()
|
||||
|
||||
|
||||
@@ -475,3 +475,55 @@ class TestHttpMedia(BaseTestHttp):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self):
|
||||
"""cameras=all must not error and must only consider allowed cameras.
|
||||
|
||||
allowed_cameras is mocked to ["front_door"]. A back_door recording that
|
||||
would otherwise fill the gap must be ignored, and the request must not
|
||||
500 the way it did when cameras was reassigned to a list.
|
||||
"""
|
||||
with AuthTestClient(self.app) as client:
|
||||
# front_door has a 20s gap (1010-1030).
|
||||
Recordings.insert(
|
||||
id="front_a",
|
||||
path="/media/recordings/front_a.mp4",
|
||||
camera="front_door",
|
||||
start_time=1000,
|
||||
end_time=1010,
|
||||
duration=10,
|
||||
motion=0,
|
||||
).execute()
|
||||
Recordings.insert(
|
||||
id="front_b",
|
||||
path="/media/recordings/front_b.mp4",
|
||||
camera="front_door",
|
||||
start_time=1030,
|
||||
end_time=1040,
|
||||
duration=10,
|
||||
motion=0,
|
||||
).execute()
|
||||
# back_door is not in allowed_cameras; its full-window coverage must
|
||||
# not mask the front_door gap.
|
||||
Recordings.insert(
|
||||
id="back_a",
|
||||
path="/media/recordings/back_a.mp4",
|
||||
camera="back_door",
|
||||
start_time=1000,
|
||||
end_time=1040,
|
||||
duration=40,
|
||||
motion=0,
|
||||
).execute()
|
||||
|
||||
response = client.get(
|
||||
"/recordings/unavailable",
|
||||
params={
|
||||
"after": 1000,
|
||||
"before": 1040,
|
||||
"scale": 5,
|
||||
"cameras": "all",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
|
||||
|
||||
Generated
+41
-31
@@ -35,7 +35,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@rjsf/core": "^6.4.1",
|
||||
"@rjsf/shadcn": "^6.4.1",
|
||||
"@rjsf/shadcn": "^6.5.2",
|
||||
"@rjsf/utils": "^6.4.1",
|
||||
"@rjsf/validator-ajv8": "^6.4.1",
|
||||
"apexcharts": "^3.52.0",
|
||||
@@ -4887,13 +4887,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/core": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.4.1.tgz",
|
||||
"integrity": "sha512-+QaiSgQnOuO6ghIsohH2u/QcylkN+Da2968a75g/i4oARYJRYVxXDm2u3JR5aXndpMb4t4jTFrYyG8cNIv6oEg==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.5.2.tgz",
|
||||
"integrity": "sha512-Fx+aVNQRYQyoY0vM8zYDZkuOiNe+5PLsxUySUdHfjljlT23mJnTCpPKMkxWJwh4UEWeSN0xjmknW9LfIwuQmOg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.23",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"markdown-to-jsx": "^8.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -4901,14 +4901,14 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rjsf/utils": "^6.4.x",
|
||||
"@rjsf/utils": "^6.5.x",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/shadcn": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.4.1.tgz",
|
||||
"integrity": "sha512-WzwXW3XY7K1jo9XrBv6M41ScdHrnQDKpSxip5i1N6xCgEE6hiyX+wn7pDO689OoidvL3lWQmtnoqMdcoJvEWjw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.5.2.tgz",
|
||||
"integrity": "sha512-h3FsGRy07Gw4MrORlxNTuSb7icuyiUM0KXimg1jgtlYLhYvMKaHzJgdL/SrAyBWiUtXIsxuBhPU3rFxnA9Ml6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -4925,19 +4925,19 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"lucide-react": "^0.548.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rjsf/core": "^6.4.x",
|
||||
"@rjsf/utils": "^6.4.x",
|
||||
"@rjsf/core": "^6.5.x",
|
||||
"@rjsf/utils": "^6.5.x",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
@@ -4951,9 +4951,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/shadcn/node_modules/tailwind-merge": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -4961,16 +4961,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/utils": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.4.1.tgz",
|
||||
"integrity": "sha512-5NL3jwt3rIS5/WRTrKt++y40FS/ScKGVwYJ3jIrHSQHSwBdLnd4cHf2zcnA97L1Klj8I6tvS/ugh+blf/Diwuw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.5.2.tgz",
|
||||
"integrity": "sha512-qBVQ5qf9BKMOQy/DjMl/IjD5s6akRDx18cgiSYunKt/CYc+kPzHyCVA2TU0rXCsigNlhgnfM8piC3LEHye6vpA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@x0k/json-schema-merge": "^1.0.2",
|
||||
"@x0k/json-schema-merge": "^1.0.3",
|
||||
"fast-equals": "^6.0.0",
|
||||
"fast-uri": "^3.1.0",
|
||||
"jsonpointer": "^5.0.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4980,6 +4981,15 @@
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/utils/node_modules/fast-equals": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.0.tgz",
|
||||
"integrity": "sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/validator-ajv8": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.4.1.tgz",
|
||||
@@ -6173,9 +6183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@x0k/json-schema-merge": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@x0k/json-schema-merge/-/json-schema-merge-1.0.2.tgz",
|
||||
"integrity": "sha512-1734qiJHNX3+cJGDMMw2yz7R+7kpbAtl5NdPs1c/0gO5kYT6s4dMbLXiIfpZNsOYhGZI3aH7FWrj4Zxz7epXNg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@x0k/json-schema-merge/-/json-schema-merge-1.0.3.tgz",
|
||||
"integrity": "sha512-lerJC4sI9CNUQWdff3PnU1YJOqazD6TjMcvxZIPXUBjn4j1cUiXE0LvzhMnGYzKKr271TkvXJtH7gEwksrtn+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
@@ -13977,9 +13987,9 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
|
||||
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@rjsf/core": "^6.4.1",
|
||||
"@rjsf/shadcn": "^6.4.1",
|
||||
"@rjsf/shadcn": "^6.5.2",
|
||||
"@rjsf/utils": "^6.4.1",
|
||||
"@rjsf/validator-ajv8": "^6.4.1",
|
||||
"apexcharts": "^3.52.0",
|
||||
|
||||
@@ -1263,7 +1263,6 @@ function ObjectDetailsTab({
|
||||
}
|
||||
|
||||
setState("submitted");
|
||||
setSearch({ ...search, plus_id: "new_upload" });
|
||||
mutate(
|
||||
(key) => isEventsKey(key),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
@@ -1286,7 +1285,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
}
|
||||
},
|
||||
[search, mutate, mapSearchResults, setSearch, isEventsKey, t],
|
||||
[search, mutate, mapSearchResults, isEventsKey, t],
|
||||
);
|
||||
|
||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -47,6 +47,7 @@ type HlsVideoPlayerProps = {
|
||||
frigateControls?: boolean;
|
||||
inpointOffset?: number;
|
||||
onClipEnded?: (currentTime: number) => void;
|
||||
onClipPrevious?: (diff: number) => void;
|
||||
onPlayerLoaded?: () => void;
|
||||
onTimeUpdate?: (time: number) => void;
|
||||
onPlaying?: () => void;
|
||||
@@ -74,6 +75,7 @@ export default function HlsVideoPlayer({
|
||||
frigateControls = true,
|
||||
inpointOffset = 0,
|
||||
onClipEnded,
|
||||
onClipPrevious,
|
||||
onPlayerLoaded,
|
||||
onTimeUpdate,
|
||||
onPlaying,
|
||||
@@ -339,11 +341,17 @@ export default function HlsVideoPlayer({
|
||||
onSeek={(diff) => {
|
||||
const currentTime = videoRef.current?.currentTime;
|
||||
|
||||
if (!videoRef.current || !currentTime) {
|
||||
if (!videoRef.current || currentTime == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
||||
const newTime = currentTime + diff;
|
||||
|
||||
if (newTime < 0 && onClipPrevious) {
|
||||
onClipPrevious(diff);
|
||||
} else {
|
||||
videoRef.current.currentTime = Math.max(0, newTime);
|
||||
}
|
||||
}}
|
||||
onSetPlaybackRate={(rate) => {
|
||||
setPlaybackRate(rate, true);
|
||||
|
||||
@@ -49,6 +49,7 @@ type DynamicVideoPlayerProps = {
|
||||
onControllerReady: (controller: DynamicVideoController) => void;
|
||||
onTimestampUpdate?: (timestamp: number) => void;
|
||||
onClipEnded?: () => void;
|
||||
onClipPrevious?: (diff: number) => void;
|
||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||
toggleFullscreen: () => void;
|
||||
@@ -68,6 +69,7 @@ export default function DynamicVideoPlayer({
|
||||
onControllerReady,
|
||||
onTimestampUpdate,
|
||||
onClipEnded,
|
||||
onClipPrevious,
|
||||
onSeekToTime,
|
||||
setFullResolution,
|
||||
toggleFullscreen,
|
||||
@@ -343,6 +345,7 @@ export default function DynamicVideoPlayer({
|
||||
onTimeUpdate={onTimeUpdate}
|
||||
onPlayerLoaded={onPlayerLoaded}
|
||||
onClipEnded={onValidateClipEnd}
|
||||
onClipPrevious={onClipPrevious}
|
||||
onSeekToTime={(timestamp, play) => {
|
||||
if (onSeekToTime) {
|
||||
onSeekToTime(timestamp, play);
|
||||
|
||||
@@ -336,6 +336,16 @@ export function RecordingView({
|
||||
[currentTimeRange, updateSelectedSegment],
|
||||
);
|
||||
|
||||
const onClipPrevious = useCallback(
|
||||
(diff: number) => {
|
||||
manuallySetCurrentTime(
|
||||
currentTime + diff,
|
||||
mainControllerRef.current?.isPlaying() ?? false,
|
||||
);
|
||||
},
|
||||
[currentTime, manuallySetCurrentTime],
|
||||
);
|
||||
|
||||
const onShareReviewLink = useCallback(
|
||||
(timestamp: number) => {
|
||||
const reviewUrl = createRecordingReviewUrl(location.pathname, {
|
||||
@@ -902,6 +912,7 @@ export function RecordingView({
|
||||
);
|
||||
}}
|
||||
onClipEnded={onClipEnded}
|
||||
onClipPrevious={onClipPrevious}
|
||||
onSeekToTime={manuallySetCurrentTime}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
|
||||
Reference in New Issue
Block a user