blob: cea87ba95fa6f39d215f1fdc65d59c1fa97f759d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(stuartmorgan): Consider extracting this to a shared local (path-based)
// package for use in all implementation packages.
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
VideoPlayerPlatform? _cachedPlatform;
VideoPlayerPlatform get _platform {
if (_cachedPlatform == null) {
_cachedPlatform = VideoPlayerPlatform.instance;
_cachedPlatform!.init();
}
return _cachedPlatform!;
}
/// The duration, current position, buffering state, error state and settings
/// of a [MiniController].
@immutable
class VideoPlayerValue {
/// Constructs a video with the given values. Only [duration] is required. The
/// rest will initialize with default values when unset.
const VideoPlayerValue({
required this.duration,
this.size = Size.zero,
this.position = Duration.zero,
this.buffered = const <DurationRange>[],
this.isInitialized = false,
this.isPlaying = false,
this.isBuffering = false,
this.playbackSpeed = 1.0,
this.errorDescription,
});
/// Returns an instance for a video that hasn't been loaded.
const VideoPlayerValue.uninitialized()
: this(duration: Duration.zero, isInitialized: false);
/// Returns an instance with the given [errorDescription].
const VideoPlayerValue.erroneous(String errorDescription)
: this(
duration: Duration.zero,
isInitialized: false,
errorDescription: errorDescription);
/// The total duration of the video.
///
/// The duration is [Duration.zero] if the video hasn't been initialized.
final Duration duration;
/// The current playback position.
final Duration position;
/// The currently buffered ranges.
final List<DurationRange> buffered;
/// True if the video is playing. False if it's paused.
final bool isPlaying;
/// True if the video is currently buffering.
final bool isBuffering;
/// The current speed of the playback.
final double playbackSpeed;
/// A description of the error if present.
///
/// If [hasError] is false this is `null`.
final String? errorDescription;
/// The [size] of the currently loaded video.
final Size size;
/// Indicates whether or not the video has been loaded and is ready to play.
final bool isInitialized;
/// Indicates whether or not the video is in an error state. If this is true
/// [errorDescription] should have information about the problem.
bool get hasError => errorDescription != null;
/// Returns [size.width] / [size.height].
///
/// Will return `1.0` if:
/// * [isInitialized] is `false`
/// * [size.width], or [size.height] is equal to `0.0`
/// * aspect ratio would be less than or equal to `0.0`
double get aspectRatio {
if (!isInitialized || size.width == 0 || size.height == 0) {
return 1.0;
}
final double aspectRatio = size.width / size.height;
if (aspectRatio <= 0) {
return 1.0;
}
return aspectRatio;
}
/// Returns a new instance that has the same values as this current instance,
/// except for any overrides passed in as arguments to [copyWidth].
VideoPlayerValue copyWith({
Duration? duration,
Size? size,
Duration? position,
List<DurationRange>? buffered,
bool? isInitialized,
bool? isPlaying,
bool? isBuffering,
double? playbackSpeed,
String? errorDescription,
}) {
return VideoPlayerValue(
duration: duration ?? this.duration,
size: size ?? this.size,
position: position ?? this.position,
buffered: buffered ?? this.buffered,
isInitialized: isInitialized ?? this.isInitialized,
isPlaying: isPlaying ?? this.isPlaying,
isBuffering: isBuffering ?? this.isBuffering,
playbackSpeed: playbackSpeed ?? this.playbackSpeed,
errorDescription: errorDescription ?? this.errorDescription,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is VideoPlayerValue &&
runtimeType == other.runtimeType &&
duration == other.duration &&
position == other.position &&
listEquals(buffered, other.buffered) &&
isPlaying == other.isPlaying &&
isBuffering == other.isBuffering &&
playbackSpeed == other.playbackSpeed &&
errorDescription == other.errorDescription &&
size == other.size &&
isInitialized == other.isInitialized;
@override
int get hashCode => Object.hash(
duration,
position,
buffered,
isPlaying,
isBuffering,
playbackSpeed,
errorDescription,
size,
isInitialized,
);
}
/// A very minimal version of `VideoPlayerController` for running the example
/// without relying on `video_player`.
class MiniController extends ValueNotifier<VideoPlayerValue> {
/// Constructs a [MiniController] playing a video from an asset.
///
/// The name of the asset is given by the [dataSource] argument and must not be
/// null. The [package] argument must be non-null when the asset comes from a
/// package and null otherwise.
MiniController.asset(this.dataSource, {this.package})
: dataSourceType = DataSourceType.asset,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [MiniController] playing a video from obtained from
/// the network.
MiniController.network(this.dataSource)
: dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [MiniController] playing a video from obtained from a file.
MiniController.file(File file)
: dataSource = Uri.file(file.absolute.path).toString(),
dataSourceType = DataSourceType.file,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// The URI to the video file. This will be in different formats depending on
/// the [DataSourceType] of the original video.
final String dataSource;
/// Describes the type of data source this [MiniController]
/// is constructed with.
final DataSourceType dataSourceType;
/// Only set for [asset] videos. The package that the asset was loaded from.
final String? package;
Timer? _timer;
Completer<void>? _creatingCompleter;
StreamSubscription<dynamic>? _eventSubscription;
/// The id of a texture that hasn't been initialized.
@visibleForTesting
static const int kUninitializedTextureId = -1;
int _textureId = kUninitializedTextureId;
/// This is just exposed for testing. It shouldn't be used by anyone depending
/// on the plugin.
@visibleForTesting
int get textureId => _textureId;
/// Attempts to open the given [dataSource] and load metadata about the video.
Future<void> initialize() async {
_creatingCompleter = Completer<void>();
late DataSource dataSourceDescription;
switch (dataSourceType) {
case DataSourceType.asset:
dataSourceDescription = DataSource(
sourceType: DataSourceType.asset,
asset: dataSource,
package: package,
);
break;
case DataSourceType.network:
dataSourceDescription = DataSource(
sourceType: DataSourceType.network,
uri: dataSource,
);
break;
case DataSourceType.file:
dataSourceDescription = DataSource(
sourceType: DataSourceType.file,
uri: dataSource,
);
break;
case DataSourceType.contentUri:
dataSourceDescription = DataSource(
sourceType: DataSourceType.contentUri,
uri: dataSource,
);
break;
}
_textureId = (await _platform.create(dataSourceDescription)) ??
kUninitializedTextureId;
_creatingCompleter!.complete(null);
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(
duration: event.duration,
size: event.size,
isInitialized: event.duration != null,
);
initializingCompleter.complete(null);
_platform.setVolume(_textureId, 1.0);
_platform.setLooping(_textureId, true);
_applyPlayPause();
break;
case VideoEventType.completed:
pause().then((void pauseResult) => seekTo(value.duration));
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
break;
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
break;
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
value = value.copyWith(isPlaying: event.isPlaying);
break;
case VideoEventType.unknown:
break;
}
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
_eventSubscription = _platform
.videoEventsFor(_textureId)
.listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
@override
Future<void> dispose() async {
if (_creatingCompleter != null) {
await _creatingCompleter!.future;
_timer?.cancel();
await _eventSubscription?.cancel();
await _platform.dispose(_textureId);
}
super.dispose();
}
/// Starts playing the video.
Future<void> play() async {
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
Future<void> _applyPlayPause() async {
_timer?.cancel();
if (value.isPlaying) {
await _platform.play(_textureId);
_timer = Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
},
);
await _applyPlaybackSpeed();
} else {
await _platform.pause(_textureId);
}
}
Future<void> _applyPlaybackSpeed() async {
if (value.isPlaying) {
await _platform.setPlaybackSpeed(
_textureId,
value.playbackSpeed,
);
}
}
/// The position in the current video.
Future<Duration?> get position async {
return _platform.getPosition(_textureId);
}
/// Sets the video's current timestamp to be at [position].
Future<void> seekTo(Duration position) async {
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await _platform.seekTo(_textureId, position);
_updatePosition(position);
}
/// Sets the playback speed.
Future<void> setPlaybackSpeed(double speed) async {
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
void _updatePosition(Duration position) {
value = value.copyWith(position: position);
}
}
/// Widget that displays the video controlled by [controller].
class VideoPlayer extends StatefulWidget {
/// Uses the given [controller] for all video rendered in this widget.
const VideoPlayer(this.controller, {super.key});
/// The [MiniController] responsible for the video being rendered in
/// this widget.
final MiniController controller;
@override
State<VideoPlayer> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
_VideoPlayerState() {
_listener = () {
final int newTextureId = widget.controller.textureId;
if (newTextureId != _textureId) {
setState(() {
_textureId = newTextureId;
});
}
};
}
late VoidCallback _listener;
late int _textureId;
@override
void initState() {
super.initState();
_textureId = widget.controller.textureId;
// Need to listen for initialization events since the actual texture ID
// becomes available after asynchronous initialization finishes.
widget.controller.addListener(_listener);
}
@override
void didUpdateWidget(VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.controller.removeListener(_listener);
_textureId = widget.controller.textureId;
widget.controller.addListener(_listener);
}
@override
void deactivate() {
super.deactivate();
widget.controller.removeListener(_listener);
}
@override
Widget build(BuildContext context) {
return _textureId == MiniController.kUninitializedTextureId
? Container()
: _platform.buildView(_textureId);
}
}
class _VideoScrubber extends StatefulWidget {
const _VideoScrubber({
required this.child,
required this.controller,
});
final Widget child;
final MiniController controller;
@override
_VideoScrubberState createState() => _VideoScrubberState();
}
class _VideoScrubberState extends State<_VideoScrubber> {
MiniController get controller => widget.controller;
@override
Widget build(BuildContext context) {
void seekToRelativePosition(Offset globalPosition) {
final RenderBox box = context.findRenderObject()! as RenderBox;
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 GestureDetector(
behavior: HitTestBehavior.opaque,
child: widget.child,
onTapDown: (TapDownDetails details) {
if (controller.value.isInitialized) {
seekToRelativePosition(details.globalPosition);
}
},
);
}
}
/// Displays the play/buffering status of the video controlled by [controller].
class VideoProgressIndicator extends StatefulWidget {
/// Construct an instance that displays the play/buffering status of the video
/// controlled by [controller].
const VideoProgressIndicator(this.controller, {super.key});
/// The [MiniController] that actually associates a video with this
/// widget.
final MiniController controller;
@override
State<VideoProgressIndicator> createState() => _VideoProgressIndicatorState();
}
class _VideoProgressIndicatorState extends State<VideoProgressIndicator> {
_VideoProgressIndicatorState() {
listener = () {
if (mounted) {
setState(() {});
}
};
}
late VoidCallback listener;
MiniController 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) {
const Color playedColor = Color.fromRGBO(255, 0, 0, 0.7);
const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2);
const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5);
Widget progressIndicator;
if (controller.value.isInitialized) {
final int duration = controller.value.duration.inMilliseconds;
final int position = controller.value.position.inMilliseconds;
int maxBuffering = 0;
for (final DurationRange range in controller.value.buffered) {
final int end = range.end.inMilliseconds;
if (end > maxBuffering) {
maxBuffering = end;
}
}
progressIndicator = Stack(
fit: StackFit.passthrough,
children: <Widget>[
LinearProgressIndicator(
value: maxBuffering / duration,
valueColor: const AlwaysStoppedAnimation<Color>(bufferedColor),
backgroundColor: backgroundColor,
),
LinearProgressIndicator(
value: position / duration,
valueColor: const AlwaysStoppedAnimation<Color>(playedColor),
backgroundColor: Colors.transparent,
),
],
);
} else {
progressIndicator = const LinearProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(playedColor),
backgroundColor: backgroundColor,
);
}
return _VideoScrubber(
controller: controller,
child: Padding(
padding: const EdgeInsets.only(top: 5.0),
child: progressIndicator,
),
);
}
}