1 /* 2 * Copyright (C) 2018 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 17 package android.car.cluster; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.hardware.display.DisplayManager; 22 import android.hardware.display.DisplayManager.DisplayListener; 23 import android.hardware.display.VirtualDisplay; 24 import android.media.MediaCodec; 25 import android.media.MediaCodec.BufferInfo; 26 import android.media.MediaCodec.CodecException; 27 import android.media.MediaCodecInfo; 28 import android.media.MediaCodecInfo.CodecProfileLevel; 29 import android.media.MediaFormat; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.util.Log; 35 import android.view.Display; 36 import android.view.Surface; 37 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.OutputStream; 41 import java.io.RandomAccessFile; 42 import java.net.ServerSocket; 43 import java.net.Socket; 44 import java.nio.ByteBuffer; 45 import java.util.UUID; 46 47 /** 48 * This class encapsulates all work related to managing networked virtual display. 49 * <p> 50 * It opens a socket and listens on port {@code PORT} for connections, or the emulator pipe. Once 51 * connection is established it creates virtual display and media encoder and starts streaming video 52 * to that socket. If the receiving part is disconnected, it will keep port open and virtual 53 * display won't be destroyed. 54 */ 55 public class NetworkedVirtualDisplay { 56 private static final String TAG = "Cluster." + NetworkedVirtualDisplay.class.getSimpleName(); 57 58 private final String mUniqueId = UUID.randomUUID().toString(); 59 60 private final DisplayManager mDisplayManager; 61 private final int mWidth; 62 private final int mHeight; 63 private final int mDpi; 64 65 private static final int FPS = 25; 66 private static final int BITRATE = 6144000; 67 private static final String MEDIA_FORMAT_MIMETYPE = MediaFormat.MIMETYPE_VIDEO_AVC; 68 69 public static final int MSG_START = 0; 70 public static final int MSG_STOP = 1; 71 public static final int MSG_SEND_FRAME = 2; 72 73 private static final String PIPE_NAME = "pipe:qemud:carCluster"; 74 private static final String PIPE_DEVICE = "/dev/qemu_pipe"; 75 76 // Constants shared with emulator in car-cluster-widget.cpp 77 public static final int PIPE_START = 1; 78 public static final int PIPE_STOP = 2; 79 80 private static final int PORT = 5151; 81 82 private SenderThread mActiveThread; 83 private HandlerThread mBroadcastThread = new HandlerThread("BroadcastThread"); 84 85 private VirtualDisplay mVirtualDisplay; 86 private MediaCodec mVideoEncoder; 87 private Handler mHandler; 88 private byte[] mBuffer = null; 89 private int mLastFrameLength = 0; 90 91 private final DebugCounter mCounter = new DebugCounter(); 92 NetworkedVirtualDisplay(Context context, int width, int height, int dpi)93 NetworkedVirtualDisplay(Context context, int width, int height, int dpi) { 94 mDisplayManager = context.getSystemService(DisplayManager.class); 95 mWidth = width; 96 mHeight = height; 97 mDpi = dpi; 98 99 DisplayListener displayListener = new DisplayListener() { 100 @Override 101 public void onDisplayAdded(int i) { 102 final Display display = mDisplayManager.getDisplay(i); 103 if (display != null && getDisplayName().equals(display.getName())) { 104 onVirtualDisplayReady(display); 105 } 106 } 107 108 @Override 109 public void onDisplayRemoved(int i) {} 110 111 @Override 112 public void onDisplayChanged(int i) {} 113 }; 114 115 mDisplayManager.registerDisplayListener(displayListener, new Handler()); 116 } 117 118 /** 119 * Opens socket and creates virtual display asynchronously once connection established. Clients 120 * of this class may subscribe to 121 * {@link android.hardware.display.DisplayManager#registerDisplayListener( 122 * DisplayListener, Handler)} to be notified when virtual display is created. 123 * Note, that this method should be called only once. 124 * 125 * @return Unique display name associated with the instance of this class. 126 * 127 * @see {@link Display#getName()} 128 * 129 * @throws IllegalStateException thrown if networked display already started 130 */ start()131 public String start() { 132 if (mBroadcastThread.isAlive()) { 133 throw new IllegalStateException("Already started"); 134 } 135 136 mBroadcastThread.start(); 137 mHandler = new BroadcastThreadHandler(mBroadcastThread.getLooper()); 138 mHandler.sendMessage(Message.obtain(mHandler, MSG_START)); 139 return getDisplayName(); 140 } 141 release()142 public void release() { 143 mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP)); 144 mBroadcastThread.quitSafely(); 145 146 if (mVirtualDisplay != null) { 147 mVirtualDisplay.setSurface(null); 148 mVirtualDisplay.release(); 149 mVirtualDisplay = null; 150 } 151 } 152 getDisplayName()153 private String getDisplayName() { 154 return "Cluster-" + mUniqueId; 155 } 156 createVirtualDisplay()157 private VirtualDisplay createVirtualDisplay() { 158 Log.i(TAG, "createVirtualDisplay " + mWidth + "x" + mHeight +"@" + mDpi); 159 return mDisplayManager.createVirtualDisplay(getDisplayName(), mWidth, mHeight, mDpi, 160 null, 0 /* flags */, null, null ); 161 } 162 onVirtualDisplayReady(Display display)163 private void onVirtualDisplayReady(Display display) { 164 Log.i(TAG, "onVirtualDisplayReady, display: " + display); 165 } 166 startCasting(Handler handler)167 private void startCasting(Handler handler) { 168 Log.i(TAG, "Start casting..."); 169 if (mVideoEncoder != null) { 170 Log.i(TAG, "Already started casting"); 171 return; 172 } 173 mVideoEncoder = createVideoStream(handler); 174 175 if (mVirtualDisplay == null) { 176 mVirtualDisplay = createVirtualDisplay(); 177 } 178 179 mVirtualDisplay.setSurface(mVideoEncoder.createInputSurface()); 180 mVideoEncoder.start(); 181 Log.i(TAG, "Video encoder started"); 182 } 183 createVideoStream(Handler handler)184 private MediaCodec createVideoStream(Handler handler) { 185 MediaCodec encoder; 186 try { 187 encoder = MediaCodec.createEncoderByType(MEDIA_FORMAT_MIMETYPE); 188 } catch (IOException e) { 189 Log.e(TAG, "Failed to create video encoder for " + MEDIA_FORMAT_MIMETYPE, e); 190 return null; 191 } 192 193 encoder.setCallback(new MediaCodec.Callback() { 194 @Override 195 public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) { 196 // Nothing to do 197 } 198 199 @Override 200 public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, 201 @NonNull BufferInfo info) { 202 mCounter.outputBuffers++; 203 doOutputBufferAvailable(index, info); 204 } 205 206 @Override 207 public void onError(@NonNull MediaCodec codec, @NonNull CodecException e) { 208 Log.e(TAG, "onError, codec: " + codec, e); 209 mCounter.bufferErrors++; 210 stopCasting(); 211 startCasting(handler); 212 } 213 214 @Override 215 public void onOutputFormatChanged(@NonNull MediaCodec codec, 216 @NonNull MediaFormat format) { 217 Log.i(TAG, "onOutputFormatChanged, codec: " + codec + ", format: " + format); 218 219 } 220 }, handler); 221 222 configureVideoEncoder(encoder, mWidth, mHeight); 223 return encoder; 224 } 225 doOutputBufferAvailable(int index, @NonNull BufferInfo info)226 private void doOutputBufferAvailable(int index, @NonNull BufferInfo info) { 227 mHandler.removeMessages(MSG_SEND_FRAME); 228 229 ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index); 230 if (encodedData == null) { 231 throw new RuntimeException("couldn't fetch buffer at index " + index); 232 } 233 234 if (info.size != 0) { 235 encodedData.position(info.offset); 236 encodedData.limit(info.offset + info.size); 237 mLastFrameLength = encodedData.remaining(); 238 if (mBuffer == null || mBuffer.length < mLastFrameLength) { 239 Log.i(TAG, "Allocating new buffer: " + mLastFrameLength); 240 mBuffer = new byte[mLastFrameLength]; 241 } 242 encodedData.get(mBuffer, 0, mLastFrameLength); 243 mVideoEncoder.releaseOutputBuffer(index, false); 244 245 // Send this frame asynchronously (avoiding blocking on the socket). We might miss 246 // frames if the consumer is not fast enough, but this is acceptable. 247 sendFrameAsync(0); 248 } else { 249 Log.e(TAG, "Skipping empty buffer"); 250 mVideoEncoder.releaseOutputBuffer(index, false); 251 } 252 } 253 sendFrameAsync(long delayMs)254 private void sendFrameAsync(long delayMs) { 255 Message msg = mHandler.obtainMessage(MSG_SEND_FRAME); 256 mHandler.sendMessageDelayed(msg, delayMs); 257 } 258 sendFrame(byte[] buf, int len)259 private void sendFrame(byte[] buf, int len) { 260 if (mActiveThread != null) { 261 mActiveThread.send(buf, len); 262 } 263 } 264 stopCasting()265 private void stopCasting() { 266 Log.i(TAG, "Stopping casting..."); 267 268 if (mVirtualDisplay != null) { 269 Surface surface = mVirtualDisplay.getSurface(); 270 if (surface != null) surface.release(); 271 } 272 273 if (mVideoEncoder != null) { 274 // Releasing encoder as stop/start didn't work well (couldn't create or reuse input 275 // surface). 276 try { 277 mVideoEncoder.stop(); 278 mVideoEncoder.release(); 279 } catch (IllegalStateException e) { 280 // do nothing, already released 281 } 282 mVideoEncoder = null; 283 } 284 Log.i(TAG, "Casting stopped"); 285 } 286 287 private class BroadcastThreadHandler extends Handler { 288 private static final int MAX_FAIL_COUNT = 10; 289 private int mFailConnectCounter; 290 BroadcastThreadHandler(Looper looper)291 BroadcastThreadHandler(Looper looper) { 292 super(looper); 293 } 294 295 @Override handleMessage(Message msg)296 public void handleMessage(Message msg) { 297 switch (msg.what) { 298 case MSG_START: 299 Log.i(TAG, "Received start message"); 300 301 // Make sure mActiveThread cannot start multiple times 302 if (mActiveThread != null) { 303 Log.w(TAG, "Trying to start a running thread. Race condition may exist"); 304 break; 305 } 306 307 // Failure to connect to either pipe or network returns null 308 if (mActiveThread == null) { 309 mActiveThread = tryPipeConnect(); 310 } 311 if (mActiveThread == null) { 312 mActiveThread = tryNetworkConnect(); 313 } 314 if (mActiveThread == null) { 315 // When failed attempt limit is reached, clean up and quit this thread. 316 mFailConnectCounter++; 317 if (mFailConnectCounter >= MAX_FAIL_COUNT) { 318 Log.e(TAG, "Too many failed connection attempts; aborting"); 319 release(); 320 throw new RuntimeException("Abort after failed connection attempts"); 321 } 322 mHandler.sendMessage(Message.obtain(mHandler, MSG_START)); 323 break; 324 } 325 326 try { 327 mFailConnectCounter = 0; 328 mCounter.clientsConnected++; 329 mActiveThread.start(); 330 startCasting(this); 331 } catch (Exception e) { 332 Log.e(TAG, "Failed to start thread", e); 333 Log.e(TAG, "DebugCounter: " + mCounter); 334 } 335 break; 336 337 case MSG_STOP: 338 Log.i(TAG, "Received stop message"); 339 stopCasting(); 340 mCounter.clientsDisconnected++; 341 if (mActiveThread != null) { 342 mActiveThread.close(); 343 try { 344 mActiveThread.join(); 345 } catch (InterruptedException e) { 346 Log.e(TAG, "Waiting for active thread to close failed", e); 347 } 348 mActiveThread = null; 349 } 350 break; 351 352 case MSG_SEND_FRAME: 353 if (mActiveThread == null) { 354 // Stop the chaining signal if there's no client to send to 355 break; 356 } 357 sendFrame(mBuffer, mLastFrameLength); 358 // We will keep sending last frame every second as a heartbeat. 359 sendFrameAsync(1000L); 360 break; 361 } 362 } 363 364 // Returns null if can't establish pipe connection 365 // Otherwise returns the corresponding client thread tryPipeConnect()366 private PipeThread tryPipeConnect() { 367 try { 368 RandomAccessFile pipe = new RandomAccessFile(PIPE_DEVICE, "rw"); 369 byte[] temp = new byte[PIPE_NAME.length() + 1]; 370 temp[PIPE_NAME.length()] = 0; 371 System.arraycopy(PIPE_NAME.getBytes(), 0, temp, 0, PIPE_NAME.length()); 372 pipe.write(temp); 373 374 // At this point, the pipe exists, so we will just wait for a start signal 375 // This is in case pipe still sends leftover stops from last instantiation 376 int signal = pipe.read(); 377 while (signal != PIPE_START) { 378 Log.i(TAG, "Received non-start signal: " + signal); 379 signal = pipe.read(); 380 } 381 return new PipeThread(mHandler, pipe); 382 } catch (IOException e) { 383 Log.e(TAG, "Failed to establish pipe connection", e); 384 return null; 385 } 386 } 387 388 // Returns null if can't establish network connection 389 // Otherwise returns the corresponding client thread tryNetworkConnect()390 private SocketThread tryNetworkConnect() { 391 try { 392 ServerSocket serverSocket = new ServerSocket(PORT); 393 Log.i(TAG, "Server socket opened"); 394 Socket socket = serverSocket.accept(); 395 socket.setTcpNoDelay(true); 396 socket.setKeepAlive(true); 397 socket.setSoLinger(true, 0); 398 399 InputStream inputStream = socket.getInputStream(); 400 OutputStream outputStream = socket.getOutputStream(); 401 402 return new SocketThread(mHandler, serverSocket, inputStream, outputStream); 403 } catch (IOException e) { 404 Log.e(TAG, "Failed to establish network connection", e); 405 return null; 406 } 407 } 408 } 409 configureVideoEncoder(MediaCodec codec, int width, int height)410 private static void configureVideoEncoder(MediaCodec codec, int width, int height) { 411 MediaFormat format = MediaFormat.createVideoFormat(MEDIA_FORMAT_MIMETYPE, width, height); 412 413 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 414 MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 415 format.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE); 416 format.setInteger(MediaFormat.KEY_FRAME_RATE, FPS); 417 format.setInteger(MediaFormat.KEY_CAPTURE_RATE, FPS); 418 format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); 419 format.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 second between I-frames 420 format.setInteger(MediaFormat.KEY_LEVEL, CodecProfileLevel.AVCLevel31); 421 format.setInteger(MediaFormat.KEY_PROFILE, 422 MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); 423 424 codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 425 } 426 427 @Override toString()428 public String toString() { 429 return getClass() + "{" 430 + ", receiver connected: " + (mActiveThread != null) 431 + ", encoder: " + mVideoEncoder 432 + ", virtualDisplay" + mVirtualDisplay 433 + "}"; 434 } 435 436 private static class DebugCounter { 437 long outputBuffers; 438 long bufferErrors; 439 long clientsConnected; 440 long clientsDisconnected; 441 442 @Override toString()443 public String toString() { 444 return getClass().getSimpleName() + "{" 445 + "outputBuffers=" + outputBuffers 446 + ", bufferErrors=" + bufferErrors 447 + ", clientsConnected=" + clientsConnected 448 + ", clientsDisconnected= " + clientsDisconnected 449 + "}"; 450 } 451 } 452 } 453