feat(thumb): add stream_thumbnail plugin - #133
Conversation
A Flutter plugin for creating a thumbnail from a local video file or from a video URL, supporting Android, iOS, and web. The iOS implementation supports both Swift Package Manager and CocoaPods. - Public Dart API: `StreamVideoThumbnail` (thumbnailData/File/Files) and `StreamThumbnailFormat`, exposed via a single barrel. - Register the package in the workspace: add `plugin_platform_interface` to shared dependencies and ignore SPM build artifacts. - Unit tests covering the API surface, method-channel encoding, and the format index contract. - Example app (Android/iOS/web) demonstrating thumbnail generation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds the ChangesVideo thumbnail plugin
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant StreamThumbnail
participant PlatformImplementation
participant VideoDecoder
participant ImageOutput
App->>StreamThumbnail: Request thumbnail
StreamThumbnail->>PlatformImplementation: Forward video and options
PlatformImplementation->>VideoDecoder: Load video and capture frame
VideoDecoder-->>PlatformImplementation: Video frame
PlatformImplementation->>ImageOutput: Encode or save thumbnail
ImageOutput-->>PlatformImplementation: Bytes or file path
PlatformImplementation-->>StreamThumbnail: Thumbnail result
StreamThumbnail-->>App: Uint8List or XFile
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #133 +/- ##
==========================================
+ Coverage 44.53% 44.60% +0.06%
==========================================
Files 178 179 +1
Lines 7243 7252 +9
==========================================
+ Hits 3226 3235 +9
Misses 4017 4017 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/stream_video_thumbnail/android/src/main/AndroidManifest.xml`:
- Around line 1-3: Remove the package attribute from the manifest element in
AndroidManifest.xml, leaving the existing android namespace declaration intact
and relying on the module namespace configured in build.gradle.kts.
In
`@packages/stream_video_thumbnail/android/src/main/kotlin/io/getstream/stream_video_thumbnail/StreamVideoThumbnailPlugin.kt`:
- Around line 50-51: Update the method handling in StreamVideoThumbnailPlugin so
callId is extracted only inside request branches that provide it, rather than
unconditionally after reading arguments. Ensure arg-less methods such as
getPlatformVersion do not cast null or missing arguments, while preserving
callId handling for branches that require it.
- Around line 154-158: Update the exception handling around
buildThumbnailFile/buildThumbnailData in the files batch loop to catch the
decode failure represented by non-IOException exceptions, including
NullPointerException, and continue processing remaining videos. Preserve the
existing skip behavior and final onResult("result#files", callId, results)
response.
- Around line 40-46: Update onAttachedToEngine in StreamVideoThumbnailPlugin to
assign the FlutterPluginBinding applicationContext to the plugin’s context field
before handling method calls. Preserve the existing MethodChannel setup while
ensuring buildThumbnailFile and contentResolver access use the initialized
context.
In
`@packages/stream_video_thumbnail/example/android/app/src/profile/AndroidManifest.xml`:
- Around line 1-7: Add the android.permission.INTERNET uses-permission entry to
the example app’s main AndroidManifest.xml, matching the existing declaration in
the profile manifest, so release builds can fetch the remote sample video.
In
`@packages/stream_video_thumbnail/ios/stream_video_thumbnail/Sources/stream_video_thumbnail/StreamVideoThumbnailPlugin.m`:
- Around line 107-112: Update the thumbnail generation guard after
copyCGImageAtTime to return nil when either error is non-nil or cgImage is NULL,
preventing subsequent CGImageGetColorSpace and UIImage imageWithCGImage calls
from receiving a null image.
- Around line 126-180: Fix ownership handling in the WebP conversion flow:
remove CGColorSpaceRelease and CGDataProviderRelease for the borrowed refs
returned by CGImageGetColorSpace and CGImageGetDataProvider, and ensure cgImage
is released on successful completion. After creating the NSData result, release
the WebP encoder buffer with WebPFree before returning, while preserving
existing failure cleanup.
In
`@packages/stream_video_thumbnail/lib/src/stream_video_thumbnail_method_channel.dart`:
- Around line 134-155: Update thumbnailFiles, thumbnailFile, and thumbnailData
to clean up the callId entry from _futures when methodChannel.invokeMethod
throws before native acknowledgement. Wrap each invocation and existing result
handling with exception cleanup, then rethrow the original exception; preserve
the current successful response and completer.future behavior.
- Around line 91-98: Update _createCompleterAndCallId to generate call IDs from
a monotonic counter rather than completer.hashCode, incrementing the counter for
each request before storing the completer in _futures. Preserve the existing
completer creation, registration, and tuple return behavior.
In `@packages/stream_video_thumbnail/lib/src/stream_video_thumbnail.dart`:
- Around line 51-73: Replace the debug-only assert checks for empty video input
in the thumbnailFile and thumbnailData public APIs with explicit ArgumentError
validation that always executes. Preserve the existing platform delegation for
valid non-empty video values and update the corresponding empty-video test
expectations to assert ArgumentError.
In `@packages/stream_video_thumbnail/lib/stream_video_thumbnail_web.dart`:
- Around line 130-260: The _createThumbnail completer can remain unresolved when
video loading or seeking produces no terminal event. Add a timeout guard around
the completer future that completes it with an appropriate thumbnail-generation
error, while preserving successful loaded/seeked completion and existing error
handling; ensure the timeout is cancelled or ignored once the completer has
completed.
- Around line 145-154: Update the loadedmetadata handler around
video.currentTime so the zero-offset case completes without relying on a seeked
event that may not fire. When timeSec is zero, resolve the pending operation
immediately (using the existing completion flow); preserve normal seeking and
cleanup behavior for nonzero offsets.
In `@packages/stream_video_thumbnail/test/stream_video_thumbnail_test.dart`:
- Around line 126-133: Update the empty video path test for
StreamVideoThumbnail.thumbnailData to expect an ArgumentError instead of an
AssertionError, matching the explicit validation used by
thumbnailData/thumbnailFile and ensuring the test remains valid in release
builds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: abc36140-56fc-4afe-8f37-bb6377c57d46
⛔ Files ignored due to path filters (32)
packages/stream_video_thumbnail/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedatapackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedis excluded by!**/Package.resolvedpackages/stream_video_thumbnail/example/ios/Runner.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedatapackages/stream_video_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolvedis excluded by!**/Package.resolvedpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/web/favicon.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/web/icons/Icon-192.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/web/icons/Icon-512.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/web/icons/Icon-maskable-192.pngis excluded by!**/*.pngpackages/stream_video_thumbnail/example/web/icons/Icon-maskable-512.pngis excluded by!**/*.png
📒 Files selected for processing (62)
.gitignoremelos.yamlpackages/stream_video_thumbnail/CHANGELOG.mdpackages/stream_video_thumbnail/LICENSEpackages/stream_video_thumbnail/README.mdpackages/stream_video_thumbnail/android/build.gradle.ktspackages/stream_video_thumbnail/android/gradle.propertiespackages/stream_video_thumbnail/android/gradle/wrapper/gradle-wrapper.propertiespackages/stream_video_thumbnail/android/settings.gradle.ktspackages/stream_video_thumbnail/android/src/main/AndroidManifest.xmlpackages/stream_video_thumbnail/android/src/main/kotlin/io/getstream/stream_video_thumbnail/StreamVideoThumbnailPlugin.ktpackages/stream_video_thumbnail/example/.metadatapackages/stream_video_thumbnail/example/README.mdpackages/stream_video_thumbnail/example/analysis_options.yamlpackages/stream_video_thumbnail/example/android/app/build.gradle.ktspackages/stream_video_thumbnail/example/android/app/src/debug/AndroidManifest.xmlpackages/stream_video_thumbnail/example/android/app/src/main/AndroidManifest.xmlpackages/stream_video_thumbnail/example/android/app/src/main/kotlin/io/getstream/stream_video_thumbnail_example/MainActivity.ktpackages/stream_video_thumbnail/example/android/app/src/main/res/drawable-v21/launch_background.xmlpackages/stream_video_thumbnail/example/android/app/src/main/res/drawable/launch_background.xmlpackages/stream_video_thumbnail/example/android/app/src/main/res/values-night/styles.xmlpackages/stream_video_thumbnail/example/android/app/src/main/res/values/styles.xmlpackages/stream_video_thumbnail/example/android/app/src/profile/AndroidManifest.xmlpackages/stream_video_thumbnail/example/android/build.gradle.ktspackages/stream_video_thumbnail/example/android/gradle.propertiespackages/stream_video_thumbnail/example/android/gradle/wrapper/gradle-wrapper.propertiespackages/stream_video_thumbnail/example/android/settings.gradle.ktspackages/stream_video_thumbnail/example/ios/Flutter/AppFrameworkInfo.plistpackages/stream_video_thumbnail/example/ios/Flutter/Debug.xcconfigpackages/stream_video_thumbnail/example/ios/Flutter/Release.xcconfigpackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/project.pbxprojpackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistpackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingspackages/stream_video_thumbnail/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcschemepackages/stream_video_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistpackages/stream_video_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingspackages/stream_video_thumbnail/example/ios/Runner/AppDelegate.swiftpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.jsonpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.jsonpackages/stream_video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.mdpackages/stream_video_thumbnail/example/ios/Runner/Base.lproj/LaunchScreen.storyboardpackages/stream_video_thumbnail/example/ios/Runner/Base.lproj/Main.storyboardpackages/stream_video_thumbnail/example/ios/Runner/Info.plistpackages/stream_video_thumbnail/example/ios/Runner/Runner-Bridging-Header.hpackages/stream_video_thumbnail/example/ios/Runner/SceneDelegate.swiftpackages/stream_video_thumbnail/example/ios/RunnerTests/RunnerTests.swiftpackages/stream_video_thumbnail/example/lib/main.dartpackages/stream_video_thumbnail/example/pubspec.yamlpackages/stream_video_thumbnail/example/web/index.htmlpackages/stream_video_thumbnail/example/web/manifest.jsonpackages/stream_video_thumbnail/ios/stream_video_thumbnail.podspecpackages/stream_video_thumbnail/ios/stream_video_thumbnail/Package.swiftpackages/stream_video_thumbnail/ios/stream_video_thumbnail/Sources/stream_video_thumbnail/StreamVideoThumbnailPlugin.mpackages/stream_video_thumbnail/ios/stream_video_thumbnail/Sources/stream_video_thumbnail/include/stream_video_thumbnail/StreamVideoThumbnailPlugin.hpackages/stream_video_thumbnail/lib/src/stream_thumbnail_format.dartpackages/stream_video_thumbnail/lib/src/stream_video_thumbnail.dartpackages/stream_video_thumbnail/lib/src/stream_video_thumbnail_method_channel.dartpackages/stream_video_thumbnail/lib/src/stream_video_thumbnail_platform.dartpackages/stream_video_thumbnail/lib/stream_video_thumbnail.dartpackages/stream_video_thumbnail/lib/stream_video_thumbnail_web.dartpackages/stream_video_thumbnail/pubspec.yamlpackages/stream_video_thumbnail/test/stream_video_thumbnail_test.dart
- Add a `thumb` scope to the conventional PR-title check and map it to packages/stream_video_thumbnail for the changelog verifier. - Exclude the platform-implementation layer (web, method channel, and platform-interface boilerplate) from coverage; it is verified end-to-end via the example app rather than unit tests. - Add a thumbnailFile unit test covering the public API facade. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Capture the application Context in the Android plugin's onAttachedToEngine (and clear it on detach) so content:// inputs and the cache-dir save path no longer NPE. - Use a monotonic call ID instead of Completer.hashCode in the method channel to avoid collisions (and hangs) under concurrent requests. - Guard against a nil cgImage (returned without an error) on iOS. - Add the INTERNET permission to the example app's main manifest so it can fetch the remote sample video. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops "video" from the name to avoid confusion with the StreamVideo product. Renames the Dart/native classes, files, method channel, and Android namespace accordingly. - Dart: StreamThumbnail / StreamThumbnailPlatform / MethodChannelStreamThumbnail / StreamThumbnailWeb; barrel is now package:stream_thumbnail/stream_thumbnail.dart. - iOS: StreamThumbnailPlugin; SPM package/product and podspec renamed; method channel plugins.getstream.io/stream_thumbnail. - Android: io.getstream.stream_thumbnail namespace + StreamThumbnailPlugin. - Update the CI changelog-scope path in pr_title.yml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the original `Copyright (c) 2019 John Zhong` notice alongside Stream's, as required by the MIT license for the forked sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rename the example stream_video_thumbnail_example -> stream_thumbnail_example (pubspec, Android namespace/package dir, iOS bundle id). - Trim example/pubspec.yaml to essentials (drop flutter-create boilerplate, cupertino_icons, and flutter_lints). - Remove example/analysis_options.yaml so it inherits the workspace config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart (1)
156-159: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle non-
trueresults safely.In the event that
invokeMethodreturnsnullor a non-Exceptionstring error whenresult != true, passing it directly to_resolveFuturewill cause a runtimeTypeError. This happens because_resolveFutureexpects a non-nullableObjectwhileCompleter<T>enforces generic type checks oncomplete(value).Wrap the non-
trueresult in anExceptionbefore resolving it to avoid crashes:
packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart#L156-L159: UpdatethumbnailFilesto safely wrapresult.packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart#L190-L193: UpdatethumbnailFileto safely wrapresult.packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart#L222-L225: UpdatethumbnailDatato safely wrapresult.🛠️ Proposed fix (apply to all sites)
if (result != true) { - _resolveFuture(callId, result); + _resolveFuture(callId, result is Exception ? result : Exception(result?.toString() ?? 'Unknown error')); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart` around lines 156 - 159, Update thumbnailFiles, thumbnailFile, and thumbnailData so every non-true invokeMethod result is converted to a non-null Exception before being passed to _resolveFuture, including null and string errors. Apply the same safe wrapping at packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart lines 156-159, 190-193, and 222-225.
🧹 Nitpick comments (2)
packages/stream_thumbnail/example/README.md (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the example project name with the renamed package. The PR objectives mention renaming the plugin from
stream_video_thumbnailtostream_thumbnail. The example project still uses the old name in several places; consider updating these to maintain consistency.
packages/stream_thumbnail/example/README.md#L1-L1: Update the title to# stream_thumbnail_example.packages/stream_thumbnail/example/android/app/build.gradle.kts#L7-L26: Update thenamespaceandapplicationIdtoio.getstream.stream_thumbnail_example.packages/stream_thumbnail/example/ios/Runner/Info.plist#L9-L20: UpdateCFBundleDisplayNametoStream Thumbnail ExampleandCFBundleNametostream_thumbnail_example.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_thumbnail/example/README.md` at line 1, Align the example project naming with the renamed stream_thumbnail package: update the title in packages/stream_thumbnail/example/README.md lines 1-1 to stream_thumbnail_example; update the namespace and applicationId in packages/stream_thumbnail/example/android/app/build.gradle.kts lines 7-26 to io.getstream.stream_thumbnail_example; and update CFBundleDisplayName and CFBundleName in packages/stream_thumbnail/example/ios/Runner/Info.plist lines 9-20 to Stream Thumbnail Example and stream_thumbnail_example.packages/stream_thumbnail/example/web/index.html (1)
26-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate leftover references to the old plugin name.
The plugin was renamed from
stream_video_thumbnailtostream_thumbnail, but the example app's web metadata still uses the old name. Consider updating these strings for consistency.
packages/stream_thumbnail/example/web/index.html#L26-L32: Change theapple-mobile-web-app-titleand<title>content tostream_thumbnail_example.packages/stream_thumbnail/example/web/manifest.json#L2-L3: Change thenameandshort_nametostream_thumbnail_example.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_thumbnail/example/web/index.html` around lines 26 - 32, Update the web metadata to use the renamed plugin identifier: in packages/stream_thumbnail/example/web/index.html lines 26-32, change both apple-mobile-web-app-title and title to stream_thumbnail_example; in packages/stream_thumbnail/example/web/manifest.json lines 2-3, change name and short_name to stream_thumbnail_example.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/stream_thumbnail/android/src/main/kotlin/io/getstream/stream_thumbnail/StreamThumbnailPlugin.kt`:
- Around line 108-112: Update onAttachedToEngine to recreate executor when the
existing instance is shut down, ensuring subsequent method calls use a live
executor after re-attachment. Keep the existing shutdown behavior in
onDetachedFromEngine unchanged.
- Around line 108-112: Reinitialize executor in onAttachedToEngine when the
existing executor is shut down, creating a fresh executor before handling
subsequent method calls. Preserve the existing shutdown behavior in
onDetachedFromEngine and avoid replacing an active executor.
In `@packages/stream_thumbnail/example/analysis_options.yaml`:
- Around line 12-26: Update the analysis configuration around the linter
settings in analysis_options.yaml to configure the project’s line width as 120
characters, using the appropriate formatter configuration key while preserving
the existing lint rules.
In `@packages/stream_thumbnail/example/ios/Runner.xcodeproj/project.pbxproj`:
- Line 63: Update the example Xcode project’s PBXFileReference path from
../../ios/stream_video_thumbnail to ../../ios/stream_thumbnail, and rename all
PRODUCT_BUNDLE_IDENTIFIER values using streamVideoThumbnailExample to the
corresponding streamThumbnailExample identifiers, including RunnerTests targets,
so no stale plugin name remains.
In
`@packages/stream_thumbnail/ios/stream_thumbnail/Sources/stream_thumbnail/StreamThumbnailPlugin.m`:
- Line 50: The file method must fulfill the thumbnailFile contract instead of
returning NSData directly. Update the generateThumbnail flow to write the
thumbnail data to a temporary file, then dispatch the Flutter result handling to
the main thread and use the established cross-platform file-result protocol
(including the corresponding Dart _resolveFuture handling) so the caller
receives an XFile-compatible result rather than a raw NSData or unsupported path
string.
- Line 50: The file method must fulfill the Dart file contract instead of
returning raw NSData. Update StreamThumbnailPlugin's thumbnail generation flow
to write the generated data to a temporary file, then dispatch the Flutter
result callback to the main thread and use the established cross-platform
file-result protocol (including the corresponding Dart _resolveFuture handling)
so thumbnailFile resolves an XFile rather than a Uint8List or unsupported
String.
---
Outside diff comments:
In `@packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart`:
- Around line 156-159: Update thumbnailFiles, thumbnailFile, and thumbnailData
so every non-true invokeMethod result is converted to a non-null Exception
before being passed to _resolveFuture, including null and string errors. Apply
the same safe wrapping at
packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart lines
156-159, 190-193, and 222-225.
---
Nitpick comments:
In `@packages/stream_thumbnail/example/README.md`:
- Line 1: Align the example project naming with the renamed stream_thumbnail
package: update the title in packages/stream_thumbnail/example/README.md lines
1-1 to stream_thumbnail_example; update the namespace and applicationId in
packages/stream_thumbnail/example/android/app/build.gradle.kts lines 7-26 to
io.getstream.stream_thumbnail_example; and update CFBundleDisplayName and
CFBundleName in packages/stream_thumbnail/example/ios/Runner/Info.plist lines
9-20 to Stream Thumbnail Example and stream_thumbnail_example.
In `@packages/stream_thumbnail/example/web/index.html`:
- Around line 26-32: Update the web metadata to use the renamed plugin
identifier: in packages/stream_thumbnail/example/web/index.html lines 26-32,
change both apple-mobile-web-app-title and title to stream_thumbnail_example; in
packages/stream_thumbnail/example/web/manifest.json lines 2-3, change name and
short_name to stream_thumbnail_example.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5505b57c-4e15-44ee-9ec3-818be53f743c
⛔ Files ignored due to path filters (32)
packages/stream_thumbnail/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedatapackages/stream_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedis excluded by!**/Package.resolvedpackages/stream_thumbnail/example/ios/Runner.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedatapackages/stream_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolvedis excluded by!**/Package.resolvedpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/web/favicon.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/web/icons/Icon-192.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/web/icons/Icon-512.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/web/icons/Icon-maskable-192.pngis excluded by!**/*.pngpackages/stream_thumbnail/example/web/icons/Icon-maskable-512.pngis excluded by!**/*.png
📒 Files selected for processing (61)
.github/workflows/pr_title.ymlpackages/stream_thumbnail/CHANGELOG.mdpackages/stream_thumbnail/LICENSEpackages/stream_thumbnail/README.mdpackages/stream_thumbnail/android/build.gradle.ktspackages/stream_thumbnail/android/gradle.propertiespackages/stream_thumbnail/android/gradle/wrapper/gradle-wrapper.propertiespackages/stream_thumbnail/android/settings.gradle.ktspackages/stream_thumbnail/android/src/main/AndroidManifest.xmlpackages/stream_thumbnail/android/src/main/kotlin/io/getstream/stream_thumbnail/StreamThumbnailPlugin.ktpackages/stream_thumbnail/example/.metadatapackages/stream_thumbnail/example/README.mdpackages/stream_thumbnail/example/analysis_options.yamlpackages/stream_thumbnail/example/android/app/build.gradle.ktspackages/stream_thumbnail/example/android/app/src/debug/AndroidManifest.xmlpackages/stream_thumbnail/example/android/app/src/main/AndroidManifest.xmlpackages/stream_thumbnail/example/android/app/src/main/kotlin/io/getstream/stream_video_thumbnail_example/MainActivity.ktpackages/stream_thumbnail/example/android/app/src/main/res/drawable-v21/launch_background.xmlpackages/stream_thumbnail/example/android/app/src/main/res/drawable/launch_background.xmlpackages/stream_thumbnail/example/android/app/src/main/res/values-night/styles.xmlpackages/stream_thumbnail/example/android/app/src/main/res/values/styles.xmlpackages/stream_thumbnail/example/android/app/src/profile/AndroidManifest.xmlpackages/stream_thumbnail/example/android/build.gradle.ktspackages/stream_thumbnail/example/android/gradle.propertiespackages/stream_thumbnail/example/android/gradle/wrapper/gradle-wrapper.propertiespackages/stream_thumbnail/example/android/settings.gradle.ktspackages/stream_thumbnail/example/ios/Flutter/AppFrameworkInfo.plistpackages/stream_thumbnail/example/ios/Flutter/Debug.xcconfigpackages/stream_thumbnail/example/ios/Flutter/Release.xcconfigpackages/stream_thumbnail/example/ios/Runner.xcodeproj/project.pbxprojpackages/stream_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistpackages/stream_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingspackages/stream_thumbnail/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcschemepackages/stream_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistpackages/stream_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingspackages/stream_thumbnail/example/ios/Runner/AppDelegate.swiftpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.jsonpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.jsonpackages/stream_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.mdpackages/stream_thumbnail/example/ios/Runner/Base.lproj/LaunchScreen.storyboardpackages/stream_thumbnail/example/ios/Runner/Base.lproj/Main.storyboardpackages/stream_thumbnail/example/ios/Runner/Info.plistpackages/stream_thumbnail/example/ios/Runner/Runner-Bridging-Header.hpackages/stream_thumbnail/example/ios/Runner/SceneDelegate.swiftpackages/stream_thumbnail/example/ios/RunnerTests/RunnerTests.swiftpackages/stream_thumbnail/example/lib/main.dartpackages/stream_thumbnail/example/pubspec.yamlpackages/stream_thumbnail/example/web/index.htmlpackages/stream_thumbnail/example/web/manifest.jsonpackages/stream_thumbnail/ios/stream_thumbnail.podspecpackages/stream_thumbnail/ios/stream_thumbnail/Package.swiftpackages/stream_thumbnail/ios/stream_thumbnail/Sources/stream_thumbnail/StreamThumbnailPlugin.mpackages/stream_thumbnail/ios/stream_thumbnail/Sources/stream_thumbnail/include/stream_thumbnail/StreamThumbnailPlugin.hpackages/stream_thumbnail/lib/src/stream_thumbnail.dartpackages/stream_thumbnail/lib/src/stream_thumbnail_format.dartpackages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dartpackages/stream_thumbnail/lib/src/stream_thumbnail_platform.dartpackages/stream_thumbnail/lib/stream_thumbnail.dartpackages/stream_thumbnail/lib/stream_thumbnail_web.dartpackages/stream_thumbnail/pubspec.yamlpackages/stream_thumbnail/test/stream_thumbnail_test.dart
… executor reuse - Android: recreate the thread-pool executor on engine re-attachment (add-to-app / engine restart) instead of submitting to a shut-down one. - iOS: deliver method-channel results on the main thread while keeping the heavy thumbnail work on a background queue. - thumbnailFile: wrap the path iOS returns in an XFile so the Future<XFile> contract holds (Android already replies via the 'result#file' callback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- iOS WebP path: stop over-releasing the get-refs colorSpace/dataProvider (double-free), release the owned cgImage, and free the libwebp output buffer via WebPFree — fixes memory leaks and a potential crash. - Dart: remove the pending completer from _futures if invokeMethod throws, so failed calls don't accumulate in memory. - Web: time out a stalled video and release the <video> element once the result settles, so a call can't hang (and retain the element) forever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Adds
stream_thumbnail, a Flutter plugin for creating a thumbnail from a local video file or a video URL. Supports Android, iOS, and web.Details
StreamThumbnail(thumbnailData/thumbnailFile/thumbnailFiles) and theStreamThumbnailFormatenum, exposed via a single barrel (package:stream_thumbnail/stream_thumbnail.dart).libwebp; results delivered on the main thread (work stays backgrounded).build.gradle.kts) with Flutter's Built-in Kotlin (no KGP applied); namespaceio.getstream.stream_thumbnail.<video>implementation viapackage:web, with a timeout guard so a stalled source can't hang.plugin_platform_interfaceadded to shared deps;thumbPR-title/changelog scope; SPM build artifacts gitignored.thumbnailFilepath. Example app (Android/iOS/web) demonstrating usage.Verification
Built and verified on:
flutter analyzeclean ·flutter testgreenNotes
get_video_thumbnail(MIT); theLICENSEretains the upstream copyright.StreamThumbnailPlugin. Oncestream_chat_flutterswapsget_thumbnail_video→stream_thumbnail, this resolves the duplicate-VideoThumbnailPlugin-symbol crash reported in stream-chat-flutter#2360.🤖 Generated with Claude Code