diff --git a/web/packages/design/src/Icon/Icons.story.tsx b/web/packages/design/src/Icon/Icons.story.tsx
index 310247742514b..75643602fd429 100644
--- a/web/packages/design/src/Icon/Icons.story.tsx
+++ b/web/packages/design/src/Icon/Icons.story.tsx
@@ -124,6 +124,7 @@ export const Icons = () => (
+
diff --git a/web/packages/design/src/Icon/Icons/FastForward.tsx b/web/packages/design/src/Icon/Icons/FastForward.tsx
new file mode 100644
index 0000000000000..38ec3b7bd1b8b
--- /dev/null
+++ b/web/packages/design/src/Icon/Icons/FastForward.tsx
@@ -0,0 +1,78 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/* MIT License
+
+Copyright (c) 2020 Phosphor Icons
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+import { forwardRef } from 'react';
+
+import { Icon, IconProps } from '../Icon';
+
+/*
+
+THIS FILE IS GENERATED. DO NOT EDIT.
+
+*/
+
+export const FastForward = forwardRef(
+ ({ size = 24, color, ...otherProps }, ref) => (
+
+
+
+
+ )
+);
diff --git a/web/packages/design/src/Icon/assets/FastForward.svg b/web/packages/design/src/Icon/assets/FastForward.svg
new file mode 100644
index 0000000000000..79e5ef336c4a8
--- /dev/null
+++ b/web/packages/design/src/Icon/assets/FastForward.svg
@@ -0,0 +1,4 @@
+
diff --git a/web/packages/design/src/Icon/index.ts b/web/packages/design/src/Icon/index.ts
index dd52f898b9500..4682e40eff00d 100644
--- a/web/packages/design/src/Icon/index.ts
+++ b/web/packages/design/src/Icon/index.ts
@@ -113,6 +113,7 @@ export { EnvelopeOpen } from './Icons/EnvelopeOpen';
export { EqualizersVertical } from './Icons/EqualizersVertical';
export { Expand } from './Icons/Expand';
export { Facebook } from './Icons/Facebook';
+export { FastForward } from './Icons/FastForward';
export { FilmStrip } from './Icons/FilmStrip';
export { FingerprintSimple } from './Icons/FingerprintSimple';
export { Floppy } from './Icons/Floppy';
diff --git a/web/packages/teleport/src/SessionRecordings/view/CurrentEventInfo.tsx b/web/packages/teleport/src/SessionRecordings/view/CurrentEventInfo.tsx
new file mode 100644
index 0000000000000..d5267bf1510aa
--- /dev/null
+++ b/web/packages/teleport/src/SessionRecordings/view/CurrentEventInfo.tsx
@@ -0,0 +1,134 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {
+ useCallback,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+ type Ref,
+} from 'react';
+import styled from 'styled-components';
+
+import { ButtonPrimary } from 'design/Button';
+import { FastForward } from 'design/Icon';
+
+import {
+ SessionRecordingEventType,
+ type SessionRecordingEvent,
+} from 'teleport/services/recordings';
+import { formatSessionRecordingDuration } from 'teleport/SessionRecordings/list/RecordingItem';
+
+export interface CurrentEventInfoHandle {
+ setTime: (time: number) => void;
+}
+
+interface CurrentEventInfoProps {
+ events: SessionRecordingEvent[];
+ onSeek: (time: number) => void;
+ ref?: Ref;
+}
+
+const EventsList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${props => props.theme.space[2]}px;
+ position: absolute;
+ top: ${props => props.theme.space[4]}px;
+ right: ${props => props.theme.space[4]}px;
+ z-index: 2;
+`;
+
+export function CurrentEventInfo({
+ events,
+ onSeek,
+ ref,
+}: CurrentEventInfoProps) {
+ const [currentEvents, setCurrentEvents] = useState(
+ []
+ );
+ const currentEventsRef = useRef([]);
+
+ useImperativeHandle(ref, () => ({
+ setTime(time: number) {
+ const eventsInTimePeriod = events.filter(
+ e => e.startTime <= time && e.endTime >= time
+ );
+
+ const hasChanged =
+ eventsInTimePeriod.length !== currentEventsRef.current.length ||
+ eventsInTimePeriod.some(
+ (event, index) => event !== currentEventsRef.current[index]
+ );
+
+ if (hasChanged) {
+ currentEventsRef.current = eventsInTimePeriod;
+ setCurrentEvents(eventsInTimePeriod);
+ }
+ },
+ }));
+
+ const handleSkipToEnd = useCallback(
+ (time: number) => {
+ onSeek(time + 1);
+ },
+ [onSeek]
+ );
+
+ const items = useMemo(() => {
+ if (currentEvents.length === 0) {
+ return null;
+ }
+
+ const items: ReactNode[] = [];
+
+ for (const [index, event] of currentEvents.entries()) {
+ if (event.type !== SessionRecordingEventType.Inactivity) {
+ continue;
+ }
+
+ items.push(
+ {
+ handleSkipToEnd(event.endTime);
+ }}
+ px={2}
+ >
+ Skip {formatSessionRecordingDuration(event.endTime - event.startTime)}{' '}
+ of inactivity
+
+
+ );
+ }
+
+ if (items.length === 0) {
+ return null;
+ }
+
+ return items;
+ }, [currentEvents, handleSkipToEnd]);
+
+ if (currentEvents.length === 0) {
+ return null;
+ }
+
+ return {items};
+}
diff --git a/web/packages/teleport/src/SessionRecordings/view/RecordingPlayer.tsx b/web/packages/teleport/src/SessionRecordings/view/RecordingPlayer.tsx
index 9d25beb0a9931..4939127872dc3 100644
--- a/web/packages/teleport/src/SessionRecordings/view/RecordingPlayer.tsx
+++ b/web/packages/teleport/src/SessionRecordings/view/RecordingPlayer.tsx
@@ -23,7 +23,10 @@ import styled from 'styled-components';
import Flex from 'design/Flex';
import Indicator from 'design/Indicator';
-import type { RecordingType } from 'teleport/services/recordings';
+import type {
+ RecordingType,
+ SessionRecordingEvent,
+} from 'teleport/services/recordings';
import { RECORDING_TYPES_WITH_METADATA } from 'teleport/services/recordings/recordings';
import { DesktopPlayer } from 'teleport/SessionRecordings/view/DesktopPlayer';
import { TtyRecordingPlayer } from 'teleport/SessionRecordings/view/player/tty/TtyRecordingPlayer';
@@ -43,6 +46,7 @@ interface RecordingPlayerProps {
fullscreen?: boolean;
initialCols?: number;
initialRows?: number;
+ events?: SessionRecordingEvent[];
ref?: RefObject;
}
@@ -75,6 +79,7 @@ export function RecordingPlayer({
recordingType,
initialCols,
initialRows,
+ events,
ref,
}: RecordingPlayerProps) {
if (recordingType === 'desktop') {
@@ -121,6 +126,7 @@ export function RecordingPlayer({
onToggleTimeline={onToggleTimeline}
initialCols={initialCols}
initialRows={initialRows}
+ events={events}
ref={ref}
/>
diff --git a/web/packages/teleport/src/SessionRecordings/view/RecordingWithMetadata.tsx b/web/packages/teleport/src/SessionRecordings/view/RecordingWithMetadata.tsx
index 883ffce0c6b09..3a0052c6212e2 100644
--- a/web/packages/teleport/src/SessionRecordings/view/RecordingWithMetadata.tsx
+++ b/web/packages/teleport/src/SessionRecordings/view/RecordingWithMetadata.tsx
@@ -147,6 +147,7 @@ export function RecordingWithMetadata({
onTimeChange={handleTimeChange}
initialCols={data.metadata.startCols}
initialRows={data.metadata.startRows}
+ events={data.metadata.events}
ref={playerRef}
/>
diff --git a/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx b/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx
index 37ee62a78948c..c1f9d61f7361b 100644
--- a/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx
+++ b/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx
@@ -31,6 +31,11 @@ import Box from 'web/packages/design/src/Box';
import Flex from 'web/packages/design/src/Flex';
import { Pause, Play } from 'web/packages/design/src/Icon';
+import type { SessionRecordingEvent } from 'teleport/services/recordings';
+import {
+ CurrentEventInfo,
+ type CurrentEventInfoHandle,
+} from 'teleport/SessionRecordings/view/CurrentEventInfo';
import type { Player } from 'teleport/SessionRecordings/view/player/Player';
import {
PlayerControls,
@@ -58,6 +63,7 @@ export interface RecordingPlayerProps<
endEventType: TEndEventType;
decodeEvent: (buffer: ArrayBuffer) => TEvent;
ref: RefObject;
+ events?: SessionRecordingEvent[];
ws: WebSocket;
}
@@ -74,6 +80,7 @@ export function RecordingPlayer<
onToggleFullscreen,
onToggleSidebar,
onToggleTimeline,
+ events,
ref,
ws,
}: RecordingPlayerProps) {
@@ -81,6 +88,7 @@ export function RecordingPlayer<
const [showPlayButton, setShowPlayButton] = useState(true);
+ const eventInfoRef = useRef(null);
const controlsRef = useRef(null);
const playerRef = useRef(null);
@@ -95,12 +103,13 @@ export function RecordingPlayer<
});
stream.on('time', time => {
- if (!controlsRef.current) {
+ if (!controlsRef.current || !eventInfoRef.current) {
return;
}
controlsRef.current.setTime(time);
onTimeChange(time);
+ eventInfoRef.current.setTime(time);
});
stream.loadInitial();
@@ -161,7 +170,14 @@ export function RecordingPlayer<
borderColor="spotBackground.1"
borderRadius={4}
overflow="hidden"
+ position="relative"
>
+
+
{showPlayButton && (