Add multi-resolution playback support

This commit is contained in:
2026-04-15 11:42:13 +08:00
parent 146f05388e
commit 6eb0baf16e
7 changed files with 337 additions and 48 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -8,6 +9,7 @@ import 'package:video_player/video_player.dart';
import '../providers/auth_provider.dart';
import '../providers/settings_provider.dart';
import '../services/api_service.dart';
import '../services/chat_service.dart';
import '../widgets/web_stream_player.dart';
@@ -24,7 +26,7 @@ class PlayerPage extends StatefulWidget {
});
@override
_PlayerPageState createState() => _PlayerPageState();
State<PlayerPage> createState() => _PlayerPageState();
}
class _PlayerPageState extends State<PlayerPage> {
@@ -42,11 +44,13 @@ class _PlayerPageState extends State<PlayerPage> {
bool _controlsVisible = true;
int _playerVersion = 0;
String _selectedResolution = 'Source';
List<String> _availableResolutions = const ['Source'];
Timer? _controlsHideTimer;
@override
void initState() {
super.initState();
_loadPlaybackOptions();
if (!kIsWeb) {
_initializePlayer();
}
@@ -55,9 +59,8 @@ class _PlayerPageState extends State<PlayerPage> {
}
Future<void> _initializePlayer() async {
_controller = VideoPlayerController.networkUrl(
Uri.parse(widget.playbackUrl),
);
final playbackUrl = _currentPlaybackUrl();
_controller = VideoPlayerController.networkUrl(Uri.parse(playbackUrl));
try {
await _controller!.initialize();
_controller!.play();
@@ -81,6 +84,55 @@ class _PlayerPageState extends State<PlayerPage> {
}
}
String _currentPlaybackUrl() {
final settings = context.read<SettingsProvider>();
final quality = _selectedResolution == 'Source'
? null
: _selectedResolution.toLowerCase();
return settings.playbackUrl(widget.roomId, quality: quality);
}
Future<void> _loadPlaybackOptions() async {
final settings = context.read<SettingsProvider>();
final auth = context.read<AuthProvider>();
final api = ApiService(settings, auth.token);
try {
final response = await api.getPlaybackOptions(widget.roomId);
if (response.statusCode != 200) {
return;
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final rawQualities =
(data['qualities'] as List<dynamic>? ?? const ['source'])
.map((item) => item.toString().trim().toLowerCase())
.where((item) => item.isNotEmpty)
.toList();
final normalized = <String>['Source'];
for (final quality in rawQualities) {
if (quality == 'source') {
continue;
}
normalized.add(quality);
}
if (!mounted) {
return;
}
setState(() {
_availableResolutions = normalized.toSet().toList();
if (!_availableResolutions.contains(_selectedResolution)) {
_selectedResolution = 'Source';
}
});
} catch (_) {
// Keep source-only playback when the capability probe fails.
}
}
void _initializeChat() {
final settings = context.read<SettingsProvider>();
final auth = context.read<AuthProvider>();
@@ -141,6 +193,8 @@ class _PlayerPageState extends State<PlayerPage> {
return;
}
await _loadPlaybackOptions();
setState(() {
_isRefreshing = true;
_isError = false;
@@ -223,22 +277,30 @@ class _PlayerPageState extends State<PlayerPage> {
Future<void> _selectResolution() async {
_showControls();
await _loadPlaybackOptions();
if (!mounted) {
return;
}
final nextResolution = await showModalBottomSheet<String>(
context: context,
builder: (context) {
const options = ['Source', '720p', '480p'];
final available = _availableResolutions.toSet();
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ListTile(
ListTile(
title: Text('Playback Resolution'),
subtitle: Text(
'Current backend only provides the source stream. Lower resolutions are reserved for future multi-bitrate output.',
available.length > 1
? 'Select an available transcoded stream.'
: 'Only the source stream is available right now.',
),
),
...options.map((option) {
final enabled = option == 'Source';
final enabled = available.contains(option);
return ListTile(
enabled: enabled,
leading: Icon(
@@ -249,7 +311,7 @@ class _PlayerPageState extends State<PlayerPage> {
title: Text(option),
subtitle: enabled
? const Text('Available now')
: const Text('Requires backend transcoding support'),
: const Text('Waiting for backend transcoding output'),
onTap: enabled ? () => Navigator.pop(context, option) : null,
);
}),
@@ -376,7 +438,7 @@ class _PlayerPageState extends State<PlayerPage> {
: kIsWeb
? WebStreamPlayer(
key: ValueKey('web-player-$_playerVersion'),
streamUrl: widget.playbackUrl,
streamUrl: _currentPlaybackUrl(),
)
: _controller != null && _controller!.value.isInitialized
? AspectRatio(