继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

安卓USB通信,PC端预览手机摄像头并控制手机拍照

潇潇雨雨
关注TA
已关注
手记 165
粉丝 25
获赞 128

功能需求:

1 手机端打开摄像头实时预览;
2 手机端作为服务端,PC端作为客户端连接;
3 连接成功后PC端可以同时预览手机端的摄像头采集的图像;
4 PC端点击拍照可以控制手机端拍摄一张照片,并将照片传给PC端。

功能模块:

1 安卓手机打开摄像头并实现预览和拍照功能;
2 手机端开启监听,并在连接成功后将摄像头采集的数据传给PC;
3 手机端读取PC发送的命令指令,执行相应的操作。

(一)开启摄像头实现预览

(1) 获取摄像头权限,并添加自动对焦属性

在应用程序的manifest.xml中添加

 <uses-permission android:name="android.permission.CAMERA" />
 <uses-feature android:name="android.hardware.camera" />  
 <uses-feature android:name="android.hardware.camera.autofocus" />
(2) 实现预览

安卓系统使用SurfaceView即可完成预览功能,使用方式如下:
activity_main.xml布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/gray_light"android:orientation="vertical" ><SurfaceView
    android:id="@+id/surview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:scaleType="fitCenter" /></LinearLayout>

在MainActivity 的onCreate中初始化SurfceView。初始化方法如下:

private void initSurfaceView() {
    surfaceView = (SurfaceView) findViewById(R.id.surview);
    surfaceHolder = surfaceView.getHolder();
    surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    surfaceHolder.setKeepScreenOn(true);
    surfaceHolder.addCallback(new Callback() {        @Override
        public void surfaceDestroyed(SurfaceHolder arg0) {

        }        @Override
        public void surfaceCreated(SurfaceHolder arg0) {            // 开启摄像头
            startCamera(curCameraIndex);
        }        @Override
        public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,                int arg3) {

        }
    });
    surfaceView.setFocusable(true);
    surfaceView.setBackgroundColor(TRIM_MEMORY_BACKGROUND);

}

当SurfceView创建时开启摄像头,开启方法如下:

// 根据索引初始化摄像头@SuppressLint("NewApi")public void startCamera(int cameraIndex) {    // 先停止摄像头
    stopCamera();    // 再初始化并打开摄像头
    if (camera == null) {
        camera = Camera.open(cameraIndex);
        cameraUtil = new CameraUtil(camera, callback);
        cameraUtil.initCamera(srcFrameHeight, srcFrameWidth, surfaceHolder);
        Log.e(TAG, "打开相机");
    }
}// 停止并释放摄像头public void stopCamera() {    if (camera != null) {
        camera.setPreviewCallback(null);
        camera.stopPreview();
        camera.release();
        camera = null;
    }
}//摄像头开启预览后采集到的数据回调接口PreviewCallback callback = new PreviewCallback() {    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Size size = camera.getParameters().getPreviewSize();        try {            if (times == 0) {
                YuvImage image = new YuvImage(data, ImageFormat.NV21,
                        size.width, size.height, null);                if (image != null) {                    // 将YuvImage对象转为字节数组
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    image.compressToJpeg(new Rect(0, 0, size.width,
                            size.height), 100, outputStream);                    byte[] srcData = outputStream.toByteArray();                    int len = srcData.length;                    
                    // 字节数组转为Bitmap
                    Bitmap src = BitmapFactory.decodeByteArray(srcData, 0,
                            len);
                    src = BitmapUtil.rotate(src, 90);                    // 压缩Bitmap,并获取压缩后的字节数组,即可获取预览数据文件
                    // outdata数据即是待发送给PC端的数据
                    byte[] outdata = BitmapUtil.transImage(src,
                            srcFrameWidth / 4, srcFrameHeight / 4);                    int datalen = outdata.length;                    if (isOpen) {                        // 写入头
                        sendData(new byte[] { (byte) 0xA0 });                        // 写入数组长度
                        sendData(intToByteArray(datalen));                        // 写入数据值
                        sendData(outdata);
                    }                    // 回收Bitmap
                    if (!src.isRecycled()) {
                        src.recycle();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

CameraUtil为设置摄像头的辅助类,代码如下:

import java.io.IOException;import android.annotation.SuppressLint;import android.graphics.ImageFormat;import android.hardware.Camera;import android.hardware.Camera.AutoFocusCallback;import android.hardware.Camera.PreviewCallback;import android.view.SurfaceHolder;public class CameraUtil {
    Camera camera;    int cameraIndex;    int srcFrameWidth;    int srcFrameHeight;
    SurfaceHolder surfaceHolder;
    PreviewCallback callback;    public CameraUtil(Camera camera, PreviewCallback callback) {        this.camera = camera;        this.callback = callback;
    }    public void initCamera(final int srcFrameWidth, final int srcFrameHeight, final SurfaceHolder surfaceHolder) {    
        this.srcFrameHeight = srcFrameHeight;        this.srcFrameWidth = srcFrameWidth;        this.surfaceHolder = surfaceHolder;
        Camera.Parameters params = camera.getParameters();        //params.setPreviewSize(srcFrameWidth / 4, srcFrameHeight / 4);
        params.setPreviewFormat(ImageFormat.NV21);
        params.setPreviewFrameRate(30);
        params.setJpegQuality(100);
        params.setPictureFormat(ImageFormat.JPEG);
        params.set("orientation", "portrait");
        params.set("rotation", 90);
        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 1连续对焦
        camera.setParameters(params);
        camera.setDisplayOrientation(90);        // 设置显示图像的SurfaceView
        try {
            camera.setPreviewDisplay(surfaceHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }
        camera.setPreviewCallback(callback);
        camera.startPreview();
        camera.autoFocus(new AutoFocusCallback() {        @Override
        public void onAutoFocus(boolean result, Camera camera) {            
            // 自动对焦完成时回调
            if (result) {
                    initCamera(srcFrameWidth, srcFrameHeight, surfaceHolder);
                    camera.cancelAutoFocus();
                }
            }
        });
    }    @SuppressLint("NewApi") 
    public void startCamera(int cameraIndex) {    
        this.cameraIndex = cameraIndex;        // 先停止摄像头
        stopCamera();        // 再初始化并打开摄像头
        if (camera == null) {
            camera = Camera.open(cameraIndex);
            initCamera(srcFrameWidth, srcFrameHeight, surfaceHolder);       
        }
    }    public void stopCamera() {        if (camera != null) {
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }
}

BitmapUtil 为图片操作辅助类

import java.io.ByteArrayOutputStream;import android.graphics.Bitmap;import android.graphics.Matrix;import android.graphics.Bitmap.CompressFormat;public class BitmapUtil {// Bitmap按照一定大小转为字节数组,以便写入socket进行发送
    public static byte[] transImage(Bitmap bitmap, int width, int height) {        // bitmap = adjustPhotoRotation(bitmap, 90);
        try {            int bitmapWidth = bitmap.getWidth();            int bitmapHeight = bitmap.getHeight();            float scaleWidth = (float) width / bitmapWidth;            float scaleHeight = (float) height / bitmapHeight;
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleHeight);            // 创建压缩后的Bitmap
            Bitmap resizeBitemp = Bitmap.createBitmap(bitmap, 0, 0,
                bitmapWidth, bitmapHeight, matrix, false);        
            // 压缩图片质量
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            resizeBitemp.compress(CompressFormat.JPEG, 80, outputStream);            // 转为字节数组
            byte[] byteArray = outputStream.toByteArray();
            outputStream.close();            // 回收资源
            if (!bitmap.isRecycled()) {
                bitmap.recycle();
            }            if (!resizeBitemp.isRecycled()) {
                resizeBitemp.recycle();
            }            return byteArray;

        } catch (Exception ex) {
            ex.printStackTrace();
        }        return null;
    }    public static Bitmap rotate(Bitmap bitmap, float degree) {
        Matrix matrix = new Matrix();       // matrix.setScale(0.5f, 0.5f);// 缩小为原来的一半
        matrix.postRotate(degree);// 旋转45度 == matrix.setSinCos(0.5f, 0.5f);
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
        bitmap.getHeight(), matrix, true);        return bitmap;
    }
}

(二) 连接功能实现

(1) onCreate中注册广播的监听
    IntentFilter filter = new IntentFilter();
    filter.addAction("NotifyServiceStart");
    filter.addAction("NotifyServiceStop");
    registerReceiver(receiver, filter);
(2) 接收系统广播
public class ServiceBroadcastReceiver extends BroadcastReceiver {    private static final String START_ACTION = "NotifyServiceStart";    private static final String STOP_ACTION = "NotifyServiceStop";    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();        if (START_ACTION.equalsIgnoreCase(action)) {            // 启动服务
            Log.e(TAG, "收到广播信息启动监听");            new Thread() {                public void run() {                    if (serverSocket != null) {                        try {
                            serverSocket.close();
                        } catch (IOException e) {

                            e.printStackTrace();
                        }
                    }                    //服务端启动监听
                    doListen();
                };
            }.start();

        } else if (STOP_ACTION.equalsIgnoreCase(action)) {

        }
    }
}

启动监听的代码:

    // 启动服务器端监听
    private void doListen() {
        serverSocket = null;        try {
            serverSocket = new ServerSocket(SERVER_PORT);            while (true) {
                Socket socket = serverSocket.accept();
                Log.e(TAG, "监听到设备连接,启动通信线程");
                threadSocket = new ThreadReadWriterIOSocket(socket);                new Thread(threadSocket).start();
            }
        } catch (IOException e) {
            Log.e(TAG, "服务端监听失败");
            e.printStackTrace();
        }
    }

ThreadReadWriterIOSocket为负责通信的线程,代码如下:

import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.IOException;import java.io.InputStream;import java.net.Socket;import org.greenrobot.eventbus.EventBus;import android.util.Log;import com.myn.usb.bean.MessageEvent;import com.myn.usb.utils.Constant;/**
 * 数据读写线程
 * 
 * @author Administrator
     * 
 */public class ThreadReadWriterIOSocket implements Runnable {    private static String TAG = "ThreadReadWriterIOSocket";    private Socket client;    private BufferedOutputStream out;    private BufferedInputStream in;    boolean isConnecting = false;    private String cmd = "";    public ThreadReadWriterIOSocket(Socket client) {        this.client = client;
    }    @Override
    public void run() {
    
        Log.e(TAG, "有客户端连接上");
        isConnecting = true;        try {            // 获取输入输出流
            out = new BufferedOutputStream(client.getOutputStream());
            in  = new BufferedInputStream(client.getInputStream());            // 循环等待,接受PC端的命令
            while (isConnecting) {                try {                    if (!client.isConnected()) {                        break;
                    }                    // 读取命令
                    cmd = readCMDFromSocket(in);    
                    Log.e(TAG, "读取到PC发送的命令" + cmd);                    /* 根据命令分别处理数据 */
                    if (cmd.equals(Constant.CONNECT)) {// 收到连接命令
                        EventBus.getDefault().post(new MessageEvent(Constant.START));
                        out.flush();
                    } else if (cmd.equalsIgnoreCase(Constant.DISCONNECT)) {// 断开命令
                        EventBus.getDefault().post(new MessageEvent(Constant.STOP));
                        out.flush();
                    }else if (cmd.equals(Const    ant.TAKEPHOTO)) {
                        EventBus.getDefault().post(new MessageEvent(Constant.TAKEPHOTO));
                        out.flush();
                    }
                    in.reset();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            out.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }    public void cancel() {
        isConnecting = false;
    }    public void writeData(byte[] data) {        if (out != null) {            try {
                out.write((data));
            } catch (IOException e) {
                Log.e(TAG, "输入输出异常");
                e.printStackTrace();
            }
        }
    }    /* 读取命令 */
    public String readCMDFromSocket(InputStream in) {        int MAX_BUFFER_BYTES = 2048;
        String msg = "";        byte[] tempbuffer = new byte[MAX_BUFFER_BYTES];        try {            int numReadedBytes = in.read(tempbuffer, 0, tempbuffer.length);
            msg = new String(tempbuffer, 0, numReadedBytes, "utf-8");
            tempbuffer = null;
        } catch (Exception e) {
            Log.e(TAG, "readCMDFromSocket读数异常" + e.toString());
            EventBus.getDefault().post(new MessageEvent(Constant.DISCONNECT));
            e.printStackTrace();
        }        return msg;
    }
}
(3) 传递命令信息

通信的通知过程采用EventBus实现
onCreate中注册EventBus

EventBus.getDefault().register(this);

实现事件处理函数

@Subscribe(threadMode = ThreadMode.MAIN)public void onMessageEvent(MessageEvent event) {    switch (event.message) {    case Constant.DISCONNECT:

        Toast.makeText(MainActivity.this, "客户端断开", Toast.LENGTH_LONG)
                .show();
        threadSocket.cancel();    case Constant.START://收到连接命令

        isOpen = true;
        Toast.makeText(MainActivity.this, "客户端连接上", Toast.LENGTH_LONG)
                .show();
        startCamera(curCameraIndex);        break;    case Constant.STOP://收到断开命令

        stopCamera();
        isOpen = false;        break;    case Constant.TAKEPHOTO://收到拍照命令

        if (isOpen) {
            camera.takePicture(new ShutterCallback() {                @Override
                public void onShutter() {

                }
            }, new Camera.PictureCallback() {                @Override
                public void onPictureTaken(byte[] arg0, Camera arg1) {

                }
            }, new Camera.PictureCallback() {                @Override
                public void onPictureTaken(byte[] data, Camera camera) {                    // 向电脑端发送数据
                    int datalen = data.length;                    if (isOpen) {                        // 写入头
                        sendData(new byte[] { (byte) 0xA1 });                        // 写入数组长度
                        sendData(intToByteArray(datalen));                        // 写入数据值
                        sendData(data);
                    }                    // 重新浏览
                    camera.stopPreview();
                    camera.startPreview();
                }
            });

        }

    }
}

当建立连接之后可以向PC端发送数据,发送数据的方法:

public void sendData(final byte[] data) {
    threadSocket.writeData(data);
}

PC端程序代码

PC端作为客户端需要向服务端发起连接。

主函数:

 public class Main {    private static Socket socket;    private static OutputStream out;    private static InputStream in;    public  static MyFrame frame;    private static String CMD = "";    public static void main(String[] args) {
    
        frame = new MyFrame();

        JPanel panel2 = new JPanel();
        panel2.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 0));        final JButton button = new JButton("连接");
        panel2.add(button);
        button.addActionListener(new ActionListener() {            @Override
        public void actionPerformed(ActionEvent arg0) {            new Thread() {                public void run() {
                        onConnect();
                    };
                }.start();
                button.setEnabled(false);
            }
        });

        JButton button2 = new JButton("断开");
        button2.addActionListener(new ActionListener() {            @Override
            public void actionPerformed(ActionEvent arg0) {

                CMD = Constant.DISCONNECT;                try {
                    out.flush();
                    out.write(CMD.getBytes());
                    out.flush();                
                } catch (IOException e) {
                
                    e.printStackTrace();
                }
                button.setEnabled(true);
            }
        });
        panel2.add(button2);
    
        JButton button3 = new JButton("拍照");
        button3.addActionListener(new ActionListener() {            @Override
            public void actionPerformed(ActionEvent arg0) {
                CMD = Constant.TAKEPHOTO;                try {
                    out.flush();
                    out.write(CMD.getBytes());
                    out.flush();                    
                } catch (IOException e) {   
                    e.printStackTrace();
                }           
            }
        });
        panel2.add(button3);

        frame.add(panel2, BorderLayout.SOUTH);
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }    //启动连接时使用adb发送系统广播
    protected static void onConnect() {        try {        // adb 指令
            Runtime.getRuntime().exec(                "adb shell am broadcast -a NotifyServiceStop");
            Thread.sleep(2000);
            Runtime.getRuntime().exec("adb forward tcp:12580 tcp:10086"); // 端口转换
            Thread.sleep(2000);
            Runtime.getRuntime().exec(                "adb shell am broadcast -a NotifyServiceStart");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }        int len = 0;        int degree = 0;        try {
            InetAddress serveraddr = null;
            serveraddr = InetAddress.getByName("127.0.0.1");
            System.out.println("TCP 1 " + "正在连接。。。。");
            socket = new Socket(serveraddr, 12580);
            System.out.println("TCP 2 " + "连接成功 ");            // 获取输入输出流
            out = new BufferedOutputStream(socket.getOutputStream());
            in  = new BufferedInputStream(socket.getInputStream());            // 发送连接命令
            CMD = Constant.CONNECT;
            out.write(CMD.getBytes());
            out.flush();            boolean flag = true;    
            if (socket.isConnected()) {                while (flag) {                    try {                        int head = in.read();                    
                        if (head == 0xA0) {                            byte[] src = new byte[4];
                            len = in.read(src);
                            len = byteArrayToInt(src);                            if (len > 0) {                                int cclen = 0;                                byte[] srcData = new byte[len];                                while (cclen < len) {                                    int readlen = in.read(srcData, cclen, len
                                        - cclen);
                                    cclen += readlen;
                                }
                                ImageIcon icon = new ImageIcon(srcData);                            if (icon != null) {
                                frame.setIcon(icon, degree);
                            }
                        }
                        }else if (head == 0xA1) {                            byte[] src = new byte[4];
                            len = in.read(src);
                            len = byteArrayToInt(src);                            if (len > 0) {                                int cclen = 0;                                byte[] srcData = new byte[len];                                while (cclen < len) {                                    int readlen = in.read(srcData, cclen, len
                                        - cclen);
                                    cclen += readlen;
                                }                            
                                //保存图片文件
                                File file = new File("D:\\images\\" + System.currentTimeMillis() + ".jpg");                                if (!file.exists()) {
                                    file.mkdirs();
                                }
                                                        
                                ImageIcon icon = new ImageIcon(srcData);
                                Image image = icon.getImage();
                                BufferedImage bi = new BufferedImage(image.getWidth(null),image.getHeight(null),BufferedImage.TYPE_INT_RGB);
                                Graphics2D g2d = bi.createGraphics();
                                g2d.drawImage(image, 0, 0, null);
                                g2d.dispose();
                                ImageIO.write(bi,"jpg",file);
                            }
                        }
                    
                    } catch (Exception e) {
                    
                    }

                }
            }
        } catch (Exception e1) {
            System.out.println(" 连接出现异常:连接失败: " + e1.toString());
        }
    }// 读取数据线程,当点击连接时启动class ReadThread extends Thread {
    InputStream in;    boolean isReading = true;    public ReadThread(InputStream inputStream) {        this.in = inputStream;
    }    @Override
    public void run() {        int len = 0;        int degree = 0;        while (isReading) {            try {                    if (in.read() == 0xA0) {                        byte[] src = new byte[4];
                        len = in.read(src);
                        len = byteArrayToInt(src);
                    }                    // System.out.println("数据长度" + len);
                    if (len > 0) {                        int cclen = 0;                        byte[] srcData = new byte[len];                        while (cclen < len) {                            int readlen = in.read(srcData, cclen, len - cclen);
                            cclen += readlen;
                        }
                        ImageIcon icon = new ImageIcon(srcData);                        if (icon != null) {
                            frame.setIcon(icon, degree);
                        }
                    }
                } catch (Exception e) {
                    isReading = false;
                }
            }
        }        public void cancel() {
            isReading = false;
        }
    }    public static int byteArrayToInt(byte[] b) {        return b[3] & 0xFF | (b[2] & 0xFF) << 8 | (b[1] & 0xFF) << 16
            | (b[0] & 0xFF) << 24;
    }
}

MyFrame类

public class MyFrame extends JFrame {    private static final long serialVersionUID = 1L;    public MyPanel panel;    public MyFrame() {        // 默认的窗体名称
        this.setTitle("USB连接显示");        // 获得面板的实例
        panel = new MyPanel();        this.add(panel);    
        this.pack();        this.setVisible(true);
    }    //设置图片
    public void setIcon(ImageIcon incon, int drgree) {  
        panel.setImage(incon, drgree);
    }
}

MyPanel类

public class MyPanel extends Panel {    private static final long serialVersionUID = 1L;    private Image bufferImage;    private final Image screenImage = new BufferedImage(800, 600, 2);    private final Graphics2D screenGraphic = (Graphics2D) screenImage
        .getGraphics();    int degree = 90;    private Image backgroundImage = screenImage;    public MyPanel() {        // 设定焦点在本窗体
        setFocusable(true);        // 设定初始构造时面板大小,这里先采用图片的大小
        setPreferredSize(new Dimension(800, 600));        // 绘制背景
        drawView();
    }    public void setImage(ImageIcon icon, int degree) {        //System.out.println("设置图片   偏转角度" + degree);
        this.degree = degree;
        backgroundImage = icon.getImage();
        drawView();
        repaint();
    }    private void drawView() {        int width  = getWidth();        int height = getHeight();    
        int x = width / 2 - backgroundImage.getWidth(null) /2;        int y = height / 2 - backgroundImage.getHeight(null) / 2;
        screenGraphic.drawImage(backgroundImage, x , y, null);
    }    @Override
    public void update(Graphics g) {    
        if (bufferImage == null) {
            bufferImage = this.createImage(this.getWidth(), this.getHeight());
        }    
        //bufferImage = rotateImage(bufferimage, degree);
    
        Graphics gBuffer = bufferImage.getGraphics();// 获得图片缓冲区的画笔
        if (gBuffer != null){
            paint(gBuffer);
        }        else{
            paint(g);
        }
        gBuffer.dispose();
        g.drawImage(bufferImage, 0, 0, null);
    }    public void paint(Graphics g) {
    
        g.drawImage(screenImage, 0, 0, null);
    }    public static BufferedImage rotateImage(final BufferedImage bufferedimage,        final int degree) {        int w = bufferedimage.getWidth();        int h = bufferedimage.getHeight();        int type = bufferedimage.getColorModel().getTransparency();
        BufferedImage img;
        Graphics2D graphics2d;
        (graphics2d = (img = new BufferedImage(w, h, type))
            .createGraphics()).setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        graphics2d.rotate(Math.toRadians(degree), w / 2, h / 2);
        graphics2d.drawImage(bufferedimage, 0, 0, null);
        graphics2d.dispose();        return img;
    }
}

两个工程均用到的Contant类

public class Constant {    public static final String addrIp = "192.168.0.119";    public static final int addrPort = 56168;    public static final String CONNECT = "CONNECT";    public static final String DISCONNECT = "DISCONNECT";    public static final String TAKEPHOTO = "TAKEPHOTO";    public static final String START = "START";    public static final String STOP = "STOP";
}

上述代码只是简单实现了手机端与PC端通过USB线的数据传输,数据的通信过程与Socket一致。
源码下载



作者:进击的Hello_World
链接:https://www.jianshu.com/p/42fa9c49bb41

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP