hm_video_player
Native PlatformView video player with Mohe-style fullscreen transition.
Flutter and native communicate through Pigeon-generated type-safe APIs.
Supports HTTP(S) streams and local media (file://, absolute paths; Android also content://).
Installation
Add to pubspec.yaml:
dependencies:
hm_video_player: ^0.7.0
Then run flutter pub get.
Package
| Item | Value |
|---|---|
| Dart package | hm_video_player |
| Android package | com.hm.video_player |
| Plugin class | HmVideoPlayerPlugin |
| PlatformView type | com.hm.video_player/view |
Public API
import 'package:hm_video_player/hm_video_player.dart';
final controller = HmVideoPlayerController();
HmVideoPlayer(
controller: controller,
videoUrl: url,
title: 'Demo',
qualities: [VideoQuality(label: 'HD', url: url)],
episodes: [VideoEpisode(title: 'Ep 1', url: url)],
);
// Control
await controller.play();
await controller.pause();
await controller.seekTo(Duration(seconds: 5));
await controller.setSpeed(1.5);
await controller.setVolume(0.8);
await controller.setMuted(false);
await controller.enterFullscreen();
await controller.enterPictureInPicture();
await controller.exitPictureInPicture();
await controller.setPictureInPictureEnabled(true);
await controller.setAutoEnterPictureInPicture(false);
await controller.setAutoRotateFullscreen(true);
// Query
final position = await controller.position;
final duration = await controller.duration;
final playing = await controller.isPlaying;
final inPip = await controller.isInPictureInPicture;
final pipSupported = await controller.isPictureInPictureSupported;
// Typed events
controller.events.listen((event) {
switch (event) {
case ProgressEvent(:final position, :final duration):
break;
case FullscreenOpenedEvent():
break;
case FullscreenClosedEvent():
break;
case PipOpenedEvent():
break;
case PipClosedEvent():
break;
default:
break;
}
});
Architecture
lib/ Public Dart API
hm_video_player.dart
src/
platform/messages.g.dart Pigeon generated (do not edit)
pigeons/messages.dart Pigeon contract (edit here)
android/.../com/hm/video_player/
ios/Classes/
Regenerate Pigeon bindings after changing pigeons/messages.dart:
bash tool/generate_pigeon.sh
Future capabilities (danmaku, subtitles, etc.) should add a separate @HostApi
in pigeons/messages.dart and share the same per-view messageChannelSuffix.
Host app (Android)
Picture-in-picture requires the host Activity to declare PiP support:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true">
supportsPictureInPicture is required on the host Activity. PiP state is tracked via activity lifecycle callbacks in the plugin; no extra configChanges flag is needed.
Host app (iOS)
Forward orientation support to the plugin:
import hm_video_player
override func application(
_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
return HmVideoPlayerPlugin.orientationMask
}
Run example
cd example
flutter pub get
flutter run
Notes
- Inline and fullscreen both use the native player (ExoPlayer / AVPlayer).
- Fullscreen transition logic is unchanged from the Mohe-style implementation.
danmakuItemsis reserved for a futureDanmakuHostApi.trialDurationis wired: native pauses and emitsTrialEndedEventwhen the limit is reached.- Picture-in-picture: use
enablePictureInPicture/autoEnterPictureInPictureon HmVideoPlayer for initial state when the PlatformView is created. For runtime control (e.g. VIP gating), callsetPictureInPictureEnabled/setAutoEnterPictureInPictureon the controller before updating Flutter UI state. Disabling the master switch exits PiP if active; disabling auto-enter only stops background auto PiP—manualenterPictureInPicture()still works. PiP is disabled during cast. Seedocs/API.mdfor platform details. Example:example/lib/main.dart. - Auto fullscreen on rotation: use
autoRotateFullscreenon HmVideoPlayer for initial state; callsetAutoRotateFullscreenon the controller at runtime to enable/disable sensor-based fullscreen entry while keeping manual fullscreen available. - Touch gestures (Tencent Video style): horizontal seek, left brightness, right system volume, center long-press speed boost. Inline drag gestures can be disabled via
enableInlineDragGestures; inline double-tap viaenableInlineDoubleTapPlayPause. Fullscreen always supports all gestures. Seedocs/API.mdfor UI hints and platform notes (iOS volume gesture requires a physical device).
Cast (投屏)
默认 castUiMode: HmCastUiMode.flutter:点击播放器投屏按钮时,原生层只发送 CastRequestedEvent,由业务层跳转自定义投屏页。原生负责设备发现、协议连接与 castStateChanged 生命周期事件。
协议支持矩阵
| 协议 | Android | iOS | Flutter 投屏页 |
|---|---|---|---|
| DLNA | 支持 | 支持 | 设备列表 + connectCast(deviceId) |
| Chromecast | 支持 | 支持(需初始化 GCKCastContext) | 设备列表 + connectCast(deviceId) |
| AirPlay | 不支持 | 支持 | presentAirPlayPicker() 打开系统选择器 |
业务集成
HmVideoPlayer(
controller: controller,
videoUrl: url,
castUiMode: HmCastUiMode.flutter, // 默认
);
controller.events.listen((event) {
if (event is CastRequestedEvent) {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => MyCastPage(
controller: controller,
supportedProtocols: event.supportedProtocols,
mediaUrl: event.mediaUrl,
title: event.title,
),
));
}
});
// 投屏页 API
await controller.getSupportedCastProtocols();
await controller.getCastRequestContext();
await controller.refreshCastDevices();
await controller.getCastDevices();
final connectResult = await controller.connectCast(
deviceId,
url: castUrl, // 可选:投屏专用地址,与本机鉴权地址不同时传入
title: '第 01 集',
position: await controller.position,
);
final disconnectResult = await controller.disconnectCast();
await controller.stopCastDiscovery(); // 离开投屏页时停止发现
await controller.presentAirPlayPicker(); // iOS only
宿主配置
iOS Info.plist(本地网络与 Bonjour,用于 DLNA/Chromecast 发现):
<key>NSLocalNetworkUsageDescription</key>
<string>用于发现局域网投屏设备</string>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_upnp._tcp</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
iOS Chromecast:在 AppDelegate 中初始化 GCKCastContext(example 已配置)。
降级原生面板:设置 castUiMode: HmCastUiMode.native 可恢复内置原生投屏侧滑面板。
API 迁移
- 推荐监听
CastRequestedEvent替代仅依赖ControlButtonTappedEvent(cast)。 CastDevicesUpdatedEvent在设备列表变化时推送。CastStateChangedEvent用于连接生命周期(discovering / connecting / connected / disconnected / failed)。CastProgressEvent/CastPlayingChangedEvent在投屏 connected 期间推送远端进度与播放状态(约 1 秒一次进度,状态变更时额外推送)。- 投屏控制方法(
connectCast/disconnectCast/castPlay/castPause/castSeekTo/castSetMedia/castSetVolume)返回CastCommandResult,建议同时监听CastCommandResultEvent获取最终命令执行结果。