From d01457e6f5131961fd605e27357f2045268eeacf Mon Sep 17 00:00:00 2001 From: ShenBen <714081644@qq.com> Date: Mon, 6 Dec 2021 18:11:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0VideoSink=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E7=B1=BB=EF=BC=9B=20=E6=B7=BB=E5=8A=A0VideoProcessor=E9=92=88?= =?UTF-8?q?=E5=AF=B9NV21=E6=A0=BC=E5=BC=8F=E6=95=B0=E6=8D=AE=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E5=8F=A0=E5=9B=BE=E5=8A=9F=E8=83=BD=E3=80=82=20v1.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension-lib/build.gradle | 5 +- .../webrtcextension/BaseNV21VideoProcessor.kt | 108 +++++++ .../OverlayNV21VideoProcessor.kt | 298 ++++++++++++++++++ .../webrtcextension/ProxyVideoSink.kt | 44 +++ .../webrtcextension/util/NV21Util.java | 175 ++++++++++ .../webrtcextension/util/Nv21BufferUtil.kt | 89 ++++++ 6 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 extension-lib/src/main/java/com/shencoder/webrtcextension/BaseNV21VideoProcessor.kt create mode 100644 extension-lib/src/main/java/com/shencoder/webrtcextension/OverlayNV21VideoProcessor.kt create mode 100644 extension-lib/src/main/java/com/shencoder/webrtcextension/ProxyVideoSink.kt create mode 100644 extension-lib/src/main/java/com/shencoder/webrtcextension/util/NV21Util.java create mode 100644 extension-lib/src/main/java/com/shencoder/webrtcextension/util/Nv21BufferUtil.kt diff --git a/extension-lib/build.gradle b/extension-lib/build.gradle index a34ca25..1a3becf 100644 --- a/extension-lib/build.gradle +++ b/extension-lib/build.gradle @@ -29,8 +29,9 @@ android { } dependencies { - compileOnly 'androidx.annotation:annotation:1.2.0' + compileOnly 'androidx.annotation:annotation:1.3.0' compileOnly 'org.webrtc:google-webrtc:1.0.32006' + api "io.github.crow-misia.libyuv:libyuv-android:0.13.0" } // Because the components are created only during the afterEvaluate phase, you must @@ -46,7 +47,7 @@ afterEvaluate { // You can then customize attributes of the publication as shown below. groupId = 'com.shencoder' artifactId = 'webrtcextension' - version = '1.0.2' + version = '1.0.4' } } } diff --git a/extension-lib/src/main/java/com/shencoder/webrtcextension/BaseNV21VideoProcessor.kt b/extension-lib/src/main/java/com/shencoder/webrtcextension/BaseNV21VideoProcessor.kt new file mode 100644 index 0000000..b7eb1bc --- /dev/null +++ b/extension-lib/src/main/java/com/shencoder/webrtcextension/BaseNV21VideoProcessor.kt @@ -0,0 +1,108 @@ +package com.shencoder.webrtcextension + +import androidx.annotation.CallSuper +import org.webrtc.* + +/** + * 仅处理[NV21Buffer]格式数据基类,会使用反射拿到[NV21Buffer.data]; + * 如果想使用此类,则需要[Camera1Enumerator.captureToTexture]是false, + * 即[Camera1Capturer.captureToTexture]为false + * + * you must be call [Camera1Enumerator(false)] + * + * @author ShenBen + * @date 2021/12/6 08:44 + * @email 714081644@qq.com + */ +abstract class BaseNV21VideoProcessor : VideoProcessor { + + protected var mSink: VideoSink? = null + + @CallSuper + override fun setSink(sink: VideoSink?) { + mSink = sink + } + + @CallSuper + override fun onCapturerStarted(success: Boolean) { + + } + + @CallSuper + override fun onCapturerStopped() { + + } + + /** + * 此方法会间接调用[NV21Buffer.cropAndScale]转为[VideoFrame.I420Buffer],所以要在super之前处理 + */ + final override fun onFrameCaptured( + frame: VideoFrame, + parameters: VideoProcessor.FrameAdaptationParameters + ) { + if (parameters.drop) { + //直接丢弃 + return + } + val buffer = frame.buffer + if (buffer is NV21Buffer) { + //进处理NV21Buffer + //通过反射拿到[NV21Buffer.data] + val nv21Class = buffer::class.java + val declaredField = nv21Class.getDeclaredField("data") + declaredField.isAccessible = true + val bytes = declaredField.get(buffer) as ByteArray + val nv21Bytes: ByteArray = checkNV21ByteArray(bytes, buffer.width, buffer.height) + //处理nv21数据是否成功 + val success = handleNV21(nv21Bytes, buffer.width, buffer.height, frame.rotation) + if (success.not()) { + super.onFrameCaptured(frame, parameters) + return + } + //将处理好的nv21数据转换为NV21Buffer,传给VideoFrame + val nv21Buffer = NV21Buffer(nv21Bytes, buffer.width, buffer.height, null) + val videoFrame = VideoFrame(nv21Buffer, frame.rotation, frame.timestampNs) + //调用super方法,传入新生成的VideoFrame + super.onFrameCaptured(videoFrame, parameters) + videoFrame.release() + } else { + super.onFrameCaptured(frame, parameters) + } + } + + @CallSuper + override fun onFrameCaptured(frame: VideoFrame) { + //将处理好的VideoFrame发送出去 + mSink?.onFrame(frame) + } + + /** + * 处理NV21数据 + * + * @param nv21 原始nv21数据,请直接修改此数组的数据,会二次使用 + * @param width 原始nv21数据的宽 + * @param height 原始nv21数据的高 + * @param rotation 原始nv21数据的方向 + * + * @return 是否处理完成;ture:发送[nv21],false:则按照原有的流程处理 + */ + abstract fun handleNV21( + nv21: ByteArray, + width: Int, + height: Int, + rotation: Int + ): Boolean + + protected fun checkNV21ByteArray(nv21: ByteArray, width: Int, height: Int): ByteArray { + //标准大小 + val size = width * height * 3 / 2 + return if (size == nv21.size) { + nv21 + } else { + //宽或高为奇数时,可能会出现比标准大小大的情况,要进行剪裁 + val byteArray = ByteArray(size) + System.arraycopy(nv21, 0, byteArray, 0, size) + byteArray + } + } +} \ No newline at end of file diff --git a/extension-lib/src/main/java/com/shencoder/webrtcextension/OverlayNV21VideoProcessor.kt b/extension-lib/src/main/java/com/shencoder/webrtcextension/OverlayNV21VideoProcessor.kt new file mode 100644 index 0000000..ac442ad --- /dev/null +++ b/extension-lib/src/main/java/com/shencoder/webrtcextension/OverlayNV21VideoProcessor.kt @@ -0,0 +1,298 @@ +package com.shencoder.webrtcextension + +import android.util.Log +import androidx.annotation.IntRange +import com.shencoder.webrtcextension.util.NV21Util +import io.github.crow_misia.libyuv.Nv21Buffer +import io.github.crow_misia.libyuv.RotateMode +import io.github.crow_misia.libyuv.rotate +import kotlin.math.abs +import kotlin.math.max +import com.shencoder.webrtcextension.util.Nv21BufferUtil +import org.webrtc.VideoFrame + +/** + * 在nv21数据上进行叠图操作,已经处理不同[VideoFrame.rotation]操作,始终以左上角为起始点; + * overlay data on nv21 bytes, different of [VideoFrame.rotation] operations have been processed, always starting from the upper left corner; + * + * + * 如果[Nv21Buffer.width]+[left]>=[videoFrameWidth] 或者 + * [Nv21Buffer.height]+[top]>=[videoFrameHeight]则会进行相应的剪裁。 + * + * if [Nv21Buffer.width]+[left]>=[videoFrameWidth] or [Nv21Buffer.height]+[top]>=[videoFrameHeight], and it will be clipped. + * + * @author ShenBen + * @date 2021/12/6 09:36 + * @email 714081644@qq.com + */ +class OverlayNV21VideoProcessor @JvmOverloads constructor( + /** + * 叠图的[Nv21Buffer] + * {@see [Nv21BufferUtil]} + */ + private val overlayNv21Buffer: Nv21Buffer, + /** + * 叠图起始左边位置,尽量确保为偶数 + * left position, try to ensure that it is even. + */ + @IntRange(from = 0) private val left: Int, + /** + * 叠图起始上边位置,尽量确保为偶数 + * top position, try to ensure that it is even. + */ + @IntRange(from = 0) private val top: Int, + /** + * [overlayNv21Buffer]中是否存在透明部分的数据; + * 尽量使用不带透明数据的,透明数据处理比较耗时。 + * + * if true, it will take more time. + */ + private val hasTransparent: Boolean = false +) : BaseNV21VideoProcessor() { + + private companion object { + private const val TAG = "OverlayNV21Processor" + } + + private lateinit var realNV21ByteArray: ByteArray + + private val lock = Object() + + private var videoFrameWidth = 0 + private var videoFrameHeight = 0 + + /** + * 当前VideoFrame方向 + */ + private var videoFrameRotation = 0 + + /** + * 在camera NV21数据中叠图的left位置 + */ + private var startLeft = left + + /** + * 在camera NV21数据中叠图的top位置 + */ + private var startTop = top + + private var overlayWidth: Int = overlayNv21Buffer.width + + private var overlayHeight: Int = overlayNv21Buffer.height + + override fun handleNV21(nv21: ByteArray, width: Int, height: Int, rotation: Int): Boolean { + synchronized(lock) { + if (videoFrameWidth != width || videoFrameHeight != height || videoFrameRotation != rotation) { + val adaptOverlayNv21 = adaptOverlayNv21(rotation, width, height) + if (adaptOverlayNv21.not()) { + return false + } + } + } + //叠图 + NV21Util.overlayNV21( + nv21, + width, + height, + startLeft, + startTop, + realNV21ByteArray, + overlayWidth, + overlayHeight, + hasTransparent + ) + return true + } + + /** + * 根据旋转角度对叠图nv21数据进行转换 + * + * @param rotation 角度:0°、90°、180°、270° + * @param frameWidth 原始帧数据的宽度 + * @param frameHeight 原始帧数据的宽度 + * + * @return true:转换成功,false:转换失败 + */ + private fun adaptOverlayNv21(rotation: Int, frameWidth: Int, frameHeight: Int): Boolean { + when (rotation) { + 0 -> {//不用旋转,直接处理即可 + //先判断是否合法 + if (left >= frameWidth) { + return false + } + if (top >= frameHeight) { + return false + } + overlayWidth = overlayNv21Buffer.width + overlayHeight = overlayNv21Buffer.height + + val tempByteArray = overlayNv21Buffer.asByteArray() + realNV21ByteArray = + checkNV21ByteArray(tempByteArray, overlayWidth, overlayHeight) + + startLeft = left + startTop = top + } + 90 -> {//需要将overlayNV21Buffer旋转270°,然后判断可显示的大小是否需要剪裁,再计算在camera nv21数据中开始叠图的位置 + //判断位置是否合法 + if (top >= frameWidth) { + return false + } + if (left >= frameHeight) { + return false + } + //宽高交换 + overlayWidth = overlayNv21Buffer.height + overlayHeight = overlayNv21Buffer.width + //将overlayNV21Buffer进行旋转270° + val rotateNv21Buffer = Nv21Buffer.allocate(overlayWidth, overlayHeight) + overlayNv21Buffer.rotate(rotateNv21Buffer, RotateMode.ROTATE_270) + val tempByteArray = rotateNv21Buffer.asByteArray() + realNV21ByteArray = + checkNV21ByteArray(tempByteArray, overlayWidth, overlayHeight) + rotateNv21Buffer.close() + + startLeft = top + + val tempTop = frameHeight - left - overlayHeight + if (tempTop < 0) { + //超出边界,需要进行剪裁 + //先计算需要剪裁的高 + val height = overlayHeight - abs(tempTop) + if (height <= 0) { + //完全超出边界,不需要进行叠图 + return false + } + //二次剪裁 + val cropNV21 = NV21Util.cropNV21( + realNV21ByteArray, + overlayWidth, + overlayHeight, + overlayWidth, + height, + 0, + abs(tempTop) + ) ?: return false + realNV21ByteArray = cropNV21 + overlayHeight = height + } + startTop = max(tempTop, 0) + } + 180 -> {//需要将overlayNV21Buffer旋转180°,然后判断可显示的大小是否需要剪裁,再计算在camera nv21数据中开始叠图的位置 + //先判断是否合法 + if (left >= frameWidth) { + return false + } + if (top >= frameHeight) { + return false + } + + overlayWidth = overlayNv21Buffer.width + overlayHeight = overlayNv21Buffer.height + //将overlayNV21Buffer数据进行旋转180° + val rotateNv21Buffer = Nv21Buffer.allocate(overlayWidth, overlayHeight) + overlayNv21Buffer.rotate(rotateNv21Buffer, RotateMode.ROTATE_180) + val tempByteArray = rotateNv21Buffer.asByteArray() + realNV21ByteArray = + checkNV21ByteArray(tempByteArray, overlayWidth, overlayHeight) + rotateNv21Buffer.close() + + val tempLeft = frameWidth - left - overlayWidth + val tempTop = frameHeight - top - overlayHeight + if (tempLeft < 0 || tempTop < 0) { + //超出边界,需要进行剪裁 + val width: Int + if (tempLeft < 0) { + //先计算需要剪裁的宽 + width = overlayHeight - abs(tempLeft) + if (width <= 0) { + //完全超出边界,不需要进行叠图 + return false + } + } else { + width = overlayWidth + } + val height: Int + if (tempTop < 0) { + //先计算需要剪裁的高 + height = overlayHeight - abs(tempTop) + if (height <= 0) { + //完全超出边界,不需要进行叠图 + return false + } + } else { + height = overlayHeight + } + //二次剪裁 + val cropNV21 = NV21Util.cropNV21( + realNV21ByteArray, + overlayWidth, + overlayHeight, + width, + height, + 0, + if (tempTop > 0) 0 else abs(tempTop) + ) ?: return false + realNV21ByteArray = cropNV21 + overlayWidth = width + overlayHeight = height + } + startLeft = max(tempLeft, 0) + startTop = max(tempTop, 0) + } + 270 -> {//需要将overlayNV21Buffer旋转90°,然后判断可显示的大小是否需要剪裁,再计算在camera nv21数据中开始叠图的位置 + //判断位置是否合法 + if (top >= frameWidth) { + return false + } + if (left >= frameHeight) { + return false + } + //宽高交换 + overlayWidth = overlayNv21Buffer.height + overlayHeight = overlayNv21Buffer.width + //将overlayNV21Buffer数据进行旋转270° + val rotateNv21Buffer = Nv21Buffer.allocate(overlayWidth, overlayHeight) + overlayNv21Buffer.rotate(rotateNv21Buffer, RotateMode.ROTATE_90) + val tempByteArray = rotateNv21Buffer.asByteArray() + realNV21ByteArray = + checkNV21ByteArray(tempByteArray, overlayWidth, overlayHeight) + + rotateNv21Buffer.close() + + val tempLeft = frameWidth - top - overlayWidth + if (tempLeft < 0) { + //超出边界,需要进行剪裁 + //先计算需要剪裁的宽 + val width = overlayHeight - abs(tempLeft) + if (width <= 0) { + //完全超出边界,不需要进行叠图 + return false + } + //二次剪裁 + val cropNV21 = NV21Util.cropNV21( + realNV21ByteArray, + overlayWidth, + overlayHeight, + width, + overlayHeight, + abs(tempLeft), + 0 + ) ?: return false + realNV21ByteArray = cropNV21 + overlayWidth = width + } + startLeft = max(tempLeft, 0) + startTop = left + } + else -> { + Log.e(TAG, "adaptOverlayNv21: unknown rotation: $rotation") + return false + } + } + videoFrameWidth = frameWidth + videoFrameHeight = frameHeight + videoFrameRotation = rotation + return true + } +} \ No newline at end of file diff --git a/extension-lib/src/main/java/com/shencoder/webrtcextension/ProxyVideoSink.kt b/extension-lib/src/main/java/com/shencoder/webrtcextension/ProxyVideoSink.kt new file mode 100644 index 0000000..d54d7c1 --- /dev/null +++ b/extension-lib/src/main/java/com/shencoder/webrtcextension/ProxyVideoSink.kt @@ -0,0 +1,44 @@ +package com.shencoder.webrtcextension + +import org.webrtc.VideoFrame +import org.webrtc.VideoSink + +/** + * [VideoSink]的代理类,可用于[VideoFrame]的二次处理 + * + *example: + * + * val svr = findViewById(R.id.svr) + * val proxy = ProxyVideoSink(svr, object:VideoFrameProcessor{ + * override fun onFrameProcessor(frame: VideoFrame): VideoFrame { + * //handle your video frame. + * val newFrame: VideoFrame = handleYourVideoFrame(frame) + * return newFrame; + * } + * }) + * val videoTrack: VideoTrack = ... + * videoTrack.addSink(proxy) + * + * + * @author ShenBen + * @date 2021/12/6 14:04 + * @email 714081644@qq.com + */ +class ProxyVideoSink(private val sink: VideoSink, private val processor: VideoFrameProcessor) : + VideoSink { + + override fun onFrame(frame: VideoFrame?) { + if (frame == null) { + return + } + val newFrame: VideoFrame + synchronized(this) { + newFrame = processor.onFrameProcessor(frame) + } + sink.onFrame(newFrame) + } + + interface VideoFrameProcessor { + fun onFrameProcessor(frame: VideoFrame): VideoFrame + } +} \ No newline at end of file diff --git a/extension-lib/src/main/java/com/shencoder/webrtcextension/util/NV21Util.java b/extension-lib/src/main/java/com/shencoder/webrtcextension/util/NV21Util.java new file mode 100644 index 0000000..8ced33d --- /dev/null +++ b/extension-lib/src/main/java/com/shencoder/webrtcextension/util/NV21Util.java @@ -0,0 +1,175 @@ +package com.shencoder.webrtcextension.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @author ShenBen + * @date 2021/11/21 14:01 + * @email 714081644@qq.com + */ +public class NV21Util { + /** + * 透明Y值 + */ + public static final byte TRANSPARENT_Y = 0x10; + /** + * 透明UV值 + */ + public static final byte TRANSPARENT_UV = (byte) 0x80; + + /** + * nv21数据剪裁 + * + * @param src 原始nv21数据 + * @param srcWidth 原始nv21数据的宽 + * @param srcHeight 原始nv21数据的高 + * @param clipWidth 剪裁的宽度 + * @param clipHeight 剪裁的高度 + * @param left 剪裁的开始的左边位置,坐标相对于nv21原始数据 + * @param top 剪裁的开始的上边位置,坐标相对于nv21原始数据 + * @return 剪裁异常时返回null,成功时返回剪裁的nv21数据 + */ + @Nullable + public static byte[] cropNV21(@NonNull byte[] src, int srcWidth, int srcHeight, int clipWidth, int clipHeight, int left, int top) { + if (src.length != srcWidth * srcHeight * 3 / 2) { + return null; + } + if (clipWidth + left > srcWidth || clipHeight + top > srcHeight) { + return null; + } + if (clipWidth == srcWidth && clipHeight == srcHeight && left == 0 && top == 0) { + return src; + } + //确保为偶数 + clipWidth &= ~1; + clipHeight &= ~1; + left &= ~1; + top &= ~1; + byte[] cropBytes = new byte[clipWidth * clipHeight * 3 / 2]; + int bottom = top + clipHeight; + //先复制Y数据 + for (int i = top; i < bottom; i++) { + System.arraycopy(src, left + i * srcWidth, cropBytes, (i - top) * clipWidth, clipWidth); + } + //复制UV数据 + int startH = srcHeight + top / 2; + int endH = srcHeight + bottom / 2; + for (int i = startH; i < endH; i++) { + System.arraycopy(src, + left + i * srcWidth, + cropBytes, + (i - startH + clipHeight) * clipWidth, + clipWidth); + } + return cropBytes; + } + + /** + * 叠图 + * 调用完成后改方法后,直接使用传入的nv21 数据即可。 + * + * @param nv21 叠图最下面的图的nv21数据,大小要比被叠图的nv21数据大 + * @param width 最下面叠图的nv21数据的宽 + * @param height 最下面叠图的nv21数据的高 + * @param left 叠图起始左边位置 + * @param top 叠图起始的上边位置 + * @param overlayNv21 小图的nv21数据 + * @param overlayWidth 小图的宽 + * @param overlayHeight 小图的高 + */ + public static void overlayNV21(@NonNull byte[] nv21, int width, int height, int left, int top, @NonNull byte[] overlayNv21, int overlayWidth, int overlayHeight) { + overlayNV21(nv21, width, height, left, top, overlayNv21, overlayWidth, overlayHeight, false); + } + + /** + * 叠图,先判断是否超出范围,超出进行剪裁。 + * 调用完成后改方法后,直接使用传入的nv21 数据即可。 + * + * @param nv21 叠图最下面的图的nv21数据,大小要比被叠图的nv21数据大 + * @param width 最下面叠图的nv21数据的宽 + * @param height 最下面叠图的nv21数据的高 + * @param left 叠图起始左边位置 + * @param top 叠图起始的上边位置 + * @param overlayNv21 小图的nv21数据 + * @param overlayWidth 小图的宽 + * @param overlayHeight 小图的高 + * @param transparent 叠图中是否有透明数据;如果有,但传参false的话,会以黑色填充;如果是true的话,会比较耗时 + */ + public static void overlayNV21(@NonNull byte[] nv21, int width, int height, int left, int top, @NonNull byte[] overlayNv21, int overlayWidth, int overlayHeight, boolean transparent) { + if (nv21.length != width * height * 3 / 2) { + return; + } + if (overlayNv21.length != overlayWidth * overlayHeight * 3 / 2) { + return; + } + int originalOverlayWidth = overlayWidth; + int originalOverlayHeight = overlayHeight; + if (overlayWidth + left > width) { + //不符合要求,进行二次剪裁 + overlayWidth = width - left; + } + if (overlayHeight + top > height) { + //不符合要求,进行二次剪裁 + overlayHeight = height - top; + } + //确保为偶数 + left &= ~1; + top &= ~1; + overlayWidth &= ~1; + overlayHeight &= ~1; + + //裁剪 + overlayNv21 = cropNV21(overlayNv21, originalOverlayWidth, originalOverlayHeight, overlayWidth, overlayHeight, 0, 0); + if (overlayNv21 == null) { + return; + } + if (transparent) { + //途中有透明部分 + //先剪裁出nv21中覆盖部分的相同位置的数据 + byte[] cropNV21 = cropNV21(nv21, width, height, overlayWidth, overlayHeight, left, top); + if (cropNV21 == null) { + return; + } + int size = overlayWidth * overlayHeight * 3 / 2; + //然后合并 + if (cropNV21.length != size || overlayNv21.length != size) { + return; + } + + int splitY = overlayWidth * overlayHeight; + for (int i = 0; i < size; i++) { + if (i < splitY) { + //Y数据 + if (overlayNv21[i] != TRANSPARENT_Y) { + cropNV21[i] = overlayNv21[i]; + } + } else { + //UV数据 + if (overlayNv21[i] != TRANSPARENT_UV) { + cropNV21[i] = overlayNv21[i]; + } + } + } + overlayNv21 = cropNV21; + } + + //先复制Y数据 + for (int i = 0; i < overlayHeight; i++) { + System.arraycopy(overlayNv21, i * overlayWidth, nv21, left + (top + i) * width, overlayWidth); + } + //复制UV数据 + int startH = overlayHeight; + int endH = overlayHeight + (overlayWidth * overlayHeight / 2) / overlayWidth; + + int basic = height + top / 2; + for (int i = startH; i < endH; i++) { + System.arraycopy(overlayNv21, + i * overlayWidth, + nv21, + left + (basic + (i - startH)) * width, + overlayWidth); + } + } + +} diff --git a/extension-lib/src/main/java/com/shencoder/webrtcextension/util/Nv21BufferUtil.kt b/extension-lib/src/main/java/com/shencoder/webrtcextension/util/Nv21BufferUtil.kt new file mode 100644 index 0000000..2cb76b7 --- /dev/null +++ b/extension-lib/src/main/java/com/shencoder/webrtcextension/util/Nv21BufferUtil.kt @@ -0,0 +1,89 @@ +package com.shencoder.webrtcextension.util + +import android.graphics.Bitmap +import io.github.crow_misia.libyuv.* +import java.lang.IllegalArgumentException +import java.nio.ByteBuffer + +/** + * + * @author ShenBen + * @date 2021/12/6 11:05 + * @email 714081644@qq.com + */ +object Nv21BufferUtil { + + /** + * [Bitmap]To[Nv21Buffer] + * Only use when [Bitmap.getConfig] is [Bitmap.Config.ARGB_8888] + * + * [Bitmap]->[AbgrBuffer]->[Nv21Buffer] + * + * @param bitmap + * @param recycleBitmap is recycle [bitmap] + */ + @JvmStatic + @JvmOverloads + fun argb8888BitmapToNv21Buffer(bitmap: Bitmap, recycleBitmap: Boolean = false): Nv21Buffer { + val config = bitmap.config + if (config != Bitmap.Config.ARGB_8888) { + throw IllegalArgumentException("Unexpected bitmap config:$config") + } + val abgrBuffer = AbgrBuffer.allocate(bitmap.width, bitmap.height) + bitmap.copyPixelsToBuffer(abgrBuffer.asBuffer()) + + val nv21Buffer = Nv21Buffer.allocate(bitmap.width, bitmap.height) + abgrBuffer.convertTo(nv21Buffer) + + abgrBuffer.close() + + if (recycleBitmap) { + bitmap.recycle() + } + return nv21Buffer + } + + /** + * [Bitmap]To[Nv21Buffer] + * Only use when [Bitmap.getConfig] is [Bitmap.Config.RGB_565] + * + * [Bitmap]->[Rgb565Buffer]->[I420Buffer]->[Nv21Buffer] + * + * @param bitmap + * @param recycleBitmap is recycle [bitmap] + */ + @JvmStatic + @JvmOverloads + fun rgb565BitmapToNv21Buffer(bitmap: Bitmap, recycleBitmap: Boolean = false): Nv21Buffer { + val config = bitmap.config + if (config != Bitmap.Config.RGB_565) { + throw IllegalArgumentException("Unexpected bitmap config:$config") + } + val rgb565Buffer = Rgb565Buffer.allocate(bitmap.width, bitmap.height) + bitmap.copyPixelsToBuffer(rgb565Buffer.asBuffer()) + + val i420Buffer = I420Buffer.allocate(bitmap.width, bitmap.height) + rgb565Buffer.convertTo(i420Buffer) + + val nv21Buffer = Nv21Buffer.allocate(bitmap.width, bitmap.height) + i420Buffer.convertTo(nv21Buffer) + + rgb565Buffer.close() + i420Buffer.close() + + if (recycleBitmap) { + bitmap.recycle() + } + return nv21Buffer + } + + /** + * nv21 [ByteArray] to [Nv21Buffer] + */ + @JvmStatic + fun nv21ByteArrayToNv21Buffer(nv21: ByteArray, width: Int, height: Int): Nv21Buffer { + val byteBuffer = ByteBuffer.allocateDirect(nv21.size) + byteBuffer.put(nv21) + return Nv21Buffer.wrap(byteBuffer, width, height) + } +} \ No newline at end of file