1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.accessorydisplay.source;
18
19import com.android.accessorydisplay.common.Protocol;
20import com.android.accessorydisplay.common.Service;
21import com.android.accessorydisplay.common.Transport;
22
23import android.content.Context;
24import android.hardware.display.DisplayManager;
25import android.hardware.display.VirtualDisplay;
26import android.media.MediaCodec;
27import android.media.MediaCodec.BufferInfo;
28import android.media.MediaCodecInfo;
29import android.media.MediaFormat;
30import android.os.Handler;
31import android.os.Message;
32import android.view.Display;
33import android.view.Surface;
34
35import java.nio.ByteBuffer;
36
37public class DisplaySourceService extends Service {
38    private static final int MSG_DISPATCH_DISPLAY_ADDED = 1;
39    private static final int MSG_DISPATCH_DISPLAY_REMOVED = 2;
40
41    private static final String DISPLAY_NAME = "Accessory Display";
42    private static final int BIT_RATE = 6000000;
43    private static final int FRAME_RATE = 30;
44    private static final int I_FRAME_INTERVAL = 10;
45
46    private final Callbacks mCallbacks;
47    private final ServiceHandler mHandler;
48    private final DisplayManager mDisplayManager;
49
50    private boolean mSinkAvailable;
51    private int mSinkWidth;
52    private int mSinkHeight;
53    private int mSinkDensityDpi;
54
55    private VirtualDisplayThread mVirtualDisplayThread;
56
57    public DisplaySourceService(Context context, Transport transport, Callbacks callbacks) {
58        super(context, transport, Protocol.DisplaySourceService.ID);
59        mCallbacks = callbacks;
60        mHandler = new ServiceHandler();
61        mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
62    }
63
64    @Override
65    public void start() {
66        super.start();
67
68        getLogger().log("Sending MSG_QUERY.");
69        getTransport().sendMessage(Protocol.DisplaySinkService.ID,
70                Protocol.DisplaySinkService.MSG_QUERY, null);
71    }
72
73    @Override
74    public void stop() {
75        super.stop();
76
77        handleSinkNotAvailable();
78    }
79
80    @Override
81    public void onMessageReceived(int service, int what, ByteBuffer content) {
82        switch (what) {
83            case Protocol.DisplaySourceService.MSG_SINK_AVAILABLE: {
84                getLogger().log("Received MSG_SINK_AVAILABLE");
85                if (content.remaining() >= 12) {
86                    final int width = content.getInt();
87                    final int height = content.getInt();
88                    final int densityDpi = content.getInt();
89                    if (width >= 0 && width <= 4096
90                            && height >= 0 && height <= 4096
91                            && densityDpi >= 60 && densityDpi <= 640) {
92                        handleSinkAvailable(width, height, densityDpi);
93                        return;
94                    }
95                }
96                getLogger().log("Receive invalid MSG_SINK_AVAILABLE message.");
97                break;
98            }
99
100            case Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE: {
101                getLogger().log("Received MSG_SINK_NOT_AVAILABLE");
102                handleSinkNotAvailable();
103                break;
104            }
105        }
106    }
107
108    private void handleSinkAvailable(int width, int height, int densityDpi) {
109        if (mSinkAvailable && mSinkWidth == width && mSinkHeight == height
110                && mSinkDensityDpi == densityDpi) {
111            return;
112        }
113
114        getLogger().log("Accessory display sink available: "
115                + "width=" + width + ", height=" + height
116                + ", densityDpi=" + densityDpi);
117        mSinkAvailable = true;
118        mSinkWidth = width;
119        mSinkHeight = height;
120        mSinkDensityDpi = densityDpi;
121        createVirtualDisplay();
122    }
123
124    private void handleSinkNotAvailable() {
125        getLogger().log("Accessory display sink not available.");
126
127        mSinkAvailable = false;
128        mSinkWidth = 0;
129        mSinkHeight = 0;
130        mSinkDensityDpi = 0;
131        releaseVirtualDisplay();
132    }
133
134    private void createVirtualDisplay() {
135        releaseVirtualDisplay();
136
137        mVirtualDisplayThread = new VirtualDisplayThread(
138                mSinkWidth, mSinkHeight, mSinkDensityDpi);
139        mVirtualDisplayThread.start();
140    }
141
142    private void releaseVirtualDisplay() {
143        if (mVirtualDisplayThread != null) {
144            mVirtualDisplayThread.quit();
145            mVirtualDisplayThread = null;
146        }
147    }
148
149    public interface Callbacks {
150        public void onDisplayAdded(Display display);
151        public void onDisplayRemoved(Display display);
152    }
153
154    private final class ServiceHandler extends Handler {
155        @Override
156        public void handleMessage(Message msg) {
157            switch (msg.what) {
158                case MSG_DISPATCH_DISPLAY_ADDED: {
159                    mCallbacks.onDisplayAdded((Display)msg.obj);
160                    break;
161                }
162
163                case MSG_DISPATCH_DISPLAY_REMOVED: {
164                    mCallbacks.onDisplayRemoved((Display)msg.obj);
165                    break;
166                }
167            }
168        }
169    }
170
171    private final class VirtualDisplayThread extends Thread {
172        private static final int TIMEOUT_USEC = 1000000;
173
174        private final int mWidth;
175        private final int mHeight;
176        private final int mDensityDpi;
177
178        private volatile boolean mQuitting;
179
180        public VirtualDisplayThread(int width, int height, int densityDpi) {
181            mWidth = width;
182            mHeight = height;
183            mDensityDpi = densityDpi;
184        }
185
186        @Override
187        public void run() {
188            MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
189            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
190                    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
191            format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
192            format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
193            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
194
195            MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
196            codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
197            Surface surface = codec.createInputSurface();
198            codec.start();
199
200            VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(
201                    DISPLAY_NAME, mWidth, mHeight, mDensityDpi, surface, 0);
202            if (virtualDisplay != null) {
203                mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_ADDED,
204                        virtualDisplay.getDisplay()).sendToTarget();
205
206                stream(codec);
207
208                mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_REMOVED,
209                        virtualDisplay.getDisplay()).sendToTarget();
210                virtualDisplay.release();
211            }
212
213            codec.signalEndOfInputStream();
214            codec.stop();
215        }
216
217        public void quit() {
218            mQuitting = true;
219        }
220
221        private void stream(MediaCodec codec) {
222            BufferInfo info = new BufferInfo();
223            ByteBuffer[] buffers = null;
224            while (!mQuitting) {
225                int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC);
226                if (index >= 0) {
227                    if (buffers == null) {
228                        buffers = codec.getOutputBuffers();
229                    }
230
231                    ByteBuffer buffer = buffers[index];
232                    buffer.limit(info.offset + info.size);
233                    buffer.position(info.offset);
234
235                    getTransport().sendMessage(Protocol.DisplaySinkService.ID,
236                            Protocol.DisplaySinkService.MSG_CONTENT, buffer);
237                    codec.releaseOutputBuffer(index, false);
238                } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
239                    buffers = null;
240                } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
241                    getLogger().log("Codec dequeue buffer timed out.");
242                }
243            }
244        }
245    }
246}
247