Skip to content

Flutter Overview

Anirudh Sevugan edited this page Feb 5, 2025 · 12 revisions

Flutter

Go to flutter-exoplayer > lib in the GitHub repository, and you will find all of the Dart source code. You will notice 3 folders inside.

Warning

This version (a.k.a, the Neo/New release) is known to not work well with platforms other than Android and the web. Beware of this when you create a release. File picking is broken on the web.

Folder Description
screens Contains screens, resembling webpages, that offer settings to change before the user does the main task.
utils Contains backend utilities, such as web-vtt.dart, which has some template code (classes) that are reused in other dart files to handle WebVTT subtitles
widgets Contains the primary utilitie(s), like video_player_widget.dart, which handles the video player, showing subtitles, playback speed controls, using chewie, video_player, and vanilla Flutter utilities.

screens

Modify this folder if you want to edit pages, for example, a home page which contains links to places that the user can go, or a settings page to toggle some settings before the action begins.

Important code

class _VideoScreenState extends State<VideoScreen> {
  String _videoUrl = '';
  String _filePath = '';
  String _subtitleUrl = ''; // Subtitle URL variable
  String _subtitleFilePath = ''; // Subtitle file path
  bool _isDarkMode = false; // Add theme state here...

This code initializes important local variables (later converted to global variables and passed on), for example, the video URL variable and video file path variable, that are passed by video_screen.dart to video_player_widget.dart. Without defining these variables (and converting them to global ones later on), your video won't play in-app, as video_player_widget.dart expects these variables to be passed as inputs for the video player to use.

// Method to show video URL dialog
  Future<void> _showVideoURLDialog() async {
    final TextEditingController videoController = TextEditingController();

    return showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Enter Video URL'),
          content: TextField(
            controller: videoController,
            decoration: InputDecoration(hintText: 'Enter a valid video URL'),
            keyboardType: TextInputType.url,
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Cancel'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('OK'),
              onPressed: () {
                setState(() {
                  _videoUrl = videoController.text;
                  _filePath = '';
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

This code is primarily for video/subtitle URL dialogs rendered with Skia and following the Material 2 UI standard. actions are used as buttons to click in the dialog. onPressed, is exactly what it says. These dialogs are also important because they serve as inputs that are set to the variables.

// Method to pick video file
  Future<void> _pickFile() async {
    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.video);

    if (result != null && result.files.single.path != null) {
      setState(() {
        _filePath = result.files.single.path!;
        _videoUrl = '';
      });
    }
  }

This code simply uses the file_picker package to initialize a file picker that expects video files as an input.

: VideoPlayerWidget(
            videoUrl: _videoUrl,
            filePath: _filePath,
            subtitleUrl: _subtitleUrl, // Pass subtitle URL to VideoPlayerWidget
            subtitleFilePath: _subtitleFilePath, // Pass subtitle file path to VideoPlayerWidget

This code is the final, most important step. It sets the local variables we mentioned earlier to global variables that can be passed to video_player_widget.dart to be used as inputs for the video player (video_player and chewie), and the subtitle handler (native Flutter).

utils

Modify this folder if you want to edit things like classes, or backend utilities that can affect how things are processed or done in files in the widgets folder. (permission_utils.dart, is not important by the way).

Important code

class WebVttCue {
  final Duration start;
  final Duration end;
  final String text;

  WebVttCue({
    required this.start,
    required this.end,
    required this.text,
  });
}

// Subtitle parsing logic (WebVTT format)
List<WebVttCue> parseWebVtt(String subtitleData) {
  final cuePattern = RegExp(r'(\d{2}:\d{2}:\d{2}.\d{3}) --> (\d{2}:\d{2}:\d{2}.\d{3})\n(.*?)\n\n', dotAll: true);
  final List<WebVttCue> cues = [];

  for (final match in cuePattern.allMatches(subtitleData)) {
    final start = _parseTime(match.group(1)!);
    final end = _parseTime(match.group(2)!);
    final text = match.group(3)!;
    cues.add(WebVttCue(start: start, end: end, text: text));
  }

  return cues;
}

// Helper method to parse time string to Duration
Duration _parseTime(String time) {
  final parts = time.split(':');
  final secondsParts = parts[2].split('.');
  return Duration(
    hours: int.parse(parts[0]),
    minutes: int.parse(parts[1]),
    seconds: int.parse(secondsParts[0]),
    milliseconds: int.parse(secondsParts[1]),
  );
}

This code specifies parameters to be used for subtitle parsing in WebVTT (hours, minutes, seconds, and milliseconds for subtitle timing), that will be used by video_player_widget.dart as classes (OOP/Object Oriented Programming) to then handle subtitles and show them in a widget that does not currently display in fullscreen, that contains the text for the subtitles.

widgets

Modify this folder if you want to edit the main aspect of the project, for example, how the video player handles the passed inputs as global variables, how the video player handles subtitles, etc, etc.

Important code

class VideoPlayerWidget extends StatefulWidget {
  final String videoUrl;
  final String filePath;
  final String subtitleUrl;
  final String subtitleFilePath;

  const VideoPlayerWidget({
    Key? key,
    required this.videoUrl,
    required this.filePath,
    required this.subtitleUrl,
    required this.subtitleFilePath,
  }) : super(key: key);

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

This code gets the global variables and converts them to local variables that can be modified without affecting major changes, and specifies required parameters, which if not fullfilled, will return an error in the console.

class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
  late VideoPlayerController _videoPlayerController;
  late ChewieController _chewieController;
  late List<WebVttCue> _subtitles;
  String? _currentSubtitle;
  bool _isLoading = true;
  double _playbackSpeed = 1.0;
  bool _controlsVisible = true; // Keep track of control visibility

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

    // Request permission for subtitle files if needed
    if (widget.subtitleFilePath.isNotEmpty) {
      requestPermissionIfNeeded(widget.subtitleFilePath, context);
    }

    // Initialize the video player controller based on video URL or file path
    if (widget.filePath.isNotEmpty) {
      _videoPlayerController = VideoPlayerController.file(File(widget.filePath));
    } else {
      _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
    }

    // Initialize the Chewie controller for video playback
    _initializeChewieController();

    // Load subtitles if needed
    _loadSubtitles();

    // Keep the screen on while the video is playing
    KeepScreenOn.turnOn();

    // Listen to video position changes to update subtitles
    _videoPlayerController.addListener(_updateCurrentSubtitle);

    // Apply immersive mode once the widget builds
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _applyImmersiveMode();
    });

    // Initialize the video player
    _initializeVideoPlayer();
  }

  void _applyImmersiveMode() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  }

void _initializeChewieController() {
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController,
      aspectRatio: 16 / 9,
      autoPlay: true,
      looping: true,
      allowPlaybackSpeedChanging: false, // Disable built-in playback speed dropdown
      customControls: MaterialControls(
        // Custom controls if needed
      ),
      showControlsOnInitialize: true, // Show controls on initialization
    );
  }

This code basically initializes core tools, like the video player and the subtitle handling, and also loads the subtitles from the parameter passed in for the subtitles, and specifies some other booleans that will be used later, for example the playback speed, used for the custom dialog to control the playback speed that does not break outside of fullscreen like chewie's dialog does.

@override
  void dispose() {
    KeepScreenOn.turnOff();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);

    _videoPlayerController.removeListener(_updateCurrentSubtitle);
    _chewieController.dispose();
    _videoPlayerController.dispose();
    super.dispose();
  }

  Future<void> _loadSubtitles() async {
    setState(() {
      _isLoading = true;
    });

    if (widget.subtitleFilePath.isNotEmpty) {
      try {
        final file = File(widget.subtitleFilePath);
        final subtitleData = await file.readAsString();
        setState(() {
          _subtitles = parseWebVtt(subtitleData);
          _isLoading = false;
        });
      } catch (e) {
        setState(() {
          _isLoading = false;
        });
        print('Error loading subtitle file: $e');
      }
    } else if (widget.subtitleUrl.isNotEmpty) {
      try {
        final response = await http.get(Uri.parse(widget.subtitleUrl));
        if (response.statusCode == 200) {
          setState(() {
            _subtitles = parseWebVtt(response.body);
            _isLoading = false;
          });
        } else {
          setState(() {
            _isLoading = false;
          });
          print('Failed to load subtitle from URL: ${response.statusCode}');
        }
      } catch (e) {
        setState(() {
          _isLoading = false;
        });
        print('Error loading subtitle from URL: $e');
      }
    }
  }

  void _updateCurrentSubtitle() {
    final currentTime = _videoPlayerController.value.position;

    for (var cue in _subtitles) {
      if (currentTime >= cue.start && currentTime <= cue.end) {
        setState(() {
          _currentSubtitle = cue.text;
        });
        return;
      }
    }

    setState(() {
      _currentSubtitle = '';
    });
  }

  void _initializeVideoPlayer() async {
    await _videoPlayerController.initialize();
    setState(() {
      _isLoading = false;
    });
  }

  void _changePlaybackSpeed(double speed) {
    setState(() {
      _playbackSpeed = speed;
      _videoPlayerController.setPlaybackSpeed(speed);

      // Reapply immersive mode after speed change
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _applyImmersiveMode();
      });
    });
  }

This code specifies some functions (e.g, changePlaybackSpeed, and updateCurrentSubtitle), and also handles when the video is paused, removing event listeners for subtitles, and disabling the wakelock created by the keep_screen_on package to keep the screen on when the video is playing.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: GestureDetector(
      onTap: () {
        setState(() {
          _controlsVisible = !_controlsVisible; // Toggle controls visibility on tap
        });
        _applyImmersiveMode(); // Reapply immersive mode
      },
      child: Column(
        children: [
          Expanded(
            child: Stack(
              children: [
                Container(
                  color: Colors.black,
                  child: Chewie(controller: _chewieController),
                ),
                if (_isLoading)
                  Center(child: CircularProgressIndicator()),
                if (_currentSubtitle != null && _currentSubtitle!.isNotEmpty && !_isLoading)
                  Positioned(
                    bottom: 70,
                    left: 0,
                    right: 0,
                    child: Container(
                      padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                      color: Colors.black.withOpacity(0.7),
                      child: Text(
                        _currentSubtitle!,
                        style: TextStyle(fontSize: 19, color: Colors.white),
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ),
                // Custom playback speed control button
                Positioned(
                  top: 20,
                  right: 20,
                  child: AnimatedOpacity(
                    opacity: _controlsVisible ? 1.0 : 0.0,
                    duration: Duration(milliseconds: 300), // Adjust duration for fade effect
                    child: IconButton(
                      icon: Icon(Icons.speed, color: Colors.white),
                      onPressed: () {
                        _showPlaybackSpeedDialog();
                      },
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

// Show a dialog to select the playback speed
void _showPlaybackSpeedDialog() {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text("Select Playback Speed"),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0].map((speed) {
            return ListTile(
              title: Text("${speed}x"),
              onTap: () {
                _changePlaybackSpeed(speed);
                Navigator.of(context).pop();
              },
            );
          }).toList(),
        ),
      );
    },
  );
}

And this just specifies the custom playback speed button, where it's placed, and the dialog that appears when it's clicked, along with the GestureDetector showing and hiding it on tap (which as of now only works if tapped in the center when the play/pause button isn't visible and video is still loading).

Compiling your app

Depending on what IDE you used, there are many ways to go about this. But in every IDE, you should see a build option (in Mac, on the top left, Build) (in Windows, probably a right-click and then Build).

But the simplest way to go would be to cd into your project's directory and execute

flutter build [app-format]

[app-format] can either be an APK or AAB file if you're distributing for Android. Learn more about the differences here.

Clone this wiki locally