这几天看的几部电影

9号就去看了《夏洛特烦恼》这部电影,看完后想叨叨两句。

和十一假期回家看的《港囧》相比,只能用一句话来形容:夏洛特烦恼比港囧好一万个煎饼侠。(原谅我《煎饼侠》只在油管上看了枪版,虽然只是一小部分)

看《港囧》后看到四分之三的时候,我的一个感觉就越发凸显:这什么玩意儿,三不沾啊。

暂不管别人对一部电影有什么期许,我觉得我自己还是要求蛮低的:能给我一点东西就好,就一点。我个人比较偏向于喜剧,科幻类电影,喜剧的话你能逗我笑,让我放松放松就好,科幻片能让我在一个很大的脑洞里看一看,顺便给我科普些知识就好,如果能再给我一点可以咀嚼的东西,那就算是锦上添花了。

港囧先只看名字和背景,给我的第一印象就是他是个喜剧,然后我就买票来看了,看了开头发现,貌似还有感情戏在里面哦,看了一半发现,咋没怎么笑过,也没怎么伤心过,是不是最后给你来个大的?看到四分之三发现,估计没戏了,也就只能继续瞎扯了;到最后灯还没亮我就站起来拉着老婆要走,失望!原本希望回去的路上能让我再回味一下以抵挡深夜的寒风,结果两人冻得跟狗一样寥寥几句话就到家了。这部电影让我领略了炒作的威力,水军的威力,就算是坨屎,那些专业人士也能给你炒成香饽饽。我能记住的,也只有以后决定要不要买票前要先看看预告片,影评什么的,票钱虽不贵,但扔了可惜。徐导演本人在知乎关于这部电影有现身,感兴趣者可移步自行搜索。

夏洛特烦恼,我觉得倒是可以作为港囧的一个正面教材。他同样是个喜剧,也加了感情因素在里面,但是他想让观众笑的时候,笑料真的够啊,他想让观众哭的时候,泪点又很足啊。那么多笑料,原创的笑料,细地不能再细节的笑料。最重要的是,他用心了,真走心了,怪不得不少人去二刷三刷。我原本也没期望能从这部电影得到什么深层次的东西,而且我觉得制作团队也没加太多这方面的进去,这是个很聪明的决定,真的和Unix的哲学很像:只做一件事,然后把它做好。真正的产品经理,擅长的不应该是如何添加功能,而是如何删除功能。

煎饼侠呢,纯粹的喜剧片,无厘头的那种,确实有屌丝男士的遗风,一段一段独立的笑料,前后无连贯情节,跟一块块补丁似的。这种形式演个20来分钟的屌丝男士还好,放到电影身上,我不敢苟同。

突然想起了几个月之前看的一部电影《少年班》,看似一部找不到闪光点的电影,我却看到了一丝讽刺的意味,也许是我过度解读了,但我依然认为没那么简单,这部电影让我想起了四大名著。豆瓣影评看到好多贬的,可能是因为电影把他想传达的传达了,但是没有用一个吸引人的故事来做外壳的原因吧。

还有,夏洛里面的几首主题歌都挺不错的,插曲选的也还行,走心!

关于Jet的一些想法

来到快网之后一直在搞流媒体,在以前的公司也是大部分时间搞音视频,看来这很可能就是我的路吧,是时候沉下心来,踏踏实实做点东西了。再加上新公司给提供的环境还可以,周围有一些牛人,不算太忙的节奏,还有主要就是在这可以深度学习挖掘一些东西,而不是像以前那样绝大部分时间都是在实现业务逻辑。

在公司做的产品就是流媒体CDN服务器。要求很简单:稳定,性能。其实互联网行业要求支持的协议不是太多,初期对性能也没什么要求,难点还是在稳定性和快速实现客户业务需求上。刚到公司时,我被分配的任务是和另外4个同事实现RTMP协议的直播转发。不知道为什么,明明知道以后肯定要支持多种协议之间的互相转换,可项目还是被拆成了2个,分2拨人去搞。我认为当初的这个决定肯定会导致以后的合并会把项目搞的越来越乱,现在看来还是有这个趋势的,不再多表。

随着公司项目的进展,我也逐渐了解了这个行业,了解了许多相关的商业的、开源的产品或项目,了解了客户的需求以及未来可能会有的需求,于是脑海中一个自己设计的项目渐渐清晰开来。我的计划是实现一个流媒体框架,类似Gstreamer,VLC那样的,可以通过插件轻松扩展。客户可以基于这个框架快速实现服务器或客户端。首先如何让客户做到快速实现?我认为这是最重要的,客户需要做的其实只有实现他自己的业务逻辑,其他一切都不需要关心:性能,流媒体协议,编解码,缓存等等。这个实现必须具有很低的学习成本,因此开发语言就很重要了,C/C++肯定不是最好的,拿它们来写业务纯粹是自虐,我的想法是给客户提供多种语言,如Lua,JavaScript等。Lua的特点是很小的footprint,高性能首选,嵌入式必选,所以它适合各种应用场景:服务器,嵌入式(智能手机,智能硬件,工控设备)都是很完美的。JavaScript则是针对很多人有很小的学习成本,看Node.JS和Github就知道了。以后可能还会考虑Java,因为Java是企业大中型项目的首选,而且wowza就提供了Java的API,所以可能会考虑支持,而我们的引擎因为是C写的,因此又会比wowza有一些性能优势。除了选择一门合适的语言,生态也是很重要的,因此我计划Lua的API参考openresty来设计,尽量做到完全兼容(应该无法做到完全兼容,区别还是很大的),JavaScript的API完全按照Node.JS设计,确保Node.js的模块可以直接运行在我的引擎上,使用了libuv的C++模块可能会兼容不了,不过这种模块应该也不是很多。

框架的实现,我计划基于coroutine来做,因为用coroutine写非阻塞IO实在是酸爽,直接甩callback几条街。还有就是看看Go,Rust就知道了。state thread这个库是我从SRS中得知的,很不错的一个库,该做的他基本都做了,不该做的他也没多做。而且有SRS这个很棒的项目一直在用,我就不用担心坑了。st没有实现异步文件IO,没有支持Windows IOCP,这些都是我以后需要添加的。异步文件IO可以按libuv那种方式设计。

框架的设计,我选择了Gstreamer,因为我认为这个框架设计的不错(虽然实现的有点不是那么追求完美),层次分明,官方积累了很多插件,基本全方位覆盖音视频各种协议和编解码,还有就是以前搞TI的Davinci的时候,官方只提供了Gstreamer的插件,所以感觉这个适用面更广一些,用户基数更大一些。虽然后来随着对Gstreamer越来越熟悉就越感觉好多地方写得不是很好(毕竟它只是被拿来做客户端的,不需要考虑太多),但是我还是认为架构设计的很不错。Gstreamer的作者说他是参照DirectShow来设计的,我也认可这一点,而且更坚定了我选择它的决心。微软下一代的多媒体框架是Media Foundation,我也会参考进来。

整个框架全部纯C开发,使用GObject实现OO,以及完成和业务层开发语言的对接。需要深度优化Gstreamer,话说看到那些lock/unlock我就很无语,而且还那么多!这方面Nginx是个典范,幸好在公司做项目对Nginx还算一般熟。网络IO肯定是使用各OS上性能最高的API:epoll,kqueue,IOCP。多线程架构,线程间使用MQ通信,需要参考ZeroMQ基于st写一套MQ库。集群的支持也可以使用这个MQ,可以参考的有云风的skynet。硬件支持计划的有x86,x64,ARM,以后可能会考虑NPU。OS支持肯定会有3大主流OS,以后可能会考虑支持一些RTOS如VxWorks,TI-RTOS等。

业务层开发语言。Lua本身支持coroutine,但是不能在C中yield,所以采用了Lua 5.1.5,打了luajit coco的补丁,然后仿照openresty,添加了可以统一调度的thread API,Lua的coroutine因为是非对称coroutine,所以无法用于非阻塞IO,只能用新加的thread API。我说的无法用的意思是openresty里的那种用法:同步方式写但是异步方式执行。JavaScript的话需要让st实现回调触发的功能,同时也要支持Lua的那种写法:同步写异步执行,可以参考的有孢子响马开源的fibjs项目,但是我的可以直接利用Node.js的生态。

应用场景想了好多好多,例如软交换,互联网行业流媒体服务器,客户端播放器,客户端播放插件,安防行业信令/转发等服务器,互联网流媒体SaaS提供商,HTPC,电视盒子等等,当然其中有很多甚至全部是不怎么靠谱的,但是当有一天我把框架做出来了,也许应用自然就有了,总之现在的当务之急就是先把它做出来。

代码会开源托管在Github,使用travis-ci做build和unit test,目前还是在heavy develop,代码还没提交,欢迎follow和star。代码提交首个版本后应该会做一个简单的页面放在Github Pages上。

关于开源,因为核心是基于GStreamer的,GStreamer遵循LGPL协议,因此整个核心也只能是LGPL协议并开源,但是插件是使用动态链接的方式,因此可以避开开源的问题。开源对该项目肯定是利大于弊的,产品的价值最终还是要体现在业务和服务上,我做的只是给大家提供一个高质量的二次开发的平台而已,当然这个大家也包括我自己。

在Nginx的worker进程fork之后

项目中遇到一个问题:CPU占用在某种条件下会到接近100%,当然肯定不是业务满载,查吧。

先看日志中的关键部分。
出问题的fd销毁时刻:
2014/10/31 21:12:10 [debug] 23574#23577: epoll: fd:28 ev:0019 d:00007F16ED6981C0
2014/10/31 21:12:10 [debug] 23574#23577: epoll_wait() error on fd:28 ev:0019
2014/10/31 21:12:10 [debug] 23574#23577: 2 recv: fd:28 -1 of 146
2014/10/31 21:12:10 [info ] 23574#23577:
2 recv() failed (104: Connection reset by peer), client: 192.168.147.75, server: 0.0.0.0:1935
2014/10/31 21:12:10 [debug] 23574#23577: 2 ngx_rtmp_put_session
2014/10/31 21:12:10 [debug] 23574#23577:
2 finalize session
该fd出问题时刻:
2014/11/04 09:47:50 [debug] 23574#23577: epoll: stale event 00007F16ED6981C0
2014/11/04 09:47:50 [debug] 23574#23577: process events delta: 0
2014/11/04 09:47:50 [debug] 23574#23577: accept mutex lock failed
2014/11/04 09:47:50 [debug] 23574#23577: epoll timer: 10
2014/11/04 09:47:50 [debug] 23574#23577: epoll: stale event 00007F16ED6981C0
2014/11/04 09:47:50 [debug] 23574#23577: process events delta: 0
2014/11/04 09:47:50 [debug] 23574#23577: accept mutex lock failed
2014/11/04 09:47:50 [debug] 23574#23577: epoll timer: 10
就是这个stale event导致epoll_wait马上返回,从而占用这么多CPU。

为了验证该问题,先看一个测试demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
void fork_server_routine()
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock < 0) return;
struct sockaddr_in addr = {0};
addr.sin_family = PF_INET;
addr.sin_port = htons(9999);
int rc = 1;
rc = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &rc , sizeof(int));
if (rc < 0) {
perror("setsockopt");
return;
}
rc = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0) {
perror("bind");
return;
}
rc = listen(sock, 32);
if (rc < 0) return;
int ep = epoll_create(1);
struct epoll_event events[32];
events[0].data.fd = sock;
events[0].events = EPOLLIN | EPOLLET;
rc = epoll_ctl(ep, EPOLL_CTL_ADD, sock, &events[0]);
if (rc < 0) return;
rc = epoll_wait(ep, events, 32, -1);
if (rc < 0) return;
printf("epoll_wait: %d\n", rc);
socklen_t sal = sizeof(addr);
int client = accept(sock, (struct sockaddr *)&addr, &sal);
if (client < 0) return;
printf("accepted: %d\n", client);
events[0].data.fd = client;
events[0].events = EPOLLIN | EPOLLET;
rc = epoll_ctl(ep, EPOLL_CTL_ADD, client, &events[0]);
if (rc < 0) return;
rc = fork();
if (rc < 0) return;
if (rc == 0) {
printf("child start\n");
for (;;) {
sleep(3);
}
printf("child quit\n");
} else {
printf("parent start\n");
memset(events, 0, sizeof(events));
rc = epoll_wait(ep, events, 32, -1);
if (rc < 0) {
perror("parent: epoll_wait");
return;
}
printf("parent: epoll1 return %d, fd = %d\n", rc, events[0].data.fd);
memset(events, 0, sizeof(events));
printf("close %d\n", client);
epoll_ctl(ep, EPOLL_CTL_DEL, client, &events[0]); // 注意这一行
close(client);
memset(events, 0, sizeof(events));
rc = epoll_wait(ep, events, 32, -1);
if (rc < 0) {
perror("parent: epoll_wait");
return;
}
printf("parent: epoll2 return %d, fd = %d\n", rc, events[0].data.fd);
}
}
void fork_client_routine()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return;
struct sockaddr_in addr = {0};
addr.sin_family = PF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int rc = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0) return;
for (;;) {
printf("connected, press any\n");
getchar();
send(sock, &addr, 1, 0);
}
}
int
main (int argc, char **argv)
{
if (argc < 2) {
printf("./t1 c or s\n");
return 0;
}
if ('c' == argv[1][0]) {
fork_client_routine();
} else if ('s' == argv[1][0]) {
fork_server_routine();
} else {
printf("./t1 c or s\n");
return 0;
}
return 0;
}

当server-parent已经接受1个客户端后,fork出server-child,则server-child对client fd增加了1个引用(实际环境是父进程fork后马上exec,但是该client fd没有指定close-on-exec,所以情况和本测试demo相同)。这时如果server-parent close这个client fd,client进程是不会知道对端已经关闭的,因为在kernel里client fd的引用还没减到0,所以client进程照样可以正常发数据。
问题就出在server-parent close这个client fd上:server-parent到底需不需要用EPOLL_CTL_DEL把client fd删除呢?当然close肯定是要调用的。

下面是Nginx代码截取:
http://trac.nginx.org/nginx/browser/nginx/src/event/modules/ngx_epoll_module.c#L441

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ngx_int_t
ngx_epoll_del_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
 int op;
 uint32_t prev;
 ngx_event_t *e;
 ngx_connection_t *c;
 struct epoll_event ee;
 /*
 * when the file descriptor is closed, the epoll automatically deletes
 * it from its queue, so we do not need to delete explicitly the event
 * before the closing the file descriptor
 */
 if (flags & NGX_CLOSE_EVENT) {
  ev->active = 0;
  return NGX_OK;
}

可以看到nginx作者还专门加了注释说明这个情况,他的意思是:如果flags指明了NGX_CLOSE_EVENT,就不需要再EPOLL_CTL_DEL了,因为epoll会自动从队列中删掉他。
而NGX_CLOSE_EVENT就是为了这个功能而设定的:

1
2
3
4
5
6
7
8
9
10
11
/*
 * The event filter is deleted just before the closing file.
 * Has no meaning for select and poll.
 * kqueue, epoll, rtsig, eventport: allows to avoid explicit delete,
 * because filter automatically is deleted
 * on file close,
 *
 * /dev/poll: we need to flush POLLREMOVE event
 * before closing file.
 */
 #define NGX_CLOSE_EVENT 1

那下面就到了调用ngx_epoll_del_event的地方了:
http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_connection.c#L911

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
ngx_close_connection(ngx_connection_t *c)
{
 ngx_err_t err;
 ngx_uint_t log_error, level;
 ngx_socket_t fd;
 if (c->fd == (ngx_socket_t) -1) {
  ngx_log_error(NGX_LOG_ALERT, c->log, 0, "connection already closed");
  return;
 }
 if (c->read->timer_set) {
  ngx_del_timer(c->read);
}
 if (c->write->timer_set) {
 ngx_del_timer(c->write);
 }
 if (ngx_del_conn) {
  ngx_del_conn(c, NGX_CLOSE_EVENT);
 }

可以看到flags确实传入了NGX_CLOSE_EVENT,所以现在可以知道了:nginx在close该client fd之前,并没有用EPOLL_CTL_DEL把client fd删除。

但是如果你用测试demo,把epoll_ctl(ep, EPOLL_CTL_DEL, client…那一行注释掉,你会惊奇的发现当client继续发数据,server-parent最后的那个epoll_wait仍然会把client fd返回到应用层(即epoll惊群问题):
parent: epoll2 return 1, fd = 5
为什么呢?因为你没有EPOLL_CTL_DEL,把注释的那行打开就好了。
nginx中的epoll_wait如果再一次拿到这个他认为已经关闭的client fd,他就会当做stale event了(通过使用instance变量),然后什么都不做。但是下次epoll_wait还是会把这个client id返回,从而导致CPU占用100%,就不知道为啥了,gdb可以看到epoll_wait返回了EPOLLIN|EPOLLHUP|EPOLLERR,查了下资料:EPOLLHUP是在对端正常关闭时发生,EPOLLERR不知道什么情况下会发生。现在可以猜测只有:server-child在client fd关闭后还在不断得向client fd写数据,从而导致server-parent不断返回错误。

总结一下:我认为这首先应该不是nginx的bug,因为nginx的worker process不会fork,这样做也省去了一次EPOLL_CTL_DEL调用;如果在自己的项目中worker在运行过程中(而非初始化时)必须要fork&exec,应该将不希望child继承的fd设置close-on-exec,当然这需要很小心且细心的处理。还存在一种情况就是worker在运行过程中只fork不exec,这种情况我认为就应该避免了。
另外,在多线程程序中调用fork不是很安全,可以参考的资料:
1.https://blog.kghost.info/2013/04/27/fork-multi-thread/
2.http://blog.codingnow.com/2011/01/fork_multi_thread.html

2014-11-15补:
今天阅读libuv的代码,发现libuv的作者针对这个问题做了EPOLL_CTL_DEL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void uv__platform_invalidate_fd(uv_loop_t* loop, int fd) {
struct uv__epoll_event* events;
struct uv__epoll_event dummy;
uintptr_t i;
uintptr_t nfds;
assert(loop->watchers != NULL);
events = (struct uv__epoll_event*) loop->watchers[loop->nwatchers];
nfds = (uintptr_t) loop->watchers[loop->nwatchers + 1];
if (events != NULL)
/* Invalidate events with same file descriptor */
for (i = 0; i < nfds; i++)
if ((int) events[i].data == fd)
events[i].data = -1;
/* Remove the file descriptor from the epoll.
* This avoids a problem where the same file description remains open
* in another process, causing repeated junk epoll events.
*
* We pass in a dummy epoll_event, to work around a bug in old kernels.
*/
if (loop->backend_fd >= 0)
uv__epoll_ctl(loop->backend_fd, UV__EPOLL_CTL_DEL, fd, &dummy);
}

三星G7106 ROM制作

首先声明一下我的所有操作都是在Ubuntu下进行的。

首先你得有个官方ROM。(我下的是G7106ZNUANB3_G7106CHUANB3_CHU.zip)
提取出system.img.ext4,并挂载到ubuntu中的某个目录;

删除自带的apk(在app目录),我删除的有:
116114_MS01_V4.0.apk DioDict3B Kies.odex QQInterceptService.apk SinaStockPhone2013.apk
BaiduMap_V5.0.apk DioDict3_for_Android_Phone_Samsung_Chn.apk Netease.apk QQInterceptUpdate.apk WeChat_V5.0.apk
BaiduNetdisk.apk Flipboard_V2.0.apk NetworkLocation_Baidu.apk QQ_V2013.apk WoPortal_MS01_V1.0.apk
ChatON_feature.apk GroupPlay_25.apk PolarisViewer5.apk Sinamicroblog_V3.0.apk WoStore_MS01_V3.0.apk
DaoDao_Stub.apk Kies.apk QQBrowser_V4.0.apk SinaNewsPhone2013.apk YellowPage.apk

安装Google Play:
先下载gapps-jb-20130813-signed.zip

root:
由于Android 4.3版本的内核启用了SELinux,所以之前的root方法不再适用了。
以前的方法:自己写一个su,直接调用setuid(0)和setgid(0),并把该su属性改成rwsr-sr-x。
现在的方法(即Supersu的方法):用一个有root权限的进程启动daemonsu,用户调用su时,会和已启动的daemonsu进程通信,把需要执行的命令发给daemonsu,让daemonsu来执行。
具体步骤是:
下载最新版本的Supersu.apk,用7z打开,提取出assets目录中的supersu.arm.png,并将supersu.arm.png改名为su;
将su放到xbin目录,打开etc/init.qcom.post_boot.sh,在最后添加一行:/system/xbin/daemonsu –auto-daemon &
将Supersu.apk放到app目录
然后按着这个配置xbin目录:

1
2
3
4
5
6
7
root@ubuntu:/mnt/loop/xbin# ll
total 180
drwxr-xr-x. 2 root 2000 4096 Apr 4 16:08 ./
drwxr-xr-x. 19 root root 4096 Apr 4 13:13 ../
lrwxrwxrwx 1 root root 2 Apr 4 16:08 daemonsu -> su*
-rwxr-xr-x. 1 root 2000 59748 Feb 14 12:37 dexdump*
-rwsr-sr-x. 1 root root 113036 Apr 4 16:05 su*

最后我写了个脚本重新打包system.img.ext4:

1
2
3
4
5
DST_DIR=/mnt/hgfs/g7106/G7106ZNUANB3_G7106CHUANB3_CHU/G7106ZNUANB3_G7106CHUANB3_G7106ZNUANB3_HOME
DST_FILE=system.img.ext4
#rm -f $DST_DIR/$DST_FILE
./make_ext4fs -s -l 1500M -a system $DST_DIR/$DST_FILE /mnt/loop
cd $DST_DIR && tar -cf system.tar $DST_FILE

下面就可以用odin将system.tar刷进去了,选PDA那个就好了,只刷系统目录。

刷进去后,可以再更新一下官方的更新文件:点关于设备中的系统更新。
我更新过后,基带版本是G7106ZNUANC1,内部版本号是JLS36C.G7106ZNUANB3

这个是我做的ROM:http://pan.baidu.com/s/1kT4vKuN

有问题,或不明白的地方,或对我的ROM有反馈建议/意见,请在下面留言,谢谢。

 

说明:

1.Kies.odex, Kies.apk, NetworkLocation_Baidu.apk还是别删了吧,不然会有问题;

《丰田栽了的原因》总结

原文链接:http://www.amobbs.com/thread-5557598-1-1.html

 

读完后有几点需要学习一下,记录下来(貌似找不到啥书讲这些哦。。):

1.函数的代码复杂度不能超过30;

2.丰田的软件包含了超过一万一千个全局变量;

3.不得使用递归;

4.对关键变量缺乏保护:镜像法,EDAC

5.栈空间地址区域远离关键数据地址,避免栈溢出带来的危险,此文指的是操作系统任务分配表;

6.用特殊的监视Task来监视别的Task是否正常

7.不要使用硬件时钟中断喂狗

 

最关键的:基础设计

 

对Barr的采访:

我:终极问题,你开什么车?
Barr:我不开丰田。接触该案以来我没买过新车。老实说我现在非常害怕买新车。我倒是问过一个与车企斗争了三十多年的职业状棍同样的问题,他开宝马。

基于pjmedia的音视频对讲(二)

最后又搞了两天,终于把这个对讲的模块搞的稳定了些。
继续丢包的问题。我在家调试的时候发现是防火墙的问题,第二天就兴冲冲的来公司告诉了测试,然后测试说:我的电脑防火墙一直是关的啊,然后我就傻眼了,God。。试了一下,问题果然还存在,得,继续吧您嘞。。
又把select模块审查了一遍,还是没发现什么问题。在一个偶然的机会,我下了一个断点,程序就停的时间长了一些,然后我发现丢包丢的更严重了,突然我就明白了,接收缓冲区溢出了。立马把接收缓冲区设成了32MB,运行,OK了。发给测试,也OK了。这么简单的一个问题,哎,说到底还是经验问题,以后要多接触网络方面的东西,多积累经验。
后来测试又报了一个状态会乱的bug,就是当一方peer意外重启后,另一方peer的状态就不对了。因为我当时本着怎么简单怎么来的态度写的,所以才导致这个bug。如果用SIP做信令控制,当然不会有这个问题,但现在不是没用么。解决方法就是在SDP包的owner行加入了发送者的包类型:inviter和invited,方法比较简陋,就不细说了。
总体来看pjmedia,个人感觉这个项目还不是十分成熟,但是他提供的功能实在是太符合我当前的需求了,再加上boss的意愿,就选了他。除了pjmedia,gstreamer应该是个相当不错的选择,但是官方没有dshow的plugin,ugly的也木有。所以以后还是多读一些,用一些开源代码,加深一下了解,这样到实际项目的时候才能游刃有余。
ps:通过这个小模块,加深了对DirectShow,COM,Windows IOCP等的一些了解,还算有点收获吧。然后google了几把COM,DCOM,DCE,CORBA的东东,感觉M$的产品设计的还是不错的。知识无国界,也应该无操作系统之分,看来我需要降低一些对Linux的狂热,静心多学学那些计算机基础的知识了。

关于WebRTC

这两天闲来无事,从前天开始就看起了WebRTC。
以前boss说要让我搞一个P2P穿透的库出来,因为对libjingle了解一点,知道他完整地实现了ICE,而且是Google做的,所以就从这个入手了。结果几天下来,是云里雾里啊。C++中的那些各种继承,各种指针,各种类,哎妈,不说了。不过通过读官方文档,还是了解了一些概念,也感觉设计的挺不错的,记忆尤深的就是传输数据时他会动态切换速度最快的candidate。可最后还是放弃了,代码太难读懂了,也许是我历练的少吧。
直到遇到了libnice,才知道什么是简洁。可是libnice只有ICE,考虑到以后很可能要和媒体数据传输相结合,于是选择了pjsip,一个设计良好,跨N多平台,小巧简练的库,我把他里面的pjnath子库封装了一下,就算交了差。
这两天查到说WebRTC是要朝着集成到浏览器中的目的去的,而且已经被chrome,firefox和opera支持了,说已经有数十亿设备支持等等。就看了一下,发现大部分代码就是重用libjingle的,然后定义了一套对外统一的接口,有js的,HTML5的和native API。libjingle已经不单独维护了,SVN的log里记录,大概2013年8月就停止更新了,最后一条写着“has been moved to WebRTC”。
checkout和build就废了我好大的劲,幸亏以前弄过libjingle,工具都装过,具体步骤就不写了,能搜到的,但需要注意的是最新的webRTC在Windows上已经不支持vs2008及以前的版本了,因此我又装了个vs2013 express,但又没有ATL和MFC的库,MFC无所谓,但webRTC包含了altbase.h等头文件,所以直接指向vs2008的ATL对应目录即可。这样应该就没什么问题了。
一上来当然是run一下example,找了找,正好有个peerconnectionclient和peerconnectionserver,试了下,有几次直接崩溃,但是还是能运行的,比较严重的问题是在我的笔记本上运行3分钟左右会卡死,简单瞅了一下,应该是一个关键段死锁了。
这咋用,bug懒得改了,还是去看看以前搞的libjingle吧,看看能不能用。试了下,还真能用,没什么大问题,运行挺稳定,唯一缺点就是太耗CPU,50%被他占去了(我用pjmedia搞的对讲模块,平均占用CPU 8%)。看了下client的代码,采集用DirectShow,显示用的是GDI。但我看到他的代码库中有video render模块,而且支持好几个平台,Windows下是用的Direct3D9,于是尝试用D3D实现显示。
又看了一大堆类的代码,然后写了点代码,到最后发现对接的时候一个表示一帧I420数据的类竟然有2个实现!分别是在webrtc命名空间和cricket命名空间里,一个叫VideoFrame,一个叫I420VideoFrame,哎,懒得转了,直接构造一个空I420VideoFrame丢给了renderer最后给了D3D,粉色图像最后还是出来了,先这样吧。估计我也不会用它。
目前看来WebRTC还是很不成熟的,文档太少,而且个人感觉太乱,可能是有历史包袱的原因吧。优化可能还不够,我也看到Google已经针对SSE优化过libvpx了,但是还是会占这么多CPU。而且我不明白为什么要针对一帧420数据实现2个类!(这俩类的功能还都挺全的,也就是说挺复杂的,也就是说代码挺多的!呃,我不喜欢这么多的高耦合代码)

基于pjmedia的音视频对讲(一)

这两天在搞音视频对讲,boss说不用搞太复杂,服务器不需要,输入对方IP直接连接通话即可,于是在我的建议下,选用了我之前做SIP应用用到的一个开源SIP库pjsip中的一个子库pjmedia。
我的思路是:信令直接用SDP包,不用SIP了。不用SIP的原因是我觉得现在的需求还用不到:需要连接时直接和对方negotiate SDP,成功后即可以采集–编码–RTP打包发送了,简单明了,需要断开时只要向对方发一个BYE的RTCP包即可;其实最重要的原因还是用SIP需要花额外的时间去读pjsip的代码,而鄙人向来收到的任务要求都是要快!总之,还是要听领导的吧,呵呵。。不过说到以后管理自己的团队时,我的想法还是要认真衡量,仔细调研的,一旦确定了,就要深入的研究自己选择的东西,仔细设计一下,以便于融入到团队的基础库中,避免以后重造轮子。
一边照着pjsua这个示例,一边参考着官方文档,搞了3天左右,自发自收可以了。但放到2台机器上试的时候,发现videodev中的dshow模块写的有问题,会失败。于是又开始硬着头皮研究dshow,哦天啊,Windows。pjsip调用的都是dshow的c接口,但是我查到的一些资料和示例都是C++写的,我也觉得既然是基于COM的东西,那就直接用C++呗,尤其是那代码写着多舒服,那CComPtr多好用,干嘛非得追求纯C实现的那个噱头,模块化嘛,还是尽量用最贴近实际操作系统的方式实现比较好。说到这,又想起了Google做的那个gyp,赞一个!
把dhsow模块自己重写了一遍,发现dshow其实不是那么恶心的,思路还是挺清晰的。这样双机通信就可以进行了,跟boss反馈了一下,就让测试人员介入了。反馈的第一个问题就是回音消除。因为我自己测试的时候只能台式机连笔记本,而台式机没有音响,所以根本就忘了加这个功能。还好pjmedia支持回音消除,选用的音频codec是speex,它本身也支持,所以查了下pjmedia的文档,在创建audio port的时候传入的option或上echo cancellation的选项就可以了,参数一律默认值。编译完了就发给了测试,试了下,效果还是有的,但有时候某一边还是会听到回音,只是不会像以前那样无限循环了。而且在连接刚建立的时候,如果有人说话,对方会听到特别刺耳的兹兹声。但是鉴于还有其他更重要的问题,而且说第二天可以拿有源MIC试下,音频就暂时先这样了。
视频的一个比较严重的问题是:连接上视频非常模糊,要等大约15秒刷新后才清晰。一开始我是完全没思路,想了一个土方法:掐着表算了下时间得出了15秒这个数值,然后在全工程里搜索15这个字符串,汗。。。(话说我挺喜欢全工程搜索这个功能的)结果是没有线索。得嘞,继续读pjmedia吧。就这样搞到了下午7点多,方才有了点发现。pjmedia的设计是这样的:一个video port会起一个线程,这个线程的流程就是从video dev取得原始数据,然后用codec编码,然后通过transport发出去;而打开dshow时我会注册一个回调函数,有视频数据进来时函数会被调用,所以这是另一个线程。所以这个问题就是一个多线程同步的问题:采集线程还没来得及将数据写入共享buffer,编码线程就从buffer取走数据去编码发送了,所以他取得的数据都是0xCC(搞Windows的应该都懂的吧),然后第一个关键帧就这样产生了,其后的预测帧都是在这张全粉色图像上算出来的,所以会花屏,直到第二个关键帧的诞生。这应该是一个bug,稍后会向pjsip项目组提交。
解决了这一个,又来了一个问题就是:刚连上时会马上刷一下屏,然后会变清晰。虽说不是大碍,但还是感觉不舒服,尤其是在测设组同学的敦促下,查吧。开始怀疑是丢包,但是局域网,不至于吧,抓了一下包,证明了不是丢包导致的(交换机:冤枉你了,呜呜)。然后加了些log,奇怪的发现还是丢包,因为发送方发出去的某seq的包没收到!呃,交换机,有wireshark作证,我不会怀疑你了,放心。。在翻wireshark抓到的包的时候,发现SDP的包有点问题:我想连的明明是192.168.0.119,但对方回给我的SDP中写的却是192.0.0.119。然后想起了我们公司好多人一台机器要设好几个IP。查了下测试同学的电脑,果然是这样。因此我猜测是不是因为最开始的时候发错了地址导致的丢包。虽然我不明白这样到底会不会导致丢,但是问题还得解决啊,于是只能把多余IP删了,每台机器只剩1个,再试!呃,还是原样。这时时间不早了,而且第二天就放假了,测试同学就走了,我继续查,查了1个小时,也没啥发现,就也回家了。
周末起床后,没事了,又想起了那个问题,继续。我又在pjlib的更底层加了些日志,顺便也拜读了下底层的代码,最后证明了:pjlib写的是没问题的,RTP包解析也是没问题的,问题出在防火墙上!比如A电脑开了防火墙,B没开,如果A在发数据之前就收到了B的数据,那么这时A的防火墙是不放行的,直到A从同一端口向B发数据为止,这就是Win 7防火墙的策略。这样的话,在公司的那台XP的电脑没这个问题也就得到解释了。赶紧把防火墙全部禁掉,问题随之而解决。
顺便也查了下程序中如何设置防火墙,嘿,资料还真不多,绝大多数都是教你鼠标操作的。终于找到一个,链接:http://msdn.microsoft.com/en-us/library/aa364726.aspx
另外的一个问题:长时间运行,某一方可能就会收不到视频数据了。之前的两次都是在公司跑出来的,晚上走之前开开,第二天去了一看就卡那了,远程附加上去瞅了一下,发现dshow的回调还是正常的,应该是编码线程卡住了,这时,测试同学把他的电脑网线拔了,然后换了个地方,然后就没然后了。今天在家跑了一会,也出来了,附加上去,发现是卡在ffmpeg的avcodec_encode_video2里了,我直接用的dev版的ffmpeg,所以只能到这,Google无果,只能明天再查了。
老婆明天要回家了,算了下,要2个月之后才能再见到,呃,写完这篇还是去陪陪老婆吧。。