Android 投屏实现纪要

在前文里介绍了 Android -> Windows 多样化投屏方案

这里记录具体的实现

(一)屏幕截取

MediaProjection/VirtualDisplay

因为权限问题,不能直接创建镜像(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR)类型的VirtualDisplay,需要通过MediaProjection 提示用户授权。

        MediaProjectionManager mediaManager = (MediaProjectionManager) getSystemService(
                Context.MEDIA_PROJECTION_SERVICE);
        startActivityForResult(
                mediaManager.createScreenCaptureIntent(), 100, null);

用户确认后,创建MediaProjection,并保留后续使用

    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        mMediaProjection = mMediaManager.getMediaProjection(resultCode, data);
    }

VirtualDisplay可以通过同一个 MediaProjection 多次创建

        mMirrorDisplay = mMediaProjection.createVirtualDisplay("Mirror",
                REQUEST_DISPLAY_WIDTH,
                REQUEST_DISPLAY_HEIGHT,
                mMetrics.densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                null, null, null);

Presentation/VirtualDisplay

Presentation是一个Dialog,输出到VirtualDisplay上,实现扩展屏幕功能。这个Dialog在Android设备上是不可见的。

先创建VistualDisplay,与通过MediaProjection 几乎一样的参数,但是需要设置 VIRTUAL_DISPLAY_FLAG_PRESENTATION。

        mPresentationDisplay = mDisplayManager.createVirtualDisplay("presentation",
                REQUEST_DISPLAY_WIDTH,
                REQUEST_DISPLAY_HEIGHT,
                mMetrics.densityDpi,
                null,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION,
                null, null);

然后创建 Presentation 对话框,并show出来

        Presentation mPresentation = new Presentation(mContext, mPresentationDisplay.getDisplay());
        mPresentation.setContentView(dialogView);
        mPresentation.show();

(二)OpenGL合成双屏幕

OpenGL有固定的代码框架,GLThread + GLRenderer + GLFilter + RenderScript,这里不做详细介绍。摘取与屏幕合成有关的部分。

GLThread

Android 没有现成的 GLThread 类,我们利用 GLSurfaceView 实现 GL 渲染线程。

public final class GLThread implements Renderer {

}
    public GLThread(Context context, SurfaceHolder holder) {
        mHolder = holder;
        mGLView = new GLSurfaceView(context) {
            @Override
            public SurfaceHolder getHolder() {
                return mHolder;
            }
        };
        mGLView.setEGLContextClientVersion(2);
        mGLView.setRenderer(this);
        mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

GLSurfaceView 只能设置一次 Renderer,为切换 Renderer,我们让 GLThread 内部管理 Renderer,自己作为 GLSurfaceView  的 Renderer 转发 onDraw 等调用。

另外用外部的 SurfaceHolder 代替 GLSurfaceView 的 SurfaceHolder,因为我们要输出图形到外部 Surface 上。SurfaceHolder 需要自己实现,还需要利用 java 反射 Surface.tramsform 方法。

GLDisplayRenderer

GLDisplayRenderer 实现基本的 Renderer 框架,内部创建两个 GLTexture,分别给两个 VirtualDisplay 使用。

        mPresentTexture = new GLTexture();
        mPresentTexture.setOESImage();

        mMirrorTexture = new GLTexture();
        mMirrorTexture.setOESImage();

        // stand-alone work thread
        Runnable work = new Runnable() {
            @Override
            public synchronized void run() {
                mMirrorSurfaceTexture = new SurfaceTexture(mMirrorTexture.id());
                mPresentSurfaceTexture = new SurfaceTexture(mPresentTexture.id());
                notify();
            }
        };
        synchronized (work) {
            sWorkThread.post(work);
            try {
                work.wait();
            } catch (InterruptedException e) {
            }
        }
        mMirrorSurfaceTexture.setOnFrameAvailableListener(this);
        mPresentSurfaceTexture.setOnFrameAvailableListener(this);

        onTextureReady(mMirrorSurfaceTexture, mPresentSurfaceTexture);

在 SurfaceTexture 准备好之后,将其绑定到 VirtualDisplay 上:

            protected void onTextureReady(SurfaceTexture mirrorTexture, SurfaceTexture presentTexture) {
                mirrorTexture.setDefaultBufferSize(REQUEST_DISPLAY_WIDTH, REQUEST_DISPLAY_HEIGHT);
                presentTexture.setDefaultBufferSize(REQUEST_DISPLAY_WIDTH, REQUEST_DISPLAY_HEIGHT);
                mMirrorDisplay.setSurface(new Surface(mirrorTexture));
                mPresentationDisplay.setSurface(new Surface(presentTexture));
            }

这里必须 setDefaultBufferSize,不然 VirtualDisplay没有内容输出

GLDisplayFilter

GLDisplayFilter 实现具体的绘图,核心是一个 fragment shader,将两个纹理叠加显示。

这里 texture2 放在上面,其透明部分(alpha < 1)能够透出 texture1;另外 texture1 取部分区域(通过 bound 控制)

    public static final String DISPLAY_FRAGMENT_SHADER =
            " varying highp vec2 textureCoordinate;\n" +
                    " varying highp vec2 textureCoordinate2;\n" +
                    "\n" +
                    " uniform sampler2D inputImageTexture;\n" +
                    " uniform sampler2D inputImageTexture2;\n" +
                    " uniform mediump vec4 bound;\n" +
                    " \n" +
                    " void main()\n" +
                    " {\n" +
                    "    mediump vec4 color = texture2D(inputImageTexture, textureCoordinate * bound.zw + bound.xy);\n" +
                    "    mediump vec4 color2 = texture2D(inputImageTexture2, textureCoordinate);\n" +
                    "    gl_FragColor = vec4(mix(color.rgb, color2.rgb, color2.a), color.a);\n" +
                    " }\n";

(三)视频编码

选择编码器

参考 libstreaming 中的方法,先用上,具体细节没有分析。

https://github.com/fyhertz/libstreaming/tree/master/src/net/majorkernelpanic/streaming/hw

这里面的3个类都要,使用方式如下:

mEncoder = MediaCodec.createByCodecName(
       EncoderDebugger.debug(mContext,1024,768).getEncoderName());

配置编码器

		int bitrate =
				width * heigth * (int) frameRate / 8;
		format = MediaFormat.createVideoFormat("video/avc", width, heigth);
		format.setFloat(MediaFormat.KEY_FRAME_RATE, frameRate);
		format.setInteger(MediaFormat.KEY_COLOR_FORMAT, // TODO: from mine type
				MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
		format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
		format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
		format.setInteger(MediaFormat.KEY_LATENCY, 0);
		mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

与 OpenGL 连接

编码器的输入Surface 交给 OpenGL 的包装 SurfaceHolder,触发 onSurfaceCreated,就完成了与 OpenGL 的连接。

编码输出

用一个线程来驱动输出

        WritableByteChannel c = Channels.newChannel(os);
        while (!Thread.interrupted()) {
            if (popSample(c)) {
                os.flush();
                ++numTotal;
            }
        }

popSample 实现如下:

		int index = mEncoder.dequeueOutputBuffer(mBufferInfo, timeout * 1000);
		if (index >= 0) {
		    ByteBuffer bytes = mEncoder.getOutputBuffer(index);
		    channel.write(bytes);
		    mEncoder.releaseOutputBuffer(index, false);
		}

(四)HTTP输出

视频流通过 HTTP 输出,有“推流”和“拉流”两种模式。

HTTP推流

在Android上,推流可以基于 Okhttp 实现,但是需要扩展RequestBody:

        RequestBody body = new RequestBody() {
            @Override public MediaType contentType() {
                return MediaType.parse("video/h264");
            }
            @Override public long contentLength() {
                return -1;
            }
            @Override public void writeTo(BufferedSink sink) throws IOException {
                writer.write(sink.outputStream());
            }
        };

因为流是无限长度的,Okhttp会用chunked模式传输。

HTTP拉流

让远程屏幕主动拉流,需要Android端实现一个HTTP服务器,用 com.sun.net.httpserver.HttpServer 可以实现一个简单的 HTTP 服务器。

                mServer = HttpServer.create(new InetSocketAddress(mPort), 0);
                mServer.createContext("/", new ConsoleHandler(this));
                mServer.setExecutor(new WorkThreadPool(TAG, 3, 6));
                mServer.start();

请求处理器:

    public void handle(HttpExchange exchange) throws IOException {
        Log.d(TAG, "handle " + exchange.getRequestURI());
        try {
            String origin = exchange.getRequestHeaders().getFirst("Origin");
            Headers responseHeaders = exchange.getResponseHeaders();
            if (origin != null) {
                responseHeaders.add("Access-Control-Allow-Origin", origin);
                responseHeaders.add("Access-Control-Allow-Methods", "*");
                responseHeaders.add("Access-Control-Allow-Headers", "X-Requested-With");
            }
            mManager.process(exchange);
            //exchange.close();
        } catch (Throwable e) {
            Log.w(TAG, "handle", e);
        }
    }

(五)视频播放

采用 ffplay 播放,需要配置低延迟,全屏。

ffplay -fs -f h264 -framerate 60 -nofind_stream_info -flags low_delay http://192.168.1.11/1.h264
Java InputMethod 输入法的实现需要以下步骤: 1. 创建一个实现了 javax.swing.InputMethod 接口的类。 2. 实现 InputMethod 接口中的以下方法: (1)activate()方法:用于激活输入法,可以在该方法中实现输入法界面的显示。 (2)deactivate()方法:用于关闭输入法,可以在该方法中实现输入法界面的隐藏。 (3)getControlObject()方法:用于获取输入法控制对象,可以在该方法中实现输入法控制界面的显示。 (4)setCompositionEnabled(boolean enabled)方法:用于设置是否启用输入法的组合输入模式。 (5)setInputMethodContext(InputMethodContext context)方法:用于设置输入法上下文,该方法将在输入法初始化时被调用。 (6)dispose()方法:用于释放输入法资源。 (7)caretPositionChanged(InputMethodEvent event)方法:用于处理光标位置变化事件。 (8)inputMethodTextChanged(InputMethodEvent event)方法:用于处理输入法文本变化事件。 (9)keyTyped(KeyEvent event)方法:用于处理键盘输入事件。 3. 在实现类中创建输入法界面和输入法控制界面。 4. 在实现类中实现与输入法相关的业务逻辑。 5. 创建一个实现了 javax.swing.InputMethodDescriptor 接口的类,用于描述输入法。 6. 实现 InputMethodDescriptor 接口中的以下方法: (1)getInputMethodDisplayName()方法:用于获取输入法名称。 (2)getInputMethodIcon()方法:用于获取输入法图标。 (3)getLocale()方法:用于获取输入法所支持的语言环境。 (4)createInputMethod()方法:用于创建输入法实例。 7. 在应用程序中注册输入法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值