android设备间实现无线投屏
前言
Android提供了MediaProjection來實現錄屏,通過MediaProjection可以獲取當前屏幕的視頻流,而視頻流需要通過編解碼來壓縮進行傳輸,通過MediaCodec可實現視頻的編碼和解碼。視頻流的推送和接收可通過Socket或WebSocket來實現,服務端推送編碼的視頻流,客戶端接收視頻流并進行解碼,然后渲染在SurfaceView上即可顯示服務端的畫面。
媒體投影MediaProjection的核心是虛擬屏幕,您可以通過對 MediaProjection 實例調用 createVirtualDisplay() 來創建虛擬屏幕(VirtualDisplay):
Android的媒體投影MediaProjection介紹:媒體投影
投屏服務端的實現
服務端主入口負責請求錄屏和啟動錄屏服務,服務端主入口的實現如下:
package com.example.screenprojection;import android.content.Intent; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.os.Bundle; import android.view.View;import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private static final int PROJECTION_REQUEST_CODE = 1;private MediaProjectionManager mediaProjectionManager;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();}private void init() {mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);}public void onClick(View view) {if (view.getId() == R.id.btn_start) {startProjection();}}// 請求開始錄屏private void startProjection() {Intent intent = mediaProjectionManager.createScreenCaptureIntent();startActivityForResult(intent, PROJECTION_REQUEST_CODE);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);if (resultCode != RESULT_OK) {return;}if (requestCode == PROJECTION_REQUEST_CODE) {Intent service = new Intent(this, ScreenService.class);service.putExtra("code", resultCode);service.putExtra("data", data);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {startForegroundService(service);} else {startService(service);}}}@Overrideprotected void onDestroy() {super.onDestroy();} }Android8.0后錄屏需要開啟前臺服務通知,錄屏服務開啟后同時啟動WebSocketServer服務端,前臺服務代碼如下:
package com.example.screenprojection;import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.graphics.BitmapFactory; import android.media.projection.MediaProjection; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.os.IBinder;public class ScreenService extends Service {private MediaProjectionManager mMediaProjectionManager;private SocketManager mSocketManager;@Overridepublic void onCreate() {super.onCreate();mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);createNotificationChannel();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {int resultCode = intent.getIntExtra("code", -1);Intent resultData = intent.getParcelableExtra("data");startProject(resultCode, resultData);return super.onStartCommand(intent, flags, startId);}// 錄屏開始后進行編碼推流private void startProject(int resultCode, Intent data) {MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);if (mediaProjection == null) {return;}// 初始化服務器端mSocketManager = new SocketManager();mSocketManager.start(mediaProjection);}private void createNotificationChannel() {Notification.Builder builder = new Notification.Builder(this.getApplicationContext());Intent nfIntent = new Intent(this, MainActivity.class);builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)).setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) // 設置下拉列表中的圖標(大圖標).setSmallIcon(R.mipmap.ic_launcher) // 設置狀態欄內的小圖標.setContentText("is running......") // 設置上下文內容.setWhen(System.currentTimeMillis()); // 設置該通知發生的時間if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {builder.setChannelId("notification_id");// 前臺服務notification適配NotificationManager notificationManager =(NotificationManager) getSystemService(NOTIFICATION_SERVICE);NotificationChannel channel =new NotificationChannel("notification_id", "notification_name", NotificationManager.IMPORTANCE_LOW);notificationManager.createNotificationChannel(channel);}Notification notification = builder.build();notification.defaults = Notification.DEFAULT_SOUND; // 設置為默認通知音startForeground(110, notification);}@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onDestroy() {mSocketManager.close();super.onDestroy();} }SocketManager為WebSocketServer的控制類,并同時將錄屏對象MediaProjection與視頻編碼器ScreenEncoder綁定。
package com.example.screenprojection;import android.media.projection.MediaProjection;import java.net.InetSocketAddress;public class SocketManager {private static final String TAG = SocketManager.class.getSimpleName();private static final int SOCKET_PORT = 50000;private final ScreenSocketServer mScreenSocketServer;private ScreenEncoder mScreenEncoder;public SocketManager() {mScreenSocketServer = new ScreenSocketServer(new InetSocketAddress(SOCKET_PORT));}public void start(MediaProjection mediaProjection) {mScreenSocketServer.start();mScreenEncoder = new ScreenEncoder(this, mediaProjection);mScreenEncoder.startEncode();}public void close() {try {mScreenSocketServer.stop();mScreenSocketServer.close();} catch (InterruptedException e) {e.printStackTrace();}if (mScreenEncoder != null) {mScreenEncoder.stopEncode();}}public void sendData(byte[] bytes) {mScreenSocketServer.sendData(bytes);} }推流服務端ScreenSocketServer代碼如下:
package com.example.screenprojection;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 ScreenSocketServer extends WebSocketServer {private final String TAG = ScreenSocketServer.class.getSimpleName();private WebSocket mWebSocket;public ScreenSocketServer(InetSocketAddress inetSocketAddress) {super(inetSocketAddress);}@Overridepublic void onOpen(WebSocket conn, ClientHandshake handshake) {Log.d(TAG, "onOpen");mWebSocket = conn;}@Overridepublic void onClose(WebSocket conn, int code, String reason, boolean remote) {Log.d(TAG, "onClose:" + reason);}@Overridepublic void onMessage(WebSocket conn, String message) {}@Overridepublic void onError(WebSocket conn, Exception ex) {Log.d(TAG, "onError:" + ex.toString());}@Overridepublic void onStart() {Log.d(TAG, "onStart");}public void sendData(byte[] bytes) {if (mWebSocket != null && mWebSocket.isOpen()) {// 通過WebSocket 發送數據Log.d(TAG, "sendData:");mWebSocket.send(bytes);}}public void close() {mWebSocket.close();} }WebSocketServer啟動成功會回調onStart方法。
@Overridepublic void onStart() {Log.d(TAG, "onStart");}當有客戶端連接回調onOpen方法,通過回調的WebSocket對象即可向客戶端發送數據。
@Overridepublic void onOpen(WebSocket conn, ClientHandshake handshake) {Log.d(TAG, "onOpen");mWebSocket = conn;}視頻編碼器ScreenEncoder采用采用H.265編碼(H.265/HEVC),需要注意不同手機支持的編碼最大分辨率不同。
package com.example.screenprojection;import android.hardware.display.DisplayManager; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.projection.MediaProjection; import android.view.Surface;import java.io.IOException; import java.nio.ByteBuffer;/*** 采用H.265編碼 H.265/HEVC的編碼架構大致上和H.264/AVC的架構相似 H.265又稱為HEVC(全稱High Efficiency Video Coding,高效率視頻編碼)*/ public class ScreenEncoder extends Thread {//不同手機支持的編碼最大分辨率不同private static final int VIDEO_WIDTH = 2160;private static final int VIDEO_HEIGHT = 3840;private static final int SCREEN_FRAME_RATE = 20;private static final int SCREEN_FRAME_INTERVAL = 1;private static final long SOCKET_TIME_OUT = 10000;// I幀private static final int TYPE_FRAME_INTERVAL = 19;// vps幀private static final int TYPE_FRAME_VPS = 32;private final MediaProjection mMediaProjection;private final SocketManager mSocketManager;private MediaCodec mMediaCodec;private boolean mPlaying = true;// 記錄vps pps spsprivate byte[] vps_pps_sps;public ScreenEncoder(SocketManager socketManager, MediaProjection mediaProjection) {mSocketManager = socketManager;mMediaProjection = mediaProjection;}public void startEncode() {MediaFormat mediaFormat =MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, VIDEO_WIDTH, VIDEO_HEIGHT);mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);// 比特率(比特/秒)mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT);// 幀率mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, SCREEN_FRAME_RATE);// I幀的頻率mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, SCREEN_FRAME_INTERVAL);try {mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);Surface surface = mMediaCodec.createInputSurface();mMediaProjection.createVirtualDisplay("screen",VIDEO_WIDTH,VIDEO_HEIGHT,1,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,surface,null,null);} catch (IOException e) {e.printStackTrace();}start();}@Overridepublic void run() {mMediaCodec.start();MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();while (mPlaying) {int outPutBufferId = mMediaCodec.dequeueOutputBuffer(bufferInfo, SOCKET_TIME_OUT);if (outPutBufferId >= 0) {ByteBuffer byteBuffer = mMediaCodec.getOutputBuffer(outPutBufferId);encodeData(byteBuffer, bufferInfo);mMediaCodec.releaseOutputBuffer(outPutBufferId, false);}}}private void encodeData(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {int offSet = 4;if (byteBuffer.get(2) == 0x01) {offSet = 3;}int type = (byteBuffer.get(offSet) & 0x7E) >> 1;if (type == TYPE_FRAME_VPS) {vps_pps_sps = new byte[bufferInfo.size];byteBuffer.get(vps_pps_sps);} else if (type == TYPE_FRAME_INTERVAL) {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);mSocketManager.sendData(newBytes);} else {byte[] bytes = new byte[bufferInfo.size];byteBuffer.get(bytes);mSocketManager.sendData(bytes);}}public void stopEncode() {mPlaying = false;if (mMediaCodec != null) {mMediaCodec.release();}if (mMediaProjection != null) {mMediaProjection.stop();}} }注意點:
1.需要依賴WebSocket相關jar。
implementation “org.java-websocket:Java-WebSocket:1.5.3”
2.AndroidManifest配置網絡和前臺服務權限。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />3.配置前臺錄屏服務foregroundServiceType屬性。
<serviceandroid:name=".ScreenService"android:enabled="true"android:exported="true"android:foregroundServiceType="mediaProjection" />投屏客戶端的實現
客戶端入口負責創建SurfaceView并初始化WebSocket客戶端管理器SocketClientManager。
package com.example.screenplayer;import android.os.Bundle; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView;import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private SocketClientManager mSocketClientManager;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);SurfaceView surfaceView = findViewById(R.id.sv_screen);surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {// 連接到服務端initSocketManager(holder.getSurface());}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {}});}private void initSocketManager(Surface surface) {mSocketClientManager = new SocketClientManager();mSocketClientManager.start(surface);}@Overrideprotected void onDestroy() {if (mSocketClientManager != null) {mSocketClientManager.stop();}super.onDestroy();} }SocketClientManager將Surface與解碼器綁定,創建WebSocket客戶端并接收服務端推送的視頻流。其中URI 需要修改為服務端的IP地址和端口。
package com.example.screenplayer;import android.view.Surface;import java.net.URI; import java.net.URISyntaxException;public class SocketClientManager implements ScreenSocketClient.SocketCallback {private static final int SOCKET_PORT = 50000;private ScreenDecoder mScreenDecoder;private ScreenSocketClient mSocketClient;public void start(Surface surface) {mScreenDecoder = new ScreenDecoder();mScreenDecoder.startDecode(surface);try {// 需要修改為服務端的IP地址與端口URI uri = new URI("ws://192.168.1.3:" + SOCKET_PORT);mSocketClient = new ScreenSocketClient(this, uri);mSocketClient.connect();} catch (URISyntaxException e) {e.printStackTrace();}}public void stop() {if (mSocketClient != null) {mSocketClient.close();}if (mScreenDecoder != null) {mScreenDecoder.stopDecode();}}@Overridepublic void onReceiveData(byte[] data) {if (mScreenDecoder != null) {mScreenDecoder.decodeData(data);}} }ScreenSocketClient代碼如下:
package com.example.screenplayer;import android.util.Log;import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake;import java.net.URI; import java.nio.ByteBuffer;public class ScreenSocketClient extends WebSocketClient {private static final String TAG = ScreenSocketClient.class.getSimpleName();private final SocketCallback mSocketCallback;public ScreenSocketClient(SocketCallback socketCallback, URI serverUri) {super(serverUri);mSocketCallback = socketCallback;}@Overridepublic void onOpen(ServerHandshake serverHandshake) {Log.d(TAG, "onOpen");}@Overridepublic void onMessage(String message) {}@Overridepublic void onMessage(ByteBuffer bytes) {byte[] buf = new byte[bytes.remaining()];bytes.get(buf);if (mSocketCallback != null) {mSocketCallback.onReceiveData(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 onReceiveData(byte[] data);} }不同手機支持的解碼最大分辨率不同,需要設置正確的分辨率才不會報錯。投屏解碼器ScreenDecoder代碼如下:
package com.example.screenplayer;import android.media.MediaCodec; import android.media.MediaFormat; import android.view.Surface;import java.io.IOException; import java.nio.ByteBuffer;public class ScreenDecoder {private static final int VIDEO_WIDTH = 2160;private static final int VIDEO_HEIGHT = 3840;private static final long DECODE_TIME_OUT = 10000;private static final int SCREEN_FRAME_RATE = 20;private static final int SCREEN_FRAME_INTERVAL = 1;private MediaCodec mMediaCodec;public ScreenDecoder() {}public void startDecode(Surface surface) {try {// 配置MediaCodecmMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);MediaFormat mediaFormat =MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, VIDEO_WIDTH, VIDEO_HEIGHT);mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT);mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, SCREEN_FRAME_RATE);mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, SCREEN_FRAME_INTERVAL);mMediaCodec.configure(mediaFormat, surface, null, 0);mMediaCodec.start();} catch (IOException e) {e.printStackTrace();}}public void decodeData(byte[] data) {int index = mMediaCodec.dequeueInputBuffer(DECODE_TIME_OUT);if (index >= 0) {ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(index);inputBuffer.clear();inputBuffer.put(data, 0, data.length);mMediaCodec.queueInputBuffer(index, 0, data.length, System.currentTimeMillis(), 0);}MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, DECODE_TIME_OUT);while (outputBufferIndex > 0) {mMediaCodec.releaseOutputBuffer(outputBufferIndex, true);outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);}}public void stopDecode() {if (mMediaCodec != null) {mMediaCodec.release();}} }注意點:
1.需要依賴WebSocket相關jar。
implementation “org.java-websocket:Java-WebSocket:1.5.3”
2.AndroidManifest配置網絡權限。
<uses-permission android:name="android.permission.INTERNET"/>效果
左手邊為服務端,右手邊為客戶端,服務端投屏到客戶端的效果如下:
遇到的錯誤
1.WebSocket出現Permission denied的報錯:
onerror =java.net.SocketException: socket failed: EACCES (Permission denied)
問題原因:需要網絡權限。
<uses-permission android:name=“android.permission.INTERNET”/>
2.WebSocket出現Host unreachable報錯:
onerror =java.net.NoRouteToHostException: Host unreachable
問題原因:不在同一局域網內或服務端未正常啟動。
3.WebSocket出現failed to connect報錯:
failed to connect to /192.168.1.3 (port 50000) from /:: (port 38262):,不在同一局域網內或服務端未正常啟動
問題原因:不在同一局域網內或服務端未正常啟動。
客戶端連接成功回調onOpen方法
@Overridepublic void onOpen(ServerHandshake handshakedata) {Log.d(TAG,"SocketClient onOpen");}4.初始化MediaFormat時報IllegalStateException異常:
Caused by: java.lang.IllegalArgumentException
at android.media.MediaCodec.native_configure(Native Method)
at android.media.MediaCodec.configure(MediaCodec.java:2023)
at android.media.MediaCodec.configure(MediaCodec.java:1951)
at com.example.screenprojection.ScreenEncoder.startEncode(ScreenEncoder.java:56)
問題原因:不同手機支持的最大編解碼分辨率不同,可通過adb shell cat /system/etc/media_codecs.xml的adb命令查看手機支持的編解碼分辨率。未設置編碼強制要求的一些配置 會拋出 IllegalStateException。
<MediaCodec name="OMX.hisi.video.decoder.hevc.secure" type="video/hevc" ><Quirk name="needs-flush-on-all-ports" /><Limit name="size" min="128x128" max="4096x2304" /><Limit name="alignment" value="2x2" /><Limit name="block-size" value="16x16" /><Limit name="block-count" range="64-36896" /><Limit name="blocks-per-second" range="99-1106880" /><Limit name="bitrate" range="1-52428800" /><Feature name="adaptive-playback" /><Feature name="secure-playback" required="true" /><Quirk name="requires-allocate-on-input-ports" /><Quirk name="requires-allocate-on-output-ports" /><Limit name="concurrent-instances" max="1" /></MediaCodec><!--config suggestion for hevc --><!--VT:1280x720@1Mbps@30fps,640x360@450kpbps@30fps,640x360@250kbps@30fps,640x360@170kbps@15fps--><!--DestkTop Share: 1280x720@500kbps@3-7fps,720x450@200kbps@3-7fps--><!--Wifi Display:1920x1080@8Mbps@30fps 4Kx2k@27M@30fps--><!--Recoding:1920x1080@12Mbps@30fps,4k2k@30Mbps@30fps,1920x1080@40M@120fps,1280X720@40M@120fps--><MediaCodec name="OMX.hisi.video.encoder.hevc" type="video/hevc" ><Limit name="size" min="176x144" max="3840x2160" /><Limit name="alignment" value="4x4" /><Limit name="block-size" value="64x64" /><Limit name="blocks-per-second" range="99-244800" /><Limit name="bitrate" range="200000-70000000" /><Quirk name="requires-allocate-on-output-ports" /><Limit name="concurrent-instances" max="3" /></MediaCodec>源碼下載地址:無線投屏源碼
總結
以上是生活随笔為你收集整理的android设备间实现无线投屏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android ping命令 -- Ru
- 下一篇: 正则验证金额大于等于0,并且只到小数点后