video: disable stream selection when irrelevant
This commit is contained in:
parent
7bedca9537
commit
e40770b1c4
4 changed files with 72 additions and 62 deletions
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -237,6 +238,21 @@ class _ButtonRow extends StatelessWidget {
|
||||||
Widget _buildOverlayButton(BuildContext context, VideoAction action) {
|
Widget _buildOverlayButton(BuildContext context, VideoAction action) {
|
||||||
late Widget child;
|
late Widget child;
|
||||||
void onPressed() => onActionSelected(action);
|
void onPressed() => onActionSelected(action);
|
||||||
|
|
||||||
|
ValueListenableBuilder<bool> _buildFromListenable(ValueListenable<bool>? enabledNotifier) {
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: enabledNotifier ?? ValueNotifier(false),
|
||||||
|
builder: (context, canDo, child) {
|
||||||
|
return IconButton(
|
||||||
|
icon: child!,
|
||||||
|
onPressed: canDo ? onPressed : null,
|
||||||
|
tooltip: action.getText(context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(action.getIcon()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case VideoAction.togglePlay:
|
case VideoAction.togglePlay:
|
||||||
child = _PlayToggler(
|
child = _PlayToggler(
|
||||||
|
@ -245,20 +261,12 @@ class _ButtonRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case VideoAction.captureFrame:
|
case VideoAction.captureFrame:
|
||||||
child = ValueListenableBuilder<bool>(
|
child = _buildFromListenable(controller?.renderingVideoNotifier);
|
||||||
valueListenable: controller?.renderingVideoNotifier ?? ValueNotifier(false),
|
break;
|
||||||
builder: (context, canDo, child) {
|
case VideoAction.selectStreams:
|
||||||
return IconButton(
|
child = _buildFromListenable(controller?.canSelectStreamNotifier);
|
||||||
icon: child!,
|
|
||||||
onPressed: canDo ? onPressed : null,
|
|
||||||
tooltip: action.getText(context),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Icon(action.getIcon()),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case VideoAction.replay10:
|
case VideoAction.replay10:
|
||||||
case VideoAction.selectStreams:
|
|
||||||
case VideoAction.setSpeed:
|
case VideoAction.setSpeed:
|
||||||
child = IconButton(
|
child = IconButton(
|
||||||
icon: Icon(action.getIcon()),
|
icon: Icon(action.getIcon()),
|
||||||
|
@ -277,7 +285,21 @@ class _ButtonRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuEntry<VideoAction> _buildPopupMenuItem(BuildContext context, VideoAction action) {
|
PopupMenuEntry<VideoAction> _buildPopupMenuItem(BuildContext context, VideoAction action) {
|
||||||
var enabled = true;
|
late final enabled;
|
||||||
|
switch (action) {
|
||||||
|
case VideoAction.togglePlay:
|
||||||
|
case VideoAction.replay10:
|
||||||
|
case VideoAction.setSpeed:
|
||||||
|
enabled = true;
|
||||||
|
break;
|
||||||
|
case VideoAction.captureFrame:
|
||||||
|
enabled = controller?.renderingVideoNotifier.value ?? false;
|
||||||
|
break;
|
||||||
|
case VideoAction.selectStreams:
|
||||||
|
enabled = controller?.canSelectStreamNotifier.value ?? false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Widget? child;
|
Widget? child;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case VideoAction.togglePlay:
|
case VideoAction.togglePlay:
|
||||||
|
@ -287,15 +309,13 @@ class _ButtonRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case VideoAction.captureFrame:
|
case VideoAction.captureFrame:
|
||||||
enabled = controller?.renderingVideoNotifier.value ?? false;
|
|
||||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
|
||||||
break;
|
|
||||||
case VideoAction.replay10:
|
case VideoAction.replay10:
|
||||||
case VideoAction.selectStreams:
|
case VideoAction.selectStreams:
|
||||||
case VideoAction.setSpeed:
|
case VideoAction.setSpeed:
|
||||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PopupMenuItem(
|
return PopupMenuItem(
|
||||||
value: action,
|
value: action,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
|
|
|
@ -27,8 +27,6 @@ abstract class AvesVideoController {
|
||||||
|
|
||||||
Stream<VideoStatus> get statusStream;
|
Stream<VideoStatus> get statusStream;
|
||||||
|
|
||||||
ValueNotifier<bool> get renderingVideoNotifier;
|
|
||||||
|
|
||||||
bool get isReady;
|
bool get isReady;
|
||||||
|
|
||||||
bool get isPlaying => status == VideoStatus.playing;
|
bool get isPlaying => status == VideoStatus.playing;
|
||||||
|
@ -43,6 +41,10 @@ abstract class AvesVideoController {
|
||||||
|
|
||||||
Stream<String?> get timedTextStream;
|
Stream<String?> get timedTextStream;
|
||||||
|
|
||||||
|
ValueNotifier<bool> get renderingVideoNotifier;
|
||||||
|
|
||||||
|
ValueNotifier<bool> get canSelectStreamNotifier;
|
||||||
|
|
||||||
ValueNotifier<double> get sarNotifier;
|
ValueNotifier<double> get sarNotifier;
|
||||||
|
|
||||||
double get speed;
|
double get speed;
|
||||||
|
@ -57,7 +59,7 @@ abstract class AvesVideoController {
|
||||||
|
|
||||||
Future<StreamSummary?> getSelectedStream(StreamType type);
|
Future<StreamSummary?> getSelectedStream(StreamType type);
|
||||||
|
|
||||||
Map<StreamSummary, bool> get streams;
|
List<StreamSummary> get streams;
|
||||||
|
|
||||||
Future<Uint8List> captureFrame();
|
Future<Uint8List> captureFrame();
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
final AChangeNotifier _completedNotifier = AChangeNotifier();
|
final AChangeNotifier _completedNotifier = AChangeNotifier();
|
||||||
Offset _macroBlockCrop = Offset.zero;
|
Offset _macroBlockCrop = Offset.zero;
|
||||||
final List<StreamSummary> _streams = [];
|
final List<StreamSummary> _streams = [];
|
||||||
final ValueNotifier<StreamSummary?> _selectedVideoStream = ValueNotifier(null);
|
|
||||||
final ValueNotifier<StreamSummary?> _selectedAudioStream = ValueNotifier(null);
|
|
||||||
final ValueNotifier<StreamSummary?> _selectedTextStream = ValueNotifier(null);
|
|
||||||
Timer? _initialPlayTimer;
|
Timer? _initialPlayTimer;
|
||||||
double _speed = 1;
|
double _speed = 1;
|
||||||
|
|
||||||
|
@ -41,6 +38,9 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
@override
|
@override
|
||||||
final ValueNotifier<bool> renderingVideoNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> renderingVideoNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ValueNotifier<bool> canSelectStreamNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final ValueNotifier<double> sarNotifier = ValueNotifier(1);
|
final ValueNotifier<double> sarNotifier = ValueNotifier(1);
|
||||||
|
|
||||||
|
@ -177,10 +177,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
_instance.applyOptions(options);
|
_instance.applyOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchSelectedStreams() async {
|
void _fetchStreams() async {
|
||||||
final mediaInfo = await _instance.getInfo();
|
final mediaInfo = await _instance.getInfo();
|
||||||
if (!mediaInfo.containsKey(Keys.streams)) return;
|
if (!mediaInfo.containsKey(Keys.streams)) return;
|
||||||
|
|
||||||
|
var videoStreamCount = 0, audioStreamCount = 0, textStreamCount = 0;
|
||||||
|
|
||||||
_streams.clear();
|
_streams.clear();
|
||||||
final allStreams = (mediaInfo[Keys.streams] as List).cast<Map>();
|
final allStreams = (mediaInfo[Keys.streams] as List).cast<Map>();
|
||||||
allStreams.forEach((stream) {
|
allStreams.forEach((stream) {
|
||||||
|
@ -195,26 +197,25 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
width: stream[Keys.width] as int?,
|
width: stream[Keys.width] as int?,
|
||||||
height: stream[Keys.height] as int?,
|
height: stream[Keys.height] as int?,
|
||||||
));
|
));
|
||||||
|
switch (type) {
|
||||||
|
case StreamType.video:
|
||||||
|
videoStreamCount++;
|
||||||
|
break;
|
||||||
|
case StreamType.audio:
|
||||||
|
audioStreamCount++;
|
||||||
|
break;
|
||||||
|
case StreamType.text:
|
||||||
|
textStreamCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
StreamSummary? _getSelectedStream(String selectedIndexKey) {
|
canSelectStreamNotifier.value = videoStreamCount > 1 || audioStreamCount > 1 || textStreamCount > 0;
|
||||||
final indexString = mediaInfo[selectedIndexKey];
|
|
||||||
if (indexString != null) {
|
|
||||||
final index = int.tryParse(indexString);
|
|
||||||
if (index != null && index != -1) {
|
|
||||||
return _streams.firstWhereOrNull((stream) => stream.index == index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedVideoStream.value = _getSelectedStream(Keys.selectedVideoStream);
|
final selectedVideo = await getSelectedStream(StreamType.video);
|
||||||
_selectedAudioStream.value = _getSelectedStream(Keys.selectedAudioStream);
|
if (selectedVideo != null) {
|
||||||
_selectedTextStream.value = _getSelectedStream(Keys.selectedTextStream);
|
final streamIndex = selectedVideo.index;
|
||||||
|
|
||||||
if (_selectedVideoStream.value != null) {
|
|
||||||
final streamIndex = _selectedVideoStream.value!.index;
|
|
||||||
final streamInfo = allStreams.firstWhereOrNull((stream) => stream[Keys.index] == streamIndex);
|
final streamInfo = allStreams.firstWhereOrNull((stream) => stream[Keys.index] == streamIndex);
|
||||||
if (streamInfo != null) {
|
if (streamInfo != null) {
|
||||||
final num = streamInfo[Keys.sarNum] ?? 0;
|
final num = streamInfo[Keys.sarNum] ?? 0;
|
||||||
|
@ -226,7 +227,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
|
|
||||||
void _onValueChanged() {
|
void _onValueChanged() {
|
||||||
if (_instance.state == FijkState.prepared && _streams.isEmpty) {
|
if (_instance.state == FijkState.prepared && _streams.isEmpty) {
|
||||||
_fetchSelectedStreams();
|
_fetchStreams();
|
||||||
}
|
}
|
||||||
_valueStreamController.add(_instance.value);
|
_valueStreamController.add(_instance.value);
|
||||||
}
|
}
|
||||||
|
@ -301,17 +302,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
// TODO TLAD [video] bug: setting speed fails when there is no audio stream or audio is disabled
|
// TODO TLAD [video] bug: setting speed fails when there is no audio stream or audio is disabled
|
||||||
void _applySpeed() => _instance.setSpeed(speed);
|
void _applySpeed() => _instance.setSpeed(speed);
|
||||||
|
|
||||||
ValueNotifier<StreamSummary?> selectedStreamNotifier(StreamType type) {
|
|
||||||
switch (type) {
|
|
||||||
case StreamType.video:
|
|
||||||
return _selectedVideoStream;
|
|
||||||
case StreamType.audio:
|
|
||||||
return _selectedAudioStream;
|
|
||||||
case StreamType.text:
|
|
||||||
return _selectedTextStream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a stream is selected, the video accelerates to catch up with it.
|
// When a stream is selected, the video accelerates to catch up with it.
|
||||||
// The duration of this acceleration phase depends on the player `min-frames` parameter.
|
// The duration of this acceleration phase depends on the player `min-frames` parameter.
|
||||||
// Calling `seekTo` after stream de/selection is a workaround to:
|
// Calling `seekTo` after stream de/selection is a workaround to:
|
||||||
|
@ -325,11 +315,9 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
final newIndex = selected.index;
|
final newIndex = selected.index;
|
||||||
if (newIndex != null) {
|
if (newIndex != null) {
|
||||||
await _instance.selectTrack(newIndex);
|
await _instance.selectTrack(newIndex);
|
||||||
selectedStreamNotifier(type).value = selected;
|
|
||||||
}
|
}
|
||||||
} else if (current != null) {
|
} else if (current != null) {
|
||||||
await _instance.deselectTrack(current.index!);
|
await _instance.deselectTrack(current.index!);
|
||||||
selectedStreamNotifier(type).value = null;
|
|
||||||
}
|
}
|
||||||
await seekTo(currentPosition);
|
await seekTo(currentPosition);
|
||||||
}
|
}
|
||||||
|
@ -342,10 +330,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<StreamSummary, bool> get streams {
|
List<StreamSummary> get streams => _streams;
|
||||||
final selectedIndices = {_selectedVideoStream, _selectedAudioStream, _selectedTextStream}.map((v) => v.value?.index).toSet();
|
|
||||||
return Map.fromEntries(_streams.map((stream) => MapEntry(stream, selectedIndices.contains(stream.index))));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> captureFrame() {
|
Future<Uint8List> captureFrame() {
|
||||||
|
|
|
@ -120,17 +120,20 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showStreamSelectionDialog(BuildContext context, AvesVideoController controller) async {
|
Future<void> _showStreamSelectionDialog(BuildContext context, AvesVideoController controller) async {
|
||||||
final selectedStreams = await showDialog<Map<StreamType, StreamSummary>>(
|
final streams = controller.streams;
|
||||||
|
final currentSelectedStreams = await Future.wait(StreamType.values.map(controller.getSelectedStream));
|
||||||
|
final currentSelectedIndices = currentSelectedStreams.where((v) => v != null).cast<StreamSummary>().map((v) => v.index).toSet();
|
||||||
|
|
||||||
|
final userSelectedStreams = await showDialog<Map<StreamType, StreamSummary>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => VideoStreamSelectionDialog(
|
builder: (context) => VideoStreamSelectionDialog(
|
||||||
streams: controller.streams,
|
streams: Map.fromEntries(streams.map((stream) => MapEntry(stream, currentSelectedIndices.contains(stream.index)))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (selectedStreams == null || selectedStreams.isEmpty) return;
|
if (userSelectedStreams == null || userSelectedStreams.isEmpty) return;
|
||||||
|
|
||||||
// TODO TLAD [video] get stream list & guess default selected streams, when the controller is not initialized yet
|
|
||||||
await Future.forEach<MapEntry<StreamType, StreamSummary>>(
|
await Future.forEach<MapEntry<StreamType, StreamSummary>>(
|
||||||
selectedStreams.entries,
|
userSelectedStreams.entries,
|
||||||
(kv) => controller.selectStream(kv.key, kv.value),
|
(kv) => controller.selectStream(kv.key, kv.value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue