Android屏幕镜像五:局域网镜像实现

前面实现了本地录制屏幕编码保存本机播放和录制手机音频数据保存本机播放。
把录制的屏幕数据和声音数据通过局域网传输到另一台Android设备上实时解码显示就可以实现一个简单的局域网镜像功能。
镜像的实现是由一个发送端一个接收端共同完成的,接下来用phone代指发送端,tv代指接收端。

一、tv端启动镜像服务

由于这里仅是一个简单的demo,没有实现局域网内设备发现,有兴趣的同学可以自行通过mDnsUPnP实现局域网内设备发现。
这里在tv端生成一个带有镜像服务信息的二维码,phone通过扫码获取tv镜像服务信息。

1、生成局域网内的http服务

借助nanohttpd可以生成一个可在局域网内访问的http服务,将这个服务作为镜像服务的主服务端口,通过这个主服务,phone可获取tv端详细的镜像服务信息。

集成nanohttpd
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
    implementation 'org.nanohttpd:nanohttpd:2.2.0'
}
处理http消息
public class TVServer extends NanoHTTPD {
    private static final String TAG = "LelinkHTTPD";
    private Context mContext;

    TVServer(Context context, int port) {
        super(port);
        this.mContext = context;
    }

    @Override
    public Response serve(IHTTPSession session) {
        Logger.i(TAG, "url:" + session.getUri());
        Method method = session.getMethod();
        if (method.equals(Method.GET)) {
            // get请求
            return handleGetRequest(session);
        } else if (method.equals(Method.POST)) {
            // post请求
            return handlePostRequest(session);
        }
        return super.serve(session);
    }

    private Response handleGetRequest(IHTTPSession session) {
        String uri = session.getUri();
        if (uri.endsWith("/startMirror")) {
            Map params = session.getParms();
            String decoder = params.get("decoder");
            if (!TextUtils.isEmpty(decoder)) {
                Config.getInstance().setDecoderType(decoder);
            }
            MirrorRender.getInstance().startAndroidReceiver();
            int videoPort = MirrorRender.getInstance().getAndroidVideoPort();
            int audioPort = MirrorRender.getInstance().getAndroidAudioPort();
            try {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("videoPort", videoPort);
                jsonObject.put("audioPort", audioPort);
                Logger.i(TAG,"startMirror " + jsonObject.toString());
                ByteArrayInputStream stream = new ByteArrayInputStream(jsonObject.toString().getBytes("UTF-8"));
                Response response = newChunkedResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, stream);
                response.addHeader("Access-Control-Allow-Origin", "*");
                return response;
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
        } else if (uri.endsWith("/stopMirror")) {
            Logger.i(TAG, "handleGetRequest stopMirror");
            Activity mirrorActivity = Config.getInstance().getMirrorActivity();
            if (mirrorActivity != null) {
                mirrorActivity.finish();
            }
            MirrorRender.getInstance().stopRender();
            return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, Response.Status.OK.getDescription());
        }
        return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
    }

    private Response handlePostRequest(IHTTPSession session) {
        Logger.i(TAG, "handlePostRequest");
        return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
    }

这里处理了两条请求,startMirror stopMirrorphone可通过
http://ip:port/startMirror http://ip:port/stopMirror对tv端请求。
startMirror中,通过MirrorRender获取到了用于接收镜像数据流的两个服务端口,phone可通过获取到的这两个端口分别传输视频流数据和音频流数据到tv端。

2、视频流服务

由于视频流不能丢失,所以这里选择tcp的方式进行传输,优先保证其稳定性。

public class AndroidVideoReceiver {
    private final static String TAG = "AndroidVideoReceiver";
    private MiniServerSocket mSocket;
    private OnReceiverListener mServerListener;
    private boolean isStop = false;
    private long mDataCount = 0;
    private long mFrameCount = 0;

    public void startReceiver() {
        try {
            mSocket = new MiniServerSocket(0);
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
            return;
        }
        Logger.i(TAG, "startMirror ");
        if (mServerListener != null) {
            mServerListener.onReceiveStarted(OnReceiverListener.ANDROID_VIDEO);
        }
        receiveVideo();
    }

    private Thread mVideoThread;

    private void receiveVideo() {
        mVideoThread = new Thread(new Runnable() {
            @Override
            public void run() {
                receive();
            }
        });
        mVideoThread.start();
    }

    public void receive() {
        try {
            isStop = false;
            while (!isStop) {
                Socket socket = mSocket.accept();
                //Logger.i(TAG, "accept **********");
                InputStream input = socket.getInputStream();
                byte[] buf = new byte[1024];
                int len = 0;
                byte[] head = null;
                int headIndex = 0;
                int dataSize = 0;
                byte[] frame = null;
                int frameIndex = 0;
                while ((len = input.read(buf)) != -1) {
                    //Logger.i(TAG, "receive new pact data " + len);
                    mDataCount += len;
                    int readIndex = 0;
                    int bufLen = len;
                    while (readIndex < len) {
                        if (dataSize == 0) {
                            //Logger.i(TAG, "receive data new ############# headIndex:" + headIndex + " readIndex:" + readIndex);
                            if (headIndex == 0) {
                                head = new byte[4];
                            }
                            int readHead = Math.min(head.length - headIndex, len - readIndex);
                            System.arraycopy(buf, readIndex, head, headIndex, readHead);
                            headIndex += readHead;
                            readIndex += readHead;
                            if (headIndex == head.length) {
                                dataSize = CodecUtils.bytesToInt(head);
                                //Logger.i(TAG, "receive data new frame size:" + dataSize);
                                frame = new byte[dataSize];
                                headIndex = 0;
                            } else {
                                //Logger.w(TAG, "receive head ****************** headIndex:" + headIndex + "  readIndex:" + readIndex);
                                break;
                            }
                        }
                        int bufLeft = bufLen - readIndex;
                        int frameLeft = frame.length - frameIndex;
                        //Logger.i(TAG, "receive data bufLeft:" + bufLeft + "  frameLeft:" + frameLeft);
                        if (bufLeft < frameLeft) {
                            System.arraycopy(buf, readIndex, frame, frameIndex, bufLeft);
                            frameIndex += bufLeft;
                            readIndex += bufLeft;
                        } else {
                            System.arraycopy(buf, readIndex, frame, frameIndex, frameLeft);
                            frameIndex += frameLeft;
                            readIndex += frameLeft;
                            if (mServerListener != null) {
                                mServerListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, resolveFrame(frame));
                            }
                            mFrameCount++;
                            //one frame read complete
                            dataSize = 0;
                            frameIndex = 0;
                        }
                        //Logger.i(TAG, "receive data readIndex:" + readIndex + "  frameIndex:" + frameIndex);
                    }
                }
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
        }
    }

    private Frame resolveFrame(byte[] data) {
        int index = 0;
        byte[] headSizeBytes = new byte[4];
        System.arraycopy(data, index, headSizeBytes, 0, headSizeBytes.length);
        index += headSizeBytes.length;
        int headSize = CodecUtils.bytesToInt(headSizeBytes);
        byte[] head = new byte[headSize];
        System.arraycopy(data, index, head, 0, head.length);
        index += head.length;
        String headContent = new String(head);
        //Logger.i(TAG, "headContent " + headContent);
        Map parameter = resolveHead(headContent);
        long pts = Long.parseLong(parameter.get("pts"));
        byte[] frameBytes = new byte[data.length - headSizeBytes.length - head.length];
        System.arraycopy(data, index, frameBytes, 0, frameBytes.length);
        Frame frame = new Frame();
        frame.buf = frameBytes;
        frame.pts = pts;
        return frame;
    }

    private Map resolveHead(String head) {
        Map map = new HashMap<>();
        String[] arr = head.split("&");
        for (String str : arr) {
            String[] arr1 = str.split("=");
            map.put(arr1[0], arr1[1]);
        }
        return map;
    }

    public void setOnReceiverListener(OnReceiverListener listener) {
        mServerListener = listener;
    }

    public int getReceiverPort() {
        if (mSocket != null) {
            return mSocket.getLocalPort();
        }
        return -1;
    }

    public long getDataCount() {
        return mDataCount;
    }

    public long getFrameCount() {
        return mFrameCount;
    }

    public void stopReceiver() {
        if (isStop) {
            return;
        }
        isStop = true;
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
            mSocket = null;
        }
        if (mServerListener != null) {
            mServerListener.onReceiveStopped(OnReceiverListener.ANDROID_VIDEO);
        }
    }

}

3、音频流服务

音频数据不怕丢失,所以这里选择udp进行传输。

public class AndroidAudioReceiver {
    private final static String TAG = "AndroidAudioReceiver";

    private MiniDatagramSocket mSocket;
    private static final int DATA_LEN = 4096;
    private byte[] inBuff = new byte[DATA_LEN];
    private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
    private OnReceiverListener mListener;
    private Thread mAudioThread;
    private boolean isStop = false;
    private Decoder mOpusDecoder;

    public void startReceiver() {
        try {
            mSocket = new MiniDatagramSocket(0);
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
            return;
        }
        Logger.i(TAG, "startReceiver");
        if (mListener != null) {
            mListener.onReceiveStarted(OnReceiverListener.ANDROID_AUDIO);
        }
        receiveAudio();
    }

    private void receiveAudio() {
        mAudioThread = new Thread(new Runnable() {
            @Override
            public void run() {
                receive();
            }
        });
        mAudioThread.start();
    }

    public void receive() {
        try {
            while (!isStop) {
                try {
                    mSocket.receive(inPacket);
                    //Logger.e(TAG, "receive length:" + inPacket.getLength());
                    byte[] data = new byte[inPacket.getLength()];
                    System.arraycopy(inPacket.getData(), 0, data, 0, data.length);
                    Frame frame = new Frame();
                    frame.buf = data;
                    //Logger.e(TAG, "receive opus:" + Arrays.toString(data));
                    //Logger.e(TAG, "receive pcm:" + Arrays.toString(frame.shortBuf));
                    if (mListener != null) {
                        mListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, frame);
                    }
                } catch (Exception e) {
                    Logger.w(TAG, "receive", e);
                }
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
        }
    }

    public int getPort() {
        if (mSocket != null) {
            return mSocket.getLocalPort();
        }
        return -1;
    }

    public void setOnReceiverListener(OnReceiverListener listener) {
        mListener = listener;
    }

    public void stopReceiver() {
        if (isStop) {
            return;
        }
        isStop = true;
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
            mSocket = null;
        }
        if (mListener != null) {
            mListener.onReceiveStopped(OnReceiverListener.ANDROID_AUDIO);
        }
    }
}

4、生成二维码

    private void updateServerInfo() {
        if (mIPTxt != null) {
            mIPTxt.setText(DeviceUtils.getIP(getActivity()) + ":" + MirrorRender.getInstance().getTVPort());
        }
        mServerInfo = "ip=" + DeviceUtils.getIP(getActivity())
                + "&port=" + MirrorRender.getInstance().getTVPort();
        mQRView.setImageBitmap(Utils.createQRCode(mServerInfo, 200, 0));
    }

二维码中含有tv端的ip和主服务端口,phone通过这个ip和主服务端口可以发起镜像。

二、phone扫码连接

通过摄像头扫码显示,这里偷个懒使用一个比较好的第三方扫码库。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.zxing:core:3.3.2'
    implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'
    implementation("com.squareup.okhttp3:okhttp:4.6.0")
}
扫码解析
public class ScanFragment extends BaseFragment {
    private final static String TAG = "ScanActivity";

    private QRCodeView.Delegate mQRDelegate = new QRCodeView.Delegate() {
        @Override
        public void onScanQRCodeSuccess(String result) {
            vibrate();
            ((MainActivity) getActivity()).notifyScanResult(result);
            getFragmentManager().popBackStack();
        }

        @Override
        public void onCameraAmbientBrightnessChanged(boolean isDark) {

        }

        @Override
        public void onScanQRCodeOpenCameraError() {
            Logger.i(TAG, "onScanQRCodeOpenCameraError");
        }
    };

    private ZXingView mQRCodeView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return View.inflate(getActivity(), R.layout.f_scan, null);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mQRCodeView = view.findViewById(R.id.zxingview);
        mQRCodeView.startCamera();
        mQRCodeView.startSpot();
        mQRCodeView.setDelegate(mQRDelegate);
    }

    private void vibrate() {
        Vibrator vibrator = (Vibrator) getActivity().getSystemService(VIBRATOR_SERVICE);
        vibrator.vibrate(200);
    }

    @Override
    public void onStop() {
        super.onStop();
        mQRCodeView.stopCamera();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mQRCodeView.onDestroy();
    }
}
解析ip和镜像服务主端口
    public void updateSinkInfo(String info) {
        Logger.i(TAG, "updateSinkInfo " + info);
        Map map = new HashMap<>();
        String[] arr = info.split("&");
        for (String str : arr) {
            String[] res = str.split("=");
            if (res.length > 1) {
                map.put(res[0], res[1]);
            }
        }
        String ip = map.get(KEY_IP);
        String port = map.get(KEY_PORT);
        if (mPortEdit != null) {
            mIPEdit.setText(ip);
            mPortEdit.setText(port);
        } else {
            Logger.i(TAG, "invalid receive");
        }
    }

三、phone发送视频流数据

开始镜像
    private void startMirror() {
        String ip = getEditString(mIPEdit);
        int port = getEditInt(mPortEdit);
        if (TextUtils.isEmpty(ip) || port <= 0) {
            Toast.makeText(getActivity(), "请输入正确的IP和端口", Toast.LENGTH_SHORT).show();
            return;
        }
        Config.getInstance().setTVIP(ip);
        Config.getInstance().setTVPort(port);
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType())
                .build();
        Logger.i(TAG, "http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                mHandler.obtainMessage(WHAT_TOAST, "请求失败").sendToTarget();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try {
                    String result = response.body().string();
                    JSONObject json = new JSONObject(result);
                    int videoPort = json.optInt("videoPort");
                    int audioPort = json.optInt("audioPort");
                    if (videoPort <= 0 || audioPort <= 0) {
                        mHandler.obtainMessage(WHAT_TOAST, "TV端口异常").sendToTarget();
                        return;
                    }
                    Config.getInstance().setVideoPort(videoPort);
                    Config.getInstance().setAudioPort(audioPort);
                    ((MainActivity) getActivity()).requestScreenCapture();
                } catch (Exception e) {
                    Logger.w(TAG, e);
                    mHandler.obtainMessage(WHAT_TOAST, "数据解析失败").sendToTarget();
                }
            }
        });
    }

在屏幕数据回调中,传输视频流数据到tv端

        @Override
        public void onCaptureVideoCallback(byte[] buf, long pts) {
//            Logger.i(TAG, "onCaptureVideoCallback " + pts);
            long start = System.currentTimeMillis();
            byte[] newBuf = new byte[buf.length];
            Frame videoFrame = new Frame();
            System.arraycopy(buf, 0, newBuf, 0, buf.length);
            videoFrame.buf = newBuf;
            videoFrame.pts = pts;
            mVideoSender.writeData(videoFrame);
            //Logger.i(TAG, "onCaptureVideoCallback cost " + (System.currentTimeMillis() - start));
        }
public class VideoSender {
    private final static String TAG = "VideoSender";
    private MiniSocket mSocket;
    private OutputStream mWriteStream;
    private ConcurrentLinkedQueue mFrameQueue = new ConcurrentLinkedQueue<>();
    private Thread mWriteThread = null;
    private OnSendListener mListener;

    public void connect(String ip, int port) {
        Logger.i(TAG, "connect " + port);
        try {
            mSocket = new MiniSocket(ip, port);
            mSocket.setTcpNoDelay(true);
            mSocket.setKeepAlive(true);
            mWriteStream = mSocket.getOutputStream();
            startWriteThread();
        } catch (Exception e) {
            Logger.w(TAG, e);
            if (mListener != null) {
                mListener.onError();
            }
        }
    }

    private void startWriteThread() {
        mWriteThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (mWriteStream == null) {
                        return;
                    }
                    if (mFrameQueue.isEmpty()) {
                        continue;
                    }
                    Frame frame = mFrameQueue.poll();
                    if (frame == null) {
                        continue;
                    }
                    long start = System.currentTimeMillis();
                    byte[] head = ("pts=" + frame.pts).getBytes();// 多个参数之间 & 分割
                    byte[] headCount = CodecUtils.intToBytes(head.length);// 多个参数之间 & 分割
                    byte[] totalCount = CodecUtils.intToBytes(headCount.length + head.length + frame.buf.length);

                    byte[] bytes = new byte[totalCount.length + headCount.length + head.length + frame.buf.length];

                    //Logger.i(TAG, "writeData  frameLen " + frame.buf.length + " headLen " + head.length);
                    int index = 0;
                    System.arraycopy(totalCount, 0, bytes, index, totalCount.length);
                    index += totalCount.length;
                    System.arraycopy(headCount, 0, bytes, index, headCount.length);
                    index += headCount.length;
                    System.arraycopy(head, 0, bytes, index, head.length);
                    index += head.length;
                    System.arraycopy(frame.buf, 0, bytes, index, frame.buf.length);
                    try {
                        mWriteStream.write(bytes);
                    } catch (Exception e) {
                        Logger.w(TAG, e);
                        mWriteStream = null;
                        if (mListener != null) {
                            mListener.onError();
                        }
                        break;
                    }
                    if (mFrameQueue.size() > 5) {
                        MirrorApplication.getApplication().changeBitrate(256 * 1024);
                    } else {
                        MirrorApplication.getApplication().changeBitrate(2 * 1024 * 1024);
                    }
                    long cost = (System.currentTimeMillis() - start);
                    if (cost > 16) {
                        Logger.i(TAG, "send video frame cost:" + cost + " len:" + bytes.length + " left frames:" + mFrameQueue.size());
                    }
                }
                disconnect();
            }
        });
        mWriteThread.start();
    }

    public void writeData(Frame frame) {
        //Logger.i(TAG,"writeData");
        mFrameQueue.add(frame);
    }

    public void disconnect() {
        try {
            if (mWriteStream != null) {
                mWriteStream.close();
                mWriteStream = null;
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
        try {
            if (mSocket != null) {
                mSocket.close();
                mSocket = null;
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
    }

    public void release() {
        mFrameQueue.clear();
    }

    public void setOnSendListener(OnSendListener listener) {
        mListener = listener;
    }
}

四、phone发送端音频流数据

与发送端视频流不同的是,音频数据发送使用udp

    private AudioCapture.OnAudioCaptureCallback mAudioCallback = new AudioCapture.OnAudioCaptureCallback() {
        @Override
        public void onCaptureAudioCallback(short[] buf) {
            Logger.i(TAG, "opus---- onCaptureAudioCallback: " + Arrays.toString(buf));
            mAudioSender.writeData(buf);
        }
    };

AudioSender

public class AudioSender {
    private final static String TAG = "PCMSender";
    private MiniDatagramSocket mSocket;
    private boolean disconnect = true;
    private String mTargetIP;
    private int mTargetPort;
    private static final int DATA_LEN = 4096;
    private OnSendListener mListener;

    public void connect(String ip, int port) {
        mTargetIP = ip;
        mTargetPort = port;
        try {
            Logger.i(TAG, "connect " + ip + "/" + port);
            mSocket = new MiniDatagramSocket(0);
            disconnect = false;
            Logger.i(TAG, "connect success " + ip + "/" + port);
        } catch (Exception e) {
            Logger.w(TAG, "connect", e);
            disconnect = true;
            if (mListener != null) {
                mListener.onError();
            }
        }
    }

    public void writeData(Frame frame) {
        if (disconnect) {
            Logger.i(TAG,"writeData ignore");
            return;
        }
        Logger.i(TAG,"writeData " + Arrays.toString(frame.buf));
        int readOffset = 0;
        while (readOffset < frame.buf.length) {
            int left = Math.min(DATA_LEN, frame.buf.length - readOffset);
            byte[] buf = new byte[left];
            System.arraycopy(frame.buf, readOffset, buf, 0, buf.length);
            try {
                InetAddress address = InetAddress.getByName(mTargetIP);
                DatagramPacket packet = new DatagramPacket(buf, buf.length, address, mTargetPort);
                mSocket.send(packet);
            } catch (Exception e) {
                Logger.w(TAG, e);
                disconnect();
                if (mListener != null) {
                    mListener.onError();
                }
                break;
            }
            readOffset += left;
        }
    }

    public void disconnect() {
        disconnect = true;
        try {
            mSocket.close();
            mSocket = null;
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
    }

    public void setOnSendListener(OnSendListener listener) {
        mListener = listener;
    }

}

解码显示部分与在本机解码显示一致,不再赘述

你可能感兴趣的:(Android屏幕镜像五:局域网镜像实现)