邓大侠 发表于 2020-9-4 15:42

DroidCam客户端

本帖最后由 邓大侠 于 2020-9-4 19:35 编辑

### DroidCamX介绍

DroidCamX是一个可以将手机摄像头作为笔记本摄像头的app,有一个客户端.我通过Wireshark抓包模拟客户端向app发送tcp报文,从而实现展示图像,打开闪光灯,聚焦,限制FPS等功能.

可以在此基础上通过cv2的模型实现了人脸识别等等操作.

因为客户端的不同会导致部分功能的缺失.

在此附上DroidCamX.apk 破解版

### 基于TCP连接的DroidCamX客户端


```python
#!/usr/bin/bin python3
# -*- coding: utf-8 -*-
# @Time    : 2020/8/28 11:31
# @AuThor: SuperDeng
# @Email   : 1821333144@qq.com
# @file    : DroidCam_TCP_Client.py


# 基于TCP协议实现的DroidCam客户端

import sys
import time
import cv2
import socket, threading
import numpy as np
import requests
from tkinter import Radiobutton, IntVar, Button, Tk, messagebox


def bytes2cv(im):
    '''二进制图片转cv2

    :param im: 二进制图片数据,bytes
    :return: cv2图像,numpy.ndarray
    '''
    return cv2.imdecode(np.array(bytearray(im), dtype='uint8'), cv2.IMREAD_UNCHANGED)# 从二进制图片数据中读取


def cv2bytes(im):
    '''cv2转二进制图片

    :param im: cv2图像,numpy.ndarray
    :return: 二进制图片数据,bytes
    '''
    return np.array(cv2.imencode('.png', im)).tobytes()


size_dict = {
    '240x320': '240x320',
    '320x240': '320x240',
    '352x288': '352x288',
    '480x320': '480x320',
    '480x360': '480x360',
    '480x640': '480x640',
    '640x360': '640x360',
    '640x480': '640x480',
    '640x640': '640x640',
    '720x480': '720x480',
    '864x480': '864x480',
    '1280x640': '1280x640',
    '1280x720': '1280x720',
    '1280x960': '1280x960',
    '1920x960': '1920x960',
    '1920x1080': '1920x1080',
}

tcp_func = {
    # 'Limit_FPS': '/cam/1/fpslimit',
    'Autofocus': b'CMD /v1/ctl?8',
    'Toggle_LED': b'CMD /v1/ctl?9',
    'Zoom_In': b'CMD /v1/ctl?7',
    'Zoom_Out': b'CMD /v1/ctl?6',
    # 'Save_Photo_on_SD': '/cam/1/takepic',      # url
    # 音频流 udp 发送至4748服务端
    'Audio': b'CMD /v2/audio',# udp 客户端向4748发送,然后获取字节
    'Stop': b'CMD /v1/stop'# #udp 客户端向4748发送
}

get_url = {
    'Limit_FPS': '/cam/1/fpslimit',
    'Autofocus': '/cam/1/af',
    'Toggle_LED': '/cam/1/led_toggle',
    'Zoom_In': '/cam/1/zoomin',
    'Zoom_Out': '/cam/1/zoomout',
    'Save_Photo_on_SD': '/cam/1/takepic',
}

s = b'\xff\xd8'
e = b'\xff\xd9'


class DroidCam_Client:
    def __init__(self, master):
      self.master = master
      self.master.protocol("WM_DELETE_WINDOW", self.handler)
      self.playEvent = threading.Event()
      self.size = size_dict['640x640']
      self.v = IntVar()
      self.v.set(8)
      self.createWidgets()
      self.PlatState = False

    def set_size(self):
      self.size = list(size_dict.values())

    def createWidgets(self):
      """Build GUI."""

      j = 0
      rr = 0
      cc = -1
      for key in size_dict:
            if cc < 6:
                cc += 1
            else:
                rr += 1
                cc = 0
            Radiobutton(self.master, variable=self.v, text=key, value=j, command=self.set_size).grid(row=rr, column=cc)
            j += 1

      s = len(size_dict) + 1
      self.setup = Button(self.master, width=15, padx=3, pady=3)
      self.setup["text"] = "Play"
      self.setup["command"] = self.Play
      self.setup.grid(row=s, column=0, padx=2, pady=2)

      self.setup = Button(self.master, width=15, padx=3, pady=3)
      self.setup["text"] = "Get_Audio"
      self.setup["command"] = self.Get_Audio
      self.setup.grid(row=s+1, column=0, padx=2, pady=2)

      c = 1
      for func in get_url:
            btn = Button(self.master, width=15, padx=3, pady=3)
            btn["text"] = func
            btn["command"] = getattr(self, func)
            btn.grid(row=s, column=c, padx=2, pady=2)
            c += 1

    def play(self, size, event):
      face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

      eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')

      smile_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_smile.xml')
      sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sk.connect(("192.168.124.3", 4747))
      s1 = 'CMD /v2/video{}?'.format(size)
      print(s1)
      sk.send(s1.encode('utf-8'))
      s = sk.recv(1024)
      jpeg_data = b''
      tmp_data = b''
      while 1:
            msg = sk.recv(1024)
            if e not in msg:
                jpeg_data += msg
            else:
                if msg.endswith(e):
                  jpeg_data += msg
                else:
                  a, b = msg.split(e)
                  jpeg_data = jpeg_data + a + e
                  tmp_data = b
                frame = bytes2cv(jpeg_data)
                faces = face_cascade.detectMultiScale(frame, 1.3, 2)
                img = frame
                for (x, y, w, h) in faces:
                  # 画出人脸框,蓝色,画笔宽度微
                  img = cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
                  # 框选出人脸区域,在人脸区域而不是全图中进行人眼检测,节省计算资源
                  face_area = img

                  ## 人眼检测
                  # 用人眼级联分类器引擎在人脸区域进行人眼识别,返回的eyes为眼睛坐标列表
                  eyes = eye_cascade.detectMultiScale(face_area, 1.3, 10)
                  for (ex, ey, ew, eh) in eyes:
                        # 画出人眼框,绿色,画笔宽度为1
                        cv2.rectangle(face_area, (ex, ey), (ex + ew, ey + eh), (0, 255, 0), 1)

                  ## 微笑检测
                  # 用微笑级联分类器引擎在人脸区域进行人眼识别,返回的eyes为眼睛坐标列表
                  smiles = smile_cascade.detectMultiScale(face_area, scaleFactor=1.16, minNeighbors=65,
                                                            minSize=(25, 25),
                                                            flags=cv2.CASCADE_SCALE_IMAGE)
                  for (ex, ey, ew, eh) in smiles:
                        # 画出微笑框,红色(BGR色彩体系),画笔宽度为1
                        cv2.rectangle(face_area, (ex, ey), (ex + ew, ey + eh), (0, 0, 255), 1)
                        cv2.putText(img, 'Smile', (x, y - 7), 3, 1.2, (0, 0, 255), 2, cv2.LINE_AA)

                # 实时展示效果画面
                cv2.imshow(f"DroidCam{size}", img)
                # 每5毫秒监听一次键盘动作
                if cv2.waitKey(5) & 0xFF == ord('q'):
                  break

                self.s = f"{len(img)}x{len(img)}"
                key = cv2.waitKey(1)
                if key == 27:
                  cv2.destroyWindow(f"DroidCam{size}")
                  event.set()
                  break
                jpeg_data = tmp_data
                tmp_data = b''
            if event.isSet():
                cv2.destroyWindow(f"DroidCam{size}")
                break

    def Play(self):
      if not self.PlatState:
            threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
            self.PlatState = True
      else:
            self.playEvent.set()
            self.OverRide()
            self.playEvent.clear()
            time.sleep(1)
            threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
            self.PlatState = True

    def handler(self):
      """Handler on explicitly closing the GUI window."""
      if messagebox.askokcancel("Quit?", "Are you sure you want to quit?"):
            self.playEvent.set()
            time.sleep(1)
            self.master.destroy()# Close the gui window
            sys.exit(0)
      else:# When the user presses cancel, resume playing.
            return

    def Toggle_LED(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/led_toggle')

    def Limit_FPS(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/fpslimit')

    def Autofocus(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/af')

    def Zoom_In(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/zoomin')

    def Zoom_Out(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/zoomout')

    def OverRide(self):
      ret = requests.get('http://192.168.124.3:4747/override')

    def Save_Photo_on_SD(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/takepic')

    def Get_Audio(self):
      f = open("123.mp3","wb")
      sk = socket.socket(type=socket.SOCK_DGRAM)
      ipaddr = ("192.168.124.3", 4748)
      sk.sendto(b"CMD /v2/audio", ipaddr)
      count = 0
      while 1:
            count += 1
            msg, _ = sk.recvfrom(1024)
            if msg:
                # 解析音频数据
                print(count)
                f.write(msg)
            else:
                break
            if count == 10000: break

      print("exit")
      f.flush()
      f.close()
      sk.close()


def run():
    root = Tk()
    app = DroidCam_Client(root)
    app.master.title("DroidCam_TCP_Client")
    root.mainloop()


if __name__ == '__main__':
    run()
```

### 基于http连接的的DroidCamX客户端

```python
#!/usr/bin/bin python3
# -*- coding: utf-8 -*-
# @Time    : 2020/8/1 19:42
# @Author: SuperDeng
# @Email   : 1821333144@qq.com
# @File    : socket_client.py
# 基于http协议传输的视频数据流播放
import sys
import time
import cv2
import socket, threading
import numpy as np
import requests
from tkinter import Radiobutton, IntVar, Button, Tk, messagebox



def bytes2cv(im):
    '''二进制图片转cv2

    :param im: 二进制图片数据,bytes
    :return: cv2图像,numpy.ndarray
    '''
    return cv2.imdecode(np.array(bytearray(im), dtype='uint8'), cv2.IMREAD_UNCHANGED)# 从二进制图片数据中读取


def cv2bytes(im):
    '''cv2转二进制图片

    :param im: cv2图像,numpy.ndarray
    :return: 二进制图片数据,bytes
    '''
    return np.array(cv2.imencode('.png', im)).tobytes()


size_dict = {
    '240p': '320x240',
    '480p': '640x480',
    '720p': '960x720',
    'FHD 720p': '1280x720',
    'FHD 1080p': '1920x1080',
}


get_url = {
    'Limit_FPS': '/cam/1/fpslimit',
    'Autofocus': '/cam/1/af',
    'Toggle_LED': '/cam/1/led_toggle',
    'Zoom_In': '/cam/1/zoomin',
    'Zoom_Out': '/cam/1/zoomout',
    'Save_Photo_on_SD': '/cam/1/takepic',
}


class DroidCam_Client:
    def __init__(self, master):
      self.master = master
      self.master.protocol("WM_DELETE_WINDOW", self.handler)
      self.playEvent = threading.Event()
      self.size = size_dict['480p']
      self.v = IntVar()
      self.v.set(1)
      self.createWidgets()
      self.PlatState = False

    def set_size(self):
      self.size = list(size_dict.values())

    def createWidgets(self):
      """Build GUI."""

      j = 0
      for key in size_dict:
            Radiobutton(self.master, variable=self.v, text=key, value=j, command=self.set_size).grid()
            j += 1

      self.setup = Button(self.master, width=15, padx=3, pady=3)
      self.setup["text"] = "Play"
      self.setup["command"] = self.Play
      self.setup.grid(row=5, column=0, padx=2, pady=2)

      c = 1
      for func in get_url:
            btn = Button(self.master, width=15, padx=3, pady=3)
            btn["text"] = func
            btn["command"] = getattr(self, func)
            btn.grid(row=5, column=c, padx=2, pady=2)
            c += 1

    def play(self, size, event):
      print(size)
      sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sk.connect(("192.168.124.3", 4747))
      data = 'GET /mjpegfeed?{} HTTP/1.1\r\nHost: 192.168.124.3:4747\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n'.format(
            size)
      sk.send(data.encode('utf-8'))
      msg = sk.recv(1024)
      recv_dict = msg.decode('utf-8').split('\r\n')
      if 'Connection: Keep-Alive' in recv_dict:
            self.__play(sk,size,event)
      else:
            print('DroidCam is connected to another client.')
            print('Disconnect the other client and Take Over')
            sk.close()

    def __play(self,sk,size,event):
      JPEG_header = b''
      JPEG = b''
      while True:
            msg = sk.recv(1024)
            if msg:
                if b'\r\n\r\n' in msg:
                  # 分界点到了
                  JPEG_header += msg.split(b'\r\n\r\n')
                  long = int(JPEG_header.split(b'\r\n')[-1].split(b':')[-1])
                  JPEG_header = b''
                  JPEG += msg.split(b'\r\n\r\n')
                  while True:
                        JPEG += sk.recv(1024)
                        if len(JPEG) >= long:
                            img = bytes2cv(JPEG[:long])
                            cv2.imshow(f"DroidCam{size}", img)
                            cv2.waitKey(1)
                            JPEG_header += JPEG
                            JPEG = b''
                            break
                else:
                  JPEG_header += msg
                if event.isSet():
                  cv2.destroyWindow(f"DroidCam{size}")
                  break
      sk.close()

    def Play(self):
      if not self.PlatState:
            threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
            self.PlatState = True
      else:
            self.playEvent.set()
            self.OverRide()
            self.playEvent.clear()
            time.sleep(1)
            threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
            self.PlatState = True

    def handler(self):
      """Handler on explicitly closing the GUI window."""
      if messagebox.askokcancel("Quit?", "Are you sure you want to quit?"):
            self.playEvent.set()
            time.sleep(1)
            self.master.destroy()# Close the gui window
            sys.exit(0)
      else:# When the user presses cancel, resume playing.
            return

    def Toggle_LED(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/led_toggle')

    def Limit_FPS(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/fpslimit')

    def Autofocus(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/af')

    def Zoom_In(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/zoomin')

    def Zoom_Out(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/zoomout')

    def OverRide(self):
      ret = requests.get('http://192.168.124.3:4747/override')

    def Save_Photo_on_SD(self):
      ret = requests.get('http://192.168.124.3:4747/cam/1/takepic')


def run():
    root = Tk()
    app = DroidCam_Client(root)
    app.master.title("DroidCam_Client")
    root.mainloop()


if __name__ == '__main__':
    run()
```


### 问题

无法实现音频流的解读,我先抛砖引玉,看看诸位大佬有啥办法没.

### 其他应用

手机摄像头,联网,调用百度API人脸识别等等,可以试着手机安装linux deploy装ubuntu系统,模拟成一个树莓派,来进行各种操作.

serafwind 发表于 2020-11-16 15:29

这不是老版本的么?

rsaudio 发表于 2020-12-5 17:49

alick123 发表于 2020-12-16 13:58

试试看 效果怎么样

木子° 发表于 2021-1-29 11:34

楼主有成品嘛。最近也在折腾这个,无奈功力不够

kis8 发表于 2021-2-3 15:18

RE: DroidCam客户端 [修改]

yccyjie 发表于 2022-5-11 09:22

本帖最后由 yccyjie 于 2022-5-11 09:25 编辑

测试成功,多谢提供如此好的范例!!!

lrr0515 发表于 2022-7-7 20:45

希望好用,下载试试

坚如磐石 发表于 2022-7-27 16:33

iriun这个好用

yangyoucai 发表于 2022-8-2 15:08

学习中,非常感谢
页: [1]
查看完整版本: DroidCam客户端