WebRTC DataChannel で 1:1 接続をやってみた

まだプロトコルを正しく理解しておらず、とにかく動くところまで。そのメモ。

こんな感じ

https://i.gyazo.com/fb201b876ce130021c991a823e905d8d.gif

コード

GitHub - mizchi-sandbox/hello-data-channel

simple-peer と react で雑に一筆書きした。simple-peer は webtorrent などでも使われていて、筋が良さそう。

none => initiator | receiver => connected と mode が遷移する。

// <div class="root"></div> みたいなDOMがあることを前提

import Peer from "simple-peer";
import React from "react";
import ReactDOM from "react-dom";

let peer: Peer.Instance = null as any;

class Initiator extends React.Component<
  {
    onHandshaked: () => void;
  },
  {
    incoming: string;
    outgoing: null | string;
  }
> {
  state = {
    incoming: "",
    outgoing: null
  };

  textareaRef: any = React.createRef();

  componentDidMount() {
    peer = new Peer({ initiator: true, trickle: false });

    peer.on("error", (err: any) => {
      console.error(err);
    });

    peer.on("signal", (data: any) => {
      console.log("signal");
      this.setState(s => ({ ...s, outgoing: data }));
    });

    peer.on("connect", () => {
      console.log("CONNECT");
      // peer.send("hey, how is it going?");
      this.props.onHandshaked();
    });

    // peer.on("data", (data: any) => {
    //   console.log("data: " + data);
    // });
    return peer;
  }

  render() {
    return (
      <div>
        {this.state.outgoing && (
          <>
            <textarea
              ref={this.textareaRef as any}
              style={{ width: 600, height: 150, background: "#ddd" }}
              value={JSON.stringify(this.state.outgoing)}
              onChange={() => {
                /**/
              }}
            />

            <button
              onClick={() => {
                this.textareaRef.current.select();
                document.execCommand("copy");
              }}
            >
              Copy to clipboard
            </button>
            <hr />
            <textarea
              style={{ width: 600, height: 150 }}
              value={this.state.incoming}
              onChange={ev => {
                this.setState({ incoming: ev.target.value });
              }}
            />
            <button
              onClick={() => {
                peer.signal(JSON.parse(this.state.incoming));
              }}
            >
              signal
            </button>
          </>
        )}
      </div>
    );
  }
}

class Receiver extends React.Component<
  {
    onHandshaked: () => void;
  },
  {
    incoming: string;
    outgoing: null | string;
  }
> {
  state = {
    mode: null,
    handshaked: false,
    incoming: "",
    outgoing: null
  };

  textareaRef: any = React.createRef();

  componentDidMount() {
    peer = new Peer({ initiator: false, trickle: false });
    peer.on("error", (err: any) => {
      console.log("error", err);
    });

    peer.on("signal", (data: any) => {
      console.log("signal");
      this.setState(s => ({ ...s, outgoing: data }));
    });

    peer.on("connect", () => {
      console.log("CONNECT");
      this.props.onHandshaked();
    });

    return peer;
  }

  render() {
    return (
      <div>
        <div>
          <textarea
            style={{ width: 600, height: 150 }}
            value={this.state.incoming}
            onChange={ev => {
              this.setState({ incoming: ev.target.value });
            }}
          />

          <button
            onClick={() => {
              peer.signal(JSON.parse(this.state.incoming));
            }}
          >
            signal
          </button>
        </div>

        <div>
          {this.state.outgoing && (
            <>
              <textarea
                ref={this.textareaRef as any}
                style={{ width: 600, height: 150, background: "#ddd" }}
                value={JSON.stringify(this.state.outgoing)}
                onChange={() => {
                  /**/
                }}
              />

              <button
                onClick={() => {
                  this.textareaRef.current.select();
                  document.execCommand("copy");
                }}
              >
                Copy to clipboard
              </button>
            </>
          )}
        </div>
      </div>
    );
  }
}

class Chat extends React.Component<
  any,
  {
    text: string;
    comments: Array<{
      owner: string;
      text: string;
      date: number;
    }>;
  }
> {
  state = { text: "", comments: [] };

  componentDidMount() {
    peer.on("data", (data: any) => {
      const json = JSON.parse(data);
      this.setState({ comments: json });
    });
  }

  render() {
    return (
      <div>
        <textarea
          value={this.state.text}
          onChange={ev => {
            this.setState({ text: ev.target.value });
          }}
        />

        <button
          onClick={() => {
            const comment = {
              owner: (peer as any)._id,
              text: this.state.text,
              date: Date.now()
            };
            const newComments = [comment, ...this.state.comments];
            peer.send(JSON.stringify(newComments));
            this.setState({ text: "", comments: newComments });
          }}
        >
          send
        </button>

        <ul>
          {this.state.comments.map((c: any, index) => {
            return (
              <li key={index}>
                {c.owner}: {c.text}: {c.date}
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

class App extends React.Component<
  {},
  {
    mode: "initiator" | "receiver" | "none" | "connected";
  }
> {
  state = {
    mode: "none"
  } as any;

  render() {
    switch (this.state.mode) {
      case "none": {
        return (
          <div>
            <button
              onClick={() => {
                this.setState({ mode: "initiator" });
              }}
            >
              Create connection
            </button>

            <button
              onClick={() => {
                this.setState({ mode: "receiver" });
              }}
            >
              Receive connection
            </button>
          </div>
        );
      }
      case "initiator": {
        return (
          <Initiator
            onHandshaked={() => {
              this.setState({ mode: "connected" });
            }}
          />
        );
      }
      case "receiver": {
        return (
          <Receiver
            onHandshaked={() => {
              this.setState({ mode: "connected" });
            }}
          />
        );
      }
      case "connected": {
        return <Chat />;
      }
    }
  }
}

ReactDOM.render(
  <>
    <h1>Handshaker</h1>
    <hr />
    <App />
  </>,
  document.querySelector(".root")
);

動いてるけど、Receiver 側でこんなエラーが出る

error Error: Ice connection failed.
    at makeError (main.16f9df1b.js:7031)
    at Peer._onIceStateChange (main.16f9df1b.js:6674)
    at RTCPeerConnection.Peer.self._pc.oniceconnectionstatechange (main.16f9df1b.js:6203)

stun しか設定してないので、そのせいだろうか。 trickle: false も怪しいが。プロトコルをちゃんと理解してないのでちゃんと調べる。

offer answer の流れを今はコピペしているが、自動化するには別途 firebase などでデータ受け渡さないといけないのかなーという気がする。そんなに難しくないので、あとでやる。