Merge pull request #174 from openmhz/wavesurfer

Wavesurfer
This commit is contained in:
Luke Berndt
2024-09-22 22:16:43 -04:00
committed by GitHub
22 changed files with 636 additions and 236 deletions

View File

@@ -6,7 +6,7 @@ import CallInfo from "./components/CallInfo";
import ListCalls from "./components/ListCalls";
import { useSelector, useDispatch } from 'react-redux'
import { useGetTalkgroupsQuery } from '../features/api/apiSlice'
import { playedCall } from "../features/calls/callsSlice";
import { playedCall } from "../features/calls/callsSlice";
import { useInView } from 'react-intersection-observer';
import {
Container,
@@ -18,7 +18,11 @@ import {
Message,
MessageHeader,
Button,
ButtonContent
ButtonContent,
Checkbox,
GridColumn, Grid,
GridRow,
Form
} from "semantic-ui-react";
import "./CallPlayer.css";
import queryString from '../query-string';
@@ -96,13 +100,13 @@ function CallPlayer(props) {
if (currentIndex > 0) {
const nextCallId = callsData.ids[currentIndex - 1];
const nextCall = callsData.entities[nextCallId];
console.log("Autoplaying next call, current Index is: " + currentIndex + " isPlaying is: " + isPlaying )
console.log("Autoplaying next call, current Index is: " + currentIndex + " isPlaying is: " + isPlaying)
setCurrentCall(nextCall);
dispatch(playedCall(nextCall._id));
setIsPlaying(true);
} else if (backgroundAutoplay) {
// there are no more calls to play, but we want to keep playing something so the webpage is not put to sleep, so we play silence!
setSilenceCount(silenceCount+1);
setSilenceCount(silenceCount + 1);
} else {
// there are no more calls to play and backgroundAutoplay is disabled
setIsPlaying(false);
@@ -132,20 +136,20 @@ function CallPlayer(props) {
}, [loadOlderInView]);
// This handles when a Call is specified in the URI
useEffect(() => {
if (props.initialCallId && callsData) {
const call = callsData.entities[props.initialCallId];
if (call && !selectedInitialCall) {
const time = new Date(call.time);
useEffect(() => {
if (props.initialCallId && callsData) {
const call = callsData.entities[props.initialCallId];
if (call && !selectedInitialCall) {
const time = new Date(call.time);
console.log("Playing Initial Call: " + call._id + " Start Time: " + time.toLocaleTimeString() );
setIsPlaying(true);
setCurrentCall(call);
setSelectedInitialCall(true); // signals that it has selected the initial call - you only want to do this once
dispatch(playedCall(call._id));
console.log("Playing Initial Call: " + call._id + " Start Time: " + time.toLocaleTimeString());
setIsPlaying(true);
setCurrentCall(call);
setSelectedInitialCall(true); // signals that it has selected the initial call - you only want to do this once
dispatch(playedCall(call._id));
}
}
}
}, [props.initialCallId, callsData])
}, [props.initialCallId, callsData])
// Triggered when a new call is selected in the parent component.
// This happens when a new call comes over the socket
@@ -153,13 +157,13 @@ useEffect(() => {
// we should only set the selectCallId to be the current call when AutoPlay is selected
// and when there isn't another call already playing
useEffect(() => {
console.log("New selected call: " + selectCallId + " IsPlaying: " + isPlaying + " autoPlay: " + autoPlay );
console.log("New selected call: " + selectCallId + " IsPlaying: " + isPlaying + " autoPlay: " + autoPlay);
if (selectCallId && callsData && !isPlaying && autoPlay) {
const call = callsData.entities[selectCallId];
if (call) {
const time = new Date(call.time);
console.log("Playing Selected Call: " + call._id + " Start Time: " + time.toLocaleTimeString() );
console.log("Playing Selected Call: " + call._id + " Start Time: " + time.toLocaleTimeString());
setIsPlaying(true);
setCurrentCall(call);
dispatch(playedCall(call._id));
@@ -169,7 +173,7 @@ useEffect(() => {
useEffect(() => {
if (system && system.status && system.status.length > 0) {
setStatusVisible(true);
setStatusVisible(true);
}
}, [system])
@@ -177,13 +181,17 @@ useEffect(() => {
setStatusVisible(false);
}
let autoplayClassName = "button-item";
if (autoPlay) {
autoplayClassName += " active";
}
return (
<div ref={positionRef}>
<Container className="main" >
{statusVisible &&
<Sticky offset={60} context={positionRef}>
<Sticky offset={60} context={positionRef}>
<Message floating onDismiss={handleStatusDismiss} warning> <MessageHeader>System Status</MessageHeader>{system.status}</Message>
</Sticky>
</Sticky>
}
<Sidebar.Pushable>
<Sidebar.Pusher
@@ -191,7 +199,7 @@ useEffect(() => {
>
<div ref={loadNewerRef} />
<ListCalls callsData={callsData} activeCallId={currentCallId} talkgroups={talkgroupsData ? talkgroupsData.talkgroups : false} playCall={playCall} />
<div ref={loadOlderRef} style={{ height: 50 }} />
</Sidebar.Pusher>
@@ -203,19 +211,41 @@ useEffect(() => {
</Rail>
</Container>
<Menu fixed="bottom" compact inverted >
<Menu.Item active={autoPlay} onClick={() => handleAutoPlay(autoPlay)}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
<Menu fixed="bottom" inverted vertical fluid>
<div className="item-container">
<div className={autoplayClassName} onClick={() => handleAutoPlay(autoPlay)}><Icon name="level up" /><span className="desktop-only">Autoplay</span></div>
<div className="mediaplayer">
<MediaPlayer call={currentCall} playSilence={silenceCount} onEnded={callEnded} onPlayPause={handlePlayPause} />
</div>
{/* <div className="button-item desktop-only"><SupportModal trigger={<Button color='grey' animated='fade' size={"small"}>
<ButtonContent visible color="red">
<Icon name='heart' /> Donate
</ButtonContent>
<ButtonContent hidden>Thank You</ButtonContent>
</Button>} /></div> */}
<div className="link-item desktop-only" >
<a href={callDownload}><Icon name="download" />Download</a>
</div>
<div className="link-item desktop-only">
<a href={callLink}><Icon name="at" />Link</a>
</div>
</div>
{/* <Menu.Item active={autoPlay} onClick={() => handleAutoPlay(autoPlay)}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
<MediaPlayer call={currentCall} playSilence={silenceCount} onEnded={callEnded} onPlayPause={handlePlayPause} />
<Menu.Menu position="right" className="desktop-only">
<Menu.Item><SupportModal trigger={<Button color='grey' animated='fade'>
<ButtonContent visible color="red">
<Icon name='heart' /> Donate
</ButtonContent>
<ButtonContent hidden>Thank You</ButtonContent>
</Button>} /></Menu.Item>
<ButtonContent visible color="red">
<Icon name='heart' /> Donate
</ButtonContent>
<ButtonContent hidden>Thank You</ButtonContent>
</Button>} /></Menu.Item>
<Menu.Item><a href={callDownload}><Icon name="download" />Download</a></Menu.Item>
<Menu.Item><a href={callLink}><Icon name="at" />Link</a></Menu.Item>
</Menu.Menu>
</Menu.Menu> */}
</Menu>
</div>

View File

@@ -72,7 +72,7 @@ function GroupModal(props) {
return (
<Modal open={props.open} onClose={onClose} size='tiny'>
<Modal open={props.open} onClose={() => handleDone(onClose)} size='tiny'>
<Modal.Header>{system && system.name}</Modal.Header>
<Modal.Content >

View File

@@ -0,0 +1,94 @@
.mediaplayer-container {
/* We first create a flex layout context */
display: flex;
padding: 0;
margin: 0;
list-style: none;
}
.item-container {
/* We first create a flex layout context */
display: flex;
padding: 0;
margin: 0;
list-style: none;
}
.label-item {
padding: 9px 2px 5px 2px;
width: 130px;
color: white;
font-weight: bold;
text-align: center;
}
.mediaplayer {
flex-grow: 1;
color: white;
font-weight: bold;
text-align: center;
}
.mediaplayer-item {
padding: 9px 5px 5px 5px;
flex-grow: 1;
color: white;
font-weight: bold;
text-align: center;
}
.link-item {
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
width: 100px;
color: white;
font-weight: bold;
text-align: center;
}
.button-item {
cursor:pointer;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
width: 100px;
color: white;
font-weight: bold;
}
.active{
background-color: #5c5c5c;
}
.link-item a {
color: white;
text-align: center;
}
.button-item a {
color: white;
text-align: center;
}
@media only screen and (max-width: 767px) {
.button-item {
width: 40px;
color: white;
}
.label-item {
width: 50px;
color: white;
}
}
i.icon {
font-size: inherit;
}

View File

@@ -1,71 +1,42 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import {
Menu,
Icon,
Progress,
Label
Label,
Grid,
GridRow,
GridColumn,
LabelGroup,
Button
} from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player'
import WavesurferPlayer from '@wavesurfer/react'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.js';
import { is } from "date-fns/locale";
import "./MediaPlayer.css";
const MediaPlayer = (props) => {
const audioRef = React.createRef();
const [sourceIndex, setSourceIndex] = useState(0);
const [playProgress, setPlayProgress] = useState(0);
const [playTime, setPlayTime] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const parentHandlePlayPause = props.onPlayPause
const playSilence = props.playSilence;
const handlePause = () => { setIsPlaying(false); }
const handlePlay = () => { setIsPlaying(true); }
const playPause = () => {
const audio = audioRef.current.audioEl.current;
if (isPlaying) {
setIsPlaying(false);
parentHandlePlayPause(false);
audio.pause();
} else {
setIsPlaying(true);
parentHandlePlayPause(true);
audio.play();
}
}
const call = props.call;
const [sourceIndex, setSourceIndex] = useState(0);
const [wavesurfer, setWavesurfer] = useState(null)
const [isPlaying, setIsPlaying] = useState(false)
const [playTime, setPlayTime] = useState(0);
const playSilence = props.playSilence;
const parentHandlePlayPause = props.onPlayPause
const regionsPlugin = useMemo(() => RegionsPlugin.create(), []);
const plugins = useMemo(() => [regionsPlugin], [regionsPlugin]);
useEffect(() => {
const audio = audioRef.current.audioEl.current;
const onEnded = props.onEnded;
setSourceIndex(0);
if (call ) {
audio.src = call.url;
const playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function () {
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
handlePause();
//onEnded();
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
}
}, [call]);
useEffect(() => {
const audio = audioRef.current.audioEl.current;
setSourceIndex(0);
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: "Waiting for Call...",
@@ -77,44 +48,82 @@ const MediaPlayer = (props) => {
]
});
}
audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";
const playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function () {
wavesurfer && wavesurfer.load("/silence.m4a");
regionsPlugin.clearRegions();
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
// // In browsers that don’t yet support this functionality,
// // playPromise won’t be defined.
// if (playPromise !== undefined) {
// playPromise.then(function () {
// }).catch(function (error) {
// console.log("Automatic playback failed: " + error);
// // Show a UI element to let the user manually start playback.
// });
// } else {
// audio.src = false;
// }
}, [playSilence]);
const updatePlayProgress = () => {
const audio = audioRef.current.audioEl.current;
const { currentTime, duration } = audio;
useEffect(() => {
setSourceIndex(0);
}, [call]);
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) {
setSourceIndex(sourceIndex + 1);
const onReady = (ws) => {
setWavesurfer(ws)
setIsPlaying(false)
regionsPlugin.clearRegions();
if (call) {
call.srcList.forEach(src => {
regionsPlugin.addRegion({
start: src.pos,
color: "rgba(128, 128, 128, 1.0)",
drag: false,
resize: false
});
});
}
}
// updates the play percentage progress and current playing time
setPlayProgress(currentTime / duration * 100);
setPlayTime(Math.floor(currentTime));
const onPlay = () => {
setIsPlaying(true);
parentHandlePlayPause(true);
}
const onPause = () => {
setIsPlaying(false);
parentHandlePlayPause(false);
}
const onPlayPause = () => {
wavesurfer && wavesurfer.playPause()
}
const updatePlayProgress = () => {
if (wavesurfer && wavesurfer.isPlaying()) {
var totalTime = wavesurfer.getDuration(),
currentTime = wavesurfer.getCurrentTime(),
remainingTime = totalTime - currentTime;
if (!isPlaying) {
setIsPlaying(true);
}
//console.log("totalTime: " + totalTime + " currentTime: " + currentTime + " remainingTime: " + remainingTime);
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) {
setSourceIndex(sourceIndex + 1);
console.log("sourceIndex: " + sourceIndex);
}
setPlayTime(Math.floor(currentTime));
}
}
let playEnabled = { "disabled": true }
let sourceId = "-";
@@ -125,40 +134,202 @@ const MediaPlayer = (props) => {
}
playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={audioRef}
onPause={handlePause}
onPlay={handlePlay}
listenInterval={100}
onListen={updatePlayProgress}
onEnded={props.onEnded}
autoPlay
/>
<Menu.Item onClick={playPause} >
<div className="mediaplayer-container">
{
isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={playProgress} />
<Label color="black">
{playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
<div className="button-item" onClick={onPlayPause}>
{
isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</div>
<div className="mediaplayer-item">
<WavesurferPlayer
autoplay={true}
height={25}
barWidth ={3}
barGap={3}
barRadius={6}
waveColor="#E81B39"
url={call.url}
onReady={onReady}
onPlay={onPlay}
onPause={onPause}
onAudioprocess={updatePlayProgress}
onFinish={props.onEnded}
plugins={plugins}
/>
</div>
<div className="label-item">
<LabelGroup size="small" >
<Label color="black">
{playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</LabelGroup>
</div>
</div>
)
/*
const audioRef = React.createRef();
const [sourceIndex, setSourceIndex] = useState(0);
const [playProgress, setPlayProgress] = useState(0);
const [playTime, setPlayTime] = useState(0);
const parentHandlePlayPause = props.onPlayPause
const playSilence = props.playSilence;
const handlePause = () => { setIsPlaying(false); }
const handlePlay = () => { setIsPlaying(true); }
const playPause = () => {
const audio = audioRef.current.audioEl.current;
if (isPlaying) {
setIsPlaying(false);
parentHandlePlayPause(false);
audio.pause();
} else {
setIsPlaying(true);
parentHandlePlayPause(true);
audio.play();
}
}
const call = props.call;
useEffect(() => {
const audio = audioRef.current.audioEl.current;
const onEnded = props.onEnded;
setSourceIndex(0);
if (call ) {
audio.src = call.url;
const playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function () {
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
handlePause();
//onEnded();
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
}
}, [call]);
useEffect(() => {
const audio = audioRef.current.audioEl.current;
setSourceIndex(0);
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: "Waiting for Call...",
album: 'OpenMHz',
artwork: [
{ src: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{ src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/android-chrome-192x192.png', sizes: '512x512', type: 'image/png' },
]
});
}
audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";
const playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function () {
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
}, [playSilence]);
const updatePlayProgress = () => {
const audio = audioRef.current.audioEl.current;
const { currentTime, duration } = audio;
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) {
setSourceIndex(sourceIndex + 1);
}
// updates the play percentage progress and current playing time
setPlayProgress(currentTime / duration * 100);
setPlayTime(Math.floor(currentTime));
}
let playEnabled = { "disabled": true }
let sourceId = "-";
if (call) {
if (call.srcList.length > sourceIndex) {
sourceId = call.srcList[sourceIndex].src;
}
playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={audioRef}
onPause={handlePause}
onPlay={handlePlay}
listenInterval={100}
onListen={updatePlayProgress}
onEnded={props.onEnded}
autoPlay
/>
<Menu.Item onClick={playPause} >
{
isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={playProgress} />
<Label color="black">
{playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
)*/
}
export default MediaPlayer;