Make demo public
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import Logger from '../Logger';
|
||||
import muiTheme from './muiTheme';
|
||||
import Notifier from './Notifier';
|
||||
import Room from './Room';
|
||||
|
||||
const logger = new Logger('App'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class App extends React.Component
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<div data-component='App'>
|
||||
<Notifier ref='Notifier'/>
|
||||
|
||||
<Room
|
||||
peerId={props.peerId}
|
||||
roomId={props.roomId}
|
||||
onNotify={this.handleNotify.bind(this)}
|
||||
onHideNotification={this.handleHideNotification.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
handleNotify(data)
|
||||
{
|
||||
this.refs.Notifier.notify(data);
|
||||
}
|
||||
|
||||
handleHideNotification(uid)
|
||||
{
|
||||
this.refs.Notifier.hideNotification(uid);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
roomId : React.PropTypes.string.isRequired
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import MicOffIcon from 'material-ui/svg-icons/av/mic-off';
|
||||
import VideoCamOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import ChangeVideoCamIcon from 'material-ui/svg-icons/av/repeat';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('LocalVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class LocalVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
micMuted : false,
|
||||
webcam : props.stream && !!props.stream.getVideoTracks()[0],
|
||||
togglingWebcam : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='LocalVideo' className={`state-${props.connectionState}`}>
|
||||
{props.stream ?
|
||||
<Video
|
||||
stream={props.stream}
|
||||
resolution={props.resolution}
|
||||
muted
|
||||
mirror={props.webcamType === 'front'}
|
||||
onResolutionChange={this.handleResolutionChange.bind(this)}
|
||||
/>
|
||||
:null}
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteMic.bind(this)}
|
||||
>
|
||||
<MicOffIcon
|
||||
color={!state.micMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={state.togglingWebcam}
|
||||
onClick={this.handleClickWebcam.bind(this)}
|
||||
>
|
||||
<VideoCamOffIcon
|
||||
color={state.webcam ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{props.multipleWebcams ?
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={!state.webcam || state.togglingWebcam}
|
||||
onClick={this.handleClickChangeWebcam.bind(this)}
|
||||
>
|
||||
<ChangeVideoCamIcon
|
||||
color='#fff'
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peerId}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
this.setState({ webcam: nextProps.stream && !!nextProps.stream.getVideoTracks()[0] });
|
||||
}
|
||||
|
||||
handleClickMuteMic()
|
||||
{
|
||||
logger.debug('handleClickMuteMic()');
|
||||
|
||||
let value = !this.state.micMuted;
|
||||
|
||||
this.props.onMicMute(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ micMuted: value });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickWebcam()
|
||||
{
|
||||
logger.debug('handleClickWebcam()');
|
||||
|
||||
let value = !this.state.webcam;
|
||||
|
||||
this.setState({ togglingWebcam: true });
|
||||
|
||||
this.props.onWebcamToggle(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ webcam: value, togglingWebcam: false });
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
this.setState({ togglingWebcam: false });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickChangeWebcam()
|
||||
{
|
||||
logger.debug('handleClickChangeWebcam()');
|
||||
|
||||
this.props.onWebcamChange();
|
||||
}
|
||||
|
||||
handleResolutionChange()
|
||||
{
|
||||
logger.debug('handleResolutionChange()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
}
|
||||
|
||||
LocalVideo.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
stream : React.PropTypes.object,
|
||||
resolution : React.PropTypes.string,
|
||||
multipleWebcams : React.PropTypes.bool.isRequired,
|
||||
webcamType : React.PropTypes.string,
|
||||
connectionState : React.PropTypes.string,
|
||||
onMicMute : React.PropTypes.func.isRequired,
|
||||
onWebcamToggle : React.PropTypes.func.isRequired,
|
||||
onWebcamChange : React.PropTypes.func.isRequired,
|
||||
onResolutionChange : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import NotificationSystem from 'react-notification-system';
|
||||
|
||||
const STYLE =
|
||||
{
|
||||
NotificationItem :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '6px 10px',
|
||||
backgroundColor : 'rgba(255,255,255, 0.9)',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 400,
|
||||
fontSize : '1rem',
|
||||
cursor : 'default',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none',
|
||||
userSelect : 'none',
|
||||
transition : '0.15s ease-in-out'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '2px solid rgba(255,0,78, 0.75)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid rgba(73,206,62, 0.75)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid #ff0014'
|
||||
}
|
||||
},
|
||||
Title :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
margin : '0 0 8px 0',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 500,
|
||||
fontSize : '1.1rem',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : 'rgba(255,0,78, 0.85)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : 'rgba(73,206,62, 0.9)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#ff0014'
|
||||
}
|
||||
},
|
||||
Dismiss :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
display : 'none'
|
||||
}
|
||||
},
|
||||
Action :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '8px 24px',
|
||||
fontSize : '1.2rem',
|
||||
cursor : 'pointer',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
backgroundColor : 'rgba(255,0,78, 1)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
backgroundColor : 'rgba(73,206,62, 0.75)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Notifier extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<NotificationSystem ref='NotificationSystem' style={STYLE} allowHTML={false}/>
|
||||
);
|
||||
}
|
||||
|
||||
notify(data)
|
||||
{
|
||||
let data2;
|
||||
|
||||
switch (data.level)
|
||||
{
|
||||
case 'info' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'success' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'error' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 3
|
||||
}, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`unknown level "${data.level}"`);
|
||||
}
|
||||
|
||||
this.refs.NotificationSystem.addNotification(data2);
|
||||
}
|
||||
|
||||
hideNotification(uid)
|
||||
{
|
||||
this.refs.NotificationSystem.removeNotification(uid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import VolumeOffIcon from 'material-ui/svg-icons/av/volume-off';
|
||||
import VideoOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('RemoteVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class RemoteVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
audioMuted : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let hasVideo = !!props.stream.getVideoTracks()[0];
|
||||
|
||||
global.SS = props.stream;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='RemoteVideo'
|
||||
className={classnames({ fullsize: !!props.fullsize })}
|
||||
>
|
||||
<Video
|
||||
stream={props.stream}
|
||||
muted={state.audioMuted}
|
||||
/>
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteAudio.bind(this)}
|
||||
>
|
||||
<VolumeOffIcon
|
||||
color={!state.audioMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickDisableVideo.bind(this)}
|
||||
>
|
||||
<VideoOffIcon
|
||||
color={hasVideo ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peer.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleClickMuteAudio()
|
||||
{
|
||||
logger.debug('handleClickMuteAudio()');
|
||||
|
||||
let value = !this.state.audioMuted;
|
||||
|
||||
this.setState({ audioMuted: value });
|
||||
}
|
||||
|
||||
handleClickDisableVideo()
|
||||
{
|
||||
logger.debug('handleClickDisableVideo()');
|
||||
|
||||
let stream = this.props.stream;
|
||||
let msid = stream.id;
|
||||
let hasVideo = !!stream.getVideoTracks()[0];
|
||||
|
||||
if (hasVideo)
|
||||
this.props.onDisableVideo(msid);
|
||||
else
|
||||
this.props.onEnableVideo(msid);
|
||||
}
|
||||
}
|
||||
|
||||
RemoteVideo.propTypes =
|
||||
{
|
||||
peer : React.PropTypes.object.isRequired,
|
||||
stream : React.PropTypes.object.isRequired,
|
||||
fullsize : React.PropTypes.bool,
|
||||
onDisableVideo : React.PropTypes.func.isRequired,
|
||||
onEnableVideo : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,487 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ClipboardButton from 'react-clipboard.js';
|
||||
import TransitionAppear from './TransitionAppear';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import Stats from './Stats';
|
||||
import Logger from '../Logger';
|
||||
import utils from '../utils';
|
||||
import Client from '../Client';
|
||||
|
||||
const logger = new Logger('Room');
|
||||
const STATS_INTERVAL = 1000;
|
||||
|
||||
export default class Room extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
peers : {},
|
||||
localStream : null,
|
||||
localVideoResolution : null, // qvga / vga / hd / fullhd.
|
||||
multipleWebcams : false,
|
||||
webcamType : null,
|
||||
connectionState : null,
|
||||
remoteStreams : {},
|
||||
showStats : false,
|
||||
stats : null
|
||||
};
|
||||
|
||||
// Mounted flag
|
||||
this._mounted = false;
|
||||
// Client instance
|
||||
this._client = null;
|
||||
// Timer to retrieve RTC stats.
|
||||
this._statsTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let numPeers = Object.keys(state.remoteStreams).length;
|
||||
|
||||
return (
|
||||
<TransitionAppear duration={2000}>
|
||||
<div data-component='Room'>
|
||||
|
||||
<div className='room-link-wrapper'>
|
||||
<div className='room-link'>
|
||||
<ClipboardButton
|
||||
component='a'
|
||||
className='link'
|
||||
button-href={window.location.href}
|
||||
data-clipboard-text={window.location.href}
|
||||
onSuccess={this.handleRoomLinkCopied.bind(this)}
|
||||
onClick={() => {}} // Avoid link action.
|
||||
>
|
||||
invite people to this room
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='remote-videos'>
|
||||
{
|
||||
Object.keys(state.remoteStreams).map((msid) =>
|
||||
{
|
||||
let stream = state.remoteStreams[msid];
|
||||
let peer;
|
||||
|
||||
for (let peerId of Object.keys(state.peers))
|
||||
{
|
||||
peer = state.peers[peerId];
|
||||
|
||||
if (peer.msids.indexOf(msid) !== -1)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!peer)
|
||||
return;
|
||||
|
||||
return (
|
||||
<TransitionAppear key={msid} duration={500}>
|
||||
<RemoteVideo
|
||||
peer={peer}
|
||||
stream={stream}
|
||||
fullsize={numPeers === 1}
|
||||
onDisableVideo={this.handleDisableRemoteVideo.bind(this)}
|
||||
onEnableVideo={this.handleEnableRemoteVideo.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TransitionAppear duration={500}>
|
||||
<div className='local-video'>
|
||||
<LocalVideo
|
||||
peerId={props.peerId}
|
||||
stream={state.localStream}
|
||||
resolution={state.localVideoResolution}
|
||||
multipleWebcams={state.multipleWebcams}
|
||||
webcamType={state.webcamType}
|
||||
connectionState={state.connectionState}
|
||||
onMicMute={this.handleLocalMute.bind(this)}
|
||||
onWebcamToggle={this.handleLocalWebcamToggle.bind(this)}
|
||||
onWebcamChange={this.handleLocalWebcamChange.bind(this)}
|
||||
onResolutionChange={this.handleLocalResolutionChange.bind(this)}
|
||||
/>
|
||||
|
||||
{state.showStats ?
|
||||
<TransitionAppear duration={500}>
|
||||
<Stats
|
||||
stats={state.stats || new Map()}
|
||||
onClose={this.handleStatsClose.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
:
|
||||
<div
|
||||
className='show-stats'
|
||||
onClick={this.handleClickShowStats.bind(this)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// Set flag
|
||||
this._mounted = true;
|
||||
|
||||
// Run the client
|
||||
this._runClient();
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
// Unset flag
|
||||
this._mounted = false;
|
||||
|
||||
// Close client
|
||||
this._client.removeAllListeners();
|
||||
this._client.close();
|
||||
|
||||
// Close local MediaStream
|
||||
if (state.localStream)
|
||||
utils.closeMediaStream(state.localStream);
|
||||
}
|
||||
|
||||
handleRoomLinkCopied()
|
||||
{
|
||||
logger.debug('handleRoomLinkCopied()');
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
position : 'tr',
|
||||
title : 'Room URL copied to the clipboard',
|
||||
message : 'Share it with others to join this room'
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalMute(value)
|
||||
{
|
||||
logger.debug('handleLocalMute() [value:%s]', value);
|
||||
|
||||
let micTrack = this.state.localStream.getAudioTracks()[0];
|
||||
|
||||
if (!micTrack)
|
||||
return Promise.reject(new Error('no audio track'));
|
||||
|
||||
micTrack.enabled = !value;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
handleLocalWebcamToggle(value)
|
||||
{
|
||||
logger.debug('handleLocalWebcamToggle() [value:%s]', value);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
if (value)
|
||||
return this._client.addVideo();
|
||||
else
|
||||
return this._client.removeVideo();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let localStream = this.state.localStream;
|
||||
|
||||
this.setState({ localStream });
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalWebcamChange()
|
||||
{
|
||||
logger.debug('handleLocalWebcamChange()');
|
||||
|
||||
this._client.changeWebcam();
|
||||
}
|
||||
|
||||
handleLocalResolutionChange()
|
||||
{
|
||||
logger.debug('handleLocalResolutionChange()');
|
||||
|
||||
this._client.changeVideoResolution();
|
||||
}
|
||||
|
||||
handleStatsClose()
|
||||
{
|
||||
logger.debug('handleStatsClose()');
|
||||
|
||||
this.setState({ showStats: false });
|
||||
this._stopStats();
|
||||
}
|
||||
|
||||
handleClickShowStats()
|
||||
{
|
||||
logger.debug('handleClickShowStats()');
|
||||
|
||||
this.setState({ showStats: true });
|
||||
this._startStats();
|
||||
}
|
||||
|
||||
handleDisableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleDisableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
this._client.disableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
handleEnableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleEnableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
this._client.enableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
_runClient()
|
||||
{
|
||||
let peerId = this.props.peerId;
|
||||
let roomId = this.props.roomId;
|
||||
|
||||
logger.debug('_runClient() [peerId:"%s", roomId:"%s"]', peerId, roomId);
|
||||
|
||||
this._client = new Client(peerId, roomId);
|
||||
|
||||
this._client.on('localstream', (stream, resolution) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
localStream : stream,
|
||||
localVideoResolution : resolution
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('join', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
title : 'Yes!',
|
||||
message : 'You are in the room!',
|
||||
image : '/resources/images/room.svg',
|
||||
imageWidth : 80,
|
||||
imageHeight : 80
|
||||
});
|
||||
|
||||
// Start retrieving WebRTC stats (unless mobile)
|
||||
if (utils.isDesktop())
|
||||
{
|
||||
this.setState({ showStats: true });
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
this._startStats();
|
||||
}, STATS_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
|
||||
this._client.on('close', (error) =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
if (error)
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Error',
|
||||
message : error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('disconnected', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Warning',
|
||||
message : 'app disconnected'
|
||||
});
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('numwebcams', (num) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
multipleWebcams : (num > 1 ? true : false)
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('webcamtype', (type) =>
|
||||
{
|
||||
this.setState({ webcamType: type });
|
||||
});
|
||||
|
||||
this._client.on('peers', (peers) =>
|
||||
{
|
||||
let peersObject = {};
|
||||
|
||||
for (let peer of peers)
|
||||
{
|
||||
peersObject[peer.id] = peer;
|
||||
}
|
||||
|
||||
this.setState({ peers: peersObject });
|
||||
});
|
||||
|
||||
this._client.on('addpeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
message : `${peer.id} joined the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('updatepeer', (peer) =>
|
||||
{
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('removepeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'info',
|
||||
message : `${peer.id} left the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
delete peers[peer.id];
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('connectionstate', (state) =>
|
||||
{
|
||||
this.setState({ connectionState: state });
|
||||
});
|
||||
|
||||
this._client.on('addstream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
remoteStreams[stream.id] = stream;
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removestream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
delete remoteStreams[stream.id];
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('addtrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removetrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('forcestreamsupdate', () =>
|
||||
{
|
||||
// Just firef for Firefox due to bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
_startStats()
|
||||
{
|
||||
logger.debug('_startStats()');
|
||||
|
||||
getStats.call(this);
|
||||
|
||||
function getStats()
|
||||
{
|
||||
this._client.getStats()
|
||||
.then((stats) =>
|
||||
{
|
||||
if (!this._mounted)
|
||||
return;
|
||||
|
||||
this.setState({ stats });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('getStats() failed: %o', error);
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopStats()
|
||||
{
|
||||
logger.debug('_stopStats()');
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
clearTimeout(this._statsTimer);
|
||||
}
|
||||
}
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
roomId : React.PropTypes.string.isRequired,
|
||||
onNotify : React.PropTypes.func.isRequired,
|
||||
onHideNotification : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,497 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import browser from 'bowser';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('Stats'); // eslint-disable-line no-unused-vars
|
||||
|
||||
// TODO: TMP
|
||||
global.BROWSER = browser;
|
||||
|
||||
export default class Stats extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport : null,
|
||||
audio : null,
|
||||
video : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount()
|
||||
{
|
||||
let stats = this.props.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stats = nextProps.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Stats'>
|
||||
<div
|
||||
className='close'
|
||||
onClick={this.handleCloseClick.bind(this)}
|
||||
/>
|
||||
{
|
||||
Object.keys(state.stats).map((blockName) =>
|
||||
{
|
||||
let block = state.stats[blockName];
|
||||
|
||||
if (!block)
|
||||
return;
|
||||
|
||||
let items = Object.keys(block).map((itemName) =>
|
||||
{
|
||||
let value = block[itemName];
|
||||
|
||||
if (value === undefined)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div key={itemName} className='item'>
|
||||
<div className='key'>{itemName}</div>
|
||||
<div className='value'>{value}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (!items.length)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div key={blockName} className='block'>
|
||||
<h1>{blockName}</h1>
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleCloseClick()
|
||||
{
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
_processStats(stats)
|
||||
{
|
||||
if (browser.check({ chrome: '58' }, true))
|
||||
{
|
||||
this._processStatsChrome58(stats);
|
||||
}
|
||||
else if (browser.check({ chrome: '40' }, true))
|
||||
{
|
||||
this._processStatsChromeOld(stats);
|
||||
}
|
||||
else if (browser.check({ firefox: '40' }, true))
|
||||
{
|
||||
this._processStatsFirefox(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn('_processStats() | unsupported browser [name:"%s", version:%s]',
|
||||
browser.name, browser.version);
|
||||
}
|
||||
}
|
||||
|
||||
_processStatsChrome58(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'transport':
|
||||
{
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'codec':
|
||||
{
|
||||
let mimeType = group.mimeType.split('/');
|
||||
let kind = mimeType[0];
|
||||
let codec = mimeType[1];
|
||||
let block;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = codec;
|
||||
block['payload type'] = group.payloadType;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'track':
|
||||
{
|
||||
if (group.kind !== 'video')
|
||||
break;
|
||||
|
||||
video['frame size'] = group.frameWidth + ' x ' + group.frameHeight;
|
||||
video['frames sent'] = group.framesSent;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.protocol;
|
||||
transport['local IP'] = localCandidate.ip;
|
||||
transport['local port'] = localCandidate.port;
|
||||
transport['remote IP'] = remoteCandidate.ip;
|
||||
transport['remote port'] = remoteCandidate.port;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsChromeOld(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'googCandidatePair':
|
||||
{
|
||||
if (group.googActiveConnection !== 'true')
|
||||
break;
|
||||
|
||||
let localAddress = group.googLocalAddress.split(':');
|
||||
let remoteAddress = group.googRemoteAddress.split(':');
|
||||
let localIP = localAddress[0];
|
||||
let localPort = localAddress[1];
|
||||
let remoteIP = remoteAddress[0];
|
||||
let remotePort = remoteAddress[1];
|
||||
|
||||
transport['protocol'] = group.googTransportType;
|
||||
transport['local IP'] = localIP;
|
||||
transport['local port'] = localPort;
|
||||
transport['remote IP'] = remoteIP;
|
||||
transport['remote port'] = remotePort;
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['RTT'] = Math.round(group.googRtt) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'VideoBwe':
|
||||
{
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.googAvailableSendBandwidth / 1000) + ' kbps';
|
||||
transport['transmit bitrate'] =
|
||||
Math.round(group.googTransmitBitrate / 1000) + ' kbps';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ssrc':
|
||||
{
|
||||
if (group.packetsSent === undefined)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = group.googCodecName;
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
block['packets lost'] = group.packetsLost;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame size'] =
|
||||
group.googFrameWidthSent + ' x ' + group.googFrameHeightSent;
|
||||
video['frame rate'] = group.googFrameRateSent;
|
||||
}
|
||||
|
||||
block['NACK count'] = group.googNacksReceived;
|
||||
block['PLI count'] = group.googPlisReceived;
|
||||
block['FIR count'] = group.googFirsReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsFirefox(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
// TODO: REMOVE
|
||||
global.STATS = stats;
|
||||
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.selected)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['bitrate'] =
|
||||
Math.round(group.bitrateMean / 1000) + ' kbps';
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame rate'] = Math.round(group.framerateMean);
|
||||
}
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.transport;
|
||||
transport['local IP'] = localCandidate.ipAddress;
|
||||
transport['local port'] = localCandidate.portNumber;
|
||||
transport['remote IP'] = remoteCandidate.ipAddress;
|
||||
transport['remote port'] = remoteCandidate.portNumber;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Stats.propTypes =
|
||||
{
|
||||
stats : React.PropTypes.object.isRequired,
|
||||
onClose : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
const DEFAULT_DURATION = 1000;
|
||||
|
||||
export default class TransitionAppear extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let duration = props.hasOwnProperty('duration') ? props.duration : DEFAULT_DURATION;
|
||||
|
||||
return (
|
||||
<ReactCSSTransitionGroup
|
||||
component={FakeTransitionWrapper}
|
||||
transitionName='transition'
|
||||
transitionAppear={!!duration}
|
||||
transitionAppearTimeout={duration}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{this.props.children}
|
||||
</ReactCSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TransitionAppear.propTypes =
|
||||
{
|
||||
children : React.PropTypes.any,
|
||||
duration : React.PropTypes.number
|
||||
};
|
||||
|
||||
class FakeTransitionWrapper extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let children = React.Children.toArray(this.props.children);
|
||||
|
||||
return children[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
FakeTransitionWrapper.propTypes =
|
||||
{
|
||||
children : React.PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Logger from '../Logger';
|
||||
import classnames from 'classnames';
|
||||
import hark from 'hark';
|
||||
|
||||
const logger = new Logger('Video'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class Video extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
width : 0,
|
||||
height : 0,
|
||||
resolution : null,
|
||||
volume : 0 // Integer from 0 to 10.
|
||||
};
|
||||
|
||||
let stream = props.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// Current MediaStreamTracks info.
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
// Periodic timer to show video dimensions.
|
||||
this._videoResolutionTimer = null;
|
||||
|
||||
// Hark instance.
|
||||
this._hark = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Video'>
|
||||
{state.width ?
|
||||
(
|
||||
<div
|
||||
className={classnames('resolution', { clickable: !!props.resolution })}
|
||||
onClick={this.handleResolutionClick.bind(this)}
|
||||
>
|
||||
<p>{state.width}x{state.height}</p>
|
||||
{props.resolution ?
|
||||
<p>{props.resolution}</p>
|
||||
:null}
|
||||
</div>
|
||||
)
|
||||
:null}
|
||||
<div className='volume'>
|
||||
<div className={classnames('bar', `level${state.volume}`)}/>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({ mirror: props.mirror })}
|
||||
autoPlay
|
||||
muted={props.muted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
let stream = this.props.stream;
|
||||
let video = this.refs.video;
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
this._showVideoResolution();
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
this._showVideoResolution();
|
||||
}, 500);
|
||||
|
||||
if (stream.getAudioTracks().length > 0)
|
||||
{
|
||||
this._hark = hark(stream);
|
||||
|
||||
this._hark.on('speaking', () =>
|
||||
{
|
||||
logger.debug('hark "speaking" event');
|
||||
});
|
||||
|
||||
this._hark.on('stopped_speaking', () =>
|
||||
{
|
||||
logger.debug('hark "stopped_speaking" event');
|
||||
|
||||
this.setState({ volume: 0 });
|
||||
});
|
||||
|
||||
this._hark.on('volume_change', (volume, threshold) =>
|
||||
{
|
||||
if (volume < threshold)
|
||||
return;
|
||||
|
||||
// logger.debug('hark "volume_change" event [volume:%sdB, threshold:%sdB]', volume, threshold);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
volume : Math.round((volume - threshold) * (-10) / threshold)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stream = nextProps.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// If there is something different in the stream, re-render it.
|
||||
|
||||
let previousTracksHash = this._tracksHash;
|
||||
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
if (this._tracksHash !== previousTracksHash)
|
||||
this.refs.video.srcObject = stream;
|
||||
}
|
||||
|
||||
handleResolutionClick()
|
||||
{
|
||||
if (!this.props.resolution)
|
||||
return;
|
||||
|
||||
logger.debug('handleResolutionClick()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
|
||||
_getTracksHash(stream)
|
||||
{
|
||||
return stream.getTracks()
|
||||
.map((track) =>
|
||||
{
|
||||
return track.id;
|
||||
})
|
||||
.join('|');
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
let video = this.refs.video;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
width : video.videoWidth,
|
||||
height : video.videoHeight
|
||||
});
|
||||
}
|
||||
|
||||
_cleanStream(stream)
|
||||
{
|
||||
// Hack for Firefox bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
let previousNumTracks = tracks.length;
|
||||
|
||||
// Remove ended tracks.
|
||||
for (let track of tracks)
|
||||
{
|
||||
if (track.readyState === 'ended')
|
||||
{
|
||||
logger.warn('_cleanStream() | removing ended track [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple live audio tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getAudioTracks().length > 1)
|
||||
{
|
||||
let track = stream.getAudioTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live audio track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
// If there are multiple live video tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getVideoTracks().length > 1)
|
||||
{
|
||||
let track = stream.getVideoTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live video track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
let numTracks = stream.getTracks().length;
|
||||
|
||||
if (numTracks !== previousNumTracks)
|
||||
logger.warn('_cleanStream() | num tracks changed from %s to %s', previousNumTracks, numTracks);
|
||||
}
|
||||
}
|
||||
|
||||
Video.propTypes =
|
||||
{
|
||||
stream : React.PropTypes.object.isRequired,
|
||||
resolution : React.PropTypes.string,
|
||||
muted : React.PropTypes.bool,
|
||||
mirror : React.PropTypes.bool,
|
||||
onResolutionChange : React.PropTypes.func
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
|
||||
import { grey500 } from 'material-ui/styles/colors';
|
||||
|
||||
// NOTE: I should clone it
|
||||
let theme = lightBaseTheme;
|
||||
|
||||
theme.palette.borderColor = grey500;
|
||||
|
||||
let muiTheme = getMuiTheme(lightBaseTheme);
|
||||
|
||||
export default muiTheme;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user