|
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; |
|
33 | 35 | import android.view.Surface; |
34 | 36 | import android.view.WindowManager; |
35 | 37 |
|
| 38 | +import androidx.annotation.NonNull; |
36 | 39 | import androidx.annotation.Nullable; |
37 | 40 | import androidx.annotation.RequiresApi; |
38 | 41 |
|
|
48 | 51 | import com.cloudwebrtc.webrtc.utils.MediaConstraintsUtils; |
49 | 52 | import com.cloudwebrtc.webrtc.utils.ObjectType; |
50 | 53 | import com.cloudwebrtc.webrtc.utils.PermissionUtils; |
| 54 | +import com.google.android.gms.tasks.OnFailureListener; |
| 55 | +import com.google.android.gms.tasks.OnSuccessListener; |
| 56 | +import com.google.mlkit.common.MlKitException; |
| 57 | +import com.google.mlkit.vision.common.InputImage; |
| 58 | +import com.google.mlkit.vision.segmentation.Segmentation; |
| 59 | +import com.google.mlkit.vision.segmentation.SegmentationMask; |
| 60 | +import com.google.mlkit.vision.segmentation.Segmenter; |
| 61 | +import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions; |
51 | 62 |
|
52 | 63 | import org.webrtc.AudioSource; |
53 | 64 | import org.webrtc.AudioTrack; |
|
58 | 69 | import org.webrtc.CameraEnumerationAndroid.CaptureFormat; |
59 | 70 | import org.webrtc.CameraEnumerator; |
60 | 71 | import org.webrtc.CameraVideoCapturer; |
| 72 | +import org.webrtc.JavaI420Buffer; |
61 | 73 | import org.webrtc.MediaConstraints; |
62 | 74 | import org.webrtc.MediaStream; |
63 | 75 | import org.webrtc.MediaStreamTrack; |
64 | 76 | import org.webrtc.PeerConnectionFactory; |
65 | 77 | import org.webrtc.SurfaceTextureHelper; |
66 | 78 | import org.webrtc.VideoCapturer; |
| 79 | +import org.webrtc.VideoFrame; |
| 80 | +import org.webrtc.VideoProcessor; |
| 81 | +import org.webrtc.VideoSink; |
67 | 82 | import org.webrtc.VideoSource; |
68 | 83 | import org.webrtc.VideoTrack; |
| 84 | +import org.webrtc.YuvHelper; |
69 | 85 | import org.webrtc.audio.JavaAudioDeviceModule; |
70 | 86 |
|
71 | 87 | import java.io.File; |
72 | 88 | import java.lang.reflect.Field; |
| 89 | +import java.nio.ByteBuffer; |
73 | 90 | import java.util.ArrayList; |
74 | 91 | import java.util.HashMap; |
75 | 92 | import java.util.List; |
76 | 93 | import java.util.Map; |
77 | 94 |
|
78 | 95 | import io.flutter.plugin.common.MethodChannel.Result; |
79 | 96 |
|
| 97 | +import android.graphics.Bitmap; |
| 98 | +import android.graphics.BitmapFactory; |
| 99 | +import android.graphics.Canvas; |
| 100 | +import android.graphics.PorterDuff; |
| 101 | +import android.media.Image; |
| 102 | +import android.util.Log; |
| 103 | +import androidx.camera.core.ImageProxy; |
| 104 | + |
80 | 105 | /** |
81 | 106 | * The implementation of {@code getUserMedia} extracted into a separate file in order to reduce |
82 | 107 | * complexity and to (somewhat) separate concerns. |
@@ -112,6 +137,14 @@ class GetUserMediaImpl { |
112 | 137 | private final SparseArray<MediaRecorderImpl> mediaRecorders = new SparseArray<>(); |
113 | 138 | private AudioDeviceInfo preferredInput = null; |
114 | 139 |
|
| 140 | + private final SelfieSegmenterOptions segmentOptions = new SelfieSegmenterOptions.Builder() |
| 141 | + .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE) |
| 142 | + .build(); |
| 143 | + private final Segmenter segmenter = Segmentation.getClient(segmentOptions); |
| 144 | + |
| 145 | + private VideoSource vbVideoSource = null; |
| 146 | + private VideoSink vbVideoSink = null; |
| 147 | + |
115 | 148 | public void screenRequestPermissions(ResultReceiver resultReceiver) { |
116 | 149 | final Activity activity = stateProvider.getActivity(); |
117 | 150 | if (activity == null) { |
@@ -739,6 +772,9 @@ private ConstraintsMap getUserVideo(ConstraintsMap constraints, MediaStream medi |
739 | 772 |
|
740 | 773 | PeerConnectionFactory pcFactory = stateProvider.getPeerConnectionFactory(); |
741 | 774 | VideoSource videoSource = pcFactory.createVideoSource(false); |
| 775 | + |
| 776 | + vbVideoSource = videoSource; |
| 777 | + |
742 | 778 | String threadName = Thread.currentThread().getName() + "_texture_camera_thread"; |
743 | 779 | SurfaceTextureHelper surfaceTextureHelper = |
744 | 780 | SurfaceTextureHelper.create(threadName, EglUtils.getRootEglBaseContext()); |
@@ -802,6 +838,221 @@ private ConstraintsMap getUserVideo(ConstraintsMap constraints, MediaStream medi |
802 | 838 | return trackParams; |
803 | 839 | } |
804 | 840 |
|
| 841 | + void setVirtualBackground() { |
| 842 | + vbVideoSource.setVideoProcessor(new VideoProcessor() { |
| 843 | + @Override |
| 844 | + public void onCapturerStarted(boolean success) { |
| 845 | + // Xử lý khi bắt đầu capture video |
| 846 | + } |
| 847 | + |
| 848 | + @Override |
| 849 | + public void onCapturerStopped() { |
| 850 | + // Xử lý khi dừng capture video |
| 851 | + } |
| 852 | + |
| 853 | + @Override |
| 854 | + public void onFrameCaptured(VideoFrame frame) { |
| 855 | + // Chuyển đổi frame thành bitmap |
| 856 | + Bitmap bitmap = videoFrameToBitmap(frame); |
| 857 | + |
| 858 | + // Xử lý segment với bitmap |
| 859 | + processSegmentation(bitmap); |
| 860 | + } |
| 861 | + |
| 862 | + @Override |
| 863 | + public void setSink(VideoSink sink) { |
| 864 | + // Lưu sink để gửi frame đã được cập nhật trở lại WebRTC |
| 865 | + // Sink sẽ được sử dụng sau khi xử lý segment |
| 866 | + vbVideoSink = sink; |
| 867 | + } |
| 868 | + }); |
| 869 | + } |
| 870 | + |
| 871 | + public Bitmap videoFrameToBitmap(VideoFrame videoFrame) { |
| 872 | + VideoFrame.Buffer buffer = videoFrame.getBuffer(); |
| 873 | + int width = buffer.getWidth(); |
| 874 | + int height = buffer.getHeight(); |
| 875 | + |
| 876 | + if (buffer instanceof VideoFrame.TextureBuffer) { |
| 877 | + // Không hỗ trợ trực tiếp chuyển đổi từ TextureBuffer sang Bitmap |
| 878 | + return null; |
| 879 | + } else if (buffer instanceof VideoFrame.I420Buffer) { |
| 880 | + VideoFrame.I420Buffer i420Buffer = (VideoFrame.I420Buffer) buffer; |
| 881 | + |
| 882 | + int ySize = width * height; |
| 883 | + int uvSize = width * height / 4; |
| 884 | + |
| 885 | + ByteBuffer dataY = i420Buffer.getDataY(); |
| 886 | + ByteBuffer dataU = i420Buffer.getDataU(); |
| 887 | + ByteBuffer dataV = i420Buffer.getDataV(); |
| 888 | + |
| 889 | + byte[] dataYArray = new byte[ySize]; |
| 890 | + byte[] dataUArray = new byte[uvSize]; |
| 891 | + byte[] dataVArray = new byte[uvSize]; |
| 892 | + |
| 893 | + dataY.get(dataYArray); |
| 894 | + dataU.get(dataUArray); |
| 895 | + dataV.get(dataVArray); |
| 896 | + |
| 897 | + // Chuyển đổi từ YUV sang RGB |
| 898 | + int[] rgbData = convertYUVtoRGB(dataYArray, dataUArray, dataVArray, width, height); |
| 899 | + |
| 900 | + // Tạo Bitmap từ dữ liệu RGB |
| 901 | + Bitmap bitmap = Bitmap.createBitmap(rgbData, width, height, Bitmap.Config.ARGB_8888); |
| 902 | + |
| 903 | + return bitmap; |
| 904 | + } |
| 905 | + |
| 906 | + return null; |
| 907 | + } |
| 908 | + |
| 909 | + private int[] convertYUVtoRGB(byte[] yData, byte[] uData, byte[] vData, int width, int height) { |
| 910 | + int[] rgbData = new int[width * height]; |
| 911 | + int uvIndex = 0; |
| 912 | + int yOffset = 0; |
| 913 | + |
| 914 | + for (int y = 0; y < height; y++) { |
| 915 | + int uvRowStart = uvIndex; |
| 916 | + int uvRowOffset = y >> 1; |
| 917 | + |
| 918 | + for (int x = 0; x < width; x++) { |
| 919 | + int yIndex = yOffset + x; |
| 920 | + int uvIndexOffset = uvRowStart + (x >> 1); |
| 921 | + |
| 922 | + int yValue = yData[yIndex] & 0xFF; |
| 923 | + int uValue = uData[uvIndexOffset] & 0xFF; |
| 924 | + int vValue = vData[uvIndexOffset] & 0xFF; |
| 925 | + |
| 926 | + int r = yValue + (int) (1.370705f * (vValue - 128)); |
| 927 | + int g = yValue - (int) (0.698001f * (vValue - 128)) - (int) (0.337633f * (uValue - 128)); |
| 928 | + int b = yValue + (int) (1.732446f * (uValue - 128)); |
| 929 | + |
| 930 | + r = Math.max(0, Math.min(255, r)); |
| 931 | + g = Math.max(0, Math.min(255, g)); |
| 932 | + b = Math.max(0, Math.min(255, b)); |
| 933 | + |
| 934 | + int pixelColor = 0xFF000000 | (r << 16) | (g << 8) | b; |
| 935 | + rgbData[y * width + x] = pixelColor; |
| 936 | + } |
| 937 | + |
| 938 | + if (y % 2 == 1) { |
| 939 | + uvIndex = uvRowStart + width / 2; |
| 940 | + yOffset += width; |
| 941 | + } |
| 942 | + } |
| 943 | + |
| 944 | + return rgbData; |
| 945 | + } |
| 946 | + |
| 947 | + private void processSegmentation(Bitmap bitmap) { |
| 948 | + // Tạo InputImage từ bitmap |
| 949 | + InputImage inputImage = InputImage.fromBitmap(bitmap, 0); |
| 950 | + |
| 951 | + // Xử lý phân đoạn |
| 952 | + segmenter.process(inputImage) |
| 953 | + .addOnSuccessListener(new OnSuccessListener<SegmentationMask>() { |
| 954 | + @Override |
| 955 | + public void onSuccess(@NonNull SegmentationMask segmentationMask) { |
| 956 | + // Xử lý khi phân đoạn thành công |
| 957 | + ByteBuffer mask = segmentationMask.getBuffer(); |
| 958 | + int maskWidth = segmentationMask.getWidth(); |
| 959 | + int maskHeight = segmentationMask.getHeight(); |
| 960 | + mask.rewind(); |
| 961 | + |
| 962 | + // Chuyển đổi buffer thành mảng màu |
| 963 | + int[] colors = maskColorsFromByteBuffer(mask, maskWidth, maskHeight); |
| 964 | + |
| 965 | + // Tạo bitmap đã được phân đoạn từ mảng màu |
| 966 | + Bitmap segmentedBitmap = createBitmapFromColors(colors, maskWidth, maskHeight); |
| 967 | + |
| 968 | + // Vẽ ảnh nền đã phân đoạn lên canvas |
| 969 | + Bitmap outputBitmap = drawSegmentedBackground(segmentedBitmap, segmentedBitmap); |
| 970 | + |
| 971 | + // Tạo VideoFrame mới từ bitmap đã xử lý |
| 972 | + int frameRotation = 180; // Frame rotation angle (customize as needed) |
| 973 | + long frameTimestamp = System.nanoTime(); // Frame timestamp (customize as needed) |
| 974 | + VideoFrame outputVideoFrame = createVideoFrame(outputBitmap, frameRotation, frameTimestamp); |
| 975 | + |
| 976 | + // Gửi frame đã được cập nhật trở lại WebRTC |
| 977 | + vbVideoSink.onFrame(outputVideoFrame); |
| 978 | + } |
| 979 | + }) |
| 980 | + .addOnFailureListener(new OnFailureListener() { |
| 981 | + @Override |
| 982 | + public void onFailure(@NonNull Exception exception) { |
| 983 | + // Xử lý khi phân đoạn thất bại |
| 984 | + Log.e(TAG, "Segmentation failed: " + exception.getMessage()); |
| 985 | + } |
| 986 | + }); |
| 987 | + } |
| 988 | + |
| 989 | + private Bitmap drawSegmentedBackground(Bitmap segmentedBitmap, Bitmap backgroundBitmap) { |
| 990 | + Bitmap outputBitmap = Bitmap.createBitmap( |
| 991 | + segmentedBitmap.getWidth(), segmentedBitmap.getHeight(), Bitmap.Config.ARGB_8888 |
| 992 | + ); |
| 993 | + Canvas canvas = new Canvas(outputBitmap); |
| 994 | + |
| 995 | + // Vẽ ảnh nền đã phân đoạn lên canvas |
| 996 | + canvas.drawBitmap(backgroundBitmap, 0, 0, null); |
| 997 | + canvas.drawBitmap(segmentedBitmap, 0, 0, null); |
| 998 | + |
| 999 | + return outputBitmap; |
| 1000 | + } |
| 1001 | + |
| 1002 | + private VideoFrame createVideoFrame(Bitmap bitmap, int rotation, long timestampNs) { |
| 1003 | + ByteBuffer buffer = ByteBuffer.allocate(bitmap.getByteCount()); |
| 1004 | + bitmap.copyPixelsToBuffer(buffer); |
| 1005 | + byte[] data = buffer.array(); |
| 1006 | + |
| 1007 | + int width = bitmap.getWidth(); |
| 1008 | + int height = bitmap.getHeight(); |
| 1009 | + int strideY = width; |
| 1010 | + int strideU = (width + 1) / 2; |
| 1011 | + int strideV = (width + 1) / 2; |
| 1012 | + |
| 1013 | + byte[] dataU = new byte[width * height / 4]; |
| 1014 | + byte[] dataV = new byte[width * height / 4]; |
| 1015 | + for (int i = 0; i < width * height / 4; i++) { |
| 1016 | + dataU[i] = data[width * height + i]; |
| 1017 | + dataV[i] = data[width * height + width * height / 4 + i]; |
| 1018 | + } |
| 1019 | + |
| 1020 | + Runnable releaseCallback = () -> { |
| 1021 | + // 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ó) |
| 1022 | + }; |
| 1023 | + |
| 1024 | + VideoFrame.I420Buffer i420Buffer = JavaI420Buffer.wrap( |
| 1025 | + width, |
| 1026 | + height, |
| 1027 | + ByteBuffer.wrap(data), |
| 1028 | + strideY, |
| 1029 | + ByteBuffer.wrap(dataU), |
| 1030 | + strideU, ByteBuffer.wrap(dataV), strideV, releaseCallback |
| 1031 | + ); |
| 1032 | + |
| 1033 | + return new VideoFrame(i420Buffer, rotation, timestampNs); |
| 1034 | + } |
| 1035 | + |
| 1036 | + |
| 1037 | + // Hàm chuyển đổi buffer thành mảng màu |
| 1038 | + private int[] maskColorsFromByteBuffer(ByteBuffer buffer, int width, int height) { |
| 1039 | + // Chuyển đổi từ ByteBuffer thành mảng màu, tùy thuộc vào định dạng màu |
| 1040 | + // của buffer. Đảm bảo bạn sử dụng đúng định dạng màu tương ứng với |
| 1041 | + // phân đoạn của ML Kit. |
| 1042 | + // Trong ví dụ này, chúng tôi giả định rằng buffer có định dạng ARGB_8888. |
| 1043 | + |
| 1044 | + // Ví dụ: chuyển đổi từ ByteBuffer thành mảng ARGB_8888 |
| 1045 | + int[] colors = new int[width * height]; |
| 1046 | + buffer.asIntBuffer().get(colors); |
| 1047 | + |
| 1048 | + return colors; |
| 1049 | + } |
| 1050 | + |
| 1051 | + // Hàm tạo bitmap từ mảng màu |
| 1052 | + private Bitmap createBitmapFromColors(int[] colors, int width, int height) { |
| 1053 | + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); |
| 1054 | + } |
| 1055 | + |
805 | 1056 | void removeVideoCapturerSync(String id) { |
806 | 1057 | synchronized (mVideoCapturers) { |
807 | 1058 | VideoCapturerInfo info = mVideoCapturers.get(id); |
|
0 commit comments