对等连接状态在webRTC中始终保持为“ RTCICEConnectionChecking”状态

问题描述 投票:0回答:1

我正在使用webRTC进行视频通话功能。我使用的是“ Google webRTC”框架而不是libJingle。建立对等连接后,它始终保持在“ RTCICEConnectionChecking”状态。

我有几个问题。

1)对等连接状态始终保持在“ RTCICEConnectionChecking”。

2]当网络不同时(3g / 4g),视频通话为不工作

3)相同的网络运行正常。

我使用了很多转弯服务器,但无法成功。

请先建议我,谢谢。

代码段是

//
//  CallConnectViewController.swift
//  socketWebRTC
//

import UIKit
import AVFoundation
import SwiftHTTP
import AVKit
import CameraManager
import WebRTC

let TAG = "CallConnectViewController"
let VIDEO_TRACK_ID = TAG + "VIDEO"
let AUDIO_TRACK_ID = TAG + "AUDIO"
let LOCAL_MEDIA_STREAM_ID = TAG + "STREAM"


class CallConnectViewController: UIViewController,RTCPeerConnectionDelegate{


    func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
        print("Negotiation method call ====>")
    }




    @IBOutlet weak var remoteview_mask: UIView!

    @IBOutlet weak var openBtn: UIButton!
    @IBOutlet weak var dropBtn: UIButton!
    @IBOutlet weak var muteBtn: UIButton!
    @IBOutlet weak var remoteUserView: RTCEAGLVideoView!
    @IBOutlet weak var localUserView: RTCEAGLVideoView!
    @IBOutlet weak var testView:UIView!

    @IBOutlet weak var txtName: UITextField!
    @IBOutlet weak var txtToName: UITextField!
    @IBOutlet weak var indicator:UIActivityIndicatorView!

    var mediaStream: RTCMediaStream!
    var localVideoTrack: RTCVideoTrack!
    var localAudioTrack: RTCAudioTrack!
    var remoteVideoTrack: RTCVideoTrack!
    var remoteAudioTrack: RTCAudioTrack!
    var roomName: String!
    var isMute : Bool = false
    var peerFactory:RTCPeerConnectionFactory = RTCPeerConnectionFactory();
    let cameraManager = CameraManager()
    let uuid = UUID().uuidString
    var randomString = ""
    var roomID = ""
    var hangUP = false

    var sdpMLineIndex:AnyObject = "" as AnyObject
    var sdpMid:AnyObject = "" as AnyObject
    var candidate:AnyObject = "" as AnyObject


    var videoSource:RTCVideoSource?

    var timer = Timer()


    func Log(_ value:String) {
        print(TAG + " " + value)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        socket = SocketIOManager.sharedInstance.initSocket(uuid)
        NotificationCenter.default.addObserver(forName:NSNotification.Name.AVRouteDetectorMultipleRoutesDetectedDidChange , object:nil, queue:nil, using:catchNotification)

        self.initComponents();

        self.initWebRTC()
        self.randomString = self.randomString(length: 12)
        print("Random String ======>",self.randomString)
        mediaStream = peerConnectionFactory.mediaStream(withStreamId:self.randomString)
        localAudioTrack = peerConnectionFactory.audioTrack(withTrackId: AUDIO_TRACK_ID)
            mediaStream.addAudioTrack(localAudioTrack)
        self.timer = Timer.scheduledTimer(timeInterval: 5,
                                          target: self,
                                          selector: #selector(self.sendCandidate),
                                          userInfo: nil,
                                          repeats: true)

        self.indicator.isHidden = true  
    }


    func onLocalStreamReadyForRender() {
        let frame = localUserView!.frame
        let rtcVideoView = RTCCameraPreviewView.init(frame: CGRect.init())
        rtcVideoView.frame = frame
        rtcVideoView.frame.origin.x = 0
        rtcVideoView.frame.origin.y = 0
        self.localUserView?.addSubview(rtcVideoView)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        if(!hangUP){
            self.dropBtnPressed(UIButton())
        }
        self.timer.invalidate()
        self.socket?.disconnect()

    }

    func catchNotification(notification:Notification) -> Void {
        let interuptionDict = notification.userInfo
        if let interuptionRouteChangeReason = interuptionDict?[AVAudioSessionRouteChangeReasonKey] {
            let routeChangeReason = interuptionRouteChangeReason as! UInt
            switch (routeChangeReason) {
            case AVAudioSession.RouteChangeReason.categoryChange.rawValue:
                do {
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
                } catch {

                }
            default:
                break;
            }
        }
    }

    @IBAction func btnActionON(_ sender:UIButton){
        self.setAudioOutputSpeaker(enabled: true)
    }
    @IBAction func btnActionOFF(_ sender:UIButton){
        self.setAudioOutputSpeaker(enabled: false)
    }

    override func viewDidDisappear(_ animated: Bool) {
        UserDefaults.standard.setValue(false, forKey: "fromAPNS")
        UserDefaults.standard.synchronize()
    }

    func videoView(_ videoView: RTCEAGLVideoView!, didChangeVideoSize size: CGSize) {
        print("DidChange Video size:",size)
    }

    func showRoomDialog() {
        sigRecoonect()
    }

    func getRoomName() -> String {
        return (roomName == nil || roomName.isEmpty) ? txtToName.text ?? "123456" : roomName
    }

    var peerConnectionFactory: RTCPeerConnectionFactory! = nil
    var peerConnection: RTCPeerConnection! = nil
    var pcConstraints: RTCMediaConstraints! = nil
    var videoConstraints: RTCMediaConstraints! = nil
    var audioConstraints: RTCMediaConstraints! = nil
    var mediaConstraints: RTCMediaConstraints! = nil

    var socket = SocketIOManager.sharedInstance.socket
    var wsServerUrl: String! = nil
    var peerStarted: Bool = false

    func initWebRTC() {
        peerConnectionFactory = RTCPeerConnectionFactory()


        RTCInitializeSSL()
        peerConnectionFactory = RTCPeerConnectionFactory()

        let mandatoryConstraints = ["OfferToReceiveAudio": "true", "OfferToReceiveVideo": "false"]
        let optionalConstraints = [ "DtlsSrtpKeyAgreement": "true", "internalSctpDataChannels" : "true"]


        mediaConstraints = RTCMediaConstraints.init(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints)
    }

    func connect() {
        if (!peerStarted) {
            sendOffer()
            peerStarted = true
        }
    }

    func hangUp() {
        stop()
        sendDisconnect()
    }

    func stop() {
        if (peerConnection != nil) {
//            socket?.disconnect()
            //self.remoteUserView.removeFromSuperview()
            self.localUserView.removeFromSuperview()

            self.peerConnection.remove(self.mediaStream)
            self.peerConnection.close()
            self.peerConnection = nil
            self.peerStarted = false
        }
    }

    func prepareNewConnection() -> RTCPeerConnection {
        let url1 =  "stun:stun.l.google.com:19302"

//        let url2 = "turn:webrtcweb.com:7788"
//        let url3 = "turn:webrtcweb.com:4455"
//        let url4 = "turn:webrtcweb.com:7788?transport=udp"
//        let url5 = "turn:webrtcweb.com:7788?transport=tcp"
//        let url6 = "turn:webrtcweb.com:4455?transport=udp"
//        let url7 = "turn:webrtcweb.com:5544?transport=tcp"


        var icsServers: [RTCIceServer] = []

        icsServers.append(RTCIceServer(urlStrings: [url1], username:"",credential: ""))
       // icsServers.append(RTCIceServer(urlStrings: [url2,url3,url4,url5,url6,url7,], username: "muazka", credential: "muazka"))

        let rtcConfig: RTCConfiguration = RTCConfiguration()
        rtcConfig.tcpCandidatePolicy = RTCTcpCandidatePolicy.enabled
        rtcConfig.bundlePolicy = RTCBundlePolicy.maxBundle
        rtcConfig.rtcpMuxPolicy = RTCRtcpMuxPolicy.require
        rtcConfig.keyType = .ECDSA
        rtcConfig.iceTransportPolicy = .all
        rtcConfig.iceServers = icsServers;

        peerConnection = peerConnectionFactory.peerConnection(with: rtcConfig, constraints: mediaConstraints, delegate: self)
        peerConnection.add(mediaStream)
        return peerConnection;
    }






    func peerConnection(_ peerConnection: RTCPeerConnection?, didGetStats stats: [Any]?) {
        print("Peer connection states ===========????????>>>>>>>",stats)
    }

    /** Called any time the IceGatheringState changes. */
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState){}
    /** Called when a group of local Ice candidates have been removed. */
    public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]){}


    //MARK:- Manual Speaker Enagle and Disable
    func setAudioOutputSpeaker(enabled: Bool)
    {
        let session = AVAudioSession.sharedInstance()
        var _: Error?
        try? session.setCategory(AVAudioSession.Category.playAndRecord)
        try? session.setMode(AVAudioSession.Mode.voiceChat)
        if enabled {
            try? session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
        } else {
            try? session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
        }
        try? session.setActive(true)
    }

    func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
        var stateString: String = ""
        switch newState {
        case .new:
            stateString = "RTCICEConnectionNew"
        case .checking:
            stateString = "RTCICEConnectionChecking"
        case .connected:
            stateString = "RTCICEConnectionConnected"
        case .completed:
            stateString = "RTCICEConnectionCompleted"
        case .failed:
            stateString = "RTCICEConnectionFailed"
        case .disconnected:
            stateString = "RTCICEConnectionDisconnected"
        case .closed:
            stateString = "RTCICEConnectionClosed"
        default:
            stateString = "Unknown"
        }
        Log("ICE connection : \(stateString)")
        if(stateString == "RTCICEConnectionConnected"){
            DispatchQueue.main.async {
                self.indicator.stopAnimating()
                self.indicator.isHidden = true
                self.openBtn.isEnabled = true
                self.dropBtn.isEnabled = true

            }
        }
    }


    /** New ice candidate has been found. */
    public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate){


        print("iceCandidate: " + candidate.description)
        let json:[String: AnyObject] = [
            "type" : "candidate" as AnyObject,
            "sdpMLineIndex" : candidate.sdpMLineIndex as AnyObject,
            "sdpMid" : candidate.sdpMid as AnyObject,
            "candidate" : candidate.sdp as AnyObject
        ]
        sigSendIce(msg: json as NSDictionary)
    }

    func sigSendIce(msg:NSDictionary) {
        let strMessage = dictToJson(dictionary: msg as NSDictionary)
        socket!.emit("event", strMessage!)
    }

    @objc func adjustmentBestSongBpmHeartRate() {
        print("adklhaskldhaklsdhaklsdhklashdlkashdlaksdhl")
    }

    //This function call every time untill and unless the answer come
    @objc private func sendCandidate(){
        //Timer Methods call
        let json:[String: AnyObject] = [
            "type" : "candidate" as AnyObject,
            "candidate" : [
                "sdpMLineIndex" :  self.sdpMLineIndex,
                "sdpMid" : self.sdpMid,
                "candidate" : self.candidate as! String
                ] as AnyObject
        ]
        let candidate = json["candidate"] as! [String:AnyObject]
        if(candidate["candidate"] as! String != ""){
            print("Timer methods call for every seconds <=======================TIMER======================>",json)
            sigSend(json as NSDictionary)
        }
    }

    func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
        if (peerConnection == nil) {
            return
        }
        if (stream.audioTracks.count > 1 || stream.videoTracks.count > 1) {
            Log("Weird-looking stream: " + stream.description)
            return
        }
        if (stream.videoTracks.count == 1) {
            remoteVideoTrack = (stream.videoTracks[0] )
            remoteVideoTrack.isEnabled = true
            remoteVideoTrack.add(remoteUserView)
        }
    }

    func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
        print("Connected--->")
    }

    func peerConnection(onRenegotiationNeeded peerConnection: RTCPeerConnection!) {
        print(peerConnection.signalingState.rawValue)
        print("Connected peer Connection--->")
    }

    func onOffer(_ sdp:RTCSessionDescription) {

         print("SDP:========:::::::",sdp)
        setOffer(sdp)
        sendAnswer()
        peerStarted = true
    }

    func onAnswer(_ sdp:RTCSessionDescription) {
         print("SDP:========:::::::",sdp)

        setAnswer(sdp)
    }

    func onCandidate(candidate:RTCIceCandidate) {
        peerConnection.add(candidate)
    }

    func sendSDP(_ sdp:RTCSessionDescription) {
//         print("SDP:========:::::::",sdp)
//        let json:[String: AnyObject] = [
//            "type" : sdp.type as AnyObject,
//            "sdp"  : sdp.description as AnyObject
//        ]
//        print(json)
//        sigSend(json as NSDictionary)
//
        print(sdp.sdp)

        self.offerSend(sdp:sdp.sdp)
    }

    func sendOffer() {
        peerConnection = prepareNewConnection();
        peerConnection.offer(for: mediaConstraints) { (RTCSessionDescription, Error) in

            if(Error == nil){
                print("send offer")

                self.peerConnection.setLocalDescription(RTCSessionDescription!, completionHandler: { (Error) in
//                    print("setLocalDescription @@@ ERRO",Error!)
                    print(RTCSessionDescription as Any)
                    self.sendSDP(RTCSessionDescription!)
                })
            } else {
                print("sdp creation error: \(Error!)")
            }

        }
    }

    func setOffer(_ sdp:RTCSessionDescription) {

         print("SDP:========:::::::",sdp)

        if (peerConnection != nil) {
            Log("peer connection already exists")
        }
        peerConnection = prepareNewConnection()
        self.peerConnection.setRemoteDescription(sdp) { (error) in
            self.peerConnection.delegate = self
        }
    }

    func sendAnswer() {
        Log("sending Answer. Creating remote session description...")
        if (peerConnection == nil) {
            Log("peerConnection NOT exist!")
            return
        }
        self.peerConnection.answer(for: mediaConstraints) { (description, error) in
            print("answer @@@ ERRO",error)
            print("Answer",description)
        }
//        peerConnection.createAnswer(with: self, constraints: mediaConstraints)
    }

    func setAnswer(_ sdp:RTCSessionDescription) {

        print("SDP:========:::::::",sdp)

        if (peerConnection == nil) {
            Log("peerConnection NOT exist!")
            return
        }
        self.peerConnection.setRemoteDescription(sdp) { (error) in
//            print("setRemoteDescription",error!)
        }
    }

    func sendDisconnect() {

        let json:[String: AnyObject] = [
            "type" : "leave" as AnyObject,
            "roomid": self.roomID as AnyObject
        ]
        sigSend(json as NSDictionary)

    }

    //this is for send offer and connection

    func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {

    }

    func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
        print("Status changes",stateChanged.rawValue)
    }

    func peerConnection(_ peerConnection: RTCPeerConnection!, didCreateSessionDescription sdp: RTCSessionDescription!, error: Error!) {
        if (error == nil) {
            self.peerConnection.setLocalDescription(sdp) { (error) in
                self.sendSDP(sdp)
            }

            //self.offerSend(sdp:sdp.sdp)
            print("SDP Connection State",peerConnection.signalingState.rawValue)
        } else {
            Log("sdp creation error: " + error.localizedDescription)
            print("setLocalDescription @@@ ERRO",error!)
        }
    }

    func peerConnection(_ peerConnection: RTCPeerConnection!, didSetSessionDescriptionWithError error: Error!) {
        print("Peer Connsction State is:--\(peerConnection.signalingState.rawValue)")
    }



    func randomString(length: Int) -> String {
        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        return String((0..<length).map{ _ in letters.randomElement()! })
    }

    func sigConnect(_ wsUrl:String) {
        wsServerUrl = wsUrl
        socket!.connect()

        let _:[String: AnyObject] = [
            "log"  : true as AnyObject
        ]
        Log("connecting to " + wsServerUrl)

        socket!.on("connect") { data,arg  in
            self.Log("WebSocket connection opened to: " + self.wsServerUrl)
            self.sigEnter()
        }

        socket!.on("disconnect") { data,arg  in
            self.Log("WebSocket connection closed.")
        }

        socket!.on("message") { (data, emitter) in
            if (data.count == 0) {
                return
            }

            let json = self.convertToDictionary(text: (data[0] as? String) ?? "")
            self.Log("WebServiceResponse->C: " + json!.description)

            var type = ""

            if let event = json!["event"] as? String {
                type = event
            }

            if let typeString = json!["type"] as? String {
                type = typeString
            }

            print("Printing log")
            print(type)
//            if (type == "login"){
//                self.Log("Login,Logout")
//                let sdp = RTCSessionDescription(type: type, sdp: (json!["sdp"] as! String))
//                self.onOffer(sdp!)
//            }else
             if (type == "joined session") || (type == "login") {
                //self.loginUser()
                self.sendOffer()
                self.peerStarted = true
            }
             else if(type == "leave"){
//                self.hangUp()
//                DispatchQueue.main.async {
//                    _ = self.navigationController?.popViewController(animated: true)
//                }
                self.dropBtnPressed(UIButton())
             }
            else if (type == "offer") {

                self.Log("Received offer, set offer, sending answer....")
                print("Json Response",json!)
                print("Json Offer",(json!["offer"]!))
                let dict:[String:String] =  (json!["offer"] as! [String : String])

                let sdp = RTCSessionDescription(type:RTCSdpType(rawValue:0)!, sdp: (dict["sdp"]!))
                self.onOffer(sdp)
            } else if (type == "answer" && self.peerStarted) {
                self.timer.invalidate()
                self.Log("Received answer, setting answer SDP")
                print("Answer form the web",json!)
                let dict:[String:String] =  (json!["answer"] as! [String : String])
                let sdp = RTCSessionDescription(type:RTCSdpType(rawValue:2)!, sdp: (dict["sdp"]!))
                self.onAnswer(sdp)
            } else if (type == "answer") {
                self.timer.invalidate()
                self.Log("Received answer, setting answer SDP")
                let sdp = RTCSessionDescription(type: RTCSdpType(rawValue:2)!, sdp: (json!["sdp"] as! String))
                print(sdp)
                self.onAnswer(sdp)
            }
            else if (type == "Room"){

                print("=========================>You are in room<=======================")
                print("Json Response",json!)

                self.roomID = json!["roomid"] as! String

            }
            else if (type == "candidate" && self.peerStarted) {
                print("Received ICE candidate...",json!["candidate"] as! NSDictionary)

                let remotecandidate: NSDictionary = json!["candidate"] as! NSDictionary
                print("Remote Candidate List",remotecandidate)
                let midIndex = remotecandidate["sdpMid"] as! String
                if(midIndex != "")
                {
                    let candidate = RTCIceCandidate(
                        sdp: (remotecandidate["sdpMid"] as! String),
                        sdpMLineIndex: (Int32(remotecandidate["sdpMLineIndex"] as! Int)),
                        sdpMid:(remotecandidate["candidate"] as? String))
                    self.onCandidate(candidate: candidate);
                }
            } else if ((type == "user disconnected" || type == "remote left") && self.peerStarted) {
                self.Log("disconnected")
                self.stop()
            }  else {
                self.Log("Unexpected WebSocket message: " + (data[0] as AnyObject).description)
            }
        }

//        socket!.connect()
    }




    func loginUser(){
        let temDict = [
            "type" : "login",
            "name" : "Raj_new",
            "mobile":"98989898989"
        ]
        socket?.emit("event",temDict as NSDictionary)
//        SocketIOManager.sharedInstance.sendMessage(temDict as NSDictionary)
    }


    func offerSend(sdp:String){

         print("SDP:========:::::::",sdp)


        let param:[String:Any] = [
            "type": "offer",
            "name": "Raj_new",
            "mobile":"98989898989",
            "offer":[
                "sdp": sdp,
                "type": "offer"],
            "image":"www.google.com",
            "address":"Ahmedabad,220 Aryan park"
        ]
//        SocketIOManager.sharedInstance.sendMessage(param as NSDictionary)
        print("=========================>Sending Offer<=======================")
        let strMessage = dictToJson(dictionary: param as NSDictionary)
        socket!.emit("event", strMessage!)
    }

    func sigRecoonect() {
        socket!.disconnect()
        socket!.connect()
    }

    func sigEnter() {
        let roomName = getRoomName()
        self.Log("Entering room: " + roomName)
        //SocketIOManager.sharedInstance.sendMessage(["method" : "createOrJoin", "sessionId": roomName])
//        SocketIOManager.sharedInstance.sendMessage(["type" : "login", "name": txtName.text as Any,"id":"9898989898"])

       // SocketIOManager.sharedInstance.sendMessage(["type" : "login", "name":"Raj_new","id":"9898989898"])
        let temDict = [
            "type" : "login",
            "name" : "Raj_new",
            "mobile":"9898989898"
        ]
        let strMessage = dictToJson(dictionary: temDict as NSDictionary)
        socket!.emit("event", strMessage!)
    }

    func sigSend(_ msg:NSDictionary) {
        let strMessage = dictToJson(dictionary: msg as NSDictionary)
        socket!.emit("event", strMessage!)
    }

    //Convert Dictonary to JOSN string
    func dictToJson(dictionary : NSDictionary) -> String? {
        print("Dictionary Value=====>",dictionary)

        if let theJSONData = try? JSONSerialization.data(
            withJSONObject: dictionary,
            options: []) {
            let theJSONText = String(data: theJSONData,
                                     encoding: .ascii)
            print("JSON string = \(theJSONText!)")
            return theJSONText!
        }
        return nil
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func openBtnPressed(_ sender:UIButton) {
        if !peerStarted {
            //self.sigConnect(SocketIOManager.sharedInstance.socket)
            sender.isEnabled = false
//            self.dropBtn.isEnabled = false
            self.indicator.isHidden = false
            self.indicator.startAnimating()
            self.sigConnect((SocketIOManager.sharedInstance.socket.manager?.socketURL.absoluteString)!)
        }
    }

    @IBAction func dropBtnPressed(_ sender: AnyObject) {
        self.hangUP = true
        self.hangUp()
        DispatchQueue.main.async {
            _ = self.navigationController?.popViewController(animated: true)
        }
    }

    @IBAction func muteBtnPressed(_ sender: AnyObject) {
        if !isMute { // if Current state is mute, turn off the mute
            if peerStarted {
                self.localAudioTrack.isEnabled = false
                isMute = true
                self.muteBtn.setImage(UIImage(named: "call_mute.png"), for: .normal)
            }
        } else { //Otherwise
            if peerStarted {
                localAudioTrack.isEnabled = true
                self.muteBtn.setImage(UIImage(named: "un_mute.png"), for: .normal)
                isMute = false
            }
        }
    }

    func convertToDictionary(text: String) -> [String: Any]? {
        if let data = text.data(using: .utf8) {
            do {
                return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            } catch {
                print(error.localizedDescription)
            }
        }
        return nil
    }
}
ios swift webrtc stun turn
1个回答
0
投票

我在使用google stun时遇到相同的问题:stun:stun.l.google.com:19302

我认为问题是由电击/转弯服务器引起的。

i通过在以下位置应用免费转弯服务器成功解决并测试了它http://numb.viagenie.ca/cgi-bin/numbacct

很快我就添加

RTCIceServer(urlStrings: ["turn:numb.viagenie.ca:3478"], username: "YourUserName", credential: "yourPassword")

进入webrtc的iceServers列表,

这对我有用,至少证明我敏捷的一面是正确的,所有其余的都是转弯/眩晕服务器端。

© www.soinside.com 2019 - 2024. All rights reserved.