一开始尝试用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就是可执行文件
值得注意的就是
- 获取摄像头数据的部分请参考sample的vi部分
- 获取数据编码的部分参考venc的部分
- 拿到数据之后, 就是在venc的原本写入文件的部分, 改为将数据丢到一个链表里面进行缓存, 链表也非常简单, FIFO的小链表
- 然后用另一个进程取出链表中的头部数据, 一旦tcp连接建立, 就用write发送数据即可
- 理论上, APP跑起来的时候, 第一时间会去连接公网上的TCP服务器, 所以这部分出现延迟的可能性不大, 链表缓存中的数据最多是因为海思到公网这部分连接的网络延时, 不过值得观察一下缓存的大小.
- 分辨率应该就是调低延迟的一个主方向, 毕竟码流小了, 速度可能会更快点儿. 目前是1920*1080
接下来在公网的服务器上, 跑一个转流的程序, 因为海思跟手机都没有公网ip, 拜谁所赐呢?不敢细想。
海思的数据, 通过read海思连接的socketFd读取出来,再马上write到手机的socketFd上去,这部分还挺简单的, 不过也参考了b站上的黑马的linux网络编程的教程, 那位老师叫啥不知道, 但是真的讲得特别好。
重点是通过select管理多连接 。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <printf.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#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
遗留的问题:
- 延迟高达300ms
- 分辨率太高
- 解码的部分可以直接丢数据包么?
- 安卓端APP会频繁的随机崩溃.
- 有人说webrtc公网150ms, 真的假的? 还有比我这样直来直去的方式更快的???