public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { Validate.notNull(context); Validate.notNull(callback); this.context = context; this.callback = callback; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.playerLock = new ReentrantLock(); this.startWhenPrepared = new AtomicBoolean(false); executor = new ThreadPoolExecutor( 1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); } }); mediaSession = new MediaSessionCompat(context, TAG); mediaSession.setCallback(sessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mediaPlayer = null; statusBeforeSeeking = null; pausedBecauseOfTransientAudiofocusLoss = false; mediaType = MediaType.UNKNOWN; playerStatus = PlayerStatus.STOPPED; videoSize = null; }
/** * Update the current media player state, optionally showing an error message. * * @param error if not null, error message to present to the user. */ private void updatePlaybackState(String error) { LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState()); long position = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; if (mPlayback != null && mPlayback.isConnected()) { position = mPlayback.getCurrentStreamPosition(); } PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder().setActions(getAvailableActions()); setCustomAction(stateBuilder); int state = mPlayback.getState(); // If there is an error message, send it to the playback state: if (error != null) { // Error states are really only supposed to be used for errors that cause playback to // stop unexpectedly and persist until the user takes action to fix it. stateBuilder.setErrorMessage(error); state = PlaybackStateCompat.STATE_ERROR; } stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); // Set the activeQueueItemId if the current index is valid. if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { MediaSessionCompat.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); stateBuilder.setActiveQueueItemId(item.getQueueId()); } mSession.setPlaybackState(stateBuilder.build()); if (state == PlaybackStateCompat.STATE_PLAYING || state == PlaybackStateCompat.STATE_PAUSED) { mMediaNotificationManager.startNotification(); } }
@Override public void onDisconnected() { LogHelper.d(TAG, "onDisconnected"); mSessionExtras.remove(EXTRA_CONNECTED_CAST); mSession.setExtras(mSessionExtras); Playback playback = new LocalPlayback(MusicService.this, mMusicProvider); mMediaRouter.setMediaSession(null); switchToPlayer(playback, false); }
/** * Build a new notification. To update the progress on the notification, use {@link * #updateProgress(int, int)} instead. * * @param episode The episode playing. * @param paused Playback state, <code>true</code> for paused. * @param canSeek If the currently played media is seekable. * @param position The current playback progress. * @param duration The length of the current episode. * @param session The media session representing current playback. * @return The notification to display. */ @NonNull public Notification build( Episode episode, boolean paused, boolean canSeek, int position, int duration, MediaSessionCompat session) { // 0. Prepare the main intent (leading back to the app) appIntent.putExtra(PODCAST_URL_KEY, episode.getPodcast().getUrl()); appIntent.putExtra(EPISODE_URL_KEY, episode.getMediaUrl()); final PendingIntent backToAppIntent = PendingIntent.getActivity(context, 0, appIntent, PendingIntent.FLAG_UPDATE_CURRENT); // 1. Create the notification builder and set values notificationBuilder = new NotificationCompat.Builder(context); notificationBuilder .setContentIntent(backToAppIntent) .setTicker(episode.getName()) .setSmallIcon(R.drawable.ic_stat) .setContentTitle(episode.getName()) .setContentText(episode.getPodcast().getName()) .setWhen(0) .setProgress(duration, position, false) .setOngoing(true); // 2. Load large image if available, see onBitmapLoaded() below if (episode.getPodcast().hasLogoUrl()) Picasso.with(context) .load(episode.getPodcast().getLogoUrl()) .resizeDimen( android.R.dimen.notification_large_icon_width, android.R.dimen.notification_large_icon_height) .into(this); // 3. Add actions to notification notificationBuilder.addAction(stopAction); if (canSeek) notificationBuilder.addAction(rewindAction); if (paused) notificationBuilder.addAction(playAction); else notificationBuilder.addAction(pauseAction); if (canSeek) notificationBuilder.addAction(forwardAction); // 4. Apply other notification features NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle().setMediaSession(session.getSessionToken()); // Make sure not to show rew/ff icons for live streams if (canSeek) style.setShowActionsInCompactView(1, 2, 3); // rewind, toggle play, forward else style.setShowActionsInCompactView(0, 1); // stop, toggle play notificationBuilder.setStyle(style); notificationBuilder.setColor(ContextCompat.getColor(context, R.color.theme_dark)); notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); notificationBuilder.setCategory(NotificationCompat.CATEGORY_TRANSPORT); return notificationBuilder.build(); }
/** * Releases internally used resources. This method should only be called when the object is not * used anymore. */ public void shutdown() { executor.shutdown(); if (mediaPlayer != null) { mediaPlayer.release(); } if (mediaSession != null) { mediaSession.release(); } releaseWifiLockIfNecessary(); }
/** * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON} intent, * passing it onto the {@link MediaSessionCompat} using {@link * MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn will trigger callbacks * to the {@link MediaSessionCompat.Callback} registered via {@link * MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}. * * <p>The returned {@link KeyEvent} is non-null if any {@link KeyEvent} is found and can be used * if any additional processing is needed beyond what is done in the {@link * MediaSessionCompat.Callback}. An example of is to prevent redelivery of a {@link * KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} Intent in the case of the Service being restarted (which, by * default, will redeliver the last received Intent). * * <pre> * KeyEvent keyEvent = MediaButtonReceiver.handleIntent(mediaSession, intent); * if (keyEvent != null && keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { * Intent emptyIntent = new Intent(intent); * emptyIntent.setAction(""); * startService(emptyIntent); * } * </pre> * * @param mediaSessionCompat A {@link MediaSessionCompat} that has a {@link * MediaSessionCompat.Callback} set. * @param intent The intent to parse. * @return The extracted {@link KeyEvent} if found, or null. */ public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) { if (mediaSessionCompat == null || intent == null || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) { return null; } KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); MediaControllerCompat mediaController = mediaSessionCompat.getController(); mediaController.dispatchMediaButtonEvent(ke); return ke; }
@Override public void onApplicationConnected( ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { // In case we are casting, send the device name as an extra on MediaSessionCompat // metadata. mSessionExtras.putString(EXTRA_CONNECTED_CAST, mCastManager.getDeviceName()); mSession.setExtras(mSessionExtras); // Now we can switch to CastPlayback Playback playback = new CastPlayback(mMusicProvider); mMediaRouter.setMediaSession(mSession); switchToPlayer(playback, true); }
/** Handle a request to play music */ private void handlePlayRequest() { LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState()); mDelayedStopHandler.removeCallbacksAndMessages(null); if (!mServiceStarted) { LogHelper.v(TAG, "Starting service"); // The MusicService needs to keep running even after the calling MediaBrowserCompat // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer // need to play media. startService(new Intent(getApplicationContext(), MusicService.class)); mServiceStarted = true; } if (!mSession.isActive()) { mSession.setActive(true); } if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { updateMetadata(); mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue)); } }
/** * (non-Javadoc) * * @see android.app.Service#onDestroy() */ @Override public void onDestroy() { LogHelper.d(TAG, "onDestroy"); unregisterReceiver(mCarConnectionReceiver); // Service is being killed, so make sure we release our resources handleStopRequest(null); mCastManager = VideoCastManager.getInstance(); mCastManager.removeVideoCastConsumer(mCastConsumer); mDelayedStopHandler.removeCallbacksAndMessages(null); // Always release the MediaSessionCompat to clean up resources // and notify associated MediaControllerCompat(s). mSession.release(); }
/** * Returns a token to this object's MediaSession. The MediaSession should only be used for * notifications at the moment. * * @return The MediaSessionCompat.Token object. */ public MediaSessionCompat.Token getSessionToken() { return mediaSession.getSessionToken(); }
/** * Internal implementation of playMediaObject. This method has an additional parameter that allows * the caller to force a media player reset even if the given playable parameter is the same * object as the currently playing media. * * <p>This method requires the playerLock and is executed on the caller's thread. * * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, * boolean) */ private void playMediaObject( final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Validate.notNull(playable); if (!playerLock.isHeldByCurrentThread()) throw new IllegalStateException("method requires playerLock"); if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { // episode is already playing -> ignore method call if (BuildConfig.DEBUG) Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); return; } else { // stop playback of this episode if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { mediaPlayer.stop(); } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } } this.media = playable; this.stream = stream; this.mediaType = media.getMediaType(); this.videoSize = null; createMediaPlayer(); PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); setPlayerStatus(PlayerStatus.INITIALIZING, media); try { media.loadMetadata(); mediaSession.setMetadata(getMediaSessionMetadata(media)); if (stream) { mediaPlayer.setDataSource(media.getStreamUrl()); } else { mediaPlayer.setDataSource(media.getLocalMediaUrl()); } setPlayerStatus(PlayerStatus.INITIALIZED, media); if (mediaType == MediaType.VIDEO) { VideoPlayer vp = (VideoPlayer) mediaPlayer; // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); } if (prepareImmediately) { setPlayerStatus(PlayerStatus.PREPARING, media); mediaPlayer.prepare(); onPrepared(startWhenPrepared); } } catch (Playable.PlayableException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } catch (IOException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } catch (IllegalStateException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } }
private void updateMetadata() { if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { LogHelper.e(TAG, "Can't retrieve current metadata."); updatePlaybackState(getResources().getString(R.string.error_no_metadata)); return; } MediaSessionCompat.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); String musicId = MediaIDHelper.extractMusicIDFromMediaID(queueItem.getDescription().getMediaId()); MediaMetadataCompat track = mMusicProvider.getMusic(musicId); if (track == null) { throw new IllegalArgumentException("Invalid musicId " + musicId); } final String trackId = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID); if (!TextUtils.equals(musicId, trackId)) { IllegalStateException e = new IllegalStateException("track ID should match musicId."); LogHelper.e( TAG, "track ID should match musicId.", " musicId=", musicId, " trackId=", trackId, " mediaId from queueItem=", queueItem.getDescription().getMediaId(), " title from queueItem=", queueItem.getDescription().getTitle(), " mediaId from track=", track.getDescription().getMediaId(), " title from track=", track.getDescription().getTitle(), " source.hashcode from track=", track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(), e); throw e; } LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId); mSession.setMetadata(track); // Set the proper album artwork on the media session, so it can be shown in the // locked screen and in other places. if (track.getDescription().getIconBitmap() == null && track.getDescription().getIconUri() != null) { String albumUri = track.getDescription().getIconUri().toString(); AlbumArtCache.getInstance() .fetch( albumUri, new AlbumArtCache.FetchListener() { @Override public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) { MediaSessionCompat.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); MediaMetadataCompat track = mMusicProvider.getMusic(trackId); track = new MediaMetadataCompat.Builder(track) // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for // example, on the lockscreen background when the media session is active. .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) // set small version of the album art in the DISPLAY_ICON. This is used on // the MediaDescriptionCompat and thus it should be small to be serialized // if // necessary.. .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, icon) .build(); mMusicProvider.updateMusic(trackId, track); // If we are still playing the same music String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID( queueItem.getDescription().getMediaId()); if (trackId.equals(currentPlayingId)) { mSession.setMetadata(track); } } }); } }
/* * (non-Javadoc) * @see android.app.Service#onCreate() */ @Override public void onCreate() { super.onCreate(); LogHelper.d(TAG, "onCreate"); mPlayingQueue = new ArrayList<>(); mMusicProvider = new MusicProvider(); mPackageValidator = new PackageValidator(this); ComponentName mediaButtonReceiver = new ComponentName(this, RemoteControlReceiver.class); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(mediaButtonReceiver); PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0); // Start a new MediaSessionCompat mSession = new MediaSessionCompat(this, "MusicService", mediaButtonReceiver, mediaPendingIntent); /* final MediaSessionCallback cb = new MediaSessionCallback(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Shouldn't really have to do this but the MediaSessionCompat method uses // an internal proxy class, which doesn't forward events such as // onPlayFromMediaId when running on Lollipop. final MediaSession session = (MediaSession) mSession.getMediaSession(); session.setCallback(new MediaSessionCallbackProxy(cb)); } else { mSession.setCallback(cb); } */ setSessionToken(mSession.getSessionToken()); mSession.setCallback(new MediaSessionCallback()); mSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mPlayback = new LocalPlayback(this, mMusicProvider); mPlayback.setState(PlaybackStateCompat.STATE_NONE); mPlayback.setCallback(this); mPlayback.start(); Context context = getApplicationContext(); Intent intent = new Intent(context, NowPlayingActivity.class); PendingIntent pi = PendingIntent.getActivity( context, 99 /*request code*/, intent, PendingIntent.FLAG_UPDATE_CURRENT); mSession.setSessionActivity(pi); mSessionExtras = new Bundle(); CarHelper.setSlotReservationFlags(mSessionExtras, true, true, true); WearHelper.setSlotReservationFlags(mSessionExtras, true, true); WearHelper.setUseBackgroundFromTheme(mSessionExtras, true); mSession.setExtras(mSessionExtras); updatePlaybackState(null); mMediaNotificationManager = new MediaNotificationManager(this); mCastManager = VideoCastManager.getInstance(); mCastManager.addVideoCastConsumer(mCastConsumer); mMediaRouter = MediaRouter.getInstance(getApplicationContext()); IntentFilter filter = new IntentFilter(CarHelper.ACTION_MEDIA_STATUS); mCarConnectionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String connectionEvent = intent.getStringExtra(CarHelper.MEDIA_CONNECTION_STATUS); mIsConnectedToCar = CarHelper.MEDIA_CONNECTED.equals(connectionEvent); LogHelper.i( TAG, "Connection event to Android Auto: ", connectionEvent, " isConnectedToCar=", mIsConnectedToCar); } }; registerReceiver(mCarConnectionReceiver, filter); }