| /* |
| * 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."); |
| } |
| } |
| } |
| } |
| } |