把摄像头转成H265数据给接收方

2,412 阅读4分钟

接上一篇使用H264或者H265实现手机投屏

概述

这一篇把摄像头的预览数据转化成H265码流发送给接收方,主要难点是

  • 摄像头的数据是NV21,但是硬件编解码 MediaCodec 不支持NV21 ,所以要把 NV21转成 YUV420
  • 摄像头厂商都是横着的,所以要选择90度
  • 既然摄像头是横着的,所以YUV420的数据也要旋转90,(注意:既然数据也旋转了,那么MediaCodec的宽高就成了摄像头的高宽了)

MainActivity

主要是请求摄像头权限,开启 Socket,预览摄像头数据

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    private static final int REQUEST_CODE_CAMERA = 100;
    private SurfaceHolder mSurfaceHolder;
    private PushSocket mPushSocket;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SurfaceView surfaceview = findViewById(R.id.surfaceview);
        surfaceview.getHolder().addCallback(this);


    }



    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_CAMERA && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //申请成功,可以拍照
           initSocket();

            Toast.makeText(this, "有权限了", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "CAMERA PERMISSION DENIED", Toast.LENGTH_SHORT).show();
        }
    }

    private void initSocket() {
        mPushSocket = new PushSocket(this,mSurfaceHolder);
        mPushSocket.start();
    }


    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        mSurfaceHolder = holder;
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {

    }


    public void start(View view) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_CAMERA);
        } else {
            initSocket();
        }
    }
}

Socket

主要是把摄像头的数据转化成 H265发送出去

public class PushSocket implements Camera.PreviewCallback {
    private static final String TAG = "PushSocket";
    private WebSocket mWebSocket;

    private Camera mCamera;
    private Camera.Size mSize;

    private SurfaceHolder mSurfaceHolder;
    private byte[] mBuffer;
    private byte[] nv12;
    private MediaCodec mMediaCodec;
    private byte[] mYuv420;
    /**
     * 端口号
     */
    private static final int PORT = 13001;

    private Context mContext;

    public PushSocket(Context context, SurfaceHolder surfaceHolder) {
        mContext = context;
        mSurfaceHolder = surfaceHolder;
    }

    public void start() {

        webSocketServer.start();
        initCamera();
    }

    private WebSocketServer webSocketServer = new WebSocketServer(new InetSocketAddress(PORT)) {
        @Override
        public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) {
            mWebSocket = webSocket;
        }

        @Override
        public void onClose(WebSocket conn, int code, String reason, boolean remote) {
            Log.i(TAG, "onClose: 关闭 socket ");
        }

        @Override
        public void onMessage(WebSocket webSocket, String message) {
        }

        @Override
        public void onError(WebSocket conn, Exception e) {
            Log.i(TAG, "onError:  " + e.toString());
        }

        @Override
        public void onStart() {

        }
    };

    /**
     * 发送数据
     *
     * @param bytes
     */
    public void sendData(byte[] bytes) {
        if (mWebSocket != null && mWebSocket.isOpen()) {
            mWebSocket.send(bytes);
        }
    }

    /**
     * 关闭 Socket
     */
    public void close() {
        try {
            mWebSocket.close();
            webSocketServer.stop();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

    }

    private void initCamera() {
        mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
        Camera.Parameters parameters = mCamera.getParameters();
        mSize = parameters.getPreviewSize();
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);
            // 因为摄像头厂家都是横着的 所以把预览方向 调整
            mCamera.setDisplayOrientation(90);
            // 缓冲数据
            mBuffer = new byte[mSize.width * mSize.height * 3 / 2];
            mCamera.addCallbackBuffer(mBuffer);
            mCamera.setPreviewCallbackWithBuffer(this);
//            输出数据怎么办
            mCamera.startPreview();

            // 因为用到了摄像头的宽高,所以写在这里
            initEncode();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void initEncode() {
        try {
            mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
            // 因为 摄像头数据 旋转了,所以这里的宽高就会变成高宽
            MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, mSize.height, mSize.width);
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1080 * 1920);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
            mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mMediaCodec.start();
            // 因为 YUV420 每个像素占用 3/2 个字节
            int bufferLength = mSize.width*mSize.height*3/2;
            nv12 = new byte[bufferLength];
            mYuv420 = new byte[bufferLength];
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public int encodeFrame(byte[] input) {

        // 因为摄像头的数据是 NV21,只有摄像头是这个格式,在硬件编码根本没有这个码
        // 所以要转成YUV420
        nv12 =YuvUtils.nv21toYUV420(input);
        // 因为 摄像头是横着的,所以 数据也是横着的,把数据旋正
        YuvUtils.portraitData2Raw(nv12, mYuv420, mSize.width, mSize.height);

        int inputBufferIndex = mMediaCodec.dequeueInputBuffer(100000);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
            inputBuffer.clear();
            inputBuffer.put(mYuv420);
            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, mYuv420.length, System.currentTimeMillis(), 0);
        }
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
            dealFrame(outputBuffer, bufferInfo);
//            saveFile(outputBuffer,bufferInfo);
            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        }
        return 0;
    }
    public static final int NAL_I = 19;
    public static final int NAL_VPS = 32;
    private byte[] vps_sps_pps_buf;

    private void dealFrame(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {
        int offset = 4;
        if (byteBuffer.get(2) == 0x01) {
            offset = 3;
        }
        int type = (byteBuffer.get(offset) & 0x7E) >> 1;
        // vps_sps_pps 帧记录下来
        if (type == NAL_VPS) {
            vps_sps_pps_buf = new byte[bufferInfo.size];
            byteBuffer.get(vps_sps_pps_buf);
        } else if (type == NAL_I) {
            // I 帧 ,把 vps_sps_pps 帧塞到 I帧之前一起发出去
            final byte[] bytes = new byte[bufferInfo.size];
            byteBuffer.get(bytes);

            byte[] newBuf = new byte[vps_sps_pps_buf.length + bytes.length];
            System.arraycopy(vps_sps_pps_buf, 0, newBuf, 0, vps_sps_pps_buf.length);
            System.arraycopy(bytes, 0, newBuf, vps_sps_pps_buf.length, bytes.length);
            sendData(newBuf);
            Log.v(TAG, "I帧 视频数据  " + Arrays.toString(bytes));
        } else {
            // B 帧 P 帧 直接发送
            final byte[] bytes = new byte[bufferInfo.size];
            byteBuffer.get(bytes);
            sendData(bytes);

        }

    }
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        encodeFrame(data);
        mCamera.addCallbackBuffer(data);
    }
    private void saveFile(ByteBuffer buffer,MediaCodec.BufferInfo bufferInfo) {
        byte[] bytes = new byte[bufferInfo.size];
        buffer.get(bytes);
        YuvUtils.writeBytes(bytes,mContext);
        YuvUtils.writeContent(bytes,mContext);
    }
}

YUV工具

主要是旋转工作

public class YuvUtils {
    private static final String TAG = "YuvUtils";
    static  byte[] yuv420;

    public static byte[] nv21toYUV420(byte[] nv21) {
        int  size = nv21.length;
         yuv420 = new byte[size];
        int len = size * 2 / 3;
        System.arraycopy(nv21, 0, yuv420, 0, len);
        int i = len;
        while(i < size - 1){
            yuv420[i] = nv21[i + 1];
            yuv420[i + 1] = nv21[i];
            i += 2;
        }
        return yuv420;
    }

    public static void portraitData2Raw(byte[] data,byte[] output,int width,int height) {
        int y_len = width * height;
        // uv数据高为y数据高的一半
        int uvHeight = height >> 1;
        int k = 0;
        for (int j = 0; j < width; j++) {
            for (int i = height - 1; i >= 0; i--) {
                output[k++] = data[width * i + j];
            }
        }
        for (int j = 0; j < width; j += 2) {
            for (int i = uvHeight - 1; i >= 0; i--) {
                output[k++] = data[y_len + width * i + j];
                output[k++] = data[y_len + width * i + j + 1];
            }
        }
    }
    public  static  void writeBytes(byte[] array, Context context) {
        FileOutputStream writer = null;
        try {
            // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
            writer = new FileOutputStream(context.getFilesDir() + "/codec.h265", true);
            writer.write(array);
            writer.write('\n');


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public  static String writeContent(byte[] array, Context context) {
        char[] HEX_CHAR_TABLE = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };
        StringBuilder sb = new StringBuilder();
        for (byte b : array) {
            sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);
            sb.append(HEX_CHAR_TABLE[b & 0x0f]);
        }
        Log.i(TAG, "writeContent: " + sb.toString());
        FileWriter writer = null;
        try {
            // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
            writer = new FileWriter(context.getFilesDir() + "/codecH265.txt", true);
            writer.write(sb.toString());
            writer.write("\n");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }


}

还用到了 Java-WebSocket

记得在在每个app的buid.gradle中 同时 别忘了在清单文件中加 INTERNET 权限

implementation "org.java-websocket:Java-WebSocket:1.4.0"

源码地址 github