Android设备之间投屏功能实现

article/2025/7/21 16:50:26

简介

简单实现两个android设备之间的投屏功能。设备间通信是通过局域网,需要连接同一个wifi。

录屏用到系统的MediaProjection,MediaProjectionManager,而编解码用的是MediaCodec,所以设备需要有DSP芯片,大部分手机应该都有。两台设备间通信使用websocket,录屏端作为服务器进行推流,显示端就是客户端,收到码流进行解码并显示。

先看看最终效果:

在这里插入图片描述

实现

首先需要先导入Java-WebSocket库,WebSocket是一种在单个TCP连接上进行全双工通信的协议,允许服务端主动向客户端推送数据。

implementation "org.java-websocket:Java-WebSocket:1.4.0"

整个工程有两个module,app是作为服务端。playscreen是客户端。需要分别运行在两台机器上面。

在这里插入图片描述

服务端 app module

MainActivity 就一个button布局就不贴出来了。

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.view.View;
import com.example.castscreen.socket.SocketService;public class MainActivity extends AppCompatActivity {private int permissionRequestCode = 100;private int captureRequestCode = 1;private MediaProjectionManager mediaProjectionManager;private SocketService socketService;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();}private void init() {//拿到MediaProjectionManagermediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);}public void onClick(View view) {switch (view.getId()){case R.id.btn_start:startCast();break;}}//请求开始录屏private void startCast(){PermissionUtil.checkPermission(this,PermissionUtil.storagePermissions,permissionRequestCode);Intent intent = mediaProjectionManager.createScreenCaptureIntent();startActivityForResult(intent,captureRequestCode);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);if (resultCode != RESULT_OK){return;}if(requestCode == this.captureRequestCode){startCast(resultCode,data);}}//录屏开始后进行编码推流private void startCast(int resultCode,Intent data){//这里需要传入resultCode而不是requestCode,在这里踩了个坑大家注意MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode,data);if (mediaProjection == null){return;}//初始化服务器端socketService = new SocketService();//将MediaProjection传给 socketService socketService.start(mediaProjection);}@Overrideprotected void onDestroy() {super.onDestroy();if (socketService != null){socketService.colse();}}
}

SocketService 作用是启动SocketServer并设置端口号 。启动CodecH265 进行编码,CodecH265 编码完一帧再通过SocketServer将数据发送出去。

import android.media.projection.MediaProjection;
import com.example.castscreen.encode.CodecH265;
import java.io.IOException;
import java.net.InetSocketAddress;public class SocketService {private static final String TAG = "SocketService";//端口号,尽量设大一些private int port = 11006;private CodecH265 codecH265;private SocketServer webSocketServer;public SocketService(){webSocketServer = new SocketServer(new InetSocketAddress(port));}public void start(MediaProjection mediaProjection){//启动webSocketServer  此时当前设备就可以作为一个服务器了webSocketServer.start();codecH265 = new CodecH265(this,mediaProjection);//开始编码codecH265.startEncode();}//关闭服务端public void colse(){try {webSocketServer.stop();webSocketServer.close();} catch (IOException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}codecH265.stopEncode();}//发送编码后的数据public void sendData(byte[] bytes){webSocketServer.sendData(bytes);}
}

SocketServer 继承自WebSocketServer,调用它的start方法就启动服务端了。

import android.util.Log;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import java.net.InetSocketAddress;public class SocketServer extends WebSocketServer {private final String TAG = "SocketServer";private WebSocket webSocket;public SocketServer(InetSocketAddress inetSocketAddress){super(inetSocketAddress);}@Overridepublic void onOpen(WebSocket conn, ClientHandshake handshake) {Log.d(TAG,"SocketServer onOpen");this.webSocket = conn;}@Overridepublic void onClose(WebSocket conn, int code, String reason, boolean remote) {}@Overridepublic void onMessage(WebSocket conn, String message) {}@Overridepublic void onError(WebSocket conn, Exception ex) {}@Overridepublic void onStart() {}public void sendData(byte[] bytes){if(webSocket != null && webSocket.isOpen()){//通过WebSocket 发送数据webSocket.send(bytes);}}public void close(){webSocket.close();}
}

CodecH265是实现编码的类,这里用的是video/hevc,也就是H265编码。需要注意的是因为H265编码只有第一帧才有vps,sps,pps,其他帧不带这些信息,所以需要我们在发送时候为每个I帧添加上vps信息,否则如果客户端不是从头开始接受的数据,那么就没办法进行解码操作。而且H265用的是哥伦布编码,如果对reEncode方法有疑问可以先了解一下哥伦布编码。另外因为编码是比较耗时的操作,肯定要方法子线程中去做。

import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.projection.MediaProjection;
import android.util.Log;
import android.view.Surface;
import com.example.castscreen.socket.SocketService;
import java.io.IOException;
import java.nio.ByteBuffer;public class CodecH265 extends Thread {private static final String TAG = "gsy";//图省事宽高直接固定了private int width = 720;private int height = 1280;//h265编码private final String enCodeType = "video/hevc";private MediaCodec mediaCodec;private MediaProjection mediaProjection;private SocketService socketService;private boolean play = true;private long timeOut = 10000;//记录vps pps spsprivate byte[] vps_pps_sps;//I帧private final int NAL_I = 19;//vps帧private final int NAL_VPS = 32;public CodecH265(SocketService socketService, MediaProjection mediaProjection) {this.socketService = socketService;this.mediaProjection = mediaProjection;}public void startEncode() {//声明MediaFormat,创建视频格式。MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, width, height);//描述视频格式的内容的颜色格式mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);//比特率(比特/秒)mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height);//帧率mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);//I帧的频率mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);try {//创建编码MediaCodec 类型是video/hevcmediaCodec = MediaCodec.createEncoderByType(enCodeType);//配置编码器mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);//创建一个目的surface来存放输入数据Surface surface = mediaCodec.createInputSurface();//获取屏幕流mediaProjection.createVirtualDisplay("screen", width, height, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);} catch (IOException e) {Log.d(TAG,"initEncode IOException");e.printStackTrace();}//启动子线程this.start();}@Overridepublic void run() {//编解码器立即进入刷新子状态mediaCodec.start();//缓存区的元数据MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();//子线程需要一直运行,进行编码推流,所以要一直循环while (play) {//查询编码输出int outPutBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOut);if (outPutBufferId >= 0) {//获取编码之后的数据输出流队列ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outPutBufferId);//添加上vps,sps,ppsreEncode(byteBuffer, bufferInfo);//处理完成,释放ByteBuffer数据mediaCodec.releaseOutputBuffer(outPutBufferId, false);}}}private void reEncode(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {//偏移4 00 00 00 01为分隔符需要跳过int offSet = 4;if (byteBuffer.get(2) == 0x01) {offSet = 3;}//计算出当前帧的类型int type = (byteBuffer.get(offSet) & 0x7E) >> 1;if (type == NAL_VPS) {//保存vps sps pps信息vps_pps_sps = new byte[bufferInfo.size];byteBuffer.get(vps_pps_sps);} else if (type == NAL_I) {//将保存的vps sps pps添加到I帧前final byte[] bytes = new byte[bufferInfo.size];byteBuffer.get(bytes);byte[] newBytes = new byte[vps_pps_sps.length + bytes.length];System.arraycopy(vps_pps_sps, 0, newBytes, 0, vps_pps_sps.length);System.arraycopy(bytes, 0, newBytes, vps_pps_sps.length, bytes.length);//将重新编码好的数据发送出去socketService.sendData(newBytes);} else {//B帧 P帧 直接发送byte[] bytes = new byte[bufferInfo.size];byteBuffer.get(bytes);socketService.sendData(bytes);}}public void stopEncode() {play = false;}}

客户端 playescreen module

客户端的页面只有一个SurfaceView,用来显示,布局代码就不贴了。MainActivity 实现了SocketServer.SocketCallback接口,当SocketServer收到数据后,通过回调将收到的码流传给MainActivity 进行解码渲染到SurfaceView。

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.nio.ByteBuffer;public class MainActivity extends AppCompatActivity implements SocketServer.SocketCallback{private static final String TAG = "gsy";private Surface surface;private SurfaceView surfaceView;private MediaCodec mediaCodec;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();}private void init() {surfaceView = findViewById(R.id.sfv_play);surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {surface = holder.getSurface();//连接到服务端initSocket();//配置MediaCodecinitDecoder(surface);}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {}});}private void initDecoder(Surface surface) {try {//配置MediaFormat MediaCodecmediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC,720,1280);mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,720*1280);mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,20);mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);mediaCodec.configure(mediaFormat,surface,null,0);mediaCodec.start();} catch (IOException e) {Log.d(TAG,"initDecoder IOException ");e.printStackTrace();}}private void initSocket() {Log.d(TAG,"initSocket");//启动客户端SocketServer socketServer = new SocketServer();socketServer.setSocketCallback(this);socketServer.start();}@Overridepublic void callBack(byte[] data) {Log.d(TAG,"mainActivity callBack");//得到填充了有效数据的input buffer的索引int index = mediaCodec.dequeueInputBuffer(10000);if (index >= 0){//获取输入缓冲区ByteBuffer inputBuffer = mediaCodec.getInputBuffer(index);//清除原来的内容以接收新的内容inputBuffer.clear();inputBuffer.put(data,0,data.length);//将其提交给编解码器 把缓存数据入队mediaCodec.queueInputBuffer(index,0,data.length,System.currentTimeMillis(),0);}MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();//请求一个输出缓存int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,10000);//直到outputBufferIndex < 0 才算处理完所有数据while (outputBufferIndex > 0){mediaCodec.releaseOutputBuffer(outputBufferIndex,true);outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);}}
}

SocketServer 就是客户端了,作用就是连接服务器,获取视频码流进行回调。注意ip地址不要,是服务端的ip地址,可以在设置中查看,比如:

在这里插入图片描述

import android.util.Log;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;public class SocketServer {private final String TAG = "gsy";private SocketClient socketClient;private SocketCallback socketCallback;//设置回调public void setSocketCallback(SocketCallback socketCallback){this.socketCallback = socketCallback;}public void start(){try {//这里要填服务端的ipURI uri = new URI("ws://192.168.1.103:11006");socketClient = new SocketClient(uri);socketClient.connect();} catch (URISyntaxException e) {Log.e(TAG,"error:"+e.toString());e.printStackTrace();}}private class SocketClient extends WebSocketClient{public SocketClient(URI serverUri) {super(serverUri);Log.d(TAG,"new SocketClient");}@Overridepublic void onOpen(ServerHandshake handshakedata) {Log.d(TAG,"SocketClient onOpen");}@Overridepublic void onMessage(String message) {Log.d(TAG,"onMessage");}@Overridepublic void onMessage(ByteBuffer bytes) {Log.d(TAG,"onMessage");//收到数据 进行回调byte[] buf = new byte[bytes.remaining()];bytes.get(buf);socketCallback.callBack(buf);}@Overridepublic void onClose(int code, String reason, boolean remote) {Log.d(TAG,"onClose ="+reason);}@Overridepublic void onError(Exception ex) {Log.d(TAG,"onerror ="+ex.toString());}}//回调public interface SocketCallback{void callBack(byte[] data);}
}

总共代码大概就这么多,推荐一个在线测试websocket的网站,http://www.websocket-test.com/ chrome浏览器打不开,可以使用IE浏览器,首先要保证服务端可以访问。今天太冷了,就不再多说了,下面是源码,有兴趣的同学可以玩一下。

源码:demo源码


http://chatgpt.dhexx.cn/article/EVK1mBYI.shtml

相关文章

CSS实现垂直居中的5种方法

第一种 position定位margin负距离 前提是知道居中元素的宽高&#xff0c;首先给居中元素定位&#xff0c;之后设置margin的负距离为具体宽高的一半便可达到垂直居中效果 <style>.box1 {height: 300px;width: 300px;border: 10px solid pink;position: relative;}.box2 {…

在html中如何使div在页面中居中显示

在html中如何使div在页面中居中显示 最近无聊中又再温习了下html,发现好多东西都忘了。尝试着写了一个html网页&#xff0c;结果就连div如何在页面中居中显示都查了好久才弄出来。其实我不知道为什么这样可以实现&#xff0c;因为css还没仔细研究过&#xff0c;等我参加完复试定…

实现div里的img图片水平垂直居中

body结构 <body><div><img src"1.jpg" alt"haha"></div> </body>方法一&#xff1a; 将display设置成table-cell&#xff0c;然后水平居中设置text-align为center&#xff0c;垂直居中设置vertical-align为middle。 <s…

程序运行的错误

程序的运行当可能会出现的问题 没有找到文件&#xff0c;可能是相应的文件没有编写&#xff0c;或者文件名输入的有错误 程序运行可能会出现500的错误&#xff0c;错误原因大概有&#xff0c;下面几种

小程序错误

小程序报错1&#xff1a;unknown: Unexpected token, expected “,” [ appservice 生成错误] pages/page04/page04.js: file: pages/page04/page04.js unknown: Unexpected token, expected “,” (25:4) 错误原因&#xff1a;没有逗号。 小程序报错2 设置 enable-flex 属…

winword.exe应用程序错误0xc0000142

正常使用Word&#xff0c;关机再开机&#xff0c;发现提示错误“winword.exe应用程序错误0xc0000142”&#xff0c;有效解决方法 1、winR&#xff0c;输入CMD 2、输入sfc/scannow 注&#xff1a;sfc/scannow&#xff1a;立即扫描所有受保护系统文件的完整性&#xff0c;并尽可…

解决devenv.exe应用程序错误,应用程序发生异常

解决devenv.exe应用程序错误&#xff0c;应用程序发生异常 打开VS2008/2010时&#xff0c;经常碰到:devenv.exe应用程序错误&#xff0c;应用程序发生异常&#xff0c;造成的原因是多种的&#xff0c;可能是环境变量配置出错&#xff0c;可能是你安装了冲突的插件&#xff0c;如…

添加或删除程序 rundll32.exe-应用程序错误

rundll32.exe-应用程序错误 点击开始-运行&#xff0c;输入 “cmd”&#xff0c;单击确定。 在打开的命令提示符窗口中依次输入第三步中的命令复制&#xff0c;然后粘贴到命令提示符窗口中运行。 regsvr32 Appwiz.cpl regsvr32 Jscript.dll regsvr32 Mshtml.dll regsvr32…

ps无法完成请求因为程序错误

目录 方法一&#xff1a;兼容性 方法二&#xff1a;清空缓存 方法三&#xff1a;图形处理器 方法一&#xff1a;兼容性 1、右击PS软件图标&#xff0c;选择“属性” 2.选择“兼容性”选项卡&#xff0c;勾选“以兼容模式运行这个程序”&#xff0c;并选择兼容的系统&#xf…

ArcGIS遇到严重的应用程序错误的解决办法

ArcGIS遇到严重的应用程序错误的解决办法 GIS思维 很多ArcGIS用户经常会碰到程序突然崩溃遇到严重错误的时候且重启无法解决。网上给出的方法基本就是几种没有&#xff0c;基本也解决不了。 也可以去测试&#xff0c;基本就是&#xff1a; ArcGIS找打不到配置信息&#xff0c…

Photoshop储存为psd出现程序错误提示怎么办?程序错误解决教程

Photoshop是大家进行平面设计是不可或缺的图像处理工具&#xff0c;然而大家在使用Photoshop做图像处理&#xff0c;准备将psd文件保存到电脑&#xff0c;是不是经常遇到过弹出了Photoshop【无法完成请求&#xff0c;因为程序错误】的提醒窗口&#xff0c;无法完成保存工作。具…

java程序出错_java程序错误类型及异常处理

一、程序的错误类型 在程序设计中,无论规模是大是小,错误总是难免的。程序的设计很少有能够一次完成,没有错误的(不是指HelloWorld这样的程序,而是要实现一定的功能,具备一定实用价值的程序),在编程的过程中由于种种原因,总会出现这样或那样的错误,这些程序的错误就是我…

VMware.exe应用程序错误--应用程序无法正常启动(0xc000007b)错误解决方法

1.找到VMware安装包&#xff0c;双击打开安装包 2.进去到选择修复界面 3.等待修复完成然后重启电脑再进去VMware正常工作了 有什么问题可以在评论区交流

计算机显示应用程序错误窗口,电脑提示explorer.exe应用程序错误怎么办|电脑explorer.exe应用程序错误的解决方法...

‍‍ 大家应该都知道explorer.exe是什么进程&#xff0c;这是windows系统中的资源管理器&#xff0c;是windows系统提供的资源管理工具。可是有很多用户遇到了电脑提示explorer.exe应用程序错误的情况&#xff0c;这该怎么办呢&#xff1f;下面由小编跟大家介绍电脑explorer.ex…

应用程序错误(0xc0000135)

dll文件是什么&#xff1f; DLL&#xff08;Dynamic Link Library)文件为动态链接库文件&#xff0c;又称“应用程序扩展”&#xff0c;是软件文件类型。 也是可执行文件&#xff0c;它应许程序共享执行特殊任务所必需的代码和其他资源。 dll文件操作&#xff1a; 理论上DLL…

计算机应用程序错误怎么解决办法,应用错误,教您怎么解决explorer.exe应用程序错误...

小可爱小可爱&#xff0c;你们有没有遇到过“explorer.exe 应用程序错误”这样的问题&#xff0c;有的话快来小编这儿呀&#xff0c;我这儿有解决方法~~不信你们往下滑就知道啦~下面小编我就开始说解决“explorer.exe 应用程序错误”的办法啦~~ 小可爱们&#xff0c;你们知道ex…

中国各个城市OSM地图数据

下载地址&#xff1a;http://download.openstreetmap.fr/extracts/asia/china/

OSM地图瓦片下载器1.0版介绍(win64)

简介 为方便在工作中随时使用OSM瓦片数据&#xff08;公开链接的&#xff09;&#xff0c;特编写此下载工具&#xff0c;并会一直更新&#xff0c;欢迎关注。如果需要了解基本的瓦片知识&#xff0c;请移步 地图瓦片讲解 注意OSM坐标系是WGS84-webMercator 特点 1.下载级别、…

如何实现OSM地图本地发布并自定义配图

文章目录 1、缘起2、准备环境2.1、安装linux系统2.2、安装docker2.3、安装Docker Compose2.4、安装git 3、发布地图3.1、拉取代码3.2、测试网络3.3、处理数据3.4、发布矢量瓦片服务3.5、自定义地图样式3.6、注意 4、总结 1、缘起 OpenStreetMap&#xff08;简称OSM&#xff09…

Openlayers案例1——加载OSM地图

1. 代码块 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>加载OSM地图</title><!-- CSS路…