概述 WebRTC名称源自网页实时通信(WEB Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌2010年以6820万美元收购Global IP Solutio
WebRTC名称源自网页实时通信(WEB Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得的一项技术。Google于2011年6月3日开源的即时通讯项目,旨在使其成为客户端视频通话的标准。其实在Google将WebRTC开源之前,微软和苹果各自的通讯产品已占用很大市场份额(如Skype),Google也是为了快速扩大市场,所以将他给开源。在行业内得到了广泛的支持和应用,成为下一代视频通话的标准。更多介绍可以去官网上看。
WebRTC被誉为是web长期开源开发的一个新启元,是近年来web开发的最重要创新。WebRTC允许Web开发者在其web应用中添加视频聊天或者点对点数据传输,不需要复杂的代码或者昂贵的配置。目前支持Chrome、Firefox和Opera,后续会支持更多的浏览器,它有能力达到数十亿的设备。
目前,WebRTC的应用已经不局限在浏览器与浏览器之间,通过官方提供的SDK,我们可以很容易的实现本地应用间的音视频传输。在Android平台上,我们也非常容易的集成WebRTC框架,用非常简洁的代码就能实现强大、可靠的音视频传输功能。
说明
本文代码修改自meshenger-android
True P2P Voice- and video phone calls without the need for accounts or access to the Internet. There is no discovery mechanism, no meshing and no servers. Just scan each others QR-Code that will contain the contacts IP address. This works in many local networks such as commUnity mesh networks, company networks or at home.
翻译如下:
真正的 P2P 语音和视频电话呼叫,无需帐户或访问互联网。 没有发现机制,没有网格化,也没有服务器。 只需相互扫描包含联系人 IP 地址的二维码即可。 这适用于许多本地网络,例如社区网状网络、公司网络或家庭网络。
版本: meshenger-android-3.0.3 最后的JAVA版本, 最新版本已经改用Kotlin
基本情况:
功能满足一个局域网P2P的视频通话功能
感谢作者
build.gradle
dependencies { implementation 'org.webrtc:google-webrtc:1.0.32006'}
创建 PeerConnectionFactory
private void initRTC() { log("initRTC"); eglCtxRemote = EglBase.create().getEglBaseContext(); eglCtxLocal = EglBase.create().getEglBaseContext(); //创建 PeerConnectionFactory //这种方法存在兼容性问题, 在一些平台上, 会导致后续流程不能正常执行. constraints = new MediaConstraints(); constraints.optional.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); constraints.optional.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); //initVideoTrack(); PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()); PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); //https://yuriyshea.com/arcHives/androidwebrtc踩坑指南 DefaultVideoEncoderFactory enVdf = new DefaultVideoEncoderFactory(eglCtxRemote, true, true); VideoDecoderFactory deVdf = new DefaultVideoDecoderFactory(eglCtxRemote); //创建 PeerConnectionFactory factory = PeerConnectionFactory.builder() .setOptions(options) .setVideoEncoderFactory(enVdf) .setVideoDecoderFactory(deVdf) .createPeerConnectionFactory(); log("initRTC done"); }
创建PeerConnection 发起呼叫
connection = factory.createPeerConnection(Collections.emptyList(), new DefaultObserver() { @Override public void onIceGatherinGChange(PeerConnection.IceGatheringState iceGatheringState) { super.onIceGatheringChange(iceGatheringState); log("Outgoing.onIceGatheringChange " + iceGatheringState.name()); if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) { log("Outgoing.connect call from remote address: " + contact.getAddresses()); reportStateChange(CallState.CONNECTING); //发送信息给接收方,告知发起通话. getPublisher().sendCall(contact.getAddresses(), connection.getLocalDescription().description); } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { log("Outgoing.onIceConnectionChange " + iceConnectionState.name()); super.onIceConnectionChange(iceConnectionState); if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { reportStateChange(CallState.DISCONNECTED); }else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) { hangUp("Outgoing.onIceConnectionChange CLOSED"); } } @Override public void onAddStream(MediaStream mediaStream) { log("Outgoing.onAddStream"); super.onAddStream(mediaStream); handleMediaStream(mediaStream); } @Override public void onDataChannel(DataChannel dataChannel) { log("Outgoing.onDataChannel"); super.onDataChannel(dataChannel); RTCCall.this.dataChannel = dataChannel; dataChannel.reGISterObserver(RTCCall.this); } });//初始化音视频通道 connection.addStream(createStream()); //创建数据通道, 可用于收发消息. this.dataChannel = connection.createDataChannel("data", new DataChannel.Init()); this.dataChannel.registerObserver(this); log("Outgoing.createOffer"); connection.createOffer(new DefaultSdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { log("Outgoing.onCreateSuccess"); super.onCreateSuccess(sessionDescription); connection.setLocalDescription(new DefaultSdpObserver(), sessionDescription); } }, constraints);
呼叫方收到后同样初始化, 并在点击接听后创建PeerConnection
public void accept(OnStateChangeListener listener) { log("accept"); this.listener = listener; new Thread(() -> { connection = factory.createPeerConnection(this.iceServers, new DefaultObserver() { @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { super.onIceGatheringChange(iceGatheringState); if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) { log("Incoming.onIceGatheringChange"); //通知已接听 getPublisher().sendAnswer(contact.getAddresses(), connection.getLocalDescription().description); reportStateChange(CallState.CONNECTED); } } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { log("Incoming.onIceConnectionChange " + iceConnectionState.name()); super.onIceConnectionChange(iceConnectionState); if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { reportStateChange(CallState.DISCONNECTED); }else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) { hangUp("Incoming.onIceConnectionChange CLOSED"); } } @Override public void onAddStream(MediaStream mediaStream) { log("Incoming.onAddStream"); super.onAddStream(mediaStream); handleMediaStream(mediaStream); } @Override public void onDataChannel(DataChannel dataChannel) { super.onDataChannel(dataChannel); RTCCall.this.dataChannel = dataChannel; dataChannel.registerObserver(RTCCall.this); } }); connection.addStream(createStream()); //this.dataChannel = connection.createDataChannel("data", new DataChannel.Init()); log("Incoming.setting remote description"); //设置会话, 创建响应应答 connection.setRemoteDescription(new DefaultSdpObserver() { @Override public void onSetSuccess() { super.onSetSuccess(); log("creating answer..."); connection.createAnswer(new DefaultSdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) {log("Incoming.onCreateSuccess");super.onCreateSuccess(sessionDescription);connection.setLocalDescription(new DefaultSdpObserver(), sessionDescription); } @Override public void onCreateFailure(String s) {super.onCreateFailure(s);log("Incoming.onCreateFailure: " + s); } }, constraints); } }, new SessionDescription(SessionDescription.Type.OFFER, offer)); }).start(); }
呼叫方处理应答
private void handleAnswer(String remoteDesc) { log("handleAnswer"); connection.setRemoteDescription(new DefaultSdpObserver() { @Override public void onSetSuccess() { super.onSetSuccess(); } @Override public void onSetFailure(String s) { super.onSetFailure(s); } }, new SessionDescription(SessionDescription.Type.ANSWER, remoteDesc)); }
启动摄像头
public void setVideoEnabled(boolean enabled) { log("setVideoEnabled enabled=" + enabled); this.videoEnabled = enabled; try { if (enabled) { this.capturer.startCapture(640, 480, 30); } else { this.capturer.stopCapture(); } JSONObject object = new jsONObject(); object.put(StateChangeMessage, enabled ? CameraEnabledMessage : CameraDisabledMessage); log("setVideoEnabled: " + object); dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(object.toString().getBytes()), false)); } catch (JSONException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }
挂断
public void hangUp(String res) { if(state == CallState.ENDED){ log("hangUp Ignored already ENDED:" + res); return; } log("hangUp:" + res); reportStateChange(CallState.ENDED); closePeerConnection();//通知挂断. new Thread(() -> { getPublisher().sendHangup(contact.getAddresses()); }).start(); }
//Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 13128 (signaling_threa), pid 13067 (d.d.meshenger) //type=1400 audit(0.0:28585): avc: granted { nlmsg_readpriv } for scontext=u:r:untrusted_app_29:s0:c5,c257,c512,c768 tcontext=u:r:untrusted_app_29:s0:c5,c257,c512,c768 tclass=netlink_route_Socket app=d.d.meshenger //pid: 13067, tid: 13128, name: signaling_threa >>> d.d.meshenger <<< private void closePeerConnection() { log("closePeerConnection"); if(localRenderer != null){ localRenderer.release(); localRenderer = null; } if(remoteRenderer != null){ remoteRenderer.release(); remoteRenderer = null; } if(capturer != null){ try { capturer.stopCapture(); capturer.dispose(); capturer = null; } catch (InterruptedException e) { e.printStackTrace(); } } if (connection != null) { try { PeerConnection conn = connection; connection = null; conn.close(); } catch (Exception e) { e.printStackTrace(); } //connection = null; log("closePeerConnection done"); } }
完整呼叫流程LOG如下
[呼叫方]
## 开始呼出2022-12-01 14:50:09.016 23145-23717 RTCCall D RTCCall created2022-12-01 14:50:09.016 23145-23717 RTCCall D initRTC2022-12-01 14:50:09.041 23145-23717 RTCCall D initRTC done2022-12-01 14:50:09.042 23145-23728 RTCCall D createPeerConnection2022-12-01 14:50:09.052 23145-23728 RTCCall D createStream2022-12-01 14:50:09.058 23145-23728 RTCCall D createCapturer2022-12-01 14:50:09.139 23145-23728 RTCCall D Outgoing.createOffer2022-12-01 14:50:09.144 23145-23725 RTCCall D Outgoing.onCreateSuccess2022-12-01 14:50:09.212 23145-23725 RTCCall D Outgoing.onIceGatheringChange GATHERING2022-12-01 14:50:09.327 23145-23725 RTCCall D Outgoing.onIceGatheringChange COMPLETE2022-12-01 14:50:09.327 23145-23725 RTCCall D transferring offer...2022-12-01 14:50:09.328 23145-23735 RTCCall D Outgoing.connect call from remote address: 192.168.7.2392022-12-01 14:50:09.328 23145-23735 RTCCall D reportStateChange CONNECTING## 对方接听2022-12-01 14:50:15.331 23145-23168 RTCCall D reportStateChange CONNECTED2022-12-01 14:50:15.331 23145-23168 RTCCall D handleAnswer2022-12-01 14:50:15.409 23145-23725 RTCCall D Outgoing.onIceConnectionChange CHECKING2022-12-01 14:50:15.415 23145-23725 RTCCall D Outgoing.onAddStream2022-12-01 14:50:15.415 23145-23725 RTCCall D handleMediaStream ava=false2022-12-01 14:50:15.416 23145-23725 RTCCall D handleAnswer.onSetSuccess2022-12-01 14:50:15.512 23145-23725 RTCCall D Outgoing.onIceConnectionChange CONNECTED2022-12-01 14:50:15.528 23145-23725 RTCCall D onStateChange## 对方开启摄像头, 并推送2022-12-01 14:50:24.939 23145-23725 RTCCall D onMessage: {"StateChange":"CameraEnabled"}2022-12-01 14:50:30.932 23145-23725 RTCCall D Outgoing.onIceConnectionChange COMPLETED## 开启摄像头并推送2022-12-01 14:50:39.122 23145-23145 RTCCall D setVideoEnabled enabled=true2022-12-01 14:50:39.122 23145-23145 RTCCall D setVideoEnabled: {"StateChange":"CameraEnabled"}2022-12-01 14:50:39.124 23145-23725 RTCCall D onBufferedAmountChange l=31## 挂断2022-12-01 14:51:03.363 23145-23145 RTCCall D hangUp:UI.callDecline click2022-12-01 14:51:03.363 23145-23145 RTCCall D reportStateChange ENDED2022-12-01 14:51:03.363 23145-23145 RTCCall D closePeerConnection2022-12-01 14:51:03.370 23145-23145 RTCCall D closePeerConnection state=CONNECTED2022-12-01 14:51:03.372 23145-23725 RTCCall D Outgoing.onIceConnectionChange CLOSED2022-12-01 14:51:03.372 23145-23725 RTCCall D hangUp Ignored already ENDED:Outgoing.onIceConnectionChange CLOSED2022-12-01 14:51:03.547 23145-23725 RTCCall D onStateChange2022-12-01 14:51:03.547 23145-23725 RTCCall D onStateChange2022-12-01 14:51:03.580 23145-23145 RTCCall D closePeerConnection done2022-12-01 14:51:04.059 23145-23145 RTCCall D releaseCamera2022-12-01 14:51:04.149 23145-23168 RTCCall D hangUp Ignored already ENDED:publisher msg
[被叫方]
## 来电并响铃2022-12-01 14:22:30.319 8916-8943 RTCCall D initRTC2022-12-01 14:22:30.363 8916-8943 RTCCall D initRTC done2022-12-01 14:22:30.364 8916-8943 RTCCall D reportStateChange RINGING## 接听2022-12-01 14:22:35.625 8916-8916 RTCCall D setRenderer2022-12-01 14:22:35.625 8916-8916 RTCCall D accept2022-12-01 14:22:35.661 8916-14310 RTCCall D createStream2022-12-01 14:22:35.662 8916-14310 RTCCall D createCapturer2022-12-01 14:22:35.677 8916-14310 RTCCall D Incoming.setting remote description2022-12-01 14:22:35.768 8916-14256 RTCCall D Incoming.onAddStream2022-12-01 14:22:35.768 8916-14256 RTCCall D handleMediaStream ava=false2022-12-01 14:22:35.769 8916-14256 RTCCall D creating answer...2022-12-01 14:22:35.773 8916-14256 RTCCall D Incoming.onCreateSuccess2022-12-01 14:22:35.873 8916-14256 RTCCall D Incoming.onIceConnectionChange CHECKING2022-12-01 14:22:36.035 8916-14256 RTCCall D Incoming.onIceGatheringChange## 连接已建立2022-12-01 14:22:36.048 8916-14256 RTCCall D reportStateChange CONNECTED2022-12-01 14:22:36.256 8916-14256 RTCCall D Incoming.onIceConnectionChange CONNECTED2022-12-01 14:22:36.278 8916-14256 RTCCall D onStateChange## 开启摄像头并推送2022-12-01 14:22:45.635 8916-8916 RTCCall D setVideoEnabled enabled=true2022-12-01 14:22:45.636 8916-8916 RTCCall D setVideoEnabled: {"StateChange":"CameraEnabled"}2022-12-01 14:22:45.638 8916-14256 RTCCall D onBufferedAmountChange l=31## 对方开启摄像头2022-12-01 14:23:02.916 8916-14256 RTCCall D onMessage: {"StateChange":"CameraEnabled"}## 对方已挂断2022-12-01 14:23:24.318 8916-14256 RTCCall D Incoming.onIceConnectionChange DISCONNECTED2022-12-01 14:23:24.318 8916-14256 RTCCall D reportStateChange DISCONNECTED2022-12-01 14:23:24.320 8916-14256 RTCCall D onStateChange2022-12-01 14:23:24.320 8916-14256 RTCCall D onStateChange2022-12-01 14:23:24.430 8916-8943 RTCCall D hangUp:publisher msg2022-12-01 14:23:24.430 8916-8943 RTCCall D reportStateChange ENDED2022-12-01 14:23:24.431 8916-8943 RTCCall D closePeerConnection2022-12-01 14:23:24.445 8916-8943 RTCCall D closePeerConnection state=CONNECTED2022-12-01 14:23:24.449 8916-14256 RTCCall D Incoming.onIceConnectionChange CLOSED2022-12-01 14:23:24.449 8916-14256 RTCCall D hangUp Ignored already ENDED:Incoming.onIceConnectionChange CLOSED2022-12-01 14:23:24.705 8916-8943 RTCCall D closePeerConnection done2022-12-01 14:23:24.924 8916-8916 RTCCall D releaseCamera
首先是源码中Socket连接的问题
Utils.java 与IPV6有关, 这里改成了IPV4.
并在后续的网络相关部分改为使用IP地址.
public static List<InetSocketAddress> getAddressPermutations(String contact_Mac, int port) { byte[] contact_mac_bytes = Utils.macAddressToBytes(contact_mac); ArrayList<InetSocketAddress> addrs = new ArrayList<InetSocketAddress>(); try { List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces()); for (NetworkInterface nif : all) { if (nif.isLoopback()) { continue; } for (InterfaceAddress ia : nif.getInterfaceAddresses()) { InetAddress addr = ia.getAddress(); if (addr.isLoopbackAddress()) { continue; } android.util.Log.d("Utils", "getAddressPermutations " + addr.getHostName() + "," + addr.getHostAddress() + ","); if (addr instanceof Inet4Address) { addrs.add(new InetSocketAddress(addr, port)); } } } } catch (Exception e) { e.printStackTrace(); }
增加本地摄像头预览显示
方法是传入本地的SurfaceViewRenderer并修改getVideoTrack
private boolean enablePreview = true; private VideoTrack getVideoTrack() { this.capturer = createCapturer(); if(!enablePreview) { return factory.createVideoTrack("video1", factory.createVideoSource(false)); }else { VideoSource videoSource = factory.createVideoSource(false); //EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext(); SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglCtxLocal); capturer.initialize(surfaceTextureHelper, App.getApp(), videoSource.getCapturerObserver()); //capturer.startCapture(480, 640, 30); VideoTrack track = factory.createVideoTrack("video1", videoSource); track.addSink(localRenderer); return track; } }
初始化方式问题导致无正常回调
constraints = new MediaConstraints(); constraints.optional.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); constraints.optional.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));//方式1:PeerConnectionFactory.initialize(PeerConnectionFactory .InitializationOptions.builder(context) .createInitializationOptions()); factory = PeerConnectionFactory.builder().createPeerConnectionFactory();//方式2: PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()); PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); DefaultVideoEncoderFactory enVdf = new DefaultVideoEncoderFactory(eglCtxRemote, true, true); VideoDecoderFactory deVdf = new DefaultVideoDecoderFactory(eglCtxRemote); factory = PeerConnectionFactory.builder() .setOptions(options) .setVideoEncoderFactory(enVdf) .setVideoDecoderFactory(deVdf) .createPeerConnectionFactory();
方式1 会导致呼出时的错误如下
connection.createOffer(new DefaultSdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { log("Outgoing.onCreateSuccess"); super.onCreateSuccess(sessionDescription); connection.setLocalDescription(new DefaultSdpObserver(){ @Override public void onSetFailure(String s) {super.onSetFailure(s);//出错时的LOG://onSetFailure s=//Failed to set local offer sdp: //Failed to set local video description recv parameters for m-section with mid='video'.log("Outgoing.onSetFailure s=" + s); } }, sessionDescription); } }, constraints);
多次调用PeerConnection.close()会导致崩溃
2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Build fingerprint: 'google/blueline/blueline:12/SP1A.210812.016.C1/8029091:user/release-keys'2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Revision: 'MP1.0'2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A ABI: 'arm64'2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Timestamp: 2022-12-01 14:31:57.642912791+08002022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Process uptime: 0s2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Cmdline: d.d.meshenger2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A pid: 22778, tid: 22851, name: signaling_threa >>> d.d.meshenger <<<2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A uid: 102612022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x02022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A Cause: null pointer dereference2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x0 0000000000000000 x1 0000000000000005 x2 0000000000000000 x3 0000007c6f2be89d2022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x4 0000007c584fd818 x5 0000007f12e7a555 x6 73656d2f642f644c x7 632f7265676e65682022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x8 0000007be2f5a000 x9 81248e9b13f44bd4 x10 0000000000430000 x11 00000000000000012022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x12 0000000000000004 x13 0000000000000004 x14 7ffbffff00000000 x15 00000000000000002022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x16 0000007c584fcf00 x17 0000007f23435688 x18 0000007be1bc6000 x19 0000007d918a09102022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x20 0000000000000005 x21 0000007c584fe000 x22 0000007c584fd960 x23 0000007c584fe0002022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x24 0000007be2f16ab8 x25 00000000ffffffff x26 0000007c584fdff8 x27 00000000000fc0002022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A x28 0000007c58405000 x29 0000007c584fda102022-12-01 14:31:57.847 22887-22887 DEBUG pid-22887 A lr 0000007be34be6e0 sp 0000007c584fd950 pc 0000007be34be6f4 pst 0000000060000000
源码及资料下载
Android WebRTC 的一些资料
WebRTC
googlesource webrtc / src
git clone Https://webrtc.googlesource.com/src (需要连接外网)
meshenger-android
WebRTC-Android 探索 - 创建音视频通话程序的基本姿势
WebRTC实现Android传屏demo
Android WebRTC踩坑指南
Google WebRtc Android 使用详解(包括客户端和服务端代码)
owt-client-android
android webrtc学习 一(源码下载和编译)
编译webrtc android源码
libwebrtc.aar
来源地址:https://blog.csdn.net/ansondroider/article/details/128126250
--结束END--
本文标题: android WebRtc 视频通话(P2P)
本文链接: https://lsjlt.com/news/380031.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0