1

我想在没有任何付费库的情况下在 React-native 和 node js 中实现电话会议

我是使用 react-native-webrtc 和 node js Socket 使用 Peer-to-Peer 实现单人视频通话

下面是我的 React-Native 文件

我的 Privider 文件


import React, { useState } from "react";
import { Alert } from "react-native";
import {
  mediaDevices,
  MediaStream,
  MediaStreamConstraints,
} from "react-native-webrtc";
import socketio from "socket.io-client";
import ReactNativeForegroundService from "@supersami/rn-foreground-service";
import { MainContext as MainContextType, User } from "../interfaces";
import {
  SERVER_URL,
  PEER_SERVER_HOST,
  PEER_SERVER_PORT,
  PEER_SERVER_PATH,
} from "../server";
// @ts-ignore
import Peer from "react-native-peerjs";
import { navigate } from "../helpers/RootNavigation";
import InCallManager from "react-native-incall-manager";
const initialValues: MainContextType = {
  username: "",
  peerId: "",
  users: [],
  localStream: null,
  remoteStream: null,
  remoteUser: null,
  initialize: () => {},
  setUsername: () => {},
  call: () => {},
  switchCamera: () => {},
  toggleMute: () => {},
  isMuted: false,
  isShareScreen: false,
  toggleSpeacker: () => {},
  isSpeaker: true,
  swipeWindow: false,
  closeCall: () => {},
  reset: () => {},
  localStreamSet: () => {},
  activeCall: null,
};

export const MainContext = React.createContext(initialValues);

interface Props {}

const MainContextProvider: React.FC<Props> = ({ children }) => {
  const [username, setUsername] = useState(initialValues.username);
  const [peerId, setPeerId] = useState(initialValues.peerId);
  const [users, setUsers] = useState<User[]>(initialValues.users);
  const [localStream, setLocalStream] = useState<MediaStream | null>(
    initialValues.localStream
  );
  const [remoteStream, setRemoteStream] = useState<MediaStream | null>(
    initialValues.remoteStream
  );
  const [remoteUser, setRemoteUser] = useState<User | null>(null);
  const [socket, setSocket] = useState<SocketIOClient.Socket | null>(null);
  const [peerServer, setPeerServer] = useState<any>(null);
  const [isMuted, setIsMuted] = useState(initialValues.isMuted);
  const [swipeWindow, setSwipeWindow] = useState(initialValues.swipeWindow);
  const [isShareScreen, setIsShareScreen] = useState(
    initialValues.isShareScreen
  );
  // const localStreamSet=(newstream)=>{
  //   setLocalStream(null)
  // }
  const [isSpeaker, setIsSpeaker] = useState(initialValues.isSpeaker);
  const [activeCall, setActiveCall] = useState<any>(null);

  const initialize = async () => {
    const isFrontCamera = true;
    const devices = await mediaDevices.enumerateDevices();

    const facing = isFrontCamera ? "front" : "environment";
    const videoSourceId = devices.find(
      (device: any) => device.kind === "videoinput" && device.facing === facing
    );
    const facingMode = isFrontCamera ? "user" : "environment";
    const constraints: MediaStreamConstraints = {
      audio: true,
      video: {
        mandatory: {
          minWidth: 1280,
          minHeight: 720,
          minFrameRate: 30,
        },
        facingMode,
        optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
      },
    };

    const newStream = await mediaDevices.getUserMedia(constraints);

    setLocalStream(newStream as MediaStream);

    const io = socketio.connect(SERVER_URL, {
      reconnection: true,
      autoConnect: true,
    });

    io.on("connect", () => {
      setSocket(io);
      io.emit("register", username);
    });

    io.on("users-change", (users: User[]) => {
      setUsers(users);
    });

    io.on("accepted-call", (user: User) => {
      InCallManager.start("video");


      setRemoteUser(user);
    });

    io.on("rejected-call", (user: User) => {
      InCallManager.stop();
      InCallManager.start();

      setRemoteUser(null);
      setActiveCall(null);
      Alert.alert("Your call request rejected by " + user?.username);
      navigate("Users");
    });

    io.on("not-available", (username: string) => {
      setRemoteUser(null);
      setActiveCall(null);
      Alert.alert(username + " is not available right now");
      navigate("Users");
    });

    const peerServer = new Peer(undefined, {
      host: PEER_SERVER_HOST,
      path: PEER_SERVER_PATH,
      secure: true,
      port: PEER_SERVER_PORT,
      config: {
        iceServers: [
          {
            urls: [
              "stun:stun1.l.google.com:19302",
              "stun:stun2.l.google.com:19302",
            ],
          },
        ],
      },
    });

    peerServer.on("error", (err: Error) =>

    );

    peerServer.on("open", (peerId: string) => {
      setPeerServer(peerServer);
      setPeerId(peerId);
      io.emit("set-peer-id", peerId);
    });

    io.on("call", (user: User) => {
      peerServer.on("call", (call: any) => {
        InCallManager.startRingtone("_BUNDLE_");
        setRemoteUser(user);
        Alert.alert(
          "New Call",
          "You have a new call from " + user?.username,
          [
            {
              text: "Reject",
              onPress: () => {
                InCallManager.stopRingtone();
                InCallManager.stop();
                io.emit("reject-call", user?.username);
                setRemoteUser(null);
                setActiveCall(null);
              },
              style: "cancel",
            },
            {
              text: "Accept",
              onPress: () => {
                InCallManager.stopRingtone();
                InCallManager.start();
                InCallManager.setSpeakerphoneOn(true);
                io.emit("accept-call", user?.username);
                call.answer(newStream);
                setActiveCall(call);
                navigate("Call");
              },
            },
          ],
          { cancelable: false }
        );

        call.on("stream", (stream: MediaStream) => {


          setRemoteStream(stream);
        });

        call.on("close", () => {
          closeCall();
        });

        call.on("error", () => {});
      });
    });
  };

  const call = (user: User) => {
    if (!peerServer || !socket) {
      Alert.alert("Peer server or socket connection not found");
      return;
    }

    if (!user.peerId) {
      Alert.alert("User not connected to peer server");
      return;
    }

    socket.emit("call", user.username);

    setRemoteUser(user);

    try {
      const call = peerServer.call(user.peerId, localStream);

      call.on(
        "stream",
        (stream: MediaStream) => {
          setActiveCall(call);
          setRemoteStream(stream);
        },
        (err: Error) => {
          console.error("Failed to get call stream", err);
        }
      );
    } catch (error) {

    }
  };

  const switchCamera = () => {
    if (localStream) {
      // @ts-ignore
      localStream.getVideoTracks().forEach((track) => track._switchCamera());
    }
  };

  const toggleMute = () => {
    if (localStream)
      localStream.getAudioTracks().forEach((track) => {
        track.enabled = !track.enabled;
        setIsMuted(!track.enabled);
      });
  };

  const toggleswipeWindow = () => {
    var templocalstream = localStream;
    var tempremotestream = remoteStream;
    if (swipeWindow == false) {
      setLocalStream(tempremotestream);
      setRemoteStream(templocalstream);
      setSwipeWindow(!swipeWindow);
    } else {
      setLocalStream(tempremotestream);
      setRemoteStream(templocalstream);
      setSwipeWindow(!swipeWindow);
    }
  };
  const toggleScreenShare = async () => {
    if (localStream)
      if (isShareScreen == false) {

        mediaDevices
          .getDisplayMedia({ video: true, audio: true })
          .then(handleSuccess, handleError);
        InCallManager.setKeepScreenOn(true);
        setIsShareScreen(true);
      } else {
        // localStream.getVideoTracks().forEach((track) => {track.stop()}
        ReactNativeForegroundService.stop();
        InCallManager.setKeepScreenOn(false);
        setIsShareScreen(false);
      }
  };
  const toggleSpeacker = () => {
    if (localStream) InCallManager.start();
    InCallManager.setSpeakerphoneOn(!isSpeaker);
    setIsSpeaker(!isSpeaker);
  };

  const closeCall = () => {
    activeCall?.close();
    setActiveCall(null);
    setRemoteUser(null);
    navigate("Users");
    Alert.alert("Call is ended");
  };

  const reset = async () => {
    peerServer?.destroy();
    socket?.disconnect();
    setActiveCall(null);
    setRemoteUser(null);
    setLocalStream(null);
    setRemoteStream(null);
    setUsername("");
    setPeerId("");
  };
  const handleError = async (error) => {

  };
  const handleSuccess = async (stream) => {


    localStream.getVideoTracks().forEach((track) => {


      localStream.removeTrack(track);

    });

    localStream.addTrack(stream.getTracks()[0]);


    if (swipeWindow == true) {
      setRemoteStream(stream);
    } else {
      setLocalStream(stream);
    }

    stream.getVideoTracks()[0].addEventListener("ended", () => {
    
    });
  };
  return (
    <MainContext.Provider
      value={{
        username,
        setUsername,
        peerId,
        setPeerId,
        users,
        setUsers,
        localStream,
        setLocalStream,
        remoteStream,
        setRemoteStream,
        initialize,
        call,
        switchCamera,
        toggleMute,
        isMuted,
        isSpeaker,
        toggleSpeacker,
        closeCall,
        reset,
        remoteUser,
        activeCall,
        toggleScreenShare,
        isShareScreen,
        toggleswipeWindow,
        swipeWindow,
        // localStreamSet
      }}
    >
      {children}
    </MainContext.Provider>
  );
};

export default MainContextProvider;


Callscreen.js 文件

import React, {useContext} from 'react';
import {
  ActivityIndicator,
  Dimensions,
  StyleSheet,
  Text,
  View,
  TouchableWithoutFeedback,
  TouchableHighlight
} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {RTCView} from 'react-native-webrtc';
import IconButton from '../components/IconButton';
import icons from '../constants/icons';
import {CallScreenNavigationProp} from '../interfaces/navigation';
import {MainContext} from '../store/MainProvider';
import InCallManager from 'react-native-incall-manager';
const {width, height} = Dimensions.get('window');
import RecordScreen from 'react-native-record-screen';
import Video from 'react-native-video';
interface Props {
  navigation: CallScreenNavigationProp;
}

const Call = ({}: Props) => {
  const {
    localStream,
    remoteStream,
    activeCall,
    remoteUser,
    isMuted,
    isSpeaker,
    toggleSpeacker,
    toggleScreenShare,
    isShareScreen,
    closeCall,
    toggleMute,
    switchCamera,
    swipeWindow,
    toggleswipeWindow
  } = useContext(MainContext);

React.useEffect(() => {

  if (InCallManager.recordPermission !== 'granted') {

    InCallManager.requestRecordPermission()

    .then((requestedRecordPermissionResult) => {

    })
    .catch((err) => {

    });
}
  });
const [recordscreen,setRecordScreen]=React.useState(false)
const btnStyle = React.useMemo(() => {
  return recordscreen ? styles.btnActive : styles.btnDefault;
}, [recordscreen]);

  const [uri, setUri] = React.useState('');
const _handleOnRecording = async () => {
  if (recordscreen) {
    setRecordScreen(false);
    const res = await RecordScreen.stopRecording().catch((error) =>
      console.warn(error)
    );

    if (res) {
      setUri(res.result.outputURL);
    }
  } else {
    setUri('');
    setRecordScreen(true);
    await RecordScreen.startRecording().catch((error) => {
      console.warn(error);
      setRecordScreen(false);
      setUri('');
    });
  }
};
  return (
    <SafeAreaView style={styles.container}>
    {uri ? (
      <View style={styles.preview}>
        <Video
          source={{
            uri,
          }}
          style={styles.video}
        />
      </View>
    ) : null}
      {remoteStream && (
        <RTCView
          key={2}
          mirror={true}
          style={styles.remoteStream}
          streamURL={remoteStream.toURL()}
          objectFit="cover"
        />
      )}


      {localStream && (
        <View style={styles.myStreamWrapper}>
        <TouchableWithoutFeedback onPress={toggleswipeWindow}>
          <RTCView
            style={styles.myStream}
            objectFit="cover"
            streamURL={localStream.toURL()}
            zOrder={1}
          />
          </TouchableWithoutFeedback>
        </View>
      )}
      {!activeCall && (
        <View style={styles.spinnerWrapper}>
          <ActivityIndicator color="#341EFF" size={120} />
          <Text style={styles.callingText}>Calling {remoteUser?.username}</Text>
        </View>
      )}
      <View style={styles.iconsWrapper}>
        <IconButton
          icon={icons.CHANGE_CAMERA}
          onPress={switchCamera}
          iconColor={'#341EFF'}
          backgroundColor="#fff"
        />

        {isMuted ? (
          <IconButton
            icon={icons.UNMUTE}
            onPress={toggleMute}
            iconColor={'#fff'}
            backgroundColor="red"
          />
        ) : (
          <IconButton
            icon={icons.MUTE}
            onPress={toggleMute}
            iconColor={'#341EFF'}
            backgroundColor="#fff"
          />
        )}
        {isSpeaker ? (
          <IconButton
            icon={icons.SPEAKERON}
            onPress={toggleSpeacker}
            iconColor={'#341EFF'}
            backgroundColor="#fff"
          />
        ) : (
          <IconButton
            icon={icons.SPEAKEROFF}
            onPress={toggleSpeacker}
            iconColor={'#341EFF'}
            backgroundColor="#fff"
          />
        )}
        {recordscreen ? (
          <IconButton
            icon={icons.RECORDING}
            onPress={_handleOnRecording}
            iconColor={'#fff'}
            backgroundColor="red"
          />
        ) : (
          <IconButton
            icon={icons.RECORDING}
            onPress={_handleOnRecording}
            iconColor={'#341EFF'}
            backgroundColor="#fff"
          />
        )}

        {isShareScreen ? (
          <IconButton
            icon={icons.SCREENSHARE}
            onPress={toggleScreenShare}
            iconColor={'#fff'}
            backgroundColor="red"
          />
        ) : (
          <IconButton
            icon={icons.SCREENSHARE}
            onPress={toggleScreenShare}
            iconColor={'#341EFF'}
            backgroundColor="#fff"
          />
        )}
        <IconButton
          icon={icons.END_CALL}
          onPress={closeCall}
          iconColor={'#fff'}
          backgroundColor="red"
        />
      </View>
    </SafeAreaView>
  );
};

export default Call;

const styles = StyleSheet.create({
  preview: {
    position: 'absolute',
    right: 12,
    bottom: 116,
    width: Dimensions.get('window').width / 2,
    height: Dimensions.get('window').height / 3,
    zIndex: 1,
    padding: 8,
    backgroundColor: '#aaa',
  },
  video: {
    flex: 1,
  },
  container: {
    backgroundColor: '#0f0f0f',
    flex: 1,
    position: 'relative',
  },
  btnDefault: {
    width: 48,
    height: 48,
    backgroundColor: '#fff',
    borderRadius: 24,
    borderWidth: 4,
    borderStyle: 'solid',
    borderColor: '#212121',
  },
  btnActive: {
    width: 36,
    height: 36,
    backgroundColor: 'red',
    borderRadius: 8,
  },
  myStream: {
    height: width * 0.6,
    width: width * 0.4,
  },
  myStreamWrapper: {
    position: 'absolute',
    bottom: 20,
    right: 20,
    height: width * 0.6 + 8,
    width: width * 0.4 + 8,
    backgroundColor: '#333',
    borderRadius: 12,
    overflow: 'hidden',
    justifyContent: 'center',
    alignItems: 'center',
  },
  remoteStreamWrapper: {},
  remoteStream: {
    width: '100%',
    height: '100%',
  },
  spinnerWrapper: {
    top: height * 0.3,
    width: '100%',
    justifyContent: 'center',
    alignItems: 'center',
  },
  callingText: {
    fontSize: 26,
    color: '#fff',
  },
  iconsWrapper: {
    position: 'absolute',
    bottom: 20,
    left: 20,
  },
});

套接字服务器.js

//socketio
const socketio = require("socket.io");

class SocketService {
  io;

  constructor() {
    this.io = null;
  }

  listen = (server) => {
    this.io = socketio(server);
    this.io.users = {};
    this.io.on("connection", (socket) => {
      socket.on("register", (username) => this.onRegister(socket, username));
      socket.on("set-peer-id", (peerId) => this.onSetPeerId(socket, peerId));
      socket.on("call", (username) => this.onCall(socket, username));
      socket.on("reject-call", (username) =>
        this.onRejectCall(socket, username)
      );
      socket.on("accept-call", (username) =>
        this.onAcceptCall(socket, username)
      );
      console.log(`${Date(Date.now()).toLocaleString()}: new user connected`);
      socket.on("disconnect", () => this.onDisconnect(socket));
    });
  };

  onAcceptCall = (socket, username) => {
    if (this.io.users[username])
      this.io
        .to(this.io.users[username].socketId)
        .emit("accepted-call", this.io.users[socket.username]);
  };

  onRejectCall = (socket, username) => {
    if (this.io.users[username]) {
      this.io
        .to(this.io.users[username].socketId)
        .emit("rejected-call", this.io.users[socket.username]);
    }
  };

  onCall = (socket, username) => {
    if (this.io.users[username]) {
      this.io
        .to(this.io.users[username].socketId)
        .emit("call", this.io.users[socket.username]);
    } else {
      socket.emit("not-available", username);
    }
  };

  onRegister = (socket, username) => {
    console.log("Registered", username);
    socket.username = username;
    this.io.users[username] = {
      username,
      peerId: "",
      socketId: socket.id,
    };
    this.onUsersChange(socket);
  };

  getUsers = () => {
    const users = [];
    Object.keys(this.io.users).forEach((key) => {
      users.push(this.io.users[key]);
    });
    return users;
  };

  onUsersChange = (socket) => {
    this.io.emit("users-change", this.getUsers());
  };

  onSetPeerId = (socket, peerId) => {
    console.log("Set Peer Id user:", socket.username, " peerId: ", peerId);
    this.io.users[socket.username] = {
      peerId,
      socketId: socket.id,
      username: socket.username,
    };
    this.onUsersChange();
  };

  onDisconnect = (socket) => {
    delete this.io.users[socket.username];
    console.log(
      `${Date(Date.now()).toLocaleString()} ID:${
        socket.username
      } user disconnected`
    );
    this.onUsersChange();
  };

  emit = (event, userId, data) => {
    if (this.io.users[userId]) {
      this.io.to(this.io.users[userId]).emit(event, data);
    }
  };
}

module.exports = new SocketService();

Peer Server.js(Node js 中的第二个 API 服务器)

require('dotenv').config()
const express = require("express");
const { ExpressPeerServer } = require("peer");

const app = express();

app.get("/", (req, res, next) => res.send("Hello world!"));

const http = require("http");

const server = http.createServer(app);
const peerServer = ExpressPeerServer(server, {
  debug: true,
  path: "/",
});

app.use("/peerjs", peerServer);

server.listen(process.env.PORT || 9000);

4

0 回答 0