Android之间互相的录屏,相机直播-点对点传输 (增加声音直播)(二)

之前尝试写了一个基于来疯直播的点对点Tcp连接传输H264进行投屏直播的demo,

(1) Android之间互相的录屏直播 –点对点传输(tcp长连接发送h264)(一)
http://blog.csdn.net/baidu_33546245/article/details/78670220
(2)Android之间互相的录屏相机直播-(增加声音直播)(二)
https://blog.csdn.net/baidu_33546245/article/details/80503091

前段时间,有些小伙伴反馈了几个bug和新的需求,趁着最近周末无事,修正了这几个bug,
实现了一些新增需求,有以下几点改动:

  • 1,增加了直播样例代码,使用来疯直播的直播组件,补充了一个成功的Camera直播样例.
  • 2,解决直播快速移动时和使用小米手机情况下,播放端模糊.
  • 3,增加了传输声音道播放端播放

1, 解决直播模糊

鉴于增加直播样例代码没什么好说的,我们一笔带过,直接说如何解决模糊,模糊应该是视频帧不清晰造成的,

首先先介绍一下MediaCodec的bitrate_mode三种模式 :
CQ 对应于 OMX_Video_ControlRateDisable,它表示完全不控制码率,尽最大可能保证图像质量.
CBR 对应于 OMX_Video_ControlRateConstant,它表示编码器会尽量把输出码率控制为设定值.
VBR 对应于 OMX_Video_ControlRateVariable,它表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;

所以,我们可以检查手机型号,只要是小米手机的话,就直接使用CQ模式去初始化MediaCodec的bitrate_mode.
因为CQ模式下,是最高图片质量,为了降低网络带宽,我们可以适当降低一下fps,并且提高关键帧的间隔(ifi),
用以减少局域网下带宽的占用.

//附上MediaCodec初始化的代码:
  public static MediaCodec getVideoMediaCodec(VideoConfiguration videoConfiguration) {
        int videoWidth = getVideoSize(videoConfiguration.width);
        int videoHeight = getVideoSize(videoConfiguration.height);
        if (Build.MANUFACTURER.equalsIgnoreCase("XIAOMI")) {
            videoConfiguration.maxBps = 500;
            videoConfiguration.fps = 10;
            videoConfiguration.ifi = 3;
        }
        MediaFormat format = MediaFormat.createVideoFormat(videoConfiguration.mime, videoWidth, videoHeight);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfiguration.maxBps * 1024);
        int fps = videoConfiguration.fps;
        //设置摄像头预览帧率
        if (BlackListHelper.deviceInFpsBlacklisted()) {
            SopCastLog.d(SopCastConstant.TAG, "Device in fps setting black list, so set mediacodec fps 15");
            fps = 15;
        }
        format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoConfiguration.ifi);

        format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 45);
        //------------------ 为解决MIUI9.5花屏而增加...-------------------------------
        if (Build.MANUFACTURER.equalsIgnoreCase("XIAOMI")) {
            format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
        } else {
            format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
        }
        format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
        MediaCodec mediaCodec = null;

        try {
            mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime);
            mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (Exception e) {
            e.printStackTrace();
            if (mediaCodec != null) {
                mediaCodec.stop();
                mediaCodec.release();
                mediaCodec = null;
            }
        }
        return mediaCodec;
    }

2,声音的采集,发送,接收,播放

声音直播相比较来说复杂:
因为我们的发送和播放视频代码都是传输的H264,H264是传输视频帧的协议.H264协议没有传输声音的片段.
为了快速出一个demo,我最后将声音AAC片段放在h264的头数据后面(0,0,0,1后面).
再收到每一个片段后,在播放端判断是视频帧还是AAC片段,然后将视频帧喂给配置有SurfaceHolder的MediaCodec,
将AAC片段喂给Audio MediaCodec,然后从AudioMediaCodec出来的片段再写入AudioTrack,由AudioTrack去播放声音.

简单来说 发送端流程:
(1)从AudioRecord中拿到pcm,
(2)通过AudioMediaCodec拿到不包括ADTS头的AAC片段
(3)给AAC片段加上ADTS的头信息,再加上h264头后发送{0,0,0,1}(加h264的头信息是我自己定义的,只是为了方便解析)
播放端流程:
(1)根据ADTS识别出AAC片段,去掉,后h264的头信息,
(2)将包括ADTS的AAC片段喂给AudioMediaCodec,获得解码后的片段.
(3)将解码后的片段传递给AudioTrack播放.

首先我们需要连接下AAC和ADTS: 
AAC是高级音频编码(Advanced Audio Coding)的缩写,
ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,
解码可以在这个流中任何位置开始。
每一个AAC片段前面都应该有ADTS,一般第一个片段不包括ADTS的话,只有两个字节,用于配置一些信息.
可以去这位大神的博客了解AAC和ADTS更多信息
这里写图片描述
接下来我们根据自己AudioRecord的配置信息,生成一段ADTS头信息,给每个AAC片段都加上

//获得ADTS头信息
 private byte[] getADTSHeader(int packetLen) {
        byte[] packet = new byte[7];
        int profile = 2;  //AAC LC
        int freqIdx = 4;  //16.0KHz
        int chanCfg = 2;  //CPE 声道数
        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
        return packet;
    }
//给AAC片段添加ADTS
  if (packetListener == null ) {
            return;
        }
        if (!mSendAudio) {
            return;
        }
        bb.position(bi.offset);
        bb.limit(bi.offset + bi.size);
        byte[] audio = new byte[bi.size];
        bb.get(audio);
        int length = 7 + audio.length;
        ByteBuffer tempBb = ByteBuffer.allocate(length + 4);
        //header是自定义逻辑,即为h264的{0,0,0,1}
        tempBb.put(header);
        tempBb.put(getADTSHeader(length));
        tempBb.put(audio);
        packetListener.onPacket(tempBb.array(), AUDIO);

播放端根据头信息,筛选出视频片段或者音频片段

筛分算法主要来自于来疯直播

 private boolean isAudio(byte[] frame) {//是否是音频片段
        if (frame.length < 5) {
            return false;
        }
        return frame[4] == ((byte) 0xFF) && frame[5] == ((byte) 0xF9);
    }

    private boolean isSps(byte[] frame) {//是否是Sps
        if (frame.length < 5) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[4] & 0x1f);
        return nal_unit_type == SPS;
    }

    private boolean isPps(byte[] frame) {//是否是Pps
        if (frame.length < 5) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[4] & 0x1f);
        return nal_unit_type == PPS;
    }

    private boolean isKeyFrame(byte[] frame) {//是否是关键帧
        if (frame.length < 5) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[4] & 0x1f);
        return nal_unit_type == IDR;
    }

*将音频片段解码并播放 

public class AudioPlay {
    private static final String TAG = "AudioPlay";
    private MediaCodec mAudioMediaCodec;
    private AudioTrack mAudioTrack;
    //用来记录解码失败的帧数
    private int count = 0;

    public AudioPlay() {
        this.mAudioMediaCodec = AudioMediaCodec.getAudioMediaCodec();
        this.mAudioTrack = AudioMediaCodec.getAudioTrack();
        this.mAudioMediaCodec.start();
        this.mAudioTrack.play();
    }

    public void playAudio(byte[] buf, int offset, int length) {
        //输入ByteBuffer
        ByteBuffer[] codecInputBuffers = mAudioMediaCodec.getInputBuffers();
        //输出ByteBuffer
        ByteBuffer[] codecOutputBuffers = mAudioMediaCodec.getOutputBuffers();
        //等待时间,0->不等待,-1->一直等待
        long kTimeOutUs = 0;
        try {
            //返回一个包含有效数据的input buffer的index,-1->不存在
            int inputBufIndex = mAudioMediaCodec.dequeueInputBuffer(kTimeOutUs);
            if (inputBufIndex >= 0) {
                //获取当前的ByteBuffer
                ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
                //清空ByteBuffer
                dstBuf.clear();
                //填充数据
                dstBuf.put(buf, offset, length);
                //将指定index的input buffer提交给解码器
                mAudioMediaCodec.queueInputBuffer(inputBufIndex, 0, length, 0, 0);
            }
            //编解码器缓冲区
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            //返回一个output buffer的index,-1->不存在
            int outputBufferIndex = mAudioMediaCodec.dequeueOutputBuffer(info, kTimeOutUs);

            if (outputBufferIndex < 0) {
                //记录解码失败的次数
                count++;
            }

            ByteBuffer outputBuffer;
            while (outputBufferIndex >= 0) {
                switch (outputBufferIndex) {
                    case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                        Log.e(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
                        break;
                    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                        Log.e(TAG, "INFO_OUTPUT_FORMAT_CHANGE");
                        break;
                    case MediaCodec.INFO_TRY_AGAIN_LATER:
                        Log.e(TAG, "INFO_TRY_AGAIN_LATER");
                        break;
                }
                //---------------------------------------------------------------
                //获取解码后的ByteBuffer
                outputBuffer = codecOutputBuffers[outputBufferIndex];
                //用来保存解码后的数据
                byte[] outData = new byte[info.size];
                outputBuffer.get(outData);
                //清空缓存
                outputBuffer.clear();
                //播放解码后的数据
                mAudioTrack.write(outData, 0, info.size);
//                Log.e("DecodeThread", "buff length = " + info.size);
                //释放已经解码的buffer
                mAudioMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                //解码未解完的数据
                outputBufferIndex = mAudioMediaCodec.dequeueOutputBuffer(info, kTimeOutUs);
                //--------------------------------------------------------
            }
        } catch (Exception e) {
            Log.e(TAG, e.toString());
            e.printStackTrace();
        }
    }

    //返回解码失败的次数
    public int getCount() {
        return count;
    }


    public void release() {
        if (mAudioMediaCodec != null) {
            mAudioMediaCodec.stop();
            mAudioMediaCodec.release();
            mAudioMediaCodec = null;
        }
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
    }
}

最后附上github地址:
数据采集,发送端
数据处理,播放端

评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值