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:
@@ -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;
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.RawResourceDataSource;
|
||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||
@@ -39,6 +40,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_SRC_START_POSITION = "startPosition";
|
||||
private static final String PROP_SRC_CROP_START = "cropStart";
|
||||
private static final String PROP_SRC_CROP_END = "cropEnd";
|
||||
private static final String PROP_SRC_METADATA = "metadata";
|
||||
private static final String PROP_AD_TAG_URL = "adTagUrl";
|
||||
private static final String PROP_SRC_TYPE = "type";
|
||||
private static final String PROP_DRM = "drm";
|
||||
@@ -82,6 +84,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_CONTROLS = "controls";
|
||||
private static final String PROP_SUBTITLE_STYLE = "subtitleStyle";
|
||||
private static final String PROP_SHUTTER_COLOR = "shutterColor";
|
||||
private static final String PROP_SHOW_NOTIFICATION_CONTROLS = "showNotificationControls";
|
||||
private static final String PROP_DEBUG = "debug";
|
||||
|
||||
private final ReactExoplayerConfig config;
|
||||
@@ -166,6 +169,32 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
}
|
||||
}
|
||||
|
||||
ReadableMap propMetadata = ReactBridgeUtils.safeGetMap(src, PROP_SRC_METADATA);
|
||||
MediaMetadata customMetadata = null;
|
||||
if (propMetadata != null) {
|
||||
String title = ReactBridgeUtils.safeGetString(propMetadata, "title");
|
||||
String subtitle = ReactBridgeUtils.safeGetString(propMetadata, "subtitle");
|
||||
String description = ReactBridgeUtils.safeGetString(propMetadata, "description");
|
||||
String artist = ReactBridgeUtils.safeGetString(propMetadata, "artist");
|
||||
String imageUriString = ReactBridgeUtils.safeGetString(propMetadata, "imageUri");
|
||||
|
||||
Uri imageUri = null;
|
||||
|
||||
try {
|
||||
imageUri = Uri.parse(imageUriString);
|
||||
} catch (Exception e) {
|
||||
DebugLog.e("ExoPlayer Warning", "Could not parse imageUri in metadata");
|
||||
}
|
||||
|
||||
customMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setDescription(description)
|
||||
.setArtist(artist)
|
||||
.setArtworkUri(imageUri)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
videoView.clearSrc();
|
||||
return;
|
||||
@@ -175,7 +204,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
Uri srcUri = Uri.parse(uriString);
|
||||
|
||||
if (srcUri != null) {
|
||||
videoView.setSrc(srcUri, startPositionMs, cropStartMs, cropEndMs, extension, headers);
|
||||
videoView.setSrc(srcUri, startPositionMs, cropStartMs, cropEndMs, extension, headers, customMetadata);
|
||||
}
|
||||
} else {
|
||||
int identifier = context.getResources().getIdentifier(
|
||||
@@ -400,6 +429,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setBufferConfig(config);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
||||
public void setShowNotificationControls(final ReactExoplayerView videoView, final boolean showNotificationControls) {
|
||||
videoView.setShowNotificationControls(showNotificationControls);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DEBUG, defaultBoolean = false)
|
||||
public void setDebug(final ReactExoplayerView videoView,
|
||||
@Nullable final ReadableMap debugConfig) {
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package com.brentvatne.exoplayer
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
class VideoPlaybackCallback(private val seekIntervalMS: Long) : MediaSession.Callback {
|
||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||
try {
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailablePlayerCommands(
|
||||
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
|
||||
.add(Player.COMMAND_SEEK_FORWARD)
|
||||
.add(Player.COMMAND_SEEK_BACK)
|
||||
.build()
|
||||
).setAvailableSessionCommands(
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.add(SessionCommand(VideoPlaybackService.COMMAND_SEEK_FORWARD, Bundle.EMPTY))
|
||||
.add(SessionCommand(VideoPlaybackService.COMMAND_SEEK_BACKWARD, Bundle.EMPTY))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
return MediaSession.ConnectionResult.reject()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand.customAction) {
|
||||
VideoPlaybackService.COMMAND_SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + seekIntervalMS)
|
||||
VideoPlaybackService.COMMAND_SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition + seekIntervalMS)
|
||||
}
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
@@ -0,0 +1,150 @@
|
||||
package com.brentvatne.exoplayer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.MediaStyleNotificationHelper
|
||||
import androidx.media3.session.SessionCommand
|
||||
import okhttp3.internal.immutableListOf
|
||||
|
||||
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
|
||||
|
||||
class VideoPlaybackService : MediaSessionService() {
|
||||
private var mediaSessionsList = mutableMapOf<ExoPlayer, MediaSession>()
|
||||
private var binder = PlaybackServiceBinder(this)
|
||||
|
||||
// Controls
|
||||
private val commandSeekForward = SessionCommand(COMMAND_SEEK_FORWARD, Bundle.EMPTY)
|
||||
private val commandSeekBackward = SessionCommand(COMMAND_SEEK_BACKWARD, Bundle.EMPTY)
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private val seekForwardBtn = CommandButton.Builder()
|
||||
.setDisplayName("forward")
|
||||
.setSessionCommand(commandSeekForward)
|
||||
.setIconResId(androidx.media3.ui.R.drawable.exo_notification_fastforward)
|
||||
.build()
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private val seekBackwardBtn = CommandButton.Builder()
|
||||
.setDisplayName("backward")
|
||||
.setSessionCommand(commandSeekBackward)
|
||||
.setIconResId(androidx.media3.ui.R.drawable.exo_notification_rewind)
|
||||
.build()
|
||||
|
||||
// Player Registry
|
||||
|
||||
fun registerPlayer(player: ExoPlayer) {
|
||||
if (mediaSessionsList.containsKey(player)) {
|
||||
return
|
||||
}
|
||||
|
||||
val mediaSession = MediaSession.Builder(this, player)
|
||||
.setId("RNVideoPlaybackService_" + player.hashCode())
|
||||
.setCallback(VideoPlaybackCallback(SEEK_INTERVAL_MS))
|
||||
.setCustomLayout(immutableListOf(seekBackwardBtn, seekForwardBtn))
|
||||
.build()
|
||||
|
||||
mediaSessionsList[player] = mediaSession
|
||||
addSession(mediaSession)
|
||||
}
|
||||
|
||||
fun unregisterPlayer(player: ExoPlayer) {
|
||||
hidePlayerNotification(player)
|
||||
val session = mediaSessionsList.remove(player)
|
||||
session?.release()
|
||||
|
||||
if (mediaSessionsList.isEmpty()) {
|
||||
cleanup()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
|
||||
createSessionNotification(session)
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
cleanup()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanup()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createSessionNotification(session: MediaSession) {
|
||||
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (session.player.currentMediaItem == null) {
|
||||
notificationManager.cancel(session.player.hashCode())
|
||||
return
|
||||
}
|
||||
|
||||
val notificationCompact = NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
|
||||
.build()
|
||||
|
||||
notificationManager.notify(session.player.hashCode(), notificationCompact)
|
||||
}
|
||||
|
||||
private fun hidePlayerNotification(player: ExoPlayer) {
|
||||
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(player.hashCode())
|
||||
}
|
||||
|
||||
private fun hideAllNotifications() {
|
||||
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancelAll()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.deleteNotificationChannel(NOTIFICATION_CHANEL_ID)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
hideAllNotifications()
|
||||
mediaSessionsList.forEach { (_, session) ->
|
||||
session.release()
|
||||
}
|
||||
mediaSessionsList.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COMMAND_SEEK_FORWARD = "SEEK_FORWARD"
|
||||
const val COMMAND_SEEK_BACKWARD = "SEEK_BACKWARD"
|
||||
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
|
||||
const val SEEK_INTERVAL_MS = 10000L
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user