feat: 视频、直播pip Android端
This commit is contained in:
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:floating/floating.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
@ -19,6 +22,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
|
|
||||||
bool isShowCover = true;
|
bool isShowCover = true;
|
||||||
bool isPlay = true;
|
bool isPlay = true;
|
||||||
|
Floating? floating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -32,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
floating = Floating();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
plPlayerController!.dispose();
|
plPlayerController!.dispose();
|
||||||
|
if (floating != null) {
|
||||||
|
floating!.dispose();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||||
|
Widget childWhenDisabled = Scaffold(
|
||||||
return Scaffold(
|
|
||||||
primary: true,
|
primary: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@ -98,6 +107,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
bottomControl: BottomControl(
|
bottomControl: BottomControl(
|
||||||
controller: plPlayerController,
|
controller: plPlayerController,
|
||||||
liveRoomCtr: _liveRoomController,
|
liveRoomCtr: _liveRoomController,
|
||||||
|
floating: floating,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox(),
|
: 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:floating/floating.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/models/video/play/url.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 {
|
class BottomControl extends StatefulWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
final LiveRoomController? liveRoomCtr;
|
final LiveRoomController? liveRoomCtr;
|
||||||
|
final Floating? floating;
|
||||||
const BottomControl({
|
const BottomControl({
|
||||||
this.controller,
|
this.controller,
|
||||||
this.liveRoomCtr,
|
this.liveRoomCtr,
|
||||||
|
this.floating,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -85,6 +91,35 @@ class _BottomControlState extends State<BottomControl> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
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(
|
ComBtn(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.fullscreen,
|
Icons.fullscreen,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
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:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -51,6 +54,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
late Future _futureBuilderFuture;
|
late Future _futureBuilderFuture;
|
||||||
// 自动退出全屏
|
// 自动退出全屏
|
||||||
late bool autoExitFullcreen;
|
late bool autoExitFullcreen;
|
||||||
|
Floating? floating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -60,6 +64,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||||
videoSourceInit();
|
videoSourceInit();
|
||||||
appbarStreamListen();
|
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!;
|
playerStatus = status!;
|
||||||
if (status == PlayerStatus.completed) {
|
if (status == PlayerStatus.completed) {
|
||||||
// 结束播放退出全屏
|
// 结束播放退出全屏
|
||||||
@ -91,7 +98,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
plPlayerController!.triggerFullScreen(status: false);
|
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!.removeStatusLister(playerListener);
|
||||||
plPlayerController!.dispose();
|
plPlayerController!.dispose();
|
||||||
}
|
}
|
||||||
|
if (floating != null) {
|
||||||
|
floating!.dispose();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +175,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||||
final double pinnedHeaderHeight =
|
final double pinnedHeaderHeight =
|
||||||
statusBarHeight + kToolbarHeight + videoHeight;
|
statusBarHeight + kToolbarHeight + videoHeight;
|
||||||
return SafeArea(
|
Widget childWhenDisabled = SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@ -209,6 +222,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
plPlayerController,
|
plPlayerController,
|
||||||
videoDetailCtr:
|
videoDetailCtr:
|
||||||
videoDetailController,
|
videoDetailController,
|
||||||
|
floating: floating,
|
||||||
),
|
),
|
||||||
danmuWidget: Obx(
|
danmuWidget: Obx(
|
||||||
() => PlDanmaku(
|
() => PlDanmaku(
|
||||||
@ -320,13 +334,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
// pinnedHeaderSliverHeightBuilder: () {
|
||||||
|
// return playerStatus != PlayerStatus.playing
|
||||||
|
// ? statusBarHeight + kToolbarHeight
|
||||||
|
// : pinnedHeaderHeight;
|
||||||
|
// },
|
||||||
|
/// 不收回
|
||||||
pinnedHeaderSliverHeightBuilder: () {
|
pinnedHeaderSliverHeightBuilder: () {
|
||||||
return playerStatus != PlayerStatus.playing
|
return pinnedHeaderHeight;
|
||||||
? statusBarHeight + kToolbarHeight
|
|
||||||
: pinnedHeaderHeight;
|
|
||||||
},
|
},
|
||||||
onlyOneScrollInBody: true,
|
onlyOneScrollInBody: true,
|
||||||
body: Container(
|
body: Container(
|
||||||
|
key: Key(Get.arguments['heroTag']),
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -402,21 +421,60 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// 重新进入会刷新
|
||||||
// 播放完成/暂停播放
|
// 播放完成/暂停播放
|
||||||
StreamBuilder(
|
// StreamBuilder(
|
||||||
stream: appbarStream.stream,
|
// stream: appbarStream.stream,
|
||||||
initialData: 0,
|
// initialData: 0,
|
||||||
builder: ((context, snapshot) {
|
// builder: ((context, snapshot) {
|
||||||
return ScrollAppBar(
|
// return ScrollAppBar(
|
||||||
snapshot.data!.toDouble(),
|
// snapshot.data!.toDouble(),
|
||||||
() => continuePlay(),
|
// () => continuePlay(),
|
||||||
playerStatus,
|
// playerStatus,
|
||||||
null,
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:floating/floating.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -14,9 +18,11 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
|
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
final VideoDetailController? videoDetailCtr;
|
final VideoDetailController? videoDetailCtr;
|
||||||
|
final Floating? floating;
|
||||||
const HeaderControl({
|
const HeaderControl({
|
||||||
this.controller,
|
this.controller,
|
||||||
this.videoDetailCtr,
|
this.videoDetailCtr,
|
||||||
|
this.floating,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -770,6 +776,39 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
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(
|
Obx(
|
||||||
() => SizedBox(
|
() => SizedBox(
|
||||||
width: 45,
|
width: 45,
|
||||||
|
|||||||
@ -752,6 +752,10 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void hiddenControls(bool val) {
|
||||||
|
showControls.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
/// 设置长按倍速状态 live模式下禁用
|
/// 设置长按倍速状态 live模式下禁用
|
||||||
void setDoubleSpeedStatus(bool val) {
|
void setDoubleSpeedStatus(bool val) {
|
||||||
if (videoType.value == 'live') {
|
if (videoType.value == 'live') {
|
||||||
|
|||||||
@ -417,6 +417,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
floating:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: floating
|
||||||
|
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@ -117,6 +117,8 @@ dependencies:
|
|||||||
status_bar_control: ^3.2.1
|
status_bar_control: ^3.2.1
|
||||||
# 代理
|
# 代理
|
||||||
system_proxy: ^0.1.0
|
system_proxy: ^0.1.0
|
||||||
|
# pip
|
||||||
|
floating: ^2.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user