From 83ee2037f6b9af7afe78253a96f013066956a0ce Mon Sep 17 00:00:00 2001 From: Eduardo Roth Date: Mon, 6 Jan 2025 09:22:16 -0600 Subject: [PATCH] feat(android): refactor android code to implement player as a service --- README.md | 2 +- android/build.gradle | 1 + android/src/main/AndroidManifest.xml | 29 +- .../eduardoroth/mediaplayer/MediaPlayer.java | 100 +++-- .../mediaplayer/MediaPlayerContainer.java | 269 ++++++++---- .../mediaplayer/MediaPlayerController.java | 386 ------------------ .../MediaPlayerControllerView.java | 188 --------- .../mediaplayer/MediaPlayerPlugin.java | 20 +- .../mediaplayer/MediaPlayerService.java | 228 +++++++++++ .../mediaplayer/models/AndroidOptions.java | 4 +- .../mediaplayer/models/MediaItem.java | 9 +- .../mediaplayer/state/MediaPlayerState.java | 8 +- .../state/MediaPlayerStateProvider.java | 4 +- .../res/layout/media_player_container.xml | 15 +- .../layout/media_player_controller_view.xml | 12 +- android/src/main/res/xml/auto_app_desc.xml | 18 + src/definitions.ts | 1 + 17 files changed, 572 insertions(+), 722 deletions(-) delete mode 100644 android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerController.java delete mode 100644 android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerControllerView.java create mode 100644 android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerService.java create mode 100644 android/src/main/res/xml/auto_app_desc.xml diff --git a/README.md b/README.md index a2ea671..32d4fc3 100644 --- a/README.md +++ b/README.md @@ -524,7 +524,7 @@ removeAllListeners(options: MediaPlayerIdOptions) => Promise #### MediaPlayerAndroidOptions -{ enableChromecast?: boolean; enablePiP?: boolean; enableBackgroundPlay?: boolean; openInFullscreen?: boolean; automaticallyEnterPiP?: boolean; fullscreenOnLandscape?: boolean; top?: number; start?: number; height?: number; width?: number; } +{ enableChromecast?: boolean; enablePiP?: boolean; enableBackgroundPlay?: boolean; openInFullscreen?: boolean; automaticallyEnterPiP?: boolean; fullscreenOnLandscape?: boolean; stopOnTaskRemoved?: boolean; top?: number; start?: number; height?: number; width?: number; } #### MediaPlayerWebOptions diff --git a/android/build.gradle b/android/build.gradle index 6a3fa45..782c97a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -61,6 +61,7 @@ dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' + implementation 'androidx.lifecycle:lifecycle-service:2.8.7' testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index eb3053c..065e2f5 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,7 +2,34 @@ + + + + tools:ignore="UnusedAttribute"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayer.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayer.java index 1350e30..673e3fa 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayer.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayer.java @@ -1,14 +1,20 @@ package dev.eduardoroth.mediaplayer; +import android.content.ComponentName; +import android.os.Bundle; import android.util.Log; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaController; +import androidx.media3.session.SessionToken; import com.getcapacitor.JSObject; import com.getcapacitor.PluginCall; +import com.google.common.util.concurrent.ListenableFuture; import java.io.File; @@ -18,7 +24,6 @@ import dev.eduardoroth.mediaplayer.state.MediaPlayerState; import dev.eduardoroth.mediaplayer.state.MediaPlayerStateProvider; -@UnstableApi public class MediaPlayer { public static long VIDEO_STEP = 10000; private final AppCompatActivity _currentActivity; @@ -27,22 +32,39 @@ public class MediaPlayer { _currentActivity = currentActivity; } - @UnstableApi + @OptIn(markerClass = UnstableApi.class) public void create(PluginCall call, String playerId, String url, AndroidOptions android, ExtraOptions extra) { - JSObject ret = new JSObject(); - ret.put("method", "create"); - try { - MediaPlayerStateProvider.getState(playerId); - ret.put("result", false); - ret.put("message", "Player with id " + playerId + " is already created"); - } catch (Error err) { - extra.poster = extra.poster != null ? getFinalPath(extra.poster) : null; - MediaPlayerContainer playerContainer = new MediaPlayerContainer(_currentActivity, getFinalPath(url), playerId, android, extra); - _currentActivity.getSupportFragmentManager().beginTransaction().add(R.id.MediaPlayerFragmentContainerView, playerContainer, playerId).commit(); - ret.put("result", true); - ret.put("value", playerId); - } - call.resolve(ret); + Bundle connectionHints = new Bundle(); + connectionHints.putString("playerId", playerId); + connectionHints.putString("videoUrl", url); + connectionHints.putSerializable("android", android); + extra.poster = extra.poster != null ? getFinalPath(extra.poster) : null; + connectionHints.putSerializable("extra", extra); + + SessionToken sessionToken = new SessionToken( + _currentActivity.getApplicationContext(), + new ComponentName(_currentActivity.getApplicationContext(), MediaPlayerService.class) + ); + ListenableFuture futureController = new MediaController + .Builder(_currentActivity.getApplicationContext(), sessionToken) + .setConnectionHints(connectionHints) + .buildAsync(); + + futureController.addListener(() -> { + JSObject ret = new JSObject(); + ret.put("method", "create"); + try { + _currentActivity.getSupportFragmentManager().beginTransaction().add(R.id.MediaPlayerFragmentContainerView, new MediaPlayerContainer(futureController.get(), playerId), playerId).commit(); + ret.put("result", true); + ret.put("value", playerId); + call.resolve(ret); + } catch (Exception | Error futureError) { + ret.put("result", false); + ret.put("message", "An error occurred while creating player with id " + playerId); + Log.e("MEDIA PLAYER ROTH", futureError.toString()); + call.resolve(ret); + } + }, _currentActivity.getMainExecutor()); } public void play(PluginCall call, String playerId) { @@ -50,7 +72,7 @@ public void play(PluginCall call, String playerId) { ret.put("method", "play"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().play(); + playerState.mediaController.get().play(); ret.put("result", true); ret.put("value", true); } catch (Error err) { @@ -65,7 +87,7 @@ public void pause(PluginCall call, String playerId) { ret.put("method", "pause"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().pause(); + playerState.mediaController.get().pause(); ret.put("result", true); ret.put("value", true); } catch (Error err) { @@ -80,7 +102,7 @@ public void getDuration(PluginCall call, String playerId) { ret.put("method", "getDuration"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - long duration = playerState.playerController.get().getDuration(); + long duration = playerState.mediaController.get().getDuration(); ret.put("result", true); ret.put("value", duration == C.TIME_UNSET ? 0 : (duration / 1000)); } catch (Error err) { @@ -95,7 +117,7 @@ public void getCurrentTime(PluginCall call, String playerId) { ret.put("method", "getCurrentTime"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - long currentTime = playerState.playerController.get().getCurrentTime(); + long currentTime = playerState.mediaController.get().getCurrentPosition(); ret.put("result", true); ret.put("value", currentTime == C.TIME_UNSET ? 0 : (currentTime / 1000)); } catch (Error err) { @@ -109,10 +131,14 @@ public void setCurrentTime(PluginCall call, String playerId, long time) { JSObject ret = new JSObject(); ret.put("method", "setCurrentTime"); try { - MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - long updatedTime = playerState.playerController.get().setCurrentTime(time); + MediaController controller = MediaPlayerStateProvider.getState(playerId).mediaController.get(); + + long duration = controller.getDuration(); + long currentTime = controller.getCurrentPosition(); + long seekPosition = currentTime == C.TIME_UNSET ? 0 : Math.min(Math.max(0, time * 1000), duration == C.TIME_UNSET ? 0 : duration); + controller.seekTo(seekPosition); ret.put("result", true); - ret.put("value", updatedTime); + ret.put("value", seekPosition); } catch (Error err) { ret.put("result", false); ret.put("message", "Player not found"); @@ -126,7 +152,7 @@ public void isPlaying(PluginCall call, String playerId) { try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); ret.put("result", true); - ret.put("value", playerState.playerController.get().isPlaying()); + ret.put("value", playerState.mediaController.get().isPlaying()); } catch (Error err) { ret.put("result", false); ret.put("message", "Player not found"); @@ -140,7 +166,7 @@ public void isMuted(PluginCall call, String playerId) { try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); ret.put("result", true); - ret.put("value", playerState.playerController.get().isMuted()); + ret.put("value", playerState.mediaController.get().getVolume() == 0); } catch (Error err) { ret.put("result", false); ret.put("message", "Player not found"); @@ -153,7 +179,7 @@ public void mute(PluginCall call, String playerId) { ret.put("method", "mute"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().mute(); + playerState.mediaController.get().setVolume(0); ret.put("result", true); ret.put("value", true); } catch (Error err) { @@ -169,7 +195,7 @@ public void getVolume(PluginCall call, String playerId) { try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); ret.put("result", true); - ret.put("value", playerState.playerController.get().getVolume()); + ret.put("value", playerState.mediaController.get().getVolume()); } catch (Error err) { ret.put("result", false); ret.put("message", "Player not found"); @@ -182,7 +208,7 @@ public void setVolume(PluginCall call, String playerId, Double volume) { ret.put("method", "setVolume"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().setVolume(volume.floatValue()); + playerState.mediaController.get().setVolume(volume.floatValue()); ret.put("result", true); ret.put("value", volume); } catch (Error err) { @@ -198,7 +224,7 @@ public void getRate(PluginCall call, String playerId) { try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); ret.put("result", true); - ret.put("value", playerState.playerController.get().getRate()); + ret.put("value", playerState.mediaController.get().getPlaybackParameters().speed); } catch (Error err) { ret.put("result", false); ret.put("message", "Player not found"); @@ -211,7 +237,7 @@ public void setRate(PluginCall call, String playerId, Double rate) { ret.put("method", "setRate"); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().setRate(rate.floatValue()); + playerState.mediaController.get().setPlaybackSpeed(rate.floatValue()); ret.put("result", true); ret.put("value", rate); } catch (Error err) { @@ -225,10 +251,9 @@ public void remove(PluginCall call, String playerId) { JSObject ret = new JSObject(); ret.put("method", "remove"); try { - MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); + MediaPlayerState state = MediaPlayerStateProvider.getState(playerId); + state.mediaController.get().stop(); Fragment playerFragment = _currentActivity.getSupportFragmentManager().findFragmentByTag(playerId); - playerState.playerController.get().destroy(); - MediaPlayerStateProvider.clearState(playerId); if (playerFragment != null) { _currentActivity.getSupportFragmentManager().beginTransaction().remove(playerFragment).commit(); } @@ -248,10 +273,9 @@ public void removeAll(PluginCall call) { _currentActivity.getSupportFragmentManager().beginTransaction().remove(fragment).commit(); try { MediaPlayerState playerState = MediaPlayerStateProvider.getState(playerId); - playerState.playerController.get().destroy(); + playerState.mediaController.get().stop(); } catch (Error ignored) { } - MediaPlayerStateProvider.clearState(playerId); MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_REMOVED).build()); }); JSObject ret = new JSObject(); @@ -270,14 +294,14 @@ private String getFinalPath(String url) { return url; } else if (url.startsWith("application") || url.contains("_capacitor_file_")) { String filesDir = _currentActivity.getFilesDir() + "/"; - path = filesDir + url.substring(url.lastIndexOf("files/") + 6); + path = filesDir + url.substring(url.lastIndexOf("files/") + "files/".length()); File file = new File(path); if (!file.exists()) { Log.e("Media Player", "File not found"); path = null; } - } else if (url.contains("assets")) { - path = "file:///android_asset/" + url; + } else if (url.contains("public/assets")) { + path = "/android_asset/" + url; } else if (url.startsWith("http")) { path = url; } diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerContainer.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerContainer.java index 17a24d8..260916c 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerContainer.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerContainer.java @@ -5,72 +5,74 @@ import android.app.PictureInPictureParams; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Rect; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Rational; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaController; +import androidx.media3.ui.CaptionStyleCompat; +import androidx.media3.ui.PlayerView; +import androidx.media3.ui.SubtitleView; +import androidx.mediarouter.app.MediaRouteButton; + +import com.google.android.gms.cast.framework.CastButtonFactory; -import dev.eduardoroth.mediaplayer.MediaPlayerControllerView.MEDIA_PLAYER_VIEW_TYPE; import dev.eduardoroth.mediaplayer.models.AndroidOptions; import dev.eduardoroth.mediaplayer.models.ExtraOptions; -import dev.eduardoroth.mediaplayer.models.MediaItem; import dev.eduardoroth.mediaplayer.models.MediaPlayerNotification; import dev.eduardoroth.mediaplayer.state.MediaPlayerState; import dev.eduardoroth.mediaplayer.state.MediaPlayerStateProvider; -@UnstableApi public class MediaPlayerContainer extends Fragment { - private final AndroidOptions _android; - private final ExtraOptions _extra; - private final MediaPlayerState _mediaPlayerState; - private MediaPlayerController _playerController; - private MediaPlayerControllerView _playerControllerEmbeddedView; - private MediaPlayerControllerView _playerControllerFullscreenView; - private final String _url; + private AndroidOptions _android; + private ExtraOptions _extra; + private MediaPlayerState _mediaPlayerState; + private MediaController _playerController; private final String _playerId; private final Rect _sourceRectHint = new Rect(); + private PlayerView _embeddedPlayerView; + private PlayerView _fullscreenPlayerView; + private FrameLayout _embeddedView; + private FrameLayout _fullscreenView; - public MediaPlayerContainer(AppCompatActivity activity, String url, String playerId, AndroidOptions android, ExtraOptions extra) { - _android = android; - _extra = extra; + public MediaPlayerContainer(MediaController playerController, String playerId) { _playerId = playerId; - _url = url; - _mediaPlayerState = MediaPlayerStateProvider.getState(playerId, activity); - - _mediaPlayerState.androidOptions.set(android); - _mediaPlayerState.extraOptions.set(extra); + _playerController = playerController; } @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - _mediaPlayerState.canUsePiP.set(_android.enablePiP && requireContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)); - _mediaPlayerState.isPlayerReady.observe(state -> { - if (state) { - view.findViewById(R.id.MediaPlayerEmbeddedLoading).setVisibility(View.GONE); - } - }); - + public void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + _mediaPlayerState = MediaPlayerStateProvider.getState(_playerId); + _mediaPlayerState.mediaController.set(_playerController); + _android = _mediaPlayerState.androidOptions.get(); + _extra = _mediaPlayerState.extraOptions.get(); requireActivity().addOnPictureInPictureModeChangedListener(state -> { if (getLifecycle().getCurrentState() == Lifecycle.State.CREATED) { _mediaPlayerState.fullscreenState.set(UI_STATE.WILL_EXIT); _mediaPlayerState.pipState.set(UI_STATE.WILL_EXIT); if (!_android.enableBackgroundPlay) { - _playerController.getActivePlayer().pause(); + _playerController.pause(); } } else if (getLifecycle().getCurrentState() == Lifecycle.State.STARTED) { if (state.isInPictureInPictureMode()) { @@ -84,13 +86,28 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { } }); + } + @OptIn(markerClass = UnstableApi.class) + @Override + public void onViewCreated(@NonNull View view, Bundle savedBundleInstance) { + super.onViewCreated(view, savedBundleInstance); + + _mediaPlayerState.isPlayerReady.observe(state -> { + if (state) { + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedLoading).setVisibility(View.GONE); + } else { + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedLoading).setVisibility(View.VISIBLE); + } + }); _mediaPlayerState.pipState.observe(state -> { switch (state) { - case ACTIVE -> MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PIP).addData("isInPictureInPicture", true).build()); - case INACTIVE -> MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PIP).addData("isInPictureInPicture", false).build()); + case ACTIVE -> + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PIP).addData("isInPictureInPicture", true).build()); + case INACTIVE -> + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PIP).addData("isInPictureInPicture", false).build()); case WILL_ENTER -> { - view.findViewById(R.id.MediaPlayerEmbeddedPiP).setVisibility(View.VISIBLE); + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedPiP).setVisibility(View.VISIBLE); PictureInPictureParams.Builder pictureInPictureParams = new PictureInPictureParams.Builder().setSourceRectHint(_mediaPlayerState.sourceRectHint.get()).setAspectRatio(new Rational(_android.width, _android.height)); @@ -107,27 +124,30 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { _mediaPlayerState.pipState.set(UI_STATE.ACTIVE); } case WILL_EXIT -> { - view.findViewById(R.id.MediaPlayerEmbeddedPiP).setVisibility(View.GONE); + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedPiP).setVisibility(View.GONE); _mediaPlayerState.pipState.set(UI_STATE.INACTIVE); } } }); - - int defaultUiVisibility = requireActivity().getWindow().getDecorView().getSystemUiVisibility(); + View decorView = requireActivity().getWindow().getDecorView(); + int defaultUiVisibility = decorView.getSystemUiVisibility(); int fullscreenUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; ActionBar actionBar = getSupportActionBar(); _mediaPlayerState.fullscreenState.observe(state -> { switch (state) { - case ACTIVE -> MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_FULLSCREEN).addData("isInFullScreen", true).build()); - case INACTIVE -> MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_FULLSCREEN).addData("isInFullScreen", false).build()); + case ACTIVE -> + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_FULLSCREEN).addData("isInFullScreen", true).build()); + case INACTIVE -> + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_FULLSCREEN).addData("isInFullScreen", false).build()); case WILL_ENTER -> { - view.findViewById(R.id.MediaPlayerFullscreenContainer).getGlobalVisibleRect(_sourceRectHint); + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedContainer).setVisibility(View.GONE); + _fullscreenView.findViewById(R.id.MediaPlayerFullscreenContainer).setVisibility(View.VISIBLE); + _fullscreenView.findViewById(R.id.MediaPlayerFullscreenContainer).getGlobalVisibleRect(_sourceRectHint); _mediaPlayerState.sourceRectHint.set(_sourceRectHint); - getChildFragmentManager().beginTransaction().detach(_playerControllerEmbeddedView).add(R.id.MediaPlayerFullscreenContainer, _playerControllerFullscreenView, "fullscreen").commit(); - view.findViewById(R.id.MediaPlayerFullscreenContainer).setVisibility(View.VISIBLE); + PlayerView.switchTargetView(_playerController, _embeddedPlayerView, _fullscreenPlayerView); - requireActivity().getWindow().getDecorView().setSystemUiVisibility(fullscreenUiVisibility); + decorView.setSystemUiVisibility(fullscreenUiVisibility); if (actionBar != null) { actionBar.hide(); @@ -136,13 +156,14 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { _mediaPlayerState.fullscreenState.set(UI_STATE.ACTIVE); } case WILL_EXIT -> { - view.findViewById(R.id.MediaPlayerEmbeddedContainer).getGlobalVisibleRect(_sourceRectHint); + _fullscreenView.findViewById(R.id.MediaPlayerFullscreenContainer).setVisibility(View.GONE); + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedContainer).setVisibility(View.VISIBLE); + _embeddedView.findViewById(R.id.MediaPlayerEmbeddedContainer).getGlobalVisibleRect(_sourceRectHint); _mediaPlayerState.sourceRectHint.set(_sourceRectHint); - getChildFragmentManager().beginTransaction().remove(_playerControllerFullscreenView).attach(_playerControllerEmbeddedView).commit(); - view.findViewById(R.id.MediaPlayerFullscreenContainer).setVisibility(View.GONE); + PlayerView.switchTargetView(_playerController, _fullscreenPlayerView, _embeddedPlayerView); - requireActivity().getWindow().getDecorView().setSystemUiVisibility(defaultUiVisibility); + decorView.setSystemUiVisibility(defaultUiVisibility); if (actionBar != null) { actionBar.show(); @@ -163,14 +184,8 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { } }); - getChildFragmentManager().beginTransaction().add(R.id.MediaPlayerEmbeddedPlayer, _playerControllerEmbeddedView, "embedded").commit(); - - view.findViewById(R.id.MediaPlayerEmbeddedContainer).getGlobalVisibleRect(_sourceRectHint); - _mediaPlayerState.sourceRectHint.set(_sourceRectHint); - - if (_android.openInFullscreen) { - _mediaPlayerState.fullscreenState.set(UI_STATE.WILL_ENTER); - } + _mediaPlayerState.canUsePiP.set(_android.enablePiP && requireContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)); + _mediaPlayerState.fullscreenState.set(_android.openInFullscreen ? UI_STATE.WILL_ENTER : UI_STATE.WILL_EXIT); } @Override @@ -178,28 +193,133 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, super.onCreateView(inflater, container, savedInstanceState); View containerView = inflater.inflate(R.layout.media_player_container, container, false); - _playerController = new MediaPlayerController(requireContext(), _playerId, _extra); - _playerController.addMediaItem(new MediaItem(Uri.parse(_url), _extra)); - - _mediaPlayerState.playerController.set(_playerController); - - _playerControllerEmbeddedView = new MediaPlayerControllerView(_playerId); - _playerControllerFullscreenView = new MediaPlayerControllerView(_playerId); + _fullscreenView = containerView.findViewById(R.id.MediaPlayerFullscreenContainer); + _embeddedView = containerView.findViewById(R.id.MediaPlayerEmbeddedContainer); - View embeddedView = containerView.findViewById(R.id.MediaPlayerEmbeddedContainer); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(_android.width, _android.height); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(_android.width, _android.height, Gravity.FILL); params.topMargin = _android.top; params.setMarginStart(_android.start); - embeddedView.setLayoutParams(params); + _embeddedView.setLayoutParams(params); + + _embeddedPlayerView = createPlayerView(inflater, _embeddedView); + _fullscreenPlayerView = createPlayerView(inflater, _fullscreenView); + + addPlayerViewListeners(_embeddedPlayerView); + addPlayerViewListeners(_fullscreenPlayerView); + + _embeddedPlayerView.setPlayer(_playerController); return containerView; } - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; - _mediaPlayerState.landscapeState.set(isLandscape ? UI_STATE.ACTIVE : UI_STATE.INACTIVE); + @OptIn(markerClass = UnstableApi.class) + private PlayerView createPlayerView(@NonNull LayoutInflater inflater, View container) { + View videoView = inflater.inflate(R.layout.media_player_controller_view, (ViewGroup) container, true); + + PlayerView _playerView = videoView.findViewById(R.id.MediaPlayerControllerView); + + _playerView.findViewById(androidx.media3.ui.R.id.exo_repeat_toggle).setVisibility(View.GONE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen).setVisibility(View.GONE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_minimal_fullscreen).setVisibility(View.GONE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_extra_controls_scroll_view).setVisibility(View.VISIBLE); + + _playerView.findViewById(androidx.media3.ui.R.id.exo_bottom_bar).setVisibility(View.VISIBLE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_rew_with_amount).setVisibility(View.VISIBLE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_ffwd_with_amount).setVisibility(View.VISIBLE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_next).setVisibility(View.GONE); + _playerView.findViewById(androidx.media3.ui.R.id.exo_prev).setVisibility(View.GONE); + + LinearLayout basicControls = _playerView.findViewById(androidx.media3.ui.R.id.exo_basic_controls); + View extraControls = inflater.inflate(R.layout.media_player_controller_view_extra_buttons, basicControls, true); + + MediaRouteButton _castButton = extraControls.findViewById(R.id.cast_button); + if (_android.enableChromecast) { + CastButtonFactory.setUpMediaRouteButton(requireContext(), _castButton); + } + + ImageButton pipButton = extraControls.findViewById(R.id.pip_button); + if (_mediaPlayerState.canUsePiP.get()) { + pipButton.setVisibility(View.VISIBLE); + pipButton.setOnClickListener(view -> _mediaPlayerState.pipState.set(MediaPlayerState.UI_STATE.WILL_ENTER)); + } + + ImageButton _fullscreenToggle = extraControls.findViewById(R.id.toggle_fullscreen); + _fullscreenToggle.setOnClickListener(view -> { + switch (_mediaPlayerState.fullscreenState.get()) { + case ACTIVE -> + _mediaPlayerState.fullscreenState.set(MediaPlayerState.UI_STATE.WILL_EXIT); + case INACTIVE -> + _mediaPlayerState.fullscreenState.set(MediaPlayerState.UI_STATE.WILL_ENTER); + } + }); + + _playerView.setUseController(_extra.showControls); + + SubtitleView subtitleView = _playerView.findViewById(androidx.media3.ui.R.id.exo_subtitles); + + if (subtitleView != null && _extra.subtitles != null) { + subtitleView.setStyle(new CaptionStyleCompat(_extra.subtitles.settings.foregroundColor, _extra.subtitles.settings.backgroundColor, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_NONE, Color.WHITE, null)); + subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, _extra.subtitles.settings.fontSize.floatValue()); + } + + _playerView.setOnKeyListener((eventContainer, keyCode, keyEvent) -> { + if (_playerController != null && keyEvent.getAction() == KeyEvent.ACTION_UP) { + long duration = _playerController.getDuration(); + long videoPosition = _playerController.getCurrentPosition(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (videoPosition < duration - MediaPlayer.VIDEO_STEP) { + _playerController.seekTo(videoPosition + MediaPlayer.VIDEO_STEP); + } + return true; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (videoPosition - MediaPlayer.VIDEO_STEP > 0) { + _playerController.seekTo(videoPosition - MediaPlayer.VIDEO_STEP); + } else { + _playerController.seekTo(0); + } + return true; + case KeyEvent.KEYCODE_DPAD_CENTER: + if (_playerController.isPlaying()) { + _playerController.pause(); + } else { + _playerController.play(); + } + return true; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + if (videoPosition < duration - (MediaPlayer.VIDEO_STEP * 2)) { + _playerController.seekTo(videoPosition + (MediaPlayer.VIDEO_STEP * 2)); + } + return true; + case KeyEvent.KEYCODE_MEDIA_REWIND: + if (videoPosition - (MediaPlayer.VIDEO_STEP * 2) > 0) { + _playerController.seekTo(videoPosition - (MediaPlayer.VIDEO_STEP * 2)); + } else { + _playerController.seekTo(0); + } + return true; + } + } + return false; + }); + + return _playerView; + } + + private void addPlayerViewListeners(PlayerView playerView) { + _mediaPlayerState.fullscreenState.observe(state -> { + ((ImageButton) playerView.findViewById(R.id.toggle_fullscreen)).setImageResource(state == UI_STATE.ACTIVE ? R.drawable.ic_fullscreen_exit : R.drawable.ic_fullscreen_enter); + }); + _mediaPlayerState.pipState.observe(state -> { + playerView.setUseController(state != UI_STATE.ACTIVE && _extra.showControls); + }); + _mediaPlayerState.canCast.observe(isCastAvailable -> { + playerView.findViewById(R.id.cast_button).setVisibility(isCastAvailable ? View.VISIBLE : View.GONE); + playerView.findViewById(R.id.cast_button).setEnabled(isCastAvailable); + }); + _mediaPlayerState.showSubtitles.observe(showSubtitles -> + playerView.findViewById(androidx.media3.ui.R.id.exo_subtitle).setVisibility(showSubtitles ? View.VISIBLE : View.GONE) + ); } @Nullable @@ -211,6 +331,13 @@ private ActionBar getSupportActionBar() { return actionBar; } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; + _mediaPlayerState.landscapeState.set(isLandscape ? UI_STATE.ACTIVE : UI_STATE.INACTIVE); + } + @Override public void onPause() { if (_android.enableBackgroundPlay) { @@ -233,10 +360,4 @@ public void onResume() { super.onResume(); } - @Override - public void onDestroy() { - _playerController.destroy(); - super.onDestroy(); - } - } diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerController.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerController.java deleted file mode 100644 index c0de202..0000000 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerController.java +++ /dev/null @@ -1,386 +0,0 @@ -package dev.eduardoroth.mediaplayer; - -import static android.app.Notification.BADGE_ICON_LARGE; - -import android.app.Notification; -import android.app.NotificationManager; -import android.content.Context; -import android.media.MediaCodec; -import android.os.Handler; - -import androidx.annotation.NonNull; -import androidx.annotation.OptIn; -import androidx.media3.cast.CastPlayer; -import androidx.media3.cast.SessionAvailabilityListener; -import androidx.media3.common.AudioAttributes; -import androidx.media3.common.C; -import androidx.media3.common.Player; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.exoplayer.DefaultLoadControl; -import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; -import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; -import androidx.media3.session.MediaSession; -import androidx.media3.ui.PlayerNotificationManager; -import androidx.mediarouter.media.MediaControlIntent; -import androidx.mediarouter.media.MediaRouteSelector; -import androidx.mediarouter.media.MediaRouter; - -import com.google.android.gms.cast.framework.CastContext; -import com.google.android.gms.cast.framework.CastState; -import com.google.common.util.concurrent.MoreExecutors; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import dev.eduardoroth.mediaplayer.MediaPlayerNotificationCenter.NOTIFICATION_TYPE; -import dev.eduardoroth.mediaplayer.models.ExtraOptions; -import dev.eduardoroth.mediaplayer.models.MediaItem; -import dev.eduardoroth.mediaplayer.models.MediaPlayerNotification; -import dev.eduardoroth.mediaplayer.state.MediaPlayerState; -import dev.eduardoroth.mediaplayer.state.MediaPlayerState.UI_STATE; -import dev.eduardoroth.mediaplayer.state.MediaPlayerStateProvider; - -@UnstableApi -public class MediaPlayerController { - private final int _layoutId; - private final String _playerId; - private final ExtraOptions _extra; - private final Map _mediaItems = new HashMap<>(); - - private final Context _context; - private CastContext _castContext = null; - private final ExoPlayer _exoPlayer; - private final CastPlayer _castPlayer; - private final MediaPlayerState _mediaPlayerState; - private MediaSession _exoPlayerMediaSession; - private MediaSession _castPlayerMediaSession; - private PlayerNotificationManager _playerNotificationManager; - - private final Handler _handlerCurrentTime = new Handler(); - - private Player _activePlayer; - - public MediaPlayerController(Context context, String playerId, ExtraOptions extra) { - _layoutId = playerId.chars().reduce(0, Integer::sum); - _playerId = playerId; - _extra = extra; - _context = context; - - _mediaPlayerState = MediaPlayerStateProvider.getState(_playerId); - - createPlayerNotificationManager(); - - _exoPlayer = createExoPlayer(); - _castPlayer = createCastPlayer(); - - setActivePlayer(); - - _mediaPlayerState.castingState.observe(state -> { - if (state == UI_STATE.WILL_ENTER) { - setActivePlayer(true); - } - if (state == UI_STATE.WILL_EXIT) { - setActivePlayer(false); - } - }); - } - - public Player getActivePlayer() { - return _activePlayer; - } - - public void play() { - _mediaPlayerState.showSubtitles.set(shouldShowSubtitles()); - _activePlayer.play(); - } - - public void pause() { - _activePlayer.pause(); - } - - public void stop() { - _activePlayer.stop(); - } - - public long getDuration() { - return _activePlayer.getDuration(); - } - - public long getCurrentTime() { - return _activePlayer.getCurrentPosition(); - } - - public long setCurrentTime(long time) { - long duration = getDuration(); - long currentTime = getCurrentTime(); - long seekPosition = currentTime == C.TIME_UNSET ? 0 : Math.min(Math.max(0, time * 1000), duration == C.TIME_UNSET ? 0 : duration); - _activePlayer.seekTo(seekPosition); - return seekPosition; - } - - public boolean isPlaying() { - return _activePlayer.isPlaying(); - } - - public boolean isMuted() { - return _activePlayer.getVolume() == 0; - } - - public void mute() { - _activePlayer.setVolume(0); - } - - public float getVolume() { - return _activePlayer.getVolume(); - } - - public void setVolume(float volume) { - _activePlayer.setVolume(volume); - } - - public float getRate() { - return _activePlayer.getPlaybackParameters().speed; - } - - public void setRate(float rate) { - _activePlayer.setPlaybackSpeed(rate); - } - - public void addMediaItem(MediaItem item) { - _mediaItems.put(item.getMediaItem().mediaId, item); - _exoPlayer.addMediaItem(item.getMediaItem()); - if (_castPlayer != null) { - _castPlayer.addMediaItem(item.getMediaItem()); - } - } - - public void addMediaItems(ArrayList items) { - items.forEach(this::addMediaItem); - } - - public boolean shouldShowSubtitles() { - androidx.media3.common.MediaItem current = _exoPlayer.getCurrentMediaItem(); - if (current != null) { - MediaItem mediaItem = _mediaItems.get(current.mediaId); - if (mediaItem != null) { - return mediaItem.hasSubtitles(); - } - } - return false; - } - - public void destroy() { - _exoPlayer.pause(); - if (_exoPlayerMediaSession != null) { - _exoPlayerMediaSession.release(); - } - _exoPlayer.release(); - - if (_mediaPlayerState.canCast.get() && _castPlayer != null) { - _castPlayer.pause(); - _castPlayer.release(); - _castPlayerMediaSession.release(); - _castPlayerMediaSession = null; - } - - _playerNotificationManager.setPlayer(null); - _playerNotificationManager.invalidate(); - } - - private void setActivePlayer() { - setActivePlayer(false); - } - - private void setActivePlayer(boolean isCasting) { - setActivePlayer(isCasting ? _castPlayer : _exoPlayer, isCasting); - } - - private void setActivePlayer(Player playerToChange, boolean isCasting) { - if (_activePlayer == playerToChange) { - return; - } - long currentTime = _mediaPlayerState.getCurrentTime.get(); - if (_activePlayer != null) { - _activePlayer.stop(); - } - _activePlayer = playerToChange; - _activePlayer.seekTo(currentTime); - _playerNotificationManager.setPlayer(_activePlayer); - _playerNotificationManager.setMediaSessionToken(isCasting ? _castPlayerMediaSession.getPlatformToken() : _exoPlayerMediaSession.getPlatformToken()); - _activePlayer.prepare(); - _mediaPlayerState.castingState.set(isCasting ? UI_STATE.ACTIVE : UI_STATE.INACTIVE); - } - - private void createPlayerNotificationManager() { - _playerNotificationManager = new PlayerNotificationManager - .Builder(_context, _layoutId, _context.getString(R.string.channel_id)) - .setChannelNameResourceId(R.string.channel_name) - .setChannelDescriptionResourceId(R.string.channel_description) - .setChannelImportance(NotificationManager.IMPORTANCE_LOW) - .setNotificationListener(new PlayerNotificationManager.NotificationListener() { - @Override - public void onNotificationCancelled(int notificationId, boolean dismissedByUser) { - PlayerNotificationManager.NotificationListener.super.onNotificationCancelled(notificationId, dismissedByUser); - } - - @Override - public void onNotificationPosted(int notificationId, @NonNull Notification notification, boolean ongoing) { - PlayerNotificationManager.NotificationListener.super.onNotificationPosted(notificationId, notification, ongoing); - } - }).build(); - _playerNotificationManager.setBadgeIconType(BADGE_ICON_LARGE); - _playerNotificationManager.setShowPlayButtonIfPlaybackIsSuppressed(true); - _playerNotificationManager.setUseChronometer(true); - _playerNotificationManager.setUseFastForwardAction(true); - _playerNotificationManager.setUseFastForwardActionInCompactView(true); - _playerNotificationManager.setUseNextAction(false); - _playerNotificationManager.setUseNextActionInCompactView(false); - _playerNotificationManager.setUsePlayPauseActions(true); - _playerNotificationManager.setUsePreviousAction(false); - _playerNotificationManager.setUsePreviousActionInCompactView(false); - _playerNotificationManager.setUseRewindAction(true); - _playerNotificationManager.setUseRewindActionInCompactView(true); - _playerNotificationManager.setUseStopAction(true); - } - - @OptIn(markerClass = UnstableApi.class) - private ExoPlayer createExoPlayer() { - ExoPlayer exoPlayer = new ExoPlayer - .Builder(_context) - .setName(_playerId) - .setTrackSelector(new DefaultTrackSelector(_context, new AdaptiveTrackSelection.Factory())) - .setLoadControl(new DefaultLoadControl()) - .setBandwidthMeter(new DefaultBandwidthMeter.Builder(_context).build()) - .setDeviceVolumeControlEnabled(true) - .setSeekBackIncrementMs(MediaPlayer.VIDEO_STEP) - .setSeekForwardIncrementMs(MediaPlayer.VIDEO_STEP) - .setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT) - .build(); - - exoPlayer.setRepeatMode(_extra.loopOnEnd ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF); - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) - .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM) - .setUsage(C.USAGE_MEDIA) - .build(), true - ); - - exoPlayer.addListener(new Player.Listener() { - @Override - public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { - Player.Listener.super.onPositionDiscontinuity(oldPosition, newPosition, reason); - if (reason == Player.DISCONTINUITY_REASON_SEEK) { - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_SEEK).addData("previousTime", oldPosition.positionMs / 1000).addData("newTime", newPosition.positionMs / 1000).build()); - } - } - - @Override - public void onIsPlayingChanged(boolean isPlaying) { - if (isPlaying) { - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_PLAY).build()); - _handlerCurrentTime.postDelayed(new Runnable() { - @Override - public void run() { - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_TIME_UPDATED).addData("currentTime", _exoPlayer.getCurrentPosition() / 1000).build()); - _handlerCurrentTime.postDelayed(this, 100); - } - }, 100); - } else { - _handlerCurrentTime.removeCallbacksAndMessages(null); - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_PAUSE).build()); - } - } - - @Override - public void onPlaybackStateChanged(int playbackState) { - Player.Listener.super.onPlaybackStateChanged(playbackState); - switch (playbackState) { - case Player.STATE_BUFFERING: - case Player.STATE_IDLE: - break; - case Player.STATE_ENDED: - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_ENDED).build()); - break; - case Player.STATE_READY: - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_READY).build()); - if (_extra.autoPlayWhenReady) { - exoPlayer.play(); - } - break; - } - } - }); - - _exoPlayerMediaSession = new MediaSession.Builder(_context, exoPlayer).setPeriodicPositionUpdateEnabled(true).build(); - - _exoPlayerMediaSession.setPlayer(exoPlayer); - exoPlayer.prepare(); - - return exoPlayer; - } - - @OptIn(markerClass = UnstableApi.class) - private CastPlayer createCastPlayer() { - try { - _castContext = CastContext.getSharedInstance(_context, MoreExecutors.directExecutor()).getResult(); - } catch (RuntimeException ignored) { - } - if (_castContext == null) { - return null; - } - - CastPlayer castPlayer = new CastPlayer(_castContext); - - MediaRouter mRouter = MediaRouter.getInstance(_context); - MediaRouteSelector mSelector = new MediaRouteSelector.Builder().addControlCategories(Arrays.asList(MediaControlIntent.CATEGORY_LIVE_AUDIO, MediaControlIntent.CATEGORY_LIVE_VIDEO)).build(); - - mRouter.addCallback(mSelector, new MediaRouter.Callback() { - }, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); - - _castContext.addCastStateListener(state -> _mediaPlayerState.canCast.set(state != CastState.NO_DEVICES_AVAILABLE)); - - castPlayer.addListener(new CastPlayer.Listener() { - @Override - public void onPlaybackStateChanged(int playbackState) { - CastPlayer.Listener.super.onPlaybackStateChanged(playbackState); - if (playbackState == CastPlayer.STATE_READY) { - if (castPlayer.isPlaying()) { - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_PLAY).build()); - } else { - MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(_playerId, NOTIFICATION_TYPE.MEDIA_PLAYER_PAUSE).build()); - } - } - } - }); - - castPlayer.setSessionAvailabilityListener(new SessionAvailabilityListener() { - @Override - public void onCastSessionAvailable() { - if (_mediaPlayerState != null) { - _mediaPlayerState.castingState.set(UI_STATE.WILL_ENTER); - } - } - - @Override - public void onCastSessionUnavailable() { - if (_mediaPlayerState != null) { - _mediaPlayerState.castingState.set(UI_STATE.WILL_EXIT); - } - } - }); - - _castPlayerMediaSession = new MediaSession.Builder(_context, castPlayer).setPeriodicPositionUpdateEnabled(true).build(); - - _castPlayerMediaSession.setPlayer(castPlayer); - - castPlayer.prepare(); - - return castPlayer; - } - -} diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerControllerView.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerControllerView.java deleted file mode 100644 index c4801e9..0000000 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerControllerView.java +++ /dev/null @@ -1,188 +0,0 @@ -package dev.eduardoroth.mediaplayer; - -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.util.TypedValue; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.media3.common.Player; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.ui.CaptionStyleCompat; -import androidx.media3.ui.PlayerView; -import androidx.mediarouter.app.MediaRouteButton; - -import com.google.android.gms.cast.framework.CastButtonFactory; - -import java.io.FileNotFoundException; - -import dev.eduardoroth.mediaplayer.models.AndroidOptions; -import dev.eduardoroth.mediaplayer.models.ExtraOptions; -import dev.eduardoroth.mediaplayer.state.MediaPlayerState; -import dev.eduardoroth.mediaplayer.state.MediaPlayerStateProvider; - -@UnstableApi -public class MediaPlayerControllerView extends Fragment { - public enum MEDIA_PLAYER_VIEW_TYPE { - FULLSCREEN, EMBEDDED, - } - - private final MediaPlayerState _mediaPlayerState; - private final MediaPlayerController _playerController; - private final AndroidOptions _android; - private final ExtraOptions _extra; - private PlayerView playerView; - private MediaRouteButton castButton; - private ImageButton fullscreenToggle; - private Drawable artwork; - - public MediaPlayerControllerView(String playerId) { - _mediaPlayerState = MediaPlayerStateProvider.getState(playerId); - _playerController = _mediaPlayerState.playerController.get(); - _android = _mediaPlayerState.androidOptions.get(); - _extra = _mediaPlayerState.extraOptions.get(); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - _mediaPlayerState.fullscreenState.observe(state -> { - switch (state) { - case ACTIVE -> fullscreenToggle.setImageResource(R.drawable.ic_fullscreen_exit); - case INACTIVE -> fullscreenToggle.setImageResource(R.drawable.ic_fullscreen_enter); - } - }); - _mediaPlayerState.pipState.observe(state -> { - switch (state) { - case ACTIVE -> playerView.setUseController(false); - case INACTIVE -> playerView.setUseController(_extra.showControls); - - } - }); - _mediaPlayerState.canCast.observe(isCastAvailable -> { - castButton.setVisibility(isCastAvailable ? View.VISIBLE : View.GONE); - castButton.setEnabled(isCastAvailable); - }); - _mediaPlayerState.showSubtitles.observe(showSubtitles -> playerView.setShowSubtitleButton(showSubtitles)); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View fragmentView = inflater.inflate(R.layout.media_player_controller_view, container, false); - - playerView = fragmentView.findViewById(R.id.MediaPlayerControllerView); - playerView.setFocusableInTouchMode(true); - - playerView.findViewById(androidx.media3.ui.R.id.exo_repeat_toggle).setVisibility(View.GONE); - playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen).setVisibility(View.GONE); - playerView.findViewById(androidx.media3.ui.R.id.exo_minimal_fullscreen).setVisibility(View.GONE); - playerView.findViewById(androidx.media3.ui.R.id.exo_extra_controls_scroll_view).setVisibility(View.VISIBLE); - - LinearLayout basicControls = playerView.findViewById(androidx.media3.ui.R.id.exo_basic_controls); - View extraControls = inflater.inflate(R.layout.media_player_controller_view_extra_buttons, basicControls, true); - - castButton = extraControls.findViewById(R.id.cast_button); - if (_android.enableChromecast) { - CastButtonFactory.setUpMediaRouteButton(requireContext(), castButton); - } - - ImageButton pipButton = extraControls.findViewById(R.id.pip_button); - if (_mediaPlayerState.canUsePiP.get()) { - pipButton.setVisibility(View.VISIBLE); - pipButton.setOnClickListener(view -> _mediaPlayerState.pipState.set(MediaPlayerState.UI_STATE.WILL_ENTER)); - } - - fullscreenToggle = extraControls.findViewById(R.id.toggle_fullscreen); - fullscreenToggle.setOnClickListener(view -> { - switch (_mediaPlayerState.fullscreenState.get()) { - case ACTIVE -> - _mediaPlayerState.fullscreenState.set(MediaPlayerState.UI_STATE.WILL_EXIT); - case INACTIVE -> - _mediaPlayerState.fullscreenState.set(MediaPlayerState.UI_STATE.WILL_ENTER); - } - }); - - playerView.setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS); - playerView.setControllerAutoShow(_extra.showControls); - playerView.setControllerHideOnTouch(true); - playerView.setControllerShowTimeoutMs(2500); - - if (artwork != null) { - try { - artwork = Drawable.createFromStream(requireContext().getContentResolver().openInputStream(Uri.parse(_extra.poster)), _extra.poster); - } catch (FileNotFoundException ignored) { - } - playerView.setDefaultArtwork(artwork); - playerView.setArtworkDisplayMode(PlayerView.ARTWORK_DISPLAY_MODE_FILL); - } - - playerView.setShowPreviousButton(false); - playerView.setShowNextButton(false); - playerView.setUseController(_extra.showControls); - playerView.setControllerAnimationEnabled(_extra.showControls); - playerView.setImageDisplayMode(PlayerView.IMAGE_DISPLAY_MODE_FIT); - playerView.setShowPlayButtonIfPlaybackIsSuppressed(true); - - if (playerView.getSubtitleView() != null && _extra.subtitles != null) { - playerView.getSubtitleView().setStyle(new CaptionStyleCompat(_extra.subtitles.settings.foregroundColor, _extra.subtitles.settings.backgroundColor, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_NONE, Color.WHITE, null)); - playerView.getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, _extra.subtitles.settings.fontSize.floatValue()); - } - - playerView.setOnKeyListener((eventContainer, keyCode, keyEvent) -> { - Player activePlayer = playerView.getPlayer(); - if (activePlayer != null && keyEvent.getAction() == KeyEvent.ACTION_UP) { - long duration = activePlayer.getDuration(); - long videoPosition = activePlayer.getCurrentPosition(); - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (videoPosition < duration - MediaPlayer.VIDEO_STEP) { - activePlayer.seekTo(videoPosition + MediaPlayer.VIDEO_STEP); - } - return true; - case KeyEvent.KEYCODE_DPAD_LEFT: - if (videoPosition - MediaPlayer.VIDEO_STEP > 0) { - activePlayer.seekTo(videoPosition - MediaPlayer.VIDEO_STEP); - } else { - activePlayer.seekTo(0); - } - return true; - case KeyEvent.KEYCODE_DPAD_CENTER: - if (activePlayer.isPlaying()) { - activePlayer.pause(); - } else { - activePlayer.play(); - } - return true; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - if (videoPosition < duration - (MediaPlayer.VIDEO_STEP * 2)) { - activePlayer.seekTo(videoPosition + (MediaPlayer.VIDEO_STEP * 2)); - } - return true; - case KeyEvent.KEYCODE_MEDIA_REWIND: - if (videoPosition - (MediaPlayer.VIDEO_STEP * 2) > 0) { - activePlayer.seekTo(videoPosition - (MediaPlayer.VIDEO_STEP * 2)); - } else { - activePlayer.seekTo(0); - } - return true; - } - } - return false; - }); - - playerView.setPlayer(_playerController.getActivePlayer()); - - _mediaPlayerState.isPlayerReady.set(true); - - return fragmentView; - } -} diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerPlugin.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerPlugin.java index 5cb10b6..821be2d 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerPlugin.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerPlugin.java @@ -7,9 +7,7 @@ import com.getcapacitor.annotation.CapacitorPlugin; import android.util.DisplayMetrics; - -import androidx.annotation.OptIn; -import androidx.media3.common.util.UnstableApi; +import android.util.Log; import org.json.JSONException; @@ -17,12 +15,10 @@ import dev.eduardoroth.mediaplayer.models.ExtraOptions; import dev.eduardoroth.mediaplayer.models.SubtitleOptions; -@UnstableApi @CapacitorPlugin(name = "MediaPlayer") public class MediaPlayerPlugin extends Plugin { private MediaPlayer implementation; - @OptIn(markerClass = UnstableApi.class) @Override public void load() { bridge.getActivity().getSupportFragmentManager(); @@ -31,7 +27,6 @@ public void load() { MediaPlayerNotificationCenter.listenNotifications(nextNotification -> notifyListeners(nextNotification.getEventName(), nextNotification.getData())); } - @OptIn(markerClass = UnstableApi.class) @PluginMethod public void create(final PluginCall call) { String playerId = call.getString("playerId"); @@ -64,10 +59,10 @@ public void create(final PluginCall call) { Integer paramWidth = androidOptions != null ? androidOptions.getInteger("width", null) : null; Integer paramHeight = androidOptions != null ? androidOptions.getInteger("height", null) : null; - int marginTop = paramTop == null ? 0 : (int) (paramTop * metrics.scaledDensity); - int marginStart = paramStart == null ? 0 : (int) (paramStart * metrics.scaledDensity); - int videoWidth = paramWidth == null ? (metrics.widthPixels - (marginStart * 2)) : (int) (paramWidth * metrics.scaledDensity); - int videoHeight = paramHeight == null ? (videoWidth * 9 / 16) : (int) (paramHeight * metrics.scaledDensity); + int marginTop = paramTop == null ? 0 : (int) (paramTop * metrics.density); + int marginStart = paramStart == null ? 0 : (int) (paramStart * metrics.density); + int videoWidth = paramWidth == null ? (metrics.widthPixels - (marginStart * 2)) : (int) (paramWidth * metrics.density); + int videoHeight = paramHeight == null ? (videoWidth * 9 / 16) : (int) (paramHeight * metrics.density); AndroidOptions android = new AndroidOptions( androidOptions == null || androidOptions.optBoolean("enableChromecast", true), @@ -76,6 +71,7 @@ public void create(final PluginCall call) { androidOptions != null && androidOptions.optBoolean("openInFullscreen", false), androidOptions != null && androidOptions.optBoolean("automaticallyEnterPiP", false), androidOptions == null || androidOptions.optBoolean("fullscreenOnLandscape", true), + androidOptions == null || androidOptions.optBoolean("stopOnTaskRemoved", false), marginTop, marginStart, videoWidth, @@ -170,7 +166,7 @@ public void getCurrentTime(final PluginCall call) { @PluginMethod public void setCurrentTime(final PluginCall call) { String playerId = call.getString("playerId"); - Long time = call.getLong("time"); + Double time = call.getDouble("time"); if (playerId == null) { JSObject ret = new JSObject(); ret.put("method", "setCurrentTime"); @@ -187,7 +183,7 @@ public void setCurrentTime(final PluginCall call) { call.resolve(ret); return; } - bridge.getActivity().runOnUiThread(() -> implementation.setCurrentTime(call, playerId, time)); + bridge.getActivity().runOnUiThread(() -> implementation.setCurrentTime(call, playerId, time.longValue())); } @PluginMethod diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerService.java b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerService.java new file mode 100644 index 0000000..46d71c1 --- /dev/null +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/MediaPlayerService.java @@ -0,0 +1,228 @@ +package dev.eduardoroth.mediaplayer; + +import static android.media.MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ServiceLifecycleDispatcher; +import androidx.media3.common.AudioAttributes; + +import static androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE; +import static androidx.media3.common.C.ALLOW_CAPTURE_BY_SYSTEM; +import static androidx.media3.common.C.USAGE_MEDIA; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; + +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultLoadControl; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; +import androidx.media3.session.MediaSession; +import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.MediaSessionService; + +import dev.eduardoroth.mediaplayer.models.AndroidOptions; +import dev.eduardoroth.mediaplayer.models.ExtraOptions; +import dev.eduardoroth.mediaplayer.models.MediaItem; +import dev.eduardoroth.mediaplayer.models.MediaPlayerNotification; +import dev.eduardoroth.mediaplayer.state.MediaPlayerState; +import dev.eduardoroth.mediaplayer.state.MediaPlayerStateProvider; + + +public class MediaPlayerService extends MediaSessionService implements LifecycleOwner { + public static long VIDEO_STEP = 10000; + + private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this); + + @OptIn(markerClass = UnstableApi.class) + @Override + public MediaSession onGetSession(@NonNull ControllerInfo controllerInfo) { + String playerId = controllerInfo.getConnectionHints().getString("playerId", "no-player-id"); + + MediaSession doesSessionExists = this.getSessions() + .stream() + .filter(s -> s.getId().equals(playerId)) + .findFirst() + .orElse(null); + + String videoUrl = controllerInfo.getConnectionHints().getString("videoUrl"); + AndroidOptions android = (AndroidOptions) controllerInfo.getConnectionHints().getSerializable("android"); + ExtraOptions extra = (ExtraOptions) controllerInfo.getConnectionHints().getSerializable("extra"); + assert android != null; + assert extra != null; + + mDispatcher.onServicePreSuperOnDestroy(); + mDispatcher.onServicePreSuperOnStart(); + + if (doesSessionExists != null) { + String currentPlayerId = doesSessionExists.getSessionExtras().getString("playerId"); + if (currentPlayerId != null && currentPlayerId.equalsIgnoreCase(playerId)) { + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_READY).build()); + } else { + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(currentPlayerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_ENDED).build()); + doesSessionExists.getPlayer().stop(); + doesSessionExists.getPlayer().release(); + doesSessionExists.setPlayer(createPlayer(playerId, videoUrl, android, extra)); + } + return doesSessionExists; + } + + MediaPlayerState mediaPlayerState = MediaPlayerStateProvider.createState(playerId, this); + mediaPlayerState.androidOptions.set(android); + mediaPlayerState.extraOptions.set(extra); + + Bundle sessionExtras = new Bundle(); + sessionExtras.putString("playerId", playerId); + sessionExtras.putString("videoUrl", videoUrl); + sessionExtras.putBoolean("stopOnTaskRemoved", android.stopOnTaskRemoved); + + MediaSession playerSession = new MediaSession + .Builder(this, createPlayer(playerId, videoUrl, android, extra)) + .setId(playerId) + .setPeriodicPositionUpdateEnabled(true) + .setSessionExtras(sessionExtras) + .build(); + + addSession(playerSession); + return playerSession; + } + + @OptIn(markerClass = UnstableApi.class) + private ExoPlayer createPlayer(String playerId, String videoUrl, AndroidOptions android, ExtraOptions extra) { + ExoPlayer exoPlayer = new ExoPlayer + .Builder(this) + .setName(playerId) + .setTrackSelector(new DefaultTrackSelector(this, new AdaptiveTrackSelection.Factory())) + .setLoadControl(new DefaultLoadControl()) + .setBandwidthMeter(new DefaultBandwidthMeter.Builder(this).build()) + .setDeviceVolumeControlEnabled(true) + .setSeekBackIncrementMs(VIDEO_STEP) + .setSeekForwardIncrementMs(VIDEO_STEP) + .setVideoScalingMode(VIDEO_SCALING_MODE_SCALE_TO_FIT) + .build(); + + exoPlayer.setAudioAttributes( + new AudioAttributes + .Builder() + .setContentType(AUDIO_CONTENT_TYPE_MOVIE) + .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM) + .setUsage(USAGE_MEDIA) + .build() + , true + ); + + exoPlayer.setRepeatMode(extra.loopOnEnd ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF); + + exoPlayer.setMediaItem(new MediaItem(Uri.parse(videoUrl), extra).getMediaItem()); + Handler handlerCurrentTime = new Handler(); + exoPlayer.addListener(new Player.Listener() { + @Override + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { + Player.Listener.super.onPositionDiscontinuity(oldPosition, newPosition, reason); + if (reason == Player.DISCONTINUITY_REASON_SEEK) { + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_SEEK).addData("previousTime", oldPosition.positionMs / 1000).addData("newTime", newPosition.positionMs / 1000).build()); + } + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + if (isPlaying) { + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PLAY).build()); + handlerCurrentTime.postDelayed(new Runnable() { + @Override + public void run() { + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_TIME_UPDATED).addData("currentTime", exoPlayer.getCurrentPosition() / 1000).build()); + handlerCurrentTime.postDelayed(this, 100); + } + }, 100); + } else { + handlerCurrentTime.removeCallbacksAndMessages(null); + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_PAUSE).build()); + } + } + + @Override + public void onPlaybackStateChanged(int playbackState) { + Player.Listener.super.onPlaybackStateChanged(playbackState); + switch (playbackState) { + case Player.STATE_BUFFERING: + case Player.STATE_IDLE: + break; + case Player.STATE_ENDED: + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_ENDED).build()); + break; + case Player.STATE_READY: + MediaPlayerNotificationCenter.post(MediaPlayerNotification.create(playerId, MediaPlayerNotificationCenter.NOTIFICATION_TYPE.MEDIA_PLAYER_READY).build()); + if (extra.autoPlayWhenReady) { + exoPlayer.play(); + } + break; + } + } + }); + + exoPlayer.prepare(); + return exoPlayer; + } + + @Override + public void onCreate() { + mDispatcher.onServicePreSuperOnCreate(); + super.onCreate(); + } + + @OptIn(markerClass = UnstableApi.class) + @Override + public void onTaskRemoved(@Nullable Intent rootIntent) { + super.onTaskRemoved(rootIntent); + getSessions().forEach(session -> { + Player player = session.getPlayer(); + if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 || player.getPlaybackState() == Player.STATE_ENDED) { + stopSelf(); + } + if(session.getSessionExtras().getBoolean("stopOnTaskRemoved")) { + player.pause(); + stopSelf(); + } + }); + } + + @Override + public IBinder onBind(Intent intent) { + mDispatcher.onServicePreSuperOnBind(); + return super.onBind(intent); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + mDispatcher.onServicePreSuperOnStart(); + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() { + mDispatcher.onServicePreSuperOnDestroy(); + super.onDestroy(); + getSessions().forEach(session -> { + session.getPlayer().release(); + session.release(); + removeSession(session); + }); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mDispatcher.getLifecycle(); + } +} \ No newline at end of file diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/models/AndroidOptions.java b/android/src/main/java/dev/eduardoroth/mediaplayer/models/AndroidOptions.java index a75f302..e5ae608 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/models/AndroidOptions.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/models/AndroidOptions.java @@ -9,18 +9,20 @@ public class AndroidOptions implements Serializable { public boolean openInFullscreen; public boolean automaticallyEnterPiP; public boolean fullscreenOnLandscape; + public boolean stopOnTaskRemoved; public int top; public int start; public int width; public int height; - public AndroidOptions(boolean enableChromecast, boolean enablePiP, boolean enableBackgroundPlay, boolean openInFullscreen, boolean automaticallyEnterPiP, boolean fullscreenOnLandscape, int top, int start, int width, int height) { + public AndroidOptions(boolean enableChromecast, boolean enablePiP, boolean enableBackgroundPlay, boolean openInFullscreen, boolean automaticallyEnterPiP, boolean fullscreenOnLandscape, boolean stopOnTaskRemoved, int top, int start, int width, int height) { this.enableChromecast = enableChromecast; this.enablePiP = enablePiP; this.enableBackgroundPlay = enableBackgroundPlay; this.openInFullscreen = openInFullscreen; this.automaticallyEnterPiP = automaticallyEnterPiP; this.fullscreenOnLandscape = fullscreenOnLandscape; + this.stopOnTaskRemoved = stopOnTaskRemoved; this.top = top; this.start = start; diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/models/MediaItem.java b/android/src/main/java/dev/eduardoroth/mediaplayer/models/MediaItem.java index 101da80..1d43c52 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/models/MediaItem.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/models/MediaItem.java @@ -1,5 +1,6 @@ package dev.eduardoroth.mediaplayer.models; +import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.media3.common.C; @@ -7,6 +8,8 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.MimeTypes; +import java.io.File; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -17,7 +20,11 @@ public class MediaItem { private boolean _hasSubtitles = false; public MediaItem(Uri url, ExtraOptions extra) { - MediaMetadata.Builder movieMetadataBuilder = new MediaMetadata.Builder().setTitle(extra.title).setSubtitle(extra.subtitle).setArtist(extra.artist).setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE); + MediaMetadata.Builder movieMetadataBuilder = new MediaMetadata.Builder() + .setTitle(extra.title) + .setSubtitle(extra.subtitle) + .setArtist(extra.artist) + .setMediaType(MediaMetadata.MEDIA_TYPE_VIDEO); if (extra.poster != null) { movieMetadataBuilder.setArtworkUri(Uri.parse(extra.poster)); } diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerState.java b/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerState.java index e0d6abd..1f1d96a 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerState.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerState.java @@ -4,13 +4,11 @@ import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModel; -import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaController; -import dev.eduardoroth.mediaplayer.MediaPlayerController; import dev.eduardoroth.mediaplayer.models.AndroidOptions; import dev.eduardoroth.mediaplayer.models.ExtraOptions; -@UnstableApi public class MediaPlayerState extends ViewModel { public enum UI_STATE { @@ -36,7 +34,7 @@ public enum UI_STATE { public final MediaPlayerStateProperty getCurrentTime; public final MediaPlayerStateProperty getDuration; - public final MediaPlayerStateProperty playerController; + public final MediaPlayerStateProperty mediaController; public final MediaPlayerStateProperty androidOptions; public final MediaPlayerStateProperty extraOptions; @@ -57,7 +55,7 @@ public MediaPlayerState(LifecycleOwner owner) { getCurrentTime = new MediaPlayerStateProperty<>(owner, 0L); getDuration = new MediaPlayerStateProperty<>(owner, 0L); - playerController = new MediaPlayerStateProperty<>(owner, null, true); + mediaController = new MediaPlayerStateProperty<>(owner, null, true); androidOptions = new MediaPlayerStateProperty<>(owner, null, true); extraOptions = new MediaPlayerStateProperty<>(owner, null, true); } diff --git a/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerStateProvider.java b/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerStateProvider.java index 42f6c30..373616d 100644 --- a/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerStateProvider.java +++ b/android/src/main/java/dev/eduardoroth/mediaplayer/state/MediaPlayerStateProvider.java @@ -1,11 +1,11 @@ package dev.eduardoroth.mediaplayer.state; +import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.media3.common.util.UnstableApi; import java.util.HashMap; -@UnstableApi public class MediaPlayerStateProvider { private static final MediaPlayerStateProvider _provider = new MediaPlayerStateProvider(); private final HashMap _instances = new HashMap<>(); @@ -20,7 +20,7 @@ public static MediaPlayerState getState(String playerId) { return _provider._instances.get(playerId); } - public static MediaPlayerState getState(String playerId, LifecycleOwner owner) { + public static MediaPlayerState createState(String playerId, @NonNull LifecycleOwner owner) { if (!_provider._instances.containsKey(playerId)) { MediaPlayerState playerState = new MediaPlayerState(owner); _provider._instances.put(playerId, playerState); diff --git a/android/src/main/res/layout/media_player_container.xml b/android/src/main/res/layout/media_player_container.xml index 51642a2..074872a 100644 --- a/android/src/main/res/layout/media_player_container.xml +++ b/android/src/main/res/layout/media_player_container.xml @@ -9,7 +9,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" - android:background="@color/black"> + android:background="@color/black" + android:visibility="visible"> - - - - + android:visibility="invisible"> diff --git a/android/src/main/res/layout/media_player_controller_view.xml b/android/src/main/res/layout/media_player_controller_view.xml index 3ff761e..dd2cf5e 100644 --- a/android/src/main/res/layout/media_player_controller_view.xml +++ b/android/src/main/res/layout/media_player_controller_view.xml @@ -1,7 +1,17 @@ + android:animateLayoutChanges="true" + android:keepScreenOn="true" + app:animation_enabled="true" + app:artwork_display_mode="fill" + app:hide_on_touch="true" + app:image_display_mode="fit" + app:resize_mode="fit" + app:show_buffering="always" + app:show_timeout="2500" + app:use_controller="true" /> diff --git a/android/src/main/res/xml/auto_app_desc.xml b/android/src/main/res/xml/auto_app_desc.xml new file mode 100644 index 0000000..f3871d3 --- /dev/null +++ b/android/src/main/res/xml/auto_app_desc.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/src/definitions.ts b/src/definitions.ts index fba8207..e64367d 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -128,6 +128,7 @@ export type MediaPlayerAndroidOptions = { openInFullscreen?: boolean; automaticallyEnterPiP?: boolean; fullscreenOnLandscape?: boolean; + stopOnTaskRemoved?: boolean; top?: number; start?: number; height?: number;