flutter_web_record 0.1.2
flutter_web_record: ^0.1.2 copied to clipboard
A Flutter web package for screen recording with audio support, pause/resume controls, customizable UI, and preview functionality.
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
import 'dart:ui' as ui;
import 'package:flutter_web_record/flutter_web_record.dart';
void main() {
runApp(const ScreenRecorderTestApp());
}
class ScreenRecorderTestApp extends StatelessWidget {
const ScreenRecorderTestApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Screen Recorder Test',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const TestHomePage(),
);
}
}
class TestHomePage extends StatefulWidget {
const TestHomePage({super.key});
@override
State<TestHomePage> createState() => _TestHomePageState();
}
class _TestHomePageState extends State<TestHomePage> {
List<RecordingResult> _recordings = [];
String _status = 'Ready to record';
void _updateStatus(String status) {
setState(() {
_status = status;
});
}
Future<void> _testBasicRecording() async {
_updateStatus('Starting basic recording...');
try {
final result = await ScreenRecorder.startRecording(
context,
onRecordingComplete: (result) {
_updateStatus('✅ Basic recording complete!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar('Recording saved: ${result.fileName}');
},
onRecordingCancelled: () {
_updateStatus('❌ Recording cancelled');
},
);
if (result == null) {
_updateStatus('⚠️ No result returned');
}
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testWithPreview() async {
_updateStatus('Starting recording with preview...');
try {
await ScreenRecorder.startRecording(
context,
showPreview: true,
onRecordingComplete: (result) {
_updateStatus('✅ Recording with preview complete!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar('Preview confirmed and saved!');
},
onRecordingCancelled: () {
_updateStatus('❌ Recording cancelled or discarded');
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testCustomStyling() async {
_updateStatus('Starting recording with custom styling...');
try {
await ScreenRecorder.startRecording(
context,
indicatorConfig: RecordingIndicatorConfig(
recordingColor: Colors.purple,
pausedColor: Colors.amber,
backgroundColor: Colors.white.withValues(alpha: 0.95),
borderWidth: 3.0,
borderRadius: 20.0,
position: Alignment.topLeft,
timeTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
controlConfig: ControlButtonConfig(
pauseColor: Colors.amber,
stopColor: Colors.purple,
cancelColor: Colors.red,
buttonSize: 40.0,
iconSize: 24.0,
),
onRecordingComplete: (result) {
_updateStatus('✅ Custom styled recording complete!');
setState(() {
_recordings.add(result);
});
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testHighQuality() async {
_updateStatus('Starting high quality recording...');
try {
await ScreenRecorder.startRecording(
context,
recordingConfig: RecordingConfig(
idealWidth: 2560,
idealHeight: 1440,
idealFrameRate: 60,
videoBitsPerSecond: 8000000,
captureAudio: true,
showCursor: true,
),
onRecordingComplete: (result) {
_updateStatus('✅ High quality recording complete!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar(
'HQ: ${(result.fileBytes.length / 1024 / 1024).toStringAsFixed(2)} MB');
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testNoAudio() async {
_updateStatus('Starting recording without audio...');
try {
await ScreenRecorder.startRecording(
context,
recordingConfig: const RecordingConfig(
audioCaptureMode: AudioCaptureMode.none,
showCursor: false,
),
onRecordingComplete: (result) {
_updateStatus('✅ Silent recording complete!');
setState(() {
_recordings.add(result);
});
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testSystemAudio() async {
_updateStatus('Starting recording with system audio...');
try {
await ScreenRecorder.startRecording(
context,
recordingConfig: const RecordingConfig(
audioCaptureMode: AudioCaptureMode.system,
),
onRecordingComplete: (result) {
_updateStatus('✅ System audio recording complete!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar('System audio captured!');
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testMicrophoneAudio() async {
_updateStatus('Starting recording with microphone...');
try {
await ScreenRecorder.startRecording(
context,
recordingConfig: const RecordingConfig(
audioCaptureMode: AudioCaptureMode.microphone,
),
onRecordingComplete: (result) {
_updateStatus('✅ Microphone recording complete!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar('Microphone audio captured!');
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
Future<void> _testBothAudio() async {
_updateStatus('Starting recording with system + microphone...');
try {
await ScreenRecorder.startRecording(
context,
recordingConfig: const RecordingConfig(
audioCaptureMode: AudioCaptureMode.both,
),
onRecordingComplete: (result) {
_updateStatus('✅ Both audio sources recorded!');
setState(() {
_recordings.add(result);
});
_showSuccessSnackbar('System + Mic captured!');
},
);
} catch (e) {
_updateStatus('❌ Error: $e');
_showErrorSnackbar('Error: $e');
}
}
void _showSuccessSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
),
);
}
void _showErrorSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
void _clearRecordings() {
setState(() {
_recordings.clear();
_status = 'Ready to record';
});
}
List<Widget> _buildTestWidgets() {
return [
_buildTestButton(
'Test 1: Basic Recording',
'Test default recording with no customization',
Icons.videocam,
Colors.blue,
_testBasicRecording),
_buildTestButton(
'Test 2: With Preview',
'Test recording with preview dialog',
Icons.preview,
Colors.green,
_testWithPreview),
_buildTestButton(
'Test 3: Custom Styling',
'Test custom colors, sizes, and positions',
Icons.palette,
Colors.purple,
_testCustomStyling),
_buildTestButton('Test 4: High Quality', 'Test 1440p @ 60fps recording',
Icons.hd, Colors.orange, _testHighQuality),
_buildTestButton(
'Test 5: No Audio',
'Test recording without audio and cursor',
Icons.volume_off,
Colors.red,
_testNoAudio),
_buildTestButton(
'Test 6: System Audio',
'Record audio playing on your computer',
Icons.volume_up,
Colors.teal,
_testSystemAudio),
_buildTestButton('Test 7: Microphone', 'Record only microphone audio',
Icons.mic, Colors.indigo, _testMicrophoneAudio),
_buildTestButton(
'Test 8: System + Microphone',
'Record both system audio and microphone',
Icons.surround_sound,
Colors.deepPurple,
_testBothAudio),
];
}
@override
Widget build(BuildContext context) {
final testWidgets = _buildTestWidgets();
return Scaffold(
appBar: AppBar(
title: const Text('Screen Recorder Test Suite'),
actions: [
if (_recordings.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear_all),
tooltip: 'Clear all recordings',
onPressed: _clearRecordings),
],
),
body: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.blue.shade50,
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 12),
Expanded(
child: Text(_status,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w500))),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Test Cases',
style:
TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 4.5,
crossAxisSpacing: 12,
mainAxisSpacing: 8),
itemCount: testWidgets.length,
itemBuilder: (context, index) => testWidgets[index],
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Recordings (${_recordings.length})',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
if (_recordings.isNotEmpty)
Text('Total: ${_getTotalSize()} MB',
style:
TextStyle(fontSize: 14, color: Colors.grey[600])),
],
),
const SizedBox(height: 16),
if (_recordings.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(
'No recordings yet.\nTry recording something!',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey))))
else
..._recordings.asMap().entries.map(
(entry) => _buildRecordingCard(entry.key, entry.value)),
],
),
),
],
),
);
}
Widget _buildTestButton(String title, String description, IconData icon,
Color color, VoidCallback onPressed) {
return Card(
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: color, size: 24)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(description,
style:
TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
),
const Icon(Icons.play_arrow, size: 20),
],
),
),
),
);
}
Widget _buildRecordingCard(int index, RecordingResult recording) {
final sizeInMB =
(recording.fileBytes.length / 1024 / 1024).toStringAsFixed(2);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue, child: Text('${index + 1}')),
title: Text(recording.fileName),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Duration: ${recording.durationSeconds}s'),
Text('Size: $sizeInMB MB')
]),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon:
const Icon(Icons.play_circle_outline, color: Colors.green),
tooltip: 'Play video',
onPressed: () => _showVideoPlayer(recording)),
IconButton(
icon: const Icon(Icons.info_outline),
tooltip: 'Show info',
onPressed: () => _showRecordingInfo(recording)),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
tooltip: 'Delete',
onPressed: () => setState(() => _recordings.removeAt(index))),
],
),
),
);
}
void _showVideoPlayer(RecordingResult recording) {
showDialog(
context: context,
builder: (context) => VideoPlayerDialog(recording: recording));
}
void _showRecordingInfo(RecordingResult recording) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Recording Info'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('File Name:', recording.fileName),
_buildInfoRow('Duration:', '${recording.durationSeconds} seconds'),
_buildInfoRow('Size:',
'${(recording.fileBytes.length / 1024 / 1024).toStringAsFixed(2)} MB'),
_buildInfoRow('MIME Type:', recording.mimeType),
_buildInfoRow('Bytes:', '${recording.fileBytes.length}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(label,
style: const TextStyle(fontWeight: FontWeight.bold))),
Expanded(child: Text(value)),
],
),
);
}
String _getTotalSize() {
final totalBytes = _recordings.fold<int>(
0, (sum, recording) => sum + recording.fileBytes.length);
return (totalBytes / 1024 / 1024).toStringAsFixed(2);
}
}
class VideoPlayerDialog extends StatefulWidget {
final RecordingResult recording;
const VideoPlayerDialog({super.key, required this.recording});
@override
State<VideoPlayerDialog> createState() => _VideoPlayerDialogState();
}
class _VideoPlayerDialogState extends State<VideoPlayerDialog> {
late String _videoViewType;
@override
void initState() {
super.initState();
_videoViewType = 'video-player-${DateTime.now().millisecondsSinceEpoch}';
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_videoViewType, (int viewId) {
final videoElement = web.HTMLVideoElement()
..src = widget.recording.blobUrl
..controls = true
..autoplay = false
..style.width = '100%'
..style.height = '100%'
..style.objectFit = 'contain'
..style.border = 'none'
..style.backgroundColor = 'black';
return videoElement;
});
}
@override
Widget build(BuildContext context) {
final sizeInMB =
(widget.recording.fileBytes.length / 1024 / 1024).toStringAsFixed(2);
return Dialog(
backgroundColor: Colors.black,
child: Container(
constraints: const BoxConstraints(maxWidth: 900, maxHeight: 700),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[900],
child: Row(
children: [
const Icon(Icons.videocam, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.recording.fileName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
Text(
'${widget.recording.durationSeconds}s • $sizeInMB MB',
style: TextStyle(
color: Colors.grey[400], fontSize: 12)),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop()),
],
),
),
Expanded(
child: Container(
color: Colors.black,
child: HtmlElementView(viewType: _videoViewType))),
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[900],
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video URL: Available for download'),
duration: Duration(seconds: 2))),
icon: const Icon(Icons.link),
label: const Text('Copy Link'),
style: TextButton.styleFrom(foregroundColor: Colors.white),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.check),
label: const Text('Done'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white),
),
],
),
),
],
),
),
);
}
}