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() {
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
db.InitDB()

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);

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
# 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.
version: 1.0.0-beta4.1
version: 1.0.0-beta4.7
environment:
sdk: ^3.11.1

View File

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