Launch Club

Chewie Video Player Tutorial (Assets, URL, & Photo Library)

Flutter Package Video

Published Apr 18, 2023

Marcus Ng

Marcus Ng

Playing videos is a pretty common requirement in a lot of apps. Maybe you need to play a video in an onboarding screen or display some video content created by users.

The video_player package lets us easily play videos from our local assets, network, and photo library. And with the chewie package, we’re able to easily implement a customizable video playback UI that includes a whole host of additional features.

Project Setup

In our pubspec.yaml, add the video_player and chewie packages.

I’ve already added a video called island.mp4 to my assets/ folder for testing, so I’ll include the assets directory. You should download and add a video too for testing.

# pubspec.yaml

dependencies:
  chewie: ^1.3.6 # or <latest_version>
  video_player: ^2.5.1 # or <latest_version>

flutter:
  uses-material-design: true

  assets:
    - assets/
island.mp4 in assets

For fetching videos from a network on iOS, we have to add NSAppTransportSecurity and NSAllowsArbitraryLoads to our Info.plist. Note that you may need to add more permissions based on your use case.

iOS Info.plist

For Android, add internet permission to the main/AndroidManifest.xml.

<uses-permission android:name="android.permission.INTERNET" />

VideoPlayerView

We want to create a reusable widget called VideoPlayerView that handles loading videos from a network, local assets, and our photo library. This means we need to know the URL or path of the video and which DataSourceType we’re fetching from.

We have two state variables _videoPlayerController and _chewieController.

In initState, assign videoPlayerController based on the DataSourceType. contentUri is Android only. We’ll include it here anyway to avoid errors.

Then set the value of _chewieController passing in the videoPlayerController and aspectRatio of 16 by 9.

Chewie has an abundance of customizations you can see by taking a look at all the different parameters and what they do. We’ll use the default values for now.

Remember to dispose of both the video player controller and chewie controller to avoid memory leaks.

The build method returns a Column with the DataSourceType as a Text widget and Chewie wrapped in AspectRatio to control the size.

class VideoPlayerView extends StatefulWidget {
  const VideoPlayerView({
    super.key,
    required this.url,
    required this.dataSourceType,
  });

  final String url;

  final DataSourceType dataSourceType;

  @override
  State<VideoPlayerView> createState() => _VideoPlayerViewState();
}

class _VideoPlayerViewState extends State<VideoPlayerView> {
  late VideoPlayerController _videoPlayerController;

  late ChewieController _chewieController;

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

    switch (widget.dataSourceType) {
      case DataSourceType.asset:
        _videoPlayerController = VideoPlayerController.asset(widget.url);
        break;
      case DataSourceType.network:
        _videoPlayerController = VideoPlayerController.network(widget.url);
        break;
      case DataSourceType.file:
        _videoPlayerController = VideoPlayerController.file(File(widget.url));
        break;
      case DataSourceType.contentUri:
        _videoPlayerController =
            VideoPlayerController.contentUri(Uri.parse(widget.url));
        break;
    }

    _videoPlayerController.initialize().then(
          (_) => setState(
            () => _chewieController = ChewieController(
              videoPlayerController: _videoPlayerController,
              aspectRatio: 16 / 9,
            ),
          ),
        );
  }

  @override
  void dispose() {
    _videoPlayerController.dispose();
    _chewieController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          widget.dataSourceType.name.toUpperCase(),
          style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const Divider(),
        AspectRatio(
          aspectRatio: 16 / 9,
          child: Chewie(controller: _chewieController),
        ),
      ],
    );
  }
}

Play video from local assets and network

Let’s add an asset VideoPlayerView and a network VideoPlayerView to our VideoPlayersScreen. If you run into any issues, be sure to test on a physical device as simulators and emulators are not always accurate. It’s working great, and we even have a nice playback UI thanks to Chewie.

class VideoPlayersScreen extends StatelessWidget {
  const VideoPlayersScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Video Players')),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: const [
          // Local Assets
          VideoPlayerView(
            url: 'assets/island.mp4',
            dataSourceType: DataSourceType.asset,
          ),
          SizedBox(height: 24),
          // Network
          VideoPlayerView(
            url:
                'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4',
            dataSourceType: DataSourceType.network,
          ),
        ],
      ),
    );
  }
}

Play video from files

For selecting a video from a user’s photo library, we need to use the image_picker package. Add image_picker to the pubspec.yaml.

# pubspec.yaml

dependencies:
  image_picker: ^0.8.6+1 # or <latest_version>

iOS requires a tiny bit of permission setup. In our Info.plist, add the NSPhotoLibraryUsageDescription key with a String value explaining why access is needed. Android doesn’t require any setup.

SelectVideo is a StatefulWidget with a nullable File state variable. If file is not null, then display the VideoPlayerView with the file path and DataSourceType of file. The TextButton uses ImagePicker to select a video file and then updates the state accordingly.

class SelectVideo extends StatefulWidget {
  const SelectVideo({super.key});

  @override
  State<SelectVideo> createState() => _SelectVideoState();
}

class _SelectVideoState extends State<SelectVideo> {
  File? _file;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextButton(
          onPressed: () async {
            final file =
                await ImagePicker().pickVideo(source: ImageSource.gallery);
            if (file != null) {
              setState(() => _file = File(file.path));
            }
          },
          child: const Text('Select Video'),
        ),
        if (_file != null)
          VideoPlayerView(
            url: _file!.path,
            dataSourceType: DataSourceType.file,
          ),
      ],
    );
  }
}

Let’s add the SelectVideo button for selecting videos.

class VideoPlayersScreen extends StatelessWidget {
  const VideoPlayersScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
          // ...
          VideoPlayerView(
            url:
                'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4',
            dataSourceType: DataSourceType.network,
          ),
          SizedBox(height: 24),
          SelectVideo(),
        ],
      ),
    );
  }
}

Dynamic Aspect Ratio

Our videos don’t all have 16 by 9 aspect ratios. To change the aspect ratio to the size of the video, it’s not as simple as only setting aspectRatio to _chewieController.aspectRatio or _videoPlayerController.value.aspectRatio. This is because the aspect ratio returned by the controller isn’t set until the video loads. We have to rebuild our VideoPlayerView widget once the VideoPlayerController loads.

So instead of using ChewieController’s autoInitialize, we need to manually initialize the videoPlayerController and call setState() to rebuild the widget. Then make sure the videoPlayerController is initialized before displaying the Chewie, otherwise we get a late initialization error. Change the aspect ratio to the video player’s aspect ratio.

class _VideoPlayerViewState extends State<VideoPlayerView> {
  late VideoPlayerController _videoPlayerController;

  late ChewieController _chewieController;

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

    // ...

    _videoPlayerController.initialize().then(
          (_) => setState(
            () => _chewieController = ChewieController(
              videoPlayerController: _videoPlayerController,
              aspectRatio: _videoPlayerController.value.aspectRatio,
            ),
          ),
        );
  }

  // ...

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // ...
        const Divider(),
        _videoPlayerController.value.isInitialized
            ? AspectRatio(
                aspectRatio: _videoPlayerController.value.aspectRatio,
                child: Chewie(controller: _chewieController),
              )
            : const SizedBox.shrink(),
      ],
    );
  }
}

Wrap Up

Now if we reload the app and take a look at all of our videos, we can see the aspect ratios are dynamic!

We’ve successfully implemented the video_player and chewie packages into our app. We can play videos with dynamic aspect ratio support from our local assets, network, and files.

Flutter and Dart

made simple.

Everything you need to build production ready apps.

No spam. Just updates. Unsubscribe whenever.