前面实现了本地录制屏幕编码保存本机播放和录制手机音频数据保存本机播放。
把录制的屏幕数据和声音数据通过局域网传输到另一台Android
设备上实时解码显示就可以实现一个简单的局域网镜像功能。
镜像的实现是由一个发送端一个接收端共同完成的,接下来用phone
代指发送端,tv
代指接收端。
一、tv端启动镜像服务
由于这里仅是一个简单的demo
,没有实现局域网内设备发现,有兴趣的同学可以自行通过mDns
或UPnP
实现局域网内设备发现。
这里在tv
端生成一个带有镜像服务信息的二维码,phone
通过扫码获取tv
镜像服务信息。
1、生成局域网内的http服务
借助nanohttpd
可以生成一个可在局域网内访问的http
服务,将这个服务作为镜像服务的主服务端口,通过这个主服务,phone
可获取tv
端详细的镜像服务信息。
集成nanohttpd
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
implementation 'org.nanohttpd:nanohttpd:2.2.0'
}
处理http消息
public class TVServer extends NanoHTTPD {
private static final String TAG = "LelinkHTTPD";
private Context mContext;
TVServer(Context context, int port) {
super(port);
this.mContext = context;
}
@Override
public Response serve(IHTTPSession session) {
Logger.i(TAG, "url:" + session.getUri());
Method method = session.getMethod();
if (method.equals(Method.GET)) {
// get请求
return handleGetRequest(session);
} else if (method.equals(Method.POST)) {
// post请求
return handlePostRequest(session);
}
return super.serve(session);
}
private Response handleGetRequest(IHTTPSession session) {
String uri = session.getUri();
if (uri.endsWith("/startMirror")) {
Map params = session.getParms();
String decoder = params.get("decoder");
if (!TextUtils.isEmpty(decoder)) {
Config.getInstance().setDecoderType(decoder);
}
MirrorRender.getInstance().startAndroidReceiver();
int videoPort = MirrorRender.getInstance().getAndroidVideoPort();
int audioPort = MirrorRender.getInstance().getAndroidAudioPort();
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("videoPort", videoPort);
jsonObject.put("audioPort", audioPort);
Logger.i(TAG,"startMirror " + jsonObject.toString());
ByteArrayInputStream stream = new ByteArrayInputStream(jsonObject.toString().getBytes("UTF-8"));
Response response = newChunkedResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, stream);
response.addHeader("Access-Control-Allow-Origin", "*");
return response;
} catch (Exception e) {
Logger.w(TAG, e);
}
} else if (uri.endsWith("/stopMirror")) {
Logger.i(TAG, "handleGetRequest stopMirror");
Activity mirrorActivity = Config.getInstance().getMirrorActivity();
if (mirrorActivity != null) {
mirrorActivity.finish();
}
MirrorRender.getInstance().stopRender();
return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, Response.Status.OK.getDescription());
}
return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
}
private Response handlePostRequest(IHTTPSession session) {
Logger.i(TAG, "handlePostRequest");
return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
}
这里处理了两条请求,startMirror
stopMirror
,phone
可通过
http://ip:port/startMirror
http://ip:port/stopMirror
对tv端请求。
在startMirror
中,通过MirrorRender
获取到了用于接收镜像数据流的两个服务端口,phone
可通过获取到的这两个端口分别传输视频流数据和音频流数据到tv
端。
2、视频流服务
由于视频流不能丢失,所以这里选择tcp
的方式进行传输,优先保证其稳定性。
public class AndroidVideoReceiver {
private final static String TAG = "AndroidVideoReceiver";
private MiniServerSocket mSocket;
private OnReceiverListener mServerListener;
private boolean isStop = false;
private long mDataCount = 0;
private long mFrameCount = 0;
public void startReceiver() {
try {
mSocket = new MiniServerSocket(0);
} catch (Exception e) {
Logger.w(TAG, e);
stopReceiver();
return;
}
Logger.i(TAG, "startMirror ");
if (mServerListener != null) {
mServerListener.onReceiveStarted(OnReceiverListener.ANDROID_VIDEO);
}
receiveVideo();
}
private Thread mVideoThread;
private void receiveVideo() {
mVideoThread = new Thread(new Runnable() {
@Override
public void run() {
receive();
}
});
mVideoThread.start();
}
public void receive() {
try {
isStop = false;
while (!isStop) {
Socket socket = mSocket.accept();
//Logger.i(TAG, "accept **********");
InputStream input = socket.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
byte[] head = null;
int headIndex = 0;
int dataSize = 0;
byte[] frame = null;
int frameIndex = 0;
while ((len = input.read(buf)) != -1) {
//Logger.i(TAG, "receive new pact data " + len);
mDataCount += len;
int readIndex = 0;
int bufLen = len;
while (readIndex < len) {
if (dataSize == 0) {
//Logger.i(TAG, "receive data new ############# headIndex:" + headIndex + " readIndex:" + readIndex);
if (headIndex == 0) {
head = new byte[4];
}
int readHead = Math.min(head.length - headIndex, len - readIndex);
System.arraycopy(buf, readIndex, head, headIndex, readHead);
headIndex += readHead;
readIndex += readHead;
if (headIndex == head.length) {
dataSize = CodecUtils.bytesToInt(head);
//Logger.i(TAG, "receive data new frame size:" + dataSize);
frame = new byte[dataSize];
headIndex = 0;
} else {
//Logger.w(TAG, "receive head ****************** headIndex:" + headIndex + " readIndex:" + readIndex);
break;
}
}
int bufLeft = bufLen - readIndex;
int frameLeft = frame.length - frameIndex;
//Logger.i(TAG, "receive data bufLeft:" + bufLeft + " frameLeft:" + frameLeft);
if (bufLeft < frameLeft) {
System.arraycopy(buf, readIndex, frame, frameIndex, bufLeft);
frameIndex += bufLeft;
readIndex += bufLeft;
} else {
System.arraycopy(buf, readIndex, frame, frameIndex, frameLeft);
frameIndex += frameLeft;
readIndex += frameLeft;
if (mServerListener != null) {
mServerListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, resolveFrame(frame));
}
mFrameCount++;
//one frame read complete
dataSize = 0;
frameIndex = 0;
}
//Logger.i(TAG, "receive data readIndex:" + readIndex + " frameIndex:" + frameIndex);
}
}
}
} catch (Exception e) {
Logger.w(TAG, e);
stopReceiver();
}
}
private Frame resolveFrame(byte[] data) {
int index = 0;
byte[] headSizeBytes = new byte[4];
System.arraycopy(data, index, headSizeBytes, 0, headSizeBytes.length);
index += headSizeBytes.length;
int headSize = CodecUtils.bytesToInt(headSizeBytes);
byte[] head = new byte[headSize];
System.arraycopy(data, index, head, 0, head.length);
index += head.length;
String headContent = new String(head);
//Logger.i(TAG, "headContent " + headContent);
Map parameter = resolveHead(headContent);
long pts = Long.parseLong(parameter.get("pts"));
byte[] frameBytes = new byte[data.length - headSizeBytes.length - head.length];
System.arraycopy(data, index, frameBytes, 0, frameBytes.length);
Frame frame = new Frame();
frame.buf = frameBytes;
frame.pts = pts;
return frame;
}
private Map resolveHead(String head) {
Map map = new HashMap<>();
String[] arr = head.split("&");
for (String str : arr) {
String[] arr1 = str.split("=");
map.put(arr1[0], arr1[1]);
}
return map;
}
public void setOnReceiverListener(OnReceiverListener listener) {
mServerListener = listener;
}
public int getReceiverPort() {
if (mSocket != null) {
return mSocket.getLocalPort();
}
return -1;
}
public long getDataCount() {
return mDataCount;
}
public long getFrameCount() {
return mFrameCount;
}
public void stopReceiver() {
if (isStop) {
return;
}
isStop = true;
if (mSocket != null) {
try {
mSocket.close();
} catch (Exception e) {
Logger.w(TAG, e);
}
mSocket = null;
}
if (mServerListener != null) {
mServerListener.onReceiveStopped(OnReceiverListener.ANDROID_VIDEO);
}
}
}
3、音频流服务
音频数据不怕丢失,所以这里选择udp
进行传输。
public class AndroidAudioReceiver {
private final static String TAG = "AndroidAudioReceiver";
private MiniDatagramSocket mSocket;
private static final int DATA_LEN = 4096;
private byte[] inBuff = new byte[DATA_LEN];
private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
private OnReceiverListener mListener;
private Thread mAudioThread;
private boolean isStop = false;
private Decoder mOpusDecoder;
public void startReceiver() {
try {
mSocket = new MiniDatagramSocket(0);
} catch (Exception e) {
Logger.w(TAG, e);
stopReceiver();
return;
}
Logger.i(TAG, "startReceiver");
if (mListener != null) {
mListener.onReceiveStarted(OnReceiverListener.ANDROID_AUDIO);
}
receiveAudio();
}
private void receiveAudio() {
mAudioThread = new Thread(new Runnable() {
@Override
public void run() {
receive();
}
});
mAudioThread.start();
}
public void receive() {
try {
while (!isStop) {
try {
mSocket.receive(inPacket);
//Logger.e(TAG, "receive length:" + inPacket.getLength());
byte[] data = new byte[inPacket.getLength()];
System.arraycopy(inPacket.getData(), 0, data, 0, data.length);
Frame frame = new Frame();
frame.buf = data;
//Logger.e(TAG, "receive opus:" + Arrays.toString(data));
//Logger.e(TAG, "receive pcm:" + Arrays.toString(frame.shortBuf));
if (mListener != null) {
mListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, frame);
}
} catch (Exception e) {
Logger.w(TAG, "receive", e);
}
}
} catch (Exception e) {
Logger.w(TAG, e);
stopReceiver();
}
}
public int getPort() {
if (mSocket != null) {
return mSocket.getLocalPort();
}
return -1;
}
public void setOnReceiverListener(OnReceiverListener listener) {
mListener = listener;
}
public void stopReceiver() {
if (isStop) {
return;
}
isStop = true;
if (mSocket != null) {
try {
mSocket.close();
} catch (Exception e) {
Logger.w(TAG, e);
}
mSocket = null;
}
if (mListener != null) {
mListener.onReceiveStopped(OnReceiverListener.ANDROID_AUDIO);
}
}
}
4、生成二维码
private void updateServerInfo() {
if (mIPTxt != null) {
mIPTxt.setText(DeviceUtils.getIP(getActivity()) + ":" + MirrorRender.getInstance().getTVPort());
}
mServerInfo = "ip=" + DeviceUtils.getIP(getActivity())
+ "&port=" + MirrorRender.getInstance().getTVPort();
mQRView.setImageBitmap(Utils.createQRCode(mServerInfo, 200, 0));
}
二维码中含有tv
端的ip
和主服务端口,phone
通过这个ip
和主服务端口可以发起镜像。
二、phone扫码连接
通过摄像头扫码显示,这里偷个懒使用一个比较好的第三方扫码库。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.zxing:core:3.3.2'
implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'
implementation("com.squareup.okhttp3:okhttp:4.6.0")
}
扫码解析
public class ScanFragment extends BaseFragment {
private final static String TAG = "ScanActivity";
private QRCodeView.Delegate mQRDelegate = new QRCodeView.Delegate() {
@Override
public void onScanQRCodeSuccess(String result) {
vibrate();
((MainActivity) getActivity()).notifyScanResult(result);
getFragmentManager().popBackStack();
}
@Override
public void onCameraAmbientBrightnessChanged(boolean isDark) {
}
@Override
public void onScanQRCodeOpenCameraError() {
Logger.i(TAG, "onScanQRCodeOpenCameraError");
}
};
private ZXingView mQRCodeView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return View.inflate(getActivity(), R.layout.f_scan, null);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mQRCodeView = view.findViewById(R.id.zxingview);
mQRCodeView.startCamera();
mQRCodeView.startSpot();
mQRCodeView.setDelegate(mQRDelegate);
}
private void vibrate() {
Vibrator vibrator = (Vibrator) getActivity().getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(200);
}
@Override
public void onStop() {
super.onStop();
mQRCodeView.stopCamera();
}
@Override
public void onDestroy() {
super.onDestroy();
mQRCodeView.onDestroy();
}
}
解析ip和镜像服务主端口
public void updateSinkInfo(String info) {
Logger.i(TAG, "updateSinkInfo " + info);
Map map = new HashMap<>();
String[] arr = info.split("&");
for (String str : arr) {
String[] res = str.split("=");
if (res.length > 1) {
map.put(res[0], res[1]);
}
}
String ip = map.get(KEY_IP);
String port = map.get(KEY_PORT);
if (mPortEdit != null) {
mIPEdit.setText(ip);
mPortEdit.setText(port);
} else {
Logger.i(TAG, "invalid receive");
}
}
三、phone发送视频流数据
开始镜像
private void startMirror() {
String ip = getEditString(mIPEdit);
int port = getEditInt(mPortEdit);
if (TextUtils.isEmpty(ip) || port <= 0) {
Toast.makeText(getActivity(), "请输入正确的IP和端口", Toast.LENGTH_SHORT).show();
return;
}
Config.getInstance().setTVIP(ip);
Config.getInstance().setTVPort(port);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType())
.build();
Logger.i(TAG, "http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType());
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
mHandler.obtainMessage(WHAT_TOAST, "请求失败").sendToTarget();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
String result = response.body().string();
JSONObject json = new JSONObject(result);
int videoPort = json.optInt("videoPort");
int audioPort = json.optInt("audioPort");
if (videoPort <= 0 || audioPort <= 0) {
mHandler.obtainMessage(WHAT_TOAST, "TV端口异常").sendToTarget();
return;
}
Config.getInstance().setVideoPort(videoPort);
Config.getInstance().setAudioPort(audioPort);
((MainActivity) getActivity()).requestScreenCapture();
} catch (Exception e) {
Logger.w(TAG, e);
mHandler.obtainMessage(WHAT_TOAST, "数据解析失败").sendToTarget();
}
}
});
}
在屏幕数据回调中,传输视频流数据到tv端
@Override
public void onCaptureVideoCallback(byte[] buf, long pts) {
// Logger.i(TAG, "onCaptureVideoCallback " + pts);
long start = System.currentTimeMillis();
byte[] newBuf = new byte[buf.length];
Frame videoFrame = new Frame();
System.arraycopy(buf, 0, newBuf, 0, buf.length);
videoFrame.buf = newBuf;
videoFrame.pts = pts;
mVideoSender.writeData(videoFrame);
//Logger.i(TAG, "onCaptureVideoCallback cost " + (System.currentTimeMillis() - start));
}
public class VideoSender {
private final static String TAG = "VideoSender";
private MiniSocket mSocket;
private OutputStream mWriteStream;
private ConcurrentLinkedQueue mFrameQueue = new ConcurrentLinkedQueue<>();
private Thread mWriteThread = null;
private OnSendListener mListener;
public void connect(String ip, int port) {
Logger.i(TAG, "connect " + port);
try {
mSocket = new MiniSocket(ip, port);
mSocket.setTcpNoDelay(true);
mSocket.setKeepAlive(true);
mWriteStream = mSocket.getOutputStream();
startWriteThread();
} catch (Exception e) {
Logger.w(TAG, e);
if (mListener != null) {
mListener.onError();
}
}
}
private void startWriteThread() {
mWriteThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (mWriteStream == null) {
return;
}
if (mFrameQueue.isEmpty()) {
continue;
}
Frame frame = mFrameQueue.poll();
if (frame == null) {
continue;
}
long start = System.currentTimeMillis();
byte[] head = ("pts=" + frame.pts).getBytes();// 多个参数之间 & 分割
byte[] headCount = CodecUtils.intToBytes(head.length);// 多个参数之间 & 分割
byte[] totalCount = CodecUtils.intToBytes(headCount.length + head.length + frame.buf.length);
byte[] bytes = new byte[totalCount.length + headCount.length + head.length + frame.buf.length];
//Logger.i(TAG, "writeData frameLen " + frame.buf.length + " headLen " + head.length);
int index = 0;
System.arraycopy(totalCount, 0, bytes, index, totalCount.length);
index += totalCount.length;
System.arraycopy(headCount, 0, bytes, index, headCount.length);
index += headCount.length;
System.arraycopy(head, 0, bytes, index, head.length);
index += head.length;
System.arraycopy(frame.buf, 0, bytes, index, frame.buf.length);
try {
mWriteStream.write(bytes);
} catch (Exception e) {
Logger.w(TAG, e);
mWriteStream = null;
if (mListener != null) {
mListener.onError();
}
break;
}
if (mFrameQueue.size() > 5) {
MirrorApplication.getApplication().changeBitrate(256 * 1024);
} else {
MirrorApplication.getApplication().changeBitrate(2 * 1024 * 1024);
}
long cost = (System.currentTimeMillis() - start);
if (cost > 16) {
Logger.i(TAG, "send video frame cost:" + cost + " len:" + bytes.length + " left frames:" + mFrameQueue.size());
}
}
disconnect();
}
});
mWriteThread.start();
}
public void writeData(Frame frame) {
//Logger.i(TAG,"writeData");
mFrameQueue.add(frame);
}
public void disconnect() {
try {
if (mWriteStream != null) {
mWriteStream.close();
mWriteStream = null;
}
} catch (Exception e) {
Logger.w(TAG, e);
}
try {
if (mSocket != null) {
mSocket.close();
mSocket = null;
}
} catch (Exception e) {
Logger.w(TAG, e);
}
}
public void release() {
mFrameQueue.clear();
}
public void setOnSendListener(OnSendListener listener) {
mListener = listener;
}
}
四、phone发送端音频流数据
与发送端视频流不同的是,音频数据发送使用udp
private AudioCapture.OnAudioCaptureCallback mAudioCallback = new AudioCapture.OnAudioCaptureCallback() {
@Override
public void onCaptureAudioCallback(short[] buf) {
Logger.i(TAG, "opus---- onCaptureAudioCallback: " + Arrays.toString(buf));
mAudioSender.writeData(buf);
}
};
AudioSender
public class AudioSender {
private final static String TAG = "PCMSender";
private MiniDatagramSocket mSocket;
private boolean disconnect = true;
private String mTargetIP;
private int mTargetPort;
private static final int DATA_LEN = 4096;
private OnSendListener mListener;
public void connect(String ip, int port) {
mTargetIP = ip;
mTargetPort = port;
try {
Logger.i(TAG, "connect " + ip + "/" + port);
mSocket = new MiniDatagramSocket(0);
disconnect = false;
Logger.i(TAG, "connect success " + ip + "/" + port);
} catch (Exception e) {
Logger.w(TAG, "connect", e);
disconnect = true;
if (mListener != null) {
mListener.onError();
}
}
}
public void writeData(Frame frame) {
if (disconnect) {
Logger.i(TAG,"writeData ignore");
return;
}
Logger.i(TAG,"writeData " + Arrays.toString(frame.buf));
int readOffset = 0;
while (readOffset < frame.buf.length) {
int left = Math.min(DATA_LEN, frame.buf.length - readOffset);
byte[] buf = new byte[left];
System.arraycopy(frame.buf, readOffset, buf, 0, buf.length);
try {
InetAddress address = InetAddress.getByName(mTargetIP);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, mTargetPort);
mSocket.send(packet);
} catch (Exception e) {
Logger.w(TAG, e);
disconnect();
if (mListener != null) {
mListener.onError();
}
break;
}
readOffset += left;
}
}
public void disconnect() {
disconnect = true;
try {
mSocket.close();
mSocket = null;
} catch (Exception e) {
Logger.w(TAG, e);
}
}
public void setOnSendListener(OnSendListener listener) {
mListener = listener;
}
}
解码显示部分与在本机解码显示一致,不再赘述