First basic implementation of cast functionality

This commit is contained in:
CappielloAntonio 2022-01-06 11:07:39 +01:00
parent 2d82007abd
commit 75bad72d83
7 changed files with 63 additions and 12 deletions

View file

@ -54,6 +54,9 @@ dependencies {
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Google GMS
implementation 'com.google.android.gms:play-services-cast-framework:21.0.0'
// Android Material // Android Material
implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.android.material:material:1.4.0'
@ -70,6 +73,7 @@ dependencies {
implementation "androidx.media3:media3-common:1.0.0-alpha01" implementation "androidx.media3:media3-common:1.0.0-alpha01"
implementation "androidx.media3:media3-exoplayer:1.0.0-alpha01" implementation "androidx.media3:media3-exoplayer:1.0.0-alpha01"
implementation "androidx.media3:media3-ui:1.0.0-alpha01" implementation "androidx.media3:media3-ui:1.0.0-alpha01"
implementation "androidx.media3:media3-cast:1.0.0-alpha01"
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
annotationProcessor 'androidx.room:room-compiler:2.4.0' annotationProcessor 'androidx.room:room-compiler:2.4.0'

View file

@ -16,6 +16,11 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:allowBackup="false"> android:allowBackup="false">
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
<activity <activity
android:name=".ui.activity.MainActivity" android:name=".ui.activity.MainActivity"
android:windowSoftInputMode="adjustPan|adjustResize" android:windowSoftInputMode="adjustPan|adjustResize"

View file

@ -5,14 +5,15 @@ import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.cast.CastPlayer;
import androidx.media3.cast.SessionAvailabilityListener;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.analytics.PlaybackStatsListener;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSourceFactory; import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.session.MediaLibraryService; import androidx.media3.session.MediaLibraryService;
@ -20,14 +21,16 @@ import androidx.media3.session.MediaSession;
import com.cappielloantonio.play.ui.activity.MainActivity; import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.util.DownloadUtil; import com.cappielloantonio.play.util.DownloadUtil;
import com.google.android.gms.cast.framework.CastContext;
public class MediaService extends MediaLibraryService { @SuppressLint("UnsafeOptInUsageError")
public class MediaService extends MediaLibraryService implements SessionAvailabilityListener {
private static final String TAG = "MediaService"; private static final String TAG = "MediaService";
public static final int REQUEST_CODE = 432; public static final int REQUEST_CODE = 432;
private ExoPlayer player; private ExoPlayer player;
private DataSource.Factory dataSourceFactory; private CastPlayer castPlayer;
private MediaSourceFactory mediaSourceFactory; private MediaSourceFactory mediaSourceFactory;
private MediaLibrarySession mediaLibrarySession; private MediaLibrarySession mediaLibrarySession;
@ -36,7 +39,11 @@ public class MediaService extends MediaLibraryService {
super.onCreate(); super.onCreate();
initializeMediaSource(); initializeMediaSource();
initializePlayer(); initializePlayer();
initializeCastPlayer();
initializeMediaLibrarySession();
initializePlayerListener(); initializePlayerListener();
setPlayer(null, castPlayer.isCastSessionAvailable() ? castPlayer : player);
} }
@Override @Override
@ -51,13 +58,11 @@ public class MediaService extends MediaLibraryService {
return mediaLibrarySession; return mediaLibrarySession;
} }
@SuppressLint("UnsafeOptInUsageError")
private void initializeMediaSource() { private void initializeMediaSource() {
dataSourceFactory = DownloadUtil.getDataSourceFactory(this); DataSource.Factory dataSourceFactory = DownloadUtil.getDataSourceFactory(this);
mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory);
} }
@SuppressLint("UnsafeOptInUsageError")
private void initializePlayer() { private void initializePlayer() {
player = new ExoPlayer.Builder(this) player = new ExoPlayer.Builder(this)
.setMediaSourceFactory(mediaSourceFactory) .setMediaSourceFactory(mediaSourceFactory)
@ -65,15 +70,40 @@ public class MediaService extends MediaLibraryService {
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK) .setWakeMode(C.WAKE_MODE_NETWORK)
.build(); .build();
}
private void initializeCastPlayer() {
castPlayer = new CastPlayer(CastContext.getSharedInstance(this));
castPlayer.setSessionAvailabilityListener(this);
}
private void initializeMediaLibrarySession() {
mediaLibrarySession = new MediaLibrarySession.Builder(this, player, new LibrarySessionCallback()) mediaLibrarySession = new MediaLibrarySession.Builder(this, player, new LibrarySessionCallback())
.setMediaItemFiller(new CustomMediaItemFiller()) .setMediaItemFiller(new CustomMediaItemFiller())
.setSessionActivity(PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, new Intent(getApplicationContext(), MainActivity.class), PendingIntent.FLAG_IMMUTABLE)) .setSessionActivity(PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, new Intent(getApplicationContext(), MainActivity.class), PendingIntent.FLAG_IMMUTABLE))
.build(); .build();
} }
@SuppressLint("UnsafeOptInUsageError") private void setPlayer(Player oldPlayer, Player newPlayer) {
if (oldPlayer == newPlayer) return;
if (oldPlayer != null) oldPlayer.stop();
mediaLibrarySession.setPlayer(newPlayer);
}
@Override
public void onCastSessionAvailable() {
setPlayer(player, castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setPlayer(castPlayer, player);
}
private void releasePlayer() { private void releasePlayer() {
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
player.release(); player.release();
mediaLibrarySession.release(); mediaLibrarySession.release();
} }
@ -81,18 +111,17 @@ public class MediaService extends MediaLibraryService {
private class LibrarySessionCallback implements MediaLibrarySession.MediaLibrarySessionCallback { private class LibrarySessionCallback implements MediaLibrarySession.MediaLibrarySessionCallback {
} }
@SuppressLint("UnsafeOptInUsageError")
private class CustomMediaItemFiller implements MediaSession.MediaItemFiller { private class CustomMediaItemFiller implements MediaSession.MediaItemFiller {
@Override @Override
public MediaItem fillInLocalConfiguration(MediaSession session, MediaSession.ControllerInfo controller, MediaItem mediaItem) { public MediaItem fillInLocalConfiguration(MediaSession session, MediaSession.ControllerInfo controller, MediaItem mediaItem) {
return mediaItem.buildUpon() return mediaItem.buildUpon()
.setUri(mediaItem.mediaMetadata.mediaUri) .setUri(mediaItem.mediaMetadata.mediaUri)
.setMediaMetadata(mediaItem.mediaMetadata) .setMediaMetadata(mediaItem.mediaMetadata)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.build(); .build();
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private void initializePlayerListener() { private void initializePlayerListener() {
player.addListener(new Player.Listener() { player.addListener(new Player.Listener() {
@Override @Override
@ -103,7 +132,7 @@ public class MediaService extends MediaLibraryService {
@Override @Override
public void onIsPlayingChanged(boolean isPlaying) { public void onIsPlayingChanged(boolean isPlaying) {
if(isPlaying) { if (isPlaying) {
MediaManager.setPlayingPausedTimestamp(player.getCurrentMediaItem(), player.getCurrentPosition()); MediaManager.setPlayingPausedTimestamp(player.getCurrentMediaItem(), player.getCurrentPosition());
} }
} }

View file

@ -46,6 +46,7 @@ import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.util.MusicUtil; import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.util.UIUtil; import com.cappielloantonio.play.util.UIUtil;
import com.cappielloantonio.play.viewmodel.HomeViewModel; import com.cappielloantonio.play.viewmodel.HomeViewModel;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.Objects; import java.util.Objects;
@ -79,6 +80,7 @@ public class HomeFragment extends Fragment {
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu); inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
} }
@Nullable @Nullable

View file

@ -1,10 +1,12 @@
package com.cappielloantonio.play.util; package com.cappielloantonio.play.util;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Artist;
@ -197,6 +199,7 @@ public class MappingUtil {
return genres; return genres;
} }
@SuppressLint("UnsafeOptInUsageError")
public static MediaItem mapMediaItem(Context context, Song song, boolean stream) { public static MediaItem mapMediaItem(Context context, Song song, boolean stream) {
boolean isDownloaded = DownloadUtil.getDownloadTracker(context).isDownloaded(MusicUtil.getSongDownloadUri(song)); boolean isDownloaded = DownloadUtil.getDownloadTracker(context).isDownloaded(MusicUtil.getSongDownloadUri(song));
@ -219,6 +222,7 @@ public class MappingUtil {
.setExtras(bundle) .setExtras(bundle)
.build() .build()
) )
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(stream && !isDownloaded ? MusicUtil.getSongStreamUri(context, song) : MusicUtil.getSongDownloadUri(song)) .setUri(stream && !isDownloaded ? MusicUtil.getSongStreamUri(context, song) : MusicUtil.getSongDownloadUri(song))
.build(); .build();
} }

View file

@ -5,7 +5,13 @@
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
android:title="@string/menu_search_button" android:title="@string/menu_search_button"
app:showAsAction="ifRoom" /> app:showAsAction="always" />
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"

View file

@ -212,4 +212,5 @@
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string> <string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="notification_channel_name">playNotificationChannelName</string> <string name="notification_channel_name">playNotificationChannelName</string>
<string name="notification_channel_description">playNotificationChannelNameDescription</string> <string name="notification_channel_description">playNotificationChannelNameDescription</string>
<string name="media_route_menu_title">Cast</string>
</resources> </resources>