• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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