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
35 changes: 35 additions & 0 deletions webapp/components/device.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../lib/device'

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,20 +25,24 @@ 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 {
userStatus,
currentDeviceSpeaker,
currentDeviceAudio,
currentDeviceVideo,
setCurrentDeviceSpeaker,
setCurrentDeviceAudio,
setCurrentDeviceVideo,
toggleEnableAudio,
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 +87,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) {
a-wing marked this conversation as resolved.
Show resolved Hide resolved
const device = speakers[0]
if (device) await setCurrentDeviceSpeaker(device.deviceId)
}

if (currentDeviceAudio === deviceNone.deviceId) {
const device = audios[0]
if (device) await setCurrentDeviceAudio(device.deviceId)
Expand All @@ -95,6 +106,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 +142,12 @@ export default function DeviceBar(props: { streamId: string }) {
return () => { navigator.mediaDevices.removeEventListener('devicechange', updateDeviceList) }
}, [])

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

const onChangedDeviceAudio = async (current: string) => {
setLoadingAudio(true)
await setCurrentDeviceAudio(current)
Expand All @@ -151,6 +169,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
2 changes: 1 addition & 1 deletion webapp/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function Layout(props: { meetingId: string }) {
<div></div>

{enabledPresentation
? <Player stream={presentationStream.stream} muted={true} width="auto" />
? <Player stream={presentationStream.stream} muted={true} width="auto" currentDeviceSpeaker={'none'} />
a-wing marked this conversation as resolved.
Show resolved Hide resolved
: null
}

Expand Down
8 changes: 6 additions & 2 deletions webapp/components/player/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function AudioWave(props: { stream: MediaStream }) {
return <div ref={refWave}></div>
}

export default function Player(props: { stream: MediaStream, muted: boolean, audio?: boolean, video?: boolean, width: string }) {
export default function Player(props: { stream: MediaStream, muted: boolean, audio?: boolean, video?: boolean, width: string, currentDeviceSpeaker: string}) {
const refVideo = useRef<HTMLVideoElement>(null)
const [showAudio, setShowAudio] = useState(false)
const audioTrack = props.stream.getAudioTracks()[0]
Expand All @@ -44,6 +44,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(props.currentDeviceSpeaker)
a-wing marked this conversation as resolved.
Show resolved Hide resolved
}
el.play()

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

useEffect(() => {
if (refVideo.current && videoTrack) {
Expand Down
6 changes: 5 additions & 1 deletion webapp/components/player/whep-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import Detail from './detail'
import Player from './player'
import { presentationStreamAtom } from '../../store/atom'
import { Stream } from '../../lib/api'
import useWhipClient from '../use/whip'
a-wing marked this conversation as resolved.
Show resolved Hide resolved
import {getStorageStream } from '../../lib/storage'

export default function WhepPlayer(props: { streamId: string, userStatus: Stream, width: string }) {
const refEnabled = useRef(false)
const localStreamId = getStorageStream()
const { currentDeviceSpeaker} = useWhipClient(localStreamId)
const { stream, restart, start, connStatus, setRemoteStatus } = useWhepClient(props.streamId)
const [, setPresentationStream] = useAtom(presentationStreamAtom)

Expand All @@ -31,7 +35,7 @@ export default function WhepPlayer(props: { streamId: string, userStatus: Stream

return (
<center className="flex flex-col">
<Player stream={stream} muted={false} width={props.width} audio={true} video={props.userStatus.video} />
<Player stream={stream} muted={false} width={props.width} audio={true} video={props.userStatus.video} currentDeviceSpeaker={currentDeviceSpeaker}/>
<Detail streamId={props.streamId} connStatus={connStatus} userStatus={props.userStatus} restart={restart} />
</center>
)
Expand Down
2 changes: 1 addition & 1 deletion webapp/components/player/whip-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function WhipPlayer(props: { streamId: string, width: string }) {

return (
<center className="flex flex-col">
<Player stream={stream} muted={true} width={props.width} audio={false} video={userStatus.video} />
<Player stream={stream} muted={true} width={props.width} audio={false} video={userStatus.video} currentDeviceSpeaker={'none'}/>
<Detail streamId={props.streamId} connStatus={userStatus.state} userStatus={userStatus} restart={restart} />
</center>
)
Expand Down
4 changes: 2 additions & 2 deletions webapp/components/prepare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function Prepare(_props: { meetingId: string }) {
const localStreamId = getStorageStream()
const [_, setMeetingJoined] = useAtom(meetingJoinedAtom)

const { id, stream, setUserName, setSyncUserStatus, start } = useWhipClient(localStreamId)
const { id, stream, currentDeviceSpeaker,setUserName, setSyncUserStatus, start } = useWhipClient(localStreamId)

const join = async () => {
setLoading(true)
Expand All @@ -39,7 +39,7 @@ export default function Prepare(_props: { meetingId: string }) {
return (
<div className="flex flex-col justify-around">
<center className="m-xs">
<Player stream={stream} muted={true} width="320px" audio={true} video={true} />
<Player stream={stream} muted={true} width="320px" audio={true} video={true} currentDeviceSpeaker={currentDeviceSpeaker}/>
</center>

<center className="mb-xs">
Expand Down
8 changes: 8 additions & 0 deletions webapp/components/svg/speaker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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>
)
}

20 changes: 20 additions & 0 deletions webapp/components/use/whip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ interface WHIPData extends Data {
setUserName: (name: string) => void,
setSyncUserStatus: (callback: (userStatus: Stream) => void) => void,

currentDeviceSpeaker: string,
currentDeviceAudio: string,
currentDeviceVideo: string,
setCurrentDeviceSpeaker: (current: string) => Promise<void>,
setCurrentDeviceAudio: (current: string) => Promise<void>,
setCurrentDeviceVideo: (current: string) => Promise<void>,
toggleEnableSpeaker: () => Promise<void>,
toggleEnableAudio: () => Promise<void>,
toggleEnableVideo: () => Promise<void>,
}
Expand All @@ -25,8 +28,10 @@ class WHIPContext extends Context {
client: WHIPClient = new WHIPClient()
cache: WHIPData

currentDeviceSpeaker = deviceNone.deviceId
currentDeviceAudio = deviceNone.deviceId
currentDeviceVideo = deviceNone.deviceId
toggleEnableSpeaker = async () => this.setCurrentDeviceSpeaker(this.userStatus.speaker ? deviceNone.deviceId : this.currentDeviceSpeaker)
toggleEnableAudio = async () => this.setCurrentDeviceAudio(this.userStatus.audio ? deviceNone.deviceId : this.currentDeviceAudio)
toggleEnableVideo = async () => this.setCurrentDeviceVideo(this.userStatus.video ? deviceNone.deviceId : this.currentDeviceVideo)

Expand Down Expand Up @@ -64,10 +69,13 @@ class WHIPContext extends Context {
setUserName: (name: string) => this.setUserName(name),
setSyncUserStatus: (callback: (userStatus: Stream) => void) => this.setSyncUserStatus(callback),

currentDeviceSpeaker: this.currentDeviceSpeaker,
currentDeviceAudio: this.currentDeviceAudio,
currentDeviceVideo: this.currentDeviceVideo,
setCurrentDeviceSpeaker: (current: string) => this.setCurrentDeviceSpeaker(current),
setCurrentDeviceAudio: (current: string) => this.setCurrentDeviceAudio(current),
setCurrentDeviceVideo: (current: string) => this.setCurrentDeviceVideo(current),
toggleEnableSpeaker: () => this.toggleEnableSpeaker(),
toggleEnableAudio: () => this.toggleEnableAudio(),
toggleEnableVideo: () => this.toggleEnableVideo(),
}
Expand Down Expand Up @@ -98,6 +106,18 @@ class WHIPContext extends Context {
}
}

async setCurrentDeviceSpeaker(current: string) {
a-wing marked this conversation as resolved.
Show resolved Hide resolved
const { userStatus, currentDeviceSpeaker } = this

if (current !== currentDeviceSpeaker || !userStatus.speaker) {
userStatus.speaker = current === deviceNone.deviceId ? false : true
this.currentDeviceSpeaker = current === deviceNone.deviceId ? this.currentDeviceSpeaker : current

this.sync()
this.syncUserStatus(userStatus)
}
}

onChangedDeviceAudio() {
const { pc, stream } = this
// If WebRTC is connected, switch track
Expand Down
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
1 change: 1 addition & 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 Down