fix(android): refactor source, fix random DRM issue and crop start on local asset (#3835)
* fix: refactor source parameter parsing Also fix a side issue when using a local file cropping props were not applied Also fix random DRM issue by refactoring initializePlayerSource https://github.com/TheWidlarzGroup/react-native-video/issues/3082 * chore: restore metadata checks before appling them
This commit is contained in:
		
							
								
								
									
										210
									
								
								android/src/main/java/com/brentvatne/common/api/Source.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								android/src/main/java/com/brentvatne/common/api/Source.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| package com.brentvatne.common.api | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.ContentResolver | ||||
| import android.content.Context | ||||
| import android.content.res.Resources | ||||
| import android.net.Uri | ||||
| import android.text.TextUtils | ||||
| import com.brentvatne.common.toolbox.DebugLog | ||||
| import com.brentvatne.common.toolbox.DebugLog.e | ||||
| import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray | ||||
| import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt | ||||
| import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetMap | ||||
| import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import java.util.Locale | ||||
|  | ||||
| /** | ||||
|  * Class representing Source props for host. | ||||
|  * Only generic code here, no reference to the player. | ||||
|  */ | ||||
| class Source { | ||||
|     /** String value of source to playback */ | ||||
|     private var uriString: String? = null | ||||
|  | ||||
|     /** Parsed value of source to playback */ | ||||
|     var uri: Uri? = null | ||||
|  | ||||
|     /** Start position of playback used to resume playback */ | ||||
|     var startPositionMs: Int = -1 | ||||
|  | ||||
|     /** Will crop content start at specified position */ | ||||
|     var cropStartMs: Int = -1 | ||||
|  | ||||
|     /** Will crop content end at specified position */ | ||||
|     var cropEndMs: Int = -1 | ||||
|  | ||||
|     /** Allow to force stream content, necessary when uri doesn't contain content type (.mlp4, .m3u, ...) */ | ||||
|     var extension: String? = null | ||||
|  | ||||
|     /** Metadata to display in notification */ | ||||
|     var metadata: Metadata? = null | ||||
|  | ||||
|     /** http header list */ | ||||
|     val headers: MutableMap<String, String> = HashMap() | ||||
|  | ||||
|     /** return true if this and src are equals  */ | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (other == null || other !is Source) return false | ||||
|         return ( | ||||
|             uri == other.uri && | ||||
|                 cropStartMs == other.cropStartMs && | ||||
|                 cropEndMs == other.cropEndMs && | ||||
|                 startPositionMs == other.startPositionMs && | ||||
|                 extension == other.extension | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** return true if this and src are equals  */ | ||||
|     fun isEquals(source: Source): Boolean { | ||||
|         return this == source | ||||
|     } | ||||
|  | ||||
|     /** Metadata to display in notification */ | ||||
|     class Metadata { | ||||
|         /** Metadata title */ | ||||
|         var title: String? = null | ||||
|  | ||||
|         /** Metadata subtitle */ | ||||
|         var subtitle: String? = null | ||||
|  | ||||
|         /** Metadata description */ | ||||
|         var description: String? = null | ||||
|  | ||||
|         /** Metadata artist */ | ||||
|         var artist: String? = null | ||||
|  | ||||
|         /** image uri to display */ | ||||
|         var imageUri: Uri? = null | ||||
|  | ||||
|         companion object { | ||||
|             private const val PROP_SRC_METADATA_TITLE = "title" | ||||
|             private const val PROP_SRC_METADATA_SUBTITLE = "subtitle" | ||||
|             private const val PROP_SRC_METADATA_DESCRIPTION = "description" | ||||
|             private const val PROP_SRC_METADATA_ARTIST = "artist" | ||||
|             private const val PROP_SRC_METADATA_IMAGE_URI = "imageUri" | ||||
|  | ||||
|             /** parse metadata object */ | ||||
|             @JvmStatic | ||||
|             fun parse(src: ReadableMap?): Metadata? { | ||||
|                 if (src != null) { | ||||
|                     val metadata = Metadata() | ||||
|                     metadata.title = safeGetString(src, PROP_SRC_METADATA_TITLE) | ||||
|                     metadata.subtitle = safeGetString(src, PROP_SRC_METADATA_SUBTITLE) | ||||
|                     metadata.description = safeGetString(src, PROP_SRC_METADATA_DESCRIPTION) | ||||
|                     metadata.artist = safeGetString(src, PROP_SRC_METADATA_ARTIST) | ||||
|                     val imageUriString = safeGetString(src, PROP_SRC_METADATA_IMAGE_URI) | ||||
|                     try { | ||||
|                         metadata.imageUri = Uri.parse(imageUriString) | ||||
|                     } catch (e: Exception) { | ||||
|                         e(TAG, "Could not parse imageUri in metadata") | ||||
|                     } | ||||
|                     return metadata | ||||
|                 } | ||||
|                 return null | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "Source" | ||||
|         private const val PROP_SRC_URI = "uri" | ||||
|         private const val PROP_SRC_START_POSITION = "startPosition" | ||||
|         private const val PROP_SRC_CROP_START = "cropStart" | ||||
|         private const val PROP_SRC_CROP_END = "cropEnd" | ||||
|         private const val PROP_SRC_TYPE = "type" | ||||
|         private const val PROP_SRC_METADATA = "metadata" | ||||
|         private const val PROP_SRC_HEADERS = "requestHeaders" | ||||
|  | ||||
|         @SuppressLint("DiscouragedApi") | ||||
|         private fun getUriFromAssetId(context: Context, uriString: String): Uri? { | ||||
|             val resources: Resources = context.resources | ||||
|             val packageName: String = context.packageName | ||||
|             var identifier = resources.getIdentifier( | ||||
|                 uriString, | ||||
|                 "drawable", | ||||
|                 packageName | ||||
|             ) | ||||
|             if (identifier == 0) { | ||||
|                 identifier = resources.getIdentifier( | ||||
|                     uriString, | ||||
|                     "raw", | ||||
|                     packageName | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             if (identifier <= 0) { | ||||
|                 // cannot find identifier of content | ||||
|                 DebugLog.d(TAG, "cannot find identifier") | ||||
|                 return null | ||||
|             } | ||||
|             return Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE).path(identifier.toString()).build() | ||||
|         } | ||||
|  | ||||
|         /** parse the source ReadableMap received from app */ | ||||
|         @JvmStatic | ||||
|         fun parse(src: ReadableMap?, context: Context): Source { | ||||
|             val source = Source() | ||||
|  | ||||
|             if (src != null) { | ||||
|                 val uriString = safeGetString(src, PROP_SRC_URI, null) | ||||
|                 if (uriString == null || TextUtils.isEmpty(uriString)) { | ||||
|                     DebugLog.d(TAG, "isEmpty uri:$uriString") | ||||
|                     return source | ||||
|                 } | ||||
|                 var uri = Uri.parse(uriString) | ||||
|                 if (uri == null) { | ||||
|                     // return an empty source | ||||
|                     DebugLog.d(TAG, "Invalid uri:$uriString") | ||||
|                     return source | ||||
|                 } else if (!isValidScheme(uri.scheme)) { | ||||
|                     uri = getUriFromAssetId(context, uriString) | ||||
|                     if (uri == null) { | ||||
|                         // cannot find identifier of content | ||||
|                         DebugLog.d(TAG, "cannot find identifier") | ||||
|                         return source | ||||
|                     } | ||||
|                 } | ||||
|                 source.uriString = uriString | ||||
|                 source.uri = uri | ||||
|                 source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1) | ||||
|                 source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1) | ||||
|                 source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1) | ||||
|                 source.extension = safeGetString(src, PROP_SRC_TYPE, null) | ||||
|  | ||||
|                 val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS) | ||||
|                 if (propSrcHeadersArray != null) { | ||||
|                     if (propSrcHeadersArray.size() > 0) { | ||||
|                         for (i in 0 until propSrcHeadersArray.size()) { | ||||
|                             val current = propSrcHeadersArray.getMap(i) | ||||
|                             val key = if (current.hasKey("key")) current.getString("key") else null | ||||
|                             val value = if (current.hasKey("value")) current.getString("value") else null | ||||
|                             if (key != null && value != null) { | ||||
|                                 source.headers[key] = value | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA)) | ||||
|             } | ||||
|             return source | ||||
|         } | ||||
|  | ||||
|         /** return true if rui scheme is supported for android playback */ | ||||
|         private fun isValidScheme(scheme: String?): Boolean { | ||||
|             if (scheme == null) { | ||||
|                 return false | ||||
|             } | ||||
|             val lowerCaseUri = scheme.lowercase(Locale.getDefault()) | ||||
|             return ( | ||||
|                 lowerCaseUri == "http" || | ||||
|                     lowerCaseUri == "https" || | ||||
|                     lowerCaseUri == "content" || | ||||
|                     lowerCaseUri == "file" || | ||||
|                     lowerCaseUri == "rtsp" || | ||||
|                     lowerCaseUri == "asset" | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,10 @@ | ||||
| package com.brentvatne.exoplayer | ||||
|  | ||||
| import androidx.media3.common.MediaItem.LiveConfiguration | ||||
| import androidx.media3.common.MediaMetadata | ||||
| import com.brentvatne.common.api.BufferConfig | ||||
| import com.brentvatne.common.api.BufferConfig.Live | ||||
| import com.brentvatne.common.api.Source | ||||
|  | ||||
| /** | ||||
|  * Helper functions to create exoplayer configuration | ||||
| @@ -33,4 +35,22 @@ object ConfigurationUtils { | ||||
|         } | ||||
|         return liveConfiguration | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate exoplayer MediaMetadata from source.Metadata | ||||
|      */ | ||||
|     @JvmStatic | ||||
|     fun buildCustomMetadata(metadata: Source.Metadata?): MediaMetadata? { | ||||
|         var customMetadata: MediaMetadata? = null | ||||
|         if (metadata != null) { | ||||
|             customMetadata = MediaMetadata.Builder() | ||||
|                 .setTitle(metadata.title) | ||||
|                 .setSubtitle(metadata.subtitle) | ||||
|                 .setDescription(metadata.description) | ||||
|                 .setArtist(metadata.artist) | ||||
|                 .setArtworkUri(metadata.imageUri) | ||||
|                 .build() | ||||
|         } | ||||
|         return customMetadata | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -113,6 +113,7 @@ import com.brentvatne.common.api.ControlsConfig; | ||||
| import com.brentvatne.common.api.ResizeMode; | ||||
| import com.brentvatne.common.api.SideLoadedTextTrack; | ||||
| import com.brentvatne.common.api.SideLoadedTextTrackList; | ||||
| import com.brentvatne.common.api.Source; | ||||
| import com.brentvatne.common.api.SubtitleStyle; | ||||
| import com.brentvatne.common.api.TimedMetadata; | ||||
| import com.brentvatne.common.api.Track; | ||||
| @@ -137,7 +138,6 @@ import java.net.CookieManager; | ||||
| import java.net.CookiePolicy; | ||||
| import java.lang.Math; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| @@ -220,11 +220,7 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     private ControlsConfig controlsConfig = new ControlsConfig(); | ||||
|  | ||||
|     // Props from React | ||||
|     private Uri srcUri; | ||||
|     private long startPositionMs = -1; | ||||
|     private long cropStartMs = -1; | ||||
|     private long cropEndMs = -1; | ||||
|     private String extension; | ||||
|     private Source source = new Source(); | ||||
|     private boolean repeat; | ||||
|     private String audioTrackType; | ||||
|     private String audioTrackValue; | ||||
| @@ -241,7 +237,6 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     private boolean preventsDisplaySleepDuringVideoPlayback = true; | ||||
|     private float mProgressUpdateInterval = 250.0f; | ||||
|     private boolean playInBackground = false; | ||||
|     private Map<String, String> requestHeaders; | ||||
|     private boolean mReportBandwidth = false; | ||||
|     private UUID drmUUID = null; | ||||
|     private String drmLicenseUrl = null; | ||||
| @@ -324,8 +319,6 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     } | ||||
|  | ||||
|     private void createViews() { | ||||
|         clearResumePosition(); | ||||
|         mediaDataSourceFactory = buildDataSourceFactory(true); | ||||
|         if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { | ||||
|             CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); | ||||
|         } | ||||
| @@ -652,22 +645,15 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|                     // Initialize core configuration and listeners | ||||
|                     initializePlayerCore(self); | ||||
|                 } | ||||
|                 if (playerNeedsSource && srcUri != null) { | ||||
|                 if (playerNeedsSource && source.getUri() != null) { | ||||
|                     exoPlayerView.invalidateAspectRatio(); | ||||
|                     // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread | ||||
|                     ExecutorService es = Executors.newSingleThreadExecutor(); | ||||
|                     es.execute(() -> { | ||||
|                         // DRM initialization must run on a different thread | ||||
|                         DrmSessionManager drmSessionManager = initializePlayerDrm(self); | ||||
|                         if (drmSessionManager == null && self.drmUUID != null) { | ||||
|                             // Failed to intialize DRM session manager - cannot continue | ||||
|                             DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); | ||||
|                             eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003"); | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         if (activity == null) { | ||||
|                             DebugLog.e(TAG, "Failed to initialize Player!"); | ||||
|                             DebugLog.e(TAG, "Failed to initialize Player!, null activity"); | ||||
|                             eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); | ||||
|                             return; | ||||
|                         } | ||||
| @@ -676,22 +662,24 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|                         activity.runOnUiThread(() -> { | ||||
|                             try { | ||||
|                                 // Source initialization must run on the main thread | ||||
|                                 initializePlayerSource(self, drmSessionManager); | ||||
|                                 initializePlayerSource(); | ||||
|                             } catch (Exception ex) { | ||||
|                                 self.playerNeedsSource = true; | ||||
|                                 DebugLog.e(TAG, "Failed to initialize Player!"); | ||||
|                                 DebugLog.e(TAG, ex.toString()); | ||||
|                                 ex.printStackTrace(); | ||||
|                                 self.eventEmitter.error(ex.toString(), ex, "1001"); | ||||
|                             } | ||||
|                         }); | ||||
|                     }); | ||||
|                 } else if (srcUri != null) { | ||||
|                     initializePlayerSource(self, null); | ||||
|                 } else if (source.getUri() != null) { | ||||
|                     initializePlayerSource(); | ||||
|                 } | ||||
|             } catch (Exception ex) { | ||||
|                 self.playerNeedsSource = true; | ||||
|                 DebugLog.e(TAG, "Failed to initialize Player!"); | ||||
|                 DebugLog.e(TAG, ex.toString()); | ||||
|                 ex.printStackTrace(); | ||||
|                 eventEmitter.error(ex.toString(), ex, "1001"); | ||||
|             } | ||||
|         }; | ||||
| @@ -782,15 +770,26 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|         return drmSessionManager; | ||||
|     } | ||||
|  | ||||
|     private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { | ||||
|     private void initializePlayerSource() { | ||||
|         if (source.getUri() == null) { | ||||
|             return; | ||||
|         } | ||||
|         DrmSessionManager drmSessionManager = initializePlayerDrm(this); | ||||
|         if (drmSessionManager == null && drmUUID != null) { | ||||
|             // Failed to intialize DRM session manager - cannot continue | ||||
|             DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); | ||||
|             eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         ArrayList<MediaSource> mediaSourceList = buildTextSources(); | ||||
|         MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, cropStartMs, cropEndMs); | ||||
|         MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs()); | ||||
|         MediaSource mediaSourceWithAds = null; | ||||
|         if (adTagUrl != null && adsLoader != null) { | ||||
|             DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) | ||||
|                     .setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView); | ||||
|             DataSpec adTagDataSpec = new DataSpec(adTagUrl); | ||||
|             mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(srcUri, adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView); | ||||
|             mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(source.getUri(), adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView); | ||||
|         } else { | ||||
|             if (adTagUrl == null && adsLoader != null) { | ||||
|                 adsLoader.release(); | ||||
| @@ -830,8 +829,8 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|         if (haveResumePosition) { | ||||
|             player.seekTo(resumeWindow, resumePosition); | ||||
|             player.setMediaSource(mediaSource, false); | ||||
|         } else if (startPositionMs > 0) { | ||||
|             player.setMediaSource(mediaSource, startPositionMs); | ||||
|         } else if (source.getStartPositionMs() > 0) { | ||||
|             player.setMediaSource(mediaSource, source.getStartPositionMs()); | ||||
|         } else { | ||||
|             player.setMediaSource(mediaSource, true); | ||||
|         } | ||||
| @@ -1029,14 +1028,14 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|                 ); | ||||
|                 break; | ||||
|             case CONTENT_TYPE_OTHER: | ||||
|                 if ("asset".equals(srcUri.getScheme())) { | ||||
|                 if ("asset".equals(uri.getScheme())) { | ||||
|                     try { | ||||
|                         DataSource.Factory assetDataSourceFactory = buildAssetDataSourceFactory(themedReactContext, srcUri); | ||||
|                         DataSource.Factory assetDataSourceFactory = buildAssetDataSourceFactory(themedReactContext, uri); | ||||
|                         mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory); | ||||
|                     } catch (Exception e) { | ||||
|                         throw new IllegalStateException("cannot open input file" + srcUri); | ||||
|                         throw new IllegalStateException("cannot open input file" + uri); | ||||
|                     } | ||||
|                 } else if ("file".equals(srcUri.getScheme()) || | ||||
|                 } else if ("file".equals(uri.getScheme()) || | ||||
|                         !useCache) { | ||||
|                     mediaSourceFactory = new ProgressiveMediaSource.Factory( | ||||
|                             mediaDataSourceFactory | ||||
| @@ -1193,7 +1192,7 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     } | ||||
|  | ||||
|     private boolean requestAudioFocus() { | ||||
|         if (disableFocus || srcUri == null || this.hasAudioFocus) { | ||||
|         if (disableFocus || source.getUri() == null || this.hasAudioFocus) { | ||||
|             return true; | ||||
|         } | ||||
|         int result = audioManager.requestAudioFocus(audioFocusChangeListener, | ||||
| @@ -1270,7 +1269,7 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|      */ | ||||
|     private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) { | ||||
|         return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, | ||||
|                 useBandwidthMeter ? bandwidthMeter : null, requestHeaders); | ||||
|                 useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -1281,7 +1280,7 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|      * @return A new HttpDataSource factory. | ||||
|      */ | ||||
|     private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { | ||||
|         return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, requestHeaders); | ||||
|         return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); | ||||
|     } | ||||
|  | ||||
|     // AudioBecomingNoisyListener implementation | ||||
| @@ -1486,7 +1485,7 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) { | ||||
|         ExecutorService es = Executors.newSingleThreadExecutor(); | ||||
|         final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); | ||||
|         final Uri sourceUri = this.srcUri; | ||||
|         final Uri sourceUri = source.getUri(); | ||||
|         final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset | ||||
|  | ||||
|         Future<ArrayList<VideoTrack>> result = es.submit(new Callable() { | ||||
| @@ -1729,34 +1728,29 @@ 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, MediaMetadata customMetadata) { | ||||
|  | ||||
|         if (!Util.areEqual(this.customMetadata, customMetadata) && player != null) { | ||||
|             MediaItem currentMediaItem = player.getCurrentMediaItem(); | ||||
|  | ||||
|             if (currentMediaItem != null) { | ||||
|  | ||||
|                 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; | ||||
|     public void setSrc(Source source) { | ||||
|         if (source.getUri() != null) { | ||||
|             clearResumePosition(); | ||||
|             boolean isSourceEqual = source.isEquals(this.source); | ||||
|             hasDrmFailed = false; | ||||
|             this.srcUri = uri; | ||||
|             this.startPositionMs = startPositionMs; | ||||
|             this.cropStartMs = cropStartMs; | ||||
|             this.cropEndMs = cropEndMs; | ||||
|             this.extension = extension; | ||||
|             this.requestHeaders = headers; | ||||
|             this.source = source; | ||||
|             this.mediaDataSourceFactory = | ||||
|                     DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, | ||||
|                             this.requestHeaders); | ||||
|             this.customMetadata = customMetadata; | ||||
|                             source.getHeaders()); | ||||
|  | ||||
|             // refresh custom Metadata | ||||
|             MediaMetadata newCustomMetadata = ConfigurationUtils.buildCustomMetadata(source.getMetadata()); | ||||
|  | ||||
|             // Apply custom metadata is possible | ||||
|             if (player != null && !Util.areEqual(newCustomMetadata, customMetadata)) { | ||||
|                 customMetadata = newCustomMetadata; | ||||
|                 MediaItem currentMediaItem = player.getCurrentMediaItem(); | ||||
|                 if (currentMediaItem != null) { | ||||
|                     MediaItem newMediaItem = currentMediaItem.buildUpon().setMediaMetadata(customMetadata).build(); | ||||
|                     // This will cause video blink/reload but won't louse progress | ||||
|                     player.setMediaItem(newMediaItem, false); | ||||
|                 } | ||||
|             } | ||||
|             if (!isSourceEqual) { | ||||
|                 reloadSource(); | ||||
|             } | ||||
| @@ -1764,19 +1758,13 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|     } | ||||
|  | ||||
|     public void clearSrc() { | ||||
|         if (srcUri != null) { | ||||
|         if (source.getUri() != null) { | ||||
|             if (player != null) { | ||||
|                 player.stop(); | ||||
|                 player.clearMediaItems(); | ||||
|             } | ||||
|             this.srcUri = null; | ||||
|             this.startPositionMs = -1; | ||||
|             this.cropStartMs = -1; | ||||
|             this.cropEndMs = -1; | ||||
|             this.extension = null; | ||||
|             this.requestHeaders = null; | ||||
|             this.source = new Source(); | ||||
|             this.mediaDataSourceFactory = null; | ||||
|             customMetadata = null; | ||||
|             clearResumePosition(); | ||||
|         } | ||||
|     } | ||||
| @@ -1793,22 +1781,9 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|         adTagUrl = uri; | ||||
|     } | ||||
|  | ||||
|     public void setRawSrc(final Uri uri, final String extension) { | ||||
|         if (uri != null) { | ||||
|             boolean isSourceEqual = uri.equals(srcUri); | ||||
|             this.srcUri = uri; | ||||
|             this.extension = extension; | ||||
|             this.mediaDataSourceFactory = buildDataSourceFactory(true); | ||||
|  | ||||
|             if (!isSourceEqual) { | ||||
|                 reloadSource(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setTextTracks(SideLoadedTextTrackList textTracks) { | ||||
|         this.textTracks = textTracks; | ||||
|         reloadSource(); | ||||
|         reloadSource(); // FIXME Shall be moved inside source | ||||
|     } | ||||
|  | ||||
|     private void reloadSource() { | ||||
|   | ||||
| @@ -1,23 +1,20 @@ | ||||
| package com.brentvatne.exoplayer; | ||||
|  | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Color; | ||||
| import android.net.Uri; | ||||
| 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 com.brentvatne.common.api.BufferConfig; | ||||
| import com.brentvatne.common.api.BufferingStrategy; | ||||
| import com.brentvatne.common.api.ControlsConfig; | ||||
| import com.brentvatne.common.api.ResizeMode; | ||||
| import com.brentvatne.common.api.SideLoadedTextTrackList; | ||||
| import com.brentvatne.common.api.Source; | ||||
| import com.brentvatne.common.api.SubtitleStyle; | ||||
| import com.brentvatne.common.react.VideoEventEmitter; | ||||
| import com.brentvatne.common.toolbox.DebugLog; | ||||
| @@ -29,7 +26,6 @@ import com.facebook.react.uimanager.ThemedReactContext; | ||||
| import com.facebook.react.uimanager.ViewGroupManager; | ||||
| import com.facebook.react.uimanager.annotations.ReactProp; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Map; | ||||
| import java.util.UUID; | ||||
| @@ -41,18 +37,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi | ||||
|     private static final String TAG = "ExoViewManager"; | ||||
|     private static final String REACT_CLASS = "RCTVideo"; | ||||
|     private static final String PROP_SRC = "src"; | ||||
|     private static final String PROP_SRC_URI = "uri"; | ||||
|     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"; | ||||
|     private static final String PROP_DRM_TYPE = "type"; | ||||
|     private static final String PROP_DRM_LICENSE_SERVER = "licenseServer"; | ||||
|     private static final String PROP_DRM_HEADERS = "headers"; | ||||
|     private static final String PROP_SRC_HEADERS = "requestHeaders"; | ||||
|     private static final String PROP_RESIZE_MODE = "resizeMode"; | ||||
|     private static final String PROP_REPEAT = "repeat"; | ||||
|     private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack"; | ||||
| @@ -93,6 +82,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi | ||||
|     private static final String PROP_DEBUG = "debug"; | ||||
|     private static final String PROP_CONTROLS_STYLES = "controlsStyles"; | ||||
|  | ||||
|  | ||||
|     private final ReactExoplayerConfig config; | ||||
|  | ||||
|     public ReactExoplayerViewManager(ReactExoplayerConfig config) { | ||||
| @@ -154,85 +144,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi | ||||
|     @ReactProp(name = PROP_SRC) | ||||
|     public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { | ||||
|         Context context = videoView.getContext().getApplicationContext(); | ||||
|         String uriString = ReactBridgeUtils.safeGetString(src, PROP_SRC_URI, null); | ||||
|         int startPositionMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_START_POSITION, -1); | ||||
|         int cropStartMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_CROP_START, -1); | ||||
|         int cropEndMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_CROP_END, -1); | ||||
|         String extension = ReactBridgeUtils.safeGetString(src, PROP_SRC_TYPE, null); | ||||
|  | ||||
|         Map<String, String> headers = new HashMap<>(); | ||||
|         ReadableArray propSrcHeadersArray = ReactBridgeUtils.safeGetArray(src, PROP_SRC_HEADERS); | ||||
|         if (propSrcHeadersArray != null) { | ||||
|             if (propSrcHeadersArray.size() > 0) { | ||||
|                 for (int i = 0; i < propSrcHeadersArray.size(); i++) { | ||||
|                     ReadableMap current = propSrcHeadersArray.getMap(i); | ||||
|                     String key = current.hasKey("key") ? current.getString("key") : null; | ||||
|                     String value = current.hasKey("value") ? current.getString("value") : null; | ||||
|                     if (key != null && value != null) { | ||||
|                         headers.put(key, value); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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(TAG, "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)) { | ||||
|         Source source = Source.parse(src, context); | ||||
|         if (source.getUri() == null) { | ||||
|             videoView.clearSrc(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (startsWithValidScheme(uriString)) { | ||||
|             Uri srcUri = Uri.parse(uriString); | ||||
|  | ||||
|             if (srcUri != null) { | ||||
|                 videoView.setSrc(srcUri, startPositionMs, cropStartMs, cropEndMs, extension, headers, customMetadata); | ||||
|             } | ||||
|         } else { | ||||
|             Resources resources = context.getResources(); | ||||
|             String packageName = context.getPackageName(); | ||||
|             int identifier = resources.getIdentifier( | ||||
|                 uriString, | ||||
|                 "drawable", | ||||
|                 packageName | ||||
|             ); | ||||
|             if (identifier == 0) { | ||||
|                 identifier = resources.getIdentifier( | ||||
|                     uriString, | ||||
|                     "raw", | ||||
|                     packageName | ||||
|                 ); | ||||
|             } | ||||
|             if (identifier > 0) { | ||||
|                 Uri srcUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE).path(Integer.toString(identifier)).build(); | ||||
|                 videoView.setRawSrc(srcUri, extension); | ||||
|             } else { | ||||
|                 videoView.clearSrc(); | ||||
|             } | ||||
|             videoView.setSrc(source); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -462,14 +378,4 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi | ||||
|         ControlsConfig controlsConfig = ControlsConfig.parse(controlsStyles); | ||||
|         videoView.setControlsStyles(controlsConfig); | ||||
|     } | ||||
|  | ||||
|     private boolean startsWithValidScheme(String uriString) { | ||||
|         String lowerCaseUri = uriString.toLowerCase(); | ||||
|         return lowerCaseUri.startsWith("http://") | ||||
|                 || lowerCaseUri.startsWith("https://") | ||||
|                 || lowerCaseUri.startsWith("content://") | ||||
|                 || lowerCaseUri.startsWith("file://") | ||||
|                 || lowerCaseUri.startsWith("rtsp://") | ||||
|                 || lowerCaseUri.startsWith("asset://"); | ||||
|     } | ||||
| } | ||||
| @@ -130,6 +130,12 @@ class VideoPlayer extends Component { | ||||
|       description: 'local file landscape', | ||||
|       uri: require('./broadchurch.mp4'), | ||||
|     }, | ||||
|     { | ||||
|       description: 'local file landscape cropped', | ||||
|       uri: require('./broadchurch.mp4'), | ||||
|       cropStart: 3000, | ||||
|       cropEnd: 10000, | ||||
|     }, | ||||
|     { | ||||
|       description: 'local file portrait', | ||||
|       uri: require('./portrait.mp4'), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user