一开始尝试用hi3518EV200, 后来为了给自己加难度, 就买了个hi3516的板子, 看看差异多大, 结论是, 不大。。。
前期准备: 你得有一个3516或者3518的开发板(淘宝有卖各种开发板, 推荐一下易百纳, 但是整个板子没有支架, 摄像头都不能支楞起来, 怎么玩? 我就打印了一个非常潦草的支架, 如上图), 有对应的sdk, 有个安卓手机, 电脑上装了虚拟ubuntu14/16, 用于编译海思的应用, 安装Android Studio, 用于编译安卓APP.
然后有最起码的海思交叉编译的知识, 安卓APP的编译安装基础知识.
数据从海思板子上的GC2053摄像头, 从MIPI接口读出来>>>>>编码成H.264>>>>>>TCP发送到公网服务器>>>>>>服务器上的端口转发数据<<<<<<安卓端APP连接到公网<<<<<<<读到数据后解码播放.
不会画图, 将就看吧.
先说说目标, 简单来说, 就是为了我的4G小车的视频推流准备的,小车这边使用海思或者RK采集视像头信号,使用h.264压缩后,通过WiFi接4G的随身路由器推流到公网的固定ip的服务器, 然后再用手机连接我服务器的公网IP, 由公网上的服务器做一个中转, 手机上使用mediacodec+jni的方式解码。
一步步来, 首先, 海思上面跑一个截图摄像头信号, 参考那个著名的venc的sample程序, 并把数据通过TCP连接把数据帧发出去.
海思这部分技能三年前点花了4000块买了朱老师的海思教程才算入门。
海思部分的代码我放到了github, 把它复制到sdk的sample目录下面, 用make编译就好了
https://github.com/MontaukLaw/hi3516_venc_tcp.git
编译好的文件在smp目录下面, mipi_venc就是可执行文件
值得注意的就是
接下来在公网的服务器上, 跑一个转流的程序, 因为海思跟手机都没有公网ip, 拜谁所赐呢?不敢细想。
海思的数据, 通过read海思连接的socketFd读取出来,再马上write到手机的socketFd上去,这部分还挺简单的, 不过也参考了b站上的黑马的linux网络编程的教程, 那位老师叫啥不知道, 但是真的讲得特别好。
重点是通过select管理多连接 。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_TCP_PORT 54322 // tcp连接的端口
#define BUFF_SIZE 1024*10 // 缓存大小
// #define BUFF_SIZE 1400
int main(int argc, char *argv[]) {
int i, j, n, nready;
int nullFd = 0; // 没有接收者连接的时候, 数据被丢弃
int maxFd = 0; //
int revFd = 0; // 接收者的fd
int listenFd, connFd;
int senderFd = 0;
int ret;
struct timeval tv;
long secNow = 0;
long byteRate = 0;
long totalSent = 0;
nullFd = open("/dev/null", O_WRONLY);
printf("null fd:%d\n", nullFd);
char buf[BUFF_SIZE];
struct sockaddr_in clientAddr, serverAddr;
socklen_t clientAddrLen;
listenFd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(SERVER_TCP_PORT);
ret = bind(listenFd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));
listen(listenFd, 128);
fd_set rset, allset;
maxFd = listenFd;
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
while (1) {
rset = allset;
nready = select(maxFd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
printf("select error\n");
continue;
}
if (FD_ISSET(listenFd, &rset)) {
clientAddrLen = sizeof(clientAddr);
connFd = accept(listenFd, (struct sockaddr *) &clientAddr, &clientAddrLen);
if (connFd < 0) {
printf("accept error\n");
continue;
}
if (senderFd == 0) {
senderFd = connFd;
printf("sender connected first, fd is %d\n", senderFd);
} else {
revFd = connFd;
printf("revFd connected, fd is %d\n", revFd);
}
FD_SET(connFd, &allset);
if (maxFd < connFd) {
maxFd = connFd;
printf("maxFd:%d\n", maxFd);
}
if (0 == --nready) {
continue;
}
}
for (i = listenFd + 1; i <= maxFd; i++) {
if (FD_ISSET(i, &rset)) {
if ((n = read(i, buf, sizeof(buf))) == 0) {
if (i == revFd) {
revFd = 0;
} else if (i == senderFd) {
senderFd = 0;
}
printf("%d disconnected \n", i);
close(i);
FD_CLR(i, &allset);
} else if (n > 0) {
// int writeFd = get_recv_fd(i, maxFd);
// printf("data from fd:%d write to %d\n", i, writeFd);
// write(writeFd, buf, n);
if (revFd != 0) {
totalSent += n;
// printf("sending :%ld to rev\n", totalSent);
byteRate += n;
gettimeofday(&tv, NULL);
if (tv.tv_sec != secNow) {
printf("total sent %ld br: %ld\n", totalSent, byteRate);
// printf("Seconds since Jan. 1, 1970: %ld\n", tv.tv_sec);
secNow = tv.tv_sec;
byteRate = 0;
}
write(revFd, buf, n);
} else {
printf("sending to null\n");
write(nullFd, buf, n);
}
}
}
}
}
close(nullFd);
}
意思是如果没有接收端进行连接, 就写到/dev/null里面去, 就是扔掉了, 如果有连接, 就往连接的句柄当中写.
这里可以调试的就是接收发送的包大小, 调低延迟的一个观察方向.
之前是安卓端使用c语言的tcp接收,通过jni往上层传递, 其实是不是可以直接用java的socket?我干嘛要用jni呢???可能是花了5000块学的享学的安卓课程里面教我用ffmpeg解码,但是后来没用上。。。
但是用c连接TCP这个我可以啊, 就直接用jni了, 一开始用udp接收的时候, 一点问题都没有, 在内网延迟非常低.
后来改成Java的Socket来接收, 折腾了最少三天, 其实就是因为Java的那些BufferedReader读取的是char流,而不是byte流, 我又把char做了错误的强转, 导致无论如何出来的图像都不对, 解码器之前报错, 保存成文件, 雷神的SpecialVH264, 居然还能认出sps, pps帧, Elecard StreamEye Tools直接就死在当场, 后来在各端观察裸数据才发现,问题就在Java的转码上…
所以接数据的部分, 我直接就用了Java的InputStream, 它是可以直接read到byte数组的.
然后做分包, 确切的说是分帧, 因为mediacodec的input数据是一帧帧的, 话又说回来, 这部分我没仔细测试, 但是应该是这样, 如果可以直接丢数据包进去, 那可能还省去了很多功夫.
总之, 就是用Java找出数据中的sps, pps, sei, I帧跟P帧, 然后放入解码器, 再在output中对videoview进行渲染.
完整代码如下:
https://github.com/MontaukLaw/tcp_h254_decode_android.git
遗留的问题: