免root兼容所有安卓屏幕实时多出共享

时间:2021-09-22 22:41:01

手机端:
package com.wanjian.screenshare;import android.app.Activity;import android.app.Application;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Point;import android.os.Build;import android.os.Handler;import android.os.HandlerThread;import android.os.Looper;import android.util.Log;import android.view.View;import android.widget.Toast;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;import java.util.ArrayList;import java.util.List;/** * Created by wanjian on 2016/11/20. */public class RecorderManager {    private static final String TAG = RecorderManager.class.getName();    public static final byte VERSION = 1;    private static RecorderManager sManager;    private Context mContext;    private Handler mCompressHandler;    private List<ClientHandler> mClientHandlers = new ArrayList<>();    private Bitmap mDrawingBoard;    private Canvas mCanvas = new Canvas();    private View rootView;    private Handler mUIHandler = new Handler(Looper.getMainLooper());    private Runnable mDrawTask = new DrawTask();    private Runnable mCompressTask = new CompressTask();    private final int MAX_CLIENT_COUNT = 10;    //    private final float fps = 60f;//    private final int delay = (int) (1000 / fps);    private Socket socket;    public static synchronized RecorderManager getInstance(Context context) {        if (sManager == null) {            sManager = new RecorderManager(context);        }        return sManager;    }    private RecorderManager(Context context) {        this.mContext = context.getApplicationContext();        new HandlerThread("Compress-Thread") {            @Override            protected void onLooperPrepared() {                super.onLooperPrepared();                mCompressHandler = new Handler();            }        }.start();        startListen();    }    private void startListen() {        new Thread() {            @Override            public void run() {                super.run();                ServerSocket serverSocket = null;                for (int i = 8080; i < 65535; i++) {                    try {                        serverSocket = new ServerSocket(i);                        final int port = i;                        mUIHandler.post(new Runnable() {                            @Override                            public void run() {                                Toast.makeText(mContext, "端口: " + port, Toast.LENGTH_SHORT).show();                            }                        });                        break;                    } catch (IOException e) {                    }                }                for (int i = 0; i < MAX_CLIENT_COUNT; ) {                    try {                        socket = serverSocket.accept();                        new HandlerThread("Client-Thread") {                            @Override                            protected void onLooperPrepared() {                                super.onLooperPrepared();                                mClientHandlers.add(new ClientHandler(socket));                            }                        }.start();                        i++;                    } catch (IOException e) {                        return;                    }                }            }        }.start();    }    public void stopRecorder() {        rootView = null;        mUIHandler.removeCallbacks(mDrawTask);        if (mCompressHandler != null) {            mCompressHandler.getLooper().quit();        }        for (ClientHandler clientHandler : mClientHandlers) {            clientHandler.getLooper().quit();        }        try {            socket.close();        } catch (Exception e) {        }        sManager = null;    }    /**     * API14(ICE_CREAM_SANDWICH)及以上版本全局初始化一次即可,context任意,可以是activity也可以是其他。     * 以下版本需在每个activity的onResume中初始化,context需要传当前activity。     *     * @param context API14(ICE_CREAM_SANDWICH)以下传当前activty,其他版本建议传当前activty也可以是任意context     * @param scale   实际传输图像尺寸与手机屏幕比例     */    public void startRecorder(final Context context, float scale) {        Point point = getScreenSize(context);        int exceptW = (int) (point.x * scale);        int exceptH = (int) (point.y * scale);        if (mDrawingBoard == null) {            mDrawingBoard = Bitmap.createBitmap(exceptW, exceptH, Bitmap.Config.RGB_565);        }        if (mDrawingBoard.getWidth() != exceptW || mDrawingBoard.getHeight() != exceptH) {            mDrawingBoard.recycle();            mDrawingBoard = Bitmap.createBitmap(exceptW, exceptH, Bitmap.Config.RGB_565);        }        mCanvas.setBitmap(mDrawingBoard);        mCanvas.scale(scale, scale);        if (context instanceof Activity) {            startRecorderActivity(((Activity) context));        } else {            Toast.makeText(context, "请下拉一下通知栏试试", Toast.LENGTH_SHORT).show();        }        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {            ((Application) context.getApplicationContext()).registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacksAdapter() {                @Override                public void onActivityResumed(Activity activity) {                    startRecorderActivity(activity);                }            });        }    }    private static Point getScreenSize(Context context) {        int w = context.getResources().getDisplayMetrics().widthPixels;        int h = context.getResources().getDisplayMetrics().heightPixels;        return new Point(w, h);    }    private void startRecorderActivity(Activity activity) {        rootView = activity.getWindow().getDecorView();        mUIHandler.removeCallbacks(mDrawTask);        mUIHandler.post(mDrawTask);    }    private class DrawTask implements Runnable {        @Override        public void run() {            if (rootView == null) {                return;            }            mUIHandler.removeCallbacks(mDrawTask);            rootView.draw(mCanvas);            mCompressHandler.removeCallbacks(mCompressTask);            mCompressHandler.post(mCompressTask);        }    }    private class CompressTask implements Runnable {        ByteArrayPool mByteArrayPool = new ByteArrayPool(1024 * 30);        PoolingByteArrayOutputStream mByteArrayOutputStream = new PoolingByteArrayOutputStream(mByteArrayPool);        @Override        public void run() {           try {//动态改变缩放比例时,由于不在该线程,可能导致bitmap被回收               mByteArrayOutputStream.reset();               long s = System.currentTimeMillis();               mDrawingBoard.compress(Bitmap.CompressFormat.JPEG, 60, mByteArrayOutputStream);               byte[] jpgBytes = mByteArrayOutputStream.toByteArray();               Log.d(TAG, "compress " + (System.currentTimeMillis() - s));               for (ClientHandler clientHandler : mClientHandlers) {                   clientHandler.sendData(jpgBytes);               }               mUIHandler.post(mDrawTask);           }catch (Exception e){}//            mUIHandler.postDelayed(mDrawTask, delay);        }    }}


package com.wanjian.screenshare;

import android.os.Handler;
import android.os.Message;
import android.util.Log;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;


/**
* Created by wanjian on 2016/11/20.
*/

public class ClientHandler extends Handler {
private BufferedOutputStream outputStream;
private final int MSG = 1;


private void writeInt(OutputStream outputStream, int v) throws IOException {
outputStream.write(v >> 24);
outputStream.write(v >> 16);
outputStream.write(v >> 8);
outputStream.write(v);
}

public void sendData(byte[] datas) {
removeMessages(MSG);
Message message = obtainMessage();
message.what = MSG;
message.obj = datas;
sendMessage(message);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (outputStream != null) {
try {
byte[] data = (byte[]) msg.obj;
Log.d("RecorderManager", "length : " + data.length);
long s = System.currentTimeMillis();
outputStream.write(RecorderManager.VERSION);
writeInt(outputStream, data.length);
outputStream.write(data);
outputStream.flush();
Log.d("RecorderManager", "write : " + (System.currentTimeMillis() - s));
} catch (IOException e) {
try {
outputStream.close();
} catch (IOException e1) {
}
outputStream = null;
}
}
}

public ClientHandler(Socket socket) {
try {
outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024 * 200);
} catch (IOException e) {
e.printStackTrace();
}

}
}


package com.wanjian.screenshare;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.os.Build;
import android.os.Bundle;

/**
* Created by wanjian on 2016/11/19.
*/

@SuppressLint("NewApi")
public class ActivityLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

}

@Override
public void onActivityStarted(Activity activity) {

}

@Override
public void onActivityResumed(Activity activity) {

}

@Override
public void onActivityPaused(Activity activity) {

}

@Override
public void onActivityStopped(Activity activity) {

}

@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

}

@Override
public void onActivityDestroyed(Activity activity) {

}
}


内存优化类(来自volley):

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.wanjian.screenshare;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
* A variation of {@link ByteArrayOutputStream} that uses a pool of byte[] buffers instead
* of always allocating them fresh, saving on heap churn.
*/
public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
/**
* If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
* the default size to which the underlying byte array is initialized.
*/
private static final int DEFAULT_SIZE = 256;

private final ByteArrayPool mPool;

/**
* Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
* to this instance, the underlying byte array will expand.
*/
public PoolingByteArrayOutputStream(ByteArrayPool pool) {
this(pool, DEFAULT_SIZE);
}

/**
* Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
* more than {@code size} bytes are written to this instance, the underlying byte array will
* expand.
*
* @param size initial size for the underlying byte array. The value will be pinned to a default
* minimum size.
*/
public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
mPool = pool;
buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
}

@Override
public void close() throws IOException {
mPool.returnBuf(buf);
buf = null;
super.close();
}

@Override
public void finalize() {
mPool.returnBuf(buf);
}

/**
* Ensures there is enough space in the buffer for the given number of additional bytes.
*/
private void expand(int i) {
/* Can the buffer handle @i more bytes, if not expand it */
if (count + i <= buf.length) {
return;
}
byte[] newbuf = mPool.getBuf((count + i) * 2);
System.arraycopy(buf, 0, newbuf, 0, count);
mPool.returnBuf(buf);
buf = newbuf;
}

@Override
public synchronized void write(byte[] buffer, int offset, int len) {
expand(len);
super.write(buffer, offset, len);
}

@Override
public synchronized void write(int oneByte) {
expand(1);
super.write(oneByte);
}
}

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.wanjian.screenshare;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

/**
* ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to
* supply those buffers to consumers who need to use them for a short period of time and then
* dispose of them. Simply creating and disposing such buffers in the conventional manner can
* considerable heap churn and garbage collection delays on Android, which lacks good management of
* short-lived heap objects. It may be advantageous to trade off some memory in the form of a
* permanently allocated pool of buffers in order to gain heap performance improvements; that is
* what this class does.
* <p>
* A good candidate user for this class is something like an I/O system that uses large temporary
* <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants
* the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks
* off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into
* account and also to maximize the odds of being able to reuse a recycled buffer, this class is
* free to return buffers larger than the requested size. The caller needs to be able to gracefully
* deal with getting buffers any size over the minimum.
* <p>
* If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
* class will allocate a new buffer and return it.
* <p>
* This class has no special ownership of buffers it creates; the caller is free to take a buffer
* it receives from this pool, use it permanently, and never return it to the pool; additionally,
* it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there
* are no other lingering references to it.
* <p>
* This class ensures that the total size of the buffers in its recycling pool never exceeds a
* certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
* least-recently-used buffers are disposed.
*/
public class ByteArrayPool {
/** The buffer pool, arranged both by last use and by buffer size */
private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);

/** The total size of the buffers in the pool */
private int mCurrentSize = 0;

/**
* The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
* under this limit.
*/
private final int mSizeLimit;

/** Compares buffers by size */
protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
@Override
public int compare(byte[] lhs, byte[] rhs) {
return lhs.length - rhs.length;
}
};

/**
* @param sizeLimit the maximum size of the pool, in bytes
*/
public ByteArrayPool(int sizeLimit) {
mSizeLimit = sizeLimit;
}

/**
* Returns a buffer from the pool if one is available in the requested size, or allocates a new
* one if a pooled one is not available.
*
* @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
* larger.
* @return a byte[] buffer is always returned.
*/
public synchronized byte[] getBuf(int len) {
for (int i = 0; i < mBuffersBySize.size(); i++) {
byte[] buf = mBuffersBySize.get(i);
if (buf.length >= len) {
mCurrentSize -= buf.length;
mBuffersBySize.remove(i);
mBuffersByLastUse.remove(buf);
return buf;
}
}
return new byte[len];
}

/**
* Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
* size.
*
* @param buf the buffer to return to the pool.
*/
public synchronized void returnBuf(byte[] buf) {
if (buf == null || buf.length > mSizeLimit) {
return;
}
mBuffersByLastUse.add(buf);
int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
if (pos < 0) {
pos = -pos - 1;
}
mBuffersBySize.add(pos, buf);
mCurrentSize += buf.length;
trim();
}

/**
* Removes buffers from the pool until it is under its size limit.
*/
private synchronized void trim() {
while (mCurrentSize > mSizeLimit) {
byte[] buf = mBuffersByLastUse.remove(0);
mBuffersBySize.remove(buf);
mCurrentSize -= buf.length;
}
}

}



接收端:

package com.example;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;

import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

public class MyClass extends JFrame {
JLabel label;
public MyClass() throws IOException {
setLayout(new BorderLayout(0, 0));

JPanel ipPanel = new JPanel(new BorderLayout(5, 5));
final JTextField ipField = new JTextField();
ipPanel.add(ipField, BorderLayout.CENTER);
ipPanel.setBorder(new EmptyBorder(5, 5, 5, 5));

JPanel portPanel = new JPanel(new BorderLayout(5, 5));
final JTextField portField = new JTextField();
portPanel.add(portField, BorderLayout.CENTER);
portPanel.setBorder(new EmptyBorder(5, 5, 5, 5));

JPanel btnPanel = new JPanel(new BorderLayout(5, 5));
JButton btn = new JButton("链接");
btnPanel.add(btn, BorderLayout.CENTER);


JPanel panelContainer = new JPanel(new BorderLayout());
panelContainer.add(ipPanel, BorderLayout.NORTH);
panelContainer.add(portPanel, BorderLayout.CENTER);
panelContainer.add(btnPanel, BorderLayout.SOUTH);


JPanel panelContainer2 = new JPanel(new BorderLayout());
panelContainer2.add(panelContainer, BorderLayout.NORTH);

label = new JLabel();

// Image image = ImageIO.read(new File("/Users/wanjian/Desktop/img.jpg"));
// label.setIcon(new ImageIcon(image));
label.setBorder(new EmptyBorder(5, 5, 5, 5));
add(panelContainer2, BorderLayout.NORTH);

add(label, BorderLayout.CENTER);

setDefaultCloseOperation(HIDE_ON_CLOSE);
setBounds(360, 20, 350, 600);

setTitle("屏幕共享 by 万剑");
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
try {
read(ipField.getText(), portField.getText());
} catch (IOException e1) {
e1.printStackTrace();
}
}

});


// label.addMouseListener(new MouseAdapter() {
// });
}

private void read(final String ip, final String port) throws IOException {
new Thread() {
@Override
public void run() {
super.run();
try {
Socket socket = new Socket(ip, Integer.parseInt(port));
BufferedInputStream inputStream = new BufferedInputStream(socket.getInputStream());
byte[] bytes = null;
while (true) {
long s1=System.currentTimeMillis();
int version=inputStream.read();
if (version==-1){
return;
}
int length = readInt(inputStream);
if (bytes == null) {
bytes = new byte[length];
}
if (bytes.length < length) {
bytes = new byte[length];
}
int read = 0;
while ((read < length)) {
read += inputStream.read(bytes, read, length - read);
}
InputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
long s2=System.currentTimeMillis();
Image image = ImageIO.read(byteArrayInputStream);
label.setIcon(new ScaleIcon(new ImageIcon(image)));
long s3=System.currentTimeMillis();

System.out.println("读取: "+(s2-s1)+" 解码: "+(s3-s2)+" "+length);
}

} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

}

private int readInt(InputStream inputStream) throws IOException {
int b1 = inputStream.read();
int b2 = inputStream.read();
int b3 = inputStream.read();
int b4 = inputStream.read();

return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
}

public static void main(String[] args) throws IOException {
new MyClass().setVisible(true);

}
}



使用:

手机端接入代码,调用

 RecorderManager.getInstance(MainActivity.this)
.startRecorder(MainActivity.this, 0.5f);
启动。

 API14(ICE_CREAM_SANDWICH)及以上版本全局初始化一次即可,context任意,可以是activity也可以是其他。
以下版本需在每个activity的onStart中初始化,context需要传当前activity。

手机端启动后,运行接收端代码,输入手机端ip和toast提示的端口即可


效果图:

免root兼容所有安卓屏幕实时多出共享


基本原理,获取当前activity的跟view,调用view.draw(new Canvas(bitmap)),把当前屏幕内容画在bitmap上,然后把bitmap压缩成jpg通过socket传输。

传输协议:

前一个字节是版本

接着4个字节是jpg帧的大小

然后是jpg帧


在mx5,scale是0.5时,当传输速度达到700KB/S时非常流畅


优点 :

免root,兼容所有安卓版本

缺点:

只能录制activity的布局,对话框等没法录制


PS:

也可以借助android5.0新增的录屏api来录制屏幕上所有内容,不限于当前app,但最低兼容android5.0


效果视频  http://weibo.com/tv/v/EiVqmylRT?fid=1034:a8982aaff233b975399ce64fa636ee52