stream-chat-expo 9.5.1-beta.4 → 9.5.1-beta.6

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.
@@ -113,20 +113,13 @@ public final class StreamVideoThumbnailGenerator: NSObject {
113
113
  return outputURL.absoluteString
114
114
  }
115
115
 
116
- let asset = try await resolveAsset(url: url)
117
- let generator = AVAssetImageGenerator(asset: asset)
118
- generator.appliesPreferredTrackTransform = true
119
- generator.maximumSize = CGSize(width: maxDimension, height: maxDimension)
116
+ let image = try await resolveThumbnailImage(url: url)
120
117
 
121
- let requestedTime = thumbnailTime(for: asset)
118
+ guard let data = image.jpegData(compressionQuality: compressionQuality) else {
119
+ throw thumbnailError(code: 2, message: "Failed to encode JPEG thumbnail for \(url)")
120
+ }
122
121
 
123
122
  do {
124
- let cgImage = try generator.copyCGImage(at: requestedTime, actualTime: nil)
125
- let image = UIImage(cgImage: cgImage)
126
- guard let data = image.jpegData(compressionQuality: compressionQuality) else {
127
- throw thumbnailError(code: 2, message: "Failed to encode JPEG thumbnail for \(url)")
128
- }
129
-
130
123
  try data.write(to: outputURL, options: .atomic)
131
124
  return outputURL.absoluteString
132
125
  } catch {
@@ -134,6 +127,32 @@ public final class StreamVideoThumbnailGenerator: NSObject {
134
127
  }
135
128
  }
136
129
 
130
+ private static func resolveThumbnailImage(url: String) async throws -> UIImage {
131
+ if isPhotoLibraryURL(url) {
132
+ return try await resolvePhotoLibraryThumbnail(url: url)
133
+ }
134
+
135
+ if let normalizedURL = normalizeLocalURL(url) {
136
+ return try generateLocalVideoThumbnail(url: normalizedURL)
137
+ }
138
+
139
+ throw thumbnailError(code: 5, message: "Unsupported video URL for thumbnail generation: \(url)")
140
+ }
141
+
142
+ private static func generateLocalVideoThumbnail(url: URL) throws -> UIImage {
143
+ let asset = AVURLAsset(url: url)
144
+ let generator = AVAssetImageGenerator(asset: asset)
145
+ generator.appliesPreferredTrackTransform = true
146
+ generator.maximumSize = CGSize(width: maxDimension, height: maxDimension)
147
+
148
+ do {
149
+ let cgImage = try generator.copyCGImage(at: thumbnailTime(for: asset), actualTime: nil)
150
+ return UIImage(cgImage: cgImage)
151
+ } catch {
152
+ throw thumbnailError(error, code: 3, message: "Thumbnail generation failed for \(url.absoluteString)")
153
+ }
154
+ }
155
+
137
156
  private static func thumbnailCacheDirectory() throws -> URL {
138
157
  let outputDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
139
158
  .appendingPathComponent(cacheDirectoryName, isDirectory: true)
@@ -170,23 +189,11 @@ public final class StreamVideoThumbnailGenerator: NSObject {
170
189
  return .zero
171
190
  }
172
191
 
173
- private static func resolveAsset(url: String) async throws -> AVAsset {
174
- if isPhotoLibraryURL(url) {
175
- return try await resolvePhotoLibraryAsset(url: url)
176
- }
177
-
178
- if let normalizedURL = normalizeLocalURL(url) {
179
- return AVURLAsset(url: normalizedURL)
180
- }
181
-
182
- throw thumbnailError(code: 5, message: "Unsupported video URL for thumbnail generation: \(url)")
183
- }
184
-
185
192
  private static func isPhotoLibraryURL(_ url: String) -> Bool {
186
193
  url.lowercased().hasPrefix("ph://")
187
194
  }
188
195
 
189
- private static func resolvePhotoLibraryAsset(url: String) async throws -> AVAsset {
196
+ private static func resolvePhotoLibraryThumbnail(url: String) async throws -> UIImage {
190
197
  let identifier = photoLibraryIdentifier(from: url)
191
198
 
192
199
  guard !identifier.isEmpty else {
@@ -198,47 +205,65 @@ public final class StreamVideoThumbnailGenerator: NSObject {
198
205
  throw thumbnailError(code: 7, message: "Failed to find photo library asset for \(url)")
199
206
  }
200
207
 
201
- let options = PHVideoRequestOptions()
202
- options.deliveryMode = .highQualityFormat
208
+ // Request a thumbnail sized image instead of the full AVAsset: PhotoKit
209
+ // serves a locally cached thumbnail even for iCloud only assets, so this
210
+ // avoids downloading the entire video the way requestAVAsset(forVideo:) would.
211
+ let options = PHImageRequestOptions()
212
+ options.deliveryMode = .fastFormat
213
+ options.resizeMode = .fast
203
214
  options.isNetworkAccessAllowed = true
204
- options.version = .current
215
+ options.isSynchronous = false
205
216
 
206
- return try await withThrowingTaskGroup(of: AVAsset.self) { group in
217
+ let targetSize = CGSize(width: maxDimension, height: maxDimension)
218
+
219
+ return try await withThrowingTaskGroup(of: UIImage.self) { group in
207
220
  group.addTask {
208
- try await requestPhotoLibraryAsset(url: url, asset: asset, options: options)
221
+ try await requestPhotoLibraryImage(
222
+ url: url,
223
+ asset: asset,
224
+ targetSize: targetSize,
225
+ options: options
226
+ )
209
227
  }
210
228
  group.addTask {
211
229
  try await Task.sleep(nanoseconds: UInt64(photoLibraryAssetResolutionTimeout * 1_000_000_000))
212
230
  throw thumbnailError(
213
231
  code: 11,
214
- message: "Timed out resolving photo library asset for \(url)"
232
+ message: "Timed out resolving photo library thumbnail for \(url)"
215
233
  )
216
234
  }
217
235
 
218
- guard let resolvedAsset = try await group.next() else {
236
+ guard let image = try await group.next() else {
219
237
  throw thumbnailError(
220
238
  code: 10,
221
- message: "Failed to resolve photo library asset for \(url)"
239
+ message: "Failed to resolve photo library thumbnail for \(url)"
222
240
  )
223
241
  }
224
242
 
225
243
  group.cancelAll()
226
- return resolvedAsset
244
+ return image
227
245
  }
228
246
  }
229
247
 
230
- private static func requestPhotoLibraryAsset(
248
+ private static func requestPhotoLibraryImage(
231
249
  url: String,
232
250
  asset: PHAsset,
233
- options: PHVideoRequestOptions
234
- ) async throws -> AVAsset {
251
+ targetSize: CGSize,
252
+ options: PHImageRequestOptions
253
+ ) async throws -> UIImage {
235
254
  let imageManager = PHImageManager.default()
236
255
  let state = StreamPhotoLibraryAssetRequestState()
237
256
 
238
257
  return try await withTaskCancellationHandler(operation: {
239
258
  try await withCheckedThrowingContinuation { continuation in
240
- let requestID = imageManager.requestAVAsset(forVideo: asset, options: options) {
241
- avAsset, _, info in
259
+ let requestID = imageManager.requestImage(
260
+ for: asset,
261
+ targetSize: targetSize,
262
+ contentMode: .aspectFit,
263
+ options: options
264
+ ) { image, info in
265
+ // Accept the first delivered image (.fastFormat sends exactly one,
266
+ // thumbnail quality, possibly flagged degraded and that's what we want).
242
267
  state.lock.lock()
243
268
  if state.didResume {
244
269
  state.lock.unlock()
@@ -251,7 +276,7 @@ public final class StreamVideoThumbnailGenerator: NSObject {
251
276
  continuation.resume(
252
277
  throwing: thumbnailError(
253
278
  code: 8,
254
- message: "Photo library asset request was cancelled for \(url)"
279
+ message: "Photo library image request was cancelled for \(url)"
255
280
  )
256
281
  )
257
282
  return
@@ -262,23 +287,23 @@ public final class StreamVideoThumbnailGenerator: NSObject {
262
287
  throwing: thumbnailError(
263
288
  error,
264
289
  code: 9,
265
- message: "Photo library asset request failed for \(url)"
290
+ message: "Photo library image request failed for \(url)"
266
291
  )
267
292
  )
268
293
  return
269
294
  }
270
295
 
271
- guard let avAsset else {
296
+ guard let image else {
272
297
  continuation.resume(
273
298
  throwing: thumbnailError(
274
299
  code: 10,
275
- message: "Failed to resolve photo library asset for \(url)"
300
+ message: "Failed to resolve photo library thumbnail for \(url)"
276
301
  )
277
302
  )
278
303
  return
279
304
  }
280
305
 
281
- continuation.resume(returning: avAsset)
306
+ continuation.resume(returning: image)
282
307
  }
283
308
 
284
309
  state.lock.lock()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-expo",
3
3
  "description": "The official Expo SDK for Stream Chat, a service for building chat applications",
4
- "version": "9.5.1-beta.4",
4
+ "version": "9.5.1-beta.6",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -27,7 +27,7 @@
27
27
  "types": "types/index.d.ts",
28
28
  "dependencies": {
29
29
  "mime": "^4.0.7",
30
- "stream-chat-react-native-core": "9.5.1-beta.4"
30
+ "stream-chat-react-native-core": "9.5.1-beta.6"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "expo": ">=52.0.0",