feat: 简单完成视频播放
This commit is contained in:
@ -1,5 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_meedu_media_kit/meedu_player.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
|
||||
@ -9,7 +14,8 @@ class VideoDetailController extends GetxController {
|
||||
RxList<String> tabs = <String>['简介', '评论'].obs;
|
||||
|
||||
// 视频aid
|
||||
String aid = Get.parameters['aid']!;
|
||||
int aid = int.parse(Get.parameters['aid']!);
|
||||
int cid = int.parse(Get.parameters['cid']!);
|
||||
|
||||
// 是否预渲染 骨架屏
|
||||
bool preRender = false;
|
||||
@ -30,6 +36,13 @@ class VideoDetailController extends GetxController {
|
||||
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
MeeduPlayerController meeduPlayerController = MeeduPlayerController(
|
||||
colorTheme: Theme.of(Get.context!).colorScheme.primary,
|
||||
pipEnabled: true,
|
||||
controlsStyle: ControlsStyle.youtube,
|
||||
enabledButtons: const EnabledButtons(pip: true),
|
||||
);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -43,16 +56,18 @@ class VideoDetailController extends GetxController {
|
||||
}
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
}
|
||||
queryVideoUrl();
|
||||
}
|
||||
|
||||
showReplyReplyPanel() {
|
||||
PersistentBottomSheetController<void>? ctr = scaffoldKey.currentState?.showBottomSheet<void>((BuildContext context) {
|
||||
PersistentBottomSheetController<void>? ctr =
|
||||
scaffoldKey.currentState?.showBottomSheet<void>((BuildContext context) {
|
||||
return VideoReplyReplyPanel(
|
||||
oid: oid,
|
||||
rpid: fRpid,
|
||||
closePanel: ()=> {
|
||||
closePanel: () => {
|
||||
fRpid = 0,
|
||||
},
|
||||
},
|
||||
firstFloor: firstFloor,
|
||||
);
|
||||
});
|
||||
@ -60,4 +75,37 @@ class VideoDetailController extends GetxController {
|
||||
fRpid = 0;
|
||||
});
|
||||
}
|
||||
|
||||
playerInit(url) {
|
||||
meeduPlayerController.setDataSource(
|
||||
DataSource(
|
||||
type: DataSourceType.network,
|
||||
source: url,
|
||||
httpHeaders: {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
|
||||
'referer': HttpString.baseUrl
|
||||
},
|
||||
),
|
||||
autoplay: true,
|
||||
looping: false
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> meeduDispose() async {
|
||||
// if (meeduPlayerController != null) {
|
||||
// _playerEventSubs?.cancel();
|
||||
// await meeduPlayerController!.dispose();
|
||||
// meeduPlayerController = null;
|
||||
// // The next line disables the wakelock again.
|
||||
// // await Wakelock.disable();
|
||||
// }
|
||||
// }
|
||||
|
||||
// 视频链接
|
||||
queryVideoUrl() async {
|
||||
var result = await VideoHttp.videoUrl(cid: cid, avid: aid);
|
||||
var url = result['data']['durl'].first['url'];
|
||||
playerInit(url);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter_meedu_media_kit/meedu_player.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
@ -7,30 +11,122 @@ 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/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class VideoDetailPage extends StatefulWidget {
|
||||
const VideoDetailPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoDetailPage> createState() => _VideoDetailPageState();
|
||||
static final RouteObserver<PageRoute> routeObserver =
|
||||
RouteObserver<PageRoute>();
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
with TickerProviderStateMixin {
|
||||
with TickerProviderStateMixin, RouteAware {
|
||||
final VideoDetailController videoDetailController =
|
||||
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
|
||||
MeeduPlayerController? _meeduPlayerController;
|
||||
ScrollController _extendNestCtr = ScrollController();
|
||||
late AnimationController animationController;
|
||||
|
||||
// final _meeduPlayerController = MeeduPlayerController(
|
||||
// pipEnabled: true,
|
||||
// controlsStyle: ControlsStyle.secondary,
|
||||
// enabledButtons: const EnabledButtons(pip: true),
|
||||
// );
|
||||
StreamSubscription? _playerEventSubs;
|
||||
bool isPlay = false;
|
||||
bool isShowCover = true;
|
||||
double doubleOffset = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_meeduPlayerController = videoDetailController.meeduPlayerController;
|
||||
_playerEventSubs = _meeduPlayerController!.onPlayerStatusChanged.listen(
|
||||
(PlayerStatus status) {
|
||||
if (status == PlayerStatus.playing) {
|
||||
Wakelock.enable();
|
||||
print('开始播放了');
|
||||
isPlay = false;
|
||||
isShowCover = false;
|
||||
setState(() {});
|
||||
} else {
|
||||
isPlay = true;
|
||||
setState(() {});
|
||||
Wakelock.disable();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 600));
|
||||
|
||||
_extendNestCtr.addListener(
|
||||
() {
|
||||
print(_extendNestCtr.position.pixels);
|
||||
|
||||
double offset = _extendNestCtr.position.pixels;
|
||||
if (offset > doubleOffset) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
doubleOffset = offset;
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _meeduDispose() async {
|
||||
if (_meeduPlayerController != null) {
|
||||
_playerEventSubs?.cancel();
|
||||
await _meeduPlayerController!.dispose();
|
||||
_meeduPlayerController = null;
|
||||
// The next line disables the wakelock again.
|
||||
await Wakelock.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoDetailController.meeduPlayerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// 离开当前页面时
|
||||
void didPushNext() async {
|
||||
if(!_meeduPlayerController!.pipEnabled){
|
||||
_meeduPlayerController!.pause();
|
||||
}
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
// 返回当前页面时
|
||||
void didPopNext() async {
|
||||
if (_extendNestCtr.position.pixels == 0) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
_meeduPlayerController!.play();
|
||||
}
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
VideoDetailPage.routeObserver
|
||||
.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double statusBarHeight = MediaQuery.of(context).padding.top;
|
||||
final double pinnedHeaderHeight = statusBarHeight +
|
||||
kToolbarHeight +
|
||||
MediaQuery.of(context).size.width * 9 / 16;
|
||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||
final double pinnedHeaderHeight =
|
||||
statusBarHeight + kToolbarHeight + videoHeight;
|
||||
return DefaultTabController(
|
||||
initialIndex: videoDetailController.tabInitialIndex,
|
||||
length: videoDetailController.tabs.length, // tab的数量.
|
||||
@ -43,20 +139,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: videoDetailController.scaffoldKey,
|
||||
body: ExtendedNestedScrollView(
|
||||
controller: _extendNestCtr,
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
title: const Text("视频详情"),
|
||||
pinned: true,
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: false,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
expandedHeight:
|
||||
MediaQuery.of(context).size.width * 9 / 16,
|
||||
collapsedHeight:
|
||||
MediaQuery.of(context).size.width * 9 / 16,
|
||||
backgroundColor: Colors.black,
|
||||
expandedHeight: videoHeight,
|
||||
// collapsedHeight: videoHeight,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
@ -65,16 +160,66 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
double PR =
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
return Hero(
|
||||
tag: videoDetailController.heroTag,
|
||||
child: NetworkImgLayer(
|
||||
type: 'emote',
|
||||
src: videoDetailController.videoItem['pic'],
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
// double PR =
|
||||
// MediaQuery.of(context).devicePixelRatio;
|
||||
return Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: MeeduVideoPlayer(
|
||||
controller: _meeduPlayerController!,
|
||||
header: (BuildContext context,
|
||||
MeeduPlayerController
|
||||
_meeduPlayerController,
|
||||
Responsive) {
|
||||
return AppBar(
|
||||
toolbarHeight: 40,
|
||||
backgroundColor: Colors.transparent,
|
||||
primary: false,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
foregroundColor: Colors.white,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'视频详情',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall!
|
||||
.fontSize),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isShowCover,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Hero(
|
||||
tag: videoDetailController.heroTag,
|
||||
child: NetworkImgLayer(
|
||||
type: 'emote',
|
||||
src: videoDetailController
|
||||
.videoItem['pic'],
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -84,7 +229,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
];
|
||||
},
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
return pinnedHeaderHeight;
|
||||
return isPlay
|
||||
? MediaQuery.of(context).padding.top + 50
|
||||
: pinnedHeaderHeight;
|
||||
},
|
||||
onlyOneScrollInBody: true,
|
||||
body: Column(
|
||||
@ -148,6 +295,51 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
// 播放完成/暂停播放
|
||||
Positioned(
|
||||
top: -MediaQuery.of(context).padding.top +
|
||||
(doubleOffset / videoHeight) * 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Opacity(
|
||||
opacity: doubleOffset / videoHeight,
|
||||
child: Container(
|
||||
height: 50 + MediaQuery.of(context).padding.top,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
padding:
|
||||
EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
child: AppBar(
|
||||
primary: false,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
title: TextButton(
|
||||
onPressed: () {
|
||||
_extendNestCtr.animateTo(0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.play_arrow_rounded),
|
||||
Text('继续播放')
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.share,
|
||||
size: 20,
|
||||
)),
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
Reference in New Issue
Block a user