Flutter & Firebase Storage: Seamless Video Uploads with Thumbnails
In today's multimedia-rich applications, enabling users to upload videos is a common and highly desired feature. Whether it's for social media, e-learning platforms, or content creation apps, providing a smooth upload experience is crucial. Furthermore, displaying a compelling thumbnail before the video starts playing significantly enhances user experience, offering a visual preview and reducing bandwidth usage on initial loads.
This article will guide you through building a Flutter application that leverages Firebase Storage to upload videos, generate high-quality thumbnails on the client side, and store both efficiently. We'll cover everything from setting up your Firebase project to implementing the Flutter code for video selection, thumbnail generation, and secure uploads.
Why Flutter and Firebase Storage?
- Flutter: A single codebase for beautiful, natively compiled applications on mobile, web, and desktop. Its rich widget set and declarative UI make complex UI tasks simpler.
- Firebase Storage: A powerful, secure, and scalable object storage service built for Google scale. It allows you to store and serve user-generated content like images, audio, and video directly from your client applications.
- Seamless Integration: Firebase provides excellent Flutter SDKs, making integration straightforward and efficient.
Prerequisites
Before we dive into the code, ensure you have the following:
- Flutter SDK installed and configured.
- A Google account.
- A basic understanding of Flutter and Dart.
1. Firebase Project Setup
- Create a Firebase Project:
- Go to the Firebase Console.
- Click "Add project" and follow the on-screen instructions.
- Add Your Flutter App to Firebase:
- In your Firebase project, click the "Android" icon and "iOS" icon (or "Web" if targeting web) to add your app.
- Follow the steps to register your app, download the
google-services.json(for Android) andGoogleService-Info.plist(for iOS) files, and place them in the correct directories (android/app/andios/Runner/respectively). - Add Firebase configuration to your Android and iOS project files as specified by Firebase.
- Enable Firebase Storage:
- In the Firebase Console, navigate to "Build" > "Storage".
- Click "Get started" and follow the prompts to set up your storage bucket. Choose a server location near your users.
- Firebase Storage Rules:
For initial development, you might relax the rules. Remember to secure your rules before deploying to production.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.auth != null; // Allow authenticated users to read/write // For testing, you might use: allow read, write: if true; } } }
2. Flutter Project Setup
First, create a new Flutter project if you haven't already:
flutter create video_upload_app
cd video_upload_app
Add Dependencies
Open your pubspec.yaml file and add the following dependencies:
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2 # Or the latest version
firebase_storage: ^11.5.6 # Or the latest version
image_picker: ^1.0.4 # For picking videos
video_thumbnail: ^0.5.3 # For generating thumbnails
path_provider: ^2.1.1 # For temporary file storage
uuid: ^4.2.2 # For unique file names
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
Run flutter pub get to fetch the new packages.
Initialize Firebase
Ensure Firebase is initialized in your main.dart. You'll need the firebase_options.dart file generated by the FlutterFire CLI, which you can get by running flutterfire configure after adding your app in the Firebase console.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // Make sure this file is generated
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Uploader',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const VideoUploadScreen(),
);
}
}
3. Implementing the Video Upload and Thumbnail Generation Logic
Now, let's create our VideoUploadScreen. This screen will handle picking a video, generating its thumbnail, and uploading both to Firebase Storage.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:path_provider/path_provider.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:uuid/uuid.dart';
class VideoUploadScreen extends StatefulWidget {
const VideoUploadScreen({super.key});
@override
State createState() => _VideoUploadScreenState();
}
class _VideoUploadScreenState extends State {
File? _videoFile;
File? _thumbnailFile;
String? _videoUrl;
String? _thumbnailUrl;
bool _isUploading = false;
double _uploadProgress = 0.0;
final ImagePicker _picker = ImagePicker();
final FirebaseStorage _storage = FirebaseStorage.instance;
final Uuid _uuid = const Uuid();
Future _pickVideo() async {
final XFile? pickedFile = await _picker.pickVideo(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_videoFile = File(pickedFile.path);
_thumbnailFile = null; // Clear previous thumbnail
_videoUrl = null;
_thumbnailUrl = null;
});
await _generateThumbnail(_videoFile!.path);
}
}
Future _generateThumbnail(String videoPath) async {
final tempDir = await getTemporaryDirectory();
final thumbnailPath = await VideoThumbnail.thumbnailFile(
video: videoPath,
thumbnailPath: '${tempDir.path}/temp_thumbnail.png',
imageFormat: ImageFormat.PNG,
maxHeight: 128, // Customize the height
quality: 75,
);
if (thumbnailPath != null) {
setState(() {
_thumbnailFile = File(thumbnailPath);
});
}
}
Future _uploadVideoAndThumbnail() async {
if (_videoFile == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a video first.')),
);
return;
}
setState(() {
_isUploading = true;
_uploadProgress = 0.0;
});
try {
final String videoFileName = 'videos/${_uuid.v4()}.mp4';
final String thumbnailFileName = 'thumbnails/${_uuid.v4()}.png';
// Upload video
UploadTask videoUploadTask = _storage.ref().child(videoFileName).putFile(_videoFile!);
videoUploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
setState(() {
_uploadProgress = (snapshot.bytesTransferred / snapshot.totalBytes);
});
});
final videoSnapshot = await videoUploadTask;
_videoUrl = await videoSnapshot.ref.getDownloadURL();
// Upload thumbnail
if (_thumbnailFile != null) {
UploadTask thumbnailUploadTask = _storage.ref().child(thumbnailFileName).putFile(_thumbnailFile!);
final thumbnailSnapshot = await thumbnailUploadTask;
_thumbnailUrl = await thumbnailSnapshot.ref.getDownloadURL();
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Video and thumbnail uploaded successfully!')),
);
// Clean up temporary files
_videoFile?.delete();
_thumbnailFile?.delete();
setState(() {
_videoFile = null;
_thumbnailFile = null;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload failed: $e')),
);
_videoUrl = null;
_thumbnailUrl = null;
} finally {
setState(() {
_isUploading = false;
_uploadProgress = 0.0;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Upload Video with Thumbnail'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_videoFile != null) ...[
Text('Selected Video: ${_videoFile!.path.split('/').last}'),
const SizedBox(height: 10),
],
if (_thumbnailFile != null) ...[
Image.file(
_thumbnailFile!,
height: 150,
width: 150,
fit: BoxFit.cover,
),
const SizedBox(height: 10),
Text('Thumbnail generated: ${_thumbnailFile!.path.split('/').last}'),
const SizedBox(height: 20),
] else if (_videoFile != null) ...[
const CircularProgressIndicator(),
const SizedBox(height: 10),
const Text('Generating thumbnail...'),
const SizedBox(height: 20),
],
ElevatedButton(
onPressed: _isUploading ? null : _pickVideo,
child: const Text('Pick Video'),
),
const SizedBox(height: 20),
if (_videoFile != null && !_isUploading)
ElevatedButton(
onPressed: _uploadVideoAndThumbnail,
child: const Text('Upload Video & Thumbnail'),
),
if (_isUploading) ...[
const SizedBox(height: 20),
LinearProgressIndicator(value: _uploadProgress),
Text('${(_uploadProgress * 100).toStringAsFixed(1)}% Uploaded'),
const SizedBox(height: 20),
],
if (_videoUrl != null) ...[
const Text('Video URL:'),
Text(_videoUrl!, textAlign: TextAlign.center),
const SizedBox(height: 10),
],
if (_thumbnailUrl != null) ...[
const Text('Thumbnail URL:'),
Text(_thumbnailUrl!, textAlign: TextAlign.center),
const SizedBox(height: 10),
],
],
),
),
),
);
}
}
Explanation of the Code
- Dependencies:
image_picker: Used to open the device's gallery and allow the user to select a video.video_thumbnail: A powerful plugin to generate a thumbnail image from a video file path.path_provider: Helps in finding a suitable temporary directory to store the generated thumbnail.firebase_storage: The official Flutter plugin for interacting with Firebase Storage.uuid: Generates universally unique identifiers for naming files in Storage, preventing collisions.
_pickVideo():- Uses
ImagePicker().pickVideo()to prompt the user to select a video. - Once a video is selected, its path is stored, and
_generateThumbnail()is immediately called.
- Uses
_generateThumbnail(String videoPath):- Gets a temporary directory path using
getTemporaryDirectory()frompath_provider. - Calls
VideoThumbnail.thumbnailFile()with the video path, desired output path, image format, max height, and quality. This saves a new image file in the temporary directory. - Updates the UI to display the generated thumbnail.
- Gets a temporary directory path using
_uploadVideoAndThumbnail():- Generates unique file names for both the video and thumbnail using
uuid.v4(). - Video Upload: Creates a reference to the Firebase Storage bucket (e.g.,
'videos/your_video_id.mp4') and uploads the video file usingputFile(). - Progress Tracking: Listens to
snapshotEventsfrom theUploadTaskto update the_uploadProgress, providing real-time feedback to the user. - Awaits the completion of the video upload, then retrieves its download URL using
getDownloadURL(). - Thumbnail Upload: If a thumbnail was successfully generated, it follows a similar process to upload the thumbnail file to a different path (e.g.,
'thumbnails/your_thumbnail_id.png') and retrieves its download URL. - Handles success and error scenarios with
SnackBarmessages. - Cleanup: After successful upload, the temporary local video and thumbnail files are deleted to conserve device storage.
- Generates unique file names for both the video and thumbnail using
- UI (
buildmethod):- Displays the selected video's filename and the generated thumbnail preview.
- Shows a loading indicator and progress bar during thumbnail generation and file uploads.
- Buttons for picking and uploading videos are enabled/disabled based on the upload state.
- Displays the download URLs for the video and thumbnail once the upload is complete.
Next Steps and Best Practices
- Store URLs in Firestore/Realtime Database: For practical applications, you'll likely want to store these download URLs (along with other metadata like user ID, timestamp, description) in a database like Cloud Firestore. This allows you to easily query and display lists of videos.
- Error Handling: Implement more robust error handling, including specific messages for different types of upload failures (e.g., network issues, permissions).
- Security Rules: Always refine your Firebase Storage security rules to restrict access to authenticated users and specific paths.
- Loading States: Improve the UI's loading states for a smoother user experience, perhaps with disableable buttons or a custom loading overlay.
- Compression: For very large videos, consider client-side video compression before uploading to reduce bandwidth and storage costs.
- Background Uploads: For long videos, consider implementing background upload capabilities using packages like
firebase_storage_platform_interfaceor workmanager. - Video Playback: Use packages like
video_playerto play the uploaded video directly from the retrieved URL.
Conclusion
You've successfully built a Flutter application capable of picking videos, generating unique thumbnails on the client side, and robustly uploading both to Firebase Storage. This setup provides a solid foundation for any multimedia-heavy application, enhancing both functionality and user experience with visual previews. By integrating these powerful tools, you can create engaging and scalable content platforms with ease.