mod: 视频播放器重构 - 基本功能

This commit is contained in:
guozhigq
2023-07-30 22:44:18 +08:00
parent ca12be5373
commit 636ff2b9ad
31 changed files with 1583 additions and 390 deletions

View File

@ -0,0 +1,645 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/painting.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/plugin/pl_player/models/data_source.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:volume_controller/volume_controller.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'models/data_status.dart';
import 'models/play_status.dart';
Box videoStorage = GStrorage.video;
class PlPlayerController {
Player? _videoPlayerController;
VideoController? _videoController;
// 流事件 监听播放状态变化
StreamSubscription? _playerEventSubs;
/// [playerStatus] has a [status] observable
final PlPlayerStatus playerStatus = PlPlayerStatus();
///
final PlPlayerDataStatus dataStatus = PlPlayerDataStatus();
bool controlsEnabled = true;
/// 响应数据
// 播放位置
final Rx<Duration> _position = Rx(Duration.zero);
final Rx<Duration> _sliderPosition = Rx(Duration.zero);
final Rx<Duration> _duration = Rx(Duration.zero);
final Rx<Duration> _buffered = Rx(Duration.zero);
final Rx<double> _playbackSpeed = 1.0.obs;
final Rx<double> _currentVolume = 1.0.obs;
final Rx<double> _currentBrightness = 0.0.obs;
final Rx<bool> _mute = false.obs;
final Rx<bool> _showControls = false.obs;
final Rx<bool> _showVolumeStatus = false.obs;
final Rx<bool> _showBrightnessStatus = false.obs;
final Rx<bool> _doubleSpeedStatus = false.obs;
final Rx<bool> _controlsClose = false.obs;
Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
///
bool _isSliderMoving = false;
PlaylistMode _looping = PlaylistMode.none;
bool _autoPlay = false;
final bool _listenersInitialized = false;
Timer? _timer;
Timer? _timerForSeek;
Timer? _timerForVolume;
Timer? _timerForShowingVolume;
Timer? _timerForGettingVolume;
Timer? timerForTrackingMouse;
Timer? videoFitChangedTimer;
// final Durations durations;
List<BoxFit> fits = [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
BoxFit.fitHeight,
BoxFit.fitWidth,
BoxFit.scaleDown
];
/// 数据加载监听
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
/// 播放状态监听
Stream<PlayerStatus> get onPlayerStatusChanged => playerStatus.status.stream;
/// 视频时长
Rx<Duration> get duration => _duration;
Stream<Duration> get onDurationChanged => _duration.stream;
/// 视频当前播放位置
Rx<Duration> get position => _position;
Stream<Duration> get onPositionChanged => _position.stream;
/// 视频播放速度
double get playbackSpeed => _playbackSpeed.value;
/// 视频缓冲
Rx<Duration> get buffered => _buffered;
Stream<Duration> get onBufferedChanged => _buffered.stream;
// 视频静音
Rx<bool> get mute => _mute;
Stream<bool> get onMuteChanged => _mute.stream;
/// [videoPlayerController] instace of Player
Player? get videoPlayerController => _videoPlayerController;
/// [videoController] instace of Player
VideoController? get videoController => _videoController;
/// 进度条位置及监听
Rx<Duration> get sliderPosition => _sliderPosition;
Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
/// 是否展示控制条及监听
Rx<bool> get showControls => _showControls;
Stream<bool> get onShowControlsChanged => _showControls.stream;
/// 音量控制条展示/隐藏
Rx<bool> get showVolumeStatus => _showVolumeStatus;
Stream<bool> get onShowVolumeStatusChanged => _showVolumeStatus.stream;
/// 亮度控制条展示/隐藏
Rx<bool> get showBrightnessStatus => _showBrightnessStatus;
Stream<bool> get onShowBrightnessStatusChanged =>
_showBrightnessStatus.stream;
/// 音量控制条
Rx<double> get volume => _currentVolume;
Stream<double> get onVolumeChanged => _currentVolume.stream;
/// 亮度控制条
Rx<double> get brightness => _currentBrightness;
Stream<double> get onBrightnessChanged => _currentBrightness.stream;
/// 是否循环
PlaylistMode get looping => _looping;
/// 是否自动播放
bool get autoplay => _autoPlay;
/// 视频比例
Rx<BoxFit> get videoFit => _videoFit;
/// 是否长按倍速
Rx<bool> get doubleSpeedStatus => _doubleSpeedStatus;
Rx<bool> isBuffering = true.obs;
Rx<bool> get controlsClose => _controlsClose;
PlPlayerController({
this.controlsEnabled = true,
this.fits = const [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
BoxFit.fitHeight,
BoxFit.fitWidth,
BoxFit.scaleDown
],
}) {
controlsEnabled = controlsEnabled;
_playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
if (status == PlayerStatus.playing) {
WakelockPlus.enable();
} else {
WakelockPlus.enable();
}
});
}
// 初始化资源
Future<void> setDataSource(
DataSource dataSource, {
bool autoplay = true,
// 默认不循环
PlaylistMode looping = PlaylistMode.single,
// 初始化播放位置
Duration seekTo = Duration.zero,
// 初始化播放速度
double speed = 1.0,
// 硬件加速
bool enableHA = true,
double? width,
double? height,
Duration? duration,
}) async {
try {
_autoPlay = autoplay;
_looping = looping;
// 初始化视频时长
_duration.value = duration ?? Duration.zero;
// 初始化视频倍速
_playbackSpeed.value = speed;
dataStatus.status.value = DataStatus.loading;
if (_videoPlayerController != null &&
_videoPlayerController!.state.playing) {
await pause(notify: false);
}
_videoPlayerController = await _createVideoController(
dataSource, _looping, enableHA, width, height);
_duration.value = _videoPlayerController!.state.duration;
dataStatus.status.value = DataStatus.loaded;
await _initializePlayer(seekTo: seekTo);
// listen the video player events
if (!_listenersInitialized) {
startListeners();
}
} catch (err) {
dataStatus.status.value = DataStatus.error;
print('plPlayer err: $err');
}
}
// 配置播放器
Future<Player> _createVideoController(
DataSource dataSource,
PlaylistMode looping,
bool enableHA,
double? width,
double? height,
) async {
Player player = _videoPlayerController ??
Player(
configuration: const PlayerConfiguration(
// 默认缓存 5M 大小
bufferSize: 5 * 1024 * 1024,
),
);
var pp = player.platform as NativePlayer;
// 音轨
if (dataSource.audioSource != '' && dataSource.audioSource != null) {
await pp.setProperty(
'audio-files',
UniversalPlatform.isWindows
? dataSource.audioSource!.replaceAll(';', '\\;')
: dataSource.audioSource!.replaceAll(':', '\\:'),
);
}
// 字幕
if (dataSource.subFiles != '' && dataSource.subFiles != null) {
await pp.setProperty(
'sub-files',
UniversalPlatform.isWindows
? dataSource.subFiles!.replaceAll(';', '\\;')
: dataSource.subFiles!.replaceAll(':', '\\:'),
);
await pp.setProperty("subs-with-matching-audio", "no");
await pp.setProperty("sub-forced-only", "yes");
await pp.setProperty("blend-subtitles", "video");
}
_videoController = _videoController ??
VideoController(
player,
configuration: VideoControllerConfiguration(
enableHardwareAcceleration: enableHA,
),
);
player.setPlaylistMode(looping);
if (dataSource.type == DataSourceType.asset) {
final assetUrl = dataSource.videoSource!.startsWith("asset://")
? dataSource.videoSource!
: "asset://${dataSource.videoSource!}";
player.open(
Media(assetUrl, httpHeaders: dataSource.httpHeaders),
play: false,
);
} else if (dataSource.type == DataSourceType.network) {
player.open(
Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders),
play: false,
);
// 音轨
// player.setAudioTrack(
// AudioTrack.uri(dataSource.audioSource!),
// );
} else {
player.open(
Media(dataSource.file!.path, httpHeaders: dataSource.httpHeaders),
play: false,
);
}
return player;
}
// 开始播放
Future _initializePlayer({
Duration seekTo = Duration.zero,
}) async {
// 跳转播放
if (seekTo != Duration.zero) {
await this.seekTo(seekTo);
}
// 设置倍速
if (_playbackSpeed.value != 1.0) {
await setPlaybackSpeed(_playbackSpeed.value);
}
// if (_looping) {
// await setLooping(_looping);
// }
// 自动播放
if (_autoPlay) {
await play();
}
}
List<StreamSubscription> subscriptions = [];
/// 播放事件监听
void startListeners() {
subscriptions.addAll(
[
videoPlayerController!.stream.playing.listen((event) {
if (event) {
playerStatus.status.value = PlayerStatus.playing;
} else {
// playerStatus.status.value = PlayerStatus.paused;
}
}),
videoPlayerController!.stream.completed.listen((event) {
if (event) {
playerStatus.status.value = PlayerStatus.completed;
} else {
// playerStatus.status.value = PlayerStatus.playing;
}
}),
videoPlayerController!.stream.position.listen((event) {
_position.value = event;
if (!_isSliderMoving) {
_sliderPosition.value = event;
}
}),
videoPlayerController!.stream.duration.listen((event) {
duration.value = event;
}),
videoPlayerController!.stream.buffer.listen((event) {
_buffered.value = event;
}),
videoPlayerController!.stream.buffering.listen((event) {
isBuffering.value = event;
}),
// videoPlayerController!.stream.volume.listen((event) {
// if (!mute.value && _volumeBeforeMute != event) {
// _volumeBeforeMute = event / 100;
// }
// }),
],
);
}
/// 移除事件监听
void removeListeners() {
for (final s in subscriptions) {
s.cancel();
}
}
/// 跳转至指定位置
Future<void> seekTo(Duration position) async {
// if (position >= duration.value) {
// position = duration.value - const Duration(milliseconds: 100);
// }
if (position < Duration.zero) {
position = Duration.zero;
}
_position.value = position;
if (duration.value.inSeconds != 0) {
// await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
// play();
// }
} else {
_timerForSeek?.cancel();
_timerForSeek =
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
//_timerForSeek = null;
if (duration.value.inSeconds != 0) {
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
// play();
// }
t.cancel();
//_timerForSeek = null;
}
});
}
}
/// 设置倍速
Future<void> setPlaybackSpeed(double speed) async {
await _videoPlayerController?.setRate(speed);
_playbackSpeed.value = speed;
}
/// 设置倍速
Future<void> togglePlaybackSpeed() async {
List<double> allowedSpeeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
if (allowedSpeeds.indexOf(_playbackSpeed.value) <
allowedSpeeds.length - 1) {
setPlaybackSpeed(
allowedSpeeds[allowedSpeeds.indexOf(_playbackSpeed.value) + 1]);
} else {
setPlaybackSpeed(allowedSpeeds[0]);
}
}
/// 播放视频
Future<void> play({bool repeat = false, bool hideControls = true}) async {
// repeat为true将从头播放
if (repeat) {
await seekTo(Duration.zero);
}
await _videoPlayerController?.play();
await getCurrentVolume();
await getCurrentBrightness();
playerStatus.status.value = PlayerStatus.playing;
// screenManager.setOverlays(false);
// 播放时自动隐藏控制条
if (hideControls) {
_hideTaskControls();
}
}
/// 暂停播放
Future<void> pause({bool notify = true}) async {
await _videoPlayerController?.pause();
playerStatus.status.value = PlayerStatus.paused;
}
/// 更改播放状态
Future<void> togglePlay() async {
if (playerStatus.playing) {
pause();
} else {
play();
}
}
/// 隐藏控制条
void _hideTaskControls() {
_timer = Timer(const Duration(milliseconds: 3000), () {
if (!_isSliderMoving) {
controls = false;
}
_timer = null;
});
}
/// 调整播放时间
onChangedSlider(double v) {
_sliderPosition.value = Duration(seconds: v.floor());
}
void onChangedSliderStart() {
_isSliderMoving = true;
}
void onChangedSliderEnd() {
_isSliderMoving = false;
_hideTaskControls();
}
/// 音量
Future<void> getCurrentVolume() async {
_currentVolume.value = await VolumeController().getVolume();
}
Future<void> setVolume(double volumeNew,
{bool videoPlayerVolume = false}) async {
if (volumeNew < 0.0) {
volumeNew = 0.0;
} else if (volumeNew > 1.0) {
volumeNew = 1.0;
}
if (volume.value == volumeNew) {
return;
}
volume.value = volumeNew;
try {
VolumeController().setVolume(volumeNew, showSystemUI: false);
} catch (err) {
print(err);
}
}
void volumeUpdated() {
showVolumeStatus.value = true;
_timerForShowingVolume?.cancel();
_timerForShowingVolume = Timer(const Duration(seconds: 1), () {
showVolumeStatus.value = false;
});
}
/// 亮度
Future<void> getCurrentBrightness() async {
try {
_currentBrightness.value = await ScreenBrightness().current;
} catch (e) {
throw 'Failed to get current brightness';
//return 0;
}
}
Future<void> setBrightness(double brightnes) async {
try {
brightness.value = brightnes;
ScreenBrightness().setScreenBrightness(brightnes);
setVideoBrightness();
} catch (e) {
throw 'Failed to set brightness';
}
}
Future<void> resetBrightness() async {
try {
await ScreenBrightness().resetScreenBrightness();
} catch (e) {
throw 'Failed to reset brightness';
}
}
/// Toggle Change the videofit accordingly
void toggleVideoFit() {
videoFitChangedTimer?.cancel();
videoFitChanged.value = true;
if (fits.indexOf(_videoFit.value) < fits.length - 1) {
_videoFit.value = fits[fits.indexOf(_videoFit.value) + 1];
} else {
_videoFit.value = fits[0];
}
videoFitChangedTimer = Timer(const Duration(seconds: 1), () {
videoFitChangedTimer = null;
videoFitChanged.value = false;
});
print(_videoFit.value);
}
/// Change Video Fit accordingly
void onVideoFitChange(BoxFit fit) {
_videoFit.value = fit;
}
/// 缓存fit
Future<void> setVideoFit() async {
videoStorage.put(VideoBoxKey.videoBrightness, _videoFit.value.name);
}
/// 读取fit
Future<void> getVideoFit() async {
String fitValue = videoStorage.get(VideoBoxKey.videoBrightness,
defaultValue: 'fitHeight');
_videoFit.value = fits.firstWhere((element) => element.name == fitValue);
}
/// 缓存亮度
Future<void> setVideoBrightness() async {}
/// 读取亮度
Future<void> getVideoBrightness() async {
double brightnessValue =
videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 0.5);
setBrightness(brightnessValue);
}
set controls(bool visible) {
_showControls.value = visible;
_timer?.cancel();
if (visible) {
_hideTaskControls();
}
}
/// 设置长按倍速状态
void setDoubleSpeedStatus(bool val) {
_doubleSpeedStatus.value = val;
}
/// 关闭控制栏
void onCloseControl(bool val) {
_controlsClose.value = val;
showControls.value = !val;
}
/// 截屏
Future screenshot() async {
final Uint8List? screenshot =
await _videoPlayerController!.screenshot(format: 'image/png');
return screenshot;
}
Future<void> videoPlayerClosed() async {
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();
timerForTrackingMouse?.cancel();
_timerForSeek?.cancel();
videoFitChangedTimer?.cancel();
}
Future<void> dispose() async {
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();
timerForTrackingMouse?.cancel();
_timerForSeek?.cancel();
videoFitChangedTimer?.cancel();
_position.close();
_playerEventSubs?.cancel();
_sliderPosition.close();
_duration.close();
_buffered.close();
_showControls.close();
_controlsClose.close();
playerStatus.status.close();
dataStatus.status.close();
removeListeners();
await _videoPlayerController?.dispose();
_videoPlayerController = null;
}
}

View File

@ -0,0 +1,7 @@
library pl_player;
export './controller.dart';
export './view.dart';
export './models/data_source.dart';
export './models/play_status.dart';
export './models/data_status.dart';

View File

@ -0,0 +1,55 @@
import 'dart:io';
/// The way in which the video was originally loaded.
///
/// This has nothing to do with the video's file type. It's just the place
/// from which the video is fetched from.
enum DataSourceType {
/// The video was included in the app's asset files.
asset,
/// The video was downloaded from the internet.
network,
/// The video was loaded off of the local filesystem.
file,
/// The video is available via contentUri. Android only.
contentUri,
}
class DataSource {
File? file;
String? videoSource;
String? audioSource;
String? subFiles;
DataSourceType type;
Map<String, String>? httpHeaders; // for headers
DataSource({
this.file,
this.videoSource,
this.audioSource,
this.subFiles,
required this.type,
this.httpHeaders,
}) : assert((type == DataSourceType.file && file != null) ||
videoSource != null);
DataSource copyWith({
File? file,
String? videoSource,
String? audioSource,
String? subFiles,
DataSourceType? type,
Map<String, String>? httpHeaders,
}) {
return DataSource(
file: file ?? this.file,
videoSource: videoSource ?? this.videoSource,
audioSource: audioSource ?? this.audioSource,
subFiles: subFiles ?? this.subFiles,
type: type ?? this.type,
httpHeaders: httpHeaders ?? this.httpHeaders,
);
}
}

View File

@ -0,0 +1,12 @@
import 'package:get/get.dart';
enum DataStatus { none, loading, loaded, error }
class PlPlayerDataStatus {
Rx<DataStatus> status = Rx(DataStatus.none);
bool get none => status.value == DataStatus.none;
bool get loading => status.value == DataStatus.loading;
bool get loaded => status.value == DataStatus.loaded;
bool get error => status.value == DataStatus.error;
}

View File

@ -0,0 +1,19 @@
import 'package:get/get.dart';
enum PlayerStatus { completed, playing, paused }
class PlPlayerStatus {
Rx<PlayerStatus> status = Rx(PlayerStatus.paused);
bool get playing {
return status.value == PlayerStatus.playing;
}
bool get paused {
return status.value == PlayerStatus.paused;
}
bool get completed {
return status.value == PlayerStatus.completed;
}
}

View File

@ -0,0 +1,29 @@
String printDuration(Duration? duration) {
if (duration == null) return "--:--";
/*String twoDigits(int n) {
if (n >= 10||n < 0) return "$n";
return "0$n";
}*/
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes).replaceAll("-", "");
String twoDigitSeconds =
twoDigits(duration.inSeconds.remainder(60)).replaceAll("-", "");
//customDebugPrint(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
String printDurationWithHours(Duration? duration) {
if (duration == null) return "--:--:--";
String twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
String twoDigitHours = twoDigits(duration.inHours);
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds";
}

View File

@ -0,0 +1,252 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/common/widgets/app_bar_ani.dart';
import 'package:pilipala/plugin/pl_player/controller.dart';
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'widgets/bottom_control.dart';
import 'widgets/common_btn.dart';
import 'widgets/header_control.dart';
class PLVideoPlayer extends StatefulWidget {
final PlPlayerController controller;
const PLVideoPlayer({required this.controller, super.key});
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
}
class _PLVideoPlayerState extends State<PLVideoPlayer>
with TickerProviderStateMixin {
late AnimationController animationController;
late VideoController videoController;
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
}
@override
void dispose() {
super.dispose();
animationController.dispose();
}
@override
Widget build(BuildContext context) {
final _ = widget.controller;
Color colorTheme = Theme.of(context).colorScheme.primary;
TextStyle subTitleStyle = const TextStyle(
height: 1.5,
fontSize: 40.0,
letterSpacing: 0.0,
wordSpacing: 0.0,
color: Color(0xffffffff),
fontWeight: FontWeight.normal,
backgroundColor: Color(0xaa000000),
);
return Stack(
clipBehavior: Clip.hardEdge,
fit: StackFit.passthrough,
children: [
Video(
controller: videoController,
controls: NoVideoControls,
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
textAlign: TextAlign.center,
padding: const EdgeInsets.all(24.0),
),
),
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 15),
child: GestureDetector(
onTap: () {
_.controls = !_.showControls.value;
},
onDoubleTap: () {
if (_.playerStatus.status.value == PlayerStatus.playing) {
_.togglePlay();
} else {
_.play();
}
},
onLongPressStart: (detail) {
feedBack();
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(true);
_.setPlaybackSpeed(currentSpeed * 2);
},
onLongPressEnd: (details) {
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(false);
_.setPlaybackSpeed(currentSpeed / 2);
},
// 水平位置 快进
onHorizontalDragUpdate: (DragUpdateDetails details) {},
onHorizontalDragEnd: (DragEndDetails details) {},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) {},
onVerticalDragEnd: (DragEndDetails details) {}),
),
if (_.controlsEnabled)
Obx(
() => Column(
children: [
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsClose.value && _.showControls.value,
position: 'top',
child: HeaderControl(controller: widget.controller),
),
),
const Spacer(),
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsClose.value && _.showControls.value,
position: 'bottom',
child: BottomControl(controller: widget.controller),
),
),
],
),
),
// 进度条
Obx(
() {
final int value = _.sliderPosition.value.inSeconds;
final int max = _.duration.value.inSeconds;
final int buffer = _.buffered.value.inSeconds;
if (value > max || max <= 0) {
return Container();
}
return Positioned(
bottom: -4,
left: 0,
right: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, -1),
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeInOut,
)),
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor:
Theme.of(context).colorScheme.primary.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3,
thumbRadius: 0.0,
onDragStart: (duration) {
_.onChangedSliderStart();
},
onDragEnd: () {
_.onChangedSliderEnd();
},
// onDragUpdate: (details) {
// print(details);
// },
onSeek: (duration) {
print(duration);
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(duration);
},
)),
);
},
),
// 长按倍速
Obx(
() => Align(
alignment: Alignment.topCenter,
child: FractionalTranslation(
translation: const Offset(0.0, 1.5), // 上下偏移量(负数向上偏移)
child: Visibility(
visible: _.doubleSpeedStatus.value,
child: const Text(
'** 倍速中 **',
style: TextStyle(
fontSize: 13,
backgroundColor: Color(0xaa000000),
color: Colors.white,
),
),
),
),
),
),
// 锁
Obx(
() => Align(
alignment: Alignment.centerLeft,
child: FractionalTranslation(
translation: const Offset(0.5, 0.0),
child: Visibility(
visible: _.showControls.value,
child: ComBtn(
icon: Icon(
_.controlsClose.value
? FontAwesomeIcons.lock
: FontAwesomeIcons.lockOpen,
size: 15,
color: Colors.white,
),
fuc: () => _.onCloseControl(!_.controlsClose.value),
),
),
),
),
),
//
Obx(() {
if (_.dataStatus.loading || _.isBuffering.value) {
return Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
);
} else {
return Container();
}
}),
],
);
}
}
class MSliderTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData? sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double trackLeft = offset.dx;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, -1, trackWidth, 3);
}
}
class PLPlayerCtr extends GetxController {}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
class AppBarAni extends StatelessWidget implements PreferredSizeWidget {
const AppBarAni({
required this.child,
required this.controller,
required this.visible,
this.position,
Key? key,
}) : super(key: key);
final PreferredSizeWidget child;
final AnimationController controller;
final bool visible;
final String? position;
@override
Size get preferredSize => child.preferredSize;
@override
Widget build(BuildContext context) {
visible ? controller.reverse() : controller.forward();
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: Offset(0, position! == 'top' ? -1 : 1),
).animate(CurvedAnimation(
parent: controller,
curve: Curves.linear,
)),
child: Container(
decoration: BoxDecoration(
gradient: position! == 'top'
? const LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
)
: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
),
),
child: child,
),
);
}
}

View File

@ -0,0 +1,149 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import '../utils.dart';
import 'common_btn.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
const BottomControl({this.controller, Key? key}) : super(key: key);
@override
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
Widget build(BuildContext context) {
Color colorTheme = Theme.of(context).colorScheme.primary;
final _ = controller!;
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
toolbarHeight: 73,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Column(
children: [
const SizedBox(height: 23),
Obx(
() {
final int value = _.sliderPosition.value.inSeconds;
final int max = _.duration.value.inSeconds;
final int buffer = _.buffered.value.inSeconds;
if (value > max || max <= 0) {
return Container();
}
return ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: colorTheme.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.0,
thumbRadius: 5.5,
onDragStart: (duration) {
_.onChangedSliderStart();
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(Duration(seconds: duration.inSeconds));
},
);
},
),
Row(
children: [
Obx(
() => ComBtn(
icon: Icon(
_.playerStatus.paused
? FontAwesomeIcons.play
: _.playerStatus.playing
? FontAwesomeIcons.pause
: FontAwesomeIcons.rotateRight,
size: 15,
color: Colors.white,
),
fuc: () => _.togglePlay(),
),
),
const SizedBox(width: 6),
// 播放时间
Obx(() {
return Text(
_.duration.value.inMinutes >= 60
? printDurationWithHours(_.position.value)
: printDuration(_.position.value),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.duration.value.inMinutes >= 60
? printDurationWithHours(_.duration.value)
: printDuration(_.duration.value),
style: textStyle,
),
),
const Spacer(),
// 倍速
Obx(
() => SizedBox(
width: 45,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.togglePlaybackSpeed();
},
child: Text(
'${_.playbackSpeed.toString()}X',
style: textStyle,
),
),
),
),
ComBtn(
icon: const Icon(
Icons.fit_screen_sharp,
size: 18,
color: Colors.white,
),
fuc: () => _.toggleVideoFit(),
),
const SizedBox(width: 4),
// 全屏
ComBtn(
icon: const Icon(
FontAwesomeIcons.expand,
size: 15,
color: Colors.white,
),
fuc: () => {},
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class ComBtn extends StatelessWidget {
final Widget? icon;
final Function? fuc;
const ComBtn({
this.icon,
this.fuc,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
fuc!();
},
icon: icon!,
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'common_btn.dart';
class HeaderControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
const HeaderControl({this.controller, Key? key}) : super(key: key);
@override
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
Widget build(BuildContext context) {
final _ = controller!;
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
centerTitle: false,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Row(
children: [
ComBtn(
icon: const Icon(
FontAwesomeIcons.arrowLeft,
size: 15,
color: Colors.white,
),
fuc: () => Get.back(),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.house,
size: 15,
color: Colors.white,
),
fuc: () => Get.back(),
),
const Spacer(),
ComBtn(
icon: const Icon(
FontAwesomeIcons.cropSimple,
size: 15,
color: Colors.white,
),
fuc: () => _.screenshot(),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.sliders,
size: 15,
color: Colors.white,
),
fuc: () => _.screenshot(),
),
],
),
);
}
}