基于IO流抽象基类的视频传输

本文探讨了一种基于IO流抽象基类实现的TCP/IP视频传输方案,通过Socket套接字在客户端和服务端之间进行数据交换。客户端使用三个线程分别处理Webcam视频显示、图片获取和IO发送,而服务端仅需一个线程接收并显示视频。在数据发送过程中,通过调整发送策略解决了接收不完整的问题,实现了流畅的视频传输。然而,该方案仍需处理网络波动导致的数据乱序和丢失问题,作者计划在未来的工作中进行改进。
摘要由CSDN通过智能技术生成

基于IO流抽象基类的视频传输

1.IO流的体系结构

IO流的非抽象基类流都是在抽象基类的基础上进行优化,封装的。所以我这里尝试了用IO流的抽象基类去实现网络视频传输,自己封装抽象基类。

2.大致思路

 

采用TCP/IP通信协议,通过Socket套接字来实现客户端与服务端之间的连接;

客户端通过Webcam获取摄像头实时图片BufferImage(将图片d绘制在客户端显示器上),通过OutputStream中的write()方法,先发送宽高,在发送图片转换成的二维数组;

服务端方面,在accept之后,通过InputStream中read()方法,先获取宽高,创建对应图片,在获取二维数组,填充图片,最后将图片绘制在服务端显示器上

3.线程分析

客户端

由于客户端要显示Webcam获取的图片(有关Webcam这个jar包可以上网搜索下载导入),还要发送图片,连接时如果添加个按钮还要添加一个监听器线程,所以我决定在客户端用三个线程,分别为:监听器线程;客户端视频显示线程;IO发送线程

这里如果将视频显示线程与IO发送线程合并,即为获取一张图片之后先显示在屏幕上再发送出去,这样如果发送阻塞的话显示也会阻塞;而且这样本身也会增加卡顿:在获取一张图片之后要先显示才能发送,显示的过程也会花费时间。综上,我选择用两个线程来分别完成这两件事。

服务端

服务端负责接收显示就行,所以一个线程即可。

4.类和功能划分

我这里创建了三个类,分别为Client,Server和Function工具类。其中Client类是客户端,Server类为服务端,这两个类都只有一个initUI()方法;Function工具类中我封装了void sendImage(BufferedImage buff, OutputStream os)方法和BufferedImage getImage(InputStream is)方法,本篇的核心内容也在这两个方法里面

5.客户端和服务端GUI界面

这里没什么好说的,上代码

客户端:

public class Client {
​
    Queue<BufferedImage> buffQueue = new PriorityQueue<>();
    Socket client_socket;
    public static BufferedImage b;
​
    public void initUI(){
        JFrame jf = new JFrame("客户端");
        jf.setSize(640,580);
        jf.setLocationRelativeTo(null);
        jf.setDefaultCloseOperation(3);
        jf.setLayout(new FlowLayout());
        JButton j = new JButton("开始");
        JButton s = new JButton("发送视频");
        jf.add(j);
        jf.add(s);
        jf.setVisible(true);
        Graphics g = jf.getGraphics();
        j.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    client_socket = new Socket("127.0.0.1",50000);
                    Webcam webcam = Webcam.getDefault();
                    webcam.setViewSize(new Dimension(640,480));
                    webcam.open();
                    //显示线程
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            while (true){
                                b = webcam.getImage();
                                g.drawImage(b,0,100,null);
​
                            }
                        }
                    }).start();
​
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        s.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //发送线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (true){
                            try {
                                if(b != null){
                                    Function.sendImage(b,client_socket.getOutputStream());
​
​
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
​
                        }
                    }
                }).start();
            }
        });
​
    }
​
    public static void main(String[] args) {
        new Client().initUI();
    }
​
}

服务端:

public class Server {
​
    InputStream is;
​
    public void initUI(){
        JFrame jf = new JFrame("服务端");
        jf.setSize(640,580);
        jf.setLocationRelativeTo(null);
        jf.setDefaultCloseOperation(3);
        jf.setLayout(new FlowLayout());
        JButton j = new JButton("开始");
        jf.add(j);
        jf.setVisible(true);
        Graphics g = jf.getGraphics();
        j.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    ServerSocket server_socket = new ServerSocket(50000);
                    Socket socket = server_socket.accept();
                    is = socket.getInputStream();
                    while(true){
                        BufferedImage b = Function.getImage(is);
                        g.drawImage(b,0,100,null);
                    }
​
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
​
            }
        });
​
    }
​
    public static void main(String[] args) {
        new Server().initUI();
    }
}

6.Function工具类

1.发送一个int数字

IO流的OutputStream中的write()方法每次只能发送一个byte大小的数,而我们获取的图片中的每一个像素点对应的颜色是由A,R,G,B四个部分组成,每个部分的取值范围都是0-255,而BufferImage中的getRGB()方法是返回一个int值,一个int对应四个字节,正好对应ARGB。所以如果我们想发送一个int值,或者想把某个像素点的RGB值发送过去,就要对这个int值进行拆分,拆为四个byte后再进行发送。

public static void sendNum(int value, OutputStream os) throws IOException {
    int b24=(value>>24)&0xFF;
    int b16=(value>>16)&0xFF;
    int b8=(value>>8)&0xFF;
    int b0=(value>>0)&0xFF;
    os.write(b24);
    os.write(b16);
    os.write(b8);
    os.write(b0);
}
​
public static int getNum(InputStream is) throws IOException {
    int b24 = is.read();
    int b16 = is.read();
    int b8 = is.read();
    int b0 = is.read();
    int value = (b24<<24) + (b16<<16) + (b8 << 8) + b0;
    return value;
}

这里的&是按位与,0x代表十六进制数字,FF为255,化为二进制则是 1111 1111正好对应byte的8位。(这里如果还不懂可以去搜一下color的RBG值转换相关内容)

2.情况一:一个个发送

public static void sendImage(BufferedImage buff, OutputStream os) throws IOException{
​
        int w = buff.getWidth();//640
        int h = buff.getHeight();//480
        sendNum(w,os);
        sendNum(h,os);
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                int value = buff.getRGB(i,j);
                sendNum(value,os);
            }
        }
​
​
    }
​
    public static BufferedImage getImage(InputStream is) throws IOException {
        int w = getNum(is);
        int h = getNum(is);
        BufferedImage buff = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
​
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                buff.setRGB(i,j,getNum(is));
            }
        }
        return buff;
    }

这里我们先发送图片的宽高,然后再遍历图片,发送图片每一个像素点的RGB值,即为每次发送32bit(四个字节)的数据,而整张照片中一共有640*480个像素点,发送的次数过多,而发送的量又太少,明显不合理。而实测也是,视频传输十分的卡顿,每三四秒才能传输一张图片,非常难受。

3.情况二:一次发送一张图片

public static void sendImage(BufferedImage buff, OutputStream os) throws IOException{
​
        int w = buff.getWidth();//640
        int h = buff.getHeight();//480
        sendNum(w,os);
        sendNum(h,os);
        byte[] bytes = new byte[w*h*4];
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                int value = buff.getRGB(i,j);
                bytes[4*i*h+4*j] = (byte)((value>>24)&0xFF);
                bytes[4*i*h+4*j+1] = (byte)((value>>16)&0xFF);
                bytes[4*i*h+4*j+2] = (byte)((value>>8)&0xFF);
                bytes[4*i*h+4*j+3] = (byte)((value>>0)&0xFF);
            }
        }
​
        os.write(bytes);
        os.flush();
​
    }
​
    public static BufferedImage getImage(InputStream is) throws IOException {
        int w = getNum(is);
        int h = getNum(is);
        BufferedImage buff = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
        byte[] bytes = new byte[w*h*4];
​
        is.read(bytes);
        
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                buff.setRGB(i,j,((bytes[4*i*h+4*j])<<24) + ((bytes[4*i*h+4*j+1])<<16) +
                        ((bytes[4*i*h+4*j+2]) << 8) + (bytes[4*i*h+4*j+3]));
            }
        }
        return buff;
    }

既然一个个发太慢太耗时,那我不如就把一张图片存在一个byte[]数组中,每次就发送这个数组,够快了吧。

我当初也是这样想的。

然后实测发现,服务端接收的时候只能收到最左端一部分,其他的根本收不到。这里我纳闷了好久,各种DEGUG测试,最后发现当数组长度大于60000多的时候,服务端就接收不到剩下发送的东西了。

4.情况四:一次发送一部分

public static void sendImage(BufferedImage buff, OutputStream os) throws IOException{
​
        int w = buff.getWidth();//640
        int h = buff.getHeight();//480
        sendNum(w,os);
        sendNum(h,os);
        byte[] bytes = new byte[w*h*4];
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                int value = buff.getRGB(i,j);
                bytes[4*i*h+4*j] = (byte)((value>>24)&0xFF);
                bytes[4*i*h+4*j+1] = (byte)((value>>16)&0xFF);
                bytes[4*i*h+4*j+2] = (byte)((value>>8)&0xFF);
                bytes[4*i*h+4*j+3] = (byte)((value>>0)&0xFF);
            }
        }
​
        int size = 60000;
        for (int i = 0; i < bytes.length/size + 1; i++) {
            if((i+1)*size < bytes.length){
                os.write(bytes,i*size,size);
            }else {
                os.write(bytes,i*size, bytes.length-i*size);
            }
        }
        os.flush();
​
    }
​
    public static BufferedImage getImage(InputStream is) throws IOException {
        int w = getNum(is);
        int h = getNum(is);
//        int w=640;
//        int h=480;
        BufferedImage buff = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
        byte[] bytes = new byte[w*h*4];
​
        int size = 60000;
        for (int i = 0; i < bytes.length/size + 1; i++) {
            if((i+1)*size < bytes.length){
                is.read(bytes,i*size,size);
            }else {
                is.read(bytes,i*size, bytes.length-i*size);
            }
        }
​
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                buff.setRGB(i,j,((bytes[4*i*h+4*j])<<24) + ((bytes[4*i*h+4*j+1])<<16) +
                        ((bytes[4*i*h+4*j+2]) << 8) + (bytes[4*i*h+4*j+3]));
            }
        }
        return buff;
    }
int read(byte b[], int off, int len)

上面是read方法的一个重载,off代表放入的开始位置的索引,len代表读取长度

我将一张图片按照size划分,每次只发送size大小的byte数组。

实测极其流畅。我也通过修改size值进行了测试,发现当size很小的时候,例如size=100,就会开始卡顿,这样证明了我上面说的发送次数会影响效率;

最后,我用二分法查找找到了临界值65174,当size=65174时是可以正常运行的,当size=65175时则会产生问题

7.漏洞说明

虽然程序到此出已经能完美流畅运行并且毫无卡顿的传输视频,但是这里涉及了网络编程,当网络产生波动的时候:例如我先发送a再发送b,但是对方却先收到b,再收到了a。如果不对这种情况进行处理则会产生数据无效的bug发生。

解决方案也很简单,每发送一个数据,在这个数据前标记一下这个数据是用来干什么的,放在哪里的。比如我发送1a,再发送2b,对方虽然先收到了2b,但是也能根据2来判断这个数据应该放在第二位,在我们收到数据之后将其拼接起来。在显示在屏幕上,就能解决这个问题。

但是若是发生了数据丢失该如何呢,我将在后续对代码进行改进(现在还没写好),敬请期待我的后续博客。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值