Created
September 12, 2025 17:52
-
-
Save dickermoshe/01e6dbdb13892b4703342c98602980db to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:async'; | |
| import 'dart:ffi'; | |
| import 'package:jni/jni.dart'; | |
| import 'package:shiur_23_app/android_utils.dart'; | |
| import 'package:shiur_23_app/player.interface.dart'; | |
| import 'package:solidart/solidart.dart'; | |
| import 'dart:core' as core show Future; | |
| import 'dart:core' hide Future; | |
| /// A class which contains the connection to the MediaController | |
| /// It is used to dispose of the MediaController when the player is disposed | |
| class _PlayerConnection { | |
| final MediaController mediaController; | |
| final ListenableFuture<MediaController?> mediaControllerFuture; | |
| final Timer positionTimer; | |
| final Effect durationEffect; | |
| final Arena arena; | |
| _PlayerConnection({ | |
| required this.mediaController, | |
| required this.mediaControllerFuture, | |
| required this.positionTimer, | |
| required this.durationEffect, | |
| required this.arena, | |
| }); | |
| void dispose() { | |
| MediaController.releaseFuture( | |
| mediaControllerFuture.as(Future.type(MediaController.nullableType)), | |
| ); | |
| arena.releaseAll(); | |
| } | |
| } | |
| class AndroidPlayer implements MyPlayer, Finalizable { | |
| final _PlayerConnection _connection; | |
| @override | |
| final Signal<PlayerState> playerState; | |
| @override | |
| final Signal<CurrentTrack?> currentTrack; | |
| @override | |
| final Signal<PlayerError?> playbackError; | |
| @override | |
| final Signal<double> speed; | |
| @override | |
| final Signal<int> position; | |
| @override | |
| final Signal<int?> duration; | |
| @override | |
| final Signal<bool> isPlaying; | |
| final Arena arena; | |
| /// A finalizer to dispose of the player when it is disposed | |
| static final _finalizer = Finalizer<_PlayerConnection>((o) { | |
| o.dispose(); | |
| }); | |
| /// The instance of the player | |
| static AndroidPlayer? _instance; | |
| AndroidPlayer( | |
| this._connection, { | |
| required this.playerState, | |
| required this.currentTrack, | |
| required this.playbackError, | |
| required this.position, | |
| required this.speed, | |
| required this.duration, | |
| required this.isPlaying, | |
| required this.arena, | |
| }); | |
| static core.Future<AndroidPlayer> create() async { | |
| // If the player is already created, return the instance | |
| if (_instance != null) { | |
| return _instance!; | |
| } | |
| // Some signals to track the player state | |
| final playerStateSignal = Signal(PlayerState.STATE_IDLE); | |
| final currentTrackSignal = Signal<CurrentTrack?>(null); | |
| final playbackErrorSignal = Signal<PlayerError?>(null); | |
| final positionSignal = Signal<int>(0); | |
| final durationSignal = Signal<int?>(null); | |
| final isPlayingSignal = Signal<bool>(false); | |
| // Arena to dispose of the objects | |
| final arena = Arena(); | |
| // Get the context and build the MediaController | |
| final contextRef = Jni.getCachedApplicationContext(); | |
| final context = Context.fromReference(contextRef); | |
| final serviceName = "com.shiur23.shiur_23_app.PlaybackService".toJString() | |
| ..releasedBy(arena); | |
| final componentName = ComponentName.new$1(context, serviceName) | |
| ..releasedBy(arena); | |
| final sessionToken = SessionToken(context, componentName) | |
| ..releasedBy(arena); | |
| final controllerBuilder = MediaController$Builder(context, sessionToken) | |
| ..releasedBy(arena); | |
| final future = controllerBuilder.buildAsync()?..releasedBy(arena); | |
| if (future == null) { | |
| throw Exception("Failed to build MediaController"); | |
| } | |
| // Wait for Java to build the MediaController | |
| MediaController? mediaController; | |
| future.addListener( | |
| Runnable.implement( | |
| $Runnable( | |
| run$async: true, | |
| run: () { | |
| mediaController = switch (future.get()) { | |
| MediaController controller => controller..releasedBy(arena), | |
| _ => throw Exception("Failed to build MediaController"), | |
| }; | |
| }, | |
| ), | |
| )..releasedBy(arena), | |
| ContextCompat.getMainExecutor(context)..releasedBy(arena), | |
| ); | |
| await core.Future(() async { | |
| while (mediaController == null) { | |
| await core.Future.delayed(Duration(milliseconds: 100)); | |
| } | |
| }).timeout(Duration(seconds: 10)); | |
| if (mediaController == null) { | |
| throw Exception("Failed to build MediaController"); | |
| } | |
| // A signal to track the speed | |
| final speed = switch (mediaController!.getPlaybackParameters()?.speed) { | |
| double speed => Signal(speed), | |
| _ => Signal(1.0), | |
| }; | |
| // A signal to track the duration | |
| final durationEffect = Effect(() { | |
| final state = playerStateSignal.value; | |
| if (state == PlayerState.STATE_READY) { | |
| durationSignal.value = mediaController!.getDuration(); | |
| } | |
| }); | |
| // Add a listener to the MediaController | |
| // to track the player state | |
| mediaController!.addListener( | |
| Player$Listener.implement( | |
| $Player$Listener( | |
| onMediaItemTransition: (MediaItem? mediaItem, int i) { | |
| if (mediaItem != null) { | |
| currentTrackSignal.value = trackFromMediaItem(mediaItem); | |
| } else { | |
| currentTrackSignal.value = null; | |
| } | |
| }, | |
| onPlaybackStateChanged: (int i) { | |
| playerStateSignal.value = PlayerState.values[i - 1]; | |
| }, | |
| onPlaybackParametersChanged: | |
| (PlaybackParameters? playbackParameters) { | |
| if (playbackParameters != null) { | |
| speed.value = playbackParameters.speed; | |
| } | |
| }, | |
| onPlayerErrorChanged: (playbackException) { | |
| if (playbackException == null) { | |
| playbackErrorSignal.value = null; | |
| } else { | |
| final message = | |
| "Error: ${playbackException.errorCode} at ${playbackException.timestampMs}"; | |
| playbackErrorSignal.value = PlayerError(message: message); | |
| } | |
| }, | |
| onPositionDiscontinuity: (int i) {}, | |
| onPositionDiscontinuity$1: | |
| ( | |
| Player$PositionInfo? positionInfo, | |
| Player$PositionInfo? positionInfo1, | |
| int i, | |
| ) {}, | |
| onPlayerError: (JObject? playbackException) {}, | |
| onSeekBackIncrementChanged: (int j) {}, | |
| onSeekForwardIncrementChanged: (int j) {}, | |
| onMaxSeekToPreviousPositionChanged: (int j) {}, | |
| onAudioSessionIdChanged: (int i) {}, | |
| onAudioAttributesChanged: (JObject? audioAttributes) {}, | |
| onVolumeChanged: (double f) {}, | |
| onSkipSilenceEnabledChanged: (bool z) {}, | |
| onDeviceInfoChanged: (JObject? deviceInfo) {}, | |
| onDeviceVolumeChanged: (int i, bool z) {}, | |
| onVideoSizeChanged: (JObject? videoSize) {}, | |
| onSurfaceSizeChanged: (int i, int i1) {}, | |
| onRenderedFirstFrame: () {}, | |
| onCues: (JList<JObject?>? list) {}, | |
| onCues$1: (JObject? cueGroup) {}, | |
| onMetadata: (JObject? metadata) {}, | |
| onEvents: (Player? player, Player$Events? events) {}, | |
| onTimelineChanged: (JObject? timeline, int i) {}, | |
| onTracksChanged: (JObject? tracks) {}, | |
| onMediaMetadataChanged: (MediaMetadata? mediaMetadata) {}, | |
| onPlaylistMetadataChanged: (MediaMetadata? mediaMetadata) {}, | |
| onIsLoadingChanged: (bool z) {}, | |
| onLoadingChanged: (bool z) {}, | |
| onAvailableCommandsChanged: (Player$Commands? commands) {}, | |
| onTrackSelectionParametersChanged: | |
| (JObject? trackSelectionParameters) {}, | |
| onPlayerStateChanged: (bool z, int i) {}, | |
| onPlayWhenReadyChanged: (bool z, int i) { | |
| isPlayingSignal.value = z; | |
| }, | |
| onPlaybackSuppressionReasonChanged: (int i) {}, | |
| onIsPlayingChanged: (bool z) {}, | |
| onRepeatModeChanged: (int i) {}, | |
| onShuffleModeEnabledChanged: (bool z) {}, | |
| ), | |
| )..releasedBy(arena), | |
| ); | |
| // Fetch the position of the player every 100 milliseconds | |
| final positionTimer = Timer.periodic(Duration(milliseconds: 100), (timer) { | |
| positionSignal.value = mediaController!.getCurrentPosition(); | |
| }); | |
| // Create the connection to the MediaController | |
| final connection = _PlayerConnection( | |
| mediaController: mediaController!, | |
| mediaControllerFuture: future, | |
| positionTimer: positionTimer, | |
| durationEffect: durationEffect, | |
| arena: arena, | |
| ); | |
| // Create the player | |
| final player = AndroidPlayer( | |
| connection, | |
| playerState: playerStateSignal, | |
| currentTrack: currentTrackSignal, | |
| playbackError: playbackErrorSignal, | |
| position: positionSignal, | |
| speed: speed, | |
| duration: durationSignal, | |
| isPlaying: isPlayingSignal, | |
| arena: arena, | |
| ); | |
| // Attach the finalizer to the player | |
| _finalizer.attach(player, connection, detach: player); | |
| return player; | |
| } | |
| @override | |
| void play() { | |
| _connection.mediaController.play(); | |
| } | |
| @override | |
| void pause() { | |
| _connection.mediaController.pause(); | |
| } | |
| @override | |
| void seek(int position) { | |
| _connection.mediaController.seekTo(position); | |
| } | |
| @override | |
| void setTrack(Track item) { | |
| try { | |
| final mediaItemBuilder = MediaItem$Builder(); | |
| switch (item) { | |
| case LocalTrack localTrack: | |
| final path = localTrack.file.path.toJString(); | |
| mediaItemBuilder.setUri$1(Uri.fromFile(File.new$1(path))); | |
| break; | |
| case RemoteTrack remoteTrack: | |
| final url = remoteTrack.url.toJString(); | |
| mediaItemBuilder.setUri$1(Uri.parse(url)); | |
| break; | |
| } | |
| final mediaId = item.id.toJString(); | |
| mediaItemBuilder.setMediaId(mediaId); | |
| final mediaMetadataBuilder = MediaMetadata$Builder(); | |
| final title = item.title.toJString(); | |
| final artist = item.artist.toJString(); | |
| mediaMetadataBuilder.setTitle(title.as(CharSequence.type)); | |
| mediaMetadataBuilder.setArtist(artist.as(CharSequence.type)); | |
| final artworkUrl = item.artworkUrl.toJString(); | |
| if (artworkUrl.toDartString().isNotEmpty) { | |
| mediaMetadataBuilder.setArtworkUri(Uri.parse(artworkUrl)); | |
| } | |
| final mediaMetadata = mediaMetadataBuilder.build(); | |
| mediaItemBuilder.setMediaMetadata(mediaMetadata); | |
| final mediaItem = mediaItemBuilder.build(); | |
| if (mediaItem == null) { | |
| throw Exception("Failed to build MediaItem"); | |
| } | |
| _connection.mediaController.setMediaItem(mediaItem); | |
| } catch (e) { | |
| playbackError.value = PlayerError(message: "Failed to set track: $e"); | |
| } | |
| } | |
| @override | |
| void setSpeed(double speed) { | |
| _connection.mediaController.setPlaybackSpeed(speed); | |
| } | |
| } | |
| CurrentTrack trackFromMediaItem(MediaItem mediaItem) { | |
| final mediaId = mediaItem.mediaId?.toDartString() ?? 'unknown'; | |
| final mediaMetadata = mediaItem.mediaMetadata; | |
| final title = | |
| mediaMetadata?.title?.toString$1()?.toDartString() ?? 'Unknown Title'; | |
| final artist = | |
| mediaMetadata?.artist?.toString$1()?.toDartString() ?? 'Unknown Artist'; | |
| final artworkUrl = | |
| mediaMetadata?.artworkUri?.toString$1()?.toDartString() ?? ''; | |
| return CurrentTrack( | |
| id: mediaId, | |
| title: title, | |
| artist: artist, | |
| artworkUrl: artworkUrl, | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment