Android PC投屏简单尝试

回顾之前的几遍文章,我们分别通过RMTP协议和简单的Socket 发送Bitmap图片的Base64编码来完成投屏。
回想这系列文章的想法来源-Vysor,它通过 USB来进行连接的。又看到了 scrcpy项目。
于是有了这个系列的*终章-仿scrcpy(Vysor)
ps:其实就是对着scrcpy的源码撸了一遍。

效果预览

%title插图%num

简单的录制效果.gif

源码地址:https://github.com/deepsadness/AppRemote

内容目录

包括的内容有

  1. 通过USB连接和adb进行手机通信
  2. 在Android端发送录制屏幕的H264 Naul
  3. 使用SDL2和FFmpeg,编写能够在PC(Windows,Mac)上运行的投屏界面

1. USB Socket连接

  • 熟悉adb foward 和 adb reverse 命令
    由于Android版本低于5.0版本不支持adb reverse

%title插图%num

adb forward.png

  1. forward –list list all forward socket connections
  2. forward [–no-rebind] LOCAL REMOTE
  3. forward socket connection using:
  4. tcp:<port> (<local> may be “tcp:0” to pick any open port)
  5. localabstract:<unix domain socket name>
  6. localreserved:<unix domain socket name>
  7. localfilesystem:<unix domain socket name>
  8. dev:<character device name>
  9. jdwp:<process pid> (remote only)
  10. forward –remove LOCAL remove specific forward socket connection
  11. forward –remove-all remove all forward socket connections

这个命令的意思是 将PC上的端口(LOCAL) 转发到 Android手机上(REMOTE) 。
这样的话,我们就可以在Android段建立Server,监听我们的REMOTE,而PC端可以通过连接连接这个LOCAL,就可以成功的建立Socket连接。
还可以注意到,我们使用的LOCALREMOTE除了可以使用 TCP的端口的形式,还可以使用 UNIX Domain Socket IPC协议 。

通常我们可以使用
adb forward tcp:8888 tcp:8888来监听两端的端口。
下面是简单的调试代码

调试代码

下面通过两种方式来进行通信

使用 tcp port的方式

  • 命令行
    adb forward tcp:8888 tcp:8888
  • socket Server(android 端)
  1. public class PortServer {
  2. public static void start() {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. //可以直接使用抽象的名字作为socket的名称
  7. ServerSocket serverSocket = null;
  8. try {
  9. serverSocket = new ServerSocket(8888);
  10. //blocking
  11. Socket accept = serverSocket.accept();
  12. Log.d(“ZZX”, “serverSocket 链接成功”);
  13. while (true) {
  14. if (!accept.isConnected()) {
  15. return;
  16. }
  17. InputStream inputStream = accept.getInputStream();
  18. String result = IO.streamToString(inputStream);
  19. Log.d(“ZZX”, “serverSocket recv =” + result);
  20. }
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }).start();
  28. }
  29. }
  • socket client 端
    java 版本
  1. private static void socket() {
  2. //开启socket clinet
  3. Socket clinet = new Socket();
  4. //用adb转发端口到8888
  5. try {
  6. //blocking
  7. clinet.connect(new InetSocketAddress(“127.0.0.1”, 8888));
  8. System.out.println(“连接成功!!”);
  9. OutputStream outputStream = clinet.getOutputStream();
  10. outputStream.write(“Hello World”.getBytes());
  11. outputStream.flush();
  12. outputStream.close();
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }

使用 localabstract 的方式

  • 命令行
    adb forward tcp:8888 localabstract: local

需要修改的只有Server 端。因为我们将服务端(Android端)改成了localabstract的方式。

  • android
  1. public class LocalServer {
  2. public static void start() {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. //可以直接使用抽象的名字作为socket的名称
  7. LocalServerSocket serverSocket = null;
  8. try {
  9. serverSocket = new LocalServerSocket(“local”);
  10. //blocking
  11. LocalSocket client = serverSocket.accept();
  12. Log.d(“ZZX”, “serverSocket 链接成功”);
  13. while (true) {
  14. if (!client.isConnected()) {
  15. return;
  16. }
  17. InputStream inputStream = client.getInputStream();
  18. String result = IO.streamToString(inputStream);
  19. Log.d(“ZZX”, “serverSocket recv =” + result);
  20. }
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }).start();
  28. }
  29. }

这样我们就通过ADB协议和USB 建立了手机和PC端的通信。之后我们就可以通过这里建立的socket,进行数据传递。

2. 在Android上运行后台程序

我们期望运行一个后台的程序,PC端开启之后,就开始给我们发送截屏的数据。
这里涉及了几个问题。
*个是在之前的文章中,我们知道,我们需要进行屏幕的截屏,需要申请对应的权限。
第二个是,如何直接在Android上运行我们写好的java程序,还不是用Activity的方式来运行。

使用app_process 运行程序

这个命令完美的满足了我们的需求。
它不但可以在后台直接运行dex中的java文件,还具有较高的权限!!!

调试代码

我们先来写一个简单的类试一下

  • 编写一个简单的java文件
  1. public class HelloWorld {
  2. public static void main(String… args) {
  3. System.out.println(“Hello, world!”);
  4. }
  5. }
  • 将其编译成dex文件
  1. javac -source 1.7 -target 1.7 HelloWorld.java
  2. $ANDROID_HOME/build-tools/27.0.2/dx \
  3. –dex –output classes.dex HelloWorld.class
  • 推送到设备上并执行它
  1. adb push classes.dex /data/local/tmp/
  2. $ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld

这样我们就运行成功了~~

这里需要注意的是app_process 只能执行dex文件

  • adb_forward
  1. task adb_forward() {
  2. doLast {
  3. println “adb_forward”
  4. def cmd = “adb forward –list”
  5. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  6. Process adbpb = builder.start()
  7. if (adbpb.waitFor() == 0) {
  8. def result = adbpb.inputStream.text
  9. println result.length()
  10. if (result.length() <= 1) {
  11. def forward_recorder = “adb forward tcp:9000 localabstract:recorder”
  12. ProcessBuilder forward_pb = new ProcessBuilder(forward_recorder.split(” “))
  13. Process forward_ps = forward_pb.start()
  14. if (forward_ps.waitFor() == 0) {
  15. println “forward success!!”
  16. }
  17. } else {
  18. println result
  19. }
  20. } else {
  21. println “error = ” + adbpb.errorStream.text
  22. }
  23. }
  24. }

在AndroidStudio中编译

上面我体验了简单的通过命令行的形式来编译。实际上,我们的开发都是在AndroidStudio中的。而且它为我们提供了一个很好的编译环境。不需要在手动去敲入这些代码。
我们只需要通过自定义Gradle Task就可以简单的完成这个任务。

  • 单纯复制dex文件
    因为运行的是dex文件,所以我们直接复制dex文件就行了。
    这个任务必须依赖于assembleDebug任务。因为只有这个任务执行完,才会有这些dex文件。
  1. //将dex文件复制。通过dependsOn来制定依赖关系
  2. //因为是设定了type是gradle中已经实现了Copy,所以直接配置它的属性
  3. task class_cls(type: Copy, dependsOn: “assembleDebug”) {
  4. from “build/intermediates/transforms/dexMerger/debug/0/”
  5. destinationDir = file(‘build/libs’)
  6. }
  • 压缩成jar
    如果不直接使用dex的话,也可以压缩成jar的形式。其实和上面直接使用dex的方式没差。就看你自己喜欢了。
  1. //将编译好的dex文件压缩成jar.
  2. task classex_jar(type: Jar, dependsOn: “assembleDebug”) {
  3. from “build/intermediates/transforms/dexMerger/debug/0/”
  4. destinationDir = file(‘build/libs’)
  5. archiveName ‘class.jar’
  6. }
  • 直接将编译好的结果,push到手机上
    这里再写一个push的task,并把将其依赖于classex_jar任务。这样运行它时,会先去运行我们依赖的classex_jar为我们打包。
  1. //将编译好的push到手机上
  2. //这里,因为是自己定义的项目类型。要把执行的代码,写在Action内
  3. //直接跟在后面的这个闭包,是在项目配置阶段运行的
  4. task adb_push(dependsOn: “classex_jar”) {
  5. //doLast是定义个Action。Action是在task运行阶段运行的
  6. doLast {
  7. File file = new File(“./app/build/libs/class.jar”)
  8. def jarDir = file.getAbsolutePath()
  9. def cmd = “adb push $jarDir /data/local/tmp”
  10. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  11. Process push = builder.start()
  12. if (push.waitFor() == 0) {
  13. println “result = “ + push.inputStream.text
  14. } else {
  15. println “error = “ + push.errorStream.text
  16. }
  17. }
  18. }
  • 直接在项目里运行调试
    我们也可以直接在项目将调试的代码运行起来。
  1. //直接运行,查看结果
  2. task adb_exc(dependsOn: “adb_push”) {
  3. //相当于制定了一个Action,在这个任务的*后执行
  4. doLast {
  5. println “adb_exc”
  6. def cmd = “adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld”
  7. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  8. Process adbpb = builder.start()
  9. println “start adb “
  10. if (adbpb.waitFor() == 0) {
  11. println “result = “ + adbpb.inputStream.text
  12. } else {
  13. println “error = “ + adbpb.errorStream.text
  14. }
  15. }
  16. }

运行结果

 

%title插图%num

运行任务.png

%title插图%num

基于gradle任务快速调试.png

这样,就把我们就可以在AndroidStudio中快速的运行调试我们的代码了~~

3. 通过Android程序,获取设备的信息和录制数据

3.1 获取设备信息

  • 获取ServiceManager
    我们平时通过Context中暴露的getService方法。来调用对应的Service 来获取设备信息的。
    因为我们是后台运行的程序,没有对应的Context。那我们要怎么办?
    我们知道Android的系统架构中,其实所有的getService方法,*后都是落实在ServerManager这个代理类中,去获取Service ManagerService中对应的真实注册的Service的远程代理对象。
    所以这里,我们就通过反射,来创建ServiceManager,同时通过它,来获取我们需要的Service的远程代理对象。
  1. @SuppressLint(“PrivateApi”)
  2. public final class ServiceManager {
  3. private final Method getServiceMethod;
  4. private DisplayManager displayManager;
  5. private PowerManager powerManager;
  6. private InputManager inputManager;
  7. public ServiceManager() {
  8. try {
  9. getServiceMethod = Class.forName(“android.os.ServiceManager”).getDeclaredMethod(“getService”, String.class);
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. throw new AssertionError(e);
  13. }
  14. }
  15. private IInterface getService(String service, String type) {
  16. try {
  17. IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
  18. Method asInterface = Class.forName(type + “$Stub”).getMethod(“asInterface”, IBinder.class);
  19. return (IInterface) asInterface.invoke(null, binder);
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. throw new AssertionError(e);
  23. }
  24. }
  25. public DisplayManager getDisplayManager() {
  26. if (displayManager == null) {
  27. IInterface service = getService(Context.DISPLAY_SERVICE, “android.hardware.display.IDisplayManager”);
  28. displayManager = new DisplayManager(service);
  29. }
  30. return displayManager;
  31. }
  32. public PowerManager getPowerManager() {
  33. if (powerManager == null) {
  34. IInterface service = getService(Context.POWER_SERVICE, “android.os.IPowerManager”);
  35. powerManager = new PowerManager(service);
  36. }
  37. return powerManager;
  38. }
  39. public InputManager getInputManager() {
  40. if (inputManager == null) {
  41. IInterface service = getService(Context.INPUT_SERVICE, “android.hardware.input.IInputManager”);
  42. inputManager = new InputManager(service);
  43. }
  44. return inputManager;
  45. }
  46. }

通过getService方法,获取BINDER对象之后,在通过对应的实现类的StubasInterface方法,转成对应的远程代理类。

  • 获取屏幕信息
    接下来,我们通过得到的远程代理对象,就可以调用方法了
  1. public class DisplayManager {
  2. /**
  3. * 这个service 对应 final class BinderService extends IDisplayManager.Stub
  4. */
  5. private final IInterface service;
  6. public DisplayManager(IInterface service) {
  7. this.service = service;
  8. }
  9. public DisplayInfo getDisplayInfo() {
  10. try {
  11. Object displayInfo = service.getClass().getMethod(“getDisplayInfo”, int.class)
  12. .invoke(service, 0);
  13. Class<?> cls = displayInfo.getClass();
  14. // width and height already take the rotation into account
  15. int width = cls.getDeclaredField(“logicalWidth”).getInt(displayInfo);
  16. int height = cls.getDeclaredField(“logicalHeight”).getInt(displayInfo);
  17. int rotation = cls.getDeclaredField(“rotation”).getInt(displayInfo);
  18. return new DisplayInfo(new Size(width, height), rotation);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. throw new AssertionError(e);
  22. }
  23. }
  24. /*
  25. 这方法是在DisplayManager里面有,但是DisplayManagerService内,没有。所以没法调用
  26. public DisplayInfo getDisplay() {
  27. try {
  28. Object display = service.getClass().getMethod(“getDisplay”, int.class)
  29. .invoke(service, 0);
  30. Point point = new Point();
  31. Method getSize = display.getClass().getMethod(“getSize”, Point.class);
  32. Method getRotation = display.getClass().getMethod(“getRotation”);
  33. getSize.invoke(display, point);
  34. int rotation = (int) getRotation.invoke(display);
  35. return new DisplayInfo(new Size(point.x, point.y), rotation);
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. throw new AssertionError(e);
  39. }
  40. }
  41. */
  42. }

3.2 进行屏幕录制

前几遍文章,我们都是通过MediaProjection来完成我们的屏幕录制的。
因为截屏需要MediaProjection这个类。它实际上是一个Serivce

  1. //在Activity中是通过这样的方式,来获取VirtualDisplay的
  2. MediaProjectionManager systemService = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
  3. MediaProjection mediaProjection = systemService.getMediaProjection();
  4. mediaProjection.createVirtualDisplay(“activity-request”, widht, height, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);

在Activity中实际上的代码是这样的。通过MediaProjectionManager来获取一个MediaProjection。然后根据我们的surface来创建一个virtualDisplay
整个步骤中,需要context来获取MediaProjectionManager ,还需要用户授权之后,才能创建VirtualDisplay

然而,我们这里既不可能让用户授权,也没有Context,要怎么办呢?

SurfaceControl

我们可以参考 adb screenrecord 命令项目中 的 ScreenRecord的操作

  1. /*
  2. * Configures the virtual display. When this completes, virtual display
  3. * frames will start arriving from the buffer producer.
  4. */
  5. static status_t prepareVirtualDisplay(const DisplayInfo& mainDpyInfo,
  6. const sp<IGraphicBufferProducer>& bufferProducer,
  7. sp<IBinder>* pDisplayHandle) {
  8. sp<IBinder> dpy = SurfaceComposerClient::createDisplay(
  9. String8(“ScreenRecorder”), false /*secure*/);
  10. SurfaceComposerClient::Transaction t;
  11. t.setDisplaySurface(dpy, bufferProducer);
  12. setDisplayProjection(t, dpy, mainDpyInfo);
  13. t.setDisplayLayerStack(dpy, 0); // default stack
  14. t.apply();
  15. *pDisplayHandle = dpy;
  16. return NO_ERROR;
  17. }

Native中的SurfaceComposerClient在Java层中,对应的就是SurfaceControl。
我们只要同样按照这样的方式调用SurfaceControl就可以了。

  • 获取SurfaceControl
    同样可以通过反射的方式来进行获取
  1. @SuppressLint(“PrivateApi”)
  2. public class SurfaceControl {
  3. private static final Class<?> CLASS;
  4. static {
  5. try {
  6. CLASS = Class.forName(“android.view.SurfaceControl“);
  7. } catch (ClassNotFoundException e) {
  8. throw new AssertionError(e);
  9. }
  10. }
  11. private SurfaceControl() {
  12. // only static methods
  13. }
  14. public static void openTransaction() {
  15. try {
  16. CLASS.getMethod(“openTransaction”).invoke(null);
  17. } catch (Exception e) {
  18. throw new AssertionError(e);
  19. }
  20. }
  21. public static void closeTransaction() {
  22. try {
  23. CLASS.getMethod(“closeTransaction”).invoke(null);
  24. } catch (Exception e) {
  25. throw new AssertionError(e);
  26. }
  27. }
  28. public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
  29. try {
  30. CLASS.getMethod(“setDisplayProjection”, IBinder.class, int.class, Rect.class, Rect.class)
  31. .invoke(null, displayToken, orientation, layerStackRect, displayRect);
  32. } catch (Exception e) {
  33. throw new AssertionError(e);
  34. }
  35. }
  36. public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
  37. try {
  38. CLASS.getMethod(“setDisplayLayerStack”, IBinder.class, int.class).invoke(null, displayToken, layerStack);
  39. } catch (Exception e) {
  40. throw new AssertionError(e);
  41. }
  42. }
  43. public static void setDisplaySurface(IBinder displayToken, Surface surface) {
  44. try {
  45. CLASS.getMethod(“setDisplaySurface”, IBinder.class, Surface.class).invoke(null, displayToken, surface);
  46. } catch (Exception e) {
  47. throw new AssertionError(e);
  48. }
  49. }
  50. public static IBinder createDisplay(String name, boolean secure) {
  51. try {
  52. return (IBinder) CLASS.getMethod(“createDisplay”, String.class, boolean.class).invoke(null, name, secure);
  53. } catch (Exception e) {
  54. throw new AssertionError(e);
  55. }
  56. }
  57. public static void destroyDisplay(IBinder displayToken) {
  58. try {
  59. CLASS.getMethod(“destroyDisplay”, IBinder.class).invoke(null, displayToken);
  60. } catch (Exception e) {
  61. e.printStackTrace();
  62. throw new AssertionError(e);
  63. }
  64. }
  65. }
  • 调用录屏
  1. public void streamScreen(){
  2. IBinder display = createDisplay();
  3. //…
  4. setDisplaySurface(display, surface, contentRect, videoRect);
  5. }
  6. private static IBinder createDisplay() {
  7. return SurfaceControl.createDisplay(“scrcpy”, false);
  8. }
  9. private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
  10. SurfaceControl.openTransaction();
  11. try {
  12. SurfaceControl.setDisplaySurface(display, surface);
  13. SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
  14. SurfaceControl.setDisplayLayerStack(display, 0);
  15. } finally {
  16. SurfaceControl.closeTransaction();
  17. }
  18. }
  • 进行录制的代码
    其实我们已经经历过很多很多次了。
    将录制的数据输入MediaCodec的Surface中。然后就可以得到编码之后的的数据了。
    再将这个数据通过socket发送
  1. package com.cry.cry.appprocessdemo;
  2. import android.graphics.Rect;
  3. import android.media.MediaCodec;
  4. import android.media.MediaCodecInfo;
  5. import android.media.MediaFormat;
  6. import android.os.IBinder;
  7. import android.view.Surface;
  8. import com.cry.cry.appprocessdemo.refect.SurfaceControl;
  9. import java.io.FileDescriptor;
  10. import java.io.IOException;
  11. import java.nio.ByteBuffer;
  12. public class ScreenRecorder {
  13. private static final int DEFAULT_FRAME_RATE = 60; // fps
  14. private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
  15. private static final int DEFAULT_BIT_RATE = 8000000; // 8Mbps
  16. private static final int DEFAULT_TIME_OUT = 2 * 1000; // 2s
  17. private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
  18. private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
  19. private static final int NO_PTS = –1;
  20. private boolean sendFrameMeta = false;
  21. private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
  22. private long ptsOrigin;
  23. private volatile boolean stop;
  24. private MediaCodec encoder;
  25. public void setStop(boolean stop) {
  26. this.stop = stop;
  27. // encoder.signalEndOfInputStream();
  28. }
  29. //进行录制的循环,录制得到的数据,都写到fd当中
  30. public void record(int width, int height, FileDescriptor fd) {
  31. //对MediaCodec进行配置
  32. boolean alive;
  33. try {
  34. do {
  35. MediaFormat mediaFormat = createMediaFormat(DEFAULT_BIT_RATE, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
  36. mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
  37. mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
  38. encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
  39. //输入输出的surface 这里是没有
  40. encoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  41. Surface inputSurface = encoder.createInputSurface();
  42. IBinder surfaceClient = setDisplaySurface(width, height, inputSurface);
  43. encoder.start();
  44. try {
  45. alive = encode(encoder, fd);
  46. alive = alive && !stop;
  47. System.out.println(“alive =” + alive + “, stop=” + stop);
  48. } finally {
  49. System.out.println(“encoder.stop”);
  50. //为什么调用stop会block主呢?
  51. // encoder.stop();
  52. System.out.println(“destroyDisplaySurface”);
  53. destroyDisplaySurface(surfaceClient);
  54. System.out.println(“encoder release”);
  55. encoder.release();
  56. System.out.println(“inputSurface release”);
  57. inputSurface.release();
  58. System.out.println(“end”);
  59. }
  60. } while (alive);
  61. } catch (IOException e) {
  62. e.printStackTrace();
  63. }
  64. System.out.println(“end record”);
  65. }
  66. //创建录制的Surface
  67. private IBinder setDisplaySurface(int width, int height, Surface inputSurface) {
  68. Rect deviceRect = new Rect(0, 0, width, height);
  69. Rect displayRect = new Rect(0, 0, width, height);
  70. IBinder surfaceClient = SurfaceControl.createDisplay(“recorder”, false);
  71. //设置和配置截屏的Surface
  72. SurfaceControl.openTransaction();
  73. try {
  74. SurfaceControl.setDisplaySurface(surfaceClient, inputSurface);
  75. SurfaceControl.setDisplayProjection(surfaceClient, 0, deviceRect, displayRect);
  76. SurfaceControl.setDisplayLayerStack(surfaceClient, 0);
  77. } finally {
  78. SurfaceControl.closeTransaction();
  79. }
  80. return surfaceClient;
  81. }
  82. private void destroyDisplaySurface(IBinder surfaceClient) {
  83. SurfaceControl.destroyDisplay(surfaceClient);
  84. }
  85. //创建MediaFormat
  86. private MediaFormat createMediaFormat(int bitRate, int frameRate, int iFrameInterval) {
  87. MediaFormat mediaFormat = new MediaFormat();
  88. mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
  89. mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
  90. mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
  91. mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
  92. mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
  93. mediaFormat.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate);//us
  94. return mediaFormat;
  95. }
  96. //进行encode
  97. private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
  98. System.out.println(“encode”);
  99. boolean eof = false;
  100. MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
  101. while (!eof) {
  102. System.out.println(“dequeueOutputBuffer outputBufferId before”);
  103. int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIME_OUT);
  104. System.out.println(“dequeueOutputBuffer outputBufferId =” + outputBufferId);
  105. eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
  106. System.out.println(“encode eof =” + eof);
  107. try {
  108. // if (consumeRotationChange()) {
  109. // // must restart encoding with new size
  110. // break;
  111. // }
  112. if (stop) {
  113. // must restart encoding with new size
  114. break;
  115. }
  116. //将得到的数据,都发送给fd
  117. if (outputBufferId >= 0) {
  118. ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
  119. System.out.println(“dequeueOutputBuffer getOutputBuffer”);
  120. if (sendFrameMeta) {
  121. writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
  122. }
  123. IO.writeFully(fd, codecBuffer);
  124. System.out.println(“writeFully”);
  125. }
  126. } finally {
  127. if (outputBufferId >= 0) {
  128. codec.releaseOutputBuffer(outputBufferId, false);
  129. System.out.println(“releaseOutputBuffer”);
  130. }
  131. }
  132. }
  133. return !eof;
  134. }
  135. private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
  136. headerBuffer.clear();
  137. long pts;
  138. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
  139. pts = NO_PTS; // non-media data packet
  140. } else {
  141. if (ptsOrigin == 0) {
  142. ptsOrigin = bufferInfo.presentationTimeUs;
  143. }
  144. pts = bufferInfo.presentationTimeUs – ptsOrigin;
  145. }
  146. headerBuffer.putLong(pts);
  147. headerBuffer.putInt(packetSize);
  148. headerBuffer.flip();
  149. IO.writeFully(fd, headerBuffer);
  150. }
  151. }
  • socket发送
    调用了Os.write方法进行发送。
  1. public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
  2. // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
  3. // count the remaining bytes manually.
  4. // See <https://github.com/Genymobile/scrcpy/issues/291>.
  5. int remaining = from.remaining();
  6. while (remaining > 0) {
  7. try {
  8. int w = Os.write(fd, from);
  9. if (BuildConfig.DEBUG && w < 0) {
  10. // w should not be negative, since an exception is thrown on error
  11. System.out.println(“Os.write() returned a negative value (“ + w + “)”);
  12. throw new AssertionError(“Os.write() returned a negative value (“ + w + “)”);
  13. }
  14. remaining -= w;
  15. } catch (ErrnoException e) {
  16. e.printStackTrace();
  17. if (e.errno != OsConstants.EINTR) {
  18. throw new IOException(e);
  19. }
  20. }
  21. }
  22. }

4. PC端建立Socket Client,接受数据。并将数据显示出来

4.1 建立Socket.

  1. //创建Socket
  2. client = socket(PF_INET, SOCK_STREAM, 0);
  3. if (!client) {
  4. perror(“can not create socket!!”);
  5. return -1;
  6. }
  7. struct sockaddr_in in_addr;
  8. memset(&in_addr, 0, sizeof(sockaddr_in));
  9. in_addr.sin_port = htons(9000);
  10. in_addr.sin_family = AF_INET;
  11. in_addr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
  12. int ret = connect(client, (struct sockaddr *) &in_addr, sizeof(struct sockaddr));
  13. if (ret < 0) {
  14. perror(“socket connect error!!\\n”);
  15. return -1;
  16. }
  17. printf(“连接成功\n”);

因为我们将PC的端口的9000转发到Android的Server上,所以我们只要去连接本地的9000端口,就相当于和Android上的Server建立了连接。

4.2 FFmepg解码和SDL2显示

在前面的其他系列文章中,对SDL2和FFmepg都进行过了介绍。而且还对ffplay的源码进行了分析。这里基本上和ffplay 的视频播放功能一样。只是我们没有传输音频数据。

FFmepg解码

这里和之前的FFmpeg解码不同的是,

  • 从内存中读取数据
    我们不是通过一个url来获取数据,而是通过socket的读取内存来进行读取数据。
    所以我们需要自己来构造这个 AVIOContext 并把它给AVFormat
  1. //通过这个方法,来进行socket的内存读取
  2. int read_socket_buffer(void *opaque, uint8_t *buf, int buf_size) {
  3. int count = recv(client, buf, buf_size, 0);
  4. if (count == 0) {
  5. return -1;
  6. }
  7. return count;
  8. }
  9. play(){
  10. avformat_network_init();
  11. AVFormatContext *format_ctx = avformat_alloc_context();
  12. unsigned char *buffer = static_cast<unsigned char *>(av_malloc(BUF_SIZE));
  13. //通过avio_alloc_context传入内存读取的地址和方法。
  14. AVIOContext *avio_ctx = avio_alloc_context(buffer, BUF_SIZE, 0, NULL, read_socket_buffer, NULL, NULL);
  15. //在给format_ctx 对象
  16. format_ctx->pb = avio_ctx;
  17. //*后在通过相同的方法打开
  18. ret = avformat_open_input(&format_ctx, NULL, NULL, NULL);
  19. if (ret < 0) {
  20. printf(“avformat_open_input error:%s\n”, av_err2str(ret));
  21. return -1;
  22. }
  23. //…
  24. }
  • 不使用avformat_find_stream_info
    因为直接发送了H264 的naul,而且没有每次都发送媒体数据,所以当我们使用avformat_find_stream_info时,会一直阻塞,获取不到。
    所以这里只需要直接创建解码器,进行read_frame就可以了~~
  1. AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
  2. if (!codec) {
  3. printf(“Did not find a video codec \n”);
  4. return -1;
  5. }
  6. AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
  7. if (!codec_ctx) {
  8. printf(“Did not alloc AVCodecContext \n”);
  9. return -1;
  10. }
  11. // avcodec_parameters_to_context(codec_ctx, video_stream->codecpar);
  12. // width=1080, height=1920
  13. //这里的解码器的长宽,只有在read_frame 之后,才能正确获取。我们这里就先死。
  14. // *后的项目,应该是先通过Android发送屏幕数据过来的。
  15. codec_ctx->width = 1080 / 2;
  16. codec_ctx->width = 720 / 2;
  17. codec_ctx->height = 1920 / 2;
  18. codec_ctx->height = 1280 / 2;
  19. ret = avcodec_open2(codec_ctx, codec, NULL);
  20. if (ret < 0) {
  21. printf(“avcodec_open2 error:%s\n”, av_err2str(ret));
  22. return -1;
  23. }
  24. printf(“成功打开编码器\n”);

SDL2显示

我们将解码后的数据,送入编码器进行显示就可以了

  • 创建SDLScreen
  1. //创建SDLScreen.
  2. SDL_Screen *sc = new SDL_Screen(“memory”, codec_ctx->width, codec_ctx->height);
  3. ret = sc->init();
  4. if (ret < 0) {
  5. printf(“SDL_Screen init error\n”);
  6. return -1;
  7. }

这里需要注意,其实这里的屏幕尺寸也是需要通过计算的。这里就简单写死了。

  • 简单的编码循环后,送入显示
  1. AVFrame *pFrame = av_frame_alloc();
  2. //对比使用av_init_packet 它必须已经为packet初始化好了内存,只是设置默认值。
  3. AVPacket *packet = av_packet_alloc();
  4. while (av_read_frame(format_ctx, packet) >= 0) {
  5. // printf(“av_read_frame success\n”);
  6. printf(“widht=%d\n”, codec_ctx->width);
  7. // if (packet->stream_index == video_index) {
  8. while (1) {
  9. ret = avcodec_send_packet(codec_ctx, packet);
  10. if (ret == 0) {
  11. // printf(“avcodec_send_packet success\n”);
  12. //成功找到了
  13. break;
  14. } else if (ret == AVERROR(EAGAIN)) {
  15. // printf(“avcodec_send_packet EAGAIN\n”);
  16. break;
  17. } else {
  18. printf(“avcodec_send_packet error:%s\n”, av_err2str(ret));
  19. av_packet_unref(packet);
  20. goto quit;
  21. }
  22. }
  23. // while (1) {
  24. ret = avcodec_receive_frame(codec_ctx, pFrame);
  25. if (ret == 0) {
  26. //成功找到了
  27. // printf(“avcodec_receive_frame success\n”);
  28. // break;
  29. } else if (ret == AVERROR(EAGAIN)) {
  30. // printf(“avcodec_receive_frame EAGAIN\n”);
  31. // break;
  32. } else {
  33. printf(“avcodec_receive_frame error:%s\n”, av_err2str(ret));
  34. goto quit;
  35. }
  36. // }
  37. //送现
  38. sc->send_frame(pFrame);
  39. //如果已经读完,就GG
  40. if (avio_ctx->eof_reached) {
  41. break;
  42. }
  43. // }
  44. av_packet_unref(packet);
  45. }
  46. quit:
  47. if (client >= 0) {
  48. close(client);
  49. client = 0;
  50. }
  51. avformat_close_input(&format_ctx);
  52. sc->destroy();
  53. return 0;
  54. }

这样,我们就初步完成了PC的投屏功能了。

额外-开发环境

  • mac上SDL2和FFmpeg开发环境的搭建
    因为在Clion中进行开发,所以只要简单的配置CmakeList.txt就可以了。
  1. cmake_minimum_required(VERSION 3.13)
  2. project(SDLDemo)
  3. set(CMAKE_CXX_STANDARD 14)
  4. include_directories(/usr/local/Cellar/ffmpeg/4.0.3/include/)
  5. link_directories(/usr/local/Cellar/ffmpeg/4.0.3/lib/)
  6. include_directories(/usr/local/Cellar/sdl2/2.0.8/include/)
  7. link_directories(/usr/local/Cellar/sdl2/2.0.8/lib/)
  8. set(SOURCE_FILES main.cpp MSPlayer.cpp MSPlayer.h )
  9. add_executable(SDLDemo ${SOURCE_FILES})
  10. target_link_libraries(
  11. SDLDemo
  12. avcodec
  13. avdevice
  14. avfilter
  15. avformat
  16. avresample
  17. avutil
  18. postproc
  19. swresample
  20. swscale
  21. SDL2
  22. )

运行

  1. Android手机通过USB连接电脑,开启USB调试模式
  2. 在Studio 的项目中。运行gradle中的 adb_forward 和adb_push 任务。%title插图%num

    adb forward result.png

%title插图%num

adb_push result.png

  1. 进入adb shell 运行app_process
adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld

%title插图%num

app_process.png

  1. 然后点击运行PC上的项目,就可以看到弹出的屏幕了。

总结

通过上述的操作,我们通过USB和ADB命令,结合SDL2的提供的API和FFMpeg解码实现了显示。从而基本实现了PC投屏。

但是还是存在缺陷

  1. 屏幕的尺寸是我们写死的。在不同分辨率的手机上需要每次都进行调整,才能显示正常。
  2. 我们还期望能够通过PC来对手机进行控制
  3. 目前直接在主线程中进行解码和显示,因为解码的延迟,很快就能感到屏幕和手机上的延迟越来越大。

android DLNA投屏

android投屏技术的基本原理就是根据DLNA以及UPNP来实现,另外还有些黑科技技术便是根据端口号或者通过广播来用adb下载本身相关的apk来间接实现投屏,当然此处不提及黑科技。原理什么的百度一堆。这里主要讲实现方式和具体实现的代码。

那么,开始开发这玩意的时候肯定要先看看有没有现成的轮子,git上是有轮子的,链接如下:

https://github.com/4thline/cling *基础的包,main类方法中有基础的调用方式

https://github.com/offbye/DroidDLNA 在cling上改动,代码表现为当前设备的媒资资源DLNA投屏,没涉及到相关的网络媒资投屏

 

DroidDLNA 中是有bug的,具体表现在api版本为25以后,是不能投屏的,原因出自AndroidNetworkAddressFactory类中的反射问题,api版本25以后,结果与之前完全不一致,改法已经在cling中完善,代码改动如下:

@Override
protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) {
boolean result = super.isUsableAddress(networkInterface, address);
if (result) {
// TODO: Workaround Android DNS reverse lookup issue, still a problem on ICS+?
// http://4thline.org/projects/mailinglists.html#nabble-td3011461
String hostName = address.getHostAddress();
Log.e(“gjh test Log”, “sdk version=” + Build.VERSION.SDK_INT + “->hostName=” + hostName);
try {
Field field0 = null;
Object target = null;
try {
field0 = InetAddress.class.getDeclaredField(“holder”);
field0.setAccessible(true);
target = field0.get(address);
field0 = target.getClass().getDeclaredField(“hostName”);
} catch( NoSuchFieldException e ) {
// Let’s try the non-OpenJDK variant
field0 = InetAddress.class.getDeclaredField(“hostName”);
target = address;
}
if (field0 != null && hostName != null) {
field0.setAccessible(true);
field0.set(target, hostName);
} else {
return false;
}
} catch (Exception ex) {
log.log(Level.SEVERE,
“Failed injecting hostName to work around Android InetAddress DNS bug: ” + address,
ex
);
return false;
}
}
return result;
}
DLNA轮子中投屏的方法为广播,代码表现为:

private void jumpToControl(ContentItem localContentItem) {

Intent localIntent = new Intent(“com.transport.info”);
localIntent.putExtra(“name”, localContentItem.toString());
localIntent.putExtra(“playURI”, localContentItem.getItem()
.getFirstResource().getValue());
localIntent.putExtra(“currentContentFormatMimeType”,
currentContentFormatMimeType);
try {
localIntent.putExtra(“metaData”,
new GenerateXml().generate(localContentItem));
} catch (Exception e) {
e.printStackTrace();
}
IndexActivity.mTabHost.setCurrentTabByTag(getString(R.string.control));
IndexActivity.setSelect();
this.sendBroadcast(localIntent);
}
当然如果需求为投射本地已经存在的资源的话只要稍微改动下界面和bug即可。

下面说说如何投射网络在线视频。

从上面的DLNA播放本地资源已经大致可以猜到只要修改其中的name、playURI和metaData即可。

但实际上我们只要改动metaData,因为*终解析是根据MetaData来拿到相关的媒资数据,其他大致上没什么用处

具体实现的代码如下:

 

private AndroidUpnpService upnpService;//DLNA投屏服务
private DeviceListRegistryListener deviceListRegistryListener;//搜索设备的回调
mContext.bindService(
new Intent(mContext, AndroidUpnpServiceImpl.class),
serviceConnection, Context.BIND_AUTO_CREATE);
private ServiceConnection serviceConnection = new ServiceConnection() {

public void onServiceConnected(ComponentName className, IBinder service) {
if (mContext != null) {
upnpService = (AndroidUpnpService) service;//本地设备服务,用于执行投屏命令
upnpService.getRegistry().addListener(deviceListRegistryListener);//搜索设备的回调
ThreadManager.execute(new Runnable() {
@Override
public void run() {
if (mContext != null && upnpService != null) {
upnpService.getControlPoint().search();//搜索相关设备
}
}
});
}
}

public void onServiceDisconnected(ComponentName className) {
upnpService = null;
}
};

public class DeviceListRegistryListener extends DefaultRegistryListener {

@Override
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
if (device.getType().getNamespace().equals(“schemas-upnp-org”)
&& device.getType().getType().equals(“MediaRenderer”)) {
final DeviceItem dmrDisplay = new DeviceItem(device, device
.getDetails().getFriendlyName(),
device.getDisplayString(), “(REMOTE) ”
+ device.getType().getDisplayString());
dmrRemoved(dmrDisplay);
}
}

@Override
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
if (device.getType().getNamespace().equals(“schemas-upnp-org”)
&& device.getType().getType().equals(“MediaRenderer”)) {
final DeviceItem dmrDisplay = new DeviceItem(device, device
.getDetails().getFriendlyName(),
device.getDisplayString(), “(REMOTE) ”
+ device.getType().getDisplayString());
dmrAdded(dmrDisplay);
}
}

public void dmrAdded(final DeviceItem di) {
if (mTvDataList == null) {
mTvDataList = new ArrayList<>();
}
mTvDataList.add(di);
}

public void dmrRemoved(final DeviceItem di) {
if (mTvDataList != null && mTvDataList.contains(di)) {
mTvDataList.remove(di);
}
}
}
private void playToTv(DeviceItem deviceItem) {
String url = “网络视频链接”;
Service avtService = deviceItem.getDevice()
.findService(new UDAServiceType(“AVTransport”));
String metaData = TvUtil.pushMediaToRender(url, “video-item”, “DLNA测试视频”, “50:00”, “ggg”);
upnpService.getControlPoint().execute(new SetAVTransportURI(avtService, url, metaData) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
LogUtil.e(“playToTv”, “——-success”);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
LogUtil.e(“playToTv”, “——-failure defaultMsg=” + defaultMsg);
}
});
}
当然上面的网络视频链接*好以*终的播放地址,不然会出现无法播放的情况.
————————————————

安卓手机无线投屏电脑 三种方法轻松搞

在手机上看视频,已经是我们现在*主流的娱乐方式。不过,长时间看小屏幕对眼睛可受不了,而且一些4K、1080P的大片还是在大屏幕上更有视觉效果。这时该怎么办呢,有没有什么办法可以将手机屏幕投射到电脑屏幕上,下面简单几步教你安卓手机无线投屏电脑。
安卓手机无线投屏电脑 三种方法轻松搞
手机投屏到电脑
其实主要使用的是WiFi传输,这种技术名为“Miracast”。简单来说,“Miracast”就是通过“WiFi Display”认证的设备。有了这一功能后,移动端的音视频文件就可以分享到电脑上显示。通过WiFi进行两端的数据传输非常方便,也不会强制要求固定在一处,灵活又省事。
方法一:
1、用数据线将手机连接到电脑。先用数据线直接将手机和电脑连接。
安卓手机无线投屏电脑 三种方法轻松搞
2、有一个连接软件,叫同步助手,可以自主下载。连接好手机当手机和电脑连接的时候,有一些确定的内容,直接点击确定即可。共享桌面接着直接在同步助手里点击共享桌面,这时候就可以进行共享了。
安卓手机无线投屏电脑 三种方法轻松搞
方法二:
必要条件:手机支持“多屏互动”,电脑为win10系统;
1、设置电脑:点击任务栏后下角小气泡,在快捷开关中找到“连接”,并点击,然后在弹出窗口的右下方点“投影到这台电脑”,在“投影到这台电脑”设置页面中,选择“所有位置都可用”。
安卓手机无线投屏电脑 三种方法轻松搞
2、确保手机、电脑在同一网络环境中,有线连接、无线连接都。设置手机:设置——“其它连接方式——投射屏幕。
安卓手机无线投屏电脑 三种方法轻松搞
3、点击“投射屏幕”,手机会自动搜索同一网络环境下的可投设备,当你看到以你电脑命名的设备出现在了电脑,点击设备即可连接,手机屏幕即可同步到电脑了。
安卓手机无线投屏电脑 三种方法轻松搞
方法三:
1、需借助到第三方工具的辅助,比如下面就是一个适合安卓手机投屏的神器软件。
安卓手机无线投屏电脑 三种方法轻松搞
2、除了手机上安装第三方工具外,电脑端也需要一个,然后同时打开,这个时候会弹出一个二维码出来,点击手机上的扫一扫投屏扫描一下二维码就可以投屏了。即可与电脑完美连接,手机中的内容自然就会显示在电脑上;
安卓手机无线投屏电脑 三种方法轻松搞
3、在投屏期间也是可以录屏的,录屏后的视频就保存在视频库中还可以进行分享。
安卓手机无线投屏电脑 三种方法轻松搞
以上就是小编分享的几种关于安卓手机如何投屏到电脑中的方法,有了这几种方法你可以随时使用手机玩游戏看电影还可以将这些画面都投射到电脑大屏幕上,再也不用担心因为看多了手机小屏幕而怕眼睛有所损伤了。

使用Scrcpy投屏Android手机

Scrcpy使用

有时候我们需要在电脑上模拟Android手机,而一些模拟器过于臃肿,电脑卡慢,于是直接使用scrcpy连接真机。

安装
Github:

https://github.com/Genymobile/scrcpy/
  • 1

解压后是这个样子
在这里插入图片描述

使用ADB连接

  1. USB连接手机
adb devices
  • 1

在这里插入图片描述

  1. wifi模式
adb tcpip ****
  • 1

在这里插入图片描述

adb connect ip:****
  • 1
  1. 投屏

拔掉数据线

  1. 投屏

拔掉数据线

scrcpy
  • 1
  1. 选项 码率
scrcpy --bit-rate 2M --max-size 800

scrcpy -b2M -m800
  • 1
  • 2
  • 3
  1. 帧频
scrcpy --max-fps 15
  • 1
  1. 位置和尺寸
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
  • 1
  1. 顶层显示
scrcpy --always-on-top
  • 1
  1. 全屏
scrcpy --fullscreen
scrcpy -f  # short version
  • 1
  • 2
  1. 只看不控制
scrcpy --no-control
scrcpy -n
  • 1
  • 2
  1. 灭屏浏览and控制
scrcpy --turn-screen-off
scrcpy -S
  • 1
  • 2
  1. 轨迹球
scrcpy --show-touches
scrcpy -t
  • 1
  • 2

快捷键
scrcpy安装apk文件时可直接拖动到手机窗口,按提示安装,可以绕过一些国产OS添加的安装器
运行scrcpy,*次运行手机上会显示是否接受PC的秘钥,选择 总是信任接受
运行

  1. 快捷链接

创建文本文档*.txt,编辑:

adb devices
scrcpy
  • 1
  • 2

后缀改为cmd
以后使用时直接双击*.cmd即可

END

Android投屏方案(基于cling)

一 、前言
*近做了一个浏览器&视频播放的项目,是在73.0.3683.90版本的chrome源码上修改而来,涉及到抓取网页里视频的播放地址、播放视频、视频投屏、视频下载、网页内广告屏蔽等方面,了解到ijkplayer、GSYVideoPlayer、ffmpeg、乐播投屏、cling、NanoHttp、adblock等相关技术,现在就准备花点时间把一些技术相关的内容整理一下,分享给大家。

为什么先写的是投屏相关的技术呢?刚开始投屏用的乐播的sdk,乐播的效果肯定是很好的,支持的协议更多,更稳定,但是乐播有一个限制,个人开发者不能获取到APPID和SDK资源,*开始是帮别人做的项目,他们提供了相关的资源,所以就没有去研究过投屏的其他方案。但是后来又有了个新项目,新项目也有一个需求是投屏,但是他们没法提供相关的APPID和SDK,所以我就只能找新的方案,它就是cling。

android相关的投屏方案封装不止cling一个,只是恰巧看到了,并且有人说cling算是封装的比较好的了,所以就直接选择了cling开始做。截止目前,我做的这个项目基本上能正常的投屏图片、音频、视频等资源了,至于控制功能暂时还未尝试,但是相关的方法是有的,只是没有尝试调用。因为需求不同,所以目前我只研究了发送端的功能,至于接收端,我给的参考链接的*后两个链接里是有代码可以参考的。

本来说到投屏技术,一般都会讲到DLNA、AirPlay、UPNP协议等相关基础,但是这方面的介绍文献实在是多如牛毛,我就不在这里浪费时间去复制粘贴别人的劳动成果了,我给出几个当时我找资料时参考的几篇文章,供大家参考:

Android手机投屏

cling源码解析

投屏Cling DLNA 播放本地/网络资源方法梳理

我demo参考的github源码

本着大家都是着重于“取而用之”的实际需求,这里先附上本次项目的源码

基于cling实现的Android投屏方案

二 、实现的过程
我这个人呢,有个特别不好的习惯,不是十分喜欢直接抄袭别人的东西,又喜欢重复造轮子,但是呢,能力又有限,所以写出来的东西会和参考的东西有所区别,但是不一定比别人的好,请大家不要见怪。但这次重复造轮子的原因,主要是因为那个demo里的代码我没办法直接用,以及要解决cling2.2.0版本在9.0系统上出现无法解析描述文件的问题。

整个工程的目录结构如下图所示

[外链图片转存失败(img-znXFPZXt-1563761574281)(https://raw.githubusercontent.com/ykbjson/ykbjson.github.io/master/blogimage/simpledlna/simpledlna_code_structure.png)]

2.1源码浅析前的说明
webserver这个module就是基于NanoHttp实现的本地http服务器的代码。

simplepermission整个module是一个权限请求的库,因为整个工程基于androidx,没花时间去找适配androidx的权限库,就自己改吧改吧了一下原来用的一个权限库来用,因为要实现投屏,必须要一些权限,参见screening module的manifest文件。

sereening module是整个项目的核心,有三个地方要先提出来说清楚,一个是log包下的AndroidLoggingHandler,这个类是为了解决cling包里的logger不输出日志的问题,具体的请看

How to configure java.util.logging on Android?

另一个是xml包下的几个类,主要是重写了cling里解析设备交互报文的SAX解析器,cling原来的代码,在生成解析器的时候抛了异常,导致设备交互的报文无法被解析,后续流程就中断了,以至于无法发现可以投屏的设备。说到这里,不得不说,大神们写的代码,设计的真的非常强大,扩展性考虑的很好,我本以为只能clone cling的源码下来自己改,没想到这个解析器可以自定义,为作者手动点赞!

*后一个地方呢,就是DLNABrowserService,里面只是重载了AndroidUpnpServiceImpl的一个方法,返回DLNAUDA10ServiceDescriptorBinderSAXImpl,以便于替换cling自带的无法在android9.0上面正常工作的UDA10ServiceDescriptorBinderSAXImpl。所以,在使用这个库的时候,在app module的manifest里声明的就不是AndroidUpnpServiceImpl而是DLNABrowserService,这一点要注意。

至于bean包下的两个类,DeviceInfo是对支持投屏的设备——Device 的一个封装;MediaInfo是为了方便传递要投屏的多媒体信息做的封装。

2.2部分源码浅析
接下来我们从listener包开始讲解整个项目的源码,里面有四个回调接口,其实我感觉有些是多余的,但是呢,因为一些操作是异步的,感觉有一个回调接口能更好的控制使用这个库的逻辑,避免出现一些错误。

###初始化DLNAManager回调接口——DLNAStateCallback

public interface DLNAStateCallback {

void onConnected();

void onDisconnected();

}

这个其实应该叫DLNAManagerInitCallback,初始化DLNAManager的时候传递的,可以为null,只要你能保证你后续代码时在DLNAManager初始化之后调用的。

###注册设备列表和状态回调接口——DLNARegistryListener

public abstract class DLNARegistryListener implements RegistryListener {
private final DeviceType DMR_DEVICE_TYPE = new UDADeviceType(“MediaRenderer”);

public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {

}

public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {

}

/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device A validated and hydrated device metadata graph, with complete service metadata.
*/
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceAdded(registry, device);
}

public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {

}

/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device A validated and hydrated device metadata graph, with complete service metadata.
*/
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceRemoved(registry, device);
}

/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device The local device added to the {@link org.fourthline.cling.registry.Registry}.
*/
public void localDeviceAdded(Registry registry, LocalDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceAdded(registry, device);
}

/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device The local device removed from the {@link org.fourthline.cling.registry.Registry}.
*/
public void localDeviceRemoved(Registry registry, LocalDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceRemoved(registry, device);
}

public void beforeShutdown(Registry registry) {

}

public void afterShutdown() {

}

public void onDeviceChanged(Collection<Device> deviceInfoList) {
onDeviceChanged(build(deviceInfoList));
}

public abstract void onDeviceChanged(List<DeviceInfo> deviceInfoList);

public void onDeviceAdded(Registry registry, Device device) {

}

public void onDeviceRemoved(Registry registry, Device device) {

}

private List<DeviceInfo> build(Collection<Device> deviceList) {
final List<DeviceInfo> deviceInfoList = new ArrayList<>();
for (Device device : deviceList) {
//过滤不支持投屏渲染的设备
if (null == device.findDevices(DMR_DEVICE_TYPE)) {
continue;
}
final DeviceInfo deviceInfo = new DeviceInfo(device, getDeviceName(device));
deviceInfoList.add(deviceInfo);
}

return deviceInfoList;
}

private String getDeviceName(Device device) {
String name = “”;
if (device.getDetails() != null && device.getDetails().getFriendlyName() != null) {
name = device.getDetails().getFriendlyName();
} else {
name = device.getDisplayString();
}

return name;
}
}

这个类只是对RegistryListener的封装,因为我当时想着这个类主要是回调当前发现的设备的列表信息,所以就简单封装了一下,每次设备数量改变的时候就把新的设备数量通过一个回调方法传递出去,忽略一些不关注的方法。

###连接设备回调接口——DLNADeviceConnectListener

public interface DLNADeviceConnectListener {

int TYPE_DLNA = 1;
int TYPE_IM = 2;
int TYPE_NEW_LELINK = 3;
int CONNECT_INFO_CONNECT_SUCCESS = 100000;
int CONNECT_INFO_CONNECT_FAILURE = 100001;
int CONNECT_INFO_DISCONNECT = 212000;
int CONNECT_INFO_DISCONNECT_SUCCESS = 212001;
int CONNECT_ERROR_FAILED = 212010;
int CONNECT_ERROR_IO = 212011;
int CONNECT_ERROR_IM_WAITTING = 212012;
int CONNECT_ERROR_IM_REJECT = 212013;
int CONNECT_ERROR_IM_TIMEOUT = 212014;
int CONNECT_ERROR_IM_BLACKLIST = 212015;

void onConnect(DeviceInfo deviceInfo, int errorCode);

void onDisconnect(DeviceInfo deviceInfo,int type,int errorCode);
}

这个类是给DLNAPlayer连接设备时用的。说到这个所谓的连接设备,其实感觉也不需要这个步骤,cling本身可能已经做好了设备之间的连接,回调回来的设备列表里的设备都是连接过了的,直接可以通信。但是我发现乐播的sdk里就有一个连接设备的方法,必须先调用连接设备的这个方法,在回调里才能继续后续操作,所以我这里也设计了一个连接设备的步骤,我怕万一是cling有专门连接设备的接口,只是我还没发现而已,后面发现了就来改写这个连接设备的方法。

###控制设备回调接口——DLNAControlCallback

public interface DLNAControlCallback {
int ERROR_CODE_NO_ERROR = 0;

int ERROR_CODE_RE_PLAY = 1;

int ERROR_CODE_RE_PAUSE = 2;

int ERROR_CODE_RE_STOP = 3;

int ERROR_CODE_DLNA_ERROR = 4;

int ERROR_CODE_SERVICE_ERROR = 5;

int ERROR_CODE_NOT_READY = 6;

void onSuccess(@Nullable ActionInvocation invocation);

void onReceived(@Nullable ActionInvocation invocation,@Nullable Object … extra);

void onFailure(@Nullable ActionInvocation invocation,
@IntRange(from = ERROR_CODE_NO_ERROR, to = ERROR_CODE_NOT_READY) int errorCode,
@Nullable String errorMsg);
}

顾名思义,这个类就是发送端在控制接收端做出一系列动作时的回调接口,包括播放、暂停、结束、静音开闭、音量调整、播放进度获取等等。播放、暂停、结束、静音开闭、音量调整等方法只会回调onSuccess和onFailure方法;获取播放进度这种需要获取结果的方法会在onReceived方法里返回结果。

看完这几个类之后,我们应该大致知道这个库整个工作的流程了:初始化DLNAManager -> 注册设备列表回调接口 -> 连接一个设备 -> 控制这个设备。只不过呢,我把连接设备和控制设备部分功能封装到了DLNAPlayer里面,不然DLNAManager会有点臃肿,不便于维护。这里说到了整个库的工作流程,那么接下来我们就从DLNAManager开始接着分析。

###整个库的入口——DLNAManager

public final class DLNAManager {
private static final String TAG = “DLNAManager”;
private static final String LOCAL_HTTP_SERVER_PORT = “9090”;

private static boolean isDebugMode = false;

private Context mContext;
private AndroidUpnpService mUpnpService;
private ServiceConnection mServiceConnection;
private DLNAStateCallback mStateCallback;

private RegistryListener mRegistryListener;
private List<DLNARegistryListener> registryListenerList;
private Handler mHandler;
private BroadcastReceiver mBroadcastReceiver;

private DLNAManager() {
AndroidLoggingHandler.injectJavaLogger();
mHandler = new Handler(Looper.getMainLooper());
registryListenerList = new ArrayList<>();
mRegistryListener = new RegistryListener() {

@Override
public void remoteDeviceDiscoveryStarted(final Registry registry, final RemoteDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.remoteDeviceDiscoveryStarted(registry, device);
}
}
});
}

@Override
public void remoteDeviceDiscoveryFailed(final Registry registry, final RemoteDevice device, final Exception ex) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.remoteDeviceDiscoveryFailed(registry, device, ex);
}
}
});
}

@Override
public void remoteDeviceAdded(final Registry registry, final RemoteDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.remoteDeviceAdded(registry, device);
}
}
});
}

@Override
public void remoteDeviceUpdated(final Registry registry, final RemoteDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.remoteDeviceUpdated(registry, device);
}
}
});
}

@Override
public void remoteDeviceRemoved(final Registry registry, final RemoteDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.remoteDeviceRemoved(registry, device);
}
}
});
}

@Override
public void localDeviceAdded(final Registry registry, final LocalDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.localDeviceAdded(registry, device);
}
}
});
}

@Override
public void localDeviceRemoved(final Registry registry, final LocalDevice device) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.localDeviceRemoved(registry, device);
}
}
});
}

@Override
public void beforeShutdown(final Registry registry) {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.beforeShutdown(registry);
}
}
});
}

@Override
public void afterShutdown() {
mHandler.post(() -> {
synchronized (DLNAManager.class) {
for (DLNARegistryListener listener : registryListenerList) {
listener.afterShutdown();
}
}
});
}
};

mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (null != intent && TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {
final NetworkInfo networkInfo = getNetworkInfo(context);
if (null == networkInfo) {
return;
}
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
initLocalMediaServer();
}
}
}
};
}

private static class DLNAManagerCreator {
private static DLNAManager manager = new DLNAManager();
}

public static DLNAManager getInstance() {
return DLNAManagerCreator.manager;
}

public void init(@NonNull Context context) {
init(context, null);
}

public void init(@NonNull Context context, @Nullable DLNAStateCallback stateCallback) {
if (null != mContext) {
logW(“ReInit DLNAManager”);
return;
}
if (context instanceof ContextThemeWrapper || context instanceof android.view.ContextThemeWrapper) {
mContext = context.getApplicationContext();
} else {
mContext = context;
}
mStateCallback = stateCallback;
initLocalMediaServer();
initConnection();
registerBroadcastReceiver();
}

private void initConnection() {
mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mUpnpService = (AndroidUpnpService) service;
mUpnpService.getRegistry().addListener(mRegistryListener);
mUpnpService.getControlPoint().search();
if (null != mStateCallback) {
mStateCallback.onConnected();
}
logD(“onServiceConnected”);
}

@Override
public void onServiceDisconnected(ComponentName name) {
mUpnpService = null;
if (null != mStateCallback) {
mStateCallback.onDisconnected();
}
logD(“onServiceDisconnected”);
}
};

mContext.bindService(new Intent(mContext, DLNABrowserService.class),
mServiceConnection, Context.BIND_AUTO_CREATE);
}

/**
* 本地视频和图片也可以直接投屏,根目录为sd卡根目录
*/
private void initLocalMediaServer() {
checkConfig();
try {
final PipedOutputStream pipedOutputStream = new PipedOutputStream();
System.setIn(new PipedInputStream(pipedOutputStream));
new Thread(() -> {
final String localIpAddress = getLocalIpStr(mContext);
final String localMediaRootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String[] args = {
“–host”,
localIpAddress,/*局域网ip地址*/
“–port”,
LOCAL_HTTP_SERVER_PORT,/*局域网端口*/
“–dir”,
localMediaRootPath/*下载视频根目录*/
};
SimpleWebServer.startServer(args);
logD(“initLocalLinkService success,localIpAddress : ” + localIpAddress +
“,localVideoRootPath : ” + localMediaRootPath);
}).start();
} catch (IOException e) {
e.printStackTrace();
logE(“initLocalLinkService failure”, e);
}
}

private void registerBroadcastReceiver() {
checkConfig();
mContext.registerReceiver(mBroadcastReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}

private void unregisterBroadcastReceiver() {
checkConfig();
mContext.unregisterReceiver(mBroadcastReceiver);
}

public void registerListener(DLNARegistryListener listener) {
checkConfig();
checkPrepared();
if (null == listener) {
return;
}
registryListenerList.add(listener);
listener.onDeviceChanged(mUpnpService.getRegistry().getDevices());
}

public void unregisterListener(DLNARegistryListener listener) {
checkConfig();
checkPrepared();
if (null == listener) {
return;
}
mUpnpService.getRegistry().removeListener(listener);
registryListenerList.remove(listener);
}

public void startBrowser() {
checkConfig();
checkPrepared();
mUpnpService.getRegistry().addListener(mRegistryListener);
mUpnpService.getControlPoint().search();
}

public void stopBrowser() {
checkConfig();
checkPrepared();
mUpnpService.getRegistry().removeListener(mRegistryListener);
}

public void destroy() {
checkConfig();
registryListenerList.clear();
unregisterBroadcastReceiver();
SimpleWebServer.stopServer();
stopBrowser();
if (null != mUpnpService) {
mUpnpService.getRegistry().removeListener(mRegistryListener);
mUpnpService.getRegistry().shutdown();
}
if (null != mServiceConnection) {
mContext.unbindService(mServiceConnection);
mServiceConnection = null;
}
if (null != mHandler) {
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
registryListenerList = null;
mRegistryListener = null;
mBroadcastReceiver = null;
mStateCallback = null;
mContext = null;
}

private void checkConfig() {
if (null == mContext) {
throw new IllegalStateException(“Must call init(Context context) at first”);
}
}

private void checkPrepared() {
if (null == mUpnpService) {
throw new IllegalStateException(“Invalid AndroidUpnpService”);
}
}

//

/**
* 获取ip地址
*
* @param context
* @return
*/
public static String getLocalIpStr(@NonNull Context context) {
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
if (null == wifiInfo) {
return “”;
}
return intToIpAddress(wifiInfo.getIpAddress());
}

/**
* int类型的ip转换成标准ip地址
*
* @param ip
* @return
*/
public static String intToIpAddress(int ip) {
return (ip & 0xff) + “.” + ((ip >> 8) & 0xff) + “.” + ((ip >> 16) & 0xff) + “.” + ((ip >> 24) & 0xff);
}

public static NetworkInfo getNetworkInfo(@NonNull Context context) {
final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
return null == connectivityManager ? null : connectivityManager.getActiveNetworkInfo();
}

static String tryTransformLocalMediaAddressToLocalHttpServerAddress(@NonNull Context context,
String sourceUrl) {
logD(“tryTransformLocalMediaAddressToLocalHttpServerAddress ,sourceUrl : ” + sourceUrl);
if (TextUtils.isEmpty(sourceUrl)) {
return sourceUrl;
}

if (!isLocalMediaAddress(sourceUrl)) {
return sourceUrl;
}

String newSourceUrl = getLocalHttpServerAddress(context) +
sourceUrl.replace(Environment.getExternalStorageDirectory().getAbsolutePath(), “”);
logD(“tryTransformLocalMediaAddressToLocalHttpServerAddress ,newSourceUrl : ” + newSourceUrl);

try {
final String[] urlSplits = newSourceUrl.split(“/”);
final String originFileName = urlSplits[urlSplits.length – 1];
String fileName = originFileName;
fileName = URLEncoder.encode(fileName, “UTF-8”);
fileName = fileName.replaceAll(“\\+”, “%20”);
newSourceUrl = newSourceUrl.replace(originFileName, fileName);
logD(“tryTransformLocalMediaAddressToLocalHttpServerAddress ,encodeNewSourceUrl : ” + newSourceUrl);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

return newSourceUrl;
}

private static boolean isLocalMediaAddress(String sourceUrl) {
return !TextUtils.isEmpty(sourceUrl)
&& !sourceUrl.startsWith(“http://”)
&& !sourceUrl.startsWith(“https://”)
&& sourceUrl.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
}

/**
* 获取本地http服务器地址
*
* @param context
* @return
*/
public static String getLocalHttpServerAddress(Context context) {
return “http://” + getLocalIpStr(context) + “:” + LOCAL_HTTP_SERVER_PORT;
}

public static void setIsDebugMode(boolean isDebugMode) {
DLNAManager.isDebugMode = isDebugMode;
}

static void logV(String content) {
logV(TAG, content);
}

public static void logV(String tag, String content) {
if (!isDebugMode) {
return;
}
Log.v(tag, content);
}

static void logD(String content) {
logD(TAG, content);
}

public static void logD(String tag, String content) {
if (!isDebugMode) {
return;
}
Log.d(tag, content);
}

static void logI(String content) {
logI(TAG, content);
}

public static void logI(String tag, String content) {
if (!isDebugMode) {
return;
}
Log.i(tag, content);
}

static void logW(String content) {
logW(TAG, content);
}

public static void logW(String tag, String content) {
if (!isDebugMode) {
return;
}
Log.w(tag, content);
}

static void logE(String content) {
logE(TAG, content);
}

public static void logE(String tag, String content) {
logE(tag, content, null);
}

static void logE(String content, Throwable throwable) {
logE(TAG, content, throwable);
}

public static void logE(String tag, String content, Throwable throwable) {
if (!isDebugMode) {
return;
}
if (null != throwable) {
Log.e(tag, content, throwable);
} else {
Log.e(tag, content);
}
}
}

这个类有点长,但是要关注的方法就那么几个。init方法里干了几件事:

1.初始化本地投屏服务——initLocalMediaServer,投屏本地视频

2.连接AndroidUpnpService——initConnection,获取控制点和投屏服务

3.注册了一个网络连接变化的广播——registerBroadcastReceiver,网络变化时重启LocalMediaServer,保证本地资源投屏成功的几率

还有就是发起搜索设备的动作、停止搜索设备的动作、注册RegistryListener、移除RegistryListener等方法。剩下一些就是可以封装到工具类里的方法,懒得在添加类了,索性就写到了里面。

这个类还有一个作用就是维护了一个RegistryListener,统一的分发局域网内设备数量、设备状态、设备服务状态变化的回调事件。当你初始化完DLNAManager,并向这个类注册了DLNARegistryListener,然后调用startBrowser发起搜索,如果局域网内有可以接受投屏的设备,你就可以在DLNARegistryListener的onDeviceChanged方法里收到当前局域网内可以投屏的设备列表了。有了可用的设备列表,接下来,我们就可以开始连接接收端设备发送投屏数据以及控制他了。

连接和控制接收端设备——DLNAPlayer
public class DLNAPlayer {

private static final String DIDL_LITE_FOOTER = “</DIDL-Lite>”;
private static final String DIDL_LITE_HEADER = “<?xml version=\”1.0\” encoding=\”utf-8\” standalone=\”no\”?>”
+ “<DIDL-Lite ”
+ “xmlns=\”urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\” ”
+ “xmlns:dc=\”http://purl.org/dc/elements/1.1/\” ”
+ “xmlns:upnp=\”urn:schemas-upnp-org:metadata-1-0/upnp/\” ”
+ “xmlns:dlna=\”urn:schemas-dlna-org:metadata-1-0/\”>”;

/**
* 未知状态
*/
public static final int UNKNOWN = -1;

/**
* 已连接状态
*/
public static final int CONNECTED = 0;

/**
* 播放状态
*/
public static final int PLAY = 1;
/**
* 暂停状态
*/
public static final int PAUSE = 2;
/**
* 停止状态
*/
public static final int STOP = 3;
/**
* 转菊花状态
*/
public static final int BUFFER = 4;
/**
* 投放失败
*/
public static final int ERROR = 5;

/**
* 已断开状态
*/
public static final int DISCONNECTED = 6;

private int currentState = UNKNOWN;
private DeviceInfo mDeviceInfo;
private Device mDevice;
private MediaInfo mMediaInfo;
private Context mContext;//鉴权预留
private ServiceConnection mServiceConnection;
private AndroidUpnpService mUpnpService;
private DLNADeviceConnectListener connectListener;
/**
* 连接、控制服务
*/
private ServiceType AV_TRANSPORT_SERVICE;
private ServiceType RENDERING_CONTROL_SERVICE;

public DLNAPlayer(@NonNull Context context) {
mContext = context;
AV_TRANSPORT_SERVICE = new UDAServiceType(“AVTransport”);
RENDERING_CONTROL_SERVICE = new UDAServiceType(“RenderingControl”);
initConnection();
}

public void setConnectListener(DLNADeviceConnectListener connectListener) {
this.connectListener = connectListener;
}

private void initConnection() {
mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mUpnpService = (AndroidUpnpService) service;
currentState = CONNECTED;
if (null != mDeviceInfo) {
mDeviceInfo.setState(CONNECTED);
mDeviceInfo.setConnected(true);
}
if (null != connectListener) {
connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS);
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
currentState = DISCONNECTED;
if (null != mDeviceInfo) {
mDeviceInfo.setState(DISCONNECTED);
mDeviceInfo.setConnected(false);
}
if (null != connectListener) {
connectListener.onDisconnect(mDeviceInfo, DLNADeviceConnectListener.TYPE_DLNA,
DLNADeviceConnectListener.CONNECT_INFO_DISCONNECT_SUCCESS);
}
mUpnpService = null;
connectListener = null;
mDeviceInfo = null;
mDevice = null;
mMediaInfo = null;
AV_TRANSPORT_SERVICE = null;
RENDERING_CONTROL_SERVICE = null;
mServiceConnection = null;
mContext = null;
}
};
}

public void connect(@NonNull DeviceInfo deviceInfo) {
checkConfig();
mDeviceInfo = deviceInfo;
mDevice = mDeviceInfo.getDevice();
if (null != mUpnpService) {
currentState = CONNECTED;
if (null != connectListener) {
connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS);
}
return;
}
mContext.bindService(new Intent(mContext, DLNABrowserService.class),
mServiceConnection, Context.BIND_AUTO_CREATE);
}

public void disconnect() {
checkConfig();
try {
mContext.unbindService(mServiceConnection);
} catch (Exception e) {
DLNAManager.logE(“DLNAPlayer disconnect error.”, e);
}
}

private void checkPrepared() {
if (null == mUpnpService) {
throw new IllegalStateException(“Invalid AndroidUpnpService”);
}
}

private void checkConfig() {
if (null == mContext) {
throw new IllegalStateException(“Invalid context”);
}
}

private void execute(@NonNull ActionCallback actionCallback) {
checkPrepared();
mUpnpService.getControlPoint().execute(actionCallback);

}

private void execute(@NonNull SubscriptionCallback subscriptionCallback) {
checkPrepared();
mUpnpService.getControlPoint().execute(subscriptionCallback);
}

public void play(@NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(PLAY, avtService, callback)) {
return;
}
execute(new Play(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
currentState = PLAY;
callback.onSuccess(invocation);
mDeviceInfo.setState(PLAY);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void pause(@NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(PAUSE, avtService, callback)) {
return;
}

execute(new Pause(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
currentState = PAUSE;
callback.onSuccess(invocation);
mDeviceInfo.setState(PAUSE);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void stop(@NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(STOP, avtService, callback)) {
return;
}
execute(new Stop(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
currentState = STOP;
callback.onSuccess(invocation);
mDeviceInfo.setState(STOP);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void seekTo(String time, @NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(avtService, callback)) {
return;
}
execute(new Seek(avtService, time) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
callback.onSuccess(invocation);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void setVolume(long volume, @NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE);
if (checkErrorBeforeExecute(avtService, callback)) {
return;
}

execute(new SetVolume(avtService, volume) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
callback.onSuccess(invocation);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void mute(boolean desiredMute, @NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE);
if (checkErrorBeforeExecute(avtService, callback)) {
return;
}
execute(new SetMute(avtService, desiredMute) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
callback.onSuccess(invocation);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
});
}

public void getPositionInfo(@NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(avtService, callback)) {
return;
}

final GetPositionInfo getPositionInfo = new GetPositionInfo(avtService) {
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}

@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
callback.onSuccess(invocation);
}

@Override
public void received(ActionInvocation invocation, PositionInfo info) {
callback.onReceived(invocation, info);
}
};

execute(getPositionInfo);
}

public void getVolume(@NonNull DLNAControlCallback callback) {
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (checkErrorBeforeExecute(avtService, callback)) {
return;
}
final GetVolume getVolume = new GetVolume(avtService) {

@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
callback.onSuccess(invocation);
}

@Override
public void received(ActionInvocation invocation, int currentVolume) {
callback.onReceived(invocation, currentVolume);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
currentState = ERROR;
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
mDeviceInfo.setState(ERROR);
}
};
execute(getVolume);
}

public void setDataSource(@NonNull MediaInfo mediaInfo) {
mMediaInfo = mediaInfo;
//尝试变换本地播放地址
mMediaInfo.setUri(DLNAManager.tryTransformLocalMediaAddressToLocalHttpServerAddress(mContext,
mMediaInfo.getUri()));
}

public void start(final @NonNull DLNAControlCallback callback) {
mDeviceInfo.setMediaID(mMediaInfo.getMediaId());
String metadata = pushMediaToRender(mMediaInfo);
final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
if (null == avtService) {
callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null);
return;
}
execute(new SetAVTransportURI(avtService, mMediaInfo.getUri(), metadata) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
play(callback);
}

@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
DLNAManager.logE(“play error:” + defaultMsg);
currentState = ERROR;
mDeviceInfo.setState(ERROR);
callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
}
});
}

private String pushMediaToRender(@NonNull MediaInfo mediaInfo) {
return pushMediaToRender(mediaInfo.getUri(), mediaInfo.getMediaId(), mediaInfo.getMediaName(),
mediaInfo.getMediaType());
}

private String pushMediaToRender(String url, String id, String name, int ItemType) {
final long size = 0;
final Res res = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), size, url);
final String creator = “unknow”;
final String parentId = “0”;
final String metadata;

switch (ItemType) {
case MediaInfo.TYPE_IMAGE:
ImageItem imageItem = new ImageItem(id, parentId, name, creator, res);
metadata = createItemMetadata(imageItem);
break;
case MediaInfo.TYPE_VIDEO:
VideoItem videoItem = new VideoItem(id, parentId, name, creator, res);
metadata = createItemMetadata(videoItem);
break;
case MediaInfo.TYPE_AUDIO:
AudioItem audioItem = new AudioItem(id, parentId, name, creator, res);
metadata = createItemMetadata(audioItem);
break;
default:
throw new IllegalArgumentException(“UNKNOWN MEDIA TYPE”);
}

DLNAManager.logE(“metadata: ” + metadata);
return metadata;
}

/**
* 创建投屏的参数
*
* @param item
* @return
*/
private String createItemMetadata(DIDLObject item) {
StringBuilder metadata = new StringBuilder();
metadata.append(DIDL_LITE_HEADER);

metadata.append(String.format(“<item id=\”%s\” parentID=\”%s\” restricted=\”%s\”>”, item.getId(), item.getParentID(), item.isRestricted() ? “1” : “0”));

metadata.append(String.format(“<dc:title>%s</dc:title>”, item.getTitle()));
String creator = item.getCreator();
if (creator != null) {
creator = creator.replaceAll(“<“, “_”);
creator = creator.replaceAll(“>”, “_”);
}
metadata.append(String.format(“<upnp:artist>%s</upnp:artist>”, creator));
metadata.append(String.format(“<upnp:class>%s</upnp:class>”, item.getClazz().getValue()));

DateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd’T’HH:mm:ss”);
Date now = new Date();
String time = sdf.format(now);
metadata.append(String.format(“<dc:date>%s</dc:date>”, time));

Res res = item.getFirstResource();
if (res != null) {
// protocol info
String protocolinfo = “”;
ProtocolInfo pi = res.getProtocolInfo();
if (pi != null) {
protocolinfo = String.format(“protocolInfo=\”%s:%s:%s:%s\””, pi.getProtocol(), pi.getNetwork(), pi.getContentFormatMimeType(), pi
.getAdditionalInfo());
}
DLNAManager.logE(“protocolinfo: ” + protocolinfo);

// resolution, extra info, not adding yet
String resolution = “”;
if (res.getResolution() != null && res.getResolution().length() > 0) {
resolution = String.format(“resolution=\”%s\””, res.getResolution());
}

// duration
String duration = “”;
if (res.getDuration() != null && res.getDuration().length() > 0) {
duration = String.format(“duration=\”%s\””, res.getDuration());
}

// res begin
// metadata.append(String.format(“<res %s>”, protocolinfo)); // no resolution & duration yet
metadata.append(String.format(“<res %s %s %s>”, protocolinfo, resolution, duration));

// url
String url = res.getValue();
metadata.append(url);

// res end
metadata.append(“</res>”);
}
metadata.append(“</item>”);

metadata.append(DIDL_LITE_FOOTER);

return metadata.toString();
}

private boolean checkErrorBeforeExecute(int expectState, Service avtService, @NonNull DLNAControlCallback callback) {
if (currentState == expectState) {
callback.onSuccess(null);
return true;
}

return checkErrorBeforeExecute(avtService, callback);
}

private boolean checkErrorBeforeExecute(Service avtService, @NonNull DLNAControlCallback callback) {
if (currentState == UNKNOWN) {
callback.onFailure(null, DLNAControlCallback.ERROR_CODE_NOT_READY, null);
return true;
}

if (null == avtService) {
callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null);
return true;
}

return false;
}

}

这个类也很长,因为干事情的就是他,所以他的方法比较多,设定播放数据、播放、暂停、停止、拖动进度、静音控制、音量控制等等都在这个DLNAPlayer里实现的。cling对设定投屏数据、播放、暂停、停止、拖动进度、静音控制、音量控制等功能都做了封装,我这里只是统一了一个回调接口,这些个方法里,只有设定投屏数据的时候才需要发送upnp协议规定的xml数据,其他方法都不需要。构建xml数据的方法也是在上面给出的链接里复制的,反正就是upnp协议规定好的,需要这中格式的数据,如果你想接收端能比较完整的显示投屏的数据信息,传递的MediaInfo可以详细些,我这里都值传递了多媒体地址信息。

三、结语
唉,终于贴完代码了,贴的时候感觉好无奈,自己也很反感这中方式,但是这只是对cling的一个简单实用实用示例,技术细节都是别人处理好了的,我只是做了点简单的分层,希望大家看了demo能直接使用cling实现投屏功能,也没什么技术分析,所以就只是贴个代码了。

至于使用的方法,我就更懒得贴了,没有任何意义,大家直接看源码的demo就可以了,我只给大家提几个需要注意的地方:

1.app module的build.gradle文件必须要加上一句

//去重复的引用
packagingOptions {
exclude ‘META-INF/beans.xml’
}

这是由于引入jetty引起的文件重复。

2.build.gradle文件里类似如下代码

minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName

里面的ext.minSdkVersion等等,请参见根目录的build.gradle。

3.所有工程的依赖库都基于androidx,所以,如果有需要的童鞋在集成到自己的工程里的时候要慎重,因为androidx库和support库不兼容。

*后,祝大家工作愉快。

arm 汇编指令:内存访问方式 、STR 、LDR 指令

Memeory accesses(内存访问的方式)

通常有以下三种方式,允许用作内存访问指令:

《1》Offset addressing (偏移寻址)

汇编语法格式:

[ <Rn> , <offset>]

内存访问地址值(memory)= 偏移地址(offset) + 基址寄存器( <Rn>)的值 。这种方式基址寄存器(<Rn>)值没有变化

《2》Pre-indexed addressing(带前索引寻址)

汇编语法格式:

【<Rn>,  <offset>]!

内存访问地址值(memory)= 偏移地址(offset) + 基址寄存器(<Rn>)的值 。此方式基址寄存器(<Rn>)的值 = 内存访问地址值(memory),在写入内存前发生变化(故名前索引)。

《3》Post-indexed addressing (带后索引寻址)

汇编语法格式:

[<Rn>], <offset>

内存访问地址值(memory)= 基址寄存器( <Rn>)的值 。此方式基址寄存器(<Rn>)的值 = 偏移地址(offset) + 基址寄存器()的值,在写入内存后发生变化(故名后索引)。

在上面的各种情况中,<Rn> 是基址寄存器,<offset> 是偏移量,<offset > 可以用以下三种方式表示

1)立即数,例如《imm8)or (imm10)  or <imm 12>;

2)  索引寄存器,<Rm>内存访问地址值(memory)(内存访问地址值(memory)就是读取该寄存器的值,作为偏移量  索引寄存器,<Rm>内存访问地址值(memory)(内存访问地址值(memory)就是读取该寄  索引寄存器,<Rm>内存访问地址值(memory)(内存访问地址值(memory)就是读取该寄);

3)带移位索引寄存器,例如 <Rm>, LSL #<shift> (此就是Rm的值向左移动#<shift>位作为偏移量(offset));

 

介绍上述内存访问指令,主要是下面的指令需要用到。

STR(立即数,thumb/ARM)

概述:STR(立即数) 指令计算出从基址寄存器的数值和立即数的偏移(通过这,得出内存(memory)地址),然后从一个寄存器(register)读取它的值(store)到内存(memory)中。

汇编语法格式:

STR{<c>}{<q>} <Rt>, [ <Rn>  {, #+/-<imm>}]

首先宏观分析下, 将”{}” 的内容都忽略掉,可以得到如下结构:

STR <Rt>, [ <Rn>]

此条汇编语句可以得到如下的运算: 由Rn寄存器的值得到内存地址(memory),memory 内存地址值=<Rt>(换句话,将<Rt> 寄存器的值,store(存储)到memory所在的地址单元内中)。

接着扩展下,将”{, #+/-<imm>}“部分加入,可以得到如下结构:

STR <Rt>, [ <Rn>  , #imm]

此部分的语法 [ <Rn>  , #imm] 就是上面Memeory accesses(内存访问的方式)提到的Offset addressing (偏移寻址),本质就是计算出内存地址 啦,不再赘述。

再接着扩展下,将“ {<c>}” 部分加入, 可以得到如下结构:

STRAL <Rt>,  [<Rn>, #imm]  @AL(Always 可以省略的),这是默认不加此部分的语法,c 指conditional ,没加就是alway unconditional 无条件执行

STRLT  <Rt>,  [<Rn>, #imm]  @ LT signed less than 有符合数小于条件满足才能执行

STRLE  <Rt>, [<Rn>, #imm]   @ LE signed less than or equal 有符号数小于或等于条件满足均可执行

STREQ  <Rt>, [<Rn>, #imm]  @ Equal 有符号数和无符号数满足等于条件才可执行

其它具体参考ARM 语法手册。

*后接着扩展下, 将“{<q>}”部分加入, 可以得到如下结构:

STRAL.N  <Rt>, 【<Rn>, #imm]

STRAL.W  <Rt>, [<Rn>, #imm]

q 是qulitifiers 的缩写,

.N 是Narrow , 意指汇编器必须为这条指令选择一个16位的编码模式。如果不能办到,就会出现汇编器错误。

.W 是wide。指定汇编器必须为这条指令选择一个32位的编码模式。如果办不到,汇编器报错。

如果忽略掉此部分,汇编可以选择16位或32位的编码,如果二者均可,他会选择16位的编码。

STR{<c>}{<q>} <Rt>, [<Rn>, #+/-<imm>]! @ 与前述区别就是上面Memeory accesses(内存访问的方式)提到的Pre-indexed addressing(带前索引寻址)
STR{<c>}{<q>} <Rt>, [<Rn>],#+/-<imm> @与前述区别就是上面Memeory accesses(内存访问的方式)提到的Post-indexed addressing (带后索引寻址)

STR (register)

汇编语法
STR{<c>}{<q>} <Rt>, [<Rn>, <Rm>{, <shift>}]
STR{<c>}{<q>} <Rt>, [<Rn>, <Rm>{, <shift>}]!
STR{<c>}{<q>} <Rt>, [<Rn>], <Rm>{, <shift>}
此语法结构和前述不同之处,就是此处用 寄存器来传递offset ,而非立即数来传递。
offset = <Rn> + (<Rm> 逻辑向左( LSL) 或逻辑向右(LSR)或者算术向右(ASR) 或者循环向右(ROR)移动 (shift )n位,或循环带进位(RRX)向右移动1位,)

这样就可变形为:

STR{<c>}{<q>} <Rt>, [<Rn>,#offset]

STR{<c>}{<q>} <Rt>, [<Rn>, #offset]!

STR{<c>}{<q>} <Rt>, #offset

 

LDR(立即数,thumb/ARM)

概述:LDR 从基址寄存器Rn 的和立即数的偏移之和 得出内存地址的位置,从内存地址里面Load(装载)一个字的值写到 Rt 寄存器。

LDR{<c>}{<q>} <Rt>, [<Rn> {, #+/-<imm>}]

LDR{<c>}{<q>} <Rt>, [<Rn> , #+/-<imm>]!

LDR{<c>}{<q>} <Rt>, [<Rn> ] , #+/-<imm>

内存地址(memory)的分析和STR 类似,不同是这些指令是将内存(memory)的值Load(装载)到寄存器(Rt)中去的。

LDR(register,thumb)

概述: Load Register 从基址寄存器的值和一个偏移寄存器的值计算出内存的地址,从该地址单元读出一个字的数值,将它写入寄存器中。

LDR{<c>}{<q>} <Rt>, [<Rn>, {+}<Rm>{, <shift>}]  @在thumb指令中,只能选择偏移寄存器Rm的值到基址寄存器Rn中,而不能

LDR(register,ARM)

概述: Load Register 从基址寄存器的值和一个偏移寄存器的值计算出内存的地址,从该地址单元读出一个字的数值,将它写入寄存器中。

LDR{<c>}{<q>} <Rt>, [<Rn>, {+/-}<Rm>{, <shift>}]

LDR{<c>}{<q>}  [<Rt>, [ <Rn>,{+/-} <Rm>{, <shift>}]!

LDR{<c>}{<q>} <Rt>,[<Rn>] ,{+/’-} <Rm>{,<shift>}

LDR(literal)

概述:从PC的值和一个偏离立即数计算出内存的地址值,从此处内存中load(加载)一个字,把它写入到寄存器中。

汇编语法

LDR{<c>} {<q>}  <Rt>, <label >          @ label  就是一个指定的标号

LDR{<c>} {<q>}  <Rt>, [PC, #+/-<imm>]

 

 

2014.10.26

ps:

str理解为 store a register to “value” ==》此处的“”value“就是“memory”(内存也就是芯片的RAM)

ldr理解为 load  ” value ”  to a register

stm :store multiple registers to ”someplace“ ==》此处的”someplace”  就是 “block memory”(某块连续的内存)

ldm: load multiple registers from “someplace”

2017.01.05

memory-reads <=> loads

memory-writes<=> stores

wiwa

如何查看端口号是否被占用

在网络程序的调试过程中,经常发生一些出乎意料的事情,比如创建一个TCP服务失败,这时候往往需要查看系统的网络情况,*常用的网络抓包当然非WireShark模式。但往往很多时候只需要查看某个端口的使用情况,它到底被那个进程(对应PID)占用了,或者你还需要把它Kill掉。如果你在Windows操作系统,你可以使用netstat命令来查询PID,然后可以打开任务管理器,查看这个PID对应的进程名;如果PID没有显示,菜单》查看》选择列》选中PID即可;得知进程后,我们可以将进程杀掉。下面我简单描述一下我所了解的在Windows和Linux系统下处理方式。(假如我们需要确定谁占用了我们的9010端口)

1、Windows平台
在windows控制台窗口下执行:
netstat -nao | findstr “9010”
TCP 127.0.0.1:9010 0.0.0.0:0 LISTENING 3017

你看到是PID为3017的进程占用了9010端口,如果进一步你想知道它的进程名称,你可以使用如下命令:

tasklist | findstr “3017”

如果你想杀死这个进程,你当然可以用前面描述的那种方法,在任务管理器里把它KILL了,但如果你喜欢高效一点,那么用taskkill命令就可以了。

taskkill /pid 3017

那么这个进程就灰灰湮灭了:)

2、Linux

如果你是个Linux爱好者,那个这个命令你应该很熟了,

netstat -pan | grep 9010

如果你稍微仔细一点,你会发现,用的都是netsta命令,事实上,netstat是一个比较通用的网络统计命令,几乎适用于所有现在流行的操作系统,无论是Linux,Window,还是其他Unix,或者Unix-like操作系统,而且用法基本一致。

下面是一个对Windows系统中netstat命令行参数的详细解释。

格式:

netstat [-a] [-e] [-n] [-o] [-p Protocol] [-b] [-r] [-s] [-v] [Interval]

参数说明:

-a 显示所有连接和监听端口。
-n 以数字形式显示地址和端口号。

-o 显示与每个连接相关的所属进程 ID。

-p 在Windows系统中,该选项用于指定默认情况的子集。proto 显示 proto 指定的协议的连接;proto 可以是下列协议之一: TCP、UDP、TCPv6 或 UDPv6。

如果与 -s 选项一起使用以显示按协议统计信息,proto 可以是下列协议之一:
IP、IPv6、ICMP、ICMPv6、TCP、TCPv6、UDP 或 UDPv6。

-b 显示包含于创建每个连接或监听端口的可执行组件。在某些情况下已知可执行组件拥有多个独立组件,并且在这些情况下;包含于创建连接或监听端口的组件序列被显示。这种情况下,可执行组件名在底部的 [] 中,顶部是其调用的组件,等等,直到 TCP/IP 部分。注意此选项
可能需要很长时间,如果没有足够权限可能失败。

-e 显示以太网统计信息。此选项可以与 -s选项组合使用。

-s 显示按协议统计信息。默认地,显示 IP、IPv6、ICMP、ICMPv6、TCP、TCPv6、UDP 和 UDPv6 的统计信息。

-r 显示路由表。

-v 与 -b 选项一起使用时将显示包含于为所有可执行组件创建连接或监听端口的组件。

interval 重新显示选定统计信息,每次显示之间暂停时间间隔(以秒计)。按 CTRL+C 停止重新显示统计信息。如果省略,netstat 显示当前
配置信息(只显示一次)。

Oralce如何选择合适的索引类型

索引就好象一本字典的目录。凭借字典的目录,我们可以非常迅速的找到我们所需要的条目。数据库也是如此。凭借Oracle数据库的索引,相关语句可以迅速的定位记录的位置,而不必去定位整个表。

虽然说,在表中是否创建索引,不会影响到Oracle数据库的使用,也不会影响数据库语句的使用。这就好像即使字典没有目录的话,用户仍然可以使用它一样。可是,若字典没有目录,那么可想而知,用户要查某个条目的话,其不得不翻遍整本字典。数据库也是如此。若没有建立相关索引的话,则数据库在查询记录的时候,不得不去查询整个表。当表中的记录比较多的时候,其查询效率就会很低。所以,合适的索引,是提高数据库运行效率的一个很好的工具。

不过,并不是说表上的索引越多越好。过之而不及。故在数据库设计过程中,还是需要为表选择一些合适的索引。宁缺勿烂,这是建立索引时的一个具体选择。在理论上,虽然一个表可以设置无限的索引。但是,数据库管理员需要知道,表中的索引越多,维护索引所需要的开销也就越大。每当数据表中记录有增加、删除、更新变化的时候,数据库系统都需要对所有索引进行更新。故数据库表中的索引*对不是多多益善。具体来说,在索引建立上,笔者对大家有如下建议。

  建议一:在基数小的字段上要善于使用位图索引。

基数是位图索引中的一个基本的定义,它是指数据库表中某个字段内容中不重复的数值。如在员工信息表中的性别字段,一般就只有男跟女两个值,所以,其基数为2;婚姻状况字段的话,则其只有已婚、未婚、离婚三种状态,其基数就为3;民族一览内也是只有有限的几个值。

对于要查询基数小的字段,如现在用户想查找所有婚姻状况为“已婚”的“女性”时,利用位图索引可以提高查询的效率。这主要是因为标准索引是通过在索引中保存排序过的索引列以及对应的ROWID来实现的。若我们在基数小的列上建立标准索引的话,则其会返回大量的记录。

而当我们在创建位图索引的时候,在Oracle会对整个表进行扫描,并且会为索引列的每个取值建立一个位图。若内容相同,则在位图上会以一个相同的数字表示。此时,若这个字段的基数比较小的话,则若需要实现对整个字段的查询的话,效率就会非常的高。因为此时,数据库只要位图中数字相同的内容找出来即可。

除了在数据表某列基数比较小的情况下,采用位图索引外,我们往往在一些特殊的情况下,也会建议采用位图索引。*常见的情况是,在Where限制条件中,若我们多次采用AND或者OR条件时,也建议采用位图索引。因为当一个查询饮用了一些部署了位图索引的列的时候,这些位图可以很方便的与AND或者Or 运算符操作结合以快速的找出用户所需要的记录。

但是,这里要注意,不是在条件语句中包含运算符的时候,采用位图索引都能够提供比较高的效率。一般来说,只有AND 或者OR运算符的时候,位图索引才会比较具有优势。若此时用户采用大于号或者不等于号作为条件语句中的限制条件的时候,则往往采用标准索引具有更大的优势。

所以,笔者在数据库设置中,一般只有在三种情况下才采用位图索引。一是列的基数比较小,而有可能需要根据这些字段的内容查找相关的记录;二是在条件语句中,用到了AND或者OR运算符的时候。除了这两种情况外,*好能够采用其他适合的索引。第三种情况是,需要用到NULL作为查询的限制条件。因为标准查询一般情况下,会忽略所有的NULL值列。也就是说,若需要查询“所有没有身份证号码”的员工的信息的时候,标准索引并不能够起到加速查询速度的作用。此时,就需要采用位图索引。因为位图索引会记录相关的NULL值列信息。

  建议二:创建索引的一些限制条件。

并不说,表或者列建立的索引越多越好。相反,索引建的越多,有时会反而会影响数据库运行的整体性能。所以,在建立索引的时候,仍然会有一些限制条件。

一是不要对一些记录内容比较少的表建立索引。在一个应用系统设计的时候,如设计一个ERP系统的数据库,其虽然有几千张表。但是,并不是每张表都有大量记录的。相反,其中有近一半左右的数据表,可能其存储的数据不会超过百条。如员工登陆帐户密码表、企业部门信息表等等。对于这些记录内容比较少的表,我们建立*好不要为其建立索引。无论是表上的,还是字段上,都不要建立索引。

二是若表中的内容比较大,但是,这个表基本上不怎么查询的时候,则只需要在表上建立索引即可;而不需要在字段上建立索引。如现在在ERP系统中,有一张表是“AD_Table”。其存储的是这个数据库中相关表的信息。这张表只有在数据库设计的时候才会用到。故这张表中的记录虽然比较多,但是由于用户用的比较少,所以,一般没有必要为这张表建立列级别上的索引。而直接用表索引来代替。

三是在一些NULL字段上,要根据实际情况来判断是否要建立索引。如现在有一张人事档案的表格,其上面有两个字段,分别为“身份证号码”与“地区”。有时会为了某个原因,企业需要所有员工都在系统中登记他们的身份证号码,以方便他们办工资卡、社会保险等等。所以人事管理可能需要经常的查询系统,看看有没有没有身份证号码的员工信息。此时,就需要利用条件“IS NULL”来查询我们所需要的记录。故为了提高查询效率,若某个记录可能为空,并且经常需要以NULL为条件进行查询的时候,则*好给这个字段添加一个索引,并且*好建立位图索引。相反,若虽然可能会以NULL这个条件作为查询的限制语句,但是,用的不是很多的时候,则就没有必要为其建立索引。

  建议三:多表连接查询的索引设计。

如现在有一个人事管理系统。人事经理想知道员工的社保缴纳情况。他需要知道员工的姓名、职务、户籍性质(农民户口跟居民户口费用不一样)、缴纳的情况等等。但是,这些信息包含在不同的表中。因为为了提高数据库的性能,在表中存储的可能只是某些序号,而不是具体的内容。如在社保表中,存储的是员工对应的编号,而不是员工的名字。所以,要得到这份报表的话,就可能需要关联员工基本信息表、公司组织结构表等表格,才能够查询到用户所需要的内容。

为此,就需要利用Join语句,把这些表格关联起来。为了提高数据库的查询效率,这些用来关联的字段,*好能够建立索引。这可以显著的提高查询的速度。

  建议四:在表的更新速度与查询速度之间寻求一个平衡点。

众所周知,索引本身并不影响数据库的使用,其主要是为了提高数据库的查询效率。但是,由于当数据库的表中的数据更新的时候,包括记录的增加、删除、更改等等,都会对虽有的索引进行更新。

很明显,索引虽然可以提高查询速度。但是,也会对一些表的更新操作产生不良的影响。当在表中建立的索引越多,这个不利影响也会越大。故数据库管理员在设置索引的时候,还需要注意,在这两个之间需要一个均衡点。

按照一般的理论来说,当某个表多数用来查询、更新相对来说比较上的话,则要多多采用索引。相反,当某个表记录更新居主导,查询相对来说比较少的话,则不要建立太多的索引,避免对更新的速度差生不利影响。

在实际工作中,若某个表频繁的被视图所调用的话,则*好就好设置比较多的索引了。

解决Linux中文乱码

方法一:    修改/root/.bash_profile文件,增加export LANG=zh_CN.GB18030

对于其他用户,也必须相应修改该文件

    使用该方法时putty能显示中文,但桌面系统是英文,而且所有的网页中文显示还是乱码

方法二:

引用:

修改/etc/sysconfig/i18n文件

#LANG=”en_US.UTF-8″
#SUPPORTED=”en_US.UTF-8:en_US:en”
#SYSFONT=”latarcyrheb-sun16″

改为

LANG=”zh_CN.GB18030″
LANGUAGE=”zh_CN.GB18030:zh_CN.GB2312:zh_CN”
SUPPORTED=”zh_CN.GB18030:zh_CN:zh”
SYSFONT=”lat0-sun16″
SYSFONTACM=”8859-15″

Myelipse6.5、MyEclipse 8.5设置自动提示代码功能

输入完字符,自动提示 

1、打开 MyEclipse 8.5 -> Window -> Perferences -> Java -> Editor -> Content Assist,在右边*下面一栏找到 auto-Activation ,下面有三个选项,找到第二个“Auto activation triggers for Java:”选项

在其后的文本框中会看到一个“.”存在。这表示:只有输入“.”之后才会有代码提示和自动补全,我们要修改的地方就是这里。把该文本框中的“.”换掉,换成“abcdefghijklmnopqrstuvwxyz.”,这样,你在MyEclipse 8.5里面写Java代码就可以做到按“abcdefghijklmnopqrstuvwxyz.”中的任意一个字符都会有代码提示。当然,你还可以在里面加上更多的字符,只要是你需要的,你都可以加上去!比如说在JPA(Java persistence API)里面会用大量的“@”符号进行相应的注解,你可以把“@”一并加上去。以后在按下“@”付好后就会有相应的代码提示,此时你只需移动鼠标或是按键盘的上下键按钮进行选择,轻松解除你不熟悉Java代码的恐惧!

2、另外,MyEclipse 6.5 的版本,这里的设置稍有不同。

打开 MyEclipse 6.5 -> Window -> Perferences -> Java -> Editor -> Content(记住,不是他的下级选项,而是它本身),*下面一栏 auto-Activation ,会看到只有一个“.”存在。表示:只有输入“.”之后才会有代码提 示,我们要修改的地方就是这里,可是Eclipse默认只允许输入4个自定义字符。
不过我们可以把当前的设置导出,保 存为一个文件,然后在文件中修改,再导入设置,这样就可以突破Eclipse的限制。
先把上图中“.“的地方输入几个 随便的字符,例如“asdf“,点*下面的“OK“来保存设置。
然 后打开 Eclipse的 File -> Export,在窗口中展开 General -> Perferences–>Export all然后点击 NEXT。然后点击“Browse”选择任意的一个路径,保存配置文件,然后点击“Finish“用记事本打开刚才 保存的那个配置文件(扩展文件名:*.epf),按“ctrl + F”,输入刚才设置的“asdf”,找到刚才字符串。把“asdf”修改为“abcdefghijklmnopqrstuvwxyz.”,然后保存,退出记事本。
打 开Eclipse的 File -> Import 然后在打开的窗口里展开 General ->
Perferences,点击NEXT, 选中刚才修改过的配置文件,Finish。现在,再打开Window ->
Perferences,并依次展开 Java -> Editor -> Content
Assist,会发现已经超过了4个字符,也就是说我们输入任何字母和“.”都 会有代码提示了。

按快捷键弹出代码提示

myelipse中的内容补齐的功能是使用content assist。
myeclipse 6把alt+/分配给了单词提示,还有代码提示的快捷代码也不是alt+/,因此要恢复代码提示用alt+/.需要做两件事.
1,windows–>preference–>general–>keys 下设置Content Assist 的快捷键
2,windows–>preference–>general–>keys 下设置word completion 的快捷键 改成其它的.

如果还是不提示有可能是提示功能被关了.
如下做
1、菜单window->Preferences->Java->Editor->Content Assist->Enable auto activation 选项要打上勾
2、window->Preferences->Java->Editor->Content Assist->Advanced 上面的选项卡Select