How to play videos sequentialy on video_player without delay?
Asked Answered
H

1

12

I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';


void main() {
  runApp(MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: MyHomePage(),
  ));
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  List<VideoPlayerController> _controllers = [];
  VoidCallback listener;
  bool _isPlaying = false;
  int _current = 0;

  @override
  void initState() {

   super.initState();


    // Add some sample videos
    _controllers.add(VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    ));
    _controllers.add(VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    ));
    _controllers.add(VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    ));

    this.tick();

    // Try refreshing by brute force (this isn't going too well)
    new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
      int delta = 99999999;
      if(_controllers[_current].value != null) {
        delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
      }
      print("Tick " + delta.toString());
      if(delta < 500) {
        _current += 1;
        this.tick();
      }
    });

  }

  void tick() async {
    print("Current: " + _current.toString());

    await _controllers[_current].initialize();
    await _controllers[_current].play();

    print("Ready");



    setState((){
      _current = _current;
    });

  }

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: _controllers[_current].value.aspectRatio,
      // Use the VideoPlayer widget to display the video
      child: VideoPlayer(_controllers[_current]),
    );
  }
}

What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.

Hereon answered 28/4, 2019 at 5:26 Comment(1)
Did you find a perfect solution on this?Mentality
M
20

Update

There is a preCache() method in the better_player library, which uses the underlying native players' cache implementations. It would be an ideal solution for seamless sequential video playback. The old answer below is a "hacky" way of achieving this.

Unfortunately, the video_player library still has no pre-caching (or even caching) feature.

Old answer

Initializing a VideoPlayerController.network() may take some time to finish. You can initialize the controller of the next video while playing the current one. This will take more memory, but I don't think it will create huge problems if you prebuffer only one or two videos. Then, when the next or previous buttons are pressed, the video will be ready to play.

Here is my workaround. It prebuffers the previous and next videos, skips to the next video when finished, shows the current position and buffer, and pauses and plays on a long press.

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

main() {
  runApp(const MaterialApp(
    home: VideoPlayerDemo(),
  ));
}

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

  @override
  State<VideoPlayerDemo> createState() => _VideoPlayerDemoState();
}

class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
  int index = 0;
  double _position = 0;
  double _buffer = 0;
  bool _lock = true;
  final Map<String, VideoPlayerController> _controllers = {};
  final Map<int, VoidCallback> _listeners = {};
  static const _urls = {
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
  };

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

    if (_urls.isNotEmpty) {
      _initController(0).then((_) {
        _playController(0);
      });
    }

    if (_urls.length > 1) {
      _initController(1).whenComplete(() => _lock = false);
    }
  }

  VoidCallback _listenerSpawner(index) {
    return () {
      int dur = _controller(index).value.duration.inMilliseconds;
      int pos = _controller(index).value.position.inMilliseconds;
      int buf = _controller(index).value.buffered.last.end.inMilliseconds;

      setState(() {
        if (dur <= pos) {
          _position = 0;
          return;
        }
        _position = pos / dur;
        _buffer = buf / dur;
      });
      if (dur - pos < 1) {
        if (index < _urls.length - 1) {
          _nextVideo();
        }
      }
    };
  }

  VideoPlayerController _controller(int index) {
    return _controllers[_urls.elementAt(index)]!;
  }

  Future<void> _initController(int index) async {
    final url = Uri.parse(_urls.elementAt(index));
    var controller = VideoPlayerController.networkUrl(url);
    _controllers[_urls.elementAt(index)] = controller;
    await controller.initialize();
  }

  void _removeController(int index) {
    _controller(index).dispose();
    _controllers.remove(_urls.elementAt(index));
    _listeners.remove(index);
  }

  void _stopController(int index) {
    _controller(index).removeListener(_listeners[index]!);
    _controller(index).pause();
    _controller(index).seekTo(const Duration(milliseconds: 0));
  }

  void _playController(int index) async {
    if (!_listeners.keys.contains(index)) {
      _listeners[index] = _listenerSpawner(index);
    }
    _controller(index).addListener(_listeners[index]!);
    await _controller(index).play();
    setState(() {});
  }

  void _previousVideo() {
    if (_lock || index == 0) {
      return;
    }
    _lock = true;

    _stopController(index);

    if (index + 1 < _urls.length) {
      _removeController(index + 1);
    }

    _playController(--index);

    if (index == 0) {
      _lock = false;
    } else {
      _initController(index - 1).whenComplete(() => _lock = false);
    }
  }

  void _nextVideo() async {
    if (_lock || index == _urls.length - 1) {
      return;
    }
    _lock = true;

    _stopController(index);

    if (index - 1 >= 0) {
      _removeController(index - 1);
    }

    _playController(++index);

    if (index == _urls.length - 1) {
      _lock = false;
    } else {
      _initController(index + 1).whenComplete(() => _lock = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Playing ${index + 1} of ${_urls.length}"),
      ),
      body: Stack(
        children: <Widget>[
          GestureDetector(
            onLongPressStart: (_) => _controller(index).pause(),
            onLongPressEnd: (_) => _controller(index).play(),
            child: Center(
              child: AspectRatio(
                aspectRatio: _controller(index).value.aspectRatio,
                child: Center(child: VideoPlayer(_controller(index))),
              ),
            ),
          ),
          Positioned(
            child: Container(
              height: 10,
              width: MediaQuery.of(context).size.width * _buffer,
              color: Colors.grey,
            ),
          ),
          Positioned(
            child: Container(
              height: 10,
              width: MediaQuery.of(context).size.width * _position,
              color: Colors.greenAccent,
            ),
          ),
        ],
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: _previousVideo,
            child: const Icon(Icons.arrow_back),
          ),
          const SizedBox(width: 24),
          FloatingActionButton(
            onPressed: _nextVideo,
            child: const Icon(Icons.arrow_forward),
          ),
        ],
      ),
    );
  }
}

All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.

Mun answered 28/4, 2019 at 15:39 Comment(3)
It's working, but when we do fast back-forward, just white screen is displaying nothing happens, only video number is changing, Can you please help me to solve it out? @MunPyriform
have you created the package ? pleas provideMuzz
better_player hasn't been updated in 14 months and the author apparently isn't maintaining it any more. The "old answer" might be better.Aviles

© 2022 - 2024 — McMap. All rights reserved.