widget_recorder_plus
A powerful Flutter package to record any widget as a high-quality MP4 video. Perfect for creating tutorials, demos, animations, and exporting dynamic content with just a few lines of code.
Features
- Record Any Widget - Capture any Flutter widget as MP4 video
- Simple API - Just 3 lines to integrate
- Configurable FPS - 15-60 FPS (default 60)
- Cross-Platform - Android (API 21+) and iOS (13+)
- Auto File Management - No path management needed
- Built-in Callbacks - Success and error handling
- High Quality - Native H.264 codec with 10 Mbps/megapixel bitrate
- Smooth Encoding - Optimized for performance
- Proper Finalization - Ensures video files are always valid
Platform Support
| Platform | Min Version | Status |
|---|---|---|
| Android | API 21 (5.0) | Fully Supported |
| iOS | 13.0 | Fully Supported |
| Web | - | Not Supported |
| macOS | - | Not Supported |
| Windows | - | Not Supported |
| Linux | - | Not Supported |
Installation
Add to your pubspec.yaml:
dependencies:
widget_recorder_plus: ^1.0.0
Then run:
flutter pub get
Quick Start
1. Import the Package
import 'package:widget_recorder_plus/widget_recorder_plus.dart';
2. Create a Controller
final controller = WidgetRecorderController();
3. Wrap Your Widget
WidgetRecorder(
controller: controller,
child: YourWidget(),
)
4. Start Recording
// Start recording
await controller.start();
// Stop recording (returns file path)
final videoPath = await controller.stop();
Complete Example
import 'package:flutter/material.dart';
import 'package:widget_recorder/widget_recorder.dart';
import 'package:open_file/open_file.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late WidgetRecorderController controller;
bool isRecording = false;
@override
void initState() {
super.initState();
controller = WidgetRecorderController(
onComplete: (path) {
setState(() => isRecording = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Video saved: $path'),
action: SnackBarAction(
label: 'Open',
onPressed: () => OpenFile.open(path),
),
),
);
},
onError: (error) {
setState(() => isRecording = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $error'), backgroundColor: Colors.red),
);
},
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Future<void> toggleRecording() async {
if (isRecording) {
await controller.stop();
} else {
setState(() => isRecording = true);
await controller.start();
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Widget Recorder Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
WidgetRecorder(
controller: controller,
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(20),
),
child: const Center(
child: Text(
'Recording this!',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
),
const SizedBox(height: 40),
ElevatedButton.icon(
onPressed: toggleRecording,
icon: Icon(isRecording ? Icons.stop : Icons.videocam),
label: Text(isRecording ? 'Stop Recording' : 'Start Recording'),
),
],
),
),
),
);
}
}
API Reference
WidgetRecorderController
Main controller for managing widget recording.
Constructor
WidgetRecorderController({
Function(String path)? onComplete,
Function(String error)? onError,
})
Parameters:
onComplete- Called when recording finishes with the video file pathonError- Called when an error occurs during recording
Properties
// Set frames per second (15-60, default: 60)
controller.fps = 30;
// Check if currently recording
bool isRecording = controller.isRecording;
Methods
// Start recording (auto-generates file path in temp directory)
await controller.start();
// Stop recording (returns file path)
final path = await controller.stop();
// Clean up resources
controller.dispose();
WidgetRecorder Widget
Wrapper widget that enables recording for its child.
WidgetRecorder(
controller: controller, // Required
child: MyWidget(), // Required
)
Usage Examples
Recording Animations
class AnimatedDemo extends StatefulWidget {
@override
State<AnimatedDemo> createState() => _AnimatedDemoState();
}
class _AnimatedDemoState extends State<AnimatedDemo>
with SingleTickerProviderStateMixin {
late AnimationController animController;
late WidgetRecorderController recController;
@override
void initState() {
super.initState();
recController = WidgetRecorderController();
animController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
}
Future<void> recordAnimation() async {
await recController.start();
await animController.forward();
await recController.stop();
}
@override
Widget build(BuildContext context) {
return WidgetRecorder(
controller: recController,
child: AnimatedBuilder(
animation: animController,
builder: (context, child) {
return Transform.scale(
scale: 0.5 + (animController.value * 0.5),
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
),
);
},
),
);
}
@override
void dispose() {
animController.dispose();
recController.dispose();
super.dispose();
}
}
Adjusting Video Quality
// Lower FPS for smaller files (good for sharing)
controller.fps = 15;
// Medium quality (balanced)
controller.fps = 30;
// High quality (smooth animations)
controller.fps = 60;
Record for Specific Duration
Future<void> recordForSeconds(int seconds) async {
await controller.start();
await Future.delayed(Duration(seconds: seconds));
await controller.stop();
}
Toggle Recording
Future<void> toggleRecording() async {
if (controller.isRecording) {
await controller.stop();
} else {
await controller.start();
}
}
Permissions
Android
Add to android/app/src/main/AndroidManifest.xml (only if saving to external storage):
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
Note: By default, videos are saved to app-specific directories (no permission needed).
iOS
Add to ios/Runner/Info.plist (only if saving to Photos library):
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save recorded videos</string>
Note: By default, videos are saved to app-specific directories (no permission needed).
Performance Tips
| Tip | Benefit |
|---|---|
| Use FPS 15-24 | Smaller files, less CPU usage |
| Keep widget size <= 1080p | Better performance |
| Close background apps | More resources available |
| Test on real devices | Accurate performance metrics |
| Dispose controller | Prevents memory leaks |
Troubleshooting
Empty or corrupted video file
Solution: Always call controller.stop() to properly finalize the video. The file is only valid after stop completes.
Widget not captured
Solution: Ensure the widget is visible on screen during recording. The widget must be rendered to be captured.
Recording is laggy or drops frames
Solution: Reduce FPS: controller.fps = 15 or reduce widget complexity.
"Unsupported media" error when opening video
Solution: Ensure recording completed successfully. Wait for onComplete callback before accessing the file.
Permission denied error
Solution: Check AndroidManifest.xml and Info.plist configurations. Ensure app has necessary permissions.
Build fails on Android
Solution: Ensure Kotlin support is enabled. Update Android Gradle plugin to 7.0+.
Build fails on iOS
Solution: Ensure Swift support is enabled. Update iOS deployment target to 13.0+.
Limitations
- Audio is not captured (video only)
- Widget must be visible on screen during recording
- Platform views (WebView, MapView) may not capture correctly
- Not suitable for real-time streaming
- Maximum recommended widget size: 1920x1080
How It Works
- Dart Layer - Uses RepaintBoundary to capture widget frames as RGBA pixel data
- Frame Conversion - Converts RGBA to YUV420 (Android) or BGRA (iOS)
- Native Encoding - Uses MediaCodec (Android) or AVAssetWriter (iOS) for H.264 encoding
- MP4 Output - Creates valid MP4 video file with proper timestamps and finalization
Video Specifications
| Property | Value |
|---|---|
| Format | MP4 (MPEG-4) |
| Codec | H.264 (AVC) |
| Container | MP4 |
| Bitrate | 10 Mbps per megapixel |
| FPS | Configurable (15-60, default 60) |
| Audio | Not supported |
| Color Space | YUV420 (Android), BGRA (iOS) |
App Store Compliance
- Google Play Store - Compliant (in-app widget recording, not screen capture)
- Apple App Store - Compliant (internal rendering, not screen recording)
Always include appropriate privacy policy disclosures about video recording features in your app.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Check the example app for working code
- Report issues on GitHub
- Discuss on pub.dev
Changelog
See CHANGELOG.md for version history and updates.
Made with love for Flutter developers