PSDK-1137 - Добавил переключение качеств и поддержку стартового качества

This commit is contained in:
Андрей Геращенко
2023-12-01 17:36:16 +03:00
committed by Elena Nazarova
parent ca6c2505ba
commit 833a478637
9 changed files with 213 additions and 42 deletions
@@ -6,12 +6,14 @@ import 'package:nut_player_example/src/common/repository/settings_repository.dar
import 'package:nut_player_example/src/features/player_screen/domain/model/subtitle.dart';
import 'package:nut_player_example/src/features/player_screen/domain/model/video_quality.dart';
import 'package:nut_player_example/src/features/player_screen/mapper/settings_mapper.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
part 'playerview_event.dart';
part 'playerview_state.dart';
class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
final VideoPlayerController controller;
late SettingsRepository startSettings;
late VoidCallback? _subtitlesListener;
late VoidCallback? _qualitiesListener;
@@ -31,6 +33,7 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
'chunk': repository.chunk
}}),
super(PlayerViewController.createFromRepository(repository)) {
startSettings = repository;
_onInitialize(repository);
on<DismissEvent>(_onDismissEvent);
on<PlayEvent>(_onPlayEvent);
@@ -54,17 +57,15 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
_qualitiesListener = () {
final qualities = controller.value.qualities;
if (qualities != null) {
if (qualities.isNotEmpty) {
if (qualities != null && qualities.isNotEmpty) {
add(QualitiesReceivedEvent(qualities));
}
final listener = _qualitiesListener;
if (listener != null) {
controller.removeListener(listener);
}
_listenToQualityChanges(false);
_qualitiesListener = null;
}
};
_listenToQualityChanges(true);
if (!repository.isSubtitlesAvailable) { return; }
_subtitlesListener = () {
final subtitles = controller.value.subtitles;
@@ -78,6 +79,13 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
_listenToSubtitlesChanges(true);
}
_listenToQualityChanges(bool listen) {
final listener = _qualitiesListener;
if (listener != null) {
listen ? controller.addListener(listener) : controller.removeListener(listener);
}
}
_listenToSubtitlesChanges(bool listen) {
final listener = _subtitlesListener;
if (listener != null) {
@@ -109,7 +117,7 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
_onQualitiesReceivedEvent(QualitiesReceivedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final List<VideoQualityOption> mappedqualities = event.qualities.map((e) =>
final List<VideoQualityOption> mappedQualities = event.qualities.map((e) =>
VideoQualityOption(
id: e["id"],
bandwidth: e["bandwidth"],
@@ -117,7 +125,12 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
height: e["height"]
)
).toList();
emit(playerState.copy(qualities: mappedqualities));
final settingsQuality = startSettings.quality;
final presetQuality = mappedQualities.firstWhere((element) =>
element.quality.identifier == settingsQuality.identifier,
orElse: () => mappedQualities.first);
var sortedQualities = mappedQualities..sort((e1, e2) => e1.height.compareTo(e2.height));
emit(playerState.copy(qualities: sortedQualities, quality: presetQuality));
}
_onSubsReceivedEvent(SubsReceivedEvent event, Emitter<PlayerViewState> emit) {
@@ -178,7 +191,15 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
}
_onQualityChangedEvent(QualityChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final currentQuality = playerState.qualities?.firstWhere((element) => event.option.value == element.id);
if (currentQuality == null) { return; }
controller.setQuality(currentQuality.id);
emit(playerState.copy(quality: currentQuality));
}
int currentNumericValue(NumericOptionData setting) {
@@ -199,6 +220,8 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
return _selectedIndexForSpeed(setting.options);
} else if (setting.key == const Key('PlaybackSubsSettingID')) {
return _selectedIndexForSubtitle(setting.options);
} else if (setting.key == const Key('PlaybackQualitySettingID')) {
return _selectedIndexForQuality(setting.options);
} else {
return 0;
}
@@ -224,4 +247,11 @@ class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
return settings.indexWhere((element) => element.value == playerState.currentSubtitle?.id);
}
int _selectedIndexForQuality(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
return settings.indexWhere((element) => element.value == playerState.quality?.id);
}
}
@@ -9,7 +9,7 @@ class PlayerViewController extends PlayerViewState {
final double volume;
final double speed;
final int? start;
final String? quality;
final VideoQualityOption? quality;
final List<Subtitle>? subtitles;
final List<VideoQualityOption>? qualities;
final Subtitle? currentSubtitle;
@@ -27,8 +27,7 @@ class PlayerViewController extends PlayerViewState {
{double? volume,
double? speed,
int? start,
String? quality,
String? subtitle,
VideoQualityOption? quality,
List<Subtitle>? subtitles,
List<VideoQualityOption>? qualities,
Subtitle? currentSubtitle}) {
@@ -81,23 +80,6 @@ class PlayerViewController extends PlayerViewState {
selectedIndex: 4,
onSelectedOption: (option) => VolumeChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackQualitySettingID'),
title: 'Качество',
options: const [
OptionData.withValueEqualTitle(Key('PlaybackOptionQualityAutoID'), 'Авто'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality4kID'), '4K'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality1440pID'), '1440p Ultra HD'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality1080pID'), '1080p FHD'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality720pID'), '720p HD'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality480pID'), '480p'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality360pID'), '360p'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality240pID'), '240p'),
OptionData.withValueEqualTitle(Key('PlaybackOptionQuality144pID'), '144p')
],
selectedIndex: 0,
onSelectedOption: (option) => QualityChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackSpeedSettingID'),
@@ -140,4 +122,21 @@ class PlayerViewController extends PlayerViewState {
onSelectedOption: (option) => SubsChangedEvent(option)
);
}
static OptionDataContainer? createQualities(List<VideoQualityOption>? qualities) {
if (qualities == null) { return null; }
final options = qualities.map((option) => OptionData(
key: Key(option.id),
title: option.title,
value: option.id
)).toList();
return OptionDataContainer(
key: const Key('PlaybackQualitySettingID'),
title: 'Качество',
options: options,
selectedIndex: 0,
onSelectedOption: (option) => QualityChangedEvent(option)
);
}
}
@@ -1,13 +1,96 @@
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
class VideoQualityOption {
String id;
int bandwidth;
int width;
int height;
late VideoQuality quality;
late String title;
VideoQualityOption({
required this.id,
required this.bandwidth,
required this.width,
required this.height
});
}) {
quality = _videoQuality(width,height,bandwidth);
title = _title(quality);
}
static String _title(VideoQuality quality) {
switch (quality) {
case VideoQuality.auto:
return "Авто";
case VideoQuality.p144:
return "p144";
case VideoQuality.p240:
return "p240";
case VideoQuality.p360:
return "p360";
case VideoQuality.p480:
return "p480";
case VideoQuality.p720:
return "p720 HD";
case VideoQuality.p1080:
return "p1080 FHD";
case VideoQuality.p1440:
return "p1440 Ultra HD";
case VideoQuality.p2160:
return "4K";
}
}
static VideoQuality _qualityFromResolution(int pixels) {
if (pixels >= 1000 && pixels < 90500) {
return VideoQuality.p144;
} else if (pixels >= 90500 && pixels < 170500) {
return VideoQuality.p240;
} else if (pixels >= 170500 && pixels < 280500) {
return VideoQuality.p360;
} else if (pixels >= 280500 && pixels < 640500) {
return VideoQuality.p480;
} else if (pixels >= 640500 && pixels < 1500500) {
return VideoQuality.p720;
} else if (pixels >= 1500500 && pixels < 2400500) {
return VideoQuality.p1080;
} else if (pixels >= 2400500 && pixels < 6000500) {
return VideoQuality.p1440;
} else if (pixels >= 6000500) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _qualityFromBandwidth(int bandwidth) {
if (bandwidth >= 1 && bandwidth < 400001) {
return VideoQuality.p144;
} else if (bandwidth >= 400001 && bandwidth < 800001) {
return VideoQuality.p240;
} else if (bandwidth >= 800001 && bandwidth < 1200001) {
return VideoQuality.p360;
} else if (bandwidth >= 1200001 && bandwidth < 1800001) {
return VideoQuality.p480;
} else if (bandwidth >= 1800001 && bandwidth < 3500001) {
return VideoQuality.p720;
} else if (bandwidth >= 3500001 && bandwidth < 8000001) {
return VideoQuality.p1080;
} else if (bandwidth >= 8000001 && bandwidth < 12000001) {
return VideoQuality.p1440;
} else if (bandwidth >= 12000001) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _videoQuality(int width, int height, int bandwidth) {
if (width != 0 && height != 0) {
final pixels = width * height;
return _qualityFromResolution(pixels);
} else {
return _qualityFromBandwidth(bandwidth);
}
}
}
@@ -156,7 +156,7 @@ class _PlayerViewState extends State<PlayerView> {
fontSize: 13,
fontWeight: FontWeight.w400)),
),
children: _buildWidgetsWithSubs(PlayerViewController.playbackSettings, _bloc)
children: _buildDynamicWidgets(PlayerViewController.playbackSettings, _bloc)
),
CupertinoListSection.insetGrouped(
margin:
@@ -221,15 +221,21 @@ class _PlayerViewState extends State<PlayerView> {
);
}
List<Widget> _buildWidgetsWithSubs(List<Object> objects, PlayerViewBloc bloc) {
List<Widget> _buildDynamicWidgets(List<Object> objects, PlayerViewBloc bloc) {
var widgets = _buildWidgets(objects, bloc);
final state = bloc.state;
if (state is! PlayerViewController) { return widgets; }
final qualities = PlayerViewController.createQualities(state.qualities);
if (qualities != null) {
final qualityWidget = _buildOptionsView(qualities, bloc);
widgets.insert(1, qualityWidget);
}
final subtitles = PlayerViewController.createSubs(state.subtitles);
if (subtitles == null) { return widgets; }
final subtitleWidget = _buildOptionsView(subtitles, bloc);
widgets.insert(2, subtitleWidget);
if (subtitles != null) {
final subtitleWidget = _buildOptionsView(subtitles, bloc);
widgets.insert(2, subtitleWidget);
}
return widgets;
}
@@ -203,7 +203,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
}
break;
case VideoEventType.didFetchQualities:
value.copyWith(qualities: event.qualities);
value = value.copyWith(qualities: event.qualities);
break;
case VideoEventType.unknown:
break;
@@ -272,6 +272,15 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
await nutPlayerPlatform.setSubtitle(playerId, subtitleID);
}
// Установить качество по идентификатору
Future<void> setQuality(String qualityID) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.setQuality(playerId, qualityID);
}
Future<void> setLog(String limitOutputLevel) async {
await nutPlayerPlatform.setLimitOutputLevel(limitOutputLevel);
}
@@ -74,6 +74,8 @@ public class NutPlayerIosPlugin: NSObject, FlutterPlugin, NutPlayerViewFactoryDe
self.pluginSetVolume(arguments, result: result)
case "pluginGetVolume":
self.pluginGetVolume(arguments, result: result)
case "pluginSetQuality":
self.pluginSetQuality(arguments, result: result)
case "pluginSetSubtitles":
self.pluginSetSubtitles(arguments, result: result)
case "pluginGetState":
@@ -194,6 +196,19 @@ public class NutPlayerIosPlugin: NSObject, FlutterPlugin, NutPlayerViewFactoryDe
result(nil)
}
private func pluginSetQuality(_ arguments: [Any], result: @escaping FlutterResult) {
guard arguments.count == 2,
let playerId = arguments.first as? Int64,
let qualityID = arguments.last as? String,
let record = self.players[playerId] else {
result(FlutterMethodNotImplemented)
return
}
record.platform.change(quality: qualityID)
result(nil)
}
private func pluginSetSubtitles(_ arguments: [Any], result: @escaping FlutterResult) {
guard arguments.count == 2,
let playerId = arguments.first as? Int64,
@@ -118,10 +118,11 @@ final class PlayerFlutterPlatform: NSObject, FlutterStreamHandler {
player.qualities
.receive(on: DispatchQueue.main)
.sink { [weak self] qualities in
guard let self else { return }
guard let self, qualities.count > 0 else { return }
let qualities = Self.mapQualitiesToDictionaries(qualities: qualities)
self.sinkEvent?([
"event": FlutterDataEvent.updatedQualities.rawValue,
"qualities": Self.mapQualitiesToDictionaries(qualities: qualities)
"qualities": qualities
])
}
.store(in: &self.cancellable)
@@ -239,6 +240,16 @@ final class PlayerFlutterPlatform: NSObject, FlutterStreamHandler {
handler(subFromMenu)
}
func change(quality: String) {
guard let player = self.nutPlayer,
let rootMenu = player.menu.first(where: { $0.group == "Qualities" }),
case let .submenu(menu) = rootMenu.element else { return }
guard let newQuality = menu.first(where: { $0.id == quality }),
case let .action(handler) = newQuality.element else { return }
handler(newQuality)
}
func play() { self.nutPlayer?.play() }
func pause() {
@@ -291,8 +302,8 @@ final class PlayerFlutterPlatform: NSObject, FlutterStreamHandler {
private static func mapToDictionary(quality: PlayerQualityRecord) -> [String: Any] {
[
"id": quality.id,
"width": quality.resolution.width,
"height": quality.resolution.height,
"width": Int(quality.resolution.width),
"height": Int(quality.resolution.height),
"bandwidth": quality.bandwidth
]
}
+14 -1
View File
@@ -107,7 +107,15 @@ class NutPlayerIosPlatform extends NutPlayerPlatform {
isPlaying: map['isPlaying'] as bool,
);
case 'updatedQualities':
return VideoEvent(eventType: VideoEventType.didFetchQualities);
final qualitiesList = map["qualities"] as List<dynamic>;
final mappedList = qualitiesList.map((e) => e as Map<dynamic, dynamic>)
.toList(growable: false);
final qualities = mappedList.map((e) => e.map((key, value) => MapEntry(key.toString(), value)))
.toList(growable: false);
return VideoEvent(
eventType: VideoEventType.didFetchQualities,
qualities: qualities,
);
default:
return VideoEvent(eventType: VideoEventType.unknown);
}
@@ -146,6 +154,11 @@ class NutPlayerIosPlatform extends NutPlayerPlatform {
return _pluginChannel.invokeMethod("pluginSetSubtitles", [playerId, id]);
}
@override
Future<void> setQuality(PlayerId playerId, String id) {
return _pluginChannel.invokeMethod("pluginSetQuality", [playerId, id]);
}
@override
Future<void> setLimitOutputLevel(String limitOutputLevel) {
return _pluginChannel.invokeMethod("pluginSetLimitOutputLevel", [limitOutputLevel]);
@@ -86,6 +86,11 @@ abstract class NutPlayerPlatform extends PlatformInterface {
throw UnimplementedError('setSubtitle() has not been implemented.');
}
/// Установить новое качество видео
Future<void> setQuality(PlayerId playerId, String qualityID) {
throw UnimplementedError('setQuality() has not been implemented.');
}
/// Sets the video position to a [Duration] from the start.
Future<void> seek(PlayerId playerId, Duration position) {
throw UnimplementedError('seekTo() has not been implemented.');