feat: add notification controls (#3723)

* feat(ios): add `showNotificationControls` prop

* feat(android): add `showNotificationControls` prop

* add docs

* feat!: add `metadata` property to srouce

This is breaking change for iOS/tvOS as we are moving some properties, but I believe that this will more readable and more user friendly

* chore(ios): remove UI blocking function

* code review changes for android

* update example

* fix readme

* fix typos

* update docs

* fix typo

* chore: improve sample metadata notification

* update codegen types

* rename properties

* update tvOS example

* reset metadata on source change

* update docs

---------

Co-authored-by: Olivier Bouillet <freeboub@gmail.com>
This commit is contained in:
Krzysztof Moch
2024-05-07 12:30:57 +02:00
committed by GitHub
parent c59d00a0f0
commit 8ad4be459b
18 changed files with 908 additions and 105 deletions

View File

@@ -12,10 +12,15 @@ import static com.brentvatne.exoplayer.DataSourceUtil.buildAssetDataSourceFactor
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
@@ -27,6 +32,7 @@ import android.widget.ImageButton;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
@@ -35,6 +41,7 @@ import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
@@ -94,6 +101,7 @@ import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.id3.Id3Frame;
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
import androidx.media3.session.MediaSessionService;
import androidx.media3.ui.LegacyPlayerControlView;
import com.brentvatne.common.api.BufferConfig;
@@ -172,6 +180,10 @@ public class ReactExoplayerView extends FrameLayout implements
private ExoPlayer player;
private DefaultTrackSelector trackSelector;
private boolean playerNeedsSource;
private MediaMetadata customMetadata;
private ServiceConnection playbackServiceConnection;
private PlaybackServiceBinder playbackServiceBinder;
private int resumeWindow;
private long resumePosition;
@@ -224,6 +236,8 @@ public class ReactExoplayerView extends FrameLayout implements
private String[] drmLicenseHeader = null;
private boolean controls;
private Uri adTagUrl;
private boolean showNotificationControls = false;
// \ End props
// React
@@ -342,6 +356,12 @@ public class ReactExoplayerView extends FrameLayout implements
cleanUpResources();
}
@Override
protected void onDetachedFromWindow() {
cleanupPlaybackService();
super.onDetachedFromWindow();
}
public void cleanUpResources() {
stopPlayback();
themedReactContext.removeLifecycleEventListener(this);
@@ -656,6 +676,10 @@ public class ReactExoplayerView extends FrameLayout implements
PlaybackParameters params = new PlaybackParameters(rate, 1f);
player.setPlaybackParameters(params);
changeAudioOutput(this.audioOutput);
if(showNotificationControls) {
setupPlaybackService();
}
}
private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) {
@@ -741,6 +765,69 @@ public class ReactExoplayerView extends FrameLayout implements
applyModifiers();
}
private void setupPlaybackService() {
if (!showNotificationControls || player == null) {
return;
}
playbackServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
playbackServiceBinder = (PlaybackServiceBinder) service;
try {
playbackServiceBinder.getService().registerPlayer(player);
} catch (Exception e) {
DebugLog.e(TAG, "Cloud not register ExoPlayer");
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
try {
playbackServiceBinder.getService().unregisterPlayer(player);
} catch (Exception ignored) {}
playbackServiceBinder = null;
}
@Override
public void onNullBinding(ComponentName name) {
DebugLog.e(TAG, "Cloud not register ExoPlayer");
}
};
Intent intent = new Intent(themedReactContext, VideoPlaybackService.class);
intent.setAction(MediaSessionService.SERVICE_INTERFACE);
themedReactContext.startService(intent);
int flags;
if (Build.VERSION.SDK_INT >= 29) {
flags = Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES;
} else {
flags = Context.BIND_AUTO_CREATE;
}
themedReactContext.bindService(intent, playbackServiceConnection, flags);
}
private void cleanupPlaybackService() {
try {
if(player != null && playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
}
playbackServiceBinder = null;
if(playbackServiceConnection != null) {
themedReactContext.unbindService(playbackServiceConnection);
}
} catch(Exception e) {
DebugLog.w(TAG, "Cloud not cleanup playback service");
}
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, 0);
}
@@ -795,7 +882,12 @@ public class ReactExoplayerView extends FrameLayout implements
}
config.setDisableDisconnectError(this.disableDisconnectError);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder()
.setUri(uri);
if (customMetadata != null) {
mediaItemBuilder.setMediaMetadata(customMetadata);
}
if (adTagUrl != null) {
mediaItemBuilder.setAdsConfiguration(
@@ -935,12 +1027,20 @@ public class ReactExoplayerView extends FrameLayout implements
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
if(playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
themedReactContext.unbindService(playbackServiceConnection);
}
updateResumePosition();
player.release();
player.removeListener(this);
trackSelector = null;
player = null;
}
if (adsLoader != null) {
adsLoader.release();
}
@@ -1542,7 +1642,21 @@ public class ReactExoplayerView extends FrameLayout implements
// ReactExoplayerViewManager public api
public void setSrc(final Uri uri, final long startPositionMs, final long cropStartMs, final long cropEndMs, final String extension, Map<String, String> headers) {
public void setSrc(final Uri uri, final long startPositionMs, final long cropStartMs, final long cropEndMs, final String extension, Map<String, String> headers, MediaMetadata customMetadata) {
if (this.customMetadata != customMetadata && player != null) {
MediaItem currentMediaItem = player.getCurrentMediaItem();
if (currentMediaItem == null) {
return;
}
MediaItem newMediaItem = currentMediaItem.buildUpon().setMediaMetadata(customMetadata).build();
// This will cause video blink/reload but won't louse progress
player.setMediaItem(newMediaItem, false);
}
if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri) && cropStartMs == this.cropStartMs && cropEndMs == this.cropEndMs;
hasDrmFailed = false;
@@ -1555,6 +1669,7 @@ public class ReactExoplayerView extends FrameLayout implements
this.mediaDataSourceFactory =
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
this.requestHeaders);
this.customMetadata = customMetadata;
if (!isSourceEqual) {
reloadSource();
@@ -1573,6 +1688,7 @@ public class ReactExoplayerView extends FrameLayout implements
this.extension = null;
this.requestHeaders = null;
this.mediaDataSourceFactory = null;
customMetadata = null;
clearResumePosition();
}
}
@@ -1956,6 +2072,16 @@ public class ReactExoplayerView extends FrameLayout implements
this.contentStartTime = contentStartTime;
}
public void setShowNotificationControls(boolean showNotificationControls) {
this.showNotificationControls = showNotificationControls;
if (playbackServiceConnection == null && showNotificationControls) {
setupPlaybackService();
} else if(!showNotificationControls && playbackServiceConnection != null) {
cleanupPlaybackService();
}
}
public void setDisableBuffering(boolean disableBuffering) {
this.disableBuffering = disableBuffering;
}