Code With Bisky

Master the Art of Video Sharing: Flutter & Appwrite Tutorial - Send Videos to Your Loved Ones! Episode [16]

Key Topics covered:

  • Trim Video Before Sending It
  • Send Videos in Real-time
  • Display Videos

Description:

In this tutorial, we'll explore how to send videos using Flutter and Appwrite. If you're a beginner looking to enhance your app development skills, this step-by-step guide is perfect for you. You'll learn how to handle image selection, encode the videos, and send them securely to your Appwrite backend. We'll cover the necessary code snippets and explain the underlying concepts, ensuring that you grasp the core concepts behind the implementation.

Add the following dependencies in your pubspec.yaml


dependencies:
  .........
  video_player: ^2.6.1
  path: ^1.8.2
  video_trimmer: ^2.0.0

        
        

Update an extensions file.

  • lib/core/extensions/extensions.dart

In OnBuildContext , implement the logic to pick and trim a video


 Future<File?> pickVideo(BuildContext context,final ValueChanged<File> onChanged) async {
    try{
      final file = await ImagePicker().pickVideo(source:  ImageSource.gallery);
      if(mounted && file != null){
        Navigator.push(context, MaterialPageRoute(builder: (context) => TrimmerView(File(file.path), onChanged),));
        return null;
      }
    }catch(e){
      print(e);
    }
    return null;
  }
        
        

Code Snippet(TrimmerView.dart):

Create an TrimmerView.dart. We need to trim a video before sending it.

  • lib/widget/TrimmerView.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:video_trimmer/video_trimmer.dart';
class TrimmerView extends StatefulWidget {
  final File file;
  final ValueChanged<File> onChanged;
  TrimmerView(this.file,this.onChanged);

  @override
  _TrimmerViewState createState() => _TrimmerViewState();
}

class _TrimmerViewState extends State<TrimmerView> {
  final Trimmer _trimmer = Trimmer();
  double _startValue = 0.0;
  double _endValue = 0.0;
  bool _isPlaying = false;
  bool _progressVisibility = false;

  Future<String?> _saveVideo() async {
    setState(() {
      _progressVisibility = true;
    });

    String? _value;
    await _trimmer
        .saveTrimmedVideo(startValue: _startValue, endValue: _endValue, onSave: (String? outputPath) {

          // callback
      if(outputPath != null){
        widget.onChanged(File(outputPath));
        Navigator.pop(context);
      }
    })
        .then((value) {
      setState(() {
        _progressVisibility = false;

      });
    });

    return _value;
  }

  void _loadVideo() {
    _trimmer.loadVideo(videoFile: widget.file);
  }

  @override
  void initState() {
    super.initState();
    _loadVideo();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Video Trimmer"),
      ),
      body: Builder(
        builder: (context) => Center(
          child: Container(
            padding: EdgeInsets.only(bottom: 30.0),
            color: Colors.black,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              mainAxisSize: MainAxisSize.max,
              children: <Widget>[
                Visibility(
                  visible: _progressVisibility,
                  child: LinearProgressIndicator(
                    backgroundColor: Colors.red,
                  ),
                ),
                ElevatedButton(
                  onPressed: _progressVisibility
                      ? null
                      : () async {
                    _saveVideo().then((outputPath) {
                      print('OUTPUT PATH: $outputPath');
                    });
                  },
                  child: const Text("Done"),
                ),
                Expanded(
                  child: VideoViewer(trimmer: _trimmer),
                ),
                Center(
                  child: TrimViewer(
                    trimmer: _trimmer,
                    viewerHeight: 50.0,
                    viewerWidth: MediaQuery.of(context).size.width,
                    maxVideoLength: const Duration(seconds: 10),
                    onChangeStart: (value) => _startValue = value,
                    onChangeEnd: (value) => _endValue = value,
                    onChangePlaybackState: (value) =>
                        setState(() => _isPlaying = value),
                  ),
                ),
                TextButton(
                  child: _isPlaying
                      ? Icon(
                    Icons.pause,
                    size: 80.0,
                    color: Colors.white,
                  )
                      : Icon(
                    Icons.play_arrow,
                    size: 80.0,
                    color: Colors.white,
                  ),
                  onPressed: () async {
                    bool playbackState = await _trimmer.videoPlaybackControl(
                      startValue: _startValue,
                      endValue: _endValue,
                    );
                    setState(() {
                      _isPlaying = playbackState;
                    });
                  },
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
} 
        

Code Snippet(MessageAppwrite.dart) modification:

Add property fileName

  • lib/model/MessageAppwrite.dart

              String? fileName;
        
        

Code Snippet(MessageState.dart) modification:

Add property File

  • lib/model/MessageState.dart

import 'package:appwrite/models.dart';
 @Default(null) File? file,
        
        

Don't forget to run the following command in your terminal to generate added properties flutter packages pub run build_runner build

Code Snippet(MessageScreen.dart) modification:

Add property File

  • lib/pages/dashboard/chat/MessageScreen.dart

// change Icons.camera_alt, to Icons.attach_file. onPressed of icon should call _modalBottomSheet();
child: IconButton(
              splashColor: Colors.white,
              icon: Icon(
                Icons.attach_file,
                color: Colors.black,
              ),
              onPressed: () {
                _modalBottomSheet();
              },
        
        

add ImageSource on pickImage() parameters

  • lib/pages/dashboard/chat/MessageScreen.dart

  void pickImage(ImageSource source) async {
    Navigator.pop(context);
    final file = await context.pickAndCropImage(3 / 4, source);
    if (file != null) {

      File? fileUploaded = await messageNotifier?.uploadMedia(
          realm.ObjectId().hexString, file.path);

      if (fileUploaded != null) {
        sendMessage("IMAGE", fileUploaded.$id, file: fileUploaded);
      }
    }
  }
        
        

add pickVideo() method

  • lib/pages/dashboard/chat/MessageScreen.dart

 void pickVideo() async {
    Navigator.pop(context);
    context.pickVideo(
      context,
      (file) async {
        File? fileUploaded = await messageNotifier?.uploadMedia(
            realm.ObjectId().hexString, file.path);
        if (fileUploaded != null) {
          sendMessage("VIDEO", fileUploaded.$id, file: fileUploaded);
        }
      },
    );
  }
        
        

Add a third parameter on sendMessage method which is a File

  • lib/pages/dashboard/chat/MessageScreen.dart

  void sendMessage(String type, String message, {File? file}) {
    if (file != null) {
      messageNotifier?.onChangedUploadedFile(file);
    }
    ..........
 }
        
        

Add BottomSheet Modal

  • lib/pages/dashboard/chat/MessageScreen.dart

void _modalBottomSheet() {
    showModalBottomSheet(
      context: context,
      builder: (builder) {
        return Container(
          height: 250.0,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.only(
                topLeft: Radius.circular(10), topRight: Radius.circular(10)),
          ),
          child: Container(
            width: double.infinity,
            color: Colors.white,
            child: Column(
              children: [
                const SizedBox(
                  height: 10,
                ),
                const Text("Select an Option"),
                const SizedBox(
                  height: 10,
                ),
                InkWell(
                  onTap: () {
                    pickVideo();
                  },
                  child: Container(
                    alignment: Alignment.center,
                    width: double.infinity,
                    height: 38,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const SizedBox(
                          height: 10,
                        ),
                        Icon(
                          Icons.video_camera_front,
                          color: Colors.grey,
                        ),
                        const Text("Video"),
                        Spacer()
                      ],
                    ),
                  ),
                ),
                Divider(),
                InkWell(
                  onTap: () {
                    pickImage(ImageSource.gallery);
                  },
                  child: Container(
                    alignment: Alignment.center,
                    width: double.infinity,
                    height: 38,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const SizedBox(
                          height: 10,
                        ),
                        Icon(
                          Icons.image,
                          color: Colors.grey,
                        ),
                        const Text("Image"),
                        Spacer()
                      ],
                    ),
                  ),
                ),
                Divider(),
                InkWell(
                  onTap: () {
                    pickImage(ImageSource.camera);
                  },
                  child: Container(
                    alignment: Alignment.center,
                    width: double.infinity,
                    height: 38,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const SizedBox(
                          height: 10,
                        ),
                        Icon(
                          Icons.camera_alt,
                          color: Colors.grey,
                        ),
                        const Text("Camera"),
                        Spacer()
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
        
        

Code Snippet(MessageViewModel.dart) modification:

Add code to save file name when sending a media in sendMessage() method

  • lib/pages/dashboard/chat/MessageViewModel.dart

// before send a video or image. Let's do this
    if(state.file != null){
            message.fileName = state.file?.name;
    }

// after you send a message let's clear the file
     state = state.copyWith(
            file: null
          );
// add this method to set the file in our state
 void onChangedUploadedFile(File file) {

    state = state.copyWith(
      file : file
    );
  }
 
        

Conclusion:

We managed to pick an video from gallery and send it. We used an existing code to upload videos in the MessageViewModel.dart. In the next tutorial, we are going to save images and videos on local device for a better performance. Don't forget to share and join our Discord Channel. May you please subscribe to our YouTube Channel.