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

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编译问题:

  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官方提供的JavaClient因为底层API原因在android上不肯用,这个nubomedia组织提供了一个kurento android client的实现,同时还提供了一个kurento room client的实现以及room使用案例:https://github.com/nubomedia-vtt/nubo-test,这家公司对其开发的开源方案管理非常及时,早晨提个接口的issue,下午已经commit了代码修改。

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

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

再就是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通信的原理的前提下,我觉得改新的代码估计也不会太难。

基于Kurento的WebRTC移动视频群聊技术方案的更多相关文章

  1. 网易云信技术分享:IM中的万人群聊技术方案实践总结

    本文来自网易云信团队的技术分享,原创发表于网易云信公众号,原文链接:mp.weixin.qq.com/s/LT2dASI7QVpcOVxDAsMeVg,收录时有改动. 1.引言 在不了解IM技术的人眼 ...

  2. android 开发,视频群聊引发短信异常

    说到 NDK 开发,其实是为了有些时候为了项目需求需要调用底层的一些 C/C++ 的一些东西:另外就是为了效率更加高些. 但是很多时候能不用就不用:这个是啥原因?个人感觉有些时候是觉得麻烦,首先要配置 ...

  3. 一套高可用、易伸缩、高并发的IM群聊架构方案设计实践

    本文原题为“一套高可用群聊消息系统实现”,由作者“于雨氏”授权整理和发布,内容有些许改动,作者博客地址:alexstocks.github.io.应作者要求,如需转载,请联系作者获得授权. 一.引言 ...

  4. ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(四) 添加表情、群聊功能

    休息了两天,还是决定把这个尾巴给收了.本篇是最后一篇,也算是草草收尾吧.今天要加上表情功能和群聊.基本上就差不多了,其他功能,读者可以自行扩展或者优化.至于我写的代码方面,自己也没去重构.好的,我们开 ...

  5. 使用java做一个能赚钱的微信群聊机器人(2020年基于PC端协议最新可用版)

    前言 微信群机器人,主要用来管理群聊,提供类似天气查询.点歌.机器人聊天等用途. 由于微信将web端的协议封杀后,很多基于http协议的群聊机器人都失效了,所以这里使用基于PC端协议的插件来实现. 声 ...

  6. 基于itchat的微信群聊小助手基础开发(一)

    前段时间由于要管理微信群,基于itchat开发了一个简单的微信机器人 主要功能有: 图灵机器人功能 群聊昵称格式修改提示 消息防撤回功能 斗图功能 要开发一个基于itchat的最基本的聊天机器人,在g ...

  7. 基于ejabberd简单实现xmpp群聊离线消息

    首先,xmpp服务器是基于ejabberd.离线消息模块是mod_interact,原地址地址:https://github.com/adamvduke/mod_interact: 修改后实现群聊离线 ...

  8. Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)

    一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...

  9. 基于websocket的单聊.群聊

    关于ai.baidu.com的 代码: #########################################核心代码################################### ...

随机推荐

  1. iOS中类单例方法的一种实现

    在Cocos2D编程中,很多情况我们需要类只生成一个实例,这称之为该类的单例类. 一般我们在类中这样实现单例方法: +(instancetype)sharedInstance{ static Foo ...

  2. Gradle 1.12翻译——第十九章. Gradle 守护进程

    有关其他已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或访问:http://gradledoc.qiniudn.com ...

  3. Gitflow工作流程

    在工作场合实施Git的时候,有很多种工作流程可供选择,此时反而会让你手足无措.本文罗列了企业团队最常用的一些Git工作流程,包括Centralized Workflow.Feature Branch ...

  4. Linux多线程实践(2) --线程基本API

    POSIX线程库 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头,要使用这些函数库,要通过引入头文<pthread.h>,而且链 ...

  5. 【嵌入式开发】 Linux Kernel 下载 配置 编译 安装 及 驱动简介

    作者 : 韩曙亮 转载请出名出处 : http://blog.csdn.net/shulianghan/article/details/38636827 一. Linux 内核简介 1. 内核功能简介 ...

  6. java组播MulticastSocket

    在单播模式中有服务器端和客户端之分,而组播模式与单播模式不同,每个端都是以路由器或交换机做为中转广播站,任意一端向路由器或交换机发送消息,路由或交换机负责发送其他节点,每个节点都是同等的.所以在编程模 ...

  7. 网站开发进阶(三十)HTML5--本地存储Web Storage

    HTML5--本地存储Web Storage Web Storage功能,顾名思义,就是在Web上针对客户端本地储存数据的功能,具体来说Web Storage分为两种: sessionStorage: ...

  8. SpriteBuilder中不能编辑自定义类或不能给节点添加属性的解决

    不能编辑自定义类 你选中一个Sub File(CCBFile)节点,在这个例子中,该节点的Custom class区域灰化禁用且不能修改.这是因为你需要在该Sub File引用的CCB文件中修改Cus ...

  9. 2014新年福利,居然有人将Ext JS 4.1的文档翻译了

    原文:http://damoqiongqiu.iteye.com/blog/1998022

  10. android Titlebar一行代码实现沉浸式效果

    github地址 一个简单易用的导航栏TitleBar,可以轻松实现IOS导航栏的各种效果  整个代码全部集中在TitleBar.java中,所有控件都动态生成,动态布局.不需要引用任何资源文件,拷贝 ...