feat: 全屏播放

This commit is contained in:
guozhigq
2023-08-02 23:42:36 +08:00
parent d512bc0bfd
commit ffd1f0a24a
12 changed files with 197 additions and 60 deletions

View File

@ -1,4 +1,6 @@
PODS:
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
@ -41,6 +43,7 @@ PODS:
- Flutter
DEPENDENCIES:
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
@ -65,6 +68,8 @@ SPEC REPOS:
- ReachabilitySwift
EXTERNAL SOURCES:
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
@ -101,6 +106,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854

View File

@ -98,10 +98,12 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
Box localCache = GStrorage.localCache;
double statusBarHeight = MediaQuery.of(context).padding.top;
double sheetHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).size.width * 9 / 16;
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);
return Scaffold(
body: FadeTransition(
opacity: _fadeAnimation!,

View File

@ -109,21 +109,21 @@ class VideoDetailController extends GetxController
/// 根据currentVideoQa 重新设置videoUrl
VideoItem firstVideo =
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
String videoUrl = firstVideo.baseUrl!;
// String videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
String audioUrl = firstAudio.baseUrl ?? '';
playerInit(videoUrl, audioUrl, defaultST: position);
playerInit(firstVideo, audioUrl, defaultST: position);
}
playerInit(source, audioSource,
playerInit(firstVideo, audioSource,
{Duration defaultST = Duration.zero, int duration = 0}) async {
plPlayerController.setDataSource(
DataSource(
videoSource: source,
videoSource: firstVideo.baseUrl,
audioSource: audioSource,
type: DataSourceType.network,
httpHeaders: {
@ -137,6 +137,9 @@ class VideoDetailController extends GetxController
autoplay: autoPlay.value,
seekTo: defaultST,
duration: Duration(milliseconds: duration),
// 宽>高 水平 否则 垂直
direction:
firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical',
);
}
@ -153,7 +156,7 @@ class VideoDetailController extends GetxController
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
VideoItem firstVideo = data.dash!.video!.first;
String videoUrl = firstVideo.baseUrl!;
// String videoUrl = firstVideo.baseUrl!;
//
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
@ -162,15 +165,17 @@ class VideoDetailController extends GetxController
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
String audioUrl = firstAudio.baseUrl ?? '';
//
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
if (firstAudio.id != null) {
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
}
playerInit(
videoUrl,
firstVideo,
audioUrl,
defaultST: Duration(milliseconds: data.lastPlayTime!),
duration: data.timeLength ?? 0,
);
}
return result;
}
void loopHeartBeat() {

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
@ -11,6 +12,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart';
import 'widgets/header_control.dart';
@ -32,11 +34,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream;
bool isPlay = false;
PlayerStatus playerStatus = PlayerStatus.playing;
bool isShowCover = true;
double doubleOffset = 0;
Box localCache = GStrorage.localCache;
late double statusBarHeight;
final videoHeight = Get.size.width * 9 / 16;
@override
void initState() {
super.initState();
@ -46,14 +51,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.markHeartBeat();
playerStatus = status;
if (status == PlayerStatus.playing) {
isPlay = false;
isShowCover = false;
setState(() {});
videoDetailController.loopHeartBeat();
} else {
videoDetailController.timer!.cancel();
isPlay = true;
setState(() {});
// setState(() {});
// 播放完成停止 or 切换下一个
if (status == PlayerStatus.completed) {
// 当只有1p或多p未打开自动播放时播放完成还原进度条展示控制栏
@ -73,6 +76,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStream.add(offset);
},
);
statusBarHeight = localCache.get('statusBarHeight');
}
void continuePlay() async {
@ -121,7 +126,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight;
@ -133,7 +137,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
Scaffold(
resizeToAvoidBottomInset: false,
key: videoDetailController.scaffoldKey,
backgroundColor: Colors.transparent,
// fix 1px black line
// backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
body: ExtendedNestedScrollView(
controller: _extendNestCtr,
headerSliverBuilder:
@ -150,8 +156,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
backgroundColor: Theme.of(context).colorScheme.background,
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top),
padding: EdgeInsets.only(top: statusBarHeight),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
@ -187,33 +192,31 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
/// 关闭自动播放时 手动播放
Obx(
() => Visibility(
visible: isShowCover &&
videoDetailController
.isEffective.value &&
!videoDetailController.autoPlay.value,
child: Positioned(
right: 12,
bottom: 6,
child: TextButton.icon(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty
.resolveWith((states) {
return Theme.of(context)
.colorScheme
.primaryContainer;
}),
),
onPressed: () => videoDetailController
.handlePlay(),
icon: const Icon(
Icons.play_circle_outline,
size: 20,
),
label: const Text('Play'),
Visibility(
visible: isShowCover &&
videoDetailController
.isEffective.value &&
!videoDetailController.autoPlay.value,
child: Positioned(
right: 12,
bottom: 6,
child: TextButton.icon(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primaryContainer;
}),
),
onPressed: () =>
videoDetailController.handlePlay(),
icon: const Icon(
Icons.play_circle_outline,
size: 20,
),
label: const Text('Play'),
),
),
),

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@ -53,6 +54,9 @@ class PlPlayerController {
final Rx<bool> _showBrightnessStatus = false.obs;
final Rx<bool> _doubleSpeedStatus = false.obs;
final Rx<bool> _controlsLock = false.obs;
final Rx<bool> _isFullScreen = false.obs;
final Rx<String> _direction = 'horizontal'.obs;
Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
@ -82,6 +86,8 @@ class PlPlayerController {
BoxFit.scaleDown
];
PreferredSizeWidget? headerControl;
/// 数据加载监听
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
@ -160,6 +166,12 @@ class PlPlayerController {
/// 屏幕锁 为true时关闭控制栏
Rx<bool> get controlsLock => _controlsLock;
/// 全屏状态
Rx<bool> get isFullScreen => _isFullScreen;
/// 全屏方向
Rx<String> get direction => _direction;
PlPlayerController({
// 直播间 传false 关闭控制栏
this.controlsEnabled = true,
@ -197,6 +209,9 @@ class PlPlayerController {
double? width,
double? height,
Duration? duration,
// 方向
String? direction,
// 全屏模式
}) async {
try {
_autoPlay = autoplay;
@ -207,6 +222,8 @@ class PlPlayerController {
_playbackSpeed.value = speed;
// 初始化数据加载状态
dataStatus.status.value = DataStatus.loading;
// 初始化全屏方向
_direction.value = direction ?? 'horizontal';
if (_videoPlayerController != null &&
_videoPlayerController!.state.playing) {
@ -624,6 +641,10 @@ class PlPlayerController {
showControls.value = !val;
}
void toggleFullScreen(bool val) {
_isFullScreen.value = val;
}
/// 截屏
Future screenshot() async {
final Uint8List? screenshot =

View File

@ -8,3 +8,5 @@ export './models/data_status.dart';
export './widgets/common_btn.dart';
export './models/play_speed.dart';
export './widgets/app_bar_ani.dart';
export './utils/fullscreen.dart';
export './utils.dart';

View File

@ -0,0 +1,9 @@
// 全屏模式
enum FullScreenMode {
// 根据内容自适应
auto,
// 始终竖屏
vertical,
// 始终横屏
horizontal
}

View File

@ -0,0 +1,40 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/services.dart';
//横屏
/// 低版本xcode不支持auto_orientation
Future<void> landScape() async {
if (Platform.isAndroid || Platform.isIOS) {
await AutoOrientation.landscapeAutoMode(forceSensor: true);
}
}
//竖屏
Future<void> verticalScreen() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
}
Future<void> enterFullScreen() async {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
);
}
//退出全屏显示
Future<void> exitFullScreen() async {
late SystemUiMode mode;
if ((Platform.isAndroid &&
(await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) ||
!Platform.isAndroid) {
mode = SystemUiMode.edgeToEdge;
} else {
mode = SystemUiMode.manual;
}
await SystemChrome.setEnabledSystemUIMode(mode,
overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
}

View File

@ -75,6 +75,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
Future.microtask(() async {
try {
@ -149,6 +150,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
@override
Widget build(BuildContext context) {
print('🌹🌹🌹🌹🌹33333');
final _ = widget.controller;
Color colorTheme = Theme.of(context).colorScheme.primary;
TextStyle subTitleStyle = const TextStyle(
@ -387,6 +389,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 双击左边区域 👈
onDoubleTapSeekBackward();
} else if (tapPosition < sectionWidth * 2) {
print('🌹🌹🌹🌹🌹333356555553');
if (_.playerStatus.status.value == PlayerStatus.playing) {
_.togglePlay();
} else {
@ -443,15 +446,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Obx(
() => Column(
children: [
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsLock.value && _.showControls.value,
position: 'top',
child: widget.headerControl!,
if (widget.headerControl != null)
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsLock.value && _.showControls.value,
position: 'top',
child: widget.headerControl!,
),
),
),
const Spacer(),
ClipRect(
clipBehavior: Clip.hardEdge,

View File

@ -5,8 +5,6 @@ import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
import '../utils.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
const BottomControl({this.controller, Key? key}) : super(key: key);
@ -138,14 +136,52 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
// ),
// const SizedBox(width: 4),
// 全屏
ComBtn(
icon: const Icon(
FontAwesomeIcons.expand,
size: 15,
color: Colors.white,
),
fuc: () => {},
),
Obx(() => ComBtn(
icon: Icon(
_.isFullScreen.value
? FontAwesomeIcons.a
: FontAwesomeIcons.expand,
size: 15,
color: Colors.white,
),
fuc: () async {
if (!_.isFullScreen.value) {
/// 按照视频宽高比决定全屏方向
if (_.direction.value == 'horizontal') {
/// 进入全屏
await enterFullScreen();
// 横屏
await landScape();
} else {
// 竖屏
await verticalScreen();
}
_.toggleFullScreen(true);
var result = await showDialog(
context: Get.context!,
builder: (context) => Dialog.fullscreen(
backgroundColor: Colors.black,
child: PLVideoPlayer(
controller: _,
headerControl: _.headerControl,
),
),
);
if (result == null) {
// 退出全屏
exitFullScreen();
await verticalScreen();
_.toggleFullScreen(false);
}
} else {
Get.back();
exitFullScreen();
await verticalScreen();
_.toggleFullScreen(false);
}
},
)),
],
),
],

View File

@ -49,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
auto_orientation:
dependency: "direct main"
description:
name: auto_orientation
sha256: cd56bb59b36fa54cc28ee254bc600524f022a4862f31d5ab20abd7bb1c54e678
url: "https://pub.dev"
source: hosted
version: "2.3.1"
boolean_selector:
dependency: transitive
description:

View File

@ -107,6 +107,7 @@ dependencies:
universal_platform: ^1.0.0+1
# 进度条
audio_video_progress_bar: ^1.0.1
auto_orientation: ^2.3.1
dev_dependencies:
flutter_test: