stream-chat-react-native 9.0.0-beta.3 → 9.0.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +30 -0
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNative.java +0 -153
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativeModule.java +2 -15
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +32 -0
- package/android/src/main/java/com/streamchatreactnative/shared/StreamVideoThumbnailGenerator.kt +159 -0
- package/android/src/newarch/com/streamchatreactnative/StreamVideoThumbnailModule.kt +50 -0
- package/android/src/oldarch/com/streamchatreactnative/StreamChatReactNative.java +1 -1
- package/ios/StreamChatReactNative.mm +16 -148
- package/ios/shared/StreamVideoThumbnail.h +14 -0
- package/ios/shared/StreamVideoThumbnail.mm +56 -0
- package/ios/shared/StreamVideoThumbnailGenerator.swift +338 -0
- package/package.json +10 -3
- package/react-native.config.js +13 -0
- package/src/handlers/compressImage.ts +0 -1
- package/src/native/NativeStreamChatReactNative.ts +0 -1
- package/src/native/NativeStreamVideoThumbnail.ts +14 -0
- package/src/native/index.tsx +0 -2
- package/src/native/types.ts +2 -0
- package/src/native/videoThumbnail.ts +8 -0
- package/src/optionalDependencies/Audio.ts +1 -1
- package/src/optionalDependencies/Sound.tsx +349 -43
- package/src/optionalDependencies/generateThumbnail.ts +8 -0
- package/src/optionalDependencies/getPhotos.ts +28 -12
- package/src/optionalDependencies/pickImage.ts +34 -9
- package/src/optionalDependencies/takePhoto.ts +7 -0
package/android/build.gradle
CHANGED
|
@@ -155,3 +155,33 @@ if (isNewArchitectureEnabled()) {
|
|
|
155
155
|
codegenJavaPackageName = "com.streamchatreactnative"
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
if (isNewArchitectureEnabled()) {
|
|
160
|
+
gradle.projectsEvaluated {
|
|
161
|
+
if (rootProject.ext.has("streamChatReactNativeCodegenHookInstalled")) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def androidAppProject = rootProject.subprojects.find { it.plugins.hasPlugin("com.android.application") }
|
|
166
|
+
if (androidAppProject == null) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def dependencyCodegenTasks = rootProject.subprojects
|
|
171
|
+
.findAll { it != androidAppProject }
|
|
172
|
+
.collect { it.tasks.findByName("generateCodegenArtifactsFromSchema") }
|
|
173
|
+
.findAll { it != null }
|
|
174
|
+
|
|
175
|
+
if (dependencyCodegenTasks.isEmpty()) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
rootProject.ext.set("streamChatReactNativeCodegenHookInstalled", true)
|
|
180
|
+
|
|
181
|
+
androidAppProject.tasks.matching { task ->
|
|
182
|
+
task.name.startsWith("configureCMake")
|
|
183
|
+
}.configureEach {
|
|
184
|
+
dependsOn(dependencyCodegenTasks)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -11,7 +11,6 @@ import android.net.Uri;
|
|
|
11
11
|
import android.os.Build;
|
|
12
12
|
import android.provider.MediaStore;
|
|
13
13
|
import android.util.Base64;
|
|
14
|
-
import android.util.Log;
|
|
15
14
|
|
|
16
15
|
import java.io.ByteArrayOutputStream;
|
|
17
16
|
import java.io.File;
|
|
@@ -33,108 +32,6 @@ public class StreamChatReactNative {
|
|
|
33
32
|
private final static String SCHEME_FILE = "file";
|
|
34
33
|
private final static String SCHEME_HTTP = "http";
|
|
35
34
|
private final static String SCHEME_HTTPS = "https";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// List of known EXIF tags we will be copying.
|
|
39
|
-
// Orientation, width, height, and some others are ignored
|
|
40
|
-
// TODO: Find any missing tag that might be useful
|
|
41
|
-
private final static String[] EXIF_TO_COPY_ROTATED = new String[]
|
|
42
|
-
{
|
|
43
|
-
ExifInterface.TAG_APERTURE_VALUE,
|
|
44
|
-
ExifInterface.TAG_MAX_APERTURE_VALUE,
|
|
45
|
-
ExifInterface.TAG_METERING_MODE,
|
|
46
|
-
ExifInterface.TAG_ARTIST,
|
|
47
|
-
ExifInterface.TAG_BITS_PER_SAMPLE,
|
|
48
|
-
ExifInterface.TAG_COMPRESSION,
|
|
49
|
-
ExifInterface.TAG_BODY_SERIAL_NUMBER,
|
|
50
|
-
ExifInterface.TAG_BRIGHTNESS_VALUE,
|
|
51
|
-
ExifInterface.TAG_CONTRAST,
|
|
52
|
-
ExifInterface.TAG_CAMERA_OWNER_NAME,
|
|
53
|
-
ExifInterface.TAG_COLOR_SPACE,
|
|
54
|
-
ExifInterface.TAG_COPYRIGHT,
|
|
55
|
-
ExifInterface.TAG_DATETIME,
|
|
56
|
-
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
57
|
-
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
58
|
-
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
|
|
59
|
-
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
|
|
60
|
-
ExifInterface.TAG_EXIF_VERSION,
|
|
61
|
-
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
|
|
62
|
-
ExifInterface.TAG_EXPOSURE_INDEX,
|
|
63
|
-
ExifInterface.TAG_EXPOSURE_MODE,
|
|
64
|
-
ExifInterface.TAG_EXPOSURE_TIME,
|
|
65
|
-
ExifInterface.TAG_EXPOSURE_PROGRAM,
|
|
66
|
-
ExifInterface.TAG_FLASH,
|
|
67
|
-
ExifInterface.TAG_FLASH_ENERGY,
|
|
68
|
-
ExifInterface.TAG_FOCAL_LENGTH,
|
|
69
|
-
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
|
|
70
|
-
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
71
|
-
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
|
|
72
|
-
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
|
|
73
|
-
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
|
|
74
|
-
ExifInterface.TAG_PLANAR_CONFIGURATION,
|
|
75
|
-
ExifInterface.TAG_F_NUMBER,
|
|
76
|
-
ExifInterface.TAG_GAIN_CONTROL,
|
|
77
|
-
ExifInterface.TAG_GAMMA,
|
|
78
|
-
ExifInterface.TAG_GPS_ALTITUDE,
|
|
79
|
-
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
|
80
|
-
ExifInterface.TAG_GPS_AREA_INFORMATION,
|
|
81
|
-
ExifInterface.TAG_GPS_DATESTAMP,
|
|
82
|
-
ExifInterface.TAG_GPS_DOP,
|
|
83
|
-
ExifInterface.TAG_GPS_LATITUDE,
|
|
84
|
-
ExifInterface.TAG_GPS_LATITUDE_REF,
|
|
85
|
-
ExifInterface.TAG_GPS_LONGITUDE,
|
|
86
|
-
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
|
87
|
-
ExifInterface.TAG_GPS_STATUS,
|
|
88
|
-
ExifInterface.TAG_GPS_DEST_BEARING,
|
|
89
|
-
ExifInterface.TAG_GPS_DEST_BEARING_REF,
|
|
90
|
-
ExifInterface.TAG_GPS_DEST_DISTANCE,
|
|
91
|
-
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
|
|
92
|
-
ExifInterface.TAG_GPS_DEST_LATITUDE,
|
|
93
|
-
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
|
|
94
|
-
ExifInterface.TAG_GPS_DEST_LONGITUDE,
|
|
95
|
-
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
|
|
96
|
-
ExifInterface.TAG_GPS_DIFFERENTIAL,
|
|
97
|
-
ExifInterface.TAG_GPS_IMG_DIRECTION,
|
|
98
|
-
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
|
|
99
|
-
ExifInterface.TAG_GPS_MAP_DATUM,
|
|
100
|
-
ExifInterface.TAG_GPS_MEASURE_MODE,
|
|
101
|
-
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
|
102
|
-
ExifInterface.TAG_GPS_SATELLITES,
|
|
103
|
-
ExifInterface.TAG_GPS_SPEED,
|
|
104
|
-
ExifInterface.TAG_GPS_SPEED_REF,
|
|
105
|
-
ExifInterface.TAG_GPS_STATUS,
|
|
106
|
-
ExifInterface.TAG_GPS_TIMESTAMP,
|
|
107
|
-
ExifInterface.TAG_GPS_TRACK,
|
|
108
|
-
ExifInterface.TAG_GPS_TRACK_REF,
|
|
109
|
-
ExifInterface.TAG_GPS_VERSION_ID,
|
|
110
|
-
ExifInterface.TAG_IMAGE_DESCRIPTION,
|
|
111
|
-
ExifInterface.TAG_IMAGE_UNIQUE_ID,
|
|
112
|
-
ExifInterface.TAG_ISO_SPEED,
|
|
113
|
-
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
|
|
114
|
-
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
|
|
115
|
-
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
116
|
-
ExifInterface.TAG_LENS_MAKE,
|
|
117
|
-
ExifInterface.TAG_LENS_MODEL,
|
|
118
|
-
ExifInterface.TAG_LENS_SERIAL_NUMBER,
|
|
119
|
-
ExifInterface.TAG_LENS_SPECIFICATION,
|
|
120
|
-
ExifInterface.TAG_LIGHT_SOURCE,
|
|
121
|
-
ExifInterface.TAG_MAKE,
|
|
122
|
-
ExifInterface.TAG_MAKER_NOTE,
|
|
123
|
-
ExifInterface.TAG_MODEL,
|
|
124
|
-
// ExifInterface.TAG_ORIENTATION, // removed
|
|
125
|
-
ExifInterface.TAG_SATURATION,
|
|
126
|
-
ExifInterface.TAG_SHARPNESS,
|
|
127
|
-
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
|
128
|
-
ExifInterface.TAG_SOFTWARE,
|
|
129
|
-
ExifInterface.TAG_SUBJECT_DISTANCE,
|
|
130
|
-
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
|
|
131
|
-
ExifInterface.TAG_SUBJECT_LOCATION,
|
|
132
|
-
ExifInterface.TAG_USER_COMMENT,
|
|
133
|
-
ExifInterface.TAG_WHITE_BALANCE
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
35
|
/**
|
|
139
36
|
* Resize the specified bitmap.
|
|
140
37
|
*/
|
|
@@ -263,55 +160,6 @@ public class StreamChatReactNative {
|
|
|
263
160
|
return file;
|
|
264
161
|
}
|
|
265
162
|
|
|
266
|
-
/**
|
|
267
|
-
* Attempts to copy exif info from one file to another. Note: orientation, width, and height
|
|
268
|
-
exif attributes are not copied since those are lost after image rotation.
|
|
269
|
-
|
|
270
|
-
* imageUri: original image URI as provided from JS
|
|
271
|
-
* dstPath: final image output path
|
|
272
|
-
* Returns true if copy was successful, false otherwise.
|
|
273
|
-
*/
|
|
274
|
-
public static boolean copyExif(Context context, Uri imageUri, String dstPath){
|
|
275
|
-
ExifInterface src = null;
|
|
276
|
-
ExifInterface dst = null;
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
|
|
280
|
-
File file = getFileFromUri(context, imageUri);
|
|
281
|
-
if (!file.exists()) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
src = new ExifInterface(file.getAbsolutePath());
|
|
286
|
-
dst = new ExifInterface(dstPath);
|
|
287
|
-
|
|
288
|
-
} catch (Exception ignored) {
|
|
289
|
-
Log.e("StreamChatReactNative::copyExif", "EXIF read failed", ignored);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if(src == null || dst == null){
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try{
|
|
297
|
-
|
|
298
|
-
for (String attr : EXIF_TO_COPY_ROTATED)
|
|
299
|
-
{
|
|
300
|
-
String value = src.getAttribute(attr);
|
|
301
|
-
if (value != null){
|
|
302
|
-
dst.setAttribute(attr, value);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
dst.saveAttributes();
|
|
306
|
-
|
|
307
|
-
} catch (Exception ignored) {
|
|
308
|
-
Log.e("StreamChatReactNative::copyExif", "EXIF copy failed", ignored);
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
163
|
/**
|
|
316
164
|
* Get orientation by reading Image metadata
|
|
317
165
|
*/
|
|
@@ -591,4 +439,3 @@ public class StreamChatReactNative {
|
|
|
591
439
|
return scaledImage;
|
|
592
440
|
}
|
|
593
441
|
}
|
|
594
|
-
|
|
@@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
|
|
|
4
4
|
import android.graphics.Bitmap;
|
|
5
5
|
import android.net.Uri;
|
|
6
6
|
import android.os.AsyncTask;
|
|
7
|
-
import android.util.Log;
|
|
8
|
-
|
|
9
7
|
import androidx.annotation.Nullable;
|
|
10
8
|
import androidx.annotation.NonNull;
|
|
11
9
|
|
|
@@ -36,7 +34,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
@ReactMethod
|
|
39
|
-
public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath,
|
|
37
|
+
public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise) {
|
|
40
38
|
WritableMap options = Arguments.createMap();
|
|
41
39
|
options.putString("mode", mode);
|
|
42
40
|
options.putBoolean("onlyScaleDown", onlyScaleDown);
|
|
@@ -46,7 +44,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
46
44
|
@Override
|
|
47
45
|
protected void doInBackgroundGuarded(Void... params) {
|
|
48
46
|
try {
|
|
49
|
-
Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath,
|
|
47
|
+
Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, options);
|
|
50
48
|
promise.resolve(response);
|
|
51
49
|
}
|
|
52
50
|
catch (IOException e) {
|
|
@@ -59,7 +57,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
59
57
|
@SuppressLint("LongLogTag")
|
|
60
58
|
private Object createResizedImageWithExceptions(String imagePath, int newWidth, int newHeight,
|
|
61
59
|
String compressFormatString, int quality, int rotation, String outputPath,
|
|
62
|
-
final boolean keepMeta,
|
|
63
60
|
final ReadableMap options) throws IOException {
|
|
64
61
|
|
|
65
62
|
Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
|
|
@@ -89,16 +86,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
89
86
|
response.putDouble("size", resizedImage.length());
|
|
90
87
|
response.putDouble("width", scaledImage.getWidth());
|
|
91
88
|
response.putDouble("height", scaledImage.getHeight());
|
|
92
|
-
|
|
93
|
-
// Copy file's metadata/exif info if required
|
|
94
|
-
if(keepMeta){
|
|
95
|
-
try{
|
|
96
|
-
StreamChatReactNative.copyExif(this.getReactApplicationContext(), imageUri, resizedImage.getAbsolutePath());
|
|
97
|
-
}
|
|
98
|
-
catch(Exception ignored){
|
|
99
|
-
Log.e("StreamChatReactNative::createResizedImageWithExceptions", "EXIF copy failed", ignored);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
89
|
} else {
|
|
103
90
|
throw new IOException("Error getting resized image path");
|
|
104
91
|
}
|
|
@@ -14,12 +14,18 @@ import java.util.List;
|
|
|
14
14
|
import java.util.Map;
|
|
15
15
|
|
|
16
16
|
public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
17
|
+
private static final String STREAM_VIDEO_THUMBNAIL_MODULE = "StreamVideoThumbnail";
|
|
17
18
|
|
|
18
19
|
@Nullable
|
|
19
20
|
@Override
|
|
20
21
|
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
|
21
22
|
if (name.equals(StreamChatReactNativeModule.NAME)) {
|
|
22
23
|
return new StreamChatReactNativeModule(reactContext);
|
|
24
|
+
} else if (name.equals(STREAM_VIDEO_THUMBNAIL_MODULE) && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
25
|
+
return createNewArchModule(
|
|
26
|
+
"com.streamchatreactnative.StreamVideoThumbnailModule",
|
|
27
|
+
reactContext
|
|
28
|
+
);
|
|
23
29
|
} else {
|
|
24
30
|
return null;
|
|
25
31
|
}
|
|
@@ -41,6 +47,17 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
|
41
47
|
false, // isCxxModule
|
|
42
48
|
isTurboModule // isTurboModule
|
|
43
49
|
));
|
|
50
|
+
moduleInfos.put(
|
|
51
|
+
STREAM_VIDEO_THUMBNAIL_MODULE,
|
|
52
|
+
new ReactModuleInfo(
|
|
53
|
+
STREAM_VIDEO_THUMBNAIL_MODULE,
|
|
54
|
+
STREAM_VIDEO_THUMBNAIL_MODULE,
|
|
55
|
+
false, // canOverrideExistingModule
|
|
56
|
+
false, // needsEagerInit
|
|
57
|
+
false, // hasConstants
|
|
58
|
+
false, // isCxxModule
|
|
59
|
+
isTurboModule // isTurboModule
|
|
60
|
+
));
|
|
44
61
|
return moduleInfos;
|
|
45
62
|
};
|
|
46
63
|
}
|
|
@@ -49,4 +66,19 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
|
49
66
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
50
67
|
return Collections.<ViewManager>singletonList(new StreamShimmerViewManager());
|
|
51
68
|
}
|
|
69
|
+
|
|
70
|
+
@Nullable
|
|
71
|
+
private NativeModule createNewArchModule(
|
|
72
|
+
String className,
|
|
73
|
+
ReactApplicationContext reactContext
|
|
74
|
+
) {
|
|
75
|
+
try {
|
|
76
|
+
Class<?> moduleClass = Class.forName(className);
|
|
77
|
+
return (NativeModule) moduleClass
|
|
78
|
+
.getConstructor(ReactApplicationContext.class)
|
|
79
|
+
.newInstance(reactContext);
|
|
80
|
+
} catch (Throwable ignored) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
52
84
|
}
|
package/android/src/main/java/com/streamchatreactnative/shared/StreamVideoThumbnailGenerator.kt
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
package com.streamchatreactnative.shared
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.media.MediaMetadataRetriever
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.util.concurrent.Executors
|
|
11
|
+
|
|
12
|
+
data class StreamVideoThumbnailResult(
|
|
13
|
+
val error: String? = null,
|
|
14
|
+
val uri: String? = null,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
object StreamVideoThumbnailGenerator {
|
|
18
|
+
private const val DEFAULT_COMPRESSION_QUALITY = 80
|
|
19
|
+
private const val DEFAULT_MAX_DIMENSION = 512
|
|
20
|
+
private const val CACHE_VERSION = "v1"
|
|
21
|
+
private const val CACHE_DIRECTORY_NAME = "@stream-io-stream-video-thumbnails"
|
|
22
|
+
private const val MAX_CONCURRENT_GENERATIONS = 5
|
|
23
|
+
|
|
24
|
+
fun generateThumbnails(context: Context, urls: List<String>): List<StreamVideoThumbnailResult> {
|
|
25
|
+
if (urls.size <= 1) {
|
|
26
|
+
return urls.map { url -> generateThumbnailResult(context, url) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
val parallelism = minOf(urls.size, MAX_CONCURRENT_GENERATIONS)
|
|
30
|
+
val executor = Executors.newFixedThreadPool(parallelism)
|
|
31
|
+
|
|
32
|
+
return try {
|
|
33
|
+
val tasks = urls.map { url ->
|
|
34
|
+
executor.submit<StreamVideoThumbnailResult> {
|
|
35
|
+
generateThumbnailResult(context, url)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
tasks.map { task -> task.get() }
|
|
39
|
+
} finally {
|
|
40
|
+
executor.shutdown()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun generateThumbnailResult(context: Context, url: String): StreamVideoThumbnailResult {
|
|
45
|
+
return try {
|
|
46
|
+
StreamVideoThumbnailResult(uri = generateThumbnail(context, url))
|
|
47
|
+
} catch (error: Throwable) {
|
|
48
|
+
StreamVideoThumbnailResult(
|
|
49
|
+
error = error.message ?: "Thumbnail generation failed for $url",
|
|
50
|
+
uri = null,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun generateThumbnail(context: Context, url: String): String {
|
|
56
|
+
val outputDirectory = File(context.cacheDir, CACHE_DIRECTORY_NAME).apply { mkdirs() }
|
|
57
|
+
val outputFile = File(outputDirectory, buildCacheFileName(url))
|
|
58
|
+
|
|
59
|
+
if (outputFile.isFile() && outputFile.length() > 0L) {
|
|
60
|
+
return Uri.fromFile(outputFile).toString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
val retriever = MediaMetadataRetriever()
|
|
64
|
+
|
|
65
|
+
return try {
|
|
66
|
+
setDataSource(retriever, context, url)
|
|
67
|
+
val thumbnail = extractThumbnailFrame(retriever, url)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
FileOutputStream(outputFile).use { stream ->
|
|
71
|
+
thumbnail.compress(Bitmap.CompressFormat.JPEG, DEFAULT_COMPRESSION_QUALITY, stream)
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
if (!thumbnail.isRecycled) {
|
|
75
|
+
thumbnail.recycle()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Uri.fromFile(outputFile).toString()
|
|
80
|
+
} catch (error: Throwable) {
|
|
81
|
+
throw IllegalStateException("Thumbnail generation failed for $url", error)
|
|
82
|
+
} finally {
|
|
83
|
+
try {
|
|
84
|
+
retriever.release()
|
|
85
|
+
} catch (_: Throwable) {
|
|
86
|
+
// Ignore cleanup failures.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fun extractThumbnailFrame(retriever: MediaMetadataRetriever, url: String): Bitmap {
|
|
92
|
+
if (Build.VERSION.SDK_INT >= 27) {
|
|
93
|
+
return retriever.getScaledFrameAtTime(
|
|
94
|
+
100000,
|
|
95
|
+
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
|
96
|
+
DEFAULT_MAX_DIMENSION,
|
|
97
|
+
DEFAULT_MAX_DIMENSION,
|
|
98
|
+
) ?: throw IllegalStateException("Failed to extract video frame for $url")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
val frame =
|
|
102
|
+
retriever.getFrameAtTime(100000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
103
|
+
?: throw IllegalStateException("Failed to extract video frame for $url")
|
|
104
|
+
val scaledFrame = scaleBitmap(frame)
|
|
105
|
+
|
|
106
|
+
if (scaledFrame != frame) {
|
|
107
|
+
frame.recycle()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return scaledFrame
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun buildCacheFileName(url: String): String {
|
|
114
|
+
val cacheKey =
|
|
115
|
+
fnv1a64("$CACHE_VERSION|$DEFAULT_MAX_DIMENSION|$DEFAULT_COMPRESSION_QUALITY|$url")
|
|
116
|
+
return "stream-video-thumbnail-$cacheKey.jpg"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun fnv1a64(value: String): String {
|
|
120
|
+
var hash = -0x340d631b8c46751fL
|
|
121
|
+
|
|
122
|
+
value.toByteArray(Charsets.UTF_8).forEach { byte ->
|
|
123
|
+
hash = hash xor (byte.toLong() and 0xff)
|
|
124
|
+
hash *= 0x100000001b3L
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return java.lang.Long.toUnsignedString(hash, 16)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun setDataSource(retriever: MediaMetadataRetriever, context: Context, url: String) {
|
|
131
|
+
val uri = Uri.parse(url)
|
|
132
|
+
val scheme = uri.scheme?.lowercase()
|
|
133
|
+
|
|
134
|
+
when {
|
|
135
|
+
scheme.isNullOrEmpty() -> retriever.setDataSource(url)
|
|
136
|
+
scheme == "content" || scheme == "file" -> retriever.setDataSource(context, uri)
|
|
137
|
+
else ->
|
|
138
|
+
throw IllegalArgumentException(
|
|
139
|
+
"Unsupported video URI scheme for thumbnail generation: $scheme. Local assets only.",
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private fun scaleBitmap(bitmap: Bitmap): Bitmap {
|
|
145
|
+
val width = bitmap.width
|
|
146
|
+
val height = bitmap.height
|
|
147
|
+
val largestDimension = maxOf(width, height)
|
|
148
|
+
|
|
149
|
+
if (largestDimension <= DEFAULT_MAX_DIMENSION) {
|
|
150
|
+
return bitmap
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
val scale = DEFAULT_MAX_DIMENSION.toFloat() / largestDimension.toFloat()
|
|
154
|
+
val targetWidth = (width * scale).toInt().coerceAtLeast(1)
|
|
155
|
+
val targetHeight = (height * scale).toInt().coerceAtLeast(1)
|
|
156
|
+
|
|
157
|
+
return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.streamchatreactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReadableArray
|
|
7
|
+
import com.streamchatreactnative.shared.StreamVideoThumbnailGenerator
|
|
8
|
+
import java.util.concurrent.Executors
|
|
9
|
+
|
|
10
|
+
class StreamVideoThumbnailModule(
|
|
11
|
+
reactContext: ReactApplicationContext,
|
|
12
|
+
) : NativeStreamVideoThumbnailSpec(reactContext) {
|
|
13
|
+
override fun getName(): String = NAME
|
|
14
|
+
|
|
15
|
+
override fun createVideoThumbnails(urls: ReadableArray, promise: Promise) {
|
|
16
|
+
val urlList = mutableListOf<String>()
|
|
17
|
+
for (index in 0 until urls.size()) {
|
|
18
|
+
urlList.add(urls.getString(index) ?: "")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
executor.execute {
|
|
22
|
+
try {
|
|
23
|
+
val thumbnails = StreamVideoThumbnailGenerator.generateThumbnails(reactApplicationContext, urlList)
|
|
24
|
+
val result = Arguments.createArray()
|
|
25
|
+
thumbnails.forEach { thumbnail ->
|
|
26
|
+
val thumbnailMap = Arguments.createMap()
|
|
27
|
+
if (thumbnail.uri != null) {
|
|
28
|
+
thumbnailMap.putString("uri", thumbnail.uri)
|
|
29
|
+
} else {
|
|
30
|
+
thumbnailMap.putNull("uri")
|
|
31
|
+
}
|
|
32
|
+
if (thumbnail.error != null) {
|
|
33
|
+
thumbnailMap.putString("error", thumbnail.error)
|
|
34
|
+
} else {
|
|
35
|
+
thumbnailMap.putNull("error")
|
|
36
|
+
}
|
|
37
|
+
result.pushMap(thumbnailMap)
|
|
38
|
+
}
|
|
39
|
+
promise.resolve(result)
|
|
40
|
+
} catch (error: Throwable) {
|
|
41
|
+
promise.reject("stream_video_thumbnail_error", error.message, error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
companion object {
|
|
47
|
+
const val NAME = "StreamVideoThumbnail"
|
|
48
|
+
private val executor = Executors.newCachedThreadPool()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -12,5 +12,5 @@ abstract class StreamChatReactNativeSpec extends ReactContextBaseJavaModule {
|
|
|
12
12
|
super(context);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath,
|
|
15
|
+
public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise);
|
|
16
16
|
}
|