/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.accessorydisplay.source; import com.android.accessorydisplay.common.Protocol; import com.android.accessorydisplay.common.Service; import com.android.accessorydisplay.common.Transport; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Handler; import android.os.Message; import android.view.Display; import android.view.Surface; import java.io.IOException; import java.nio.ByteBuffer; public class DisplaySourceService extends Service { private static final int MSG_DISPATCH_DISPLAY_ADDED = 1; private static final int MSG_DISPATCH_DISPLAY_REMOVED = 2; private static final String DISPLAY_NAME = "Accessory Display"; private static final int BIT_RATE = 6000000; private static final int FRAME_RATE = 30; private static final int I_FRAME_INTERVAL = 10; private final Callbacks mCallbacks; private final ServiceHandler mHandler; private final DisplayManager mDisplayManager; private boolean mSinkAvailable; private int mSinkWidth; private int mSinkHeight; private int mSinkDensityDpi; private VirtualDisplayThread mVirtualDisplayThread; public DisplaySourceService(Context context, Transport transport, Callbacks callbacks) { super(context, transport, Protocol.DisplaySourceService.ID); mCallbacks = callbacks; mHandler = new ServiceHandler(); mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); } @Override public void start() { super.start(); getLogger().log("Sending MSG_QUERY."); getTransport().sendMessage(Protocol.DisplaySinkService.ID, Protocol.DisplaySinkService.MSG_QUERY, null); } @Override public void stop() { super.stop(); handleSinkNotAvailable(); } @Override public void onMessageReceived(int service, int what, ByteBuffer content) { switch (what) { case Protocol.DisplaySourceService.MSG_SINK_AVAILABLE: { getLogger().log("Received MSG_SINK_AVAILABLE"); if (content.remaining() >= 12) { final int width = content.getInt(); final int height = content.getInt(); final int densityDpi = content.getInt(); if (width >= 0 && width <= 4096 && height >= 0 && height <= 4096 && densityDpi >= 60 && densityDpi <= 640) { handleSinkAvailable(width, height, densityDpi); return; } } getLogger().log("Receive invalid MSG_SINK_AVAILABLE message."); break; } case Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE: { getLogger().log("Received MSG_SINK_NOT_AVAILABLE"); handleSinkNotAvailable(); break; } } } private void handleSinkAvailable(int width, int height, int densityDpi) { if (mSinkAvailable && mSinkWidth == width && mSinkHeight == height && mSinkDensityDpi == densityDpi) { return; } getLogger().log("Accessory display sink available: " + "width=" + width + ", height=" + height + ", densityDpi=" + densityDpi); mSinkAvailable = true; mSinkWidth = width; mSinkHeight = height; mSinkDensityDpi = densityDpi; createVirtualDisplay(); } private void handleSinkNotAvailable() { getLogger().log("Accessory display sink not available."); mSinkAvailable = false; mSinkWidth = 0; mSinkHeight = 0; mSinkDensityDpi = 0; releaseVirtualDisplay(); } private void createVirtualDisplay() { releaseVirtualDisplay(); mVirtualDisplayThread = new VirtualDisplayThread( mSinkWidth, mSinkHeight, mSinkDensityDpi); mVirtualDisplayThread.start(); } private void releaseVirtualDisplay() { if (mVirtualDisplayThread != null) { mVirtualDisplayThread.quit(); mVirtualDisplayThread = null; } } public interface Callbacks { public void onDisplayAdded(Display display); public void onDisplayRemoved(Display display); } private final class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DISPATCH_DISPLAY_ADDED: { mCallbacks.onDisplayAdded((Display)msg.obj); break; } case MSG_DISPATCH_DISPLAY_REMOVED: { mCallbacks.onDisplayRemoved((Display)msg.obj); break; } } } } private final class VirtualDisplayThread extends Thread { private static final int TIMEOUT_USEC = 1000000; private final int mWidth; private final int mHeight; private final int mDensityDpi; private volatile boolean mQuitting; public VirtualDisplayThread(int width, int height, int densityDpi) { mWidth = width; mHeight = height; mDensityDpi = densityDpi; } @Override public void run() { MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); MediaCodec codec; try { codec = MediaCodec.createEncoderByType("video/avc"); } catch (IOException e) { throw new RuntimeException( "failed to create video/avc encoder", e); } codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface surface = codec.createInputSurface(); codec.start(); VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay( DISPLAY_NAME, mWidth, mHeight, mDensityDpi, surface, 0); if (virtualDisplay != null) { mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_ADDED, virtualDisplay.getDisplay()).sendToTarget(); stream(codec); mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_REMOVED, virtualDisplay.getDisplay()).sendToTarget(); virtualDisplay.release(); } codec.signalEndOfInputStream(); codec.stop(); } public void quit() { mQuitting = true; } private void stream(MediaCodec codec) { BufferInfo info = new BufferInfo(); ByteBuffer[] buffers = null; while (!mQuitting) { int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC); if (index >= 0) { if (buffers == null) { buffers = codec.getOutputBuffers(); } ByteBuffer buffer = buffers[index]; buffer.limit(info.offset + info.size); buffer.position(info.offset); getTransport().sendMessage(Protocol.DisplaySinkService.ID, Protocol.DisplaySinkService.MSG_CONTENT, buffer); codec.releaseOutputBuffer(index, false); } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { buffers = null; } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { getLogger().log("Codec dequeue buffer timed out."); } } } } }