用 React 和声网 SDK 创建一个语音聊天应用(语音 SDK)

新冠疫情对大众服务应用进行了重新洗牌,音频流应用和播客成了现在应用市场上的“香饽饽”。虽然 Clubhouse 之类的应用引领了趋势,但 Twitter 和 Discord 这些社交应用巨头很快就加入赛道,推出了自己的音频流应用。

还好现在有很多能帮我们轻松完成任务的服务,让我们不至于从零开始。事实上我们可以在很短时间内搭建好一个语音 SDK 应用,还能兼顾安全性和个性化要求。下面这个教程会告诉我们如何在几分钟内创建一个与那些音频流应用类似的应用。


技术栈

  • React(前端)
  • Firebase(数据库)
  • 声网 SDK(服务器)
  • Geist UI(样式)


前期准备


文件夹结构

文件夹结构


根文件结构

我们把 react-router 组件导入这个文件,在authContext中包入所有路径。

import { BrowserRouter as Router, Route } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import Page from "@geist-ui/react/esm/page";
import Home from "./components/Home";
import Login from "./components/Login";
import PrivateRoute from "./utils/PrivateRoute";
import Navbar from "./utils/Navbar";
import Join from "./components/Join";
const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Page size="small">
          <Page.Header style={{ padding: 0.1 }}>
            <Navbar />
          </Page.Header>
          <Page.Content
            style={{ padding: 0, maxHeight: "80vh", overflow: "hidden" }}
          >
            <PrivateRoute path="/" exact component={Home} />
            <PrivateRoute path="/join/:id" exact component={Join} />
            <Route path="/login" component={Login} />
          </Page.Content>
        </Page>
      </Router>
    </AuthProvider>
  );
};
export default App;

我们一共有三个路径:

  • 登录界面
  • 显示所有聊天室界面
  • 加入聊天室界面


登录路径

我们允许用户在这个路径中使用 Firebase 的 Google OAuth 令牌在应用中创建帐户。


显示所有聊天室路径

当用户在应用中完成注册后,我们就把他们跳转到显示所有公共聊天室的应用主页。用户点击一个聊天室,就可以跳转到加入聊天室路径。


加入聊天室路径

我们允许用户在这个路径中加入一个聊天室成为听众和参与聊天。

用户登录后才能加入聊天室,为了保护用户信息,我们会使用 Context API 把当前用户的信息储存在authContext.js中。


受保护路径组件

我们的应用中的一些路径只对登录用户开放,所以如果没有活跃的登录用户,我们就需要请用户先登录。为了解决这个问题,我们创建了一个查看当前用户登陆状态的组件。如果用户已登陆,这个组件就会允许该用户访问私有路径,如果用户没有登录,它会把用户界面跳转到登录界面。

import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../AuthContext";
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
  const { currentUser } = useContext(AuthContext);
  return (
    <Route
      {...rest}
      render={(routeProps) =>
        currentUser ? (
          <RouteComponent {...routeProps} />
        ) : (
          <Redirect to={"/login"} />
        )
      }
    />
  );
};
export default PrivateRoute;


认证环境

这个文件是用来储存用户的全面信息的,所有组件都可以通过这个组件获得用户状态。它使用 firebase 的onAuthStateChanged函数把当前用户的信息储存进 currentUser状态,然后把该状态传输给所有组件。每当用户状态发生改变时,firebase 会自动触发onAuthStateChanged函数并把用户状态更新给所有组件。

import React, { useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";
import Loading from "@geist-ui/react/esm/loading";

export const AuthContext = React.createContext();

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [pending, setPending] = useState(true);
  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      setCurrentUser(user);
      setPending(false);
    });
  }, []);

  if (pending) {
    return <Loading />;
  }

  return (
    <AuthContext.Provider
      value={{
        currentUser,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};


登录组件

只有登录用户可以创建和加入聊天室。我们会使用 firebase 谷歌登录来进行用户验证。首先,我们要向用户显示一个登录按钮,用户点击这个按钮就开始 firebase 谷歌登录,等用户成功登录后,我们使用 react-router-dom中的 useHistory钩子推动用户进入新路径。如果用户已经登录,我们会自动把用户跳转到新路径。

import Button from "@geist-ui/react/esm/button";
import { signInWithPopup } from "firebase/auth";
import { auth, provider } from "../firebase";
import { useHistory } from "react-router";
import { useContext, useEffect } from "react";
import { AuthContext } from "../AuthContext";
const Login = () => {
  const history = useHistory();
  const { currentUser } = useContext(AuthContext);
  useEffect(() => {
    if (currentUser) history.replace("/");
  });
  return (
    <div style={{ display: "grid", placeItems: "center" }}>
      <Button
        onClick={() => {
          signInWithPopup(auth, provider)
            .then(() => {
              history.replace("/");
            })
            .catch((e) => console.log(e));
        }}
        size="large"
      >
        Login with google
      </Button>
    </div>
  );
};
export default Login;

登录按钮登录按钮


显示所有聊天室

在这个组件(Home.js)里,我们从 Firestore 实时获取所有聊天室并在 /路径上进行渲染。Create Room 按钮是用来创建聊天室的。

const Home = () => {
  const [state, setState] = useState(false);
  const [title, setTitle] = useState("");
return (
    <>
      <Modal_ state={state} setState={setState} title={title} />
<Button
        onClick={() => {
          setTitle("create room");
          setState(true);
        }}
        icon={<Plus />}
        type="success"
        width="100%"
      >
        Create room
      </Button>
      <Rooms />
    </>
  );
};
export default Home;


我们用聊天室组件从数据库获得所有聊天室并把它们显示出来。当用户点击下图中的垃圾箱标识时,我们就调用deleteRoom函数,它会使用文档 ID 从数据库删除这个聊天室。当用户点击 Join Room 按钮时,我们就把他们跳转到加入聊天室的新路径。

const Rooms = () => {
  const [rooms, setRooms] = useState([]);
  const [loading, setLoading] = useState(true);
  const { currentUser } = useContext(AuthContext);
  const [load, setLoad] = useState(false);

  const deleteRoom = async (id) => {
    setLoad(true);
    deleteDoc(doc(db, "rooms", id))
      .then(() => setLoad(false))
      .catch((e) => console.log(e));
  };

  useEffect(() => {
    let l = [];
    const unsub = onSnapshot(collection(db, "rooms"), (room) => {
      setRooms([]);
      room.docs.forEach((doc) => {
        if (
          !doc.data()?.private ||
          (doc.data()?.private && doc.data()?.owner.uid == currentUser.uid)
        ) {
          setRooms((prev) => [...prev, { ...doc.data(), key: doc.id }]);
        }
      });
      setLoading(false);
    });

    return () => unsub();
  }, []);
  return (
    <div style={{ margin: "20px auto" }}>
      {loading ? (
        <Loading size="large">loading..</Loading>
      ) : (
        rooms.map((room) => (
          <Card
            hoverable={true}
            key={room.key}
            style={{ marginBottom: 14 }}
            width="100%"
          >
            <div
              style={{
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
              }}
            >
              <h4>{room.room_name}</h4>
              {room?.owner.uid == currentUser.uid &&
                (load ? (
                  <Spinner />
                ) : (
                  <div style={{ cursor: "pointer" }}>
                    <Trash2 onClick={() => deleteRoom(room.key)} color="red" />
                  </div>
                ))}
            </div>
            <Tag type="lite">{room.members.length} members</Tag>
            <Card.Footer>
              <Link to={`/join/${room.key}`}>
                <p>Join room</p>
              </Link>
            </Card.Footer>
          </Card>
        ))
      )}
    </div>
  );
};

export default Rooms;

all rooms所有聊天室


创建一个聊天室

创建聊天室组件是用来创建聊天室的。我们会显示一个模态框来获取创建聊天室需要的信息,例如聊天室名称和可见性状态。当用户点击 Create Room 按钮时,我们就使用addDoc函数在 Cloud Firestore 数据库里添加一个新文档。

const Modal_ = ({ state, title, setState }) => {
  const { currentUser } = useContext(AuthContext);
  const [loading, setLoading] = useState(false);
  const [name, setName] = useState("");
  const history = useHistory();
  const [hidden, setHidden] = useState(false);
const createRoom = async () => {
    if (name.trim().length > 0) {
      setLoading(true);
const room = await addDoc(collection(db, "rooms"), {
        room_name: name,
        created_at: new Date(),
        private: hidden,
        owner: {
          uid: currentUser.uid,
          pic: currentUser.photoURL,
          name: currentUser.displayName,
        },
        members: [],
      });
history.push({
        pathname: `/join/${room.id}`,
      });
setLoading(false);
      setState(false);
    }
  };
return (
    <Modal open={state} onClose={() => setState(false)}>
      <Modal.Title>{title}</Modal.Title>
      <Modal.Content>
        <Input
          width="100%"
          required
          size="large"
          placeholder="room name"
          onChange={(e) => setName(e.target.value)}
        />
        <Select
          placeholder="Visibility"
          width="100%"
          style={{ marginTop: 20 }}
          onChange={(e) => {
            if (e == 1) setHidden(false);
            else setHidden(true);
          }}
        >
          <Select.Option value="1">Public</Select.Option>
          <Select.Option value="2">Private</Select.Option>
        </Select>
      </Modal.Content>
      <Modal.Action passive onClick={() => setState(false)}>
        Cancel
      </Modal.Action>
      <Modal.Action loading={loading} onClick={createRoom}>
        create
      </Modal.Action>
    </Modal>
  );
};
export default Modal_;

Creat room创建聊天室


加入聊天室组件

我们使用加入聊天室组件来让用户加入聊天室。当用户点击 Join Room 按钮时,我们就把当前用户添加进音频流。首先,我们要调用 API 到一个服务器为当前用户生成一个验证令牌。然后,API 会向我们返回用来加入音频流的令牌。接下来,当用户加入聊天室后,我们就立即把他们的信息添加到聊天室对应文档的 Cloud Firestore 数据库里。最后,我们使用声网 SDK 的play()函数在 div 标签下使用 meID 开启声网音频流。

const join = async () => {
    setLoad(true);
    const { token, uid } = await (
      await fetch(
        `https://agora-token.azurewebsites.net/api/trigger?name=${room}`
      )
    ).json();
    client.join(token, room, uid, async (userId) => {
      setStreamId(userId);
      localStorage.setItem("ID", id);
      localStorage.setItem("streamId", userId);
      let new_active = active.filter((user) => user.uid != currentUser.uid);
      new_active.push({
        uid: currentUser.uid,
        pic: currentUser.photoURL,
        name: currentUser.displayName,
        userId: userId,
      });
      await updateDoc(doc(db, "rooms", id), {
        members: new_active,
      })
        .then(() => {
          let localStream = AgoraRTC.createStream({
            audio: true,
            video: false,
          });
          setstream(localStream);
          localStream.init(() => {
            localStream.play("me");
            client.publish(localStream, handleError);
          }, handleError);
          setLoad(false);
          setConn_state(client.getConnectionState());
          setStreamId(userId);
        })
        .catch((e) => setLoad(false));
    });
  };


测试

首先我们要开启 dev 服务器,然后点击创建聊天室,创建一个包含所有细节的聊天室并加入这个聊天室。这时候打开另一个浏览器浏览这个网站,就可以看到我们之前创建的聊天室啦(如果是公共聊天室的话)。点击加入聊天室后就会在屏幕上看到有2位参与者,屏幕底部是静音、改变音频流质量、输入和输出设备的控制键。到这里配置就完成啦。下面是测试过程的截图。
0001加入聊天室
0002聊天室内
audio profiles音频文件


总结

以上就是所有步骤啦!在声网 SDK 等技术的帮助下,我们可以搭建一个成熟的、生产级的网页版语音 SDK。声网兼顾了所有后端需求,所以我们不必担心遇到无法解决的复杂问题,并且声网提供每个月 10000 分钟免费使用时间哦。


其他资源


原文作者:Manitej Pratha Manitej Pratha是一位热爱研发的学生开发者,ReactJS爱好者,Javascript发烧友,对Web、Android和Azure Cloud也有浓厚兴趣。可以在manitej.rocks了解更多关于Manitej 的信息哦!
原文链接:Building a Voice Chat App Using React and the Agora SDK
注册登录 后评论
    // 作者
    声网技术社区 发布于 声网开发者社区
    • 0
    // 本帖子
    关键词
    // 相关帖子
    Coming soon...
    • 0
    用 React 和声网 SDK 创建一个语音聊天应用(语音 SDK)声网技术社区 发布于 声网开发者社区