stream-chat-react-native 9.0.0-beta.9 → 9.0.0
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/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/ios/shared/StreamVideoThumbnail.h +14 -0
- package/ios/shared/StreamVideoThumbnail.mm +56 -0
- package/ios/shared/StreamVideoThumbnailGenerator.swift +338 -0
- package/package.json +15 -12
- package/react-native.config.js +13 -0
- package/src/native/NativeStreamVideoThumbnail.ts +14 -0
- 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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
|
+
|
|
3
|
+
#if __has_include("StreamChatReactNativeSpec.h")
|
|
4
|
+
#import "StreamChatReactNativeSpec.h"
|
|
5
|
+
#elif __has_include("StreamChatExpoSpec.h")
|
|
6
|
+
#import "StreamChatExpoSpec.h"
|
|
7
|
+
#else
|
|
8
|
+
#error "Unable to find generated codegen spec header for StreamVideoThumbnail."
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
@interface StreamVideoThumbnail : NSObject <NativeStreamVideoThumbnailSpec>
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
#endif
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#import "StreamVideoThumbnail.h"
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
|
|
5
|
+
#if __has_include(<stream_chat_react_native/stream_chat_react_native-Swift.h>)
|
|
6
|
+
#import <stream_chat_react_native/stream_chat_react_native-Swift.h>
|
|
7
|
+
#elif __has_include(<stream_chat_expo/stream_chat_expo-Swift.h>)
|
|
8
|
+
#import <stream_chat_expo/stream_chat_expo-Swift.h>
|
|
9
|
+
#elif __has_include("stream_chat_react_native-Swift.h")
|
|
10
|
+
#import "stream_chat_react_native-Swift.h"
|
|
11
|
+
#elif __has_include("stream_chat_expo-Swift.h")
|
|
12
|
+
#import "stream_chat_expo-Swift.h"
|
|
13
|
+
#else
|
|
14
|
+
#error "Unable to import generated Swift header for StreamVideoThumbnail."
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
@implementation StreamVideoThumbnail
|
|
18
|
+
|
|
19
|
+
RCT_EXPORT_MODULE(StreamVideoThumbnail)
|
|
20
|
+
|
|
21
|
+
RCT_REMAP_METHOD(createVideoThumbnails, urls:(NSArray<NSString *> *)urls resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
22
|
+
{
|
|
23
|
+
[self createVideoThumbnails:urls resolve:resolve reject:reject];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
- (void)createVideoThumbnails:(NSArray<NSString *> *)urls
|
|
27
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
28
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
29
|
+
{
|
|
30
|
+
[StreamVideoThumbnailGenerator generateThumbnailsWithUrls:urls completion:^(NSArray<StreamVideoThumbnailResult *> *thumbnails) {
|
|
31
|
+
NSMutableArray<NSDictionary<NSString *, id> *> *payload = [NSMutableArray arrayWithCapacity:thumbnails.count];
|
|
32
|
+
|
|
33
|
+
for (StreamVideoThumbnailResult *thumbnail in thumbnails) {
|
|
34
|
+
NSMutableDictionary<NSString *, id> *entry = [NSMutableDictionary dictionaryWithCapacity:2];
|
|
35
|
+
entry[@"uri"] = thumbnail.uri ?: [NSNull null];
|
|
36
|
+
entry[@"error"] = thumbnail.error ?: [NSNull null];
|
|
37
|
+
[payload addObject:entry];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@try {
|
|
41
|
+
resolve(payload);
|
|
42
|
+
} @catch (NSException *exception) {
|
|
43
|
+
reject(@"stream_video_thumbnail_error", exception.reason, nil);
|
|
44
|
+
}
|
|
45
|
+
}];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
49
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
50
|
+
{
|
|
51
|
+
return std::make_shared<facebook::react::NativeStreamVideoThumbnailSpecJSI>(params);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@end
|
|
55
|
+
|
|
56
|
+
#endif
|