• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.JELLY_BEAN;
4 import static android.os.Build.VERSION_CODES.LOLLIPOP;
5 import static android.os.Build.VERSION_CODES.N_MR1;
6 import static android.os.Build.VERSION_CODES.O;
7 import static android.os.Build.VERSION_CODES.TIRAMISU;
8 import static com.google.common.base.Preconditions.checkState;
9 import static java.util.concurrent.TimeUnit.MICROSECONDS;
10 import static org.robolectric.shadow.api.Shadow.invokeConstructor;
11 import static org.robolectric.util.ReflectionHelpers.callConstructor;
12 
13 import android.annotation.NonNull;
14 import android.annotation.Nullable;
15 import android.media.MediaCodec;
16 import android.media.MediaCodec.BufferInfo;
17 import android.media.MediaCodec.CodecException;
18 import android.media.MediaCodecInfo;
19 import android.media.MediaCodecList;
20 import android.media.MediaCrypto;
21 import android.media.MediaFormat;
22 import android.view.Surface;
23 import com.google.common.annotations.VisibleForTesting;
24 import java.nio.Buffer;
25 import java.nio.ByteBuffer;
26 import java.nio.ByteOrder;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.concurrent.BlockingQueue;
33 import java.util.concurrent.LinkedBlockingDeque;
34 import org.robolectric.annotation.Implementation;
35 import org.robolectric.annotation.Implements;
36 import org.robolectric.annotation.RealObject;
37 import org.robolectric.annotation.Resetter;
38 import org.robolectric.util.ReflectionHelpers;
39 import org.robolectric.util.ReflectionHelpers.ClassParameter;
40 import org.robolectric.versioning.AndroidVersions.U;
41 
42 /**
43  * Implementation of {@link android.media.MediaCodec} which supports both asynchronous and
44  * synchronous modes.
45  *
46  * <p>By default for any encoded required, a 1 to 1 mapping will be used between the input and
47  * output buffers. Data from a queued input buffer will be copied to the output buffer. In the case
48  * that is it necessary so simulate some form of data compression, a custom encoder or decoder can
49  * be added via {@link #addEncoder(String, CodecConfig)} and {@link #addDecoder(String,
50  * CodecConfig)} respectively.
51  *
52  * <p>Asynchronous mode: Once the codec is started, a format change will be reported, switching to
53  * an empty {@link android.media.MediaFormat} with fake codec-specific info. Following this, the
54  * implementation will present an input buffer, which will be copied to an output buffer once
55  * queued, which will be subsequently presented to the callback handler.
56  */
57 @Implements(value = MediaCodec.class, minSdk = JELLY_BEAN, looseSignatures = true)
58 public class ShadowMediaCodec {
59   private static final int DEFAULT_BUFFER_SIZE = 512;
60   @VisibleForTesting static final int BUFFER_COUNT = 10;
61 
62   // Must keep in sync with MediaCodec.java
63   private static final int EVENT_CALLBACK = 1;
64   private static final int CB_INPUT_AVAILABLE = 1;
65   private static final int CB_OUTPUT_AVAILABLE = 2;
66   private static final int CB_OUTPUT_FORMAT_CHANGE = 4;
67 
68   private static final Map<String, CodecConfig> encoders = new HashMap<>();
69   private static final Map<String, CodecConfig> decoders = new HashMap<>();
70 
71   /**
72    * Default codec that simply moves bytes from the input to the output buffers where the buffers
73    * are of equal size.
74    */
75   private static final CodecConfig DEFAULT_CODEC =
76       new CodecConfig(DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_SIZE, (in, out) -> out.put(in));
77 
78   /** Add a fake encoding codec to the Shadow. */
addEncoder(String type, CodecConfig config)79   public static void addEncoder(String type, CodecConfig config) {
80     encoders.put(type, config);
81   }
82 
83   /** Add a fake decoding codec to the Shadow. */
addDecoder(String type, CodecConfig config)84   public static void addDecoder(String type, CodecConfig config) {
85     decoders.put(type, config);
86   }
87 
88   /** Clears any previously added encoders and decoders. */
89   @Resetter
clearCodecs()90   public static void clearCodecs() {
91     encoders.clear();
92     decoders.clear();
93   }
94 
95   @RealObject private MediaCodec realCodec;
96   @Nullable private CodecConfig.Codec fakeCodec;
97 
98   @Nullable private MediaCodec.Callback callback;
99 
100   @Nullable private MediaFormat pendingOutputFormat;
101   @Nullable private MediaFormat outputFormat;
102 
103   private final BlockingQueue<Integer> inputBuffersPendingDequeue = new LinkedBlockingDeque<>();
104   private final BlockingQueue<Integer> outputBuffersPendingDequeue = new LinkedBlockingDeque<>();
105   /*
106    * Ensures that a dequeued input buffer cannot be queued again until its corresponding output
107    * buffer is dequeued and released.
108    */
109   private final List<Integer> inputBuffersPendingQueuing =
110       Collections.synchronizedList(new ArrayList<>());
111 
112   private final ByteBuffer[] inputBuffers = new ByteBuffer[BUFFER_COUNT];
113   private final ByteBuffer[] outputBuffers = new ByteBuffer[BUFFER_COUNT];
114   private final BufferInfo[] outputBufferInfos = new BufferInfo[BUFFER_COUNT];
115 
116   private boolean isAsync = false;
117 
118   // Member methods.
119 
120   @Implementation
__constructor__(String name, boolean nameIsType, boolean encoder)121   protected void __constructor__(String name, boolean nameIsType, boolean encoder) {
122     invokeConstructor(
123         MediaCodec.class,
124         realCodec,
125         ClassParameter.from(String.class, name),
126         ClassParameter.from(boolean.class, nameIsType),
127         ClassParameter.from(boolean.class, encoder));
128 
129     if (!nameIsType) {
130       for (MediaCodecInfo codecInfo :
131           new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
132         if (codecInfo.getName().equals(name)) {
133           encoder = codecInfo.isEncoder();
134           break;
135         }
136       }
137     }
138 
139     CodecConfig codecConfig =
140         encoder
141             ? encoders.getOrDefault(name, DEFAULT_CODEC)
142             : decoders.getOrDefault(name, DEFAULT_CODEC);
143     fakeCodec = codecConfig.codec;
144 
145     for (int i = 0; i < BUFFER_COUNT; i++) {
146       inputBuffers[i] =
147           ByteBuffer.allocateDirect(codecConfig.inputBufferSize).order(ByteOrder.LITTLE_ENDIAN);
148       outputBuffers[i] =
149           ByteBuffer.allocateDirect(codecConfig.outputBufferSize).order(ByteOrder.LITTLE_ENDIAN);
150       outputBufferInfos[i] = new BufferInfo();
151     }
152   }
153 
154   /** Saves the callback to allow use inside the shadow. */
155   @Implementation(minSdk = LOLLIPOP)
native_setCallback(MediaCodec.Callback callback)156   protected void native_setCallback(MediaCodec.Callback callback) {
157     this.callback = callback;
158   }
159 
160   @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
native_configure( String[] keys, Object[] values, Surface surface, MediaCrypto crypto, int flags)161   protected void native_configure(
162       String[] keys, Object[] values, Surface surface, MediaCrypto crypto, int flags) {
163     innerConfigure(keys, values, surface, crypto, flags);
164   }
165 
166   @Implementation(minSdk = O)
native_configure( Object keys, Object values, Object surface, Object crypto, Object descramblerBinder, Object flags)167   protected void native_configure(
168       Object keys,
169       Object values,
170       Object surface,
171       Object crypto,
172       Object descramblerBinder,
173       Object flags) {
174     innerConfigure(
175         (String[]) keys, (Object[]) values, (Surface) surface, (MediaCrypto) crypto, (int) flags);
176   }
177 
innerConfigure( String[] keys, Object[] values, @Nullable Surface surface, @Nullable MediaCrypto mediaCrypto, int flags)178   private void innerConfigure(
179       String[] keys,
180       Object[] values,
181       @Nullable Surface surface,
182       @Nullable MediaCrypto mediaCrypto,
183       int flags) {
184     isAsync = callback != null;
185     pendingOutputFormat = recreateMediaFormatFromKeysValues(keys, values);
186     fakeCodec.onConfigured(pendingOutputFormat, surface, mediaCrypto, flags);
187   }
188 
189   /**
190    * Starts the async encoding process, by first reporting a format change event, and then
191    * presenting an input buffer to the callback.
192    */
193   @Implementation(minSdk = LOLLIPOP)
native_start()194   protected void native_start() {
195     // Reset state
196     inputBuffersPendingDequeue.clear();
197     outputBuffersPendingDequeue.clear();
198     for (int i = 0; i < BUFFER_COUNT; i++) {
199       inputBuffersPendingDequeue.add(i);
200     }
201 
202     if (isAsync) {
203       // Report the format as changed, to simulate adding codec specific info before making input
204       // buffers available.
205       HashMap<String, Object> format = new HashMap<>();
206       format.put("csd-0", ByteBuffer.wrap(new byte[] {0x13, 0x10}));
207       format.put("csd-1", ByteBuffer.wrap(new byte[0]));
208       postFakeNativeEvent(EVENT_CALLBACK, CB_OUTPUT_FORMAT_CHANGE, 0, format);
209 
210       try {
211         makeInputBufferAvailable(inputBuffersPendingDequeue.take());
212       } catch (InterruptedException e) {
213         Thread.currentThread().interrupt();
214       }
215     }
216   }
217 
218   /** Flushes the available output buffers. */
219   @Implementation(minSdk = LOLLIPOP)
native_flush()220   protected void native_flush() {
221     // Reset input buffers only if the MediaCodec is in synchronous mode. If it is in asynchronous
222     // mode, the client needs to call start().
223     if (!isAsync) {
224       inputBuffersPendingDequeue.clear();
225       outputBuffersPendingDequeue.clear();
226       inputBuffersPendingQueuing.clear();
227       for (int i = 0; i < BUFFER_COUNT; i++) {
228         inputBuffersPendingDequeue.add(i);
229         ((Buffer) inputBuffers[i]).clear();
230       }
231     }
232   }
233 
234   /** Returns the shadow buffers used for input or output. */
235   @Implementation
getBuffers(boolean input)236   protected ByteBuffer[] getBuffers(boolean input) {
237     return input ? inputBuffers : outputBuffers;
238   }
239 
240   /** Returns the input or output buffer corresponding to the given index, or null if invalid. */
241   @Implementation(minSdk = LOLLIPOP)
getBuffer(boolean input, int index)242   protected ByteBuffer getBuffer(boolean input, int index) {
243     ByteBuffer[] buffers = input ? inputBuffers : outputBuffers;
244     return index >= 0 && index < buffers.length && !(input && codecOwnsInputBuffer(index))
245         ? buffers[index]
246         : null;
247   }
248 
249   @Implementation(minSdk = LOLLIPOP)
native_dequeueInputBuffer(long timeoutUs)250   protected int native_dequeueInputBuffer(long timeoutUs) {
251     checkState(!isAsync, "Attempting to deque buffer in Async mode.");
252     try {
253       Integer index;
254 
255       if (timeoutUs < 0) {
256         index = inputBuffersPendingDequeue.take();
257       } else {
258         index = inputBuffersPendingDequeue.poll(timeoutUs, MICROSECONDS);
259       }
260 
261       if (index == null) {
262         return MediaCodec.INFO_TRY_AGAIN_LATER;
263       }
264 
265       inputBuffersPendingQueuing.add(index);
266       return index;
267     } catch (InterruptedException e) {
268       Thread.currentThread().interrupt();
269       return MediaCodec.INFO_TRY_AGAIN_LATER;
270     }
271   }
272 
273   /**
274    * Triggers presentation of the corresponding output buffer for the given input buffer, and passes
275    * the given metadata as buffer info.
276    */
277   @Implementation(minSdk = LOLLIPOP)
native_queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags)278   protected void native_queueInputBuffer(
279       int index, int offset, int size, long presentationTimeUs, int flags) {
280     if (index < 0
281         || index >= inputBuffers.length
282         || codecOwnsInputBuffer(index)
283         || !canQueueInputBuffer(index)) {
284       throwCodecException(
285           /* errorCode= */ 0, /* actionCode= */ 0, "Input buffer not owned by client: " + index);
286     }
287 
288     BufferInfo info = new BufferInfo();
289     info.set(offset, size, presentationTimeUs, flags);
290 
291     makeOutputBufferAvailable(index, info);
292     inputBuffersPendingQueuing.remove(Integer.valueOf(index));
293   }
294 
295   @Implementation(minSdk = LOLLIPOP)
native_dequeueOutputBuffer(BufferInfo info, long timeoutUs)296   protected int native_dequeueOutputBuffer(BufferInfo info, long timeoutUs) {
297     checkState(!isAsync, "Attempting to deque buffer in Async mode.");
298     try {
299       if (pendingOutputFormat != null) {
300         outputFormat = pendingOutputFormat;
301         pendingOutputFormat = null;
302         return MediaCodec.INFO_OUTPUT_FORMAT_CHANGED;
303       }
304 
305       Integer index;
306       if (timeoutUs < 0) {
307         index = outputBuffersPendingDequeue.take();
308       } else {
309         index = outputBuffersPendingDequeue.poll(timeoutUs, MICROSECONDS);
310       }
311 
312       if (index == null) {
313         return MediaCodec.INFO_TRY_AGAIN_LATER;
314       }
315 
316       copyBufferInfo(outputBufferInfos[index], info);
317 
318       return index;
319     } catch (InterruptedException e) {
320       Thread.currentThread().interrupt();
321       return MediaCodec.INFO_TRY_AGAIN_LATER;
322     }
323   }
324 
325   @Implementation
releaseOutputBuffer(int index, boolean renderer)326   protected void releaseOutputBuffer(int index, boolean renderer) {
327     releaseOutputBuffer(index);
328   }
329 
330   @Implementation(minSdk = LOLLIPOP)
releaseOutputBuffer(int index, long renderTimestampNs)331   protected void releaseOutputBuffer(int index, long renderTimestampNs) {
332     releaseOutputBuffer(index);
333   }
334 
releaseOutputBuffer(int index)335   private void releaseOutputBuffer(int index) {
336     if (outputBuffersPendingDequeue.contains(index)) {
337       throw new IllegalStateException("Cannot release a buffer when it's still owned by the codec");
338     }
339     makeInputBufferAvailable(index);
340   }
341 
makeInputBufferAvailable(int index)342   private void makeInputBufferAvailable(int index) {
343     if (index < 0 || index >= inputBuffers.length) {
344       throw new IndexOutOfBoundsException("Cannot make non-existent input available.");
345     }
346 
347     // Reset the input buffer.
348     ((Buffer) inputBuffers[index]).clear();
349 
350     if (isAsync) {
351       inputBuffersPendingQueuing.add(index);
352       // Signal input buffer availability.
353       postFakeNativeEvent(EVENT_CALLBACK, CB_INPUT_AVAILABLE, index, null);
354     } else {
355       inputBuffersPendingDequeue.add(index);
356     }
357   }
358 
makeOutputBufferAvailable(int index, BufferInfo info)359   private void makeOutputBufferAvailable(int index, BufferInfo info) {
360     if (index < 0 || index >= outputBuffers.length) {
361       throw new IndexOutOfBoundsException("Cannot make non-existent output buffer available.");
362     }
363     Buffer inputBuffer = inputBuffers[index];
364     Buffer outputBuffer = outputBuffers[index];
365     BufferInfo outputBufferInfo = outputBufferInfos[index];
366 
367     // Clears the output buffer, as it's already fully consumed.
368     outputBuffer.clear();
369 
370     inputBuffer.position(info.offset).limit(info.offset + info.size);
371     fakeCodec.process(inputBuffers[index], outputBuffers[index]);
372 
373     outputBufferInfo.flags = info.flags;
374     outputBufferInfo.size = outputBuffer.position();
375     outputBufferInfo.offset = info.offset;
376     outputBufferInfo.presentationTimeUs = info.presentationTimeUs;
377     outputBuffer.flip();
378 
379     outputBuffersPendingDequeue.add(index);
380 
381     if (isAsync) {
382       // Dequeue the buffer to signal its availablility to the client.
383       outputBuffersPendingDequeue.remove(Integer.valueOf(index));
384       // Signal output buffer availability.
385       postFakeNativeEvent(EVENT_CALLBACK, CB_OUTPUT_AVAILABLE, index, outputBufferInfos[index]);
386     }
387   }
388 
postFakeNativeEvent(int what, int arg1, int arg2, @Nullable Object obj)389   private void postFakeNativeEvent(int what, int arg1, int arg2, @Nullable Object obj) {
390     ReflectionHelpers.callInstanceMethod(
391         MediaCodec.class,
392         realCodec,
393         "postEventFromNative",
394         ClassParameter.from(int.class, what),
395         ClassParameter.from(int.class, arg1),
396         ClassParameter.from(int.class, arg2),
397         ClassParameter.from(Object.class, obj));
398   }
399 
codecOwnsInputBuffer(int index)400   private boolean codecOwnsInputBuffer(int index) {
401     return inputBuffersPendingDequeue.contains(index);
402   }
403 
canQueueInputBuffer(int index)404   private boolean canQueueInputBuffer(int index) {
405     return inputBuffersPendingQueuing.contains(index);
406   }
407 
408   /** Prevents calling Android-only methods on basic ByteBuffer objects. */
409   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
invalidateByteBuffer(@ullable ByteBuffer[] buffers, int index)410   protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
411 
412   @Implementation(minSdk = U.SDK_INT)
invalidateByteBufferLocked( @ullable ByteBuffer[] buffers, int index, boolean input)413   protected void invalidateByteBufferLocked(
414       @Nullable ByteBuffer[] buffers, int index, boolean input) {}
415 
416   /** Prevents calling Android-only methods on basic ByteBuffer objects. */
417   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
validateInputByteBuffer(@ullable ByteBuffer[] buffers, int index)418   protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
419 
420   @Implementation(minSdk = U.SDK_INT)
validateInputByteBufferLocked(@ullable ByteBuffer[] buffers, int index)421   protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {}
422 
423   /** Prevents calling Android-only methods on basic ByteBuffer objects. */
424   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
revalidateByteBuffer(@ullable ByteBuffer[] buffers, int index)425   protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
426 
427   @Implementation(minSdk = U.SDK_INT)
revalidateByteBuffer(@ullable ByteBuffer[] buffers, int index, boolean input)428   protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
429 
430   /**
431    * Prevents calling Android-only methods on basic ByteBuffer objects. Replicates existing behavior
432    * adjusting buffer positions and limits.
433    */
434   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
validateOutputByteBuffer( @ullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info)435   protected void validateOutputByteBuffer(
436       @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
437     if (buffers != null && index >= 0 && index < buffers.length) {
438       Buffer buffer = (Buffer) buffers[index];
439       if (buffer != null) {
440         buffer.limit(info.offset + info.size).position(info.offset);
441       }
442     }
443   }
444 
445   @Implementation(minSdk = U.SDK_INT)
validateOutputByteBufferLocked( @ullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info)446   protected void validateOutputByteBufferLocked(
447       @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
448     validateOutputByteBuffer(buffers, index, info);
449   }
450 
451   /** Prevents calling Android-only methods on basic ByteBuffer objects. */
452   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
invalidateByteBuffers(@ullable ByteBuffer[] buffers)453   protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
454 
455   @Implementation(minSdk = U.SDK_INT)
invalidateByteBuffersLocked(@ullable ByteBuffer[] buffers)456   protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {}
457 
458   /** Prevents attempting to free non-direct ByteBuffer objects. */
459   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
freeByteBuffer(@ullable ByteBuffer buffer)460   protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}
461 
462   @Implementation(minSdk = U.SDK_INT)
freeByteBufferLocked(@ullable ByteBuffer buffer)463   protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {}
464 
465   /** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
466   @Implements(className = "android.media.MediaCodec$BufferMap$CodecBuffer", minSdk = LOLLIPOP)
467   protected static class ShadowCodecBuffer {
468 
469     // Seems to be required to work.
ShadowCodecBuffer()470     public ShadowCodecBuffer() {}
471 
472     // Seems to be required to work.
473     @Implementation
__constructor__()474     protected void __constructor__() {}
475 
476     /** Prevents attempting to free non-direct ByteBuffer objects. */
477     @Implementation
free()478     protected void free() {}
479   }
480 
481   /** Returns a default {@link MediaFormat} if not set via {@link #getOutputFormat()}. */
482   @Implementation
getOutputFormat()483   protected MediaFormat getOutputFormat() {
484     if (outputFormat == null) {
485       return new MediaFormat();
486     }
487     return outputFormat;
488   }
489 
copyBufferInfo(BufferInfo from, BufferInfo to)490   private static void copyBufferInfo(BufferInfo from, BufferInfo to) {
491     to.set(from.offset, from.size, from.presentationTimeUs, from.flags);
492   }
493 
recreateMediaFormatFromKeysValues(String[] keys, Object[] values)494   private static MediaFormat recreateMediaFormatFromKeysValues(String[] keys, Object[] values) {
495     MediaFormat mediaFormat = new MediaFormat();
496 
497     // This usage of `instanceof` is how API 29 `MediaFormat#getValueTypeForKey` works.
498     for (int i = 0; i < keys.length; i++) {
499       if (values[i] == null || values[i] instanceof ByteBuffer) {
500         mediaFormat.setByteBuffer(keys[i], (ByteBuffer) values[i]);
501       } else if (values[i] instanceof Integer) {
502         mediaFormat.setInteger(keys[i], (Integer) values[i]);
503       } else if (values[i] instanceof Long) {
504         mediaFormat.setLong(keys[i], (Long) values[i]);
505       } else if (values[i] instanceof Float) {
506         mediaFormat.setFloat(keys[i], (Float) values[i]);
507       } else if (values[i] instanceof String) {
508         mediaFormat.setString(keys[i], (String) values[i]);
509       } else {
510         throw new IllegalArgumentException("Invalid value for key.");
511       }
512     }
513 
514     return mediaFormat;
515   }
516 
517   /**
518    * Configuration that can be supplied to {@link ShadowMediaCodec} to simulate actual
519    * encoding/decoding.
520    */
521   public static final class CodecConfig {
522 
523     private final int inputBufferSize;
524     private final int outputBufferSize;
525     private final Codec codec;
526 
527     /**
528      * @param inputBufferSize the size of the buffers offered as input to the codec.
529      * @param outputBufferSize the size of the buffers offered as output from the codec.
530      * @param codec should be able to map from input size -> output size
531      */
CodecConfig(int inputBufferSize, int outputBufferSize, Codec codec)532     public CodecConfig(int inputBufferSize, int outputBufferSize, Codec codec) {
533       this.inputBufferSize = inputBufferSize;
534       this.outputBufferSize = outputBufferSize;
535 
536       this.codec = codec;
537     }
538 
539     /**
540      * A codec is implemented as part of the configuration to allow the {@link ShadowMediaCodec} to
541      * simulate actual encoding/decoding. It's not expected for implementations to perform real
542      * encoding/decoding, but to produce a output similar in size ratio to the expected codec..
543      */
544     public interface Codec {
545 
546       /** Move the bytes on the in buffer to the out buffer */
process(ByteBuffer in, ByteBuffer out)547       void process(ByteBuffer in, ByteBuffer out);
548       /** Called when the codec is configured. @see MediaCodec#configure */
onConfigured( MediaFormat format, @Nullable Surface surface, @Nullable MediaCrypto crypto, int flags)549       default void onConfigured(
550           MediaFormat format, @Nullable Surface surface, @Nullable MediaCrypto crypto, int flags) {}
551     }
552   }
553 
554   /** Reflectively throws a {@link CodecException}. */
throwCodecException(int errorCode, int actionCode, String message)555   private static void throwCodecException(int errorCode, int actionCode, String message) {
556     throw callConstructor(
557         MediaCodec.CodecException.class,
558         ClassParameter.from(Integer.TYPE, errorCode),
559         ClassParameter.from(Integer.TYPE, actionCode),
560         ClassParameter.from(String.class, message));
561   }
562 }
563