如何用海思HI3516DV300/3518EV200推流H.264

一开始尝试用hi3518EV200, 后来为了给自己加难度, 就买了个hi3516的板子, 看看差异多大, 结论是, 不大。。。
如何用海思HI3516DV300/3518EV200推流H.264_第1张图片

前期准备: 你得有一个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就是可执行文件
值得注意的就是

  1. 获取摄像头数据的部分请参考sample的vi部分
  2. 获取数据编码的部分参考venc的部分
  3. 拿到数据之后, 就是在venc的原本写入文件的部分, 改为将数据丢到一个链表里面进行缓存, 链表也非常简单, FIFO的小链表
  4. 然后用另一个进程取出链表中的头部数据, 一旦tcp连接建立, 就用write发送数据即可
  5. 理论上, APP跑起来的时候, 第一时间会去连接公网上的TCP服务器, 所以这部分出现延迟的可能性不大, 链表缓存中的数据最多是因为海思到公网这部分连接的网络延时, 不过值得观察一下缓存的大小.
  6. 分辨率应该就是调低延迟的一个主方向, 毕竟码流小了, 速度可能会更快点儿. 目前是1920*1080

接下来在公网的服务器上, 跑一个转流的程序, 因为海思跟手机都没有公网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

遗留的问题:

  1. 延迟高达300ms
  2. 分辨率太高
  3. 解码的部分可以直接丢数据包么?
  4. 安卓端APP会频繁的随机崩溃.
  5. 有人说webrtc公网150ms, 真的假的? 还有比我这样直来直去的方式更快的???

你可能感兴趣的:(视频编解码)