基于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来判断这个数据应该放在第二位,在我们收到数据之后将其拼接起来。在显示在屏幕上,就能解决这个问题。
但是若是发生了数据丢失该如何呢,我将在后续对代码进行改进(现在还没写好),敬请期待我的后续博客。