Unify web player controls and add volume control

This commit is contained in:
2026-04-22 10:45:46 +08:00
parent 6eb0baf16e
commit 425ea363f8
6 changed files with 124 additions and 4 deletions

View File

@@ -42,6 +42,7 @@ class _PlayerPageState extends State<PlayerPage> {
bool _isRefreshing = false;
bool _isFullscreen = false;
bool _controlsVisible = true;
double _volume = kIsWeb ? 0.0 : 1.0;
int _playerVersion = 0;
String _selectedResolution = 'Source';
List<String> _availableResolutions = const ['Source'];
@@ -63,6 +64,7 @@ class _PlayerPageState extends State<PlayerPage> {
_controller = VideoPlayerController.networkUrl(Uri.parse(playbackUrl));
try {
await _controller!.initialize();
await _controller!.setVolume(_volume);
_controller!.play();
if (mounted) setState(() {});
} catch (e) {
@@ -254,6 +256,76 @@ class _PlayerPageState extends State<PlayerPage> {
_showControls();
}
Future<void> _setVolume(double volume) async {
final nextVolume = volume.clamp(0.0, 1.0);
if (!mounted) {
return;
}
setState(() => _volume = nextVolume);
if (!kIsWeb && _controller != null) {
await _controller!.setVolume(nextVolume);
}
}
Future<void> _openVolumeSheet() async {
_showControls();
await showModalBottomSheet<void>(
context: context,
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 28),
child: StatefulBuilder(
builder: (context, setSheetState) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Volume',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Row(
children: [
Icon(
_volume == 0
? Icons.volume_off
: _volume < 0.5
? Icons.volume_down
: Icons.volume_up,
),
Expanded(
child: Slider(
value: _volume,
min: 0,
max: 1,
divisions: 20,
label: '${(_volume * 100).round()}%',
onChanged: (value) {
setSheetState(() => _volume = value);
_setVolume(value);
},
),
),
SizedBox(
width: 48,
child: Text('${(_volume * 100).round()}%'),
),
],
),
],
);
},
),
),
);
},
);
}
void _showControls() {
_controlsHideTimer?.cancel();
if (mounted) {
@@ -439,6 +511,7 @@ class _PlayerPageState extends State<PlayerPage> {
? WebStreamPlayer(
key: ValueKey('web-player-$_playerVersion'),
streamUrl: _currentPlaybackUrl(),
volume: _volume,
)
: _controller != null && _controller!.value.isInitialized
? AspectRatio(
@@ -550,6 +623,15 @@ class _PlayerPageState extends State<PlayerPage> {
label: "Refresh",
onPressed: _refreshPlayer,
),
_buildControlButton(
icon: _volume == 0
? Icons.volume_off
: _volume < 0.5
? Icons.volume_down
: Icons.volume_up,
label: "Volume",
onPressed: _openVolumeSheet,
),
_buildControlButton(
icon: _showDanmaku ? Icons.subtitles : Icons.subtitles_off,
label: _showDanmaku ? "Danmaku On" : "Danmaku Off",

View File

@@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
class WebStreamPlayer extends StatelessWidget {
final String streamUrl;
final double volume;
final int? refreshToken;
const WebStreamPlayer({
super.key,
required this.streamUrl,
required this.volume,
this.refreshToken,
});

View File

@@ -1,3 +1,5 @@
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
import 'dart:html' as html;
import 'dart:ui_web' as ui_web;
@@ -5,11 +7,13 @@ import 'package:flutter/material.dart';
class WebStreamPlayer extends StatefulWidget {
final String streamUrl;
final double volume;
final int? refreshToken;
const WebStreamPlayer({
super.key,
required this.streamUrl,
required this.volume,
this.refreshToken,
});
@@ -19,6 +23,7 @@ class WebStreamPlayer extends StatefulWidget {
class _WebStreamPlayerState extends State<WebStreamPlayer> {
late final String _viewType;
html.IFrameElement? _iframe;
@override
void initState() {
@@ -29,15 +34,30 @@ class _WebStreamPlayerState extends State<WebStreamPlayer> {
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final iframe = html.IFrameElement()
..src =
'flv_player.html?v=$cacheBuster&src=${Uri.encodeComponent(widget.streamUrl)}'
'flv_player.html?v=$cacheBuster'
'&src=${Uri.encodeComponent(widget.streamUrl)}'
'&volume=${widget.volume}'
..style.border = '0'
..style.width = '100%'
..style.height = '100%'
..style.pointerEvents = 'none'
..allow = 'autoplay; fullscreen';
_iframe = iframe;
return iframe;
});
}
@override
void didUpdateWidget(covariant WebStreamPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.volume != widget.volume) {
_iframe?.contentWindow?.postMessage({
'type': 'setVolume',
'value': widget.volume,
}, '*');
}
}
@override
Widget build(BuildContext context) {
return HtmlElementView(viewType: _viewType);