|
8 | 8 | import android.content.Context;
|
9 | 9 | import android.content.Intent;
|
10 | 10 | import android.content.pm.PackageManager;
|
| 11 | +import android.graphics.Bitmap; |
| 12 | +import android.graphics.ImageFormat; |
11 | 13 | import android.hardware.Camera;
|
12 | 14 | import android.hardware.Camera.Parameters;
|
13 | 15 | import android.hardware.camera2.CameraAccessException;
|
|
35 | 37 | import android.view.Surface;
|
36 | 38 | import android.view.WindowManager;
|
37 | 39 |
|
| 40 | +import androidx.annotation.NonNull; |
38 | 41 | import androidx.annotation.Nullable;
|
39 | 42 | import androidx.annotation.RequiresApi;
|
40 | 43 |
|
|
51 | 54 | import com.cloudwebrtc.webrtc.utils.MediaConstraintsUtils;
|
52 | 55 | import com.cloudwebrtc.webrtc.utils.ObjectType;
|
53 | 56 | import com.cloudwebrtc.webrtc.utils.PermissionUtils;
|
| 57 | +import com.google.android.gms.tasks.OnFailureListener; |
| 58 | +import com.google.android.gms.tasks.OnSuccessListener; |
| 59 | +import com.google.mlkit.common.MlKitException; |
| 60 | +import com.google.mlkit.vision.common.InputImage; |
| 61 | +import com.google.mlkit.vision.segmentation.Segmentation; |
| 62 | +import com.google.mlkit.vision.segmentation.SegmentationMask; |
| 63 | +import com.google.mlkit.vision.segmentation.Segmenter; |
| 64 | +import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions; |
54 | 65 |
|
55 | 66 | import org.webrtc.AudioSource;
|
56 | 67 | import org.webrtc.AudioTrack;
|
|
61 | 72 | import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
|
62 | 73 | import org.webrtc.CameraEnumerator;
|
63 | 74 | import org.webrtc.CameraVideoCapturer;
|
| 75 | +import org.webrtc.JavaI420Buffer; |
64 | 76 | import org.webrtc.MediaConstraints;
|
65 | 77 | import org.webrtc.MediaStream;
|
66 | 78 | import org.webrtc.MediaStreamTrack;
|
67 | 79 | import org.webrtc.PeerConnectionFactory;
|
68 | 80 | import org.webrtc.SurfaceTextureHelper;
|
69 | 81 | import org.webrtc.VideoCapturer;
|
| 82 | +import org.webrtc.VideoFrame; |
| 83 | +import org.webrtc.VideoProcessor; |
| 84 | +import org.webrtc.VideoSink; |
70 | 85 | import org.webrtc.VideoSource;
|
71 | 86 | import org.webrtc.VideoTrack;
|
| 87 | +import org.webrtc.YuvHelper; |
72 | 88 | import org.webrtc.audio.JavaAudioDeviceModule;
|
73 | 89 | import org.webrtc.audio.WebRtcAudioTrackUtils;
|
74 | 90 |
|
|
83 | 99 |
|
84 | 100 | import io.flutter.plugin.common.MethodChannel.Result;
|
85 | 101 |
|
| 102 | +import android.graphics.Bitmap; |
| 103 | +import android.graphics.BitmapFactory; |
| 104 | +import android.graphics.Canvas; |
| 105 | +import android.graphics.PorterDuff; |
| 106 | +import android.media.Image; |
| 107 | +import android.util.Log; |
| 108 | +import androidx.camera.core.ImageProxy; |
| 109 | + |
86 | 110 | /**
|
87 | 111 | * The implementation of {@code getUserMedia} extracted into a separate file in order to reduce
|
88 | 112 | * complexity and to (somewhat) separate concerns.
|
@@ -830,6 +854,221 @@ private ConstraintsMap getUserVideo(ConstraintsMap constraints, MediaStream medi
|
830 | 854 | return trackParams;
|
831 | 855 | }
|
832 | 856 |
|
| 857 | + void setVirtualBackground() { |
| 858 | + vbVideoSource.setVideoProcessor(new VideoProcessor() { |
| 859 | + @Override |
| 860 | + public void onCapturerStarted(boolean success) { |
| 861 | + // Xử lý khi bắt đầu capture video |
| 862 | + } |
| 863 | + |
| 864 | + @Override |
| 865 | + public void onCapturerStopped() { |
| 866 | + // Xử lý khi dừng capture video |
| 867 | + } |
| 868 | + |
| 869 | + @Override |
| 870 | + public void onFrameCaptured(VideoFrame frame) { |
| 871 | + // Chuyển đổi frame thành bitmap |
| 872 | + Bitmap bitmap = videoFrameToBitmap(frame); |
| 873 | + |
| 874 | + // Xử lý segment với bitmap |
| 875 | + processSegmentation(bitmap); |
| 876 | + } |
| 877 | + |
| 878 | + @Override |
| 879 | + public void setSink(VideoSink sink) { |
| 880 | + // Lưu sink để gửi frame đã được cập nhật trở lại WebRTC |
| 881 | + // Sink sẽ được sử dụng sau khi xử lý segment |
| 882 | + vbVideoSink = sink; |
| 883 | + } |
| 884 | + }); |
| 885 | + } |
| 886 | + |
| 887 | + public Bitmap videoFrameToBitmap(VideoFrame videoFrame) { |
| 888 | + VideoFrame.Buffer buffer = videoFrame.getBuffer(); |
| 889 | + int width = buffer.getWidth(); |
| 890 | + int height = buffer.getHeight(); |
| 891 | + |
| 892 | + if (buffer instanceof VideoFrame.TextureBuffer) { |
| 893 | + // Không hỗ trợ trực tiếp chuyển đổi từ TextureBuffer sang Bitmap |
| 894 | + return null; |
| 895 | + } else if (buffer instanceof VideoFrame.I420Buffer) { |
| 896 | + VideoFrame.I420Buffer i420Buffer = (VideoFrame.I420Buffer) buffer; |
| 897 | + |
| 898 | + int ySize = width * height; |
| 899 | + int uvSize = width * height / 4; |
| 900 | + |
| 901 | + ByteBuffer dataY = i420Buffer.getDataY(); |
| 902 | + ByteBuffer dataU = i420Buffer.getDataU(); |
| 903 | + ByteBuffer dataV = i420Buffer.getDataV(); |
| 904 | + |
| 905 | + byte[] dataYArray = new byte[ySize]; |
| 906 | + byte[] dataUArray = new byte[uvSize]; |
| 907 | + byte[] dataVArray = new byte[uvSize]; |
| 908 | + |
| 909 | + dataY.get(dataYArray); |
| 910 | + dataU.get(dataUArray); |
| 911 | + dataV.get(dataVArray); |
| 912 | + |
| 913 | + // Chuyển đổi từ YUV sang RGB |
| 914 | + int[] rgbData = convertYUVtoRGB(dataYArray, dataUArray, dataVArray, width, height); |
| 915 | + |
| 916 | + // Tạo Bitmap từ dữ liệu RGB |
| 917 | + Bitmap bitmap = Bitmap.createBitmap(rgbData, width, height, Bitmap.Config.ARGB_8888); |
| 918 | + |
| 919 | + return bitmap; |
| 920 | + } |
| 921 | + |
| 922 | + return null; |
| 923 | + } |
| 924 | + |
| 925 | + private int[] convertYUVtoRGB(byte[] yData, byte[] uData, byte[] vData, int width, int height) { |
| 926 | + int[] rgbData = new int[width * height]; |
| 927 | + int uvIndex = 0; |
| 928 | + int yOffset = 0; |
| 929 | + |
| 930 | + for (int y = 0; y < height; y++) { |
| 931 | + int uvRowStart = uvIndex; |
| 932 | + int uvRowOffset = y >> 1; |
| 933 | + |
| 934 | + for (int x = 0; x < width; x++) { |
| 935 | + int yIndex = yOffset + x; |
| 936 | + int uvIndexOffset = uvRowStart + (x >> 1); |
| 937 | + |
| 938 | + int yValue = yData[yIndex] & 0xFF; |
| 939 | + int uValue = uData[uvIndexOffset] & 0xFF; |
| 940 | + int vValue = vData[uvIndexOffset] & 0xFF; |
| 941 | + |
| 942 | + int r = yValue + (int) (1.370705f * (vValue - 128)); |
| 943 | + int g = yValue - (int) (0.698001f * (vValue - 128)) - (int) (0.337633f * (uValue - 128)); |
| 944 | + int b = yValue + (int) (1.732446f * (uValue - 128)); |
| 945 | + |
| 946 | + r = Math.max(0, Math.min(255, r)); |
| 947 | + g = Math.max(0, Math.min(255, g)); |
| 948 | + b = Math.max(0, Math.min(255, b)); |
| 949 | + |
| 950 | + int pixelColor = 0xFF000000 | (r << 16) | (g << 8) | b; |
| 951 | + rgbData[y * width + x] = pixelColor; |
| 952 | + } |
| 953 | + |
| 954 | + if (y % 2 == 1) { |
| 955 | + uvIndex = uvRowStart + width / 2; |
| 956 | + yOffset += width; |
| 957 | + } |
| 958 | + } |
| 959 | + |
| 960 | + return rgbData; |
| 961 | + } |
| 962 | + |
| 963 | + private void processSegmentation(Bitmap bitmap) { |
| 964 | + // Tạo InputImage từ bitmap |
| 965 | + InputImage inputImage = InputImage.fromBitmap(bitmap, 0); |
| 966 | + |
| 967 | + // Xử lý phân đoạn |
| 968 | + segmenter.process(inputImage) |
| 969 | + .addOnSuccessListener(new OnSuccessListener<SegmentationMask>() { |
| 970 | + @Override |
| 971 | + public void onSuccess(@NonNull SegmentationMask segmentationMask) { |
| 972 | + // Xử lý khi phân đoạn thành công |
| 973 | + ByteBuffer mask = segmentationMask.getBuffer(); |
| 974 | + int maskWidth = segmentationMask.getWidth(); |
| 975 | + int maskHeight = segmentationMask.getHeight(); |
| 976 | + mask.rewind(); |
| 977 | + |
| 978 | + // Chuyển đổi buffer thành mảng màu |
| 979 | + int[] colors = maskColorsFromByteBuffer(mask, maskWidth, maskHeight); |
| 980 | + |
| 981 | + // Tạo bitmap đã được phân đoạn từ mảng màu |
| 982 | + Bitmap segmentedBitmap = createBitmapFromColors(colors, maskWidth, maskHeight); |
| 983 | + |
| 984 | + // Vẽ ảnh nền đã phân đoạn lên canvas |
| 985 | + Bitmap outputBitmap = drawSegmentedBackground(segmentedBitmap, segmentedBitmap); |
| 986 | + |
| 987 | + // Tạo VideoFrame mới từ bitmap đã xử lý |
| 988 | + int frameRotation = 180; // Frame rotation angle (customize as needed) |
| 989 | + long frameTimestamp = System.nanoTime(); // Frame timestamp (customize as needed) |
| 990 | + VideoFrame outputVideoFrame = createVideoFrame(outputBitmap, frameRotation, frameTimestamp); |
| 991 | + |
| 992 | + // Gửi frame đã được cập nhật trở lại WebRTC |
| 993 | + vbVideoSink.onFrame(outputVideoFrame); |
| 994 | + } |
| 995 | + }) |
| 996 | + .addOnFailureListener(new OnFailureListener() { |
| 997 | + @Override |
| 998 | + public void onFailure(@NonNull Exception exception) { |
| 999 | + // Xử lý khi phân đoạn thất bại |
| 1000 | + Log.e(TAG, "Segmentation failed: " + exception.getMessage()); |
| 1001 | + } |
| 1002 | + }); |
| 1003 | + } |
| 1004 | + |
| 1005 | + private Bitmap drawSegmentedBackground(Bitmap segmentedBitmap, Bitmap backgroundBitmap) { |
| 1006 | + Bitmap outputBitmap = Bitmap.createBitmap( |
| 1007 | + segmentedBitmap.getWidth(), segmentedBitmap.getHeight(), Bitmap.Config.ARGB_8888 |
| 1008 | + ); |
| 1009 | + Canvas canvas = new Canvas(outputBitmap); |
| 1010 | + |
| 1011 | + // Vẽ ảnh nền đã phân đoạn lên canvas |
| 1012 | + canvas.drawBitmap(backgroundBitmap, 0, 0, null); |
| 1013 | + canvas.drawBitmap(segmentedBitmap, 0, 0, null); |
| 1014 | + |
| 1015 | + return outputBitmap; |
| 1016 | + } |
| 1017 | + |
| 1018 | + private VideoFrame createVideoFrame(Bitmap bitmap, int rotation, long timestampNs) { |
| 1019 | + ByteBuffer buffer = ByteBuffer.allocate(bitmap.getByteCount()); |
| 1020 | + bitmap.copyPixelsToBuffer(buffer); |
| 1021 | + byte[] data = buffer.array(); |
| 1022 | + |
| 1023 | + int width = bitmap.getWidth(); |
| 1024 | + int height = bitmap.getHeight(); |
| 1025 | + int strideY = width; |
| 1026 | + int strideU = (width + 1) / 2; |
| 1027 | + int strideV = (width + 1) / 2; |
| 1028 | + |
| 1029 | + byte[] dataU = new byte[width * height / 4]; |
| 1030 | + byte[] dataV = new byte[width * height / 4]; |
| 1031 | + for (int i = 0; i < width * height / 4; i++) { |
| 1032 | + dataU[i] = data[width * height + i]; |
| 1033 | + dataV[i] = data[width * height + width * height / 4 + i]; |
| 1034 | + } |
| 1035 | + |
| 1036 | + Runnable releaseCallback = () -> { |
| 1037 | + // Thực hiện các thao tác giải phóng tài nguyên liên quan tại đây (nếu có) |
| 1038 | + }; |
| 1039 | + |
| 1040 | + VideoFrame.I420Buffer i420Buffer = JavaI420Buffer.wrap( |
| 1041 | + width, |
| 1042 | + height, |
| 1043 | + ByteBuffer.wrap(data), |
| 1044 | + strideY, |
| 1045 | + ByteBuffer.wrap(dataU), |
| 1046 | + strideU, ByteBuffer.wrap(dataV), strideV, releaseCallback |
| 1047 | + ); |
| 1048 | + |
| 1049 | + return new VideoFrame(i420Buffer, rotation, timestampNs); |
| 1050 | + } |
| 1051 | + |
| 1052 | + |
| 1053 | + // Hàm chuyển đổi buffer thành mảng màu |
| 1054 | + private int[] maskColorsFromByteBuffer(ByteBuffer buffer, int width, int height) { |
| 1055 | + // Chuyển đổi từ ByteBuffer thành mảng màu, tùy thuộc vào định dạng màu |
| 1056 | + // của buffer. Đảm bảo bạn sử dụng đúng định dạng màu tương ứng với |
| 1057 | + // phân đoạn của ML Kit. |
| 1058 | + // Trong ví dụ này, chúng tôi giả định rằng buffer có định dạng ARGB_8888. |
| 1059 | + |
| 1060 | + // Ví dụ: chuyển đổi từ ByteBuffer thành mảng ARGB_8888 |
| 1061 | + int[] colors = new int[width * height]; |
| 1062 | + buffer.asIntBuffer().get(colors); |
| 1063 | + |
| 1064 | + return colors; |
| 1065 | + } |
| 1066 | + |
| 1067 | + // Hàm tạo bitmap từ mảng màu |
| 1068 | + private Bitmap createBitmapFromColors(int[] colors, int width, int height) { |
| 1069 | + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); |
| 1070 | + } |
| 1071 | + |
833 | 1072 | void removeVideoCapturerSync(String id) {
|
834 | 1073 | synchronized (mVideoCapturers) {
|
835 | 1074 | // Dispose Virtual Background
|
|
0 commit comments