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.
@@ -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
  }
@@ -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