React による WebRTC-SIP の再構築 ②通信処理
どうも,筆者です.
前回に引き続き,今度は通信処理(Communication 部分)を実装していく.
workspacememory.hatenablog.com
React による通信処理の実装
さっそく,実装していく.
ディレクトリ構成
コンポーネントに Communication.js を追加する.
. │ docker-compose.yml │ README.md │ sample.env │ └─frontend │ Dockerfile │ entrypoint.sh │ ├─public │ favicon.ico │ index.html │ manifest.json │ robots.txt │ └─src │ App.js │ App.test.js │ index.js │ reportWebVitals.js │ setupTests.js │ ├─components │ Communication.js │ ErrorMessage.js │ Login.js │ └─services config.js WebPhone.js
src/App.js の更新
Communication.js のコンポーネントを追加する.
import React from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import ErrorMessage from './components/ErrorMessage.js'; import Login from './components/Login.js'; import Communication from './components/Communication.js'; // 追加 import webPhone from './services/WebPhone.js'; // (中略) render() { const loggedIn = this.state.loggedIn; const isLogoutProcess = this.state.isLogoutProcess; const error = this.state.error; return ( <Container> <Row> <Col> <h1>WebPhone</h1> </Col> </Row> <Login loggedIn={loggedIn} isLogoutProcess={isLogoutProcess} /> <Row> <Col> <ErrorMessage message={error} /> <hr /> </Col> </Row> <Communication loggedIn={loggedIn} /> </Container> ); // Communication を追加 }
src/Communication.js
通信処理の内容を以下に示す.
import React from 'react'; import { Row, Col, Button, FormControl } from 'react-bootstrap'; import ErrorMessage from './ErrorMessage.js'; import webPhone from '../services/WebPhone.js'; const CallMessage = (props) => { return ( <Row> <Col> <h3>{props.title}</h3> <p> <label>{props.displayType}:</label> <span>{props.displayName}</span> </p> </Col> </Row> ); }; const IncomingCall = (props) => { if (!props.isIncoming) { return null; } return ( <Row className="mt-1"> <Col> <CallMessage title={'Incoming Call'} displayType={'Incoming'} displayName={props.peerName} /> <Row className="mt-1"> <Col xs={12} lg={6}> <Button variant="success" className="btn-block" onClick={() => props.onClick(true)}> Answer </Button> </Col> <Col xs={12} lg={6}> <Button variant="danger" className="btn-block" onClick={() => props.onClick(false)}> Reject </Button> </Col> </Row> </Col> </Row> ); }; class InCall extends React.Component { constructor(props) { super(props); this.state = { isMuted: false, }; // define callback functions this.updateMuteMode = (isMuted) => { this.setState({ isMuted: isMuted, }); }; } componentDidMount() { webPhone.on('changeMuteMode', this.updateMuteMode); } componentWillUnmount() { webPhone.off('changeMuteMode', this.updateMuteMode); } render() { if (!this.props.isCalling) { return null; } const isMuted = this.state.isMuted; const variant = isMuted ? 'warning' : 'primary'; const text = isMuted ? 'Unmute (sound are muted now)' : 'Mute (sound are not muted now)'; return ( <Row className="mt-1"> <Col> <CallMessage title={'In Call'} displayType={'Peer'} displayName={this.props.peerName} /> <Row className="mt-1"> <Col> <Button variant="danger" className="btn-block" onClick={() => webPhone.hangup()}>Hangup</Button> </Col> <Col> <Button variant={variant} className="btn-block" onClick={() => webPhone.updateMuteMode()}>{text}</Button> </Col> </Row> </Col> </Row> ); } } class OutgoingCall extends React.Component { constructor(props) { super(props); this.state = { destNumber: '', errors: { destNumber: [], }, }; // define callback functions this.updateDtmf = (text) => { const value = `${this.state.destNumber}${text}`; this.setState({ destNumber: value, }); }; } componentDidMount() { webPhone.on('pushdial', this.updateDtmf); } componentWillUnmount() { webPhone.off('pushdial', this.updateDtmf); } handleChange(event) { const value = event.target.value.replace(/[^0-9*#]/g, ''); this.setState({ destNumber: value, }); } renderPad(text) { return ( <Button variant="outline-dark" className="btn-block" onClick={() => webPhone.updateDtmf(text)}> {text} </Button> ); } handleClick() { const validator = (target, message) => !target ? [message] : null; const destNumber = this.state.destNumber; this.setState({ errors: { destNumber: [], }, }); const invalidDestNumber = validator(destNumber, 'Enter the destination phone number'); if (invalidDestNumber) { this.setState({ errors: { destNumber: invalidDestNumber, }, }); return; } webPhone.call(destNumber); } handlerClear() { this.setState({ destNumber: '', }); } handleDelete() { const destNumber = this.state.destNumber; const value = destNumber.substring(0, destNumber.length - 1); this.setState({ destNumber: value, }); } render() { const isCalling = this.props.isCalling; const errors = this.state.errors; let callButton = ''; if (!isCalling) { callButton = ( <Row className='mt-3'> <Col> <Button variant="success" className="btn-block" onClick={() => this.handleClick()}> Call </Button> </Col> </Row> ); } return ( <Row className="mt-1"> <Col> <Row> <Col> <h3>Dial Pad</h3> <Row> <Col> <FormControl type="tel" name="destNumber" placeholder="enter the destination phone number" value={this.state.destNumber} onChange={(event) => this.handleChange(event)} disabled={isCalling} /> <ErrorMessage message={errors.destNumber} /> </Col> </Row> <Row className="mt-1"> <Col> <Button variant="outline-secondary" className="btn-block" onClick={() => this.handlerClear()}> Clear </Button> </Col> <Col> <Button variant="outline-danger" className="btn-block" onClick={() => this.handleDelete()}> Delete </Button> </Col> </Row> </Col> </Row> <Row className="mt-3"> <Col xs={{ span: 7, offset: 2 }} md={{ span: 4, offset: 4 }}> <Row className="no-gutters"> <Col>{this.renderPad(1)}</Col> <Col>{this.renderPad(2)}</Col> <Col>{this.renderPad(3)}</Col> </Row> <Row className="no-gutters"> <Col>{this.renderPad(4)}</Col> <Col>{this.renderPad(5)}</Col> <Col>{this.renderPad(6)}</Col> </Row> <Row className="no-gutters"> <Col>{this.renderPad(7)}</Col> <Col>{this.renderPad(8)}</Col> <Col>{this.renderPad(9)}</Col> </Row> <Row className="no-gutters"> <Col>{this.renderPad('*')}</Col> <Col>{this.renderPad(0)}</Col> <Col>{this.renderPad('#')}</Col> </Row> </Col> </Row> {callButton} </Col> </Row> ); } } class Communication extends React.Component { constructor(props) { super(props); this.callTypes = Object.freeze({ donothing: 0, incoming: 1, incall: 2, }); this.state = { callType: this.callTypes.donothing, peerName: 'Unknown', }; // define callback functions const getPeerName = (session) => { const extension = session.remote_identity.uri.user; const name = session.remote_identity.display_name; const peerName = (name) ? `${extension} (${name})` : extension; return peerName; }; this.resetSession = () => { this.setState({ callType: this.callTypes.donothing, peerName: 'Unknown', }); }; this.progress = (session) => { const peerName = getPeerName(session); const callType = (session._direction === 'incoming') ? this.callTypes.incoming : this.callTypes.incall; this.setState({ callType: callType, peerName: peerName, }); }; this.confirmed = (session) => { const peerName = getPeerName(session); this.setState({ callType: this.callTypes.incall, peerName: peerName, }); }; } componentDidMount() { webPhone.on('resetSession', this.resetSession); webPhone.on('progress', this.progress); webPhone.on('confirmed', this.confirmed); } componentWillUnmount() { webPhone.off('resetSession', this.resetSession); webPhone.off('progress', this.progress); webPhone.off('confirmed', this.confirmed); } handleIncomingCall(isAccepted) { if (isAccepted) { webPhone.answer(); this.setState({ callType: this.callTypes.incall, }); } else { webPhone.hangup(); this.setState({ callType: this.callTypes.donothing, peerName: 'Unknown', }); } } render() { if (!this.props.loggedIn) { return null; } const callType = this.state.callType; const isIncoming = callType === this.callTypes.incoming; const isCalling = callType === this.callTypes.incall; return ( <Row className="justify-content-center"> <Col> <IncomingCall isIncoming={isIncoming} peerName={this.state.peerName} onClick={(isAccepted) => this.handleIncomingCall(isAccepted)} /> <InCall isCalling={isCalling} peerName={this.state.peerName} /> <OutgoingCall isCalling={isCalling} /> </Col> </Row> ); } } export default Communication;
実装結果
実装結果を以下に格納した.必要に応じて参照して欲しい.FreePBX のサーバとアカウントがあれば利用できることを確認している.