【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解

闲置在家不用的Android手机有一两个都蒙尘了,想要把它们充分利用起来,可知道,现有的智能手机是可以充当Wifi摄像头来使用的,这就需要装一个App就能实现了,如果是用别的下载来APP安装用来会不会不放心呢,如果自己有能力,那就可以通过开发Android App项目过程来实现视频监控,有兴趣的来看看接下来的实现方案,

要完成整个过程,至少需要两部手机,一个手机用来充当WIFI摄像头(可以开启WIFI热点),另一个手机当视频监控用的,还是建议用WIFI路由器,就看中它信号强,网络又稳定

关于能看懂此文章的条件

  1. 会使用Android Studio开发工具
  2. 熟悉Java编程语言,开发过Android App
  3. 对WIFI路由器设置和网络信息收发报文TCPUDP原理有过了解

1.首先,打开Android Studio开发工具,选择新建Android 项目,使用Java语言,模板就选择 Emtpy Activity,在activity_main.xml文件中做好布局,具体布局内容太多这里就不贴了,自己布局就好,拖放组件是很简单的操作,只需要放三个按钮组件即可,分别是扫描摄像头开启摄像头退出APP,其它的不重要
【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第1张图片
2. 然后,在MainActivity.class上写代码,实现能打开按钮对应的页面即可,请看如下代码,其中用到的一些类,例如DeviceInfo.class, Common.class, BaseBackActivity.class这些就不贴了,看注释,具体的请等在后面提供的项目源码里看

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //...这里省略了,只是处理了对标题栏的隐藏
        setContentView(R.layout.activity_main);
        //获取布局中的按钮组件
        Button btnScan = findViewById(R.id.button_scan);
        Button btnPreview = findViewById(R.id.button_preview);
        Button btnExit = findViewById(R.id.buttonExit);

        final Context context = MainActivity.this;
        //设置点击事件
        btnExit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                MainActivity.this.finish();//退出
            }
        });
        btnScan.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
            	//初始化设备信息,包括了手机的摄像头相关属性,如名称,IP,数量
                DeviceInfo di = DeviceInfo.init(context);
                //...省略了一些判断细节,如判断IP是否正确,判断摄像头的网络状态
                //打开扫描局域网内的摄像头页面
                BaseBackActivity.navigateTo(MainActivity.this, ScanActivity.class, di);
            }
        });
        btnPreview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
	            //初始化设备信息
                DeviceInfo di = DeviceInfo.init(context);
                //...省略了一些判断细节,这一步是判断摄像头的授权
                if(Common.requestCameraPermission(MainActivity.this)){
                	//打开WIFI摄像头的预览页面
                    BaseBackActivity.navigateTo(MainActivity.this, PreviewActivity.class, di);
                }
            }
        });
    }
}
  1. 接下来,做一个扫描摄像头页面的布局,文件是activity_scan.xml,大致布局如下图所示,运行后的效果图,就一个ListView展示列表的组件,还有标题栏上的搜索图标,那是扫描按钮
    【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第2张图片

  2. 接着,创建一个对应页面的类ScanActivity.class 文件后,写上代码,如下

/**
 * 扫描摄像头窗口
 * */
public class ScanActivity extends BaseBackActivity {
	//定义扫描线程
    private ScanThread thread;
   	//定义列表组件
    private ListView list;
    //定义对初始化扫描的判断值
    private boolean isFirstScan = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scan);
		//省略了,处理初始化标题栏的
        //获取上一页传来的设备信息对象, getSerializable()是来自父类BaseBackActivity的方法
        DeviceInfo di = (DeviceInfo) getSerializable();
		//创建线程时,传入设备信息对象
        thread = new ScanThread(this, di, new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                //处理线程传来的消息
                switch (msg.what) {
                	//扫描完成通知
                    case BaseThread.MESSAGE_SUCCESS:
                    {
                    	//获取扫描后的局域网内所有可用的摄像头
                        ArrayList<RemoteCamera> cameras = thread.getCameras();
                        if(cameras.isEmpty()) {
                        	//showToast方法来自父类,弹出提示
                            showToast(ScanActivity.this, "找不到可用的摄像头!");
                        }else{
                        	//更新摄像头列表显示的
                            CamerasAdapter adapter = new CamerasAdapter(ScanActivity.this, cameras);
                            list.setAdapter(adapter);
                            list.invalidate();
                            showToast(ScanActivity.this, "扫描完成!");
                        }
                    }
                        break;
                    //扫描失败,或更新状态
                    case ScanThread.MESSAGE_FAIL:
                    case ScanThread.MESSAGE_LOADING:
                        showToast(ScanActivity.this, (String) msg.obj);
                        break;
                    default:
                }
            }
        });

        list = findViewById(R.id.listview);
		//列表的点击事件
        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                RemoteCamera camera = thread.getCameras().get(i);
                //打开远程摄像头连接页面,传递一个摄像头信息camera
                navigateTo(ScanActivity.this, RemoteActivity.class, camera);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //...此处省略,加载菜单布局的
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        //监听菜单按钮
        switch (item.getItemId()) {
        	//扫描图标按钮被点击
            case R.id.app_bar_search:
                thread.startScanCamera();
                return true;
            default:
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //第一次打开页面就扫描
        if (!isFirstScan) {
            thread.startScanCamera();
            isFirstScan = true;
        }
    }
}
  1. 看上面就会发现,扫描的处理操作是比较耗时的,放在线程ScanThread.class里处理是合理的,这样用户操作就不会觉得卡,处理操作的方法大致讲一下
public class ScanThread extends BaseThread {

    private ArrayList<RemoteCamera> cameras;
    private DeviceInfo info;
    //定义一个扫描线程
    private Thread scanThread = null;

    public ScanThread(Activity context, DeviceInfo info, Handler handler) {
    	//传参给父类BaseThread的构造方法,初始化
        super(context, handler);
        this.info = info;
        this.cameras = new ArrayList<RemoteCamera>();
		//来自父类的线程,用于处理接收的
        thread = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                try {
                	//初始化端口
                    mSocket = new DatagramSocket(null);
                    //...
                    mSocket.bind(new InetSocketAddress(SenderThread.FIND_CAMERA_PORT));
                    while(!Thread.interrupted()) {
                    	//定一个空的数据报文
                        DatagramPacket pack = new DatagramPacket(new byte[1028], 1028);
                        //用空数据报文来接收数据,这时会一直等待,阻塞
                        mSocket.receive(pack);
                        //收到时,将报文里的数据转换成字符串
                        String s = new String(pack.getData(), 0, pack.getLength());
                        //在把字符串转成字符串数组,将接收到数据按照约定的协议转换一下
                        String[] datas = Common.getDeviceData(s);
                        //...此处省略,处理拿到count, 是摄像头数量,添加到cameras中
                        cameras.add(new RemoteCamera(cameras.size(), datas[0], count, datas[2]));
                        //发完成提示消息
                        showToast("扫到一个摄像头", MESSAGE_SUCCESS);
                    }
                } catch (Exception e) {
                    showToast(e.getMessage());//遇到错误!
                } finally {
                    cancelScan(true);
                }
            }

        });
        thread.start();
    }

    public void startScanCamera() {
        //...次数省略判断的细节,下一步提示用户扫描中,建一个线程处理
        showToast("扫描中...", MESSAGE_LOADING);
        scanThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //...此处省略一些细节,定义data数据
                    // 定义局域网的广播地址,这样表示 *.*.*.255,
                    InetAddress cameraAddress = InetAddress.getByName(Common.getWanIP(info.getLocalIp())+"255");
                    // 将data数据封装到报文中,还有IP地址,FIND_CAMERA_PORT 是 30000
                    DatagramPacket pack = new DatagramPacket(data, data.length, cameraAddress, FIND_CAMERA_PORT);
                    //将数据报文发送到广播地址,只要是连接到此局域网内的所有设备开放的30000端口都会收到该广播报文
                    mSocket.send(pack);
                } catch (Exception e) {
                    showToast(e.getMessage());//遇到错误!
                } finally {
                	//处理完后取消操作
                    cancelScan(false);
                }
            }
        });
        scanThread.start();
    }
	//判断是否在扫描
    public boolean isScaning() {
        return scanThread!=null;
    }

    public ArrayList<RemoteCamera> getCameras() {
        return cameras;
    }
	//取消扫描
    public void cancelScan(boolean isCancelAll) {
        if(isCancelAll) {
        	//处理来自父类的方法
            cancelThread();
        }
        if (isScaning()) {
            scanThread.interrupt();
            scanThread = null;
        }
    }
}

小提示
注意到创建的页面都有继承BaseBackActivity.class类,创建的线程都有继承类BaseThread.class,具体怎么写的,这里不详细讲了,那说下它的作用,它是相当于一个可以复用的类吧,类似模板,可以这样理解,稍微能明白,实现不会复杂

  1. 把需要添加的权限都写上,在AndroidManifest.xml文件中,添加如下关键的代码

<manifest xmlns:android="...">

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />

    
manifest>
  1. 接下来,打开视频监控,也就是远程摄像头预览的页面,大致布局如下图所示,文件是activity_remote.xml,放一个展示状态的TextView组件,还有一个预览画面的SurfaceView组件放在中间,宽高分别是固定的320dp,240dp
    【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第3张图片

小提示
有没有注意到,看视频监控上的状态栏,网络保持在23.3K/s每秒,这已经是一帧一帧的传输图像了,图像是320x240分辨率的,传输量会不会低了,可能有点卡吧,跟网络传输延迟有关的

  1. 接着,创建一个对应页面的类,在RemoteActivity.class 文件里,写上代码,如下
public class RemoteActivity extends BaseBackActivity {
	//定义一个网络接收的线程
    private ReceiveThread thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_remote);
        //...省略了,处理初始化标题栏的
        //获取上一页传来的设备信息对象, getSerializable()是来自父类BaseBackActivity的方法
        RemoteCamera remote = (RemoteCamera) getSerializable();
		//从布局中获取组件
        SurfaceView view = findViewById(R.id.surfaceView2);
        final TextView showState = findViewById(R.id.textView_state2);
		//将远程设备信息设置到标题栏上
        setTitle("远程摄像头:"+remote.toString());
        //先获取焦点,然后设置屏幕长亮
        view.setFocusable(true);
        view.setKeepScreenOn(true);
		//建立一个接收线程,传一个远程设备信息对象,还有预览组件的holder用于更新画面
        thread = new ReceiveThread(this, new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                //...处理线程发来的消息提示
            }
        }, remote, view.getHolder());
    }

    @Override
    protected void onPostResume() {
        super.onPostResume();
        //让线程开始接收工作
        thread.startReceive();
    }

    @Override
    protected void onDestroy() {
	    //当前页面关闭时,让线程结束工作
        thread.cancelReceive();
        super.onDestroy();
    }
}
  1. 看上一步就会发现,关键的处理接收方法都放在线程ReceiveThread.class里,那是比较耗时的操作,大致讲一下
public class ReceiveThread extends BaseThread {

    private RemoteCamera remote;
    private SurfaceHolder holder;

    public ReceiveThread(Activity context, Handler handler, RemoteCamera remote, SurfaceHolder holder) {
        super(context, handler);
        this.remote = remote;
        this.holder = holder;
    }

    public void startReceive() {
        if (thread!=null) {
            return;
        }
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                String errMsg = "未知错误";
                //...
                try {
                    //...
                    if (mSocket==null) {
                        mSocket = new DatagramSocket(null);
                        //用开放30000端口来接收  FIND_CAMERA_PORT
                        mSocket.bind(new InetSocketAddress(FIND_CAMERA_PORT));
                        //...
                        showToast("连接中...", MESSAGE_UPDATE_STATE);
                        //发送请求接收下一帧图片
                        sendRet(mSocket, baos);
                        //...
                        showToast("等待接收...", MESSAGE_UPDATE_STATE);
                        while(mSocket!=null) {
                            //...定义空的数据报文packet
                            try {
                                //接收中,等待,此处阻塞
                                mSocket.receive(packet);
                            }catch (SocketTimeoutException te) {
                                showToast("连接超时..."+getLocalDateTime(), MESSAGE_UPDATE_STATE);
                                //再次发送请求
                                sendRet(mSocket, baos);
                                //...继续循环,重新接收
                                continue;
                            }
                            //判断一帧图片baos数据是否接收完成
                            if(packet.getLength() == endlen) {
                                String end = new String(packet.getData(), 0, endlen);
                                if(end.startsWith(PACKET_END)) {
                                    //...获取time时间数据,下一步更新显示
                                    updateViewDisplay(baos, time);
                                    //设置接收下一帧等待时长,至少每100ms接收下一帧,可以设置更小,让视频看着更流畅
                                    Thread.sleep(100);
                                    baos.flush();
                                    //再次发送请求
                                    sendRet(mSocket, baos);
                                    //...
                                    showToast("接收中..."+getLocalDateTime(), MESSAGE_UPDATE_STATE);
                                    continue;
                                }
                            }
                            //接收一帧图片数据流
                            baos.write(packet.getData(), 0, packet.getLength());
                        }
                    }
                } catch (Exception e) {
                    errMsg = e.getMessage();//断开连接!;
                } finally {
                    //...
                    cancelThread(errMsg);
                }
            }
        });
        thread.start();
    }

    private void sendRet(DatagramSocket dSocket, ByteArrayOutputStream baos) throws IOException, Exception {
        //省略...处理发送接收下一帧图片请求
        InetAddress address = InetAddress.getByName(remote.getIp());
        DatagramPacket pack = new DatagramPacket(data, data.length, address, FIND_CAMERA_PORT);
        dSocket.send(pack);
        //...
    }

    private void updateViewDisplay(final ByteArrayOutputStream baos, final String time) {
        //省略...处理转换图片
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                //...锁定中,从组件中获取画布Canvas
                Canvas canvas = holder.lockCanvas(null);
                //...将图片画组件中,让用户可以看到
                canvas.drawBitmap(bitmap2, 0, 0, null);
                //...画上时间
                canvas.drawText(time, 20, 30, p);
                //解除锁定
                holder.unlockCanvasAndPost(canvas);
                //...
            }

        });
    }

    public void cancelReceive() {
        cancelThread();
    }
}
  1. 接下来,做一个开启摄像头页面的布局,文件是activity_preview.xml,大致布局如下图所示,是运行后的效果图,同上面讲过,跟远程摄像头页面布局那个是一样的,现在是有多放了一个选择摄像头的下拉框组件Spinner
    【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第4张图片

  2. 接着,创建一个对应的页面类PreviewActivity.class文件,写上代码,参考如下

public class PreviewActivity extends BaseBackActivity {

    private Camera camera = null;
    private SurfaceHolder holder;
    private Spinner seletep;
    private int selectCameraId = 0;
    //定义发送图片的线程
    private SenderThread thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preview);
        //...
        final DeviceInfo info = (DeviceInfo) getSerializable();
        //...获取布局中的组件,SurfaceView是绘制组件
        final SurfaceView view = findViewById(R.id.surfaceView);
        seletep = findViewById(R.id.spinner);
        final TextView stateView = findViewById(R.id.textView_state);
		//创建一个发送图片的线程,传入设备信息对象
        thread = new SenderThread(this, info, new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                //...处理线程发来的消息
            }
        });

        String localIp = thread.getLocalIp();
        String name = thread.getDeviceName();
        setTitle("设备名:"+name+ ", 局域网IP:"+localIp);

        seletep.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                //...切换摄像头
            }
        });

        //先获取焦点
        view.setFocusable(true);
        //然后设置屏幕长亮
        view.setKeepScreenOn(true);

        holder = view.getHolder();
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        holder.addCallback(new SurfaceHolder.Callback() {

            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                //绘制组件创建,准备摄像头
                int cameraCount = Camera.getNumberOfCameras();
                //...
                thread.setCameraCount(cameraCount);
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
                //绘制组件大小改变,重置摄像头
                //...
                openCamera();
            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
                thread.cancelThread();
                //绘制组件销毁,释放摄像头资源
                closeCamera();
            }
        });
    }
    
    @Override
    protected void onPostResume() {
        super.onPostResume();
        //可被局域网内发现摄像头
        thread.canFind(true);
    }

    private void openCamera() {
        //...
        try{
            camera = Camera.open(selectCameraId);
            Camera.Parameters params = camera.getParameters();
            List<Camera.Size> sizes = params.getSupportedPictureSizes();
            //图像大小
            final int PICTURE_WIDTH = 320, PICTURE_HEIGHT = 240;
            Camera.Size size = null;
            //省略细节...查找摄像头配置参数,赋值图像大小
            params.setPreviewSize(size.width, size.height);
            params.setPreviewFrameRate(20);
            params.setPictureFormat(PixelFormat.YCbCr_420_SP);
            camera.setParameters(params);
            //讲摄像头的图像设置到绘制组件中
            camera.setPreviewDisplay(holder);
            camera.setPreviewCallback(new Camera.PreviewCallback(){

                @Override
                public void onPreviewFrame(byte[] bytes, Camera camera) {
                    //...省略细节...处理摄像头传来的图片,将bytes转换成image,当然可以不转换,直接发送更高效吧
                    //交给线程去发送
                    thread.setSendCameraImage(image);
                }
            });
            camera.startPreview();
        } catch (Exception e) {
            showToast(this, "开启摄像头遇到了错误!");
        }
    }

    void closeCamera() {
        //...
        camera.stopPreview();
        camera.setPreviewCallback(null);
        camera.release();
        camera = null;
    }

}
  1. 看上一步就会发现,关键的处理发送方法都放在线程SenderThread.class里,那也是比较耗时的操作,大致讲一下
public class SenderThread extends BaseThread {
    private int cameraCount = 0;
    public boolean isSending = false;
    private DeviceInfo info;
    private YuvImage image = null;

    public SenderThread(Activity context, DeviceInfo info, Handler handler){
        super(context, handler);
        this.info = info;
    }
    //...
    public void setCameraCount(int cameraCount) {
        this.cameraCount = cameraCount;
    }

    public void canFind(boolean isFind) {
        if(isFind==true && thread==null) {
            this.thread = new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    String errMsg = "未知错误";
                    try {
                        mSocket = new DatagramSocket(null);
                        //...绑定开放的30000端口
                        mSocket.bind(new InetSocketAddress(FIND_CAMERA_PORT));
                        do {
                            //...
                            DatagramPacket pack = new DatagramPacket(new byte[1028], 1028);
                            try {
                            	//接收数据,等待中,会阻塞
                                mSocket.receive(pack);
                            }catch (Exception e){
                                e.printStackTrace();
                                throw e;
                            }
                            //获取发来请求的设备地址
                            SocketAddress sendAddress = pack.getSocketAddress();
                            //...
                            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(pack.getData()));
                            Integer code = (Integer) ois.readObject();
                            //...
                            switch (code){
                                case GET_CAMERA_IP:
                                {
                                    //...判断请求1,将摄像头的数据包装成data,封装在报文中回发过去
                                    DatagramPacket packet = new DatagramPacket(data, data.length, sendAddress);
                                    mSocket.send(packet);
                                }
                                break;
                                case RET_CAMERA_IP:
                                {
                                    isSending = true;
                                    //...判断请求2,处理一帧图片回发过去
                                    sendImage(image, sendAddress, sendTime);
                                    isSending = false;
                                }
                                break;
                                default:
                            }
                        }while (!thread.isInterrupted() && mSocket!=null);
                    } catch (Exception e) {
                        errMsg = e.getMessage();
                    } finally {
                        cancelThread(errMsg);
                    }
                }

            });
            this.thread.start();
        }else{
            cancelThread();
        }
    }

    private void sendImage(YuvImage image, SocketAddress sendAddress, String sendTime) throws Exception {
	    ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	    //将图片转换成数据流,压缩了图片就变小,减少传输量
        image.compressToJpeg(new Rect(0,0, image.getWidth(), image.getHeight()), 80, outStream);
        //...定义缓存大小,转换图片数据流
        byte[] buffer = new byte[1024];
        ByteArrayInputStream bais = new ByteArrayInputStream(outStream.toByteArray());
        try{
            int len;
            DatagramPacket pack;
            //...读取图片数据流,并拆分几次分发出去
            while((len = bais.read(buffer, 0, buffer.length)) != -1) {
                pack = new DatagramPacket(buffer, len, sendAddress);
                mSocket.send(pack);
            }
            //分发完成后,最后发一个结束信息,告诉接收方这一帧图片已发完
            byte[] end = (PACKET_END+sendTime).getBytes();
            pack = new DatagramPacket(end, end.length, sendAddress);
            mSocket.send(pack);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bais.close();
        }
    }

    public void setSendCameraImage(YuvImage image) {
        if (isSending()) {
            return;
        }
        this.image = image;
    }

    public boolean isSending() {
        return isSending;
    }
}
  1. 接下来,剩下的完善细节不再是重点就不讲了,有个清晰的思路就好,需要自己完善一下细节,到最后能将Android项目顺利编译运行起来,接下来,做个实验测试几遍,准备一个WIFI路由器,用一个网线连接上电脑上(或者WIFI连接也可以),只要能登录路由器的控制页面,找到如下图所示,看看是否已取消勾选开启AP隔离,再点保存就可以了,取消AP隔离这样能让局域网的各种设备可互相连通,不需要连接到互联网
    【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第5张图片

小提示

  • 为了安全起见,路由器中不建议对访客开放的WIFI网络中禁用AP隔离哦,
  • 有些路由器中有访客WIFI开关,这个是没有AP隔离可禁用的
  • 没有WIFI路由器的话,可用其中的一个手机开启WIFI热点功能代替,然后安装上面开发的APP,点击开启摄像头按钮就可以了,其它的手机都能扫描到这个摄像头的
  1. 不知不觉发现写了很多,就讲到这里了,关于此Android项目源代码就在这里点此查看,在里面可找到,请放心下载,感谢耐心看完,若觉得此文章很有帮助,请点个赞❤再走,TA远方在此谢过~

【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解_第6张图片

你可能感兴趣的:(#,Android,android,视频监控,摄像头)