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.sample; 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.net.ServerSocket; 42 import java.net.Socket; 43 import java.nio.ByteBuffer; 44 import java.util.UUID; 45 46 /** 47 * This class encapsulates all work related to managing networked virtual display. 48 * <p> 49 * It opens server socket and listens on port {@code PORT} for incoming connections. Once connection 50 * is established it creates virtual display and media encoder and starts streaming video to that 51 * socket. If the receiving part is disconnected, it will keep port open and virtual display won't 52 * be destroyed. 53 */ 54 public class NetworkedVirtualDisplay { 55 private static final String TAG = "Cluster." + NetworkedVirtualDisplay.class.getSimpleName(); 56 57 private final String mUniqueId = UUID.randomUUID().toString(); 58 59 private final DisplayManager mDisplayManager; 60 private final int mWidth; 61 private final int mHeight; 62 private final int mDpi; 63 64 private static final int PORT = 5151; 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 private static final int MSG_START = 0; 70 private static final int MSG_STOP = 1; 71 private static final int MSG_RESUBMIT_FRAME = 2; 72 73 private VirtualDisplay mVirtualDisplay; 74 private MediaCodec mVideoEncoder; 75 private HandlerThread mThread = new HandlerThread("NetworkThread"); 76 private Handler mHandler; 77 private ServerSocket mServerSocket; 78 private OutputStream mOutputStream; 79 private byte[] mBuffer = null; 80 private int mLastFrameLength = 0; 81 82 private final DebugCounter mCounter = new DebugCounter(); 83 84 NetworkedVirtualDisplay(Context context, int width, int height, int dpi) { 85 mDisplayManager = context.getSystemService(DisplayManager.class); 86 mWidth = width; 87 mHeight = height; 88 mDpi = dpi; 89 90 DisplayListener displayListener = new DisplayListener() { 91 @Override 92 public void onDisplayAdded(int i) { 93 final Display display = mDisplayManager.getDisplay(i); 94 if (display != null && getDisplayName().equals(display.getName())) { 95 onVirtualDisplayReady(display); 96 } 97 } 98 99 @Override 100 public void onDisplayRemoved(int i) {} 101 102 @Override 103 public void onDisplayChanged(int i) {} 104 }; 105 106 mDisplayManager.registerDisplayListener(displayListener, new Handler()); 107 } 108 109 /** 110 * Opens socket and creates virtual display asynchronously once connection established. Clients 111 * of this class may subscribe to 112 * {@link android.hardware.display.DisplayManager#registerDisplayListener( 113 * DisplayListener, Handler)} to be notified when virtual display is created. 114 * Note, that this method should be called only once. 115 * 116 * @return Unique display name associated with the instance of this class. 117 * 118 * @see {@link Display#getName()} 119 * 120 * @throws IllegalStateException thrown if networked display already started 121 */ 122 public String start() { 123 if (mThread.isAlive()) { 124 throw new IllegalStateException("Already started"); 125 } 126 mThread.start(); 127 mHandler = new NetworkThreadHandler(mThread.getLooper()); 128 mHandler.sendMessage(Message.obtain(mHandler, MSG_START)); 129 130 return getDisplayName(); 131 } 132 133 public void release() { 134 stopCasting(); 135 136 if (mVirtualDisplay != null) { 137 mVirtualDisplay.release(); 138 mVirtualDisplay = null; 139 } 140 mThread.quit(); 141 } 142 143 private String getDisplayName() { 144 return "Cluster-" + mUniqueId; 145 } 146 147 148 private VirtualDisplay createVirtualDisplay() { 149 Log.i(TAG, "createVirtualDisplay " + mWidth + "x" + mHeight +"@" + mDpi); 150 return mDisplayManager.createVirtualDisplay(getDisplayName(), mWidth, mHeight, mDpi, 151 null, 0 /* flags */, null, null ); 152 } 153 154 private void onVirtualDisplayReady(Display display) { 155 Log.i(TAG, "onVirtualDisplayReady, display: " + display); 156 } 157 158 private void startCasting(Handler handler) { 159 Log.i(TAG, "Start casting..."); 160 mVideoEncoder = createVideoStream(handler); 161 162 if (mVirtualDisplay == null) { 163 mVirtualDisplay = createVirtualDisplay(); 164 } 165 mVirtualDisplay.setSurface(mVideoEncoder.createInputSurface()); 166 mVideoEncoder.start(); 167 168 Log.i(TAG, "Video encoder started"); 169 } 170 171 private MediaCodec createVideoStream(Handler handler) { 172 MediaCodec encoder; 173 try { 174 encoder = MediaCodec.createEncoderByType(MEDIA_FORMAT_MIMETYPE); 175 } catch (IOException e) { 176 Log.e(TAG, "Failed to create video encoder for " + MEDIA_FORMAT_MIMETYPE, e); 177 return null; 178 } 179 180 encoder.setCallback(new MediaCodec.Callback() { 181 @Override 182 public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) { 183 Log.i(TAG, "onInputBufferAvailable, index: " + index); 184 } 185 186 @Override 187 public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, 188 @NonNull BufferInfo info) { 189 Log.i(TAG, "onOutputBufferAvailable, index: " + index); 190 mCounter.outputBuffers++; 191 doOutputBufferAvailable(index, info); 192 } 193 194 @Override 195 public void onError(@NonNull MediaCodec codec, @NonNull CodecException e) { 196 Log.e(TAG, "onError, codec: " + codec, e); 197 mCounter.bufferErrors++; 198 } 199 200 @Override 201 public void onOutputFormatChanged(@NonNull MediaCodec codec, 202 @NonNull MediaFormat format) { 203 Log.i(TAG, "onOutputFormatChanged, codec: " + codec + ", format: " + format); 204 205 } 206 }, handler); 207 208 configureVideoEncoder(encoder, mWidth, mHeight); 209 return encoder; 210 } 211 212 private void doOutputBufferAvailable(int index, @NonNull BufferInfo info) { 213 mHandler.removeMessages(MSG_RESUBMIT_FRAME); 214 215 ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index); 216 if (encodedData == null) { 217 throw new RuntimeException("couldn't fetch buffer at index " + index); 218 } 219 220 if (info.size != 0) { 221 encodedData.position(info.offset); 222 encodedData.limit(info.offset + info.size); 223 mLastFrameLength = encodedData.remaining(); 224 if (mBuffer == null || mBuffer.length < mLastFrameLength) { 225 Log.i(TAG, "Allocating new buffer: " + mLastFrameLength); 226 mBuffer = new byte[mLastFrameLength]; 227 } 228 encodedData.get(mBuffer, 0, mLastFrameLength); 229 mVideoEncoder.releaseOutputBuffer(index, false); 230 231 sendFrame(mBuffer, mLastFrameLength); 232 233 // If nothing happens in Virtual Display we won't receive new frames. If we won't keep 234 // sending frames it could be a problem for the receiver because it needs certain 235 // number of frames in order to start decoding. 236 scheduleResendingLastFrame(1000 / FPS); 237 } else { 238 Log.e(TAG, "Skipping empty buffer"); 239 mVideoEncoder.releaseOutputBuffer(index, false); 240 } 241 } 242 243 private void scheduleResendingLastFrame(long delayMs) { 244 Message msg = mHandler.obtainMessage(MSG_RESUBMIT_FRAME); 245 mHandler.sendMessageDelayed(msg, delayMs); 246 } 247 248 private void sendFrame(byte[] buf, int len) { 249 try { 250 mOutputStream.write(buf, 0, len); 251 Log.i(TAG, "Bytes written: " + len); 252 } catch (IOException e) { 253 mCounter.clientsDisconnected++; 254 mOutputStream = null; 255 Log.e(TAG, "Failed to write data to socket, restart casting", e); 256 restart(); 257 } 258 } 259 260 private void stopCasting() { 261 Log.i(TAG, "Stopping casting..."); 262 if (mServerSocket != null) { 263 try { 264 mServerSocket.close(); 265 } catch (IOException e) { 266 Log.w(TAG, "Failed to close server socket, ignoring", e); 267 } 268 mServerSocket = null; 269 } 270 271 if (mVirtualDisplay != null) { 272 // We do not want to destroy virtual display (as it will also destroy all the 273 // activities on that display, instead we will turn off the display by setting 274 // a null surface. 275 Surface surface = mVirtualDisplay.getSurface(); 276 if (surface != null) surface.release(); 277 mVirtualDisplay.setSurface(null); 278 } 279 280 if (mVideoEncoder != null) { 281 // Releasing encoder as stop/start didn't work well (couldn't create or reuse input 282 // surface). 283 mVideoEncoder.stop(); 284 mVideoEncoder.release(); 285 mVideoEncoder = null; 286 } 287 Log.i(TAG, "Casting stopped"); 288 } 289 290 private synchronized void restart() { 291 // This method could be called from different threads when receiver has disconnected. 292 if (mHandler.hasMessages(MSG_START)) return; 293 294 mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP)); 295 mHandler.sendMessage(Message.obtain(mHandler, MSG_START)); 296 } 297 298 private class NetworkThreadHandler extends Handler { 299 300 NetworkThreadHandler(Looper looper) { 301 super(looper); 302 } 303 304 @Override 305 public void handleMessage(Message msg) { 306 switch (msg.what) { 307 case MSG_START: 308 if (mServerSocket == null) { 309 mServerSocket = openServerSocket(); 310 } 311 Log.i(TAG, "Server socket opened"); 312 313 mOutputStream = waitForReceiver(mServerSocket); 314 if (mOutputStream == null) { 315 sendMessage(Message.obtain(this, MSG_START)); 316 break; 317 } 318 mCounter.clientsConnected++; 319 320 startCasting(this); 321 break; 322 323 case MSG_STOP: 324 stopCasting(); 325 break; 326 327 case MSG_RESUBMIT_FRAME: 328 if (mServerSocket != null && mOutputStream != null) { 329 Log.i(TAG, "Resending the last frame again. Buffer: " + mLastFrameLength); 330 sendFrame(mBuffer, mLastFrameLength); 331 } 332 // We will keep sending last frame every second as a heartbeat. 333 scheduleResendingLastFrame(1000L); 334 break; 335 } 336 } 337 } 338 339 private static void configureVideoEncoder(MediaCodec codec, int width, int height) { 340 MediaFormat format = MediaFormat.createVideoFormat(MEDIA_FORMAT_MIMETYPE, width, height); 341 342 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 343 MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 344 format.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE); 345 format.setInteger(MediaFormat.KEY_FRAME_RATE, FPS); 346 format.setInteger(MediaFormat.KEY_CAPTURE_RATE, FPS); 347 format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); 348 format.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 second between I-frames 349 format.setInteger(MediaFormat.KEY_LEVEL, CodecProfileLevel.AVCLevel31); 350 format.setInteger(MediaFormat.KEY_PROFILE, 351 MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); 352 353 codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 354 } 355 356 private OutputStream waitForReceiver(ServerSocket serverSocket) { 357 try { 358 Log.i(TAG, "Listening for incoming connections on port: " + PORT); 359 Socket socket = serverSocket.accept(); 360 361 Log.i(TAG, "Receiver connected: " + socket); 362 listenReceiverDisconnected(socket.getInputStream()); 363 364 return socket.getOutputStream(); 365 } catch (IOException e) { 366 Log.e(TAG, "Failed to accept connection"); 367 return null; 368 } 369 } 370 371 private void listenReceiverDisconnected(InputStream inputStream) { 372 new Thread(() -> { 373 try { 374 if (inputStream.read() == -1) throw new IOException(); 375 } catch (IOException e) { 376 Log.w(TAG, "Receiver has disconnected", e); 377 } 378 restart(); 379 }).start(); 380 } 381 382 private static ServerSocket openServerSocket() { 383 try { 384 return new ServerSocket(PORT); 385 } catch (IOException e) { 386 Log.e(TAG, "Failed to create server socket", e); 387 throw new RuntimeException(e); 388 } 389 } 390 391 @Override 392 public String toString() { 393 return getClass() + "{" 394 + mServerSocket 395 +", receiver connected: " + (mOutputStream != null) 396 +", encoder: " + mVideoEncoder 397 +", virtualDisplay" + mVirtualDisplay 398 + "}"; 399 } 400 401 private static class DebugCounter { 402 long outputBuffers; 403 long bufferErrors; 404 long clientsConnected; 405 long clientsDisconnected; 406 407 @Override 408 public String toString() { 409 return getClass().getSimpleName() + "{" 410 + "outputBuffers=" + outputBuffers 411 + ", bufferErrors=" + bufferErrors 412 + ", clientsConnected=" + clientsConnected 413 + ", clientsDisconnected= " + clientsDisconnected 414 + "}"; 415 } 416 } 417 } 418