作業中のメモ

よく「計算機」を使って作業をする.知らなかったことを中心にまとめるつもり.

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 のサーバとアカウントがあれば利用できることを確認している.

drive.google.com