Add multi-resolution playback support
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user