Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speaker button #22

Merged
merged 11 commits into from
Nov 21, 2024
37 changes: 37 additions & 0 deletions webapp/components/device.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import useWhipClient from './use/whip'
import { useAtom } from 'jotai'
import { useEffect, useState } from 'react'
import {
Device,
deviceNone,
deviceScreen,
} from '../lib/device'
import { deviceSpeakerAtom } from './../store/atom'

import Loading from './svg/loading'
import SvgSpeaker from './svg/speaker'
import SvgAudio from './svg/audio'
import SvgVideo from './svg/video'
import { SvgPresentCancel, SvgPresentToAll } from './svg/present'
Expand All @@ -24,10 +27,13 @@ export default function DeviceBar(props: { streamId: string }) {
const [permissionAudio, setPermissionAudio] = useState('')
const [permissionVideo, setPermissionVideo] = useState('')

const [loadingSpeaker, setLoadingSpeaker] = useState(false)
const [loadingAudio, setLoadingAudio] = useState(false)
const [loadingVideo, setLoadingVideo] = useState(false)
const [loadingScreen, setLoadingScreen] = useState(false)

const [currentDeviceSpeaker, setCurrentDeviceSpeaker] = useAtom(deviceSpeakerAtom)

const {
userStatus,
currentDeviceAudio,
Expand All @@ -38,6 +44,7 @@ export default function DeviceBar(props: { streamId: string }) {
toggleEnableVideo,
} = useWhipClient(props.streamId)

const [deviceSpeaker, setDeviceSpeaker] = useState<Device[]>([deviceNone])
const [deviceAudio, setDeviceAudio] = useState<Device[]>([deviceNone])
const [deviceVideo, setDeviceVideo] = useState<Device[]>([deviceNone])

Expand Down Expand Up @@ -82,9 +89,15 @@ export default function DeviceBar(props: { streamId: string }) {

const devices = (await navigator.mediaDevices.enumerateDevices()).filter(i => !!i.deviceId)

const speakers = devices.filter(i => i.kind === 'audiooutput').map(toDevice)
const audios = devices.filter(i => i.kind === 'audioinput').map(toDevice)
const videos = devices.filter(i => i.kind === 'videoinput').map(toDevice)

if (currentDeviceSpeaker === deviceNone.deviceId) {
const device = speakers[0]
if (device) setCurrentDeviceSpeaker(device.deviceId)
}

if (currentDeviceAudio === deviceNone.deviceId) {
const device = audios[0]
if (device) await setCurrentDeviceAudio(device.deviceId)
Expand All @@ -95,6 +108,7 @@ export default function DeviceBar(props: { streamId: string }) {
if (device) await setCurrentDeviceVideo(device.deviceId)
}

setDeviceSpeaker([...speakers])
setDeviceAudio([...audios])
setDeviceVideo([...videos, deviceScreen])
}
Expand Down Expand Up @@ -130,6 +144,12 @@ export default function DeviceBar(props: { streamId: string }) {
return () => { navigator.mediaDevices.removeEventListener('devicechange', updateDeviceList) }
}, [])

const onChangedDeviceSpeaker = async (current: string) => {
setLoadingSpeaker(true)
setCurrentDeviceSpeaker(current)
setLoadingSpeaker(false)
}

const onChangedDeviceAudio = async (current: string) => {
setLoadingAudio(true)
await setCurrentDeviceAudio(current)
Expand All @@ -151,6 +171,23 @@ export default function DeviceBar(props: { streamId: string }) {
return (
<div className="flex flex-row flex-wrap justify-around p-xs">
<center className="flex flex-row flex-wrap justify-around">
<section className="m-1 p-1 flex flex-row justify-center rounded-md border-1 border-indigo-500">
<button className="text-rose-400 rounded-md w-8 h-8" onClick={async () => {
}}>
<center>{ loadingSpeaker ? <Loading/> : <SvgSpeaker/> }</center>
</button>
<div className="flex flex-col justify-between w-1 pointer-events-none"></div>
<select
className="w-3.5 h-8 rounded-sm rotate-180"
value={currentDeviceSpeaker}
onChange={e => onChangedDeviceSpeaker(e.target.value)}
>
{deviceSpeaker.map(device =>
<option key={device.deviceId} value={device.deviceId}>{device.label}</option>
)}
</select>
</section>

<section className="m-1 p-1 flex flex-row justify-center rounded-md border-1 border-indigo-500">
<button className="text-rose-400 rounded-md w-8 h-8" onClick={async () => {
setLoadingAudio(true)
Expand Down
9 changes: 8 additions & 1 deletion webapp/components/player/player.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useEffect, useRef, useState } from 'react'
import { useAtom } from 'jotai'
import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record'
import { isWechat } from '../../lib/util'
import SvgProgress from '../svg/progress'
import { deviceSpeakerAtom } from '../../store/atom'

function AudioWave(props: { stream: MediaStream }) {
const refWave = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -34,6 +36,7 @@ export default function Player(props: { stream: MediaStream, muted: boolean, aud
const [showAudio, setShowAudio] = useState(false)
const audioTrack = props.stream.getAudioTracks()[0]
const videoTrack = props.stream.getVideoTracks()[0]
const [currentDeviceSpeaker] = useAtom(deviceSpeakerAtom)

useEffect(() => {
if (audioTrack && !videoTrack) {
Expand All @@ -44,6 +47,10 @@ export default function Player(props: { stream: MediaStream, muted: boolean, aud
if (audioTrack && props.audio) {
const el = document.createElement('audio')
el.srcObject = new MediaStream([audioTrack])

if (el.setSinkId) {
a-wing marked this conversation as resolved.
Show resolved Hide resolved
el.setSinkId(currentDeviceSpeaker)
}
el.play()

return () => {
Expand All @@ -52,7 +59,7 @@ export default function Player(props: { stream: MediaStream, muted: boolean, aud
el.remove()
}
}
}, [audioTrack, videoTrack])
}, [audioTrack, videoTrack, currentDeviceSpeaker])

useEffect(() => {
if (refVideo.current && videoTrack) {
Expand Down
7 changes: 7 additions & 0 deletions webapp/components/svg/speaker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function SvgSpeaker() {
return (
<svg focusable="false" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M4 7h4l5-5v20l-5-5H4c-1.1 0-2-.9-2-2V9c0-1.1.9-2 2-2zm11.54 3.88l1.41-1.41c1.78 1.78 1.78 4.66 0 6.44l-1.41-1.41c1.17-1.17 1.17-3.07 0-4.24zm2.83-2.83l1.41-1.41c3.12 3.12 3.12 8.19 0 11.31l-1.41-1.41c2.34-2.34 2.34-6.13 0-8.49z"></path>
</svg>
)
}
1 change: 1 addition & 0 deletions webapp/components/use/whxp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Context extends EventTarget {
userStatus: Stream = {
name: '',
state: StreamState.New,
speaker: true,
a-wing marked this conversation as resolved.
Show resolved Hide resolved
audio: true,
video: true,
screen: false,
Expand Down
1 change: 1 addition & 0 deletions webapp/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum StreamState {
interface Stream {
name: string,
state: StreamState
speaker: boolean,
a-wing marked this conversation as resolved.
Show resolved Hide resolved
audio: boolean,
video: boolean,
screen: boolean,
Expand Down
4 changes: 4 additions & 0 deletions webapp/store/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface UserStatus {
*/
name: string
state: StreamState
speaker: boolean
a-wing marked this conversation as resolved.
Show resolved Hide resolved
audio: boolean
video: boolean
screen: boolean
Expand All @@ -35,13 +36,16 @@ presentationStreamAtom.debugLabel = 'presentationStream'
const enabledPresentationAtom = atom(get => get(presentationStreamAtom).stream.getVideoTracks().length !== 0)
enabledPresentationAtom.debugLabel = 'enabledPresentation'

const deviceSpeakerAtom = atom<string>('default')
a-wing marked this conversation as resolved.
Show resolved Hide resolved

export {
locationAtom,
presentationStreamAtom,

meetingIdAtom,
meetingJoinedAtom,
enabledPresentationAtom,
deviceSpeakerAtom,
}

export type {
Expand Down