Unify web player controls and add volume control
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user