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

@@ -10,7 +10,7 @@ import (
func main() { func main() {
monitor.Init(2000) monitor.Init(2000)
monitor.Infof("Starting Hightube Server v1.0.0-Beta4.1") monitor.Infof("Starting Hightube Server v1.0.0-Beta4.7")
// Initialize Database and run auto-migrations // Initialize Database and run auto-migrations
db.InitDB() db.InitDB()

View File

@@ -42,6 +42,7 @@ class _PlayerPageState extends State<PlayerPage> {
bool _isRefreshing = false; bool _isRefreshing = false;
bool _isFullscreen = false; bool _isFullscreen = false;
bool _controlsVisible = true; bool _controlsVisible = true;
double _volume = kIsWeb ? 0.0 : 1.0;
int _playerVersion = 0; int _playerVersion = 0;
String _selectedResolution = 'Source'; String _selectedResolution = 'Source';
List<String> _availableResolutions = const ['Source']; List<String> _availableResolutions = const ['Source'];
@@ -63,6 +64,7 @@ class _PlayerPageState extends State<PlayerPage> {
_controller = VideoPlayerController.networkUrl(Uri.parse(playbackUrl)); _controller = VideoPlayerController.networkUrl(Uri.parse(playbackUrl));
try { try {
await _controller!.initialize(); await _controller!.initialize();
await _controller!.setVolume(_volume);
_controller!.play(); _controller!.play();
if (mounted) setState(() {}); if (mounted) setState(() {});
} catch (e) { } catch (e) {
@@ -254,6 +256,76 @@ class _PlayerPageState extends State<PlayerPage> {
_showControls(); _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() { void _showControls() {
_controlsHideTimer?.cancel(); _controlsHideTimer?.cancel();
if (mounted) { if (mounted) {
@@ -439,6 +511,7 @@ class _PlayerPageState extends State<PlayerPage> {
? WebStreamPlayer( ? WebStreamPlayer(
key: ValueKey('web-player-$_playerVersion'), key: ValueKey('web-player-$_playerVersion'),
streamUrl: _currentPlaybackUrl(), streamUrl: _currentPlaybackUrl(),
volume: _volume,
) )
: _controller != null && _controller!.value.isInitialized : _controller != null && _controller!.value.isInitialized
? AspectRatio( ? AspectRatio(
@@ -550,6 +623,15 @@ class _PlayerPageState extends State<PlayerPage> {
label: "Refresh", label: "Refresh",
onPressed: _refreshPlayer, onPressed: _refreshPlayer,
), ),
_buildControlButton(
icon: _volume == 0
? Icons.volume_off
: _volume < 0.5
? Icons.volume_down
: Icons.volume_up,
label: "Volume",
onPressed: _openVolumeSheet,
),
_buildControlButton( _buildControlButton(
icon: _showDanmaku ? Icons.subtitles : Icons.subtitles_off, icon: _showDanmaku ? Icons.subtitles : Icons.subtitles_off,
label: _showDanmaku ? "Danmaku On" : "Danmaku Off", label: _showDanmaku ? "Danmaku On" : "Danmaku Off",

View File

@@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
class WebStreamPlayer extends StatelessWidget { class WebStreamPlayer extends StatelessWidget {
final String streamUrl; final String streamUrl;
final double volume;
final int? refreshToken; final int? refreshToken;
const WebStreamPlayer({ const WebStreamPlayer({
super.key, super.key,
required this.streamUrl, required this.streamUrl,
required this.volume,
this.refreshToken, 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:html' as html;
import 'dart:ui_web' as ui_web; import 'dart:ui_web' as ui_web;
@@ -5,11 +7,13 @@ import 'package:flutter/material.dart';
class WebStreamPlayer extends StatefulWidget { class WebStreamPlayer extends StatefulWidget {
final String streamUrl; final String streamUrl;
final double volume;
final int? refreshToken; final int? refreshToken;
const WebStreamPlayer({ const WebStreamPlayer({
super.key, super.key,
required this.streamUrl, required this.streamUrl,
required this.volume,
this.refreshToken, this.refreshToken,
}); });
@@ -19,6 +23,7 @@ class WebStreamPlayer extends StatefulWidget {
class _WebStreamPlayerState extends State<WebStreamPlayer> { class _WebStreamPlayerState extends State<WebStreamPlayer> {
late final String _viewType; late final String _viewType;
html.IFrameElement? _iframe;
@override @override
void initState() { void initState() {
@@ -29,15 +34,30 @@ class _WebStreamPlayerState extends State<WebStreamPlayer> {
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final iframe = html.IFrameElement() final iframe = html.IFrameElement()
..src = ..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.border = '0'
..style.width = '100%' ..style.width = '100%'
..style.height = '100%' ..style.height = '100%'
..style.pointerEvents = 'none'
..allow = 'autoplay; fullscreen'; ..allow = 'autoplay; fullscreen';
_iframe = iframe;
return 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return HtmlElementView(viewType: _viewType); return HtmlElementView(viewType: _viewType);

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0-beta4.1 version: 1.0.0-beta4.7
environment: environment:
sdk: ^3.11.1 sdk: ^3.11.1

View File

@@ -45,14 +45,21 @@
<script src="flv.min.js"></script> <script src="flv.min.js"></script>
</head> </head>
<body> <body>
<video id="player" controls autoplay muted playsinline></video> <video id="player" autoplay muted playsinline></video>
<div id="message">Loading live stream...</div> <div id="message">Loading live stream...</div>
<script> <script>
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const streamUrl = params.get('src'); const streamUrl = params.get('src');
const initialVolume = Number.parseFloat(params.get('volume') || '1');
const video = document.getElementById('player'); const video = document.getElementById('player');
const message = document.getElementById('message'); const message = document.getElementById('message');
function applyVolume(value) {
const normalized = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 1;
video.volume = normalized;
video.muted = normalized === 0;
}
function showMessage(text) { function showMessage(text) {
video.style.display = 'none'; video.style.display = 'none';
message.style.display = 'flex'; message.style.display = 'flex';
@@ -66,6 +73,8 @@
} else if (!flvjs.isSupported()) { } else if (!flvjs.isSupported()) {
showMessage('This browser does not support FLV playback.'); showMessage('This browser does not support FLV playback.');
} else { } else {
applyVolume(initialVolume);
const player = flvjs.createPlayer({ const player = flvjs.createPlayer({
type: 'flv', type: 'flv',
url: streamUrl, url: streamUrl,
@@ -90,6 +99,13 @@
video.style.display = 'block'; video.style.display = 'block';
message.style.display = 'none'; message.style.display = 'none';
window.addEventListener('message', function(event) {
const data = event.data || {};
if (data.type === 'setVolume') {
applyVolume(Number(data.value));
}
});
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {
player.destroy(); player.destroy();
}); });