DVR行车记录仪与APP的直播,开发过程难点记录

最近做一个APP与行车记录仪的直播功能。
     C层同事说没有后台服务器支撑,所以我之前编译好的ijkplayer的so库也就没用了。

     与C层同事沟通如下:

           1、JNI调用这块我自己写(不是很熟悉....呜呜)

           2、根据C层的结构体,我Java这边写出对应的实体类bean

           目前大致是这样的。

问题点:

     1、在JNI层进行回调实时流数据到Java层,代码如下:

 //获取流数据,里面的OnGetFrameListener 接口是需要传到JNI层进行回调的
    public native void getFrame(OnGetFrameListener frameListener);


// 这个是回调接口
 public interface OnGetFrameListener {
        public  void getFrameData(FrameDataBean dataBean);

    }

下面是JNI代码:

//获取要回调的方法ID
    getFrameMethodId = (*env).GetMethodID(javaClass,
                                          "方法名",
                                          "方法的相关签名");

// 这个是需要回调的方法中的参数
jclass class_temp = (*env).FindClass("包名/类名");

// 创建一下jni线程,用于处理循环获取实时数据流
pthread_t pt;

// 获取参数对象(object)中的每个参数
env->GetFieldID()

// 给每个参数赋值,根据参数类型的不同选择不同的SetXXField()方法
env->SetObjectField()

在这里要注意:如果是实时的传递大数据,比如大型的数组,我们应该这么处理:

// 创建一个jobject,对应的就是 ByteBuffer
jobject buffer = 0;
buffer = env->NewDirectByteBuffer();
(*env).SetObjectField(X, X, buffer);

这个很关键,浪费我2天时间

这样的话就获取到实时数据流了。
2、播放实时数据流(我这里C层给的是一帧一帧的char[],java可以转为byte[])

首先,我们得要问一下C层同事:给的帧数据中有没有包含帧头部,这与我们之后的解码相关,我这里是没有帧头部的。

这里只给出方法名:

// 参数frame是我们需要处理的帧数据,offset正常没有偏移设为0,length是我们的数组长度(这些都是没有帧头部的情况;假如有帧头部的话,offset就是帧头部的长度,length就是数组长度-帧头部长度)
private void onFrame(byte[] frame, int offset, int length)

可以参考这个链接:https://blog.csdn.net/qq_36467463/article/details/77977562

3、假如是播放有服务器的直播或者回放的话

这个我们可以自己编译ijkplayer,这个有FFmpeg内核处理,需要支持h264  aac  h265  http  https都可以在相关的ffmpeg文件中进行修改,我自己编译好的so库就不给大家了,毕竟需求不一样。

4、在JNI中开启线程,从C层循环获取数据【一帧h264数据】后,用mediacode解码导致画面延迟、马赛克

在jni中开启线程循环获取数据(线程不休眠或者休眠时间很短暂,此【usleep】方法),这时候mediacode解码会报错:MediaCodec.native_dequeueInputBuffer    java.lang.IllegalStateException;这样的情况会导致完全展示不了帧画面或者延迟、马赛克。

解决办法:但是经过调试,在jni的线程while循环中usleep 10毫秒的话,可以正常解码,正常播放直播。

原理以及问题的产生点:如果不做线程休眠会导致硬解码这边的缓冲区溢出。

                                                                                                                                                                          

5、处理Android10系统连接WiFi导致socket在不同网段无法通讯的问题

处理:WiFi连接方式改用之前的连接方法,

wifiManager.disableNetwork(wifiManager.getConnectionInfo().getNetworkId());
        int netId = wifiManager.addNetwork(getWifiConfig(ssid, capabilities, pws));
        boolean isConnected = wifiManager.enableNetwork(netId, true);

Android10 的WiFi连接方式为,

private void connectWifiVersionCodeQ(String ssid, String pws, final ConnectWifi connectWifi) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            NetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
                    .setSsidPattern(new PatternMatcher(ssid, PatternMatcher.PATTERN_PREFIX))
                    .setWpa2Passphrase(pws)
                    .build();

            NetworkRequest request = new NetworkRequest.Builder()
                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                    .setNetworkSpecifier(specifier)
                    .build();

            connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

            networkCallback = new ConnectivityManager.NetworkCallback() {
                @Override
                public void onAvailable(@NonNull Network network) {
                    try {
                        network.bindSocket(TcpTestClass.TCP_TEST_CLASS.getSocket());
                    } catch (IOException e) {
                        e.printStackTrace();
                        LogUtil.LOG_UTIL.e(this, "e: " + e);
                    }
                    connectWifi.connectSuccess();
                }

                @Override
                public void onUnavailable() {
                    connectWifi.connectFail();
                }
            };

            connectivityManager.requestNetwork(request, networkCallback);
        }
    }

当然假如你对手机有root权限,在手机的路由表添加IP地址也可以,但是Android这块在底层已经写死,也不建议这么做。

6、使用mediacode解码AAC帧数据,有的可以解码,有的不能解码的问题:

背景:项目播放的AAC是终端设备那边传送过来的实时流帧数据,采用mediacode进行硬解码

问题点:同一样的代码,对于不同的AAC文件,有的可以解码,有的不能解码。例如:

 MediaFormat mediaFormat = new MediaFormat();
            //数据类型
            mediaFormat.setString(MediaFormat.KEY_MIME, mine);
            //声道个数
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, KEY_CHANNEL_COUNT);
            //采样率
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, KEY_SAMPLE_RATE);
            //比特率
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 95435);
            //用来标记AAC是否有adts头,1->有
            mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);
            //用来标记aac的类型
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            
            byte[] data = new byte[]{(byte) 0x11, (byte) 0x90};
            ByteBuffer csd_0 = ByteBuffer.wrap(data);
            mediaFormat.setByteBuffer("csd-0", csd_0);
            //解码器配置
            mDecoder.configure(mediaFormat, null, null, 0);

这样设置的MediaFormat对应的是这样的AAC:

DVR行车记录仪与APP的直播,开发过程难点记录_第1张图片

箭头标注的是AudioTrack的构造函数的参数。

下面是另外一个AAC文件格式,项目中用到的AAC格式:

MediaFormat mediaFormat = new MediaFormat();
            //数据类型
            mediaFormat.setString(MediaFormat.KEY_MIME, mine);
            //声道个数
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, KEY_CHANNEL_COUNT);
            //采样率
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, KEY_SAMPLE_RATE);
            //比特率
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 15678);
            //用来标记AAC是否有adts头,1->有
            mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);
            //用来标记aac的类型
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            //ADT头的解码信息
            byte[] data = new byte[]{(byte) 0x15, (byte) 0x88};
            ByteBuffer csd_0 = ByteBuffer.wrap(data);
            mediaFormat.setByteBuffer("csd-0", csd_0);
            //解码器配置
            mDecoder.configure(mediaFormat, null, null, 0);

DVR行车记录仪与APP的直播,开发过程难点记录_第2张图片

两者的能否解码成功,主要还是看  mediaFormat.setByteBuffer("csd-0", csd_0),ADT头的解码信息这块。具体说明可以参考下面这个博客:https://blog.csdn.net/chailongger/article/details/84378721。

一开始用同样的代码,解码失败,各种查看AAC信息,设置采样率、声道等等都解码失败,最后发现关键点还是mediaFormat.setByteBuffer("csd-0", csd_0),ADT头的解码信息这块。还是对于这块不熟啊!!!!!!

7、使用MediaMuxer合成实时流的h264+aac帧数据封装成MP4问题点

首先对于这个不熟悉,看文档以及网上各种搜,大部分都是合成文件形式的demon,这个大家可以去查一下,网上很多的;但是我这项目是实时流,所以这又是一个坑,哈哈哈............

我接下来要讲的内容,希望大家先看一下这篇文章:https://blog.csdn.net/stn_lcd/article/details/72625954

大致流程就是

new MediaMuxer()--->mediaMuxer.addTrack()--->mediaMuxer.start()--->mediaMuxer.writeSampleData()

1、mediaMuxer.addTrack(MediaFormat format)

一、对于h264的format我们最重要的是设置pps和PSP,

mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(csd0));
mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(csd1));

根据h264的PSP、pps规则,0x67之后表示的为PSP,0x68之后的为pps,他们中间以0x00, 0x00, 0x00, 0x01分隔,一般这个都不会变的,我是用notepad++的HEX 16进制插件查看的h264的PSP、pps。

二、对于AAC的format我们则需要设置

mediaFormat.setByteBuffer("csd-0", csd_0),adt头的编码信息?我AAC解码的adt头的解码信息和在这的adt头信息不一样,得要根据声道数、采样率算出对应的16进制

2、MediaCodec.BufferInfo作为mediaMuxer.writeSampleData()中参数,

videoBufferInfo.size = videoByteBuffer.limit();
                videoBufferInfo.offset = 0;
            videoBufferInfo.flags = type;
            videoBufferInfo.presentationTimeUs += 1000 * 1000 / VIDEO_FRAME_RATE;

h264:size、offset不用说了,flags标志的为是否关键帧,我这C层会回调给我I帧是时候,flags=1,其他flags=0,videoBufferInfo.presentationTimeUs表示h264的时间戳(单位微秒),所以我这是1000*1000/帧率之后自增

AAC:size、offset不用说了,flags标志的为是否关键帧,我这都给flags=1了,我用FFmpeg的ffprobe命令查看MP4的AAC信息flags都是关键帧,presentationTimeUs=1024*1000/采样率*1000的自增。

3、什么时候mediaMuxer.addTrack()?

h264、AAC我都是在mDecoder.dequeueOutputBuffer()=INFO_OUTPUT_FORMAT_CHANGED执行的

4、MediaMuxer在调用stop出错的问题:

format没设置正确、MediaCodec.BufferInfo没设置正确、addtrack()方法调用时机不正确等等都会导致

5、合成的视频有马赛克或者花屏?

这个是由于视频帧数据输入的时候不是从I帧关键帧开始的,我们只需要判断实时流帧数据的类型,从I帧开始进行合成就行

你可能感兴趣的:(jni,C/C++,Java,Android,android)