Home | History | Annotate | Download | only in sample
      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