• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2017 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.media.MediaCodec;
14 import android.media.MediaCodecInfo.CodecCapabilities;
15 import android.media.MediaFormat;
16 import android.os.SystemClock;
17 import android.view.Surface;
18 import androidx.annotation.Nullable;
19 import java.io.IOException;
20 import java.nio.ByteBuffer;
21 import java.util.concurrent.BlockingDeque;
22 import java.util.concurrent.LinkedBlockingDeque;
23 import java.util.concurrent.TimeUnit;
24 import org.webrtc.ThreadUtils.ThreadChecker;
25 
26 /**
27  * Android hardware video decoder.
28  */
29 @SuppressWarnings("deprecation")
30 // Cannot support API 16 without using deprecated methods.
31 // TODO(sakal): Rename to MediaCodecVideoDecoder once the deprecated implementation is removed.
32 class AndroidVideoDecoder implements VideoDecoder, VideoSink {
33   private static final String TAG = "AndroidVideoDecoder";
34 
35   // TODO(magjed): Use MediaFormat.KEY_* constants when part of the public API.
36   private static final String MEDIA_FORMAT_KEY_STRIDE = "stride";
37   private static final String MEDIA_FORMAT_KEY_SLICE_HEIGHT = "slice-height";
38   private static final String MEDIA_FORMAT_KEY_CROP_LEFT = "crop-left";
39   private static final String MEDIA_FORMAT_KEY_CROP_RIGHT = "crop-right";
40   private static final String MEDIA_FORMAT_KEY_CROP_TOP = "crop-top";
41   private static final String MEDIA_FORMAT_KEY_CROP_BOTTOM = "crop-bottom";
42 
43   // MediaCodec.release() occasionally hangs.  Release stops waiting and reports failure after
44   // this timeout.
45   private static final int MEDIA_CODEC_RELEASE_TIMEOUT_MS = 5000;
46 
47   // WebRTC queues input frames quickly in the beginning on the call. Wait for input buffers with a
48   // long timeout (500 ms) to prevent this from causing the codec to return an error.
49   private static final int DEQUEUE_INPUT_TIMEOUT_US = 500000;
50 
51   // Dequeuing an output buffer will block until a buffer is available (up to 100 milliseconds).
52   // If this timeout is exceeded, the output thread will unblock and check if the decoder is still
53   // running.  If it is, it will block on dequeue again.  Otherwise, it will stop and release the
54   // MediaCodec.
55   private static final int DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US = 100000;
56 
57   private final MediaCodecWrapperFactory mediaCodecWrapperFactory;
58   private final String codecName;
59   private final VideoCodecMimeType codecType;
60 
61   private static class FrameInfo {
62     final long decodeStartTimeMs;
63     final int rotation;
64 
FrameInfo(long decodeStartTimeMs, int rotation)65     FrameInfo(long decodeStartTimeMs, int rotation) {
66       this.decodeStartTimeMs = decodeStartTimeMs;
67       this.rotation = rotation;
68     }
69   }
70 
71   private final BlockingDeque<FrameInfo> frameInfos;
72   private int colorFormat;
73 
74   // Output thread runs a loop which polls MediaCodec for decoded output buffers.  It reformats
75   // those buffers into VideoFrames and delivers them to the callback.  Variable is set on decoder
76   // thread and is immutable while the codec is running.
77   @Nullable private Thread outputThread;
78 
79   // Checker that ensures work is run on the output thread.
80   private ThreadChecker outputThreadChecker;
81 
82   // Checker that ensures work is run on the decoder thread.  The decoder thread is owned by the
83   // caller and must be used to call initDecode, decode, and release.
84   private ThreadChecker decoderThreadChecker;
85 
86   private volatile boolean running;
87   @Nullable private volatile Exception shutdownException;
88 
89   // Dimensions (width, height, stride, and sliceHeight) may be accessed by either the decode thread
90   // or the output thread.  Accesses should be protected with this lock.
91   private final Object dimensionLock = new Object();
92   private int width;
93   private int height;
94   private int stride;
95   private int sliceHeight;
96 
97   // Whether the decoder has finished the first frame.  The codec may not change output dimensions
98   // after delivering the first frame.  Only accessed on the output thread while the decoder is
99   // running.
100   private boolean hasDecodedFirstFrame;
101   // Whether the decoder has seen a key frame.  The first frame must be a key frame.  Only accessed
102   // on the decoder thread.
103   private boolean keyFrameRequired;
104 
105   private final @Nullable EglBase.Context sharedContext;
106   // Valid and immutable while the decoder is running.
107   @Nullable private SurfaceTextureHelper surfaceTextureHelper;
108   @Nullable private Surface surface;
109 
110   private static class DecodedTextureMetadata {
111     final long presentationTimestampUs;
112     final Integer decodeTimeMs;
113 
DecodedTextureMetadata(long presentationTimestampUs, Integer decodeTimeMs)114     DecodedTextureMetadata(long presentationTimestampUs, Integer decodeTimeMs) {
115       this.presentationTimestampUs = presentationTimestampUs;
116       this.decodeTimeMs = decodeTimeMs;
117     }
118   }
119 
120   // Metadata for the last frame rendered to the texture.
121   private final Object renderedTextureMetadataLock = new Object();
122   @Nullable private DecodedTextureMetadata renderedTextureMetadata;
123 
124   // Decoding proceeds asynchronously.  This callback returns decoded frames to the caller.  Valid
125   // and immutable while the decoder is running.
126   @Nullable private Callback callback;
127 
128   // Valid and immutable while the decoder is running.
129   @Nullable private MediaCodecWrapper codec;
130 
AndroidVideoDecoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName, VideoCodecMimeType codecType, int colorFormat, @Nullable EglBase.Context sharedContext)131   AndroidVideoDecoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName,
132       VideoCodecMimeType codecType, int colorFormat, @Nullable EglBase.Context sharedContext) {
133     if (!isSupportedColorFormat(colorFormat)) {
134       throw new IllegalArgumentException("Unsupported color format: " + colorFormat);
135     }
136     Logging.d(TAG,
137         "ctor name: " + codecName + " type: " + codecType + " color format: " + colorFormat
138             + " context: " + sharedContext);
139     this.mediaCodecWrapperFactory = mediaCodecWrapperFactory;
140     this.codecName = codecName;
141     this.codecType = codecType;
142     this.colorFormat = colorFormat;
143     this.sharedContext = sharedContext;
144     this.frameInfos = new LinkedBlockingDeque<>();
145   }
146 
147   @Override
initDecode(Settings settings, Callback callback)148   public VideoCodecStatus initDecode(Settings settings, Callback callback) {
149     this.decoderThreadChecker = new ThreadChecker();
150 
151     this.callback = callback;
152     if (sharedContext != null) {
153       surfaceTextureHelper = createSurfaceTextureHelper();
154       surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
155       surfaceTextureHelper.startListening(this);
156     }
157     return initDecodeInternal(settings.width, settings.height);
158   }
159 
160   // Internal variant is used when restarting the codec due to reconfiguration.
initDecodeInternal(int width, int height)161   private VideoCodecStatus initDecodeInternal(int width, int height) {
162     decoderThreadChecker.checkIsOnValidThread();
163     Logging.d(TAG,
164         "initDecodeInternal name: " + codecName + " type: " + codecType + " width: " + width
165             + " height: " + height);
166     if (outputThread != null) {
167       Logging.e(TAG, "initDecodeInternal called while the codec is already running");
168       return VideoCodecStatus.FALLBACK_SOFTWARE;
169     }
170 
171     // Note:  it is not necessary to initialize dimensions under the lock, since the output thread
172     // is not running.
173     this.width = width;
174     this.height = height;
175 
176     stride = width;
177     sliceHeight = height;
178     hasDecodedFirstFrame = false;
179     keyFrameRequired = true;
180 
181     try {
182       codec = mediaCodecWrapperFactory.createByCodecName(codecName);
183     } catch (IOException | IllegalArgumentException | IllegalStateException e) {
184       Logging.e(TAG, "Cannot create media decoder " + codecName);
185       return VideoCodecStatus.FALLBACK_SOFTWARE;
186     }
187     try {
188       MediaFormat format = MediaFormat.createVideoFormat(codecType.mimeType(), width, height);
189       if (sharedContext == null) {
190         format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
191       }
192       codec.configure(format, surface, null, 0);
193       codec.start();
194     } catch (IllegalStateException | IllegalArgumentException e) {
195       Logging.e(TAG, "initDecode failed", e);
196       release();
197       return VideoCodecStatus.FALLBACK_SOFTWARE;
198     }
199     running = true;
200     outputThread = createOutputThread();
201     outputThread.start();
202 
203     Logging.d(TAG, "initDecodeInternal done");
204     return VideoCodecStatus.OK;
205   }
206 
207   @Override
decode(EncodedImage frame, DecodeInfo info)208   public VideoCodecStatus decode(EncodedImage frame, DecodeInfo info) {
209     decoderThreadChecker.checkIsOnValidThread();
210     if (codec == null || callback == null) {
211       Logging.d(TAG, "decode uninitalized, codec: " + (codec != null) + ", callback: " + callback);
212       return VideoCodecStatus.UNINITIALIZED;
213     }
214 
215     if (frame.buffer == null) {
216       Logging.e(TAG, "decode() - no input data");
217       return VideoCodecStatus.ERR_PARAMETER;
218     }
219 
220     int size = frame.buffer.remaining();
221     if (size == 0) {
222       Logging.e(TAG, "decode() - input buffer empty");
223       return VideoCodecStatus.ERR_PARAMETER;
224     }
225 
226     // Load dimensions from shared memory under the dimension lock.
227     final int width;
228     final int height;
229     synchronized (dimensionLock) {
230       width = this.width;
231       height = this.height;
232     }
233 
234     // Check if the resolution changed and reset the codec if necessary.
235     if (frame.encodedWidth * frame.encodedHeight > 0
236         && (frame.encodedWidth != width || frame.encodedHeight != height)) {
237       VideoCodecStatus status = reinitDecode(frame.encodedWidth, frame.encodedHeight);
238       if (status != VideoCodecStatus.OK) {
239         return status;
240       }
241     }
242 
243     if (keyFrameRequired) {
244       // Need to process a key frame first.
245       if (frame.frameType != EncodedImage.FrameType.VideoFrameKey) {
246         Logging.e(TAG, "decode() - key frame required first");
247         return VideoCodecStatus.NO_OUTPUT;
248       }
249     }
250 
251     int index;
252     try {
253       index = codec.dequeueInputBuffer(DEQUEUE_INPUT_TIMEOUT_US);
254     } catch (IllegalStateException e) {
255       Logging.e(TAG, "dequeueInputBuffer failed", e);
256       return VideoCodecStatus.ERROR;
257     }
258     if (index < 0) {
259       // Decoder is falling behind.  No input buffers available.
260       // The decoder can't simply drop frames; it might lose a key frame.
261       Logging.e(TAG, "decode() - no HW buffers available; decoder falling behind");
262       return VideoCodecStatus.ERROR;
263     }
264 
265     ByteBuffer buffer;
266     try {
267       buffer = codec.getInputBuffer(index);
268     } catch (IllegalStateException e) {
269       Logging.e(TAG, "getInputBuffer with index=" + index + " failed", e);
270       return VideoCodecStatus.ERROR;
271     }
272 
273     if (buffer.capacity() < size) {
274       Logging.e(TAG, "decode() - HW buffer too small");
275       return VideoCodecStatus.ERROR;
276     }
277     buffer.put(frame.buffer);
278 
279     frameInfos.offer(new FrameInfo(SystemClock.elapsedRealtime(), frame.rotation));
280     try {
281       codec.queueInputBuffer(index, 0 /* offset */, size,
282           TimeUnit.NANOSECONDS.toMicros(frame.captureTimeNs), 0 /* flags */);
283     } catch (IllegalStateException e) {
284       Logging.e(TAG, "queueInputBuffer failed", e);
285       frameInfos.pollLast();
286       return VideoCodecStatus.ERROR;
287     }
288     if (keyFrameRequired) {
289       keyFrameRequired = false;
290     }
291     return VideoCodecStatus.OK;
292   }
293 
294   @Override
getImplementationName()295   public String getImplementationName() {
296     return codecName;
297   }
298 
299   @Override
release()300   public VideoCodecStatus release() {
301     // TODO(sakal): This is not called on the correct thread but is still called synchronously.
302     // Re-enable the check once this is called on the correct thread.
303     // decoderThreadChecker.checkIsOnValidThread();
304     Logging.d(TAG, "release");
305     VideoCodecStatus status = releaseInternal();
306     if (surface != null) {
307       releaseSurface();
308       surface = null;
309       surfaceTextureHelper.stopListening();
310       surfaceTextureHelper.dispose();
311       surfaceTextureHelper = null;
312     }
313     synchronized (renderedTextureMetadataLock) {
314       renderedTextureMetadata = null;
315     }
316     callback = null;
317     frameInfos.clear();
318     return status;
319   }
320 
321   // Internal variant is used when restarting the codec due to reconfiguration.
releaseInternal()322   private VideoCodecStatus releaseInternal() {
323     if (!running) {
324       Logging.d(TAG, "release: Decoder is not running.");
325       return VideoCodecStatus.OK;
326     }
327     try {
328       // The outputThread actually stops and releases the codec once running is false.
329       running = false;
330       if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) {
331         // Log an exception to capture the stack trace and turn it into a TIMEOUT error.
332         Logging.e(TAG, "Media decoder release timeout", new RuntimeException());
333         return VideoCodecStatus.TIMEOUT;
334       }
335       if (shutdownException != null) {
336         // Log the exception and turn it into an error.  Wrap the exception in a new exception to
337         // capture both the output thread's stack trace and this thread's stack trace.
338         Logging.e(TAG, "Media decoder release error", new RuntimeException(shutdownException));
339         shutdownException = null;
340         return VideoCodecStatus.ERROR;
341       }
342     } finally {
343       codec = null;
344       outputThread = null;
345     }
346     return VideoCodecStatus.OK;
347   }
348 
reinitDecode(int newWidth, int newHeight)349   private VideoCodecStatus reinitDecode(int newWidth, int newHeight) {
350     decoderThreadChecker.checkIsOnValidThread();
351     VideoCodecStatus status = releaseInternal();
352     if (status != VideoCodecStatus.OK) {
353       return status;
354     }
355     return initDecodeInternal(newWidth, newHeight);
356   }
357 
createOutputThread()358   private Thread createOutputThread() {
359     return new Thread("AndroidVideoDecoder.outputThread") {
360       @Override
361       public void run() {
362         outputThreadChecker = new ThreadChecker();
363         while (running) {
364           deliverDecodedFrame();
365         }
366         releaseCodecOnOutputThread();
367       }
368     };
369   }
370 
371   // Visible for testing.
372   protected void deliverDecodedFrame() {
373     outputThreadChecker.checkIsOnValidThread();
374     try {
375       MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
376       // Block until an output buffer is available (up to 100 milliseconds).  If the timeout is
377       // exceeded, deliverDecodedFrame() will be called again on the next iteration of the output
378       // thread's loop.  Blocking here prevents the output thread from busy-waiting while the codec
379       // is idle.
380       int index = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US);
381       if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
382         reformat(codec.getOutputFormat());
383         return;
384       }
385 
386       if (index < 0) {
387         Logging.v(TAG, "dequeueOutputBuffer returned " + index);
388         return;
389       }
390 
391       FrameInfo frameInfo = frameInfos.poll();
392       Integer decodeTimeMs = null;
393       int rotation = 0;
394       if (frameInfo != null) {
395         decodeTimeMs = (int) (SystemClock.elapsedRealtime() - frameInfo.decodeStartTimeMs);
396         rotation = frameInfo.rotation;
397       }
398 
399       hasDecodedFirstFrame = true;
400 
401       if (surfaceTextureHelper != null) {
402         deliverTextureFrame(index, info, rotation, decodeTimeMs);
403       } else {
404         deliverByteFrame(index, info, rotation, decodeTimeMs);
405       }
406 
407     } catch (IllegalStateException e) {
408       Logging.e(TAG, "deliverDecodedFrame failed", e);
409     }
410   }
411 
412   private void deliverTextureFrame(final int index, final MediaCodec.BufferInfo info,
413       final int rotation, final Integer decodeTimeMs) {
414     // Load dimensions from shared memory under the dimension lock.
415     final int width;
416     final int height;
417     synchronized (dimensionLock) {
418       width = this.width;
419       height = this.height;
420     }
421 
422     synchronized (renderedTextureMetadataLock) {
423       if (renderedTextureMetadata != null) {
424         codec.releaseOutputBuffer(index, false);
425         return; // We are still waiting for texture for the previous frame, drop this one.
426       }
427       surfaceTextureHelper.setTextureSize(width, height);
428       surfaceTextureHelper.setFrameRotation(rotation);
429       renderedTextureMetadata = new DecodedTextureMetadata(info.presentationTimeUs, decodeTimeMs);
430       codec.releaseOutputBuffer(index, /* render= */ true);
431     }
432   }
433 
434   @Override
435   public void onFrame(VideoFrame frame) {
436     final VideoFrame newFrame;
437     final Integer decodeTimeMs;
438     final long timestampNs;
439     synchronized (renderedTextureMetadataLock) {
440       if (renderedTextureMetadata == null) {
441         throw new IllegalStateException(
442             "Rendered texture metadata was null in onTextureFrameAvailable.");
443       }
444       timestampNs = renderedTextureMetadata.presentationTimestampUs * 1000;
445       decodeTimeMs = renderedTextureMetadata.decodeTimeMs;
446       renderedTextureMetadata = null;
447     }
448     // Change timestamp of frame.
449     final VideoFrame frameWithModifiedTimeStamp =
450         new VideoFrame(frame.getBuffer(), frame.getRotation(), timestampNs);
451     callback.onDecodedFrame(frameWithModifiedTimeStamp, decodeTimeMs, null /* qp */);
452   }
453 
454   private void deliverByteFrame(
455       int index, MediaCodec.BufferInfo info, int rotation, Integer decodeTimeMs) {
456     // Load dimensions from shared memory under the dimension lock.
457     int width;
458     int height;
459     int stride;
460     int sliceHeight;
461     synchronized (dimensionLock) {
462       width = this.width;
463       height = this.height;
464       stride = this.stride;
465       sliceHeight = this.sliceHeight;
466     }
467 
468     // Output must be at least width * height bytes for Y channel, plus (width / 2) * (height / 2)
469     // bytes for each of the U and V channels.
470     if (info.size < width * height * 3 / 2) {
471       Logging.e(TAG, "Insufficient output buffer size: " + info.size);
472       return;
473     }
474 
475     if (info.size < stride * height * 3 / 2 && sliceHeight == height && stride > width) {
476       // Some codecs (Exynos) report an incorrect stride.  Correct it here.
477       // Expected size == stride * height * 3 / 2.  A bit of algebra gives the correct stride as
478       // 2 * size / (3 * height).
479       stride = info.size * 2 / (height * 3);
480     }
481 
482     ByteBuffer buffer = codec.getOutputBuffer(index);
483     buffer.position(info.offset);
484     buffer.limit(info.offset + info.size);
485     buffer = buffer.slice();
486 
487     final VideoFrame.Buffer frameBuffer;
488     if (colorFormat == CodecCapabilities.COLOR_FormatYUV420Planar) {
489       frameBuffer = copyI420Buffer(buffer, stride, sliceHeight, width, height);
490     } else {
491       // All other supported color formats are NV12.
492       frameBuffer = copyNV12ToI420Buffer(buffer, stride, sliceHeight, width, height);
493     }
494     codec.releaseOutputBuffer(index, /* render= */ false);
495 
496     long presentationTimeNs = info.presentationTimeUs * 1000;
497     VideoFrame frame = new VideoFrame(frameBuffer, rotation, presentationTimeNs);
498 
499     // Note that qp is parsed on the C++ side.
500     callback.onDecodedFrame(frame, decodeTimeMs, null /* qp */);
501     frame.release();
502   }
503 
504   private VideoFrame.Buffer copyNV12ToI420Buffer(
505       ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
506     // toI420 copies the buffer.
507     return new NV12Buffer(width, height, stride, sliceHeight, buffer, null /* releaseCallback */)
508         .toI420();
509   }
510 
511   private VideoFrame.Buffer copyI420Buffer(
512       ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
513     if (stride % 2 != 0) {
514       throw new AssertionError("Stride is not divisible by two: " + stride);
515     }
516 
517     // Note that the case with odd `sliceHeight` is handled in a special way.
518     // The chroma height contained in the payload is rounded down instead of
519     // up, making it one row less than what we expect in WebRTC. Therefore, we
520     // have to duplicate the last chroma rows for this case. Also, the offset
521     // between the Y plane and the U plane is unintuitive for this case. See
522     // http://bugs.webrtc.org/6651 for more info.
523     final int chromaWidth = (width + 1) / 2;
524     final int chromaHeight = (sliceHeight % 2 == 0) ? (height + 1) / 2 : height / 2;
525 
526     final int uvStride = stride / 2;
527 
528     final int yPos = 0;
529     final int yEnd = yPos + stride * height;
530     final int uPos = yPos + stride * sliceHeight;
531     final int uEnd = uPos + uvStride * chromaHeight;
532     final int vPos = uPos + uvStride * sliceHeight / 2;
533     final int vEnd = vPos + uvStride * chromaHeight;
534 
535     VideoFrame.I420Buffer frameBuffer = allocateI420Buffer(width, height);
536 
537     buffer.limit(yEnd);
538     buffer.position(yPos);
539     copyPlane(
540         buffer.slice(), stride, frameBuffer.getDataY(), frameBuffer.getStrideY(), width, height);
541 
542     buffer.limit(uEnd);
543     buffer.position(uPos);
544     copyPlane(buffer.slice(), uvStride, frameBuffer.getDataU(), frameBuffer.getStrideU(),
545         chromaWidth, chromaHeight);
546     if (sliceHeight % 2 == 1) {
547       buffer.position(uPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.
548 
549       ByteBuffer dataU = frameBuffer.getDataU();
550       dataU.position(frameBuffer.getStrideU() * chromaHeight); // Seek to beginning of last row.
551       dataU.put(buffer); // Copy the last row.
552     }
553 
554     buffer.limit(vEnd);
555     buffer.position(vPos);
556     copyPlane(buffer.slice(), uvStride, frameBuffer.getDataV(), frameBuffer.getStrideV(),
557         chromaWidth, chromaHeight);
558     if (sliceHeight % 2 == 1) {
559       buffer.position(vPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.
560 
561       ByteBuffer dataV = frameBuffer.getDataV();
562       dataV.position(frameBuffer.getStrideV() * chromaHeight); // Seek to beginning of last row.
563       dataV.put(buffer); // Copy the last row.
564     }
565 
566     return frameBuffer;
567   }
568 
569   private void reformat(MediaFormat format) {
570     outputThreadChecker.checkIsOnValidThread();
571     Logging.d(TAG, "Decoder format changed: " + format.toString());
572     final int newWidth;
573     final int newHeight;
574     if (format.containsKey(MEDIA_FORMAT_KEY_CROP_LEFT)
575         && format.containsKey(MEDIA_FORMAT_KEY_CROP_RIGHT)
576         && format.containsKey(MEDIA_FORMAT_KEY_CROP_BOTTOM)
577         && format.containsKey(MEDIA_FORMAT_KEY_CROP_TOP)) {
578       newWidth = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_RIGHT)
579           - format.getInteger(MEDIA_FORMAT_KEY_CROP_LEFT);
580       newHeight = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_BOTTOM)
581           - format.getInteger(MEDIA_FORMAT_KEY_CROP_TOP);
582     } else {
583       newWidth = format.getInteger(MediaFormat.KEY_WIDTH);
584       newHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
585     }
586     // Compare to existing width, height, and save values under the dimension lock.
587     synchronized (dimensionLock) {
588       if (newWidth != width || newHeight != height) {
589         if (hasDecodedFirstFrame) {
590           stopOnOutputThread(new RuntimeException("Unexpected size change. "
591               + "Configured " + width + "*" + height + ". "
592               + "New " + newWidth + "*" + newHeight));
593           return;
594         } else if (newWidth <= 0 || newHeight <= 0) {
595           Logging.w(TAG,
596               "Unexpected format dimensions. Configured " + width + "*" + height + ". "
597                   + "New " + newWidth + "*" + newHeight + ". Skip it");
598           return;
599         }
600         width = newWidth;
601         height = newHeight;
602       }
603     }
604 
605     // Note:  texture mode ignores colorFormat.  Hence, if the texture helper is non-null, skip
606     // color format updates.
607     if (surfaceTextureHelper == null && format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
608       colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
609       Logging.d(TAG, "Color: 0x" + Integer.toHexString(colorFormat));
610       if (!isSupportedColorFormat(colorFormat)) {
611         stopOnOutputThread(new IllegalStateException("Unsupported color format: " + colorFormat));
612         return;
613       }
614     }
615 
616     // Save stride and sliceHeight under the dimension lock.
617     synchronized (dimensionLock) {
618       if (format.containsKey(MEDIA_FORMAT_KEY_STRIDE)) {
619         stride = format.getInteger(MEDIA_FORMAT_KEY_STRIDE);
620       }
621       if (format.containsKey(MEDIA_FORMAT_KEY_SLICE_HEIGHT)) {
622         sliceHeight = format.getInteger(MEDIA_FORMAT_KEY_SLICE_HEIGHT);
623       }
624       Logging.d(TAG, "Frame stride and slice height: " + stride + " x " + sliceHeight);
625       stride = Math.max(width, stride);
626       sliceHeight = Math.max(height, sliceHeight);
627     }
628   }
629 
630   private void releaseCodecOnOutputThread() {
631     outputThreadChecker.checkIsOnValidThread();
632     Logging.d(TAG, "Releasing MediaCodec on output thread");
633     try {
634       codec.stop();
635     } catch (Exception e) {
636       Logging.e(TAG, "Media decoder stop failed", e);
637     }
638     try {
639       codec.release();
640     } catch (Exception e) {
641       Logging.e(TAG, "Media decoder release failed", e);
642       // Propagate exceptions caught during release back to the main thread.
643       shutdownException = e;
644     }
645     Logging.d(TAG, "Release on output thread done");
646   }
647 
648   private void stopOnOutputThread(Exception e) {
649     outputThreadChecker.checkIsOnValidThread();
650     running = false;
651     shutdownException = e;
652   }
653 
654   private boolean isSupportedColorFormat(int colorFormat) {
655     for (int supported : MediaCodecUtils.DECODER_COLOR_FORMATS) {
656       if (supported == colorFormat) {
657         return true;
658       }
659     }
660     return false;
661   }
662 
663   // Visible for testing.
664   protected SurfaceTextureHelper createSurfaceTextureHelper() {
665     return SurfaceTextureHelper.create("decoder-texture-thread", sharedContext);
666   }
667 
668   // Visible for testing.
669   // TODO(sakal): Remove once Robolectric commit fa991a0 has been rolled to WebRTC.
670   protected void releaseSurface() {
671     surface.release();
672   }
673 
674   // Visible for testing.
675   protected VideoFrame.I420Buffer allocateI420Buffer(int width, int height) {
676     return JavaI420Buffer.allocate(width, height);
677   }
678 
679   // Visible for testing.
680   protected void copyPlane(
681       ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height) {
682     YuvHelper.copyPlane(src, srcStride, dst, dstStride, width, height);
683   }
684 }
685