feat ui: new audio player for multiple tracks etc (#2261)
* feat ui: new audio player for multiple tracks etc * fix ui: cleanup, fix speed control
This commit is contained in:
parent
5574519775
commit
7a2f95d766
4 changed files with 176 additions and 134 deletions
|
|
@ -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<any, any>; 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<HTMLAudioElement>(null);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const lastPlayerTime = useRef(0);
|
||||
const audioRefs = useRef<Record<string, HTMLAudioElement | null>>({});
|
||||
|
||||
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 (
|
||||
<div className={'relative'}>
|
||||
<div
|
||||
className={'flex items-center'}
|
||||
style={{ height: 24 }}
|
||||
>
|
||||
<div className={'flex items-center'} style={{ height: 24 }}>
|
||||
<Popover
|
||||
trigger={'click'}
|
||||
className={'h-full'}
|
||||
|
|
@ -137,7 +170,11 @@ function DropdownAudioPlayer({ url }: { url: string }) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'px-2 h-full cursor-pointer border rounded-l border-gray-light hover:border-main hover:text-main hover:z-10'}>
|
||||
<div
|
||||
className={
|
||||
'px-2 h-full cursor-pointer border rounded-l border-gray-light hover:border-main hover:text-main hover:z-10'
|
||||
}
|
||||
>
|
||||
{isMuted ? <MutedOutlined /> : <SoundOutlined />}
|
||||
</div>
|
||||
</Popover>
|
||||
|
|
@ -153,46 +190,64 @@ function DropdownAudioPlayer({ url }: { url: string }) {
|
|||
</div>
|
||||
|
||||
{isVisible ? (
|
||||
<div className={"absolute left-1/2 top-0 border shadow border-gray-light rounded bg-white p-4 flex flex-col gap-4 mb-4"}
|
||||
style={{ width: 240, transform: 'translate(-75%, -110%)', zIndex: 101 }}>
|
||||
<div className={"font-semibold flex items-center gap-2"}>
|
||||
<ControlOutlined />
|
||||
<div>Audio Track Synchronization</div>
|
||||
<div
|
||||
className={
|
||||
'absolute left-1/2 top-0 border shadow border-gray-light rounded bg-white p-4 flex flex-col gap-4 mb-4'
|
||||
}
|
||||
style={{
|
||||
width: 240,
|
||||
transform: 'translate(-75%, -110%)',
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<div className={'font-semibold flex items-center gap-2'}>
|
||||
<ControlOutlined />
|
||||
<div>Audio Track Synchronization</div>
|
||||
</div>
|
||||
<InputNumber
|
||||
style={{ width: 180 }}
|
||||
value={deltaInputValue}
|
||||
size={'small'}
|
||||
step={'0.250'}
|
||||
name={'audio delta'}
|
||||
formatter={(value) => `${value}s`}
|
||||
parser={(value) => value?.replace('s', '') as unknown as number}
|
||||
stringMode
|
||||
onChange={handleDelta}
|
||||
/>
|
||||
<div className={'w-full flex items-center gap-2'}>
|
||||
<Button size={'small'} type={'primary'} onClick={onSync}>
|
||||
Sync
|
||||
</Button>
|
||||
<Button size={'small'} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
className={'ml-auto'}
|
||||
onClick={onReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<InputNumber
|
||||
style={{ width: 180 }}
|
||||
value={deltaInputValue}
|
||||
size={"small"}
|
||||
step={"0.250"}
|
||||
name={"audio delta"}
|
||||
formatter={(value) => `${value}s`}
|
||||
parser={(value) => value?.replace('s', '') as unknown as number}
|
||||
stringMode
|
||||
onChange={handleDelta} />
|
||||
<div className={"w-full flex items-center gap-2"}>
|
||||
<Button size={"small"} type={"primary"} onClick={onSync}>Sync</Button>
|
||||
<Button size={"small"} onClick={onCancel}>Cancel</Button>
|
||||
|
||||
<Button size={"small"} type={"text"} className={"ml-auto"} onClick={onReset}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
controls
|
||||
muted={isMuted}
|
||||
className="w-full"
|
||||
style={{ height: 32 }}
|
||||
>
|
||||
<source src={url} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<div style={{ display: 'none' }}>
|
||||
{files.map((file) => (
|
||||
<audio
|
||||
key={file.url}
|
||||
ref={(el) => (audioRefs.current[file.url] = el)}
|
||||
controls
|
||||
muted={isMuted}
|
||||
className="w-full"
|
||||
style={{ height: 32 }}
|
||||
>
|
||||
<source src={file.url} type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={styles.controls}>
|
||||
<Timeline />
|
||||
|
|
@ -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 ? <SummaryButton onClick={showSummary} /> : null}
|
||||
|
|
@ -350,7 +337,7 @@ const DevtoolsButtons = observer(
|
|||
label="Profiler"
|
||||
/>
|
||||
)}
|
||||
{audioUrl ? <DropdownAudioPlayer url={audioUrl} /> : null}
|
||||
{possibleAudio.length ? <DropdownAudioPlayer audioEvents={possibleAudio} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue