image

05 Jan 2026

9K

35K

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

  1. Create a Firebase Project:
    • Go to the Firebase Console.
    • Click "Add project" and follow the on-screen instructions.
  2. 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) and GoogleService-Info.plist (for iOS) files, and place them in the correct directories (android/app/ and ios/Runner/ respectively).
    • Add Firebase configuration to your Android and iOS project files as specified by Firebase.
  3. 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.
  4. 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

  1. 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.
  2. _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.
  3. _generateThumbnail(String videoPath):
    • Gets a temporary directory path using getTemporaryDirectory() from path_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.
  4. _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 using putFile().
    • Progress Tracking: Listens to snapshotEvents from the UploadTask to 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 SnackBar messages.
    • Cleanup: After successful upload, the temporary local video and thumbnail files are deleted to conserve device storage.
  5. UI (build method):
    • 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_interface or workmanager.
  • Video Playback: Use packages like video_player to 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.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is