feat: 视频、直播pip Android端

This commit is contained in:
guozhigq
2023-09-07 18:58:58 +08:00
parent 7f961e998c
commit 0dfcd4ed40
7 changed files with 196 additions and 20 deletions

View File

@ -1,3 +1,6 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -19,6 +22,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bool isShowCover = true;
bool isPlay = true;
Floating? floating;
@override
void initState() {
@ -32,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
},
);
if (Platform.isAndroid) {
floating = Floating();
}
}
@override
void dispose() {
plPlayerController!.dispose();
if (floating != null) {
floating!.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
return Scaffold(
Widget childWhenDisabled = Scaffold(
primary: true,
appBar: AppBar(
centerTitle: false,
@ -98,6 +107,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
@ -123,5 +133,25 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
],
),
);
Widget childWhenEnabled = AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
),
)
: const SizedBox(),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
);
} else {
return childWhenDisabled;
}
}
}

View File

@ -1,4 +1,8 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/url.dart';
@ -9,9 +13,11 @@ import 'package:pilipala/utils/storage.dart';
class BottomControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final LiveRoomController? liveRoomCtr;
final Floating? floating;
const BottomControl({
this.controller,
this.liveRoomCtr,
this.floating,
Key? key,
}) : super(key: key);
@ -85,6 +91,35 @@ class _BottomControlState extends State<BottomControl> {
),
),
const SizedBox(width: 4),
if (Platform.isAndroid) ...[
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
bool canUsePiP = false;
widget.controller!.hiddenControls(false);
try {
canUsePiP = await widget.floating!.isPipAvailable;
} on PlatformException catch (_) {
canUsePiP = false;
}
if (canUsePiP) {
await widget.floating!.enable();
} else {}
},
icon: const Icon(
Icons.picture_in_picture_outlined,
size: 18,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
],
ComBtn(
icon: const Icon(
Icons.fullscreen,

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
@ -51,6 +54,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late Future _futureBuilderFuture;
// 自动退出全屏
late bool autoExitFullcreen;
Floating? floating;
@override
void initState() {
@ -60,6 +64,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
videoSourceInit();
appbarStreamListen();
if (Platform.isAndroid) {
floating = Floating();
}
}
// 获取视频资源,初始化播放器
@ -83,7 +90,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
// 播放器状态监听
void playerListener(PlayerStatus? status) {
void playerListener(PlayerStatus? status) async {
playerStatus = status!;
if (status == PlayerStatus.completed) {
// 结束播放退出全屏
@ -91,7 +98,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.triggerFullScreen(status: false);
}
// 播放完展示控制栏
plPlayerController!.onLockControl(false);
PiPStatus currentStatus = await floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false);
}
}
}
@ -116,6 +126,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose();
}
if (floating != null) {
floating!.dispose();
}
super.dispose();
}
@ -162,7 +175,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight;
return SafeArea(
Widget childWhenDisabled = SafeArea(
top: false,
bottom: false,
child: Stack(
@ -209,6 +222,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController,
videoDetailCtr:
videoDetailController,
floating: floating,
),
danmuWidget: Obx(
() => PlDanmaku(
@ -320,13 +334,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
];
},
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回
pinnedHeaderSliverHeightBuilder: () {
return playerStatus != PlayerStatus.playing
? statusBarHeight + kToolbarHeight
: pinnedHeaderHeight;
return pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
body: Container(
key: Key(Get.arguments['heroTag']),
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
@ -402,21 +421,60 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
),
),
/// 重新进入会刷新
// 播放完成/暂停播放
StreamBuilder(
stream: appbarStream.stream,
initialData: 0,
builder: ((context, snapshot) {
return ScrollAppBar(
snapshot.data!.toDouble(),
() => continuePlay(),
playerStatus,
null,
);
}),
)
// StreamBuilder(
// stream: appbarStream.stream,
// initialData: 0,
// builder: ((context, snapshot) {
// return ScrollAppBar(
// snapshot.data!.toDouble(),
// () => continuePlay(),
// playerStatus,
// null,
// );
// }),
// )
],
),
);
Widget childWhenEnabled = FutureBuilder(
key: Key(Get.arguments['heroTag']),
future: _futureBuilderFuture,
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() => !videoDetailController.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
),
danmuWidget: Obx(
() => PlDanmaku(
key: Key(
videoDetailController.danmakuCid.value.toString()),
cid: videoDetailController.danmakuCid.value,
playerController: plPlayerController!,
),
),
),
);
} else {
return const SizedBox();
}
}),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
);
} else {
return childWhenDisabled;
}
}
}

View File

@ -1,4 +1,8 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@ -14,9 +18,11 @@ import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr;
final Floating? floating;
const HeaderControl({
this.controller,
this.videoDetailCtr,
this.floating,
Key? key,
}) : super(key: key);
@ -770,6 +776,39 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
const SizedBox(width: 4),
if (Platform.isAndroid) ...[
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
bool canUsePiP = false;
widget.controller!.hiddenControls(false);
try {
canUsePiP = await widget.floating!.isPipAvailable;
} on PlatformException catch (_) {
canUsePiP = false;
}
if (canUsePiP) {
final aspectRatio = Rational(
widget.videoDetailCtr!.data.dash!.video!.first.width!,
widget.videoDetailCtr!.data.dash!.video!.first.height!,
);
await widget.floating!.enable(aspectRatio: aspectRatio);
} else {}
},
icon: const Icon(
Icons.picture_in_picture_outlined,
size: 19,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
],
Obx(
() => SizedBox(
width: 45,

View File

@ -752,6 +752,10 @@ class PlPlayerController {
}
}
void hiddenControls(bool val) {
showControls.value = val;
}
/// 设置长按倍速状态 live模式下禁用
void setDoubleSpeedStatus(bool val) {
if (videoType.value == 'live') {

View File

@ -417,6 +417,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
floating:
dependency: "direct main"
description:
name: floating
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter:
dependency: "direct main"
description: flutter

View File

@ -117,6 +117,8 @@ dependencies:
status_bar_control: ^3.2.1
# 代理
system_proxy: ^0.1.0
# pip
floating: ^2.0.1
dev_dependencies:
flutter_test: