Skip to content

Commit 9f6503e

Browse files
authored
fix: Merge pull request #7 from UniversalDataTool/audio-playback
Audio Playback
2 parents f54111d + b7b95e5 commit 9f6503e

File tree

13 files changed

+259
-7
lines changed

13 files changed

+259
-7
lines changed

src/components/MainLayout/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export const MainLayout = ({
4242
onChangeTimestamps,
4343
enabledTools = defaultEnabledTools,
4444
showValues = false,
45+
onStartPlayback,
46+
onStopPlayback,
47+
isPlayingMedia,
4548
}) => {
4649
const themeColors = useColors()
4750
const [activeDurationGroup, setActiveDurationGroup] = useState(null)
@@ -173,6 +176,9 @@ export const MainLayout = ({
173176
selectedDurationIndex={selectedDurationIndex}
174177
durationGroups={durationGroups}
175178
allowCustomLabels={allowCustomLabels}
179+
onStartPlayback={onStartPlayback}
180+
onStopPlayback={onStopPlayback}
181+
isPlayingMedia={isPlayingMedia}
176182
/>
177183
<Timeline
178184
timeFormat={timeFormat}

src/components/MainLayout/index.stories.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,32 @@ export const ExampleMiscLayer = () => {
9393
/>
9494
)
9595
}
96+
97+
export const AudioPlayback = () => {
98+
const [durationGroups, setDurationGroups] = useState([])
99+
100+
const [timestamps, setTimestamps] = useState([])
101+
102+
const [isPlayingMedia, setIsPlayingMedia] = useState(false)
103+
104+
return (
105+
<MainLayout
106+
timeFormat="dates"
107+
curveGroups={[
108+
[
109+
{
110+
data: tesla.curve2017.sort((a, b) => a[0] - b[0]),
111+
color: solarized.green,
112+
},
113+
],
114+
]}
115+
durationGroups={durationGroups}
116+
timestamps={timestamps}
117+
onChangeTimestamps={setTimestamps}
118+
onChangeDurationGroups={setDurationGroups}
119+
onStartPlayback={() => setIsPlayingMedia(true)}
120+
onStopPlayback={() => setIsPlayingMedia(false)}
121+
isPlayingMedia={isPlayingMedia}
122+
/>
123+
)
124+
}

src/components/MouseTransformHandler/index.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useRef, useEffect } from "react"
1+
import React, { useState, useRef, useEffect, useCallback } from "react"
22
import { styled } from "@material-ui/core/styles"
33
import useEventCallback from "use-event-callback"
44
import useToolMode from "../../hooks/use-tool-mode"
@@ -146,14 +146,20 @@ export const MouseTransformHandler = ({
146146
e.preventDefault()
147147
})
148148

149-
// TODO
149+
const containerMountCallback = useCallback((ref) => {
150+
if (ref === null) {
151+
containerRef.current.removeEventListener("wheel", onWheel)
152+
}
153+
containerRef.current = ref
154+
ref.addEventListener("wheel", onWheel, { passive: false })
155+
}, [])
156+
150157
return (
151158
<Container
152-
ref={containerRef}
159+
ref={containerMountCallback}
153160
onMouseMove={onMouseMove}
154161
onMouseDown={onMouseDown}
155162
onMouseUp={onMouseUp}
156-
onWheel={onWheel}
157163
onContextMenu={onContextMenu}
158164
>
159165
{children}

src/components/ReactTimeSeries/ReactTimeSeries.stories.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,19 @@ export const LargeTimeNoneFormat = () => {
172172
/>
173173
)
174174
}
175+
176+
export const AudioPlayback = () => {
177+
return (
178+
<ReactTimeSeries
179+
interface={{
180+
timeFormat: "none",
181+
allowCustomLabels: true,
182+
}}
183+
sample={{
184+
audioUrl:
185+
"https://s3.amazonaws.com/datasets.workaround.online/voice-samples/001/voice.mp3",
186+
}}
187+
onModifySample={() => null}
188+
/>
189+
)
190+
}

src/components/ReactTimeSeries/index.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import React, { useMemo, useState } from "react"
1+
import React, { useMemo, useState, useEffect, useRef } from "react"
22
import { setIn } from "seamless-immutable"
33
import useEventCallback from "use-event-callback"
44
import { useAsyncMemo } from "use-async-memo"
55
import { RecoilRoot } from "recoil"
66
import useGetRandomColorUsingHash from "../../hooks/use-get-random-color-using-hash"
77
import Measure from "react-measure"
8+
import { useSetTimeCursorTime } from "../../hooks/use-time-cursor-time"
9+
import useRootAudioElm from "../../hooks/use-root-audio-elm"
810

911
import MainLayout from "../MainLayout"
1012

@@ -52,6 +54,7 @@ export const ReactTimeSeriesWithoutContext = ({
5254

5355
const timeDataAvailable = [sampleTimeData, audioUrl, csvUrl].some(Boolean)
5456

57+
const [isPlayingMedia, setIsPlayingMedia] = useState(false)
5558
const [error, setError] = useState(null)
5659
const timeData = useAsyncMemo(
5760
async () => {
@@ -195,6 +198,43 @@ export const ReactTimeSeriesWithoutContext = ({
195198
if (!widthProp) setWidth(bounds.width)
196199
})
197200

201+
const audioSource = useRef()
202+
203+
const [, setRootAudioElm] = useRootAudioElm()
204+
const setTimeCursorTime = useSetTimeCursorTime()
205+
206+
useEffect(() => {
207+
if (audioUrl) {
208+
audioSource.current = new Audio(audioUrl)
209+
setTimeCursorTime(0)
210+
}
211+
}, [])
212+
213+
const onAudioTimeChanged = useEventCallback(() => {
214+
setTimeCursorTime(audioSource.current.currentTime * 1000)
215+
})
216+
const onAudioPaused = useEventCallback(() => {
217+
audioSource.current.removeEventListener("timeupdate", onAudioTimeChanged)
218+
audioSource.current.removeEventListener("pause", onAudioPaused)
219+
})
220+
221+
const onStartPlayback = useEventCallback(() => {
222+
setIsPlayingMedia(true)
223+
if (audioSource.current) {
224+
audioSource.current.play()
225+
audioSource.current.addEventListener("timeupdate", onAudioTimeChanged)
226+
audioSource.current.addEventListener("pause", onAudioPaused)
227+
setRootAudioElm(audioSource.current)
228+
}
229+
})
230+
231+
const onStopPlayback = useEventCallback(() => {
232+
setIsPlayingMedia(false)
233+
if (audioSource.current) {
234+
audioSource.current.pause()
235+
}
236+
})
237+
198238
if (timeDataLoading) return "loading" // TODO real loader
199239

200240
if (!timeData) {
@@ -228,6 +268,9 @@ export const ReactTimeSeriesWithoutContext = ({
228268
allowCustomLabels={allowCustomLabels}
229269
enabledTools={enabledTools}
230270
showValues={showValues}
271+
onStartPlayback={onStartPlayback}
272+
onStopPlayback={onStopPlayback}
273+
isPlayingMedia={isPlayingMedia}
231274
/>
232275
</div>
233276
)}

src/components/Timeline/Timeline.stories.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,22 @@ export const TimeWithTextMarkers = (args) => {
7575
/>
7676
)
7777
}
78+
79+
export const TimeWithCurrentTimeCursor = (args) => {
80+
const colors = useColors()
81+
return (
82+
<Timeline
83+
{...args}
84+
timeFormat="timecolons"
85+
width={500}
86+
visibleTimeStart={0}
87+
visibleTimeEnd={60000 * 80}
88+
timeCursorTime={20 * 60000}
89+
timestamps={[
90+
{ time: 10 * 60000, color: colors.cyan, label: "Timestamp 1" },
91+
{ time: 50 * 60000, color: colors.red, label: "Another Timestamp" },
92+
]}
93+
gridLineMetrics={getMinorMajorDurationLines(new Matrix(), 500)}
94+
/>
95+
)
96+
}

src/components/Timeline/index.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import React from "react"
1+
import React, { useRef } from "react"
22
import range from "lodash/range"
33
import { styled } from "@material-ui/core/styles"
44
import useColors from "../../hooks/use-colors"
55
import TimeStamp from "../TimeStamp"
6+
import {
7+
useTimeCursorTime,
8+
useSetTimeCursorTime,
9+
} from "../../hooks/use-time-cursor-time"
10+
import useRootAudioElm from "../../hooks/use-root-audio-elm"
11+
import useEventCallback from "use-event-callback"
612

713
import { formatTime } from "../../utils/format-time"
814

@@ -11,6 +17,7 @@ const Container = styled("div")(({ width, themeColors }) => ({
1117
overflow: "hidden",
1218
position: "relative",
1319
height: 64,
20+
cursor: "pointer",
1421
borderBottom: `1px solid ${themeColors.Selection}`,
1522
color: themeColors.fg,
1623
}))
@@ -21,13 +28,25 @@ const TimeText = styled("div")(({ x, faded }) => ({
2128
fontSize: 12,
2229
fontVariantNumeric: "tabular-nums",
2330
position: "absolute",
31+
top: 16,
2432
left: x,
2533
borderLeft: "1px solid rgba(255,255,255,0.5)",
2634
paddingLeft: 4,
2735
whiteSpace: "pre-wrap",
2836
opacity: faded ? 0.25 : 0.75,
2937
}))
3038

39+
const TimeCursor = styled("div")(({ left, themeColors }) => ({
40+
position: "absolute",
41+
width: 0,
42+
height: 0,
43+
top: 0,
44+
left: left - 6,
45+
borderLeft: "8px solid transparent",
46+
borderRight: "8px solid transparent",
47+
borderTop: `12px solid ${themeColors.green}`,
48+
}))
49+
3150
const Svg = styled("svg")({
3251
position: "absolute",
3352
left: 0,
@@ -43,6 +62,7 @@ export const Timeline = ({
4362
gridLineMetrics,
4463
onClickTimestamp,
4564
onRemoveTimestamp,
65+
timeCursorTime: timeCursorTimeProp,
4666
}) => {
4767
const themeColors = useColors()
4868
const visibleDuration = visibleTimeEnd - visibleTimeStart
@@ -51,15 +71,39 @@ export const Timeline = ({
5171
const timeTextTimes = range(timeTextCount).map(
5272
(i) => visibleTimeStart + (visibleDuration / timeTextCount) * i
5373
)
74+
const recoilTimeCursorTime = useTimeCursorTime()
75+
const setTimeCursorTime = useSetTimeCursorTime()
76+
const [rootAudioElm] = useRootAudioElm()
77+
const timeCursorTime =
78+
timeCursorTimeProp === undefined ? recoilTimeCursorTime : timeCursorTimeProp
5479

5580
const {
5681
numberOfMajorGridLines,
5782
majorGridLinePixelOffset,
5883
majorGridLinePixelDistance,
5984
} = gridLineMetrics
6085

86+
const containerRef = useRef()
87+
88+
const onClickTimeline = useEventCallback((e) => {
89+
if (!rootAudioElm) return
90+
const { clientX } = e
91+
const pxDistanceFromStart =
92+
clientX - containerRef.current.getBoundingClientRect().left
93+
const time =
94+
(pxDistanceFromStart / width) * (visibleTimeEnd - visibleTimeStart) +
95+
visibleTimeStart
96+
rootAudioElm.currentTime = time / 1000
97+
setTimeCursorTime(time)
98+
})
99+
61100
return (
62-
<Container themeColors={themeColors} width={width}>
101+
<Container
102+
ref={containerRef}
103+
themeColors={themeColors}
104+
width={width}
105+
onClick={rootAudioElm ? onClickTimeline : undefined}
106+
>
63107
{range(timeTextCount).map((timeTextIndex) => (
64108
<TimeText
65109
key={timeTextIndex}
@@ -102,6 +146,12 @@ export const Timeline = ({
102146
/>
103147
)
104148
})}
149+
{timeCursorTime !== undefined && (
150+
<TimeCursor
151+
themeColors={themeColors}
152+
left={((timeCursorTime - visibleTimeStart) / visibleDuration) * width}
153+
/>
154+
)}
105155
</Container>
106156
)
107157
}

src/components/Toolbar/Toolbar.stories.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export const Primary = () => {
4141
)
4242
)
4343
}}
44+
onStartPlayback={() => null}
45+
onStopPlayback={() => null}
46+
isPlayingMedia={false}
4447
/>
4548
)
4649
}

src/components/Toolbar/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import NormalSelect from "react-select"
1818
import LocationOnIcon from "@material-ui/icons/LocationOn"
1919
import TimelapseIcon from "@material-ui/icons/Timelapse"
2020
import ZoomInIcon from "@material-ui/icons/ZoomIn"
21+
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
22+
import PauseCircleOutlineIcon from "@material-ui/icons/PauseCircleOutline"
2123
import Color from "color"
2224

2325
const Container = styled("div")(({ themeColors }) => ({
@@ -96,6 +98,9 @@ export const Toolbar = ({
9698
selectedDurationIndex,
9799
onChangeSelectedItemLabel,
98100
allowCustomLabels = false,
101+
onStartPlayback,
102+
onStopPlayback,
103+
isPlayingMedia = false,
99104
}) => {
100105
const themeColors = useColors()
101106
const [mode, setToolMode] = useToolMode()
@@ -219,6 +224,22 @@ export const Toolbar = ({
219224
)}
220225
</Box>
221226
<ButtonGroup size="small">
227+
{onStartPlayback && !isPlayingMedia && (
228+
<Button
229+
onClick={onStartPlayback}
230+
className={classnames({ active: isPlayingMedia })}
231+
>
232+
<PlayCircleOutlineIcon />
233+
</Button>
234+
)}
235+
{onStopPlayback && isPlayingMedia && (
236+
<Button
237+
onClick={onStopPlayback}
238+
className={classnames({ active: isPlayingMedia })}
239+
>
240+
<PauseCircleOutlineIcon />
241+
</Button>
242+
)}
222243
<Button
223244
onClick={onSelectCreateTool}
224245
className={classnames({ active: mode === "create" })}

0 commit comments

Comments
 (0)