基于WiFi的Android局域网视频监控的设计与实现(h264编码、解码,surfaceview、socket)

该文详细阐述了一种方法,通过摄像头捕获视频并进行h264编码,然后利用socket通信在WiFi网络下将编码后的视频数据实时传输到另一端进行解码播放。过程中涉及的关键技术包括Android的MediaCodec库用于编码和解码,以及socket连接确保数据的传输。
摘要由CSDN通过智能技术生成

一、整体思路

1.摄像头实时捕获视频并进行h264编码
2.接收wifi的视频数据并实时在另一个监控视频手机查看,接收到的数据进行h264解码
3.wifi的数据传输利用socket通信

二、摄像头捕获数据并进行h264编码

摄像头捕获数据

打开相机,设置预览画面,设置监听获取视频流的每一帧:

    /**
     * 打开相机
     */
    private void openCamera() {
        camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
        //获取相机参数
        Camera.Parameters parameters = camera.getParameters();
        //获取相机支持的预览的大小
        Camera.Size previewSize = getCameraPreviewSize(parameters);

        int width = previewSize.width;
        int height = previewSize.height;
        Log.d("AAA",""+width+" "+height);
        //设置预览格式(也就是每一帧的视频格式)YUV420下的NV21
        parameters.setPreviewFormat(ImageFormat.NV21);
        //设置预览图像分辨率

        parameters.setPreviewSize(width, height);
        //相机旋转90度
        camera.setDisplayOrientation(90);
        //配置camera参数
        camera.setParameters(parameters);
        try {
            camera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //设置监听获取视频流的每一帧
        camera.setPreviewCallback(new Camera.PreviewCallback() {
            @Override
            public void onPreviewFrame(byte[] data, Camera camera) {
            }
        });
        nv21EncoderH264 = new NV21EncoderH264(width, height);
        nv21EncoderH264.setEncoderListener(this);
        //调用startPreview()用以更新preview的surface
        camera.startPreview();
        camera.autoFocus(null);
    }
                //设置监听获取视频流的每一帧
                camera.setPreviewCallback(new Camera.PreviewCallback() {
                    @Override
                    public void onPreviewFrame(byte[] data, Camera camera) {
                        nv21EncoderH264.encoderH264(data);
                    }
                });

对数据进行h264编码:

public class NV21EncoderH264 {

    private int width, height;
    private int frameRate = 30;
    private MediaCodec mediaCodec;
    private EncoderListener encoderListener;

    public NV21EncoderH264(int width, int height) {
        this.width = width;
        this.height = height;
        initMediaCodec();
    }

    private void initMediaCodec() {
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            //height和width一般都是照相机的height和width。
            //TODO 因为获取到的视频帧数据是逆时针旋转了90度的,所以这里宽高需要对调
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
            //描述平均位速率(以位/秒为单位)的键。 关联的值是一个整数
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
            //描述视频格式的帧速率(以帧/秒为单位)的键。帧率,一般在15至30之内,太小容易造成视频卡顿。
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
            //色彩格式,具体查看相关API,不同设备支持的色彩格式不尽相同
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
            //关键帧间隔时间,单位是秒
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            //开始编码
            mediaCodec.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 将NV21编码成H264
     */
    public void encoderH264(byte[] data) {
        //将NV21编码成NV12
        byte[] bytes = NV21ToNV12(data, width, height);
        //视频顺时针旋转90度
        byte[] nv12 = rotateNV290(bytes, width, height);

        try {
            //拿到输入缓冲区,用于传送数据进行编码
            ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
            //拿到输出缓冲区,用于取到编码后的数据
            ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
            //当输入缓冲区有效时,就是>=0
            if (inputBufferIndex >= 0) {
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                //往输入缓冲区写入数据
                inputBuffer.put(nv12);
                //五个参数,第一个是输入缓冲区的索引,第二个数据是输入缓冲区起始索引,第三个是放入的数据大小,第四个是时间戳,保证递增就是
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, nv12.length, System.nanoTime() / 1000, 0);
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            //拿到输出缓冲区的索引
            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                byte[] outData = new byte[bufferInfo.size];
                outputBuffer.get(outData);

                //outData就是输出的h264数据
                if (encoderListener != null) {
                    encoderListener.h264(outData);
                }

                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    /**
     * 因为从MediaCodec不支持NV21的数据编码,所以需要先讲NV21的数据转码为NV12
     */
    private byte[] NV21ToNV12(byte[] nv21, int width, int height) {
        byte[] nv12 = new byte[width * height * 3 / 2];
        int frameSize = width * height;
        int i, j;
        System.arraycopy(nv21, 0, nv12, 0, frameSize);
        for (i = 0; i < frameSize; i++) {
            nv12[i] = nv21[i];
        }
        for (j = 0; j < frameSize / 2; j += 2) {
            nv12[frameSize + j - 1] = nv21[j + frameSize];
        }
        for (j = 0; j < frameSize / 2; j += 2) {
            nv12[frameSize + j] = nv21[j + frameSize - 1];
        }
        return nv12;
    }

    /**
     * 此处为顺时针旋转旋转90度
     *
     * @param data        旋转前的数据
     * @param imageWidth  旋转前数据的宽
     * @param imageHeight 旋转前数据的高
     * @return 旋转后的数据
     */
    private byte[] rotateNV290(byte[] data, int imageWidth, int imageHeight) {
        byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
        // Rotate the Y luma
        int i = 0;
        for (int x = 0; x < imageWidth; x++) {
            for (int y = imageHeight - 1; y >= 0; y--) {
                yuv[i] = data[y * imageWidth + x];
                i++;
            }
        }
        // Rotate the U and V color components
        i = imageWidth * imageHeight * 3 / 2 - 1;
        for (int x = imageWidth - 1; x > 0; x = x - 2) {
            for (int y = 0; y < imageHeight / 2; y++) {
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
                i--;
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
                i--;
            }
        }
        return yuv;
    }

    /**
     * 设置编码成功后数据回调
     */
    public void setEncoderListener(EncoderListener listener) {
        encoderListener = listener;
    }

    public interface EncoderListener {
        void h264(byte[] data);
    }
}

三、接收到的数据进行h264解码

package com.example.receiver;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

public class H264DeCodePlay {

    private static final String TAG = "zqf-dev";
    //使用android MediaCodec解码
    private MediaCodec mediaCodec;
    private Surface surface;
    MediaCodec.BufferInfo mediaCodecBufferInfo;

    H264DeCodePlay(Surface surface) {

        this.surface = surface;
        initMediaCodec();

    }

    private void initMediaCodec() {
        try {
            //创建解码器 H264的Type为  AAC
            mediaCodec = MediaCodec.createDecoderByType("video/avc");
            //创建配置
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 1080, 1920);
            //设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】

            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            //配置绑定mediaFormat和surface
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mediaCodec.configure(mediaFormat, surface, null, 0);

            mediaCodecBufferInfo = new MediaCodec.BufferInfo();
            mediaCodec.start();
            Log.e(TAG, "创建解码成功");
        } catch (IOException e) {
            e.printStackTrace();
            //创建解码失败
            Log.e(TAG, "创建解码失败");
        }
    }

    /**
     * 解码播放
     */
    void decodePlay() {


    }

    public void decode(byte[] buf) {
        try {
            //获取MediaCodec的输入流
           // ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
            //设置解码等待时间,0为不等待,-1为一直等待,其余为时间单位
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(0);
            //填充数据到输入流
            if (inputBufferIndex >= 0) {
                ByteBuffer inputBuffer;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                    inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
                } else {
                    //兼容安卓5.0以下,如不需要可以删掉
                    inputBuffer = mediaCodec.getInputBuffers()[inputBufferIndex];
                }
               // inputBuffer.put(buf);
                //mediaCodec.queueInputBuffer(inputBufferIndex, 0,buf.length , System.nanoTime(), 0);
                inputBuffer.put(buf, 0, buf.length);
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, buf.length, System.nanoTime() , 0);
            }
            //解码数据到surface,实际项目中最好将以下代码放入另一个线程,不断循环解码以降低延迟
            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(mediaCodecBufferInfo, 0);
            if (outputBufferIndex >= 0) {
                mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //此处可以或得到视频的实际分辨率,用以修正宽高比
                //fixHW();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }


    }


    //读取一帧数据
    private int findByFrame(byte[] bytes, int start, int totalSize) {
        for (int i = start; i < totalSize - 4; i++) {
            //对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
            if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
                return i;
            }
        }
        return -1;
    }

    private byte[] getBytes(String videoPath) throws IOException {
        InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
        int len;
        int size = 1024;
        byte[] buf;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        buf = new byte[size];
        while ((len = is.read(buf, 0, size)) != -1)
            bos.write(buf, 0, len);
        buf = bos.toByteArray();
        return buf;
    }
}

四、wifi的数据传输利用socket通信

  • 由于H264是一种帧间编码,只记录每一帧之间的变化,后期解码的时候只要在上一帧基础上算出变化就可以了。并且我们监听完视频流后就开始对每一帧进行h264编码,如果这时socket还没连接好,接受放就无法收到第一帧数据,从而无法对h264的数据进行解码,造成视频数据丢失,所以我们应该在socket链接后再对视频流进行监听,再进行编码、发送数据。
  • 发送端的socket的连接:
    class SocketConnectThread extends Thread{
        public void run(){
            Log.e("info", "run: ============线程启动" );
            try {
                //等待客户端的连接,Accept会阻塞,直到建立连接,
                //所以需要放在子线程中运行。
                mSocket = mServerSocket.accept();
                 os = new DataOutputStream(mSocket.getOutputStream());
                 mSocket.setSendBufferSize(1000000);
                //设置监听获取视频流的每一帧
                camera.setPreviewCallback(new Camera.PreviewCallback() {
                    @Override
                    public void onPreviewFrame(byte[] data, Camera camera) {
                        nv21EncoderH264.encoderH264(data);
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
            Log.e("info","connect success========================================");

        }
    }
  • 编码成功的回调:
    //编码成功的回调
    @Override
    public void h264(byte[] data) {
        Log.e("TAG1", data.length+ "");
        try {
            send(data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  • 先在子节流中写入一帧数据的字节数组大小,这样接受端就能提前创建好对应大小的字节数组:
    public void send( byte[] data) throws InterruptedException {
        Thread thread=new Thread() {
            @Override
            public void run() {
                try {
                    // socket.getInputStream()
                    if(mSocket!=null&&isSending) {
                        os.writeInt(data.length);
                        os.flush();
                        os.write(data);
                        os.flush();
                       // writer.writeUTF(str); // 写一个UTF-8的信息
                    }
                } catch (IOException e) {
                    socketConnectThread=new SocketConnectThread();
                    socketConnectThread.start();
                    e.printStackTrace();
                }
            }
        };
        executorService.submit(thread);
    }

接收端接受数据并进行解码,在进行测试的时候,由于每次发送的字节数组过大,read()可能没办法每次都能读完,这里我们再加个循环保证接收到的数据是完整的:

            new Thread(){
                @Override
                public void run() {
                    DataInputStream reader= new DataInputStream(mInStream);
                    try {

                        while (true) {
                            // 获取读取流
                            int length=reader.readInt();
                            byte[] bytes =new byte[length];
                            System.out.println("*等待客户端输入*");
                            int index = 0;
                            int len = 0;
                            while(index < length){
                                len = reader.read(bytes,index,length - index);
                                //每次读取完判断数据是否全部读取完毕
                                if(len > 0){
                                    index += len;
                                }else {
                                    break;
                                }
                            }
                            Log.e("TAG1", length+ " "+bytes.length);
                            h264DeCodePlay.decode(bytes);

                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();

本文demo(发送端):https://github.com/gujunhe/client

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gujunhe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值