mirror of https://github.com/NekoX-Dev/NekoX.git
695 lines
26 KiB
Java
695 lines
26 KiB
Java
/*
|
|
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
package org.webrtc;
|
|
|
|
import android.media.MediaCodec;
|
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
|
import android.media.MediaFormat;
|
|
import android.os.SystemClock;
|
|
import androidx.annotation.Nullable;
|
|
import android.view.Surface;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.concurrent.BlockingDeque;
|
|
import java.util.concurrent.LinkedBlockingDeque;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import org.telegram.messenger.FileLog;
|
|
import org.webrtc.ThreadUtils.ThreadChecker;
|
|
|
|
/**
|
|
* Android hardware video decoder.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
// Cannot support API 16 without using deprecated methods.
|
|
// TODO(sakal): Rename to MediaCodecVideoDecoder once the deprecated implementation is removed.
|
|
class AndroidVideoDecoder implements VideoDecoder, VideoSink {
|
|
private static final String TAG = "AndroidVideoDecoder";
|
|
|
|
// TODO(magjed): Use MediaFormat.KEY_* constants when part of the public API.
|
|
private static final String MEDIA_FORMAT_KEY_STRIDE = "stride";
|
|
private static final String MEDIA_FORMAT_KEY_SLICE_HEIGHT = "slice-height";
|
|
private static final String MEDIA_FORMAT_KEY_CROP_LEFT = "crop-left";
|
|
private static final String MEDIA_FORMAT_KEY_CROP_RIGHT = "crop-right";
|
|
private static final String MEDIA_FORMAT_KEY_CROP_TOP = "crop-top";
|
|
private static final String MEDIA_FORMAT_KEY_CROP_BOTTOM = "crop-bottom";
|
|
|
|
// MediaCodec.release() occasionally hangs. Release stops waiting and reports failure after
|
|
// this timeout.
|
|
private static final int MEDIA_CODEC_RELEASE_TIMEOUT_MS = 5000;
|
|
|
|
// WebRTC queues input frames quickly in the beginning on the call. Wait for input buffers with a
|
|
// long timeout (500 ms) to prevent this from causing the codec to return an error.
|
|
private static final int DEQUEUE_INPUT_TIMEOUT_US = 500000;
|
|
|
|
// Dequeuing an output buffer will block until a buffer is available (up to 100 milliseconds).
|
|
// If this timeout is exceeded, the output thread will unblock and check if the decoder is still
|
|
// running. If it is, it will block on dequeue again. Otherwise, it will stop and release the
|
|
// MediaCodec.
|
|
private static final int DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US = 100000;
|
|
|
|
private final MediaCodecWrapperFactory mediaCodecWrapperFactory;
|
|
private final String codecName;
|
|
private final VideoCodecMimeType codecType;
|
|
|
|
private static class FrameInfo {
|
|
final long decodeStartTimeMs;
|
|
final int rotation;
|
|
|
|
FrameInfo(long decodeStartTimeMs, int rotation) {
|
|
this.decodeStartTimeMs = decodeStartTimeMs;
|
|
this.rotation = rotation;
|
|
}
|
|
}
|
|
|
|
private final BlockingDeque<FrameInfo> frameInfos;
|
|
private int colorFormat;
|
|
|
|
// Output thread runs a loop which polls MediaCodec for decoded output buffers. It reformats
|
|
// those buffers into VideoFrames and delivers them to the callback. Variable is set on decoder
|
|
// thread and is immutable while the codec is running.
|
|
@Nullable private Thread outputThread;
|
|
|
|
// Checker that ensures work is run on the output thread.
|
|
private ThreadChecker outputThreadChecker;
|
|
|
|
// Checker that ensures work is run on the decoder thread. The decoder thread is owned by the
|
|
// caller and must be used to call initDecode, decode, and release.
|
|
private ThreadChecker decoderThreadChecker;
|
|
|
|
private volatile boolean running;
|
|
@Nullable private volatile Exception shutdownException;
|
|
|
|
// Dimensions (width, height, stride, and sliceHeight) may be accessed by either the decode thread
|
|
// or the output thread. Accesses should be protected with this lock.
|
|
private final Object dimensionLock = new Object();
|
|
private int width;
|
|
private int height;
|
|
private int stride;
|
|
private int sliceHeight;
|
|
|
|
// Whether the decoder has finished the first frame. The codec may not change output dimensions
|
|
// after delivering the first frame. Only accessed on the output thread while the decoder is
|
|
// running.
|
|
private boolean hasDecodedFirstFrame;
|
|
// Whether the decoder has seen a key frame. The first frame must be a key frame. Only accessed
|
|
// on the decoder thread.
|
|
private boolean keyFrameRequired;
|
|
|
|
private final @Nullable EglBase.Context sharedContext;
|
|
// Valid and immutable while the decoder is running.
|
|
@Nullable private SurfaceTextureHelper surfaceTextureHelper;
|
|
@Nullable private Surface surface;
|
|
|
|
private static class DecodedTextureMetadata {
|
|
final long presentationTimestampUs;
|
|
final Integer decodeTimeMs;
|
|
|
|
DecodedTextureMetadata(long presentationTimestampUs, Integer decodeTimeMs) {
|
|
this.presentationTimestampUs = presentationTimestampUs;
|
|
this.decodeTimeMs = decodeTimeMs;
|
|
}
|
|
}
|
|
|
|
// Metadata for the last frame rendered to the texture.
|
|
private final Object renderedTextureMetadataLock = new Object();
|
|
@Nullable private DecodedTextureMetadata renderedTextureMetadata;
|
|
|
|
// Decoding proceeds asynchronously. This callback returns decoded frames to the caller. Valid
|
|
// and immutable while the decoder is running.
|
|
@Nullable private Callback callback;
|
|
|
|
// Valid and immutable while the decoder is running.
|
|
@Nullable private MediaCodecWrapper codec;
|
|
|
|
AndroidVideoDecoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName,
|
|
VideoCodecMimeType codecType, int colorFormat, @Nullable EglBase.Context sharedContext) {
|
|
if (!isSupportedColorFormat(colorFormat)) {
|
|
throw new IllegalArgumentException("Unsupported color format: " + colorFormat);
|
|
}
|
|
Logging.d(TAG,
|
|
"ctor name: " + codecName + " type: " + codecType + " color format: " + colorFormat
|
|
+ " context: " + sharedContext);
|
|
this.mediaCodecWrapperFactory = mediaCodecWrapperFactory;
|
|
this.codecName = codecName;
|
|
this.codecType = codecType;
|
|
this.colorFormat = colorFormat;
|
|
this.sharedContext = sharedContext;
|
|
this.frameInfos = new LinkedBlockingDeque<>();
|
|
}
|
|
|
|
@Override
|
|
public VideoCodecStatus initDecode(Settings settings, Callback callback) {
|
|
this.decoderThreadChecker = new ThreadChecker();
|
|
|
|
this.callback = callback;
|
|
if (sharedContext != null) {
|
|
surfaceTextureHelper = createSurfaceTextureHelper();
|
|
surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
|
|
surfaceTextureHelper.startListening(this);
|
|
}
|
|
return initDecodeInternal(settings.width, settings.height);
|
|
}
|
|
|
|
// Internal variant is used when restarting the codec due to reconfiguration.
|
|
private VideoCodecStatus initDecodeInternal(int width, int height) {
|
|
decoderThreadChecker.checkIsOnValidThread();
|
|
Logging.d(TAG,
|
|
"initDecodeInternal name: " + codecName + " type: " + codecType + " width: " + width
|
|
+ " height: " + height);
|
|
if (outputThread != null) {
|
|
Logging.e(TAG, "initDecodeInternal called while the codec is already running");
|
|
return VideoCodecStatus.FALLBACK_SOFTWARE;
|
|
}
|
|
|
|
// Note: it is not necessary to initialize dimensions under the lock, since the output thread
|
|
// is not running.
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
stride = width;
|
|
sliceHeight = height;
|
|
hasDecodedFirstFrame = false;
|
|
keyFrameRequired = true;
|
|
|
|
try {
|
|
codec = mediaCodecWrapperFactory.createByCodecName(codecName);
|
|
} catch (IOException | IllegalArgumentException | IllegalStateException e) {
|
|
Logging.e(TAG, "Cannot create media decoder " + codecName);
|
|
return VideoCodecStatus.FALLBACK_SOFTWARE;
|
|
}
|
|
try {
|
|
MediaFormat format = MediaFormat.createVideoFormat(codecType.mimeType(), width, height);
|
|
if (sharedContext == null) {
|
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
|
|
}
|
|
codec.configure(format, surface, null, 0);
|
|
codec.start();
|
|
} catch (IllegalStateException | IllegalArgumentException e) {
|
|
Logging.e(TAG, "initDecode failed", e);
|
|
release();
|
|
return VideoCodecStatus.FALLBACK_SOFTWARE;
|
|
}
|
|
running = true;
|
|
outputThread = createOutputThread();
|
|
outputThread.start();
|
|
|
|
Logging.d(TAG, "initDecodeInternal done");
|
|
return VideoCodecStatus.OK;
|
|
}
|
|
|
|
@Override
|
|
public VideoCodecStatus decode(EncodedImage frame, DecodeInfo info) {
|
|
decoderThreadChecker.checkIsOnValidThread();
|
|
if (codec == null || callback == null) {
|
|
Logging.d(TAG, "decode uninitalized, codec: " + (codec != null) + ", callback: " + callback);
|
|
return VideoCodecStatus.UNINITIALIZED;
|
|
}
|
|
|
|
if (frame.buffer == null) {
|
|
Logging.e(TAG, "decode() - no input data");
|
|
return VideoCodecStatus.ERR_PARAMETER;
|
|
}
|
|
|
|
int size = frame.buffer.remaining();
|
|
if (size == 0) {
|
|
Logging.e(TAG, "decode() - input buffer empty");
|
|
return VideoCodecStatus.ERR_PARAMETER;
|
|
}
|
|
|
|
// Load dimensions from shared memory under the dimension lock.
|
|
final int width;
|
|
final int height;
|
|
synchronized (dimensionLock) {
|
|
width = this.width;
|
|
height = this.height;
|
|
}
|
|
|
|
// Check if the resolution changed and reset the codec if necessary.
|
|
if (frame.encodedWidth * frame.encodedHeight > 0
|
|
&& (frame.encodedWidth != width || frame.encodedHeight != height)) {
|
|
VideoCodecStatus status = reinitDecode(frame.encodedWidth, frame.encodedHeight);
|
|
if (status != VideoCodecStatus.OK) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (keyFrameRequired) {
|
|
// Need to process a key frame first.
|
|
if (frame.frameType != EncodedImage.FrameType.VideoFrameKey) {
|
|
Logging.e(TAG, "decode() - key frame required first");
|
|
return VideoCodecStatus.NO_OUTPUT;
|
|
}
|
|
}
|
|
|
|
int index;
|
|
try {
|
|
index = codec.dequeueInputBuffer(DEQUEUE_INPUT_TIMEOUT_US);
|
|
} catch (IllegalStateException e) {
|
|
Logging.e(TAG, "dequeueInputBuffer failed", e);
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
if (index < 0) {
|
|
// Decoder is falling behind. No input buffers available.
|
|
// The decoder can't simply drop frames; it might lose a key frame.
|
|
Logging.e(TAG, "decode() - no HW buffers available; decoder falling behind");
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
|
|
ByteBuffer buffer;
|
|
try {
|
|
buffer = codec.getInputBuffers()[index];
|
|
} catch (IllegalStateException e) {
|
|
Logging.e(TAG, "getInputBuffers failed", e);
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
|
|
if (buffer.capacity() < size) {
|
|
Logging.e(TAG, "decode() - HW buffer too small");
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
buffer.put(frame.buffer);
|
|
|
|
frameInfos.offer(new FrameInfo(SystemClock.elapsedRealtime(), frame.rotation));
|
|
try {
|
|
codec.queueInputBuffer(index, 0 /* offset */, size,
|
|
TimeUnit.NANOSECONDS.toMicros(frame.captureTimeNs), 0 /* flags */);
|
|
} catch (IllegalStateException e) {
|
|
Logging.e(TAG, "queueInputBuffer failed", e);
|
|
frameInfos.pollLast();
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
if (keyFrameRequired) {
|
|
keyFrameRequired = false;
|
|
}
|
|
return VideoCodecStatus.OK;
|
|
}
|
|
|
|
@Override
|
|
public boolean getPrefersLateDecoding() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public String getImplementationName() {
|
|
return codecName;
|
|
}
|
|
|
|
@Override
|
|
public VideoCodecStatus release() {
|
|
// TODO(sakal): This is not called on the correct thread but is still called synchronously.
|
|
// Re-enable the check once this is called on the correct thread.
|
|
// decoderThreadChecker.checkIsOnValidThread();
|
|
Logging.d(TAG, "release");
|
|
VideoCodecStatus status = releaseInternal();
|
|
if (surface != null) {
|
|
releaseSurface();
|
|
surface = null;
|
|
surfaceTextureHelper.stopListening();
|
|
surfaceTextureHelper.dispose();
|
|
surfaceTextureHelper = null;
|
|
}
|
|
synchronized (renderedTextureMetadataLock) {
|
|
renderedTextureMetadata = null;
|
|
}
|
|
callback = null;
|
|
frameInfos.clear();
|
|
return status;
|
|
}
|
|
|
|
// Internal variant is used when restarting the codec due to reconfiguration.
|
|
private VideoCodecStatus releaseInternal() {
|
|
if (!running) {
|
|
Logging.d(TAG, "release: Decoder is not running.");
|
|
return VideoCodecStatus.OK;
|
|
}
|
|
try {
|
|
// The outputThread actually stops and releases the codec once running is false.
|
|
running = false;
|
|
if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) {
|
|
// Log an exception to capture the stack trace and turn it into a TIMEOUT error.
|
|
Logging.e(TAG, "Media decoder release timeout", new RuntimeException());
|
|
return VideoCodecStatus.TIMEOUT;
|
|
}
|
|
if (shutdownException != null) {
|
|
// Log the exception and turn it into an error. Wrap the exception in a new exception to
|
|
// capture both the output thread's stack trace and this thread's stack trace.
|
|
Logging.e(TAG, "Media decoder release error", new RuntimeException(shutdownException));
|
|
shutdownException = null;
|
|
return VideoCodecStatus.ERROR;
|
|
}
|
|
} finally {
|
|
codec = null;
|
|
outputThread = null;
|
|
}
|
|
return VideoCodecStatus.OK;
|
|
}
|
|
|
|
private VideoCodecStatus reinitDecode(int newWidth, int newHeight) {
|
|
decoderThreadChecker.checkIsOnValidThread();
|
|
VideoCodecStatus status = releaseInternal();
|
|
if (status != VideoCodecStatus.OK) {
|
|
return status;
|
|
}
|
|
return initDecodeInternal(newWidth, newHeight);
|
|
}
|
|
|
|
private Thread createOutputThread() {
|
|
return new Thread("AndroidVideoDecoder.outputThread") {
|
|
@Override
|
|
public void run() {
|
|
outputThreadChecker = new ThreadChecker();
|
|
while (running) {
|
|
deliverDecodedFrame();
|
|
}
|
|
releaseCodecOnOutputThread();
|
|
}
|
|
};
|
|
}
|
|
|
|
// Visible for testing.
|
|
protected void deliverDecodedFrame() {
|
|
outputThreadChecker.checkIsOnValidThread();
|
|
try {
|
|
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
|
// Block until an output buffer is available (up to 100 milliseconds). If the timeout is
|
|
// exceeded, deliverDecodedFrame() will be called again on the next iteration of the output
|
|
// thread's loop. Blocking here prevents the output thread from busy-waiting while the codec
|
|
// is idle.
|
|
int result = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US);
|
|
if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
reformat(codec.getOutputFormat());
|
|
return;
|
|
}
|
|
|
|
if (result < 0) {
|
|
Logging.v(TAG, "dequeueOutputBuffer returned " + result);
|
|
return;
|
|
}
|
|
|
|
FrameInfo frameInfo = frameInfos.poll();
|
|
Integer decodeTimeMs = null;
|
|
int rotation = 0;
|
|
if (frameInfo != null) {
|
|
decodeTimeMs = (int) (SystemClock.elapsedRealtime() - frameInfo.decodeStartTimeMs);
|
|
rotation = frameInfo.rotation;
|
|
}
|
|
|
|
hasDecodedFirstFrame = true;
|
|
|
|
if (surfaceTextureHelper != null) {
|
|
deliverTextureFrame(result, info, rotation, decodeTimeMs);
|
|
} else {
|
|
deliverByteFrame(result, info, rotation, decodeTimeMs);
|
|
}
|
|
|
|
} catch (IllegalStateException e) {
|
|
Logging.e(TAG, "deliverDecodedFrame failed", e);
|
|
}
|
|
}
|
|
|
|
private void deliverTextureFrame(final int index, final MediaCodec.BufferInfo info,
|
|
final int rotation, final Integer decodeTimeMs) {
|
|
// Load dimensions from shared memory under the dimension lock.
|
|
final int width;
|
|
final int height;
|
|
synchronized (dimensionLock) {
|
|
width = this.width;
|
|
height = this.height;
|
|
}
|
|
|
|
synchronized (renderedTextureMetadataLock) {
|
|
if (renderedTextureMetadata != null) {
|
|
codec.releaseOutputBuffer(index, false);
|
|
return; // We are still waiting for texture for the previous frame, drop this one.
|
|
}
|
|
surfaceTextureHelper.setTextureSize(width, height);
|
|
surfaceTextureHelper.setFrameRotation(rotation);
|
|
renderedTextureMetadata = new DecodedTextureMetadata(info.presentationTimeUs, decodeTimeMs);
|
|
codec.releaseOutputBuffer(index, /* render= */ true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFrame(VideoFrame frame) {
|
|
final VideoFrame newFrame;
|
|
final Integer decodeTimeMs;
|
|
final long timestampNs;
|
|
synchronized (renderedTextureMetadataLock) {
|
|
if (renderedTextureMetadata == null) {
|
|
throw new IllegalStateException(
|
|
"Rendered texture metadata was null in onTextureFrameAvailable.");
|
|
}
|
|
timestampNs = renderedTextureMetadata.presentationTimestampUs * 1000;
|
|
decodeTimeMs = renderedTextureMetadata.decodeTimeMs;
|
|
renderedTextureMetadata = null;
|
|
}
|
|
// Change timestamp of frame.
|
|
final VideoFrame frameWithModifiedTimeStamp =
|
|
new VideoFrame(frame.getBuffer(), frame.getRotation(), timestampNs);
|
|
callback.onDecodedFrame(frameWithModifiedTimeStamp, decodeTimeMs, null /* qp */);
|
|
}
|
|
|
|
private void deliverByteFrame(
|
|
int result, MediaCodec.BufferInfo info, int rotation, Integer decodeTimeMs) {
|
|
// Load dimensions from shared memory under the dimension lock.
|
|
int width;
|
|
int height;
|
|
int stride;
|
|
int sliceHeight;
|
|
synchronized (dimensionLock) {
|
|
width = this.width;
|
|
height = this.height;
|
|
stride = this.stride;
|
|
sliceHeight = this.sliceHeight;
|
|
}
|
|
|
|
// Output must be at least width * height bytes for Y channel, plus (width / 2) * (height / 2)
|
|
// bytes for each of the U and V channels.
|
|
if (info.size < width * height * 3 / 2) {
|
|
Logging.e(TAG, "Insufficient output buffer size: " + info.size);
|
|
return;
|
|
}
|
|
|
|
if (info.size < stride * height * 3 / 2 && sliceHeight == height && stride > width) {
|
|
// Some codecs (Exynos) report an incorrect stride. Correct it here.
|
|
// Expected size == stride * height * 3 / 2. A bit of algebra gives the correct stride as
|
|
// 2 * size / (3 * height).
|
|
stride = info.size * 2 / (height * 3);
|
|
}
|
|
|
|
ByteBuffer buffer = codec.getOutputBuffers()[result];
|
|
buffer.position(info.offset);
|
|
buffer.limit(info.offset + info.size);
|
|
buffer = buffer.slice();
|
|
|
|
final VideoFrame.Buffer frameBuffer;
|
|
if (colorFormat == CodecCapabilities.COLOR_FormatYUV420Planar) {
|
|
frameBuffer = copyI420Buffer(buffer, stride, sliceHeight, width, height);
|
|
} else {
|
|
// All other supported color formats are NV12.
|
|
frameBuffer = copyNV12ToI420Buffer(buffer, stride, sliceHeight, width, height);
|
|
}
|
|
codec.releaseOutputBuffer(result, /* render= */ false);
|
|
|
|
long presentationTimeNs = info.presentationTimeUs * 1000;
|
|
VideoFrame frame = new VideoFrame(frameBuffer, rotation, presentationTimeNs);
|
|
|
|
// Note that qp is parsed on the C++ side.
|
|
callback.onDecodedFrame(frame, decodeTimeMs, null /* qp */);
|
|
frame.release();
|
|
}
|
|
|
|
private VideoFrame.Buffer copyNV12ToI420Buffer(
|
|
ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
|
|
// toI420 copies the buffer.
|
|
return new NV12Buffer(width, height, stride, sliceHeight, buffer, null /* releaseCallback */)
|
|
.toI420();
|
|
}
|
|
|
|
private VideoFrame.Buffer copyI420Buffer(
|
|
ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
|
|
if (stride % 2 != 0) {
|
|
throw new AssertionError("Stride is not divisible by two: " + stride);
|
|
}
|
|
|
|
// Note that the case with odd |sliceHeight| is handled in a special way.
|
|
// The chroma height contained in the payload is rounded down instead of
|
|
// up, making it one row less than what we expect in WebRTC. Therefore, we
|
|
// have to duplicate the last chroma rows for this case. Also, the offset
|
|
// between the Y plane and the U plane is unintuitive for this case. See
|
|
// http://bugs.webrtc.org/6651 for more info.
|
|
final int chromaWidth = (width + 1) / 2;
|
|
final int chromaHeight = (sliceHeight % 2 == 0) ? (height + 1) / 2 : height / 2;
|
|
|
|
final int uvStride = stride / 2;
|
|
|
|
final int yPos = 0;
|
|
final int yEnd = yPos + stride * height;
|
|
final int uPos = yPos + stride * sliceHeight;
|
|
final int uEnd = uPos + uvStride * chromaHeight;
|
|
final int vPos = uPos + uvStride * sliceHeight / 2;
|
|
final int vEnd = vPos + uvStride * chromaHeight;
|
|
|
|
VideoFrame.I420Buffer frameBuffer = allocateI420Buffer(width, height);
|
|
try { //don't crash
|
|
buffer.limit(yEnd);
|
|
buffer.position(yPos);
|
|
copyPlane(
|
|
buffer.slice(), stride, frameBuffer.getDataY(), frameBuffer.getStrideY(), width, height);
|
|
|
|
buffer.limit(uEnd);
|
|
buffer.position(uPos);
|
|
copyPlane(buffer.slice(), uvStride, frameBuffer.getDataU(), frameBuffer.getStrideU(),
|
|
chromaWidth, chromaHeight);
|
|
if (sliceHeight % 2 == 1) {
|
|
buffer.position(uPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.
|
|
|
|
ByteBuffer dataU = frameBuffer.getDataU();
|
|
dataU.position(frameBuffer.getStrideU() * chromaHeight); // Seek to beginning of last row.
|
|
dataU.put(buffer); // Copy the last row.
|
|
}
|
|
|
|
buffer.limit(vEnd);
|
|
buffer.position(vPos);
|
|
copyPlane(buffer.slice(), uvStride, frameBuffer.getDataV(), frameBuffer.getStrideV(),
|
|
chromaWidth, chromaHeight);
|
|
if (sliceHeight % 2 == 1) {
|
|
buffer.position(vPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.
|
|
|
|
ByteBuffer dataV = frameBuffer.getDataV();
|
|
dataV.position(frameBuffer.getStrideV() * chromaHeight); // Seek to beginning of last row.
|
|
dataV.put(buffer); // Copy the last row.
|
|
}
|
|
} catch (Throwable e) {
|
|
FileLog.e(e);
|
|
}
|
|
|
|
return frameBuffer;
|
|
}
|
|
|
|
private void reformat(MediaFormat format) {
|
|
outputThreadChecker.checkIsOnValidThread();
|
|
Logging.d(TAG, "Decoder format changed: " + format.toString());
|
|
final int newWidth;
|
|
final int newHeight;
|
|
if (format.containsKey(MEDIA_FORMAT_KEY_CROP_LEFT)
|
|
&& format.containsKey(MEDIA_FORMAT_KEY_CROP_RIGHT)
|
|
&& format.containsKey(MEDIA_FORMAT_KEY_CROP_BOTTOM)
|
|
&& format.containsKey(MEDIA_FORMAT_KEY_CROP_TOP)) {
|
|
newWidth = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_RIGHT)
|
|
- format.getInteger(MEDIA_FORMAT_KEY_CROP_LEFT);
|
|
newHeight = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_BOTTOM)
|
|
- format.getInteger(MEDIA_FORMAT_KEY_CROP_TOP);
|
|
} else {
|
|
newWidth = format.getInteger(MediaFormat.KEY_WIDTH);
|
|
newHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
|
|
}
|
|
// Compare to existing width, height, and save values under the dimension lock.
|
|
synchronized (dimensionLock) {
|
|
if (newWidth != width || newHeight != height) {
|
|
if (hasDecodedFirstFrame) {
|
|
stopOnOutputThread(new RuntimeException("Unexpected size change. "
|
|
+ "Configured " + width + "*" + height + ". "
|
|
+ "New " + newWidth + "*" + newHeight));
|
|
return;
|
|
} else if (newWidth <= 0 || newHeight <= 0) {
|
|
Logging.w(TAG,
|
|
"Unexpected format dimensions. Configured " + width + "*" + height + ". "
|
|
+ "New " + newWidth + "*" + newHeight + ". Skip it");
|
|
return;
|
|
}
|
|
width = newWidth;
|
|
height = newHeight;
|
|
}
|
|
}
|
|
|
|
// Note: texture mode ignores colorFormat. Hence, if the texture helper is non-null, skip
|
|
// color format updates.
|
|
if (surfaceTextureHelper == null && format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
|
|
colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
|
|
Logging.d(TAG, "Color: 0x" + Integer.toHexString(colorFormat));
|
|
if (!isSupportedColorFormat(colorFormat)) {
|
|
stopOnOutputThread(new IllegalStateException("Unsupported color format: " + colorFormat));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Save stride and sliceHeight under the dimension lock.
|
|
synchronized (dimensionLock) {
|
|
if (format.containsKey(MEDIA_FORMAT_KEY_STRIDE)) {
|
|
stride = format.getInteger(MEDIA_FORMAT_KEY_STRIDE);
|
|
}
|
|
if (format.containsKey(MEDIA_FORMAT_KEY_SLICE_HEIGHT)) {
|
|
sliceHeight = format.getInteger(MEDIA_FORMAT_KEY_SLICE_HEIGHT);
|
|
}
|
|
Logging.d(TAG, "Frame stride and slice height: " + stride + " x " + sliceHeight);
|
|
stride = Math.max(width, stride);
|
|
sliceHeight = Math.max(height, sliceHeight);
|
|
}
|
|
}
|
|
|
|
private void releaseCodecOnOutputThread() {
|
|
outputThreadChecker.checkIsOnValidThread();
|
|
Logging.d(TAG, "Releasing MediaCodec on output thread");
|
|
try {
|
|
codec.stop();
|
|
} catch (Exception e) {
|
|
Logging.e(TAG, "Media decoder stop failed", e);
|
|
}
|
|
try {
|
|
codec.release();
|
|
} catch (Exception e) {
|
|
Logging.e(TAG, "Media decoder release failed", e);
|
|
// Propagate exceptions caught during release back to the main thread.
|
|
shutdownException = e;
|
|
}
|
|
Logging.d(TAG, "Release on output thread done");
|
|
}
|
|
|
|
private void stopOnOutputThread(Exception e) {
|
|
outputThreadChecker.checkIsOnValidThread();
|
|
running = false;
|
|
shutdownException = e;
|
|
}
|
|
|
|
private boolean isSupportedColorFormat(int colorFormat) {
|
|
for (int supported : MediaCodecUtils.DECODER_COLOR_FORMATS) {
|
|
if (supported == colorFormat) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Visible for testing.
|
|
protected SurfaceTextureHelper createSurfaceTextureHelper() {
|
|
return SurfaceTextureHelper.create("decoder-texture-thread", sharedContext);
|
|
}
|
|
|
|
// Visible for testing.
|
|
// TODO(sakal): Remove once Robolectric commit fa991a0 has been rolled to WebRTC.
|
|
protected void releaseSurface() {
|
|
surface.release();
|
|
}
|
|
|
|
// Visible for testing.
|
|
protected VideoFrame.I420Buffer allocateI420Buffer(int width, int height) {
|
|
return JavaI420Buffer.allocate(width, height);
|
|
}
|
|
|
|
// Visible for testing.
|
|
protected void copyPlane(
|
|
ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height) {
|
|
YuvHelper.copyPlane(src, srcStride, dst, dstStride, width, height);
|
|
}
|
|
}
|