PSDK-1137 - Добавил переключение качеств и поддержку стартового качества
This commit is contained in:
committed by
Elena Nazarova
parent
ca6c2505ba
commit
833a478637
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
-20
@@ -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
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
|
||||
Reference in New Issue
Block a user