1 /* 2 * Copyright 2018 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc; 12 13 import android.graphics.SurfaceTexture; 14 import android.media.MediaCodec; 15 import android.media.MediaCodecInfo.CodecCapabilities; 16 import android.media.MediaCrypto; 17 import android.media.MediaFormat; 18 import android.os.Bundle; 19 import android.view.Surface; 20 import androidx.annotation.Nullable; 21 import java.nio.ByteBuffer; 22 import java.util.ArrayList; 23 import java.util.List; 24 25 /** 26 * Fake MediaCodec that implements the basic state machine. 27 * 28 * @note This class is only intended for single-threaded tests and is not thread-safe. 29 */ 30 public class FakeMediaCodecWrapper implements MediaCodecWrapper { 31 private static final int NUM_INPUT_BUFFERS = 10; 32 private static final int NUM_OUTPUT_BUFFERS = 10; 33 private static final int MAX_ENCODED_DATA_SIZE_BYTES = 1_000; 34 35 /** 36 * MediaCodec state as defined by: 37 * https://developer.android.com/reference/android/media/MediaCodec.html 38 */ 39 public enum State { 40 STOPPED_CONFIGURED(Primary.STOPPED), 41 STOPPED_UNINITIALIZED(Primary.STOPPED), 42 STOPPED_ERROR(Primary.STOPPED), 43 EXECUTING_FLUSHED(Primary.EXECUTING), 44 EXECUTING_RUNNING(Primary.EXECUTING), 45 EXECUTING_END_OF_STREAM(Primary.EXECUTING), 46 RELEASED(Primary.RELEASED); 47 48 public enum Primary { STOPPED, EXECUTING, RELEASED } 49 50 private final Primary primary; 51 State(Primary primary)52 State(Primary primary) { 53 this.primary = primary; 54 } 55 getPrimary()56 public Primary getPrimary() { 57 return primary; 58 } 59 } 60 61 /** Represents an output buffer that will be returned by dequeueOutputBuffer. */ 62 public static class QueuedOutputBufferInfo { 63 private int index; 64 private int offset; 65 private int size; 66 private long presentationTimeUs; 67 private int flags; 68 QueuedOutputBufferInfo( int index, int offset, int size, long presentationTimeUs, int flags)69 private QueuedOutputBufferInfo( 70 int index, int offset, int size, long presentationTimeUs, int flags) { 71 this.index = index; 72 this.offset = offset; 73 this.size = size; 74 this.presentationTimeUs = presentationTimeUs; 75 this.flags = flags; 76 } 77 create( int index, int offset, int size, long presentationTimeUs, int flags)78 public static QueuedOutputBufferInfo create( 79 int index, int offset, int size, long presentationTimeUs, int flags) { 80 return new QueuedOutputBufferInfo(index, offset, size, presentationTimeUs, flags); 81 } 82 getIndex()83 public int getIndex() { 84 return index; 85 } 86 getOffset()87 public int getOffset() { 88 return offset; 89 } 90 getSize()91 public int getSize() { 92 return size; 93 } 94 getPresentationTimeUs()95 public long getPresentationTimeUs() { 96 return presentationTimeUs; 97 } 98 getFlags()99 public int getFlags() { 100 return flags; 101 } 102 } 103 104 private State state = State.STOPPED_UNINITIALIZED; 105 private @Nullable MediaFormat configuredFormat; 106 private int configuredFlags; 107 private final MediaFormat inputFormat; 108 private final MediaFormat outputFormat; 109 private final ByteBuffer[] inputBuffers = new ByteBuffer[NUM_INPUT_BUFFERS]; 110 private final ByteBuffer[] outputBuffers = new ByteBuffer[NUM_OUTPUT_BUFFERS]; 111 private final boolean[] inputBufferReserved = new boolean[NUM_INPUT_BUFFERS]; 112 private final boolean[] outputBufferReserved = new boolean[NUM_OUTPUT_BUFFERS]; 113 private final List<QueuedOutputBufferInfo> queuedOutputBuffers = new ArrayList<>(); 114 FakeMediaCodecWrapper(MediaFormat inputFormat, MediaFormat outputFormat)115 public FakeMediaCodecWrapper(MediaFormat inputFormat, MediaFormat outputFormat) { 116 this.inputFormat = inputFormat; 117 this.outputFormat = outputFormat; 118 } 119 120 /** Returns the current simulated state of MediaCodec. */ getState()121 public State getState() { 122 return state; 123 } 124 125 /** Gets the last configured media format passed to configure. */ getConfiguredFormat()126 public @Nullable MediaFormat getConfiguredFormat() { 127 return configuredFormat; 128 } 129 130 /** Returns the last flags passed to configure. */ getConfiguredFlags()131 public int getConfiguredFlags() { 132 return configuredFlags; 133 } 134 135 /** 136 * Adds a texture buffer that will be returned by dequeueOutputBuffer. Returns index of the 137 * buffer. 138 */ addOutputTexture(long presentationTimestampUs, int flags)139 public int addOutputTexture(long presentationTimestampUs, int flags) { 140 int index = getFreeOutputBuffer(); 141 queuedOutputBuffers.add(QueuedOutputBufferInfo.create( 142 index, /* offset= */ 0, /* size= */ 0, presentationTimestampUs, flags)); 143 return index; 144 } 145 146 /** 147 * Adds a byte buffer buffer that will be returned by dequeueOutputBuffer. Returns index of the 148 * buffer. 149 */ addOutputData(byte[] data, long presentationTimestampUs, int flags)150 public int addOutputData(byte[] data, long presentationTimestampUs, int flags) { 151 int index = getFreeOutputBuffer(); 152 ByteBuffer outputBuffer = outputBuffers[index]; 153 154 outputBuffer.clear(); 155 outputBuffer.put(data); 156 outputBuffer.rewind(); 157 158 queuedOutputBuffers.add(QueuedOutputBufferInfo.create( 159 index, /* offset= */ 0, data.length, presentationTimestampUs, flags)); 160 return index; 161 } 162 163 /** 164 * Returns the first output buffer that is not reserved and reserves it. It will be stay reserved 165 * until released with releaseOutputBuffer. 166 */ getFreeOutputBuffer()167 private int getFreeOutputBuffer() { 168 for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { 169 if (!outputBufferReserved[i]) { 170 outputBufferReserved[i] = true; 171 return i; 172 } 173 } 174 throw new RuntimeException("All output buffers reserved!"); 175 } 176 177 @Override configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)178 public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { 179 if (state != State.STOPPED_UNINITIALIZED) { 180 throw new IllegalStateException("Expected state STOPPED_UNINITIALIZED but was " + state); 181 } 182 state = State.STOPPED_CONFIGURED; 183 configuredFormat = format; 184 configuredFlags = flags; 185 186 final int width = configuredFormat.getInteger(MediaFormat.KEY_WIDTH); 187 final int height = configuredFormat.getInteger(MediaFormat.KEY_HEIGHT); 188 final int yuvSize = width * height * 3 / 2; 189 final int inputBufferSize; 190 final int outputBufferSize; 191 192 if ((flags & MediaCodec.CONFIGURE_FLAG_ENCODE) != 0) { 193 final int colorFormat = configuredFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT); 194 195 inputBufferSize = colorFormat == CodecCapabilities.COLOR_FormatSurface ? 0 : yuvSize; 196 outputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; 197 } else { 198 inputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; 199 outputBufferSize = surface != null ? 0 : yuvSize; 200 } 201 202 for (int i = 0; i < inputBuffers.length; i++) { 203 inputBuffers[i] = ByteBuffer.allocateDirect(inputBufferSize); 204 } 205 for (int i = 0; i < outputBuffers.length; i++) { 206 outputBuffers[i] = ByteBuffer.allocateDirect(outputBufferSize); 207 } 208 } 209 210 @Override start()211 public void start() { 212 if (state != State.STOPPED_CONFIGURED) { 213 throw new IllegalStateException("Expected state STOPPED_CONFIGURED but was " + state); 214 } 215 state = State.EXECUTING_RUNNING; 216 } 217 218 @Override flush()219 public void flush() { 220 if (state.getPrimary() != State.Primary.EXECUTING) { 221 throw new IllegalStateException("Expected state EXECUTING but was " + state); 222 } 223 state = State.EXECUTING_FLUSHED; 224 } 225 226 @Override stop()227 public void stop() { 228 if (state.getPrimary() != State.Primary.EXECUTING) { 229 throw new IllegalStateException("Expected state EXECUTING but was " + state); 230 } 231 state = State.STOPPED_UNINITIALIZED; 232 } 233 234 @Override release()235 public void release() { 236 state = State.RELEASED; 237 } 238 239 @Override dequeueInputBuffer(long timeoutUs)240 public int dequeueInputBuffer(long timeoutUs) { 241 if (state != State.EXECUTING_FLUSHED && state != State.EXECUTING_RUNNING) { 242 throw new IllegalStateException( 243 "Expected state EXECUTING_FLUSHED or EXECUTING_RUNNING but was " + state); 244 } 245 state = State.EXECUTING_RUNNING; 246 247 for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { 248 if (!inputBufferReserved[i]) { 249 inputBufferReserved[i] = true; 250 return i; 251 } 252 } 253 return MediaCodec.INFO_TRY_AGAIN_LATER; 254 } 255 256 @Override queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags)257 public void queueInputBuffer( 258 int index, int offset, int size, long presentationTimeUs, int flags) { 259 if (state.getPrimary() != State.Primary.EXECUTING) { 260 throw new IllegalStateException("Expected state EXECUTING but was " + state); 261 } 262 if (flags != 0) { 263 throw new UnsupportedOperationException( 264 "Flags are not implemented in FakeMediaCodecWrapper."); 265 } 266 } 267 268 @Override dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs)269 public int dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs) { 270 if (state.getPrimary() != State.Primary.EXECUTING) { 271 throw new IllegalStateException("Expected state EXECUTING but was " + state); 272 } 273 274 if (queuedOutputBuffers.isEmpty()) { 275 return MediaCodec.INFO_TRY_AGAIN_LATER; 276 } 277 QueuedOutputBufferInfo outputBufferInfo = queuedOutputBuffers.remove(/* index= */ 0); 278 info.set(outputBufferInfo.getOffset(), outputBufferInfo.getSize(), 279 outputBufferInfo.getPresentationTimeUs(), outputBufferInfo.getFlags()); 280 return outputBufferInfo.getIndex(); 281 } 282 283 @Override releaseOutputBuffer(int index, boolean render)284 public void releaseOutputBuffer(int index, boolean render) { 285 if (state.getPrimary() != State.Primary.EXECUTING) { 286 throw new IllegalStateException("Expected state EXECUTING but was " + state); 287 } 288 if (!outputBufferReserved[index]) { 289 throw new RuntimeException("Released output buffer was not in use."); 290 } 291 outputBufferReserved[index] = false; 292 } 293 294 @Override getInputBuffer(int index)295 public ByteBuffer getInputBuffer(int index) { 296 return inputBuffers[index]; 297 } 298 299 @Override getOutputBuffer(int index)300 public ByteBuffer getOutputBuffer(int index) { 301 return outputBuffers[index]; 302 } 303 304 @Override getInputFormat()305 public MediaFormat getInputFormat() { 306 return inputFormat; 307 } 308 309 @Override getOutputFormat()310 public MediaFormat getOutputFormat() { 311 return outputFormat; 312 } 313 314 @Override createInputSurface()315 public Surface createInputSurface() { 316 return new Surface(new SurfaceTexture(/* texName= */ 0)); 317 } 318 319 @Override setParameters(Bundle params)320 public void setParameters(Bundle params) {} 321 } 322