手机端:
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也可以是其他。手机端启动后,运行接收端代码,输入手机端ip和toast提示的端口即可
以下版本需在每个activity的onStart中初始化,context需要传当前activity。
效果图:
基本原理,获取当前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