diff --git a/frontend/app/components/Session/Player/ReplayPlayer/AudioPlayer.tsx b/frontend/app/components/Session/Player/ReplayPlayer/AudioPlayer.tsx index f70999d7f..159e27055 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/AudioPlayer.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/AudioPlayer.tsx @@ -1,34 +1,52 @@ -import { MutedOutlined, SoundOutlined, CaretDownOutlined, ControlOutlined } from '@ant-design/icons'; -import { Button, Popover, InputNumber } from 'antd'; +import { + CaretDownOutlined, + ControlOutlined, + MutedOutlined, + SoundOutlined, +} from '@ant-design/icons'; +import { Button, InputNumber, Popover } from 'antd'; import { Slider } from 'antd'; import { observer } from 'mobx-react-lite'; -import React, { useRef, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; -import { PlayerContext } from '../../playerContext'; +import { PlayerContext } from 'App/components/Session/playerContext'; -function DropdownAudioPlayer({ url }: { url: string }) { - const { store } = React.useContext(PlayerContext); +function DropdownAudioPlayer({ + audioEvents, +}: { + audioEvents: { payload: Record; timestamp: number }[]; +}) { + const { store } = useContext(PlayerContext); const [isVisible, setIsVisible] = useState(false); - const [volume, setVolume] = useState(0); + const [volume, setVolume] = useState(35); const [delta, setDelta] = useState(0); const [deltaInputValue, setDeltaInputValue] = useState(0); - const [isMuted, setIsMuted] = useState(true); - const lastPlayerTime = React.useRef(0); - const audioRef = useRef(null); + const [isMuted, setIsMuted] = useState(false); + const lastPlayerTime = useRef(0); + const audioRefs = useRef>({}); - const { time = 0, speed = 1, playing } = store?.get() ?? {}; + const { time = 0, speed = 1, playing, sessionStart } = store?.get() ?? {}; + + const files = audioEvents.map((pa) => { + const data = pa.payload; + return { + url: data.url, + timestamp: data.timestamp, + start: pa.timestamp - sessionStart, + }; + }); const toggleMute = () => { - if (audioRef.current) { - if (audioRef.current?.paused && playing) { - audioRef.current?.play(); - } - audioRef.current.muted = !audioRef.current.muted; - if (isMuted) { - onVolumeChange(35) - } else { - onVolumeChange(0) + Object.values(audioRefs.current).forEach((audio) => { + if (audio) { + audio.muted = !audio.muted; } + }); + setIsMuted(!isMuted); + if (!isMuted) { + onVolumeChange(0); + } else { + onVolumeChange(35); } }; @@ -37,87 +55,102 @@ function DropdownAudioPlayer({ url }: { url: string }) { }; const handleDelta = (value: any) => { - setDeltaInputValue(parseFloat(value)) - } + setDeltaInputValue(parseFloat(value)); + }; const onSync = () => { - setDelta(deltaInputValue) - handleSeek(time + deltaInputValue * 1000) - } + setDelta(deltaInputValue); + handleSeek(time + deltaInputValue * 1000); + }; const onCancel = () => { - setDeltaInputValue(0) - setIsVisible(false) - } + setDeltaInputValue(0); + setIsVisible(false); + }; const onReset = () => { - setDelta(0) - setDeltaInputValue(0) - handleSeek(time) - } + setDelta(0); + setDeltaInputValue(0); + handleSeek(time); + }; const onVolumeChange = (value: number) => { - if (audioRef.current) { - audioRef.current.volume = value / 100; - } - if (value === 0) { - setIsMuted(true); - } - if (value > 0) { - setIsMuted(false); - } + Object.values(audioRefs.current).forEach((audio) => { + if (audio) { + audio.volume = value / 100; + } + }); setVolume(value); + setIsMuted(value === 0); }; const handleSeek = (timeMs: number) => { - if (audioRef.current) { - audioRef.current.currentTime = (timeMs + delta * 1000) / 1000; - } + Object.entries(audioRefs.current).forEach(([key, audio]) => { + if (audio) { + const file = files.find((f) => f.url === key); + if (file) { + audio.currentTime = Math.max( + (timeMs + delta * 1000 - file.start) / 1000, + 0 + ); + } + } + }); }; const changePlaybackSpeed = (speed: number) => { - if (audioRef.current) { - audioRef.current.playbackRate = speed; - } + Object.values(audioRefs.current).forEach((audio) => { + if (audio) { + audio.playbackRate = speed; + } + }); }; - React.useEffect(() => { + useEffect(() => { const deltaMs = delta * 1000; if (Math.abs(lastPlayerTime.current - time - deltaMs) >= 250) { handleSeek(time); } - if (audioRef.current) { - if (audioRef.current.paused && playing) { - audioRef.current?.play(); + Object.entries(audioRefs.current).forEach(([url, audio]) => { + if (audio) { + const file = files.find((f) => f.url === url); + if (file && time >= file.start) { + if (audio.paused && playing) { + audio.play(); + } + } else { + audio.pause(); + } + if (audio.muted !== isMuted) { + console.log(isMuted, audio.muted); + audio.muted = isMuted; + } } - if (audioRef.current.muted !== isMuted) { - audioRef.current.muted = isMuted; - } - } + }); lastPlayerTime.current = time + deltaMs; }, [time, delta]); - React.useEffect(() => { + useEffect(() => { changePlaybackSpeed(speed); }, [speed]); - React.useEffect(() => { - if (playing) { - audioRef.current?.play(); - } else { - audioRef.current?.pause(); - } - const volume = audioRef.current?.volume ?? 0 - const shouldBeMuted = audioRef.current?.muted ?? isMuted - setVolume(shouldBeMuted ? 0 : volume * 100); + useEffect(() => { + Object.entries(audioRefs.current).forEach(([url, audio]) => { + if (audio) { + const file = files.find((f) => f.url === url); + if (file && playing && time >= file.start) { + audio.play(); + } else { + audio.pause(); + } + } + }); + setVolume(isMuted ? 0 : volume); }, [playing]); return (
-
+
} > -
+
{isMuted ? : }
@@ -153,46 +190,64 @@ function DropdownAudioPlayer({ url }: { url: string }) {
{isVisible ? ( -
-
- -
Audio Track Synchronization
+
+
+ +
Audio Track Synchronization
+
+ `${value}s`} + parser={(value) => value?.replace('s', '') as unknown as number} + stringMode + onChange={handleDelta} + /> +
+ + + +
- `${value}s`} - parser={(value) => value?.replace('s', '') as unknown as number} - stringMode - onChange={handleDelta} /> -
- - - - -
-
) : null} -
- +
+ {files.map((file) => ( + + ))}
); diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index 23a349194..682b8b0e5 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -5,43 +5,27 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; import { connect } from 'react-redux'; + + import { PlayerContext } from 'App/components/Session/playerContext'; import { useStore } from 'App/mstore'; import { FullScreenButton, PlayButton, PlayingState } from 'App/player-ui'; import { session as sessionRoute, withSiteId } from 'App/routes'; import useShortcuts from 'Components/Session/Player/ReplayPlayer/useShortcuts'; -import { - LaunchConsoleShortcut, - LaunchEventsShortcut, - LaunchNetworkShortcut, - LaunchPerformanceShortcut, - LaunchStateShortcut, - LaunchXRaShortcut, -} from 'Components/Session_/Player/Controls/components/KeyboardHelp'; -import { - CONSOLE, - GRAPHQL, - INSPECTOR, - NETWORK, - OVERVIEW, - PERFORMANCE, - PROFILER, - STACKEVENTS, - STORAGE, - changeSkipInterval, - fullscreenOff, - fullscreenOn, - toggleBottomBlock, -} from 'Duck/components/player'; +import { LaunchConsoleShortcut, LaunchEventsShortcut, LaunchNetworkShortcut, LaunchPerformanceShortcut, LaunchStateShortcut, LaunchXRaShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp'; +import { CONSOLE, GRAPHQL, INSPECTOR, NETWORK, OVERVIEW, PERFORMANCE, PROFILER, STACKEVENTS, STORAGE, changeSkipInterval, fullscreenOff, fullscreenOn, toggleBottomBlock } from 'Duck/components/player'; import { fetchSessions } from 'Duck/liveSearch'; import { Icon } from 'UI'; + + import DropdownAudioPlayer from '../../../Session/Player/ReplayPlayer/AudioPlayer'; import ControlButton from './ControlButton'; import Timeline from './Timeline'; import PlayerControls from './components/PlayerControls'; import styles from './controls.module.css'; + export const SKIP_INTERVALS = { 2: 2e3, 5: 5e3, @@ -144,6 +128,7 @@ function Controls(props: any) { ? PlayingState.Playing : PlayingState.Paused; + const events = session.stackEvents ?? []; return (
@@ -181,7 +166,7 @@ function Controls(props: any) { toggleBottomTools={toggleBottomTools} bottomBlock={bottomBlock} disabled={disabled} - audioUrl={session.audio} + events={events} /> )} @@ -204,7 +189,7 @@ interface IDevtoolsButtons { toggleBottomTools: (blockName: number) => void; bottomBlock: number; disabled: boolean; - audioUrl?: string; + events: any[]; } const DevtoolsButtons = observer( @@ -213,7 +198,7 @@ const DevtoolsButtons = observer( toggleBottomTools, bottomBlock, disabled, - audioUrl, + events, }: IDevtoolsButtons) => { const { aiSummaryStore } = useStore(); const { store, player } = React.useContext(PlayerContext); @@ -250,6 +235,8 @@ const DevtoolsButtons = observer( } aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary); }; + + const possibleAudio = events.filter((e) => e.name.includes('media/audio')); return ( <> {isSaas ? : null} @@ -350,7 +337,7 @@ const DevtoolsButtons = observer( label="Profiler" /> )} - {audioUrl ? : null} + {possibleAudio.length ? : null} ); } diff --git a/tracker/tracker-assist/bun.lockb b/tracker/tracker-assist/bun.lockb index 7ee069c43..b218a8969 100755 Binary files a/tracker/tracker-assist/bun.lockb and b/tracker/tracker-assist/bun.lockb differ diff --git a/tracker/tracker/bun.lockb b/tracker/tracker/bun.lockb index ee7dac7c8..6ecd363ba 100755 Binary files a/tracker/tracker/bun.lockb and b/tracker/tracker/bun.lockb differ