skip to Main Content

I want to record a long video (> 30 min). The file size is custom for every device. I would like to know the approximate file size per minute recorded (min/MB) for different video qualities (low, medium, high) to choose the appropriate video quality. How can I do this without recording beforehand in different qualities?

This is the class:

CameraController(cam, ResolutionPreset.low);

2

Answers


  1. Why not recording one video for each resolution quality during three minutes to have a average file size by minute ?

    Login or Signup to reply.
  2. on MediaTranscodingManager.java there is a function to calculate default bit rate :

    /**
             * Generate a default bitrate with the fixed bpp(bits-per-pixel) 0.25.
             * This maps to:
             * 1080P@30fps -> 16Mbps
             * 1080P@60fps-> 32Mbps
             * 4K@30fps -> 62Mbps
             */
            private static int getDefaultBitrate(int width, int height, float frameRate) {
                return (int) (width * height * frameRate * BPP);
            }
    

    we can use this to calculate approximated video size, based on Duration :

    int get bitRateAprrox => (width * height * fps * bpp).toInt();
    
      String videoSizeApprox(Duration duration) {
        final convertedDuration = duration.inMilliseconds / 1000;
    
        // 1 byte = 8 bit
        const bitPerRawSample = 8;
        final bytes = (bitRateAprrox / bitPerRawSample) * convertedDuration;
        print("BIT RATE= $bitRateAprrox, bytes = $bytes");
        const int decimals = 2;
        if (bytes <= 0) return "0 B";
        const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
        var i = (log(bytes) / log(1024)).floor();
        return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
      }
    

    based on resolution_preset.dart :

    • low: 352×288 on iOS, 240p (320×240) on Android and Web
    • medium: 480p (640×480 on iOS, 720×480 on Android and Web)
    • high: 720p (1280×720)
    • veryHigh : 1080p (1920×1080)
    • ultraHigh: 2160p (3840×2160 on Android and iOS, 4096×2160 on Web)
    • max : The highest resolution available.

    here the result i get so far:

    enter image description here

    not precise, but close.

    copy paste this code and run it (need some packages like video_player) :

    import 'package:camera/camera.dart';
    import 'package:flutter/material.dart';
    import 'dart:math';
    import 'dart:io';
    
    import 'package:video_player/video_player.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatefulWidget {
      const MyApp({super.key});
    
      @override
      State<MyApp> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      String _duration = '';
      String _fileSize = '';
      Duration _estimatedDur = const Duration();
      ResolutionPreset _currentPreset = ResolutionPreset.medium;
    
      String _approx = '';
    
      late VideoPlayerController _videoPlayerController;
    
    
      @override
      void dispose() {
        // TODO: implement dispose
        super.dispose();
        _videoPlayerController.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: Builder(builder: (context) {
            final camProfile = CamProfileApprox(_currentPreset);
    
            return Scaffold(
              body: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  DropdownButton<ResolutionPreset>(
                      value: _currentPreset,
                      items: ResolutionPreset.values
                          .map((preset) => DropdownMenuItem<ResolutionPreset>(
                                value: preset,
                                child: Text(preset.name),
                              ))
                          .toList(),
                      onChanged: (preset) {
                        if (preset != null) {
                          setState(() {
                            _currentPreset = preset;
                          });
                        }
                      }),
                  Text(
                      "Estimated Duration : ${_estimatedDur.inMilliseconds / 1000.0} s"),
                  Slider(
                      min: 0.0,
                      max: 100,
                      value: _estimatedDur.inMilliseconds / 1000.0,
                      onChanged: (value) {
                        setState(() {
                          _estimatedDur =
                              Duration(milliseconds: (value * 1000).toInt());
                        });
                      }),
                  Text(
                      'size based on Estimated Duration = ${camProfile.videoSizeApprox(_estimatedDur)}'),
                  Text('actual duration = $_duration'),
                  Text('actual video size = $_fileSize'),
                  Text('approx based actual duration = $_approx'),
                  Center(
                    child: TextButton(
                      onPressed: () async {
                        XFile? file = await Navigator.push(
                            context,
                            MaterialPageRoute(
                                builder: (context) => Camera(
                                      preset: _currentPreset,
                                    )));
                        if (file != null) {
    
                          _videoPlayerController = VideoPlayerController.file(File(file.path));
                          await _videoPlayerController.initialize();
                          final bytes = await file.length();
                          final fileSize = _getFileSize(bytes, 2);
                          final actualDur = _videoPlayerController.value.duration;
                          final size = _videoPlayerController.value.size;
                          print("SIZE = $size");
                          setState(() {
                            _approx = camProfile.videoSizeApprox(actualDur);
                            _duration =
                                '${actualDur.inMilliseconds / 1000.0} s';
                            _fileSize = fileSize;
                          });
                        }
                      },
                      child: const Text("Open Camera"),
                    ),
                  ),
                ],
              ),
            );
          }),
        );
      }
    
      String _getFileSize(int bytes, int decimals) {
        if (bytes <= 0) return "0 B";
        const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
        var i = (log(bytes) / log(1024)).floor();
        return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
      }
    }
    
    class CamProfileApprox {
      final ResolutionPreset preset;
      CamProfileApprox(this.preset);
    
      int get width {
        switch (preset) {
          case ResolutionPreset.low:
            if (Platform.isIOS) return 288;
            return 240;
          case ResolutionPreset.medium:
            return 480;
          case ResolutionPreset.high:
            return 720;
          case ResolutionPreset.veryHigh:
            return 1080;
          case ResolutionPreset.ultraHigh:
            return 2160;
          case ResolutionPreset.max:
            return 2160;
        }
      }
    
      int get height {
        switch (preset) {
          case ResolutionPreset.low:
            if (Platform.isIOS) return 352;
            return 320;
          case ResolutionPreset.medium:
            if (Platform.isIOS) return 640;
            return 720;
          case ResolutionPreset.high:
            return 1280;
          case ResolutionPreset.veryHigh:
            return 1920;
          case ResolutionPreset.ultraHigh:
            return 3840;
          case ResolutionPreset.max:
            return 3840;
        }
      }
    
      //source: MediaTranscodingManager.java
      static const fps = 30.0;
    
      //BPP -> Default bpp(bits-per-pixel) to use for calculating default bitrate.
      //source: android.media.MediaTranscodingManager
      static const double bpp = 0.25;
    
      int get bitRateAprrox => (width * height * fps * bpp).toInt();
    
      String videoSizeApprox(Duration duration) {
        final convertedDuration = duration.inMilliseconds / 1000;
    
        // 1 byte = 8 bit
        const bitPerRawSample = 8;
        final bytes = (bitRateAprrox / bitPerRawSample) * convertedDuration;
        print("BIT RATE= $bitRateAprrox, bytes = $bytes");
        const int decimals = 2;
        if (bytes <= 0) return "0 B";
        const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
        var i = (log(bytes) / log(1024)).floor();
        return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
      }
    }
    
    
    class Camera extends StatefulWidget {
      final ResolutionPreset preset;
      const Camera({Key? key, required this.preset}) : super(key: key);
    
      @override
      State<Camera> createState() => _CameraState();
    }
    
    class _CameraState extends State<Camera> {
    
      late CameraController _controller;
      late bool _recording;
      late bool _loaded;
    
      late Stopwatch _stopwatch;
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        _recording = false;
        _loaded = false;
        _stopwatch = Stopwatch();
        _initCamera();
    
    
      }
    
      Future<void> _initCamera () async {
        final camera = await availableCameras();
        _controller = CameraController(camera[0], widget.preset);
        await _controller.initialize();
        if(mounted){
          setState((){
            _loaded = true;
          });
        }
      }
    
      @override
      void dispose() {
        super.dispose();
        _controller.dispose();
      }
    
    
      Future<void> _takeVideo () async{
        if(_recording){
          _stopwatch.stop();
          XFile video = await _controller.stopVideoRecording();
          setState((){
            _recording = false;
          });
    
          if(mounted){
            Navigator.pop(context, video);
          }
        }
        else {
          _stopwatch.reset();
          await _controller.prepareForVideoRecording();
          await _controller.startVideoRecording();
          _stopwatch.start();
          setState((){
            _recording = true;
          });
    
    
        }
    
      }
    
    
      @override
      Widget build(BuildContext context) {
        if(!_loaded) return const Center(child: CircularProgressIndicator(),);
        return Scaffold(
          body: Stack(
            children: [
              CameraPreview(_controller),
              Positioned(
                bottom: 10,
                right: 10,
                left: 10,
                child: IconButton(onPressed: (){
                  _takeVideo();
                }, icon: _recording ? const Icon(Icons.stop, color: Colors.red,):const Icon(Icons.video_call, color: Colors.green,)),
              )
            ],
          ),
        );
      }
    
    
    }
    

    nb : "If a preset is not available on the camera being used a preset
    of lower quality will be selected automatically."

    This means that if the preset is not available, it not applies only to the lower preset, but to the high preset as well. For ex. the device not supported the ultraHigh preset it will select the veryHigh preset if available., this is the wrong thing. Somehow, we must get the actual preset being used in native code. And its actual size will be very smaller than the approximated size. But since its just approximated, I guess its ok for it to be smaller.

    And when on low preset, its very small than approx, maybe the fps gets lower than 30 fps. While, I am not sure about the audio bit rate, i hope it helps and gives you some direction. Cheers!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search