hm_video_player 0.4.1
hm_video_player: ^0.4.1 copied to clipboard
跨平台视频播放器插件,支持全屏、画中画、投屏、倍速等功能。
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:hm_video_player/hm_video_player.dart';
import 'cast_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: DemoHomePage(),
);
}
}
class DemoHomePage extends StatefulWidget {
const DemoHomePage({super.key});
@override
State<DemoHomePage> createState() => _DemoHomePageState();
}
class _DemoHomePageState extends State<DemoHomePage> {
final controller = HmVideoPlayerController();
static const url =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
@override
void initState() {
super.initState();
controller.events.listen((event) {
if (!mounted) return;
debugPrint('hm player event: $event');
if (event is CastRequestedEvent) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => CastPage(
controller: controller,
initialContext: CastRequestContext(
mediaUrl: event.mediaUrl,
title: event.title,
position: event.position,
playing: event.playing,
supportedProtocols: event.supportedProtocols,
currentSession: event.currentSession,
),
),
),
);
}
if (event is CastStateChangedEvent) {
debugPrint(
'cast state: ${event.state} protocol=${event.protocol} '
'device=${event.deviceName} message=${event.message}',
);
}
if (event is QualitySelectedEvent) {
// 应用层:根据 qualities[event.index].value 请求新播放地址,再调用 setSource。
// controller.setSource(newUrl, position: currentPosition);
}
if (event is PipOpenedEvent) {
debugPrint('pip opened');
}
if (event is PipClosedEvent) {
debugPrint('pip closed');
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff7f7f7),
appBar: AppBar(
title: const Text('HM Video Player'),
backgroundColor: Colors.white,
surfaceTintColor: Colors.white,
),
body: ListView(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: HmVideoPlayer(
controller: controller,
videoUrl: url,
title: '莫离 第01集',
cover: false,
looping: true,
autoPlay: true,
autoRotateFullscreen: true,
syncSystemGestureDirection: true,
showNativeControls: true,
inlineControls: NativeControlsConfig.inlinePreset,
fullscreenControls: NativeControlsConfig.fullscreenPreset,
qualities: const [
VideoQuality(
name: '臻彩MAX',
subName: 'AI增强',
value: '6',
vip: true,
vipDesc: 'VIP尊享',
fullWidth: true,
),
VideoQuality(
name: '4K',
subName: '超高清 SDR',
value: '5',
vip: true,
vipDesc: 'VIP尊享',
fullWidth: true,
),
VideoQuality(
name: '1080P',
subName: 'HDR Vivid',
value: '4',
fullWidth: true,
),
VideoQuality(
name: '1080P',
subName: '高清 SDR',
value: '3',
),
VideoQuality(
name: '720P',
subName: '准高清',
value: '2',
),
VideoQuality(
name: '480P',
subName: '标清',
value: '1',
),
],
currentQualityIndex: 4,
currentEpisodeIndex: 2,
episodes: [
for (var i = 1; i <= 100; i++)
VideoEpisode(
title: switch (i) {
1 => '沾喜气!精分孤女轮椅战神王府大婚',
2 => '王爷初遇莫离',
3 => '轮椅上的秘密',
4 => '王府夜宴风波',
5 => '莫离展露锋芒',
6 => '暗流涌动',
7 => '危机四伏',
8 => '联手破局',
9 => '真相浮现',
10 => '情感纠葛',
11 => '绝地反击',
12 => '大婚前夕',
_ => '第 $i 集',
},
url: url,
cover: 'https://picsum.photos/seed/hm_episode_$i/320/180',
),
],
danmakuItems: const [
DanmakuItem(text: '原生小窗播放器', at: Duration(seconds: 1)),
DanmakuItem(
text: '同一个原生播放器进入全屏',
at: Duration(seconds: 2),
),
],
speeds: const [3.0, 2.0, 1.5, 1.25, 1.0, 0.5],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: FilledButton(
onPressed: controller.enterPictureInPicture,
child: const Text('进入画中画'),
),
),
// Padding(
// padding: const EdgeInsets.all(16),
// child: Wrap(
// spacing: 12,
// runSpacing: 12,
// children: [
// FilledButton(
// onPressed: controller.enterFullscreen,
// child: const Text('进入全屏'),
// ),
// OutlinedButton(
// onPressed: () => controller.setSpeed(1.5),
// child: const Text('1.5x'),
// ),
// OutlinedButton(
// onPressed: () =>
// controller.seekTo(const Duration(seconds: 5)),
// child: const Text('跳到 5 秒'),
// ),
// ],
// ),
// ),
// const _InfoCard(
// title: '架构说明',
// body:
// 'Flutter 与原生通过 Pigeon 生成的类型安全 API 通信。小窗和全屏都是原生播放器,避免 Flutter texture 与原生画面交接问题。',
// ),
// const _InfoCard(
// title: '控制栏',
// body:
// '小屏/全屏控制栏均为原生实现,默认布局参考腾讯视频。可通过 inlineControls / fullscreenControls 配置各按钮显隐,controlButtonTapped 事件回传 Flutter。',
// ),
// const _InfoCard(
// title: '测试重点',
// body:
// '1. 点击全屏按钮进入全屏。\n2. 旋转手机进入全屏。\n3. 全屏中转回竖屏自动退出。\n4. 全屏中左右横屏切换。\n5. 退出时是否还有双层画面或底部残留。',
// ),
// const SizedBox(height: 800),
],
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({required this.title, required this.body});
final String title;
final String body;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(body, style: const TextStyle(height: 1.45)),
],
),
);
}
}