video: disable stream selection when irrelevant

This commit is contained in:
Thibault Deckers 2021-06-22 18:10:09 +09:00
parent 7bedca9537
commit e40770b1c4
4 changed files with 72 additions and 62 deletions

View file

@ -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,

View file

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

View file

@ -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() {

View file

@ -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),
); );
} }