MCU進階-kurento-之五: kurento的composite組件測試與應用


1. kurento-composite-node-example例子的測試 
原文鏈接:
https://github.com/subu1979/kurento-composite-conference-recording-nodejs

這是一個nodejs的例子,
$ git clone https://github.com/subu1979/kurento-composite-conference-recording-nodejs.git
$ cd kurento-composite-node-example
$ npm install
$ npm start

啟動后無法工作; 

2. kurento-composite-recording的測試
原文鏈接:
https://codedump.io/share/Slmk2efR1g4v/1/composite-recording-creates-empty-video
http://stackoverflow.com/questions/34823540/compoiste-recording-creates-empty-video

先確認基於kurento-media-server的6.4.0的kurento-one2one-call能正常工作; 
然后修改CallMediaPipeline.java成如下:

public CallMediaPipeline(KurentoClient kurento) {
    try {
      this.pipeline = kurento.createMediaPipeline();


      /* The original
      this.callerWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();
      this.calleeWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();

      this.callerWebRtcEp.connect(this.calleeWebRtcEp);
      this.calleeWebRtcEp.connect(this.callerWebRtcEp);
      */


      /* Added by Hank, for testing composite */
      this.callerWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();
      this.calleeWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();

      Composite composite = new Composite.Builder(this.pipeline).build();
      HubPort hubPort1 = new HubPort.Builder(composite).build();
      HubPort hubPort2 = new HubPort.Builder(composite).build();
      HubPort hubPort3 = new HubPort.Builder(composite).build();

      RecorderEndpoint recorderEP =
              new RecorderEndpoint.Builder(this.pipeline,
                      "file:////opt/PJT_kurento/kms-6.4.0/kurento-composite-implement/record-samples/recording.webm").withMediaProfile(MediaProfileSpecType.WEBM).build();

      this.callerWebRtcEp.connect(this.calleeWebRtcEp);
      this.callerWebRtcEp.connect(hubPort1);

      this.calleeWebRtcEp.connect(hubPort2);
      this.calleeWebRtcEp.connect(this.callerWebRtcEp);

      hubPort3.connect(recorderEP);
      recorderEP.record();
      /* END */
    } catch (Throwable t) {
      if (this.pipeline != null) {
        pipeline.release();
      }
    }
}


運行后可以的目錄:
/opt/PJT_kurento/kms-6.4.0/kurento-composite-implement/record-samples/
下看到錄制的音頻與視頻文件: recording.webm

NOTE:
1).如果沒有看到文件的生成,則需要查看下目錄和kurento-media-server是不是相同的用戶名,
不是的話,修改目錄的用戶名即可。

2). 因為音頻的編碼是opus, 視頻的編碼是vp8,所以很多播放器是播放不了的,
用ffmpeg轉碼后可看; 

代碼分析:
原始的pipeline結構為:
callerWebRtcEp -------> calleeWebRtcEp
callerWebRtcEp <------- calleeWebRtcEp

添加了Composite與recorder后pipeline結構為:
callerWebRtcEp -----------------|------------------>calleeWebRtcEp
                                          V
                                     HubPort1
                            ________|_______
                           |        composite   |-->HubPort3-->recorderEp
                           |______________|
                                         ^
                                      HubPort2
callerWebRtcEp <----------------|-------------------calleeWebRtcEp

3. 開發成caller顯示雙人的畫面
修改CallMediaPipeline.java代碼如下:
  public CallMediaPipeline(KurentoClient kurento) {
    try {
      this.pipeline = kurento.createMediaPipeline();


      /* The original
      this.callerWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();
      this.calleeWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();


      this.callerWebRtcEp.connect(this.calleeWebRtcEp);
      this.calleeWebRtcEp.connect(this.callerWebRtcEp);
      */


      /* Added by Hank, for testing composite */
      this.callerWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();
      this.calleeWebRtcEp = new WebRtcEndpoint.Builder(pipeline).build();


      Composite composite = new Composite.Builder(this.pipeline).build();
      HubPort hubPort1 = new HubPort.Builder(composite).build();
      HubPort hubPort2 = new HubPort.Builder(composite).build();
      HubPort hubPort3 = new HubPort.Builder(composite).build();


      RecorderEndpoint recorderEP =
              new RecorderEndpoint.Builder(this.pipeline,
                      "file:////opt/PJT_kurento/kms-6.4.0/kurento-composite-implement/record-samples/recording.webm").withMediaProfile(MediaProfileSpecType.WEBM).build();


      this.callerWebRtcEp.connect(this.calleeWebRtcEp);
      this.callerWebRtcEp.connect(hubPort1);


      this.calleeWebRtcEp.connect(hubPort2);
      hubPort3.connect(this.callerWebRtcEp);


      /* END */


    } catch (Throwable t) {
      if (this.pipeline != null) {
        pipeline.release();
      }
    }
}


添加了Composite后pipeline結構為:
callerWebRtcEp -----------------|------------------>calleeWebRtcEp
                                          V
                                      hubPort1
                              ________|_______
callerWebRtcEp<-hubport3|  composite   |
                                 |______________|
                                           ^
                                       hubPort2<--------------calleeWebRtcEp
運行后的效果圖如下:


二、kurento-group-call添加composite功能
修改的文件有兩個,完整的代碼如下:
Room.java

  1. /*
  2.  * (C) Copyright 2014 Kurento (http://kurento.org/)
  3.  *
  4.  * All rights reserved. This program and the accompanying materials
  5.  * are made available under the terms of the GNU Lesser General Public License
  6.  * (LGPL) version 2.1 which accompanies this distribution, and is available at
  7.  * http://www.gnu.org/licenses/lgpl-2.1.html
  8.  *
  9.  * This library is distributed in the hope that it will be useful,
  10.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12.  * Lesser General Public License for more details.
  13.  *
  14.  */
  15. package org.kurento.tutorial.groupcall;
  16. import java.io.Closeable;
  17. import java.io.IOException;
  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.List;
  21. import java.util.concurrent.ConcurrentHashMap;
  22. import java.util.concurrent.ConcurrentMap;
  23. import javax.annotation.PreDestroy;
  24. import org.kurento.client.Continuation;
  25. import org.kurento.client.MediaPipeline;
  26. import org.slf4j.Logger;
  27. import org.slf4j.LoggerFactory;
  28. import org.springframework.web.socket.WebSocketSession;
  29. import com.google.gson.JsonArray;
  30. import com.google.gson.JsonElement;
  31. import com.google.gson.JsonObject;
  32. import com.google.gson.JsonPrimitive;
  33. /* Added by Hank */
  34. import org.kurento.client.*;
  35. /* END */
  36. /**
  37.  * @author Ivan Gracia (izanmail@gmail.com)
  38.  * @since 4.3.1
  39.  */
  40. public class Room implements Closeable {
  41.   private final Logger log = LoggerFactory.getLogger(Room.class);
  42.   private final ConcurrentMap<String, UserSession> participants = new ConcurrentHashMap<>();
  43.   private final MediaPipeline pipeline;
  44.   private final String name;
  45.   /* Added by Hank */
  46.   private Composite composite = null;
  47.   private RecorderEndpoint recorderEndpoint;
  48.   private HubPort compoisteOutputHubport;
  49.   public HubPort getCompositeOutputHubport() {
  50.     return compoisteOutputHubport;
  51.   }
  52.   /* END */
  53.   public String getName() {
  54.     return name;
  55.   }
  56.   public Room(String roomName, MediaPipeline pipeline) {
  57.     this.name = roomName;
  58.     this.pipeline = pipeline;
  59.     /* Added by Hank */
  60.     this.composite = new Composite.Builder(this.pipeline).build();
  61.     this.compoisteOutputHubport = new HubPort.Builder(composite).build();
  62.     /* END */
  63.     log.info("ROOM {} has been created", roomName);
  64.   }
  65.   @PreDestroy
  66.   private void shutdown() {
  67.     this.close();
  68.   }
  69.   public UserSession join(String userName, WebSocketSession session) throws IOException {
  70.     log.info("ROOM {}: adding participant {}", userName, userName);
  71.     /* Added by Hank */
  72.     // Original
  73.     //final UserSession participant = new UserSession(userName, this.name, session, this.pipeline);
  74.     // Adding
  75.     final UserSession participant = new UserSession(userName, this.name, session,
  76.             this.pipeline, this.composite, this.compoisteOutputHubport);
  77.     if (participants.size() == 1){
  78.       // first user join
  79.       log.info("ROOM {} : Start recording", getName());
  80.       this.recorderEndpoint = new RecorderEndpoint.Builder(pipeline,
  81.               "file:///opt/PJT_kurento/kms-6.4.0/kurento-composite-implement/record-samples/"+getName()+".webm")
  82.               .withMediaProfile(MediaProfileSpecType.MP4)
  83.               .build();
  84.       compoisteOutputHubport.connect(recorderEndpoint);
  85.       recorderEndpoint.connect(compoisteOutputHubport);
  86.       recorderEndpoint.record();
  87.     }
  88.     /* END */
  89.     joinRoom(participant);
  90.     participants.put(participant.getName(), participant);
  91.     sendParticipantNames(participant);
  92.     return participant;
  93.   }
  94.   public void leave(UserSession user) throws IOException {
  95.     log.debug("PARTICIPANT {}: Leaving room {}", user.getName(), this.name);
  96.     this.removeParticipant(user.getName());
  97.     user.close();
  98.   }
  99.   private Collection<String> joinRoom(UserSession newParticipant) throws IOException {
  100.     final JsonObject newParticipantMsg = new JsonObject();
  101.     newParticipantMsg.addProperty("id", "newParticipantArrived");
  102.     newParticipantMsg.addProperty("name", newParticipant.getName());
  103.     final List<String> participantsList = new ArrayList<>(participants.values().size());
  104.     log.debug("ROOM {}: notifying other participants of new participant {}", name,
  105.         newParticipant.getName());
  106.     for (final UserSession participant : participants.values()) {
  107.       try {
  108.         participant.sendMessage(newParticipantMsg);
  109.       } catch (final IOException e) {
  110.         log.debug("ROOM {}: participant {} could not be notified", name, participant.getName(), e);
  111.       }
  112.       participantsList.add(participant.getName());
  113.     }
  114.     return participantsList;
  115.   }
  116.   private void removeParticipant(String name) throws IOException {
  117.     participants.remove(name);
  118.     log.debug("ROOM {}: notifying all users that {} is leaving the room", this.name, name);
  119.     final List<String> unnotifiedParticipants = new ArrayList<>();
  120.     final JsonObject participantLeftJson = new JsonObject();
  121.     participantLeftJson.addProperty("id", "participantLeft");
  122.     participantLeftJson.addProperty("name", name);
  123.     for (final UserSession participant : participants.values()) {
  124.       try {
  125.         participant.cancelVideoFrom(name);
  126.         participant.sendMessage(participantLeftJson);
  127.       } catch (final IOException e) {
  128.         unnotifiedParticipants.add(participant.getName());
  129.       }
  130.     }
  131.     if (!unnotifiedParticipants.isEmpty()) {
  132.       log.debug("ROOM {}: The users {} could not be notified that {} left the room", this.name,
  133.           unnotifiedParticipants, name);
  134.     }
  135.   }
  136.   public void sendParticipantNames(UserSession user) throws IOException {
  137.     final JsonArray participantsArray = new JsonArray();
  138.     for (final UserSession participant : this.getParticipants()) {
  139.       if (!participant.equals(user)) {
  140.         final JsonElement participantName = new JsonPrimitive(participant.getName());
  141.         participantsArray.add(participantName);
  142.       }
  143.     }
  144.     final JsonObject existingParticipantsMsg = new JsonObject();
  145.     existingParticipantsMsg.addProperty("id", "existingParticipants");
  146.     existingParticipantsMsg.add("data", participantsArray);
  147.     log.debug("PARTICIPANT {}: sending a list of {} participants", user.getName(),
  148.         participantsArray.size());
  149.     user.sendMessage(existingParticipantsMsg);
  150.   }
  151.   public Collection<UserSession> getParticipants() {
  152.     return participants.values();
  153.   }
  154.   public UserSession getParticipant(String name) {
  155.     return participants.get(name);
  156.   }
  157.   @Override
  158.   public void close() {
  159.     for (final UserSession user : participants.values()) {
  160.       try {
  161.         user.close();
  162.       } catch (IOException e) {
  163.         log.debug("ROOM {}: Could not invoke close on participant {}", this.name, user.getName(),
  164.             e);
  165.       }
  166.     }
  167.     participants.clear();
  168.     pipeline.release(new Continuation<Void>() {
  169.       @Override
  170.       public void onSuccess(Void result) throws Exception {
  171.         log.trace("ROOM {}: Released Pipeline", Room.this.name);
  172.       }
  173.       @Override
  174.       public void onError(Throwable cause) throws Exception {
  175.         log.warn("PARTICIPANT {}: Could not release Pipeline", Room.this.name);
  176.       }
  177.     });
  178.     log.debug("Room {} closed", this.name);
  179.   }
  180. }




    1. // UserSession.java
    2. /*
    3.  * (C) Copyright 2014 Kurento (http://kurento.org/)
    4.  *
    5.  * All rights reserved. This program and the accompanying materials
    6.  * are made available under the terms of the GNU Lesser General Public License
    7.  * (LGPL) version 2.1 which accompanies this distribution, and is available at
    8.  * http://www.gnu.org/licenses/lgpl-2.1.html
    9.  *
    10.  * This library is distributed in the hope that it will be useful,
    11.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    13.  * Lesser General Public License for more details.
    14.  *
    15.  */
    16. package org.kurento.tutorial.groupcall;
    17. import java.io.Closeable;
    18. import java.io.IOException;
    19. import java.util.concurrent.ConcurrentHashMap;
    20. import java.util.concurrent.ConcurrentMap;
    21. import org.kurento.client.Continuation;
    22. import org.kurento.client.EventListener;
    23. import org.kurento.client.IceCandidate;
    24. import org.kurento.client.MediaPipeline;
    25. import org.kurento.client.OnIceCandidateEvent;
    26. import org.kurento.client.WebRtcEndpoint;
    27. /* Added by Hank */
    28. import org.kurento.client.*;
    29. /* END */
    30. import org.kurento.jsonrpc.JsonUtils;
    31. import org.slf4j.Logger;
    32. import org.slf4j.LoggerFactory;
    33. import org.springframework.web.socket.TextMessage;
    34. import org.springframework.web.socket.WebSocketSession;
    35. import com.google.gson.JsonObject;
    36. /**
    37.  *
    38.  * @author Ivan Gracia (izanmail@gmail.com)
    39.  * @since 4.3.1
    40.  */
    41. public class UserSession implements Closeable {
    42.   private static final Logger log = LoggerFactory.getLogger(UserSession.class);
    43.   private final String name;
    44.   private final WebSocketSession session;
    45.   private final MediaPipeline pipeline;
    46.   private final String roomName;
    47.   private final WebRtcEndpoint outgoingMedia;
    48.   private final ConcurrentMap<String, WebRtcEndpoint> incomingMedia = new ConcurrentHashMap<>();
    49.   /* Added by Hank */
    50.   private HubPort compositeInputHubPort;
    51.   private HubPort compositeOutputHubPort;
    52.   /* END */
    53.   /* Added by Hank */
    54.   // Original
    55.   //public UserSession(final String name, String roomName, final WebSocketSession session,
    56.   // MediaPipeline pipeline) {
    57.   // New
    58.   public UserSession(final String name, String roomName, final WebSocketSession session,
    59.     MediaPipeline pipeline, Hub composite, HubPort compositeOutputHubPort) {
    60.     /* END */
    61.     this.pipeline = pipeline;
    62.     this.name = name;
    63.     this.session = session;
    64.     this.roomName = roomName;
    65.     this.outgoingMedia = new WebRtcEndpoint.Builder(pipeline).build();
    66.     /* Added by Hank */
    67.     this.compositeInputHubPort = new HubPort.Builder(composite).build();
    68.     compositeInputHubPort.connect(outgoingMedia);
    69.     outgoingMedia.connect(compositeInputHubPort);
    70.     this.compositeOutputHubPort = compositeOutputHubPort;
    71.     /* END */
    72.     this.outgoingMedia.addOnIceCandidateListener(new EventListener<OnIceCandidateEvent>() {
    73.       @Override
    74.       public void onEvent(OnIceCandidateEvent event) {
    75.         JsonObject response = new JsonObject();
    76.         response.addProperty("id", "iceCandidate");
    77.         response.addProperty("name", name);
    78.         response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
    79.         try {
    80.           synchronized (session) {
    81.             session.sendMessage(new TextMessage(response.toString()));
    82.           }
    83.         } catch (IOException e) {
    84.           log.debug(e.getMessage());
    85.         }
    86.       }
    87.     });
    88.   }
    89.   public WebRtcEndpoint getOutgoingWebRtcPeer() {
    90.     return outgoingMedia;
    91.   }
    92.   public String getName() {
    93.     return name;
    94.   }
    95.   public WebSocketSession getSession() {
    96.     return session;
    97.   }
    98.   /**
    99.    * The room to which the user is currently attending.
    100.    *
    101.    * @return The room
    102.    */
    103.   public String getRoomName() {
    104.     return this.roomName;
    105.   }
    106.   public void receiveVideoFrom(UserSession sender, String sdpOffer) throws IOException {
    107.     log.info("USER {}: connecting with {} in room {}", this.name, sender.getName(), this.roomName);
    108.     log.trace("USER {}: SdpOffer for {} is {}", this.name, sender.getName(), sdpOffer);
    109.     final String ipSdpAnswer = this.getEndpointForUser(sender).processOffer(sdpOffer);
    110.     final JsonObject scParams = new JsonObject();
    111.     scParams.addProperty("id", "receiveVideoAnswer");
    112.     scParams.addProperty("name", sender.getName());
    113.     scParams.addProperty("sdpAnswer", ipSdpAnswer);
    114.     log.trace("USER {}: SdpAnswer for {} is {}", this.name, sender.getName(), ipSdpAnswer);
    115.     this.sendMessage(scParams);
    116.     log.debug("gather candidates");
    117.     this.getEndpointForUser(sender).gatherCandidates();
    118.   }
    119.   private WebRtcEndpoint getEndpointForUser(final UserSession sender) {
    120.     if (sender.getName().equals(name)) {
    121.       log.debug("PARTICIPANT {}: configuring loopback", this.name);
    122.       return outgoingMedia;
    123.     }
    124.     log.debug("PARTICIPANT {}: receiving video from {}", this.name, sender.getName());
    125.     WebRtcEndpoint incoming = incomingMedia.get(sender.getName());
    126.     if (incoming == null) {
    127.       log.debug("PARTICIPANT {}: creating new endpoint for {}", this.name, sender.getName());
    128.       incoming = new WebRtcEndpoint.Builder(pipeline).build();
    129.       incoming.addOnIceCandidateListener(new EventListener<OnIceCandidateEvent>() {
    130.         @Override
    131.         public void onEvent(OnIceCandidateEvent event) {
    132.           JsonObject response = new JsonObject();
    133.           response.addProperty("id", "iceCandidate");
    134.           response.addProperty("name", sender.getName());
    135.           response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
    136.           try {
    137.             synchronized (session) {
    138.               session.sendMessage(new TextMessage(response.toString()));
    139.             }
    140.           } catch (IOException e) {
    141.             log.debug(e.getMessage());
    142.           }
    143.         }
    144.       });
    145.       incomingMedia.put(sender.getName(), incoming);
    146.     }
    147.     log.debug("PARTICIPANT {}: obtained endpoint for {}", this.name, sender.getName());
    148.     /* Added by Hank */
    149.     // Original
    150.     //sender.getOutgoingWebRtcPeer().connect(incoming);
    151.     // New
    152.     this.compositeInputHubPort.connect(incoming);
    153.     /* END */
    154.     return incoming;
    155.   }
    156.   public void cancelVideoFrom(final UserSession sender) {
    157.     this.cancelVideoFrom(sender.getName());
    158.   }
    159.   public void cancelVideoFrom(final String senderName) {
    160.     log.debug("PARTICIPANT {}: canceling video reception from {}", this.name, senderName);
    161.     final WebRtcEndpoint incoming = incomingMedia.remove(senderName);
    162.     log.debug("PARTICIPANT {}: removing endpoint for {}", this.name, senderName);
    163.     incoming.release(new Continuation<Void>() {
    164.       @Override
    165.       public void onSuccess(Void result) throws Exception {
    166.         log.trace("PARTICIPANT {}: Released successfully incoming EP for {}", UserSession.this.name,
    167.             senderName);
    168.       }
    169.       @Override
    170.       public void onError(Throwable cause) throws Exception {
    171.         log.warn("PARTICIPANT {}: Could not release incoming EP for {}", UserSession.this.name,
    172.             senderName);
    173.       }
    174.     });
    175.   }
    176.   @Override
    177.   public void close() throws IOException {
    178.     log.debug("PARTICIPANT {}: Releasing resources", this.name);
    179.     for (final String remoteParticipantName : incomingMedia.keySet()) {
    180.       log.trace("PARTICIPANT {}: Released incoming EP for {}", this.name, remoteParticipantName);
    181.       final WebRtcEndpoint ep = this.incomingMedia.get(remoteParticipantName);
    182.       ep.release(new Continuation<Void>() {
    183.         @Override
    184.         public void onSuccess(Void result) throws Exception {
    185.           log.trace("PARTICIPANT {}: Released successfully incoming EP for {}",
    186.               UserSession.this.name, remoteParticipantName);
    187.         }
    188.         @Override
    189.         public void onError(Throwable cause) throws Exception {
    190.           log.warn("PARTICIPANT {}: Could not release incoming EP for {}", UserSession.this.name,
    191.               remoteParticipantName);
    192.         }
    193.       });
    194.     }
    195.     outgoingMedia.release(new Continuation<Void>() {
    196.       @Override
    197.       public void onSuccess(Void result) throws Exception {
    198.         log.trace("PARTICIPANT {}: Released outgoing EP", UserSession.this.name);
    199.       }
    200.       @Override
    201.       public void onError(Throwable cause) throws Exception {
    202.         log.warn("USER {}: Could not release outgoing EP", UserSession.this.name);
    203.       }
    204.     });
    205.   }
    206.   public void sendMessage(JsonObject message) throws IOException {
    207.     log.debug("USER {}: Sending message {}", name, message);
    208.     synchronized (session) {
    209.       session.sendMessage(new TextMessage(message.toString()));
    210.     }
    211.   }
    212.   public void addCandidate(IceCandidate candidate, String name) {
    213.     if (this.name.compareTo(name) == 0) {
    214.       outgoingMedia.addIceCandidate(candidate);
    215.     } else {
    216.       WebRtcEndpoint webRtc = incomingMedia.get(name);
    217.       if (webRtc != null) {
    218.         webRtc.addIceCandidate(candidate);
    219.       }
    220.     }
    221.   }
    222.   /*
    223.    * (non-Javadoc)
    224.    *
    225.    * @see java.lang.Object#equals(java.lang.Object)
    226.    */
    227.   @Override
    228.   public boolean equals(Object obj) {
    229.     if (this == obj) {
    230.       return true;
    231.     }
    232.     if (obj == null || !(obj instanceof UserSession)) {
    233.       return false;
    234.     }
    235.     UserSession other = (UserSession) obj;
    236.     boolean eq = name.equals(other.name);
    237.     eq &= roomName.equals(other.roomName);
    238.     return eq;
    239.   }
    240.   /*
    241.    * (non-Javadoc)
    242.    *
    243.    * @see java.lang.Object#hashCode()
    244.    */
    245.   @Override
    246.   public int hashCode() {
    247.     int result = 1;
    248.     result = 31 * result + name.hashCode();
    249.     result = 31 * result + roomName.hashCode();
    250.     return result;
    251.   }
    252. }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM