在前文裏介紹了 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