blob: a12e37af458213b6d7e59f243b1ae85f7d79e889 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer')
// This will clear all open videos on the platform when a full restart is
// performed.
..invokeMethod("init");
class DurationRange {
DurationRange(this.start, this.end);
final Duration start;
final Duration end;
double startFraction(Duration duration) {
return start.inMilliseconds / duration.inMilliseconds;
}
double endFraction(Duration duration) {
return end.inMilliseconds / duration.inMilliseconds;
}
@override
String toString() => '$runtimeType(start: $start, end: $end)';
}
class VideoPlayerValue {
final Duration duration;
final Duration position;
final List<DurationRange> buffered;
final bool isPlaying;
final bool isLooping;
final double volume;
final String errorDescription;
VideoPlayerValue({
@required this.duration,
this.position: const Duration(),
this.buffered: const <DurationRange>[],
this.isPlaying: false,
this.isLooping: false,
this.volume: 1.0,
this.errorDescription,
});
VideoPlayerValue.uninitialized() : this(duration: null);
VideoPlayerValue.erroneous(String errorDescription)
: this(duration: null, errorDescription: errorDescription);
bool get initialized => duration != null;
bool get isErroneous => errorDescription != null;
VideoPlayerValue copyWith({
Duration duration,
Duration position,
List<DurationRange> buffered,
bool isPlaying,
bool isLooping,
double volume,
String errorDescription,
}) {
return new VideoPlayerValue(
duration: duration ?? this.duration,
position: position ?? this.position,
buffered: buffered ?? this.buffered,
isPlaying: isPlaying ?? this.isPlaying,
isLooping: isLooping ?? this.isLooping,
volume: volume ?? this.volume,
errorDescription: errorDescription ?? this.errorDescription,
);
}
@override
String toString() {
return '$runtimeType('
'duration: $duration, '
'position: $position, '
'buffered: [${buffered.join(', ')}], '
'isplaying: $isPlaying, '
'isLooping: $isLooping, '
'volume: $volume, '
'errorDescription: $errorDescription)';
}
}
/// Controls a platform video player, and provides updates when the state is
/// changing.
///
/// Instances must be initialized with initialize.
///
/// The video is displayed in a Flutter app by creating a [VideoPlayer] widget.
///
/// To reclaim the resources used by the player call [dispose].
///
/// After [dispose] all further calls are ignored.
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
int _textureId;
final String uri;
Timer timer;
bool isDisposed = false;
Completer<Null> _creatingCompleter;
StreamSubscription<Map<String, dynamic>> _eventSubscription;
_VideoAppLifeCycleObserver _lifeCycleObserver;
VideoPlayerController(this.uri) : super(new VideoPlayerValue(duration: null));
Future<Null> initialize() async {
_lifeCycleObserver = new _VideoAppLifeCycleObserver(this);
_lifeCycleObserver.initialize();
_creatingCompleter = new Completer<Null>();
final Map<String, dynamic> response = await _channel.invokeMethod(
'create',
<String, dynamic>{'dataSource': uri},
);
_textureId = response["textureId"];
_creatingCompleter.complete(null);
DurationRange toDurationRange(List<int> values) {
return new DurationRange(
new Duration(milliseconds: values[0]),
new Duration(milliseconds: values[1]),
);
}
void eventListener(Map<String, dynamic> event) {
if (event["event"] == "initialized") {
value = value.copyWith(
duration: new Duration(milliseconds: event["duration"]),
);
_applyLooping();
_applyVolume();
_applyPlayPause();
} else if (event["event"] == "completed") {
value = value.copyWith(isPlaying: false);
timer?.cancel();
} else if (event["event"] == "bufferingUpdate") {
final List<List<int>> bufferedValues = event["values"];
value = value.copyWith(
buffered: bufferedValues.map(toDurationRange).toList(),
);
}
}
void errorListener(PlatformException e) {
value = new VideoPlayerValue.erroneous(e.message);
timer?.cancel();
}
_eventSubscription = _eventChannelFor(_textureId)
.receiveBroadcastStream()
.listen(eventListener, onError: errorListener);
}
EventChannel _eventChannelFor(int textureId) {
return new EventChannel("flutter.io/videoPlayer/videoEvents$textureId");
}
@override
Future<Null> dispose() async {
if (_creatingCompleter != null) {
await _creatingCompleter.future;
if (!isDisposed) {
timer?.cancel();
await _eventSubscription?.cancel();
await _channel.invokeMethod(
'dispose',
<String, dynamic>{'textureId': _textureId},
);
}
_lifeCycleObserver.dispose();
}
isDisposed = true;
super.dispose();
}
Future<Null> play() async {
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
Future<Null> setLooping(bool looping) async {
value = value.copyWith(isLooping: looping);
await _applyLooping();
}
Future<Null> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
Future<Null> _applyLooping() async {
if (!value.initialized || isDisposed) {
return;
}
_channel.invokeMethod(
'setLooping',
<String, dynamic>{'textureId': _textureId, 'looping': value.isLooping},
);
}
Future<Null> _applyPlayPause() async {
if (!value.initialized || isDisposed) {
return;
}
if (value.isPlaying) {
await _channel.invokeMethod(
'play',
<String, dynamic>{'textureId': _textureId},
);
timer = new Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
final Duration newPosition = await position;
value = value.copyWith(position: newPosition);
},
);
} else {
timer?.cancel();
await _channel.invokeMethod(
'pause',
<String, dynamic>{'textureId': _textureId},
);
}
}
Future<Null> _applyVolume() async {
if (!value.initialized || isDisposed) {
return;
}
await _channel.invokeMethod(
'setVolume',
<String, dynamic>{'textureId': _textureId, 'volume': value.volume},
);
}
/// The position in the current video.
Future<Duration> get position async {
if (isDisposed) {
return null;
}
return new Duration(
milliseconds: await _channel.invokeMethod(
'position',
<String, dynamic>{'textureId': _textureId},
),
);
}
Future<Null> seekTo(Duration moment) async {
if (isDisposed) {
return;
}
if (moment > value.duration) {
moment = value.duration;
} else if (moment < const Duration()) {
moment = const Duration();
}
await _channel.invokeMethod('seekTo', <String, dynamic>{
'textureId': _textureId,
'location': moment.inMilliseconds,
});
value = value.copyWith(position: moment);
}
/// Sets the audio volume of [this].
///
/// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a
/// linear scale.
Future<Null> setVolume(double volume) async {
value = value.copyWith(volume: volume.clamp(0.0, 1.0));
await _applyVolume();
}
}
class _VideoAppLifeCycleObserver extends WidgetsBindingObserver {
bool _wasPlayingBeforePause = false;
final VideoPlayerController _controller;
_VideoAppLifeCycleObserver(this._controller);
void initialize() {
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
_wasPlayingBeforePause = _controller.value.isPlaying;
_controller.pause();
break;
case AppLifecycleState.resumed:
if (_wasPlayingBeforePause) {
_controller.play();
}
break;
default:
}
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
}
}
/// Displays the video controlled by [controller].
class VideoPlayer extends StatelessWidget {
final VideoPlayerController controller;
VideoPlayer(this.controller);
@override
Widget build(BuildContext context) {
return controller._textureId == null
? new Container()
: new Texture(textureId: controller._textureId);
}
}
class VideoProgressColors {
final Paint playedPaint;
final Paint bufferedPaint;
final Paint handlePaint;
final Paint disabledPaint;
VideoProgressColors({
Color playedColor: const Color.fromRGBO(255, 0, 0, 0.7),
Color bufferedColor: const Color.fromRGBO(30, 30, 200, 0.2),
Color handleColor: const Color.fromRGBO(200, 200, 200, 1.0),
Color disabledColor: const Color.fromRGBO(200, 200, 200, 0.5),
})
: playedPaint = new Paint()..color = playedColor,
bufferedPaint = new Paint()..color = bufferedColor,
handlePaint = new Paint()..color = handleColor,
disabledPaint = new Paint()..color = disabledColor;
}
/// Displays the play/buffering status of the video controlled by [controller].
class VideoProgressBar extends StatefulWidget {
final VideoPlayerController controller;
final VideoProgressColors colors;
VideoProgressBar(this.controller, {VideoProgressColors colors})
: colors = colors ?? new VideoProgressColors();
@override
_VideoProgressBarState createState() {
return new _VideoProgressBarState();
}
}
class _VideoProgressBarState extends State<VideoProgressBar> {
VoidCallback listener;
bool _controllerWasPlaying = false;
_VideoProgressBarState() {
listener = () {
setState(() {});
};
}
VideoPlayerController get controller => widget.controller;
@override
void initState() {
super.initState();
controller.addListener(listener);
}
@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
}
@override
Widget build(BuildContext context) {
void seekToRelativePosition(Offset globalPosition) {
final RenderBox box = context.findRenderObject();
final Offset tapPos = box.globalToLocal(globalPosition);
final double relative = tapPos.dx / box.size.width;
final Duration position = controller.value.duration * relative;
controller.seekTo(position);
}
return new GestureDetector(
child: (controller.value.isErroneous)
? new Text(controller.value.errorDescription)
: new CustomPaint(
painter: new ProgressBarPainter(controller.value, widget.colors),
),
onHorizontalDragStart: (DragStartDetails details) {
if (!controller.value.initialized) {
return;
}
_controllerWasPlaying = controller.value.isPlaying;
if (_controllerWasPlaying) {
controller.pause();
}
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (!controller.value.initialized) {
return;
}
seekToRelativePosition(details.globalPosition);
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_controllerWasPlaying) {
controller.play();
}
},
onTapDown: (TapDownDetails details) {
if (!controller.value.initialized) {
return;
}
seekToRelativePosition(details.globalPosition);
},
);
}
}
class ProgressBarPainter extends CustomPainter {
VideoPlayerValue value;
VideoProgressColors colors;
ProgressBarPainter(this.value, this.colors);
@override
bool shouldRepaint(CustomPainter painter) {
return true;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(
new Rect.fromPoints(
const Offset(0.0, 0.0),
new Offset(size.width, size.height),
),
colors.disabledPaint,
);
if (!value.initialized) {
return;
}
final double playedPart = value.position.inMilliseconds /
value.duration.inMilliseconds *
size.width;
for (DurationRange range in value.buffered) {
final double start = range.startFraction(value.duration) * size.width;
final double end = range.endFraction(value.duration) * size.width;
canvas.drawRect(
new Rect.fromPoints(
new Offset(start, 0.0),
new Offset(end, size.height),
),
colors.bufferedPaint,
);
}
canvas.drawRect(
new Rect.fromPoints(Offset.zero, new Offset(playedPart, size.height)),
colors.playedPaint,
);
canvas.drawCircle(
new Offset(playedPart, size.height / 2),
size.height / 2,
colors.handlePaint,
);
}
}