Skip to content

Commit

Permalink
refactor(webapp): use react hooks whip-player
Browse files Browse the repository at this point in the history
  • Loading branch information
a-wing committed Jan 14, 2024
1 parent 4d6d8a5 commit aa5e9b3
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 152 deletions.
173 changes: 21 additions & 152 deletions webapp/components/player/whip-player.tsx
Original file line number Diff line number Diff line change
@@ -1,193 +1,62 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect } from "react"
import useWhipClient from "../use/whip"
import Player from './player'
import { useAtom } from 'jotai'
import {
localStreamAtom,
presentationStreamAtom,

localUserStatusAtom,

currentDeviceAudioAtom,
currentDeviceVideoAtom,
} from '../../store/atom'
import Player from './player'
import { WHIPClient } from '@binbat/whip-whep/whip'
import { deviceScreen } from '../../lib/device'
import SvgProgress from '../svg/progress'

export default function WhipPlayer(props: { streamId: string, width: string }) {
const refEnabled = useRef(false)
const refTimer = useRef<ReturnType<typeof setInterval> | null>(null)
const refPC = useRef<RTCPeerConnection | null>(null)
const refClient = useRef<WHIPClient | null>(null)
// TODO: Need fix
// - name
// - audio
// - video
const [localUserStatus] = useAtom(localUserStatusAtom)
const [localStream] = useAtom(localStreamAtom)
const [localUserStatus, setLocalUserStatus] = useAtom(localUserStatusAtom)
const refUserStatus = useRef(localUserStatus)

const [loading, setLoading] = useState(true)
const { userStatus, setCurrentDeviceAudio, setCurrentDeviceVideo, restart } = useWhipClient(localUserStatus.name, props.streamId, localStream.stream)

const [currentDeviceAudio] = useAtom(currentDeviceAudioAtom)
const [currentDeviceVideo] = useAtom(currentDeviceVideoAtom)

const [presentationStream, setPresentationStream] = useAtom(presentationStreamAtom)
refUserStatus.current = localUserStatus

const newPeerConnection = () => {
const stream = localStream.stream
if (stream) {
const pc = new RTCPeerConnection()
pc.onconnectionstatechange = () => setLocalUserStatus({
...localUserStatus,
state: pc.connectionState
})

// NOTE: array audio index is: 0
if (!stream.getAudioTracks().length) {
pc.addTransceiver('audio', { 'direction': 'sendonly' })
} else {
stream.getAudioTracks().map(track => pc.addTrack(track))
}

// NOTE: array video index is: 1
if (!stream.getVideoTracks().length) {
pc.addTransceiver('video', { 'direction': 'sendonly' })
} else {
stream.getVideoTracks().map(track => pc.addTrack(track))
}

//pc.addTransceiver(stream.getVideoTracks()[0], {
// direction: 'sendonly',
// //sendEncodings: [
// // { rid: 'a', scaleResolutionDownBy: 2.0 },
// // { rid: 'b', scaleResolutionDownBy: 1.0, },
// // { rid: 'c' }
// //]
//})

refPC.current = pc
}
}

const start = async (resource: string) => {
const stream = localStream.stream
if (stream) {
if (refPC.current) {
refUserStatus.current.state = 'signaled'
try {
const whip = new WHIPClient();
const url = location.origin + `/whip/${resource}`
await whip.publish(refPC.current, url)
refClient.current = whip
} catch (e) {
console.log(e)
refUserStatus.current.state = 'failed'
}
}
}
setLoading(false)
}

const restart = async (resource: string) => {
setLoading(true)
if (refPC.current) {
refPC.current.close()
}
newPeerConnection()
start(resource)
}

const run = () => refUserStatus.current.state !== "connected" && refUserStatus.current.state !== "signaled" ? restart(props.streamId) : null

const init = () => {
if (!!localStream.stream.getTracks().length) {
if (!refEnabled.current) {
refEnabled.current = true
refTimer.current = setInterval(run, 5000)
newPeerConnection()
start(props.streamId)
}
} else {
if (refEnabled.current) {
// TODO: live777 need remove `If-Match`
//refClient.current?.stop()
//console.log("should closed whip")
}
}
}
useEffect(() => {
init()
return () => {
if (refEnabled.current && refClient.current) {
clearInterval(refTimer.current!)
refTimer.current = null
refClient.current.stop()
refClient.current = null
refEnabled.current = false
}
}
}, [])

useEffect(() => {
const mediaStream = localStream.stream
// If WebRTC is connected, switch track
// NOTE: array audio index is: 0
refPC.current?.getSenders().filter((_, i) => i === 0).map(sender => {
if (mediaStream) {
mediaStream.getAudioTracks().map(track => sender.replaceTrack(track))
}
})
init()
setCurrentDeviceAudio(currentDeviceAudio)
}, [currentDeviceAudio])

useEffect(() => {
if (currentDeviceVideo === deviceScreen.deviceId) {
setPresentationStream(localStream)
} else {
setPresentationStream({
stream: new MediaStream,
name: presentationStream.name
})
}

const mediaStream = localStream.stream
// If WebRTC is connected, switch track
// NOTE: array video index is: 1
refPC.current?.getSenders().filter((_, i) => i === 1).map(sender => {
if (mediaStream) {
mediaStream.getVideoTracks().map(track => sender.replaceTrack(track))
}
})
init()
setCurrentDeviceVideo(currentDeviceVideo)
}, [currentDeviceVideo])

return (
<div className='flex flex-col'>
<center>
{ loading
? <div className='m-xl'><SvgProgress/></div>
: <Player user={localStream} muted={true} width={props.width} display="auto" />
}
<Player user={localStream} muted={true} width={props.width} display="auto" />
</center>

<details className='text-white mx-2 text-sm font-border' style={{
position: 'absolute',
}}>
<summary className='text-center'>{localUserStatus.name}</summary>
<summary className='text-center'>{userStatus.name}</summary>
<center>
<div className='flex flex-row flex-wrap justify-around'>
<p>name: <code>{localUserStatus.name}</code></p>
<p>state: <code>{String(localUserStatus.state)}</code></p>
<p>name: <code>{userStatus.name}</code></p>
<p>state: <code>{String(userStatus.state)}</code></p>
</div>
<div className='flex flex-row flex-wrap justify-around'>
<p>audio: <code>{String(localUserStatus.audio)}</code></p>
<p>video: <code>{String(localUserStatus.video)}</code></p>
<p>screen: <code>{String(localUserStatus.screen)}</code></p>
<p>audio: <code>{String(userStatus.audio)}</code></p>
<p>video: <code>{String(userStatus.video)}</code></p>
<p>screen: <code>{String(userStatus.screen)}</code></p>
</div>

<code>{props.streamId}</code>
</center>

<center className='text-white flex flex-row justify-around'>
<p className='rounded-xl p-2 b-1 hover:border-orange-300'>{localUserStatus.state}</p>
<button className='btn-primary' disabled={localUserStatus.state === 'connected'} onClick={() => restart(props.streamId)}>restart</button>
<p className='rounded-xl p-2 b-1 hover:border-orange-300'>{userStatus.state}</p>
<button className='btn-primary' onClick={() => restart()}>restart</button>
</center>
</details>
</div>
Expand Down
137 changes: 137 additions & 0 deletions webapp/components/use/whip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useSyncExternalStore } from 'react'
import { UserStatus } from '../../store/atom'
import { WHIPClient } from '@binbat/whip-whep/whip'
import { deviceNone } from '../../lib/device'

interface Context {
id: string
pc: RTCPeerConnection
client: WHIPClient
stream: MediaStream
userStatus: UserStatus

currentDeviceAudio: string
currentDeviceVideo: string

setCurrentDeviceAudio: (_: string) => void
setCurrentDeviceVideo: (_: string) => void

sync: () => void
restart: () => void
}

const userStatus: UserStatus = {
name: "",
state: "",
audio: true,
video: true,
screen: false,
}

function newPeerConnection() {
const { pc, stream } = getContext()

// NOTE: array audio index is: 0
if (!stream.getAudioTracks().length) {
pc.addTransceiver('audio', { 'direction': 'sendonly' })
} else {
stream.getAudioTracks().map(track => pc.addTrack(track))
}

// NOTE: array video index is: 1
if (!stream.getVideoTracks().length) {
pc.addTransceiver('video', { 'direction': 'sendonly' })
} else {
stream.getVideoTracks().map(track => pc.addTrack(track))
}
}

const context: Context = {
id: "",
pc: new RTCPeerConnection(),
client: new WHIPClient(),
stream: new MediaStream(),
userStatus: userStatus,

currentDeviceAudio: deviceNone.deviceId,
currentDeviceVideo: deviceNone.deviceId,

setCurrentDeviceAudio: setCurrentDeviceAudio,
setCurrentDeviceVideo: setCurrentDeviceVideo,

sync: () => {},
restart: restart,
}

function getContext() {
return context
}

function setCurrentDeviceAudio(deviceId: string) {
const { pc, stream } = context
// If WebRTC is connected, switch track
// NOTE: array audio index is: 0
pc.getSenders().filter((_, i) => i === 0).map(sender => {
if (stream) {
stream.getAudioTracks().map(track => sender.replaceTrack(track))
}
})
}

function setCurrentDeviceVideo(deviceId: string) {
const { pc, stream } = context
// If WebRTC is connected, switch track
// NOTE: array video index is: 1
pc.getSenders().filter((_, i) => i === 1).map(sender => {
if (stream) {
stream.getVideoTracks().map(track => sender.replaceTrack(track))
}
})
}

async function start() {
const { id, pc, client, userStatus, sync } = context
pc.onconnectionstatechange = () => {
userStatus.state = pc.connectionState
sync()
}
userStatus.state = 'signaled'
newPeerConnection()

try {
const url = location.origin + `/whip/${id}`
await client.publish(pc, url)
} catch (e) {
console.log(e)
userStatus.state = 'failed'
}
}

async function restart() {
context.client.stop()
context.pc = new RTCPeerConnection()
start()
}

function run() {
console.log("=== RUN ===")
const { userStatus } = context
if (userStatus.state === "") start()
}

function subscribe(callback: () => void) {
context.sync = callback
console.log("subscribe")
const timer = setInterval(run, 3000)
return () => {
clearInterval(timer)
console.log("subscribe end")
}
}

export default function useWhipClient(name: string, streamId: string, stream: MediaStream) {
context.id = streamId
context.stream = stream
context.userStatus.name = name
return useSyncExternalStore(subscribe, getContext)
}

0 comments on commit aa5e9b3

Please sign in to comment.