WebRTC using clients Python(aiortc) + ReactNative(webrtc)

288 views Asked by At

I just need an answer to this question. Is it possible to connect with webRTC python clent 1 and react native client 2 with server springBoot and livestreav real time video and audio?

i am doing it in react native and it is working fine.But when i am doing the same with python its not connecting.It says in python ICE connection stage is checking and in react native it says ICE connection stage changes to Failed.I dont know what is the problem

Here is the react Native code(Offer)

import React, { useEffect, useState,useRef } from 'react';
import { View, Text,StyleSheet, ActivityIndicator,TouchableHighlight,Button,PermissionsAndroid } from 'react-native';
import { RTCPeerConnection,RTCView, mediaDevices,RTCIceCandidate, RTCSessionDescription } from 'react-native-webrtc';
import EntypoIcon from 'react-native-vector-icons/Entypo'
import SockJsClient from 'react-stomp';
import axios from 'axios';
import { err } from 'react-native-svg/lib/typescript/xml';

const RTCScreen = () => {
  const [isRecorded, setisRecorded] = useState(false);
  const [remoteStreamURL, setRemoteStreamURL] = useState(null);
  const [localStream, setlocalStream] = useState(null);
  const peerConnection = useRef(null);
  const ws = useRef(null);
  let collectedCandidates = [];

  useEffect(() => {
    requestCameraPermission()
    localStreamSetup();
    const configuration = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
    peerConnection.current = new RTCPeerConnection(configuration);

    peerConnection.current.oniceconnectionstatechange = (event) => {
      console.log("ICE connection state change:", peerConnection.current.iceConnectionState,event);
    };
    
    peerConnection.current.onicegatheringstatechange = (event) => {
      console.log("ICE gathering state change:", peerConnection.current.iceGatheringState);
    };
    
    peerConnection.current.onicecandidateerror = (event) => {
      console.error("ICE candidate error:", event.errorCode, event.errorText);
    };



      return () => {
        // Close peer connection
        if (peerConnection.current) {
          peerConnection.current.close();
        }
      };
  },[]);

  try{
    peerConnection.current.ontrack = event => {
      console.log("Track Recieved");
      console.log(event.streams[0]);
      setRemoteStreamURL(event.streams[0].toURL());
    };
  }catch(error){
    console.log(error);
  }

  try{
    peerConnection.current.onicecandidate = (event) => {
      if (event.candidate) {
        console.log(event.candidate);
        collectedCandidates.push(event.candidate);
        // You can use the collectedCandidates array when sending the offer
      }
    };
  }
  catch(error){
    console.log(error);
  }

  async function localStreamSetup(){
    await mediaDevices.getUserMedia({ audio: true, video: true })
      .then(stream => {
        setlocalStream(stream);
        stream.getTracks().forEach(track => peerConnection.current.addTrack(track, stream));
      })
      .catch(error => {
        console.log('Error accessing media devices:', error);
      });
  }



  const startCall = () => {
    // Send a request to the server to initiate a call
    this.clientRef.sendMessage('/app/audio/python-topic/rtc',JSON.stringify({ type: 'start-call' }));
  };


  function sendMessage(msg){
    this.clientRef.sendMessage('/app/audio/python-topic/rtc', msg);
    // console.log("Message Sent:"+msg);
  }

  async function messageHandler(message){
    const messageString = JSON.stringify(message);
    convertedMessage = JSON.parse(messageString);;
    if (convertedMessage.type === 'answer') {
      // Handle incoming offer from server
      const remoteDesc = new RTCSessionDescription(convertedMessage.answer);
      await peerConnection.current.setRemoteDescription(remoteDesc);
      convertedMessage.candidate.forEach(async candidate => {
        console.log(candidate);
        await peerConnection.current.addIceCandidate(candidate);
      });
      // handleOffer(message);
    }
  }

  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  async function makeCall() {
    console.log("Making offer...");
    const offer = await peerConnection.current.createOffer();
    await peerConnection.current.setLocalDescription(offer);
    await delay(2000);
    // Collect candidates before sending the offer
    console.log("Copying candidates...!");
    const candidatesToSend = collectedCandidates.slice(); // Create a copy of collected candidates
    await delay(1000);
    sendMessage(JSON.stringify({"type":"offer",'offer': offer, "candidates": candidatesToSend}));
    console.log("Offer sent!");
  }

  const requestCameraPermission = async () => {
    try {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.CAMERA,
        {
          title: 'Cool Photo App Camera Permission',
          message:
            'Cool Photo App needs access to your camera ' +
            'so you can take awesome pictures.',
          buttonNeutral: 'Ask Me Later',
          buttonNegative: 'Cancel',
          buttonPositive: 'OK',
        },
      );
      if (granted === PermissionsAndroid.RESULTS.GRANTED) {
        console.log('You can use the camera');
      } else {
        console.log('Camera permission denied');
      }
    } catch (err) {
      console.warn(err);
    }

    try {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
        {
          title: 'Microphone Permission',
          message: 'This app needs access to your microphone.',
          buttonNeutral: 'Ask Me Later',
          buttonNegative: 'Cancel',
          buttonPositive: 'OK',
        }
      );
      if (granted === PermissionsAndroid.RESULTS.GRANTED) {
        console.log('Microphone permission granted');
        // Proceed with accessing the microphone or setting up WebRTC
        // Call the function for setting up WebRTC here, for instance
      } else {
        console.log('Microphone permission denied');
        // Handle denial of permission, show a message, etc.
      }
    } catch (err) {
      console.warn('Error requesting microphone permission:', err);
    }
  };
  
  

  return (
    <View style={styles.container}>
      <Text>Real Time Communication</Text>
      <SockJsClient url='http://192.168.1.10:9091/audio-websocket' topics={['/topic/python-topic/rtc']}
              onMessage={(msg) => { console.log("Message Recieved:"+JSON.stringify(msg));messageHandler(msg); }}
              ref={ (client) => { this.clientRef = client }} />
      <Button title="Start Call" onPress={makeCall} />
      {/* Display remote stream (if available) */}
      {localStream && <RTCView streamURL={localStream.toURL()} zOrder={20}  objectFit={"cover"} mirror={true} style={{ width: 200, height: 150 }} />}
      {remoteStreamURL && <RTCView streamURL={remoteStreamURL} zOrder={20}  objectFit={"cover"} mirror={true} style={{ width: 200, height: 150 }} />}
    </View>
  );
};

export default RTCScreen;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    gap: 20,
    backgroundColor: '#f2f2f2',
  },
});

and the python Client:

async def RTCSetup(producer):
    ws = create_connection("ws://192.168.1.10:9091/audio-websocket")
    v = str(random.randint(0, 1000))
    ws.send("CONNECT\naccept-version:1.0,1.1,2.0\n\n\x00\n")
    sub = stomper.subscribe(dest="/topic/python-topic/rtc/raspberry",idx=v, ack='auto')
    ws.send(sub)
    producer.send("python-topic",json.dumps({"type": "start-call"}).encode('utf-8'),partition=1)
    ice_servers = [
        {"urls": "stun:stun.l.google.com:19302"},  # Example Google STUN server
    ]
    print(ice_servers[0])
    print(ice_servers[0]['urls'][0])
    config = RTCConfiguration(iceServers=ice_servers)
    pc = RTCPeerConnection(config)

    gather_complete = asyncio.Event()

    @pc.on("track")
    async def on_track(track):
        print("Track received")
        print(track.kind)
        pc.addTrack(track)
    @pc.on("iceconnectionstatechange")
    async def on_iceconnectionstatechange():
        print(f"ICE connection state is {pc.iceConnectionState}")

    @pc.on("connectionstatechange")
    async def on_connectionstatechange():
        print(f"Connection state is {pc.connectionState}")

    @pc.on("signalingstatechange")
    async def on_signalingState():
        print(f"Signal state is {pc.signalingState}")
    
    @pc.on("icegatheringstatechange")
    async def on_icegatheringstatechange():
        print(f"ICE gathering state is {pc.iceGatheringState}")
        if(pc.iceGatheringState == 'complete'):
            gather_complete.set()
    cardidates = []
    while True:
        # Handle incoming messages
        message = ws.recv()
        try:
            dataMsg = message.splitlines()
            print(dataMsg[7])
            msg = dataMsg[7]
            cleaned_string_data = ''.join(filter(lambda x: x in string.printable, msg))
            Msgdata = json.loads(cleaned_string_data)
            print(Msgdata['type'])
            message_type = Msgdata['type']
            
            if message_type == "offer":
                print("Offer recieved!")
                print("Creating video track...")
                # video_track = await create_video_track()
                audio_track = AudioStreamTrack()
                # video_track = VideoStreamTrack()
                video_track = VideoTrack()
                frame_reception_task = asyncio.create_task(receive_frames(video_track))
                print("Adding video track...")
                # pc.addTrack(video_track)
                pc.addTrack(audio_track)
                pc.addTrack(video_track)
                print("Streaming media...")
                answer = await create_answer(pc,Msgdata['offer'],Msgdata['candidates'],cardidates,gather_complete)
                print("Sending answer...")
                producer.send("python-topic",json.dumps({"type": "answer", "answer": answer,"candidate":cardidates}).encode('utf-8'),partition=1)
                print("answer sent successfully!!")

            elif message_type == "ice-candidate":
                # Handle incoming ICE candidate from the server
                # handle_ice_candidate(data.get("candidate"))
                print("ICE")
        except Exception as error:
            print(error)
    


async def create_answer(pc,offer,candidate,cardidatesToSend,gather_complete :Event):
    # print(offer)
    # Assuming 'offer' is the received offer as a dictionary

    # Assuming 'offer' contains SDP description and ICE candidates

    print("Setting peer description...")
    lines = offer['sdp'].split('\r\n')
    candidatesT = []
    sdp = ""
    for line in lines:
        if line.startswith('a=candidate:'):
            candidatesT.append(line)
        else:
            sdp +=line+"\r\n"

    await pc.setRemoteDescription(RTCSessionDescription(
        sdp=offer['sdp'],
        type=offer['type']
    ))
    print("Peer description set!!")

    print("Creating answer...")
    # Assuming 'pc' is the initialized peer connection
    answer = await pc.createAnswer()

    print("Setting local description...")

    await pc.setLocalDescription(answer)

    # await gather_complete.wait()

    await asyncio.sleep(2)
    
    print("Adding cardidates...")
    # Add ICE candidates
    for candidate_str in candidate:
        parts = candidate_str['candidate'].split()
        Newcandidate = RTCIceCandidate(
            foundation=parts[0],
            ip=parts[4],
            port=int(parts[5]),
            priority=int(parts[3]),
            protocol=parts[2],
            type=parts[7],
            sdpMid=candidate_str['sdpMid'],
            sdpMLineIndex=candidate_str['sdpMLineIndex'],
            component=1,
            # Add other arguments as necessary based on your ICE candidate format
        )
        print(Newcandidate)
        await pc.addIceCandidate(Newcandidate)

    await asyncio.sleep(1)
    print("Generating SDP and CARDIDATES...")
    lines = pc.localDescription.sdp.split('\r\n')
    sdpToSend = ""
    for line in lines:
        if line.startswith('a=candidate:'):
            newLine = str(line).replace("a=",'')
            parts = newLine.split()
            cardinateJSON = {"candidate":newLine,"sdpMid":candidate_str['sdpMid'],"sdpMLineIndex":candidate_str['sdpMLineIndex']}
            cardidatesToSend.append(cardinateJSON)
        else:
            sdpToSend +=line+"\r\n"
    
    print("Returning answer...")
    await asyncio.sleep(1)
    return {
        'sdp': pc.localDescription.sdp,
        'type': pc.localDescription.type,
    }

0

There are 0 answers