1+ import React , { useEffect , useRef , useState } from 'react'
2+ import { useLocation } from 'react-router-dom'
3+ import ErrorHeader from '../error-header/errorHeader'
4+
5+ const mediaOptions = {
6+ audio : true ,
7+ video : true
8+ }
9+
10+ enum ErrorMessageEnum {
11+ NoMediaDevices ,
12+ NotAllowedError ,
13+ NotFoundError
14+ }
15+
16+ function getMediaErrorMessage ( value : ErrorMessageEnum ) : string {
17+ switch ( value ) {
18+ case ErrorMessageEnum . NoMediaDevices :
19+ return `MediaDevices API was not found. Publishing in Broadcast Box requires HTTPS 👮` ;
20+ case ErrorMessageEnum . NotFoundError :
21+ return `Seems like you don't have camera 😭 Or you just blocked access to it...\nCheck camera settings, browser permissions and system permissions.` ;
22+ case ErrorMessageEnum . NotAllowedError :
23+ return `You can't publish stream using your camera, because you have blocked access to it 😞` ;
24+ default :
25+ return "Could not access your media device" ;
26+ }
27+ }
28+
29+ function BrowserBroadcaster ( ) {
30+ const videoRef = useRef < HTMLVideoElement > ( null )
31+ const location = useLocation ( )
32+ const [ mediaAccessError , setMediaAccessError ] = useState < ErrorMessageEnum | null > ( null )
33+ const [ publishSuccess , setPublishSuccess ] = useState ( false )
34+ const [ useDisplayMedia , setUseDisplayMedia ] = useState ( false )
35+ const [ peerConnectionDisconnected , setPeerConnectionDisconnected ] = useState ( false )
36+
37+ const apiPath = import . meta. env . VITE_API_PATH ;
38+
39+ useEffect ( ( ) => {
40+ const peerConnection = new RTCPeerConnection ( )
41+ let stream : MediaStream | undefined = undefined ;
42+
43+ if ( ! navigator . mediaDevices ) {
44+ setMediaAccessError ( ErrorMessageEnum . NoMediaDevices ) ;
45+ return
46+ }
47+
48+ const mediaPromise = useDisplayMedia ?
49+ navigator . mediaDevices . getDisplayMedia ( mediaOptions ) :
50+ navigator . mediaDevices . getUserMedia ( mediaOptions )
51+
52+ mediaPromise . then ( mediaStream => {
53+ if ( peerConnection . connectionState === "closed" ) {
54+ mediaStream
55+ . getTracks ( )
56+ . forEach ( mediaStreamTrack => mediaStreamTrack . stop ( ) )
57+
58+ return ;
59+ }
60+
61+ stream = mediaStream
62+ videoRef . current ! . srcObject = mediaStream
63+
64+ mediaStream
65+ . getTracks ( )
66+ . forEach ( mediaStreamTrack => {
67+ if ( mediaStreamTrack . kind === 'audio' ) {
68+ peerConnection . addTransceiver ( mediaStreamTrack , {
69+ direction : 'sendonly'
70+ } )
71+ } else {
72+ peerConnection . addTransceiver ( mediaStreamTrack , {
73+ direction : 'sendonly' ,
74+ sendEncodings : [
75+ {
76+ rid : 'high'
77+ } ,
78+ {
79+ rid : 'med' ,
80+ scaleResolutionDownBy : 2.0
81+ } ,
82+ {
83+ rid : 'low' ,
84+ scaleResolutionDownBy : 4.0
85+ }
86+ ]
87+ } )
88+ }
89+ } )
90+
91+ peerConnection . oniceconnectionstatechange = ( ) => {
92+ if ( peerConnection . iceConnectionState === 'connected' || peerConnection . iceConnectionState === 'completed' ) {
93+ setPublishSuccess ( true )
94+ setPeerConnectionDisconnected ( false )
95+ } else if ( peerConnection . iceConnectionState === 'disconnected' || peerConnection . iceConnectionState === 'failed' ) {
96+ setPublishSuccess ( false )
97+ setPeerConnectionDisconnected ( true )
98+ }
99+ }
100+
101+ peerConnection
102+ . createOffer ( )
103+ . then ( offer => {
104+ peerConnection . setLocalDescription ( offer )
105+ . catch ( ( err ) => console . error ( err ) ) ;
106+
107+ fetch ( `${ apiPath } /whip` , {
108+ method : 'POST' ,
109+ body : offer . sdp ,
110+ headers : {
111+ Authorization : `Bearer ${ location . pathname . split ( '/' ) . pop ( ) } ` ,
112+ 'Content-Type' : 'application/sdp'
113+ }
114+ } ) . then ( r => r . text ( ) )
115+ . then ( answer => {
116+ peerConnection . setRemoteDescription ( {
117+ sdp : answer ,
118+ type : 'answer'
119+ } )
120+ . catch ( ( err ) => console . error ( err ) )
121+ } )
122+ } )
123+ } , setMediaAccessError )
124+
125+ return function cleanup ( ) {
126+ peerConnection . close ( )
127+ if ( stream ) {
128+ stream
129+ . getTracks ( )
130+ . forEach ( ( streamTrack : MediaStreamTrack ) => streamTrack . stop ( ) )
131+ }
132+ }
133+ } , [ videoRef , useDisplayMedia , location . pathname ] )
134+
135+ return (
136+ < div className = 'container mx-auto' >
137+ { mediaAccessError != null && < ErrorHeader > { getMediaErrorMessage ( mediaAccessError ) } </ ErrorHeader > }
138+ { peerConnectionDisconnected && < ErrorHeader > WebRTC has disconnected or failed to connect at all 😭 </ ErrorHeader > }
139+ { publishSuccess && < PublishSuccess /> }
140+
141+ < video
142+ ref = { videoRef }
143+ autoPlay
144+ muted
145+ controls
146+ playsInline
147+ className = 'w-full h-full'
148+ />
149+
150+ < button
151+ onClick = { ( ) => setUseDisplayMedia ( ! useDisplayMedia ) }
152+ className = "appearance-none border w-full mt-5 py-2 px-3 leading-tight focus:outline-hidden focus:shadow-outline bg-gray-700 border-gray-700 text-white rounded-sm shadow-md placeholder-gray-200" >
153+ { ! useDisplayMedia && < > Publish Screen/Window/Tab instead </ > }
154+ { useDisplayMedia && < > Publish Webcam instead </ > }
155+ </ button >
156+ </ div >
157+ )
158+ }
159+
160+ function PublishSuccess ( ) {
161+ const subscribeUrl = window . location . href . replace ( 'publish/' , '' )
162+
163+ return (
164+ < p className = { 'bg-green-800 text-white text-lg text-center p-5 rounded-t-lg whitespace-pre-wrap' } >
165+ Live: Currently streaming to < a href = { subscribeUrl } target = "_blank" rel = "noreferrer" className = "hover:underline" > { subscribeUrl } </ a >
166+ </ p >
167+ )
168+ }
169+
170+ export default BrowserBroadcaster
0 commit comments