1 /* 2 * Copyright (C) 2023 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 com.example.android.vdmdemo.common; 18 19 import static com.google.common.util.concurrent.Uninterruptibles.putUninterruptibly; 20 21 import android.media.MediaCodec; 22 import android.media.MediaCodec.BufferInfo; 23 import android.media.MediaCodec.CodecException; 24 import android.media.MediaCodecInfo; 25 import android.media.MediaFormat; 26 import android.os.Environment; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.util.Log; 30 import android.view.Surface; 31 32 import androidx.annotation.GuardedBy; 33 import androidx.annotation.NonNull; 34 35 import com.example.android.vdmdemo.common.RemoteEventProto.EncodedFrame; 36 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; 37 import com.google.protobuf.ByteString; 38 39 import java.io.BufferedOutputStream; 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.OutputStream; 45 import java.nio.ByteBuffer; 46 import java.util.Objects; 47 import java.util.Optional; 48 import java.util.concurrent.BlockingQueue; 49 import java.util.concurrent.LinkedBlockingQueue; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 import java.util.function.Consumer; 52 53 /** Shared class between the client and the host, managing the video encoding and decoding. */ 54 public class VideoManager { 55 private static final String TAG = "VideoManager"; 56 private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC; 57 58 @GuardedBy("mCodecLock") 59 private MediaCodec mMediaCodec; 60 61 private final Object mCodecLock = new Object(); 62 private final HandlerThread mCallbackThread; 63 private final boolean mRecordEncoderOutput; 64 private final BlockingQueue<EncodedFrame> mEventQueue = new LinkedBlockingQueue<>(100); 65 private final BlockingQueue<Integer> mFreeInputBuffers = new LinkedBlockingQueue<>(100); 66 private final RemoteIo mRemoteIo; 67 private final Consumer<RemoteEvent> mRemoteFrameConsumer = this::processFrameProto; 68 private StorageFile mStorageFile; 69 private DecoderThread mDecoderThread; 70 71 private VideoManagerProtoHelper mProtoHelper; 72 73 private interface VideoManagerProtoHelper { 74 extractEncodedFrame(RemoteEvent event)75 Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event); 76 createFrameProto(byte[] data, int flags, long presentationTimeUs)77 RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs); 78 getVideoManagerId()79 String getVideoManagerId(); 80 } 81 VideoManager( VideoManagerProtoHelper protoHelper, RemoteIo remoteIo, MediaCodec mediaCodec, boolean recordEncoderOutput)82 private VideoManager( 83 VideoManagerProtoHelper protoHelper, RemoteIo remoteIo, MediaCodec mediaCodec, 84 boolean recordEncoderOutput) { 85 mProtoHelper = protoHelper; 86 mRemoteIo = remoteIo; 87 mMediaCodec = mediaCodec; 88 mRecordEncoderOutput = recordEncoderOutput; 89 90 mCallbackThread = new HandlerThread("VideoManager-" + protoHelper.getVideoManagerId()); 91 mCallbackThread.start(); 92 mediaCodec.setCallback(new MediaCodecCallback(), new Handler(mCallbackThread.getLooper())); 93 94 if (!mediaCodec.getCodecInfo().isEncoder()) { 95 remoteIo.addMessageConsumer(mRemoteFrameConsumer); 96 } 97 98 if (recordEncoderOutput) { 99 mStorageFile = new StorageFile(protoHelper.getVideoManagerId()); 100 } 101 } 102 103 /** Creates a VideoManager instance for encoding display stream. */ createDisplayEncoder( int displayId, RemoteIo remoteIo, boolean recordEncoderOutput)104 public static VideoManager createDisplayEncoder( 105 int displayId, RemoteIo remoteIo, boolean recordEncoderOutput) { 106 try { 107 MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); 108 return new VideoManager(new DisplayProtoHelper(displayId), remoteIo, mediaCodec, 109 recordEncoderOutput); 110 } catch (IOException e) { 111 throw new AssertionError("Unhandled exception", e); 112 } 113 } 114 115 /** Creates a VideoManager instance for decoding display stream. */ createDisplayDecoder(int displayId, RemoteIo remoteIo)116 public static VideoManager createDisplayDecoder(int displayId, RemoteIo remoteIo) { 117 try { 118 MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE); 119 return new VideoManager(new DisplayProtoHelper(displayId), remoteIo, mediaCodec, false); 120 } catch (IOException e) { 121 throw new AssertionError("Unhandled exception", e); 122 } 123 } 124 125 /** Creates a VideoManager instance for encoding camera stream. */ createCameraEncoder( String cameraId, RemoteIo remoteIo, boolean recordEncoderOutput)126 public static VideoManager createCameraEncoder( 127 String cameraId, RemoteIo remoteIo, boolean recordEncoderOutput) { 128 try { 129 MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); 130 return new VideoManager(new CameraProtoHelper(cameraId), remoteIo, mediaCodec, 131 recordEncoderOutput); 132 } catch (IOException e) { 133 throw new AssertionError("Unhandled exception", e); 134 } 135 } 136 137 /** Creates a VideoManager instance for decoding camera stream. */ createCameraDecoder(String cameraId, RemoteIo remoteIo)138 public static VideoManager createCameraDecoder(String cameraId, RemoteIo remoteIo) { 139 try { 140 MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE); 141 return new VideoManager(new CameraProtoHelper(cameraId), remoteIo, mediaCodec, false); 142 } catch (IOException e) { 143 throw new AssertionError("Unhandled exception", e); 144 } 145 } 146 147 /** Stops processing and resets the internal state. */ stop()148 public void stop() { 149 synchronized (mCodecLock) { 150 if (mMediaCodec == null) { 151 return; 152 } 153 if (mMediaCodec.getCodecInfo().isEncoder()) { 154 mMediaCodec.signalEndOfInputStream(); 155 } else { 156 mRemoteIo.removeMessageConsumer(mRemoteFrameConsumer); 157 mEventQueue.clear(); 158 mDecoderThread.exit(); 159 } 160 mCallbackThread.quitSafely(); 161 try { 162 mMediaCodec.flush(); 163 mMediaCodec.stop(); 164 } catch (IllegalStateException exception) { 165 // It's possible the codec transitioned from "executing" state, because 166 // the surface was already released. 167 Log.w(TAG, "IllegalStateException while flushing codec", exception); 168 } 169 mMediaCodec.release(); 170 mMediaCodec = null; 171 } 172 if (mRecordEncoderOutput) { 173 mStorageFile.closeOutputFile(); 174 } 175 } 176 177 /** Creates a surface for encoding. */ createInputSurface(int width, int height, int frameRate)178 public Surface createInputSurface(int width, int height, int frameRate) { 179 MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height); 180 mediaFormat.setInteger( 181 MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 182 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500000); 183 mediaFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 0); 184 mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); 185 mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); 186 synchronized (mCodecLock) { 187 mMediaCodec.configure( 188 mediaFormat, /* surface= */ null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 189 return mMediaCodec.createInputSurface(); 190 } 191 } 192 193 /** Starts encoding. {@link #createInputSurface} must have been called already. */ startEncoding()194 public void startEncoding() { 195 synchronized (mCodecLock) { 196 mMediaCodec.start(); 197 } 198 } 199 200 /** Starts decoding from the given surface. */ startDecoding(Surface surface, int width, int height)201 public void startDecoding(Surface surface, int width, int height) { 202 MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height); 203 mediaFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1); 204 mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 100); 205 synchronized (mCodecLock) { 206 mMediaCodec.configure(mediaFormat, surface, null, 0); 207 mMediaCodec.start(); 208 } 209 mDecoderThread = new DecoderThread(); 210 mDecoderThread.start(); 211 } 212 processFrameProto(RemoteEvent event)213 private void processFrameProto(RemoteEvent event) { 214 mProtoHelper.extractEncodedFrame(event).ifPresent( 215 encodedFrame -> putUninterruptibly(mEventQueue, encodedFrame)); 216 } 217 218 private final class MediaCodecCallback extends MediaCodec.Callback { 219 @Override onInputBufferAvailable(@onNull MediaCodec codec, int i)220 public void onInputBufferAvailable(@NonNull MediaCodec codec, int i) { 221 mFreeInputBuffers.add(i); 222 } 223 224 @Override onOutputBufferAvailable( @onNull MediaCodec codec, int i, @NonNull BufferInfo bufferInfo)225 public void onOutputBufferAvailable( 226 @NonNull MediaCodec codec, int i, @NonNull BufferInfo bufferInfo) { 227 synchronized (mCodecLock) { 228 if (mMediaCodec == null) { 229 return; 230 } 231 if (mMediaCodec.getCodecInfo().isEncoder()) { 232 ByteBuffer buffer = mMediaCodec.getOutputBuffer(i); 233 byte[] data = new byte[bufferInfo.size]; 234 Objects.requireNonNull(buffer).get(data, bufferInfo.offset, bufferInfo.size); 235 mMediaCodec.releaseOutputBuffer(i, false); 236 if (mRecordEncoderOutput) { 237 mStorageFile.writeOutputFile(data); 238 } 239 240 mRemoteIo.sendMessage( 241 mProtoHelper.createFrameProto( 242 data, bufferInfo.flags, bufferInfo.presentationTimeUs)); 243 } else { 244 try { 245 mMediaCodec.releaseOutputBuffer(i, true); 246 } catch (CodecException exception) { 247 Log.e(TAG, "Codec exception ", exception); 248 } 249 } 250 } 251 } 252 253 @Override onError(@onNull MediaCodec mediaCodec, @NonNull CodecException e)254 public void onError(@NonNull MediaCodec mediaCodec, @NonNull CodecException e) { 255 } 256 257 @Override onOutputFormatChanged( @onNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat)258 public void onOutputFormatChanged( 259 @NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) { 260 } 261 } 262 263 private class DecoderThread extends Thread { 264 265 private final AtomicBoolean mExit = new AtomicBoolean(false); 266 267 @SuppressWarnings("Interruption") exit()268 void exit() { 269 mExit.set(true); 270 interrupt(); 271 } 272 273 @Override run()274 public void run() { 275 while (!(Thread.interrupted() && mExit.get())) { 276 try { 277 EncodedFrame encodedFrame = mEventQueue.take(); 278 int inputBuffer = mFreeInputBuffers.take(); 279 280 synchronized (mCodecLock) { 281 if (mMediaCodec == null) { 282 continue; 283 } 284 try { 285 ByteBuffer inBuffer = mMediaCodec.getInputBuffer(inputBuffer); 286 byte[] data = encodedFrame.getFrameData().toByteArray(); 287 Objects.requireNonNull(inBuffer).put(data); 288 if (mRecordEncoderOutput) { 289 mStorageFile.writeOutputFile(data); 290 } 291 mMediaCodec.queueInputBuffer( 292 inputBuffer, 293 0, 294 encodedFrame.getFrameData().size(), 295 encodedFrame.getPresentationTimeUs(), 296 encodedFrame.getFlags()); 297 } catch (MediaCodec.CodecException exception) { 298 Log.e(TAG, "MediaCodec exception while queuing input", exception); 299 mMediaCodec.release(); 300 mMediaCodec = null; 301 } 302 } 303 } catch (InterruptedException e) { 304 if (mExit.get()) { 305 break; 306 } 307 } 308 } 309 } 310 } 311 312 private static class StorageFile { 313 private static final String DIR = "Download"; 314 private static final String FILENAME = "vdmdemo_encoder_output"; 315 316 private OutputStream mOutputStream; 317 StorageFile(String id)318 private StorageFile(String id) { 319 String filePath = DIR + "/" + FILENAME + "_" + id + ".h264"; 320 File f = new File(Environment.getExternalStorageDirectory(), filePath); 321 try { 322 mOutputStream = new BufferedOutputStream(new FileOutputStream(f)); 323 } catch (FileNotFoundException e) { 324 Log.e(TAG, "Error creating or opening storage file", e); 325 } 326 } 327 writeOutputFile(byte[] data)328 private void writeOutputFile(byte[] data) { 329 if (mOutputStream == null) { 330 return; 331 } 332 try { 333 mOutputStream.write(data); 334 } catch (IOException e) { 335 Log.e(TAG, "Error writing to output file", e); 336 } 337 } 338 closeOutputFile()339 private void closeOutputFile() { 340 if (mOutputStream == null) { 341 return; 342 } 343 try { 344 mOutputStream.flush(); 345 mOutputStream.close(); 346 } catch (IOException e) { 347 Log.e(TAG, "Error closing output file", e); 348 } 349 } 350 } 351 352 private static class DisplayProtoHelper implements VideoManagerProtoHelper { 353 private final int mDisplayId; 354 private int mFrameIndex = 0; 355 DisplayProtoHelper(int displayId)356 DisplayProtoHelper(int displayId) { 357 mDisplayId = displayId; 358 } 359 360 @Override extractEncodedFrame(RemoteEvent event)361 public Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event) { 362 if (event.hasDisplayFrame() && event.getDisplayId() == mDisplayId) { 363 return Optional.of(event.getDisplayFrame()); 364 } 365 return Optional.empty(); 366 } 367 368 @Override createFrameProto(byte[] data, int flags, long presentationTimeUs)369 public RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs) { 370 return RemoteEvent.newBuilder() 371 .setDisplayId(mDisplayId) 372 .setDisplayFrame( 373 EncodedFrame.newBuilder() 374 .setFrameData(ByteString.copyFrom(data)) 375 .setFrameIndex(mFrameIndex++) 376 .setPresentationTimeUs(presentationTimeUs) 377 .setFlags(flags)) 378 .build(); 379 } 380 381 @Override getVideoManagerId()382 public String getVideoManagerId() { 383 return "display" + mDisplayId; 384 } 385 } 386 387 private static class CameraProtoHelper implements VideoManagerProtoHelper { 388 private final String mCameraId; 389 private int mFrameIndex = 0; 390 CameraProtoHelper(String cameraId)391 CameraProtoHelper(String cameraId) { 392 mCameraId = cameraId; 393 } 394 395 @Override extractEncodedFrame(RemoteEvent event)396 public Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event) { 397 if (event.hasCameraFrame() && event.getCameraFrame().getCameraId().equals(mCameraId)) { 398 Log.d(TAG, "Received encoded frame " 399 + event.getCameraFrame().getCameraFrame().getFrameIndex()); 400 return Optional.of(event.getCameraFrame().getCameraFrame()); 401 } 402 return Optional.empty(); 403 } 404 405 @Override createFrameProto(byte[] data, int flags, long presentationTimeUs)406 public RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs) { 407 Log.d(TAG, "Sending " + data.length + "B encoded camera frame"); 408 return RemoteEvent.newBuilder() 409 .setCameraFrame( 410 RemoteEventProto.CameraFrame.newBuilder() 411 .setCameraId(mCameraId) 412 .setCameraFrame( 413 EncodedFrame.newBuilder() 414 .setFrameData(ByteString.copyFrom(data)) 415 .setFrameIndex(mFrameIndex++) 416 .setPresentationTimeUs(presentationTimeUs) 417 .setFlags(flags)) 418 ) 419 .build(); 420 } 421 422 @Override getVideoManagerId()423 public String getVideoManagerId() { 424 return "camera" + mCameraId; 425 } 426 } 427 } 428