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