zoukankan      html  css  js  c++  java
  • 基于Kurento的WebRTC移动视频群聊技术方案(转)

    https://www.cnblogs.com/lidabo/p/6934744.html
     

    说在前面的话:视频实时群聊天有三种架构

           Mesh架构:终端之间互相连接,没有中心服务器,产生的问题,每个终端都要连接n-1个终端,每个终端的编码和网络压力都很大。群聊人数N不可能太大。

           Router架构:终端之间引入中心服务器,学名MCU(Multi Point Control Unit),每个终端的视频流都发布到MCU服务器上,然后服务器负责编码发布多视频流的工作,减轻客户端的压力。

           Mix架构:在Router架构基础上,多个视频流在服务器端被合为一个视频流,减轻网络压力。

           下面讲我们的选择,在MCU方面有licode、kurento等解决方案。kurento在视频群聊领域有专门的kurento Room解决方案,官方还提供一个kurento room server的样例实现。

           首先可以考虑不是一个Kurento Room Demo作为搭建方案原型的MCU组件。

           Room Demo的部署可见:http://doc-kurento-room.readthedocs.io/en/stable/demo_deployment.html

          其中碰到一些Maven编译问题:

    [plain] view plain copy
     
     print?
    1. Unable to initialise extensions Component descriptor role: 'com.jcraft.jsch.UIKeyboardInteractive', implementation: 'org.apache.maven.wagon.providers.ssh.jsch.interactive.PrompterUIKeyboardInteractive', role hint: 'default' has a hint, but there are other implementations that don't  


          Maven的安装版本需要时3.0以上

          还有碰到找不到bower命令行问题。bower是Node.js下面的一个包管理工具,安装node.js以后用npm安装即可

          最后按照部署指南网页中的命令启动服务器即可。

          Demo服务器有两部分,一部分是Demo Web服务器,二是把官方的kurento room server也集成到了这个demo中。不用再架设独立的kurento room server

          说说Android段的实施:再说一个公司:http://www.nubomedia.eu/,这家公司提供实时媒体通信开源云服务,核心组件可能是kurento media server,它的官网和kurento官网用一个模板,about里面显示两家组织有联系,kurento官方提供的Java Client因为底层API原因在Android上不肯用,这个nubomedia组织提供了一个kurento android client的实现,同时还提供了一个kurento room client的实现以及room使用案例:https://github.com/nubomedia-vtt/nubo-test,这家公司对其开发的开源方案管理非常及时,早晨提个接口的issue,下午已经commit了代码修改。

           这个案例虽然支持room沟通,但视频沟通是基于room发布订阅机制做的双人聊天。略改一下代码应该就可以实现多人聊天不过这家组织提供的两个client实现和官方的接口高度相似。主要改的是PeerVideoActivity这个类,下面我share一个基本走通多端通信的这个类的代码,供大家参考:

    [java] view plain copy
     
     print?
    1. package fi.vtt.nubotest;  
    2.   
    3. import android.app.ListActivity;  
    4. import android.content.SharedPreferences;  
    5. import android.graphics.PixelFormat;  
    6. import android.opengl.GLSurfaceView;  
    7. import android.os.Bundle;  
    8. import android.os.Handler;  
    9. import android.util.Log;  
    10. import android.view.Menu;  
    11. import android.view.MenuItem;  
    12. import android.view.View;  
    13. import android.view.WindowManager;  
    14. import android.widget.TextView;  
    15. import android.widget.Toast;  
    16.   
    17. import org.webrtc.IceCandidate;  
    18. import org.webrtc.MediaStream;  
    19. import org.webrtc.PeerConnection;  
    20. import org.webrtc.RendererCommon;  
    21. import org.webrtc.SessionDescription;  
    22. import org.webrtc.VideoRenderer;  
    23. import org.webrtc.VideoRendererGui;  
    24.   
    25. import java.util.Map;  
    26.   
    27. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomError;  
    28. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomListener;  
    29. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomNotification;  
    30. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomResponse;  
    31. import fi.vtt.nubomedia.webrtcpeerandroid.NBMMediaConfiguration;  
    32. import fi.vtt.nubomedia.webrtcpeerandroid.NBMPeerConnection;  
    33. import fi.vtt.nubomedia.webrtcpeerandroid.NBMWebRTCPeer;  
    34.   
    35. import fi.vtt.nubotest.util.Constants;  
    36.   
    37. /** 
    38.  * Activity for receiving the video stream of a peer 
    39.  * (based on PeerVideoActivity of Pubnub's video chat tutorial example. 
    40.  */  
    41. public class PeerVideoActivity extends ListActivity implements NBMWebRTCPeer.Observer, RoomListener {  
    42.     private static final String TAG = "PeerVideoActivity";  
    43.   
    44.     private NBMMediaConfiguration peerConnectionParameters;  
    45.     private NBMWebRTCPeer nbmWebRTCPeer;  
    46.   
    47.     private SessionDescription localSdp;  
    48.     private SessionDescription remoteSdp;  
    49.     private String PaticipantID;  
    50.   
    51.     private VideoRenderer.Callbacks localRender;  
    52.     private VideoRenderer.Callbacks remoteRender;  
    53.     private GLSurfaceView videoView;  
    54.   
    55.     private SharedPreferences mSharedPreferences;  
    56.   
    57.     private int publishVideoRequestId;  
    58.     private int sendIceCandidateRequestId;  
    59.   
    60.     private TextView mCallStatus;  
    61.   
    62.     private String  username, calluser;  
    63.     private boolean backPressed = false;  
    64.     private Thread  backPressedThread = null;  
    65.   
    66.     private static final int LOCAL_X_CONNECTED = 72;  
    67.     private static final int LOCAL_Y_CONNECTED = 72;  
    68.     private static final int LOCAL_WIDTH_CONNECTED = 25;  
    69.     private static final int LOCAL_HEIGHT_CONNECTED = 25;  
    70.     // Remote video screen position  
    71.     private static final int REMOTE_X = 0;  
    72.     private static  int REMOTE_Y = 0;  
    73.     private static final int REMOTE_WIDTH = 25;  
    74.     private static final int REMOTE_HEIGHT = 25;  
    75.   
    76.     private Handler mHandler;  
    77.     private CallState callState;  
    78.   
    79.     private enum CallState{  
    80.         IDLE, PUBLISHING, PUBLISHED, WAITING_REMOTE_USER, RECEIVING_REMOTE_USER,PATICIPANT_JOINED,RECEIVING_PATICIPANT,  
    81.     }  
    82.   
    83.     @Override  
    84.     public void onCreate(Bundle savedInstanceState) {  
    85.         super.onCreate(savedInstanceState);  
    86.         callState = CallState.IDLE;  
    87.   
    88.         setContentView(R.layout.activity_video_chat);  
    89.         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
    90.         mHandler = new Handler();  
    91.         Bundle extras = getIntent().getExtras();  
    92.         if (extras == null || !extras.containsKey(Constants.USER_NAME)) {  
    93.             ;  
    94.             Toast.makeText(this, "Need to pass username to PeerVideoActivity in intent extras (Constants.USER_NAME).",  
    95.                     Toast.LENGTH_SHORT).show();  
    96.             finish();  
    97.             return;  
    98.         }  
    99.         this.username      = extras.getString(Constants.USER_NAME, "");  
    100.         Log.i(TAG, "username: " + username);  
    101.   
    102.         if (extras.containsKey(Constants.CALL_USER)) {  
    103.             this.calluser      = extras.getString(Constants.CALL_USER, "");  
    104.             Log.i(TAG, "callUser: " + calluser);  
    105.         }  
    106.   
    107.         this.mCallStatus   = (TextView) findViewById(R.id.call_status);  
    108.         TextView prompt   = (TextView) findViewById(R.id.receive_prompt);  
    109.         prompt.setText("Receive from " + calluser);  
    110.   
    111.         this.videoView = (GLSurfaceView) findViewById(R.id.gl_surface);  
    112.         // Set up the List View for chatting  
    113.         RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SCALE_ASPECT_FILL;  
    114.         VideoRendererGui.setView(videoView, null);  
    115.   
    116.   
    117.         localRender = VideoRendererGui.create(  LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,  
    118.                 LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,  
    119.                 scalingType, true);  
    120.         NBMMediaConfiguration.NBMVideoFormat receiverVideoFormat = new NBMMediaConfiguration.NBMVideoFormat(352, 288, PixelFormat.RGB_888, 20);  
    121.         peerConnectionParameters = new NBMMediaConfiguration(   NBMMediaConfiguration.NBMRendererType.OPENGLES,  
    122.                 NBMMediaConfiguration.NBMAudioCodec.OPUS, 0,  
    123.                 NBMMediaConfiguration.NBMVideoCodec.VP8, 0,  
    124.                 receiverVideoFormat,  
    125.                 NBMMediaConfiguration.NBMCameraPosition.FRONT);  
    126.         nbmWebRTCPeer = new NBMWebRTCPeer(peerConnectionParameters, this, localRender, this);  
    127.         nbmWebRTCPeer.initialize();  
    128.         Log.i(TAG, "PeerVideoActivity initialized");  
    129.         mHandler.postDelayed(publishDelayed, 4000);  
    130.   
    131.         MainActivity.getKurentoRoomAPIInstance().addObserver(this);  
    132.   
    133.   
    134.   
    135.         callState = CallState.PUBLISHING;  
    136.         mCallStatus.setText("Publishing...");  
    137.   
    138.     }  
    139.   
    140.     private Runnable publishDelayed = new Runnable() {  
    141.         @Override  
    142.         public void run() {  
    143.             nbmWebRTCPeer.generateOffer("derp", true);  
    144.         }  
    145.     };  
    146.   
    147.     @Override  
    148.     public boolean onCreateOptionsMenu(Menu menu) {  
    149.         // Inflate the menu; this adds items to the action bar if it is present.  
    150.         getMenuInflater().inflate(R.menu.menu_video_chat, menu);  
    151.         return true;  
    152.     }  
    153.   
    154.     @Override  
    155.     public boolean onOptionsItemSelected(MenuItem item) {  
    156.         // Handle action bar item clicks here. The action bar will  
    157.         // automatically handle clicks on the Home/Up button, so long  
    158.         // as you specify a parent activity in AndroidManifest.xml.  
    159.         int id = item.getItemId();  
    160.   
    161.         //noinspection SimplifiableIfStatement  
    162.         if (id == R.id.action_settings) {  
    163.             return true;  
    164.         }  
    165.   
    166.         return super.onOptionsItemSelected(item);  
    167.     }  
    168.   
    169.     @Override  
    170.     protected void onStart() {  
    171.         super.onStart();  
    172.   
    173.     }  
    174.   
    175.     @Override  
    176.     protected void onPause() {  
    177.         nbmWebRTCPeer.stopLocalMedia();  
    178.         super.onPause();  
    179.     }  
    180.   
    181.     @Override  
    182.     protected void onResume() {  
    183.         super.onResume();  
    184.         nbmWebRTCPeer.startLocalMedia();  
    185.     }  
    186.   
    187.     @Override  
    188.     protected void onStop() {  
    189.         endCall();  
    190.         super.onStop();  
    191.     }  
    192.   
    193.     @Override  
    194.     protected void onDestroy() {  
    195.         super.onDestroy();  
    196.     }  
    197.   
    198.     @Override  
    199.     public void onBackPressed() {  
    200.         // If back button has not been pressed in a while then trigger thread and toast notification  
    201.         if (!this.backPressed){  
    202.             this.backPressed = true;  
    203.             Toast.makeText(this,"Press back again to end.",Toast.LENGTH_SHORT).show();  
    204.             this.backPressedThread = new Thread(new Runnable() {  
    205.                 @Override  
    206.                 public void run() {  
    207.                     try {  
    208.                         Thread.sleep(5000);  
    209.                         backPressed = false;  
    210.                     } catch (InterruptedException e){ Log.d("VCA-oBP","Successfully interrupted"); }  
    211.                 }  
    212.             });  
    213.             this.backPressedThread.start();  
    214.         }  
    215.         // If button pressed the second time then call super back pressed  
    216.         // (eventually calls onDestroy)  
    217.         else {  
    218.             if (this.backPressedThread != null)  
    219.                 this.backPressedThread.interrupt();  
    220.             super.onBackPressed();  
    221.         }  
    222.     }  
    223.   
    224.     public void hangup(View view) {  
    225.         finish();  
    226.     }  
    227.   
    228.     public void receiveFromRemote(View view){  
    229.         Log.e(TAG,"--->receiveFromRemote");  
    230.         if (callState == CallState.PUBLISHED){  
    231.             callState = CallState.WAITING_REMOTE_USER;  
    232.             nbmWebRTCPeer.generateOffer("remote", false);  
    233.             runOnUiThread(new Runnable() {  
    234.                 @Override  
    235.                 public void run() {  
    236.                     mCallStatus.setText("Waiting remote stream...");  
    237.                 }  
    238.             });  
    239.         }  
    240.     }  
    241.   
    242.     /** 
    243.      * Terminates the current call and ends activity 
    244.      */  
    245.     private void endCall() {  
    246.         callState = CallState.IDLE;  
    247.         try  
    248.         {  
    249.             if (nbmWebRTCPeer != null) {  
    250.                 nbmWebRTCPeer.close();  
    251.                 nbmWebRTCPeer = null;  
    252.             }  
    253.         }  
    254.         catch (Exception e){e.printStackTrace();}  
    255.     }  
    256.   
    257.     @Override  
    258.     public void onLocalSdpOfferGenerated(final SessionDescription sessionDescription, NBMPeerConnection nbmPeerConnection) {  
    259.         Log.e(TAG,"--->onLocalSdpOfferGenerated");  
    260.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
    261.             localSdp = sessionDescription;  
    262.             Log.e(TAG,"--->onLocalSdpOfferGenerated:publish");  
    263.             runOnUiThread(new Runnable() {  
    264.                 @Override  
    265.                 public void run() {  
    266.                     if (MainActivity.getKurentoRoomAPIInstance() != null) {  
    267.                         Log.d(TAG, "Sending " + sessionDescription.type);  
    268.                         publishVideoRequestId = ++Constants.id;  
    269.   
    270. //                    String sender = calluser + "_webcam";  
    271. //                    MainActivity.getKurentoRoomAPIInstance().sendReceiveVideoFrom(sender, localSdp.description, publishVideoRequestId);  
    272.   
    273.                         MainActivity.getKurentoRoomAPIInstance().sendPublishVideo(localSdp.description, false, publishVideoRequestId);  
    274.                     }  
    275.                 }  
    276.             });  
    277.         } else { // Asking for remote user video  
    278.             Log.e(TAG,"--->onLocalSdpOfferGenerated:remote");  
    279.             remoteSdp = sessionDescription;  
    280. //            nbmWebRTCPeer.selectCameraPosition(NBMMediaConfiguration.NBMCameraPosition.BACK);  
    281.             runOnUiThread(new Runnable() {  
    282.                 @Override  
    283.                 public void run() {  
    284.                     if (MainActivity.getKurentoRoomAPIInstance() != null) {  
    285.                         Log.e(TAG, "Sending--> " +calluser+ sessionDescription.type);  
    286.                         publishVideoRequestId = ++Constants.id;  
    287.   
    288.                         String sender = calluser + "_webcam";  
    289.                         MainActivity.getKurentoRoomAPIInstance().sendReceiveVideoFrom(sender, remoteSdp.description, publishVideoRequestId);  
    290.                     }  
    291.                 }  
    292.             });  
    293.         }  
    294.     }  
    295.   
    296.     @Override  
    297.     public void onLocalSdpAnswerGenerated(SessionDescription sessionDescription, NBMPeerConnection nbmPeerConnection) {  
    298.     }  
    299.   
    300.     @Override  
    301.     public void onIceCandidate(IceCandidate iceCandidate, NBMPeerConnection nbmPeerConnection) {  
    302.         Log.e(TAG,"--->onIceCandidate");  
    303.         sendIceCandidateRequestId = ++Constants.id;  
    304.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED){  
    305.             Log.e(TAG,"--->onIceCandidate:publish");  
    306.             MainActivity.getKurentoRoomAPIInstance().sendOnIceCandidate(this.username, iceCandidate.sdp,  
    307.                     iceCandidate.sdpMid, Integer.toString(iceCandidate.sdpMLineIndex), sendIceCandidateRequestId);  
    308.         } else{  
    309.             Log.e(TAG,"--->onIceCandidate:"+this.calluser);  
    310.             MainActivity.getKurentoRoomAPIInstance().sendOnIceCandidate(this.calluser, iceCandidate.sdp,  
    311.                     iceCandidate.sdpMid, Integer.toString(iceCandidate.sdpMLineIndex), sendIceCandidateRequestId);  
    312.         }  
    313.     }  
    314.   
    315.     @Override  
    316.     public void onIceStatusChanged(PeerConnection.IceConnectionState iceConnectionState, NBMPeerConnection nbmPeerConnection) {  
    317.         Log.i(TAG, "onIceStatusChanged");  
    318.     }  
    319.   
    320.     @Override  
    321.     public void onRemoteStreamAdded(MediaStream mediaStream, NBMPeerConnection nbmPeerConnection) {  
    322.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
    323.             Log.e(TAG, "-->onRemoteStreamAdded-->no");  
    324.             return;  
    325.   
    326.   
    327.         }  
    328.         Log.e(TAG, "-->onRemoteStreamAdded");  
    329.         RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SCALE_ASPECT_FILL;  
    330.         remoteRender = VideoRendererGui.create( REMOTE_X, REMOTE_Y,  
    331.                 REMOTE_WIDTH, REMOTE_HEIGHT,  
    332.                 scalingType, false);  
    333.         REMOTE_Y = REMOTE_Y+25;  
    334.         nbmWebRTCPeer.attachRendererToRemoteStream(remoteRender, mediaStream);  
    335.         runOnUiThread(new Runnable() {  
    336.             @Override  
    337.             public void run() {  
    338.                 mCallStatus.setText("");  
    339.             }  
    340.         });  
    341.     }  
    342.   
    343.     @Override  
    344.     public void onRemoteStreamRemoved(MediaStream mediaStream, NBMPeerConnection nbmPeerConnection) {  
    345.         Log.i(TAG, "onRemoteStreamRemoved");  
    346.     }  
    347.   
    348.     @Override  
    349.     public void onPeerConnectionError(String s) {  
    350.         Log.e(TAG, "onPeerConnectionError:" + s);  
    351.     }  
    352.   
    353.     @Override  
    354.     public void onRoomResponse(RoomResponse response) {  
    355.         Log.e(TAG, "-->OnRoomResponse:" + response);  
    356.         if (Integer.valueOf(response.getId()) == publishVideoRequestId){  
    357.             SessionDescription sd = new SessionDescription(SessionDescription.Type.ANSWER,  
    358.                     response.getValue("sdpAnswer").get(0));  
    359.             if (callState == CallState.PUBLISHING){  
    360.                 callState = CallState.PUBLISHED;  
    361.                 nbmWebRTCPeer.processAnswer(sd, "derp");  
    362.             } else if (callState == CallState.WAITING_REMOTE_USER){  
    363.                 callState = CallState.RECEIVING_REMOTE_USER;  
    364.                 nbmWebRTCPeer.processAnswer(sd, "remote");  
    365.             } else if (callState == CallState.PATICIPANT_JOINED){  
    366.   
    367.                 callState = CallState.RECEIVING_PATICIPANT;  
    368.                 nbmWebRTCPeer.processAnswer(sd, this.PaticipantID);  
    369.                 //NOP  
    370.             }  
    371.         }  
    372.     }  
    373.   
    374.     @Override  
    375.     public void onRoomError(RoomError error) {  
    376.         Log.e(TAG, "OnRoomError:" + error);  
    377.     }  
    378.   
    379.     @Override  
    380.     public void onRoomNotification(RoomNotification notification) {  
    381.         Log.e(TAG, "OnRoomNotification--> (state=" + callState.toString() + "):" + notification);  
    382.   
    383.         if(notification.getMethod().equals("iceCandidate")) {  
    384.             Map<String, Object> map = notification.getParams();  
    385.   
    386.             String sdpMid = map.get("sdpMid").toString();  
    387.             int sdpMLineIndex = Integer.valueOf(map.get("sdpMLineIndex").toString());  
    388.             String sdp = map.get("candidate").toString();  
    389.   
    390.             IceCandidate ic = new IceCandidate(sdpMid, sdpMLineIndex, sdp);  
    391.             Log.e(TAG, "callState-->" + callState);  
    392.             if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
    393.                 nbmWebRTCPeer.addRemoteIceCandidate(ic, "derp");  
    394.             }else if(callState==CallState.PATICIPANT_JOINED ||  callState== CallState.RECEIVING_PATICIPANT){  
    395.                 nbmWebRTCPeer.addRemoteIceCandidate(ic,this.PaticipantID);  
    396.             }else {  
    397.                 nbmWebRTCPeer.addRemoteIceCandidate(ic, "remote");  
    398.             }  
    399.         }  
    400.         if(notification.getMethod().equals("participantPublished"))  
    401.         {  
    402.             Map<String, Object> map = notification.getParams();  
    403.             final String user = map.get("id").toString();  
    404.             this.calluser = user;  
    405.             this.PaticipantID = "pt_"+this.calluser;  
    406.   
    407.             PeerVideoActivity.this.runOnUiThread(new Runnable() {  
    408.                 @Override  
    409.                 public void run() {  
    410.                     callState = CallState.PATICIPANT_JOINED;  
    411.                     nbmWebRTCPeer.generateOffer(PaticipantID, false);  
    412.   
    413.                 }  
    414.             });  
    415.         }  
    416.     }  
    417.   
    418.     @Override  
    419.     public void onRoomConnected() {  
    420.   
    421.     }  
    422.   
    423.     @Override  
    424.     public void onRoomDisconnected() {  
    425.   
    426.     }  
    427. }  



           再就是android room demo中的MainActivity的添加cert的代码要去掉注释,让这段代码生效,就可以连通服务器了。

           在iOS的实施方面,上面这家公司也提供了一个工具包:https://github.com/nubomediaTI/Kurento-iOS ,工具包里面也有demo

           Web方面,最上面官方的哪个demo就足够参考了

           后记:很荣幸这篇博客获得了很多CSDN程序员的关注和询问,这只能证明我很荣幸有机会在去年的那个时间点(16年7月)在大家之前处理了一个后续大家都很关注的技术问题,而处理这个问题主要用到的服务器端room server项目和android端nubo test项目,官方在后续好像都做了一定的升级,反而是我自己搞完这个之后,因为产品设计的原因,后来再没有深入地去生产实施这个东西,甚至开发笔记本关于这个项目的源码项目好像都已经删除了,对于大家提出的问题,早期的我还能答一答,后面的我估计你们用到的源码和我用到的源码估计都不是一个版本了,再就是里面的代码细节也基本忘得差不多,在这儿我建议后续开发这个功能可以去深入阅读分析Kurento官方(https://github.com/Kurento)和欧洲媒体服务云服务商nubomedia官方(https://github.com/nubomedia-vtt)的代码示例和文档。我面给出的代码样例是基于nubomedia一对一视聊样例改的,官方原始代码样例在这段时间内都有了变更。在掌握大的基本WebRTC通信的原理的前提下,我觉得改新的代码估计也不会太难。

     
     
    好文要顶 关注我 收藏该文  
    1
    0
     
     
     
    « 上一篇:使用 nginx 和 rtmp 插件搭建视频直播和点播服务器
    » 下一篇:WebRTC MCU( Multipoint Conferencing Unit)服务器调研
  • 相关阅读:
    Jms的MessageListener中的Jms事务
    Maven依赖排除 禁止依赖传递 取消依赖的方法
    数据库事务隔离级别-- 脏读、幻读、不可重复读(清晰解释)
    【JMS】JMS之ActiveMQ的使用
    servlet3.0 新特性——异步处理
    pyCharm远程调试
    pycharm激活方法(包括永久激活)
    UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa1 in position 3: invalid start byte错误解决办法
    Python数据分析Pandas的编程经验总结
    java版的状态机实现
  • 原文地址:https://www.cnblogs.com/yasepix/p/10776969.html
Copyright © 2011-2022 走看看