草庐IT

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

TA远方 2023-06-21 原文

闲置在家不用的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,其它的不重要

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展示列表的组件,还有标题栏上的搜索图标,那是扫描按钮

  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文件中,添加如下关键的代码
<?xml version="1.0" encoding="utf-8"?>
<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

💡小提示
有没有注意到,看视频监控上的状态栏,网络保持在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

  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隔离这样能让局域网的各种设备可互相连通,不需要连接到互联网

💡 小提示

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

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

  1. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  2. ruby-on-rails - 关于 Ruby 的一般问题 - 2

    我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

  3. ruby - 我怎样才能更好地了解/了解更多关于 Ruby 的知识? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我最近开始学习Ruby,这是我的第一门编程语言。我对语法感到满意,并且我已经完成了许多只教授相同基础知识的教程。我已经写了一些小程序(包括我自己的数组排序方法,在有人告诉我谷歌“冒泡排序”之前我认为它非常聪明),但我觉得我需要尝试更大更难的东西来理解更多关于Ruby.关于如何执行此操作的任何想法?

  4. ruby - 关于 Ruby 中 Dir[] 和 File.join() 的混淆 - 2

    我在Ruby中遇到了一个关于Dir[]和File.join()的简单程序,blobs_dir='/path/to/dir'Dir[File.join(blobs_dir,"**","*")].eachdo|file|FileUtils.rm_rf(file)ifFile.symlink?(file)我有两个困惑:首先,File.join(@blobs_dir,"**","*")中的第二个和第三个参数是什么意思?其次,Dir[]在Ruby中有什么用?我只知道它等价于Dir.glob(),但是,我对Dir.glob()确实不是很清楚。 最佳答案

  5. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  6. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  7. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  8. 关于Qt程序打包后运行库依赖的常见问题分析及解决方法 - 2

    目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'

  9. ruby - 关于 Ruby/ChefSpec 编码风格的反馈 - 2

    我是Ruby的新手,但过去两周我一直在对Chef测试进行大量研究。该测试使用ChefSpec和Fauxhai,但它看起来不是很“像ruby”,我希望社区能给我一些编码风格的建议。有没有更好的方法来编写这样的嵌套循环?Recipe/foo/recipes/default.rbpackage"foo"doaction:installendRecipe/foo/spec/default_spec.rbrequire'chefspec'describe'foo::default'doplatforms={"debian"=>['6.0.5'],"ubuntu"=>['12.04','10.04

  10. ruby - 关于 ruby​​ 类变量的困惑 - 2

    假设一个使用类变量的简单ruby​​程序,classHolder@@var=99defHolder.var=(val)@@var=valenddefvar@@varendend@@var="toplevelvariable"a=Holder.newputsa.var我猜结果应该是99,但输出不是99。我想知道为什么。由于类变量的范围是类,我假设@@var="toplevelvariable"行不会影响类中的变量。 最佳答案 @@var是Holder的类变量。而顶层的@@var不是Holder的同名类变量@@var,是你在创建类Obj

随机推荐