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:
Olivier Bouillet 2024-05-30 08:53:49 +02:00 committed by GitHub
parent 1b51c15348
commit bdf3e556d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 296 additions and 179 deletions

View 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"
)
}
}
}

View File

@ -1,8 +1,10 @@
package com.brentvatne.exoplayer package com.brentvatne.exoplayer
import androidx.media3.common.MediaItem.LiveConfiguration import androidx.media3.common.MediaItem.LiveConfiguration
import androidx.media3.common.MediaMetadata
import com.brentvatne.common.api.BufferConfig import com.brentvatne.common.api.BufferConfig
import com.brentvatne.common.api.BufferConfig.Live import com.brentvatne.common.api.BufferConfig.Live
import com.brentvatne.common.api.Source
/** /**
* Helper functions to create exoplayer configuration * Helper functions to create exoplayer configuration
@ -33,4 +35,22 @@ object ConfigurationUtils {
} }
return liveConfiguration 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
}
} }

View File

@ -113,6 +113,7 @@ import com.brentvatne.common.api.ControlsConfig;
import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrack; import com.brentvatne.common.api.SideLoadedTextTrack;
import com.brentvatne.common.api.SideLoadedTextTrackList; import com.brentvatne.common.api.SideLoadedTextTrackList;
import com.brentvatne.common.api.Source;
import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.api.TimedMetadata; import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.api.Track; import com.brentvatne.common.api.Track;
@ -137,7 +138,6 @@ import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.lang.Math; import java.lang.Math;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
@ -220,11 +220,7 @@ public class ReactExoplayerView extends FrameLayout implements
private ControlsConfig controlsConfig = new ControlsConfig(); private ControlsConfig controlsConfig = new ControlsConfig();
// Props from React // Props from React
private Uri srcUri; private Source source = new Source();
private long startPositionMs = -1;
private long cropStartMs = -1;
private long cropEndMs = -1;
private String extension;
private boolean repeat; private boolean repeat;
private String audioTrackType; private String audioTrackType;
private String audioTrackValue; private String audioTrackValue;
@ -241,7 +237,6 @@ public class ReactExoplayerView extends FrameLayout implements
private boolean preventsDisplaySleepDuringVideoPlayback = true; private boolean preventsDisplaySleepDuringVideoPlayback = true;
private float mProgressUpdateInterval = 250.0f; private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false; private boolean playInBackground = false;
private Map<String, String> requestHeaders;
private boolean mReportBandwidth = false; private boolean mReportBandwidth = false;
private UUID drmUUID = null; private UUID drmUUID = null;
private String drmLicenseUrl = null; private String drmLicenseUrl = null;
@ -324,8 +319,6 @@ public class ReactExoplayerView extends FrameLayout implements
} }
private void createViews() { private void createViews() {
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
@ -652,22 +645,15 @@ public class ReactExoplayerView extends FrameLayout implements
// Initialize core configuration and listeners // Initialize core configuration and listeners
initializePlayerCore(self); initializePlayerCore(self);
} }
if (playerNeedsSource && srcUri != null) { if (playerNeedsSource && source.getUri() != null) {
exoPlayerView.invalidateAspectRatio(); exoPlayerView.invalidateAspectRatio();
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
ExecutorService es = Executors.newSingleThreadExecutor(); ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(() -> { es.execute(() -> {
// DRM initialization must run on a different thread // 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) { 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"); eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001");
return; return;
} }
@ -676,22 +662,24 @@ public class ReactExoplayerView extends FrameLayout implements
activity.runOnUiThread(() -> { activity.runOnUiThread(() -> {
try { try {
// Source initialization must run on the main thread // Source initialization must run on the main thread
initializePlayerSource(self, drmSessionManager); initializePlayerSource();
} catch (Exception ex) { } catch (Exception ex) {
self.playerNeedsSource = true; self.playerNeedsSource = true;
DebugLog.e(TAG, "Failed to initialize Player!"); DebugLog.e(TAG, "Failed to initialize Player!");
DebugLog.e(TAG, ex.toString()); DebugLog.e(TAG, ex.toString());
ex.printStackTrace();
self.eventEmitter.error(ex.toString(), ex, "1001"); self.eventEmitter.error(ex.toString(), ex, "1001");
} }
}); });
}); });
} else if (srcUri != null) { } else if (source.getUri() != null) {
initializePlayerSource(self, null); initializePlayerSource();
} }
} catch (Exception ex) { } catch (Exception ex) {
self.playerNeedsSource = true; self.playerNeedsSource = true;
DebugLog.e(TAG, "Failed to initialize Player!"); DebugLog.e(TAG, "Failed to initialize Player!");
DebugLog.e(TAG, ex.toString()); DebugLog.e(TAG, ex.toString());
ex.printStackTrace();
eventEmitter.error(ex.toString(), ex, "1001"); eventEmitter.error(ex.toString(), ex, "1001");
} }
}; };
@ -782,15 +770,26 @@ public class ReactExoplayerView extends FrameLayout implements
return drmSessionManager; 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(); 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; MediaSource mediaSourceWithAds = null;
if (adTagUrl != null && adsLoader != null) { if (adTagUrl != null && adsLoader != null) {
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView); .setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
DataSpec adTagDataSpec = new DataSpec(adTagUrl); 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 { } else {
if (adTagUrl == null && adsLoader != null) { if (adTagUrl == null && adsLoader != null) {
adsLoader.release(); adsLoader.release();
@ -830,8 +829,8 @@ public class ReactExoplayerView extends FrameLayout implements
if (haveResumePosition) { if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition); player.seekTo(resumeWindow, resumePosition);
player.setMediaSource(mediaSource, false); player.setMediaSource(mediaSource, false);
} else if (startPositionMs > 0) { } else if (source.getStartPositionMs() > 0) {
player.setMediaSource(mediaSource, startPositionMs); player.setMediaSource(mediaSource, source.getStartPositionMs());
} else { } else {
player.setMediaSource(mediaSource, true); player.setMediaSource(mediaSource, true);
} }
@ -1029,14 +1028,14 @@ public class ReactExoplayerView extends FrameLayout implements
); );
break; break;
case CONTENT_TYPE_OTHER: case CONTENT_TYPE_OTHER:
if ("asset".equals(srcUri.getScheme())) { if ("asset".equals(uri.getScheme())) {
try { try {
DataSource.Factory assetDataSourceFactory = buildAssetDataSourceFactory(themedReactContext, srcUri); DataSource.Factory assetDataSourceFactory = buildAssetDataSourceFactory(themedReactContext, uri);
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory); mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
} catch (Exception e) { } 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) { !useCache) {
mediaSourceFactory = new ProgressiveMediaSource.Factory( mediaSourceFactory = new ProgressiveMediaSource.Factory(
mediaDataSourceFactory mediaDataSourceFactory
@ -1193,7 +1192,7 @@ public class ReactExoplayerView extends FrameLayout implements
} }
private boolean requestAudioFocus() { private boolean requestAudioFocus() {
if (disableFocus || srcUri == null || this.hasAudioFocus) { if (disableFocus || source.getUri() == null || this.hasAudioFocus) {
return true; return true;
} }
int result = audioManager.requestAudioFocus(audioFocusChangeListener, int result = audioManager.requestAudioFocus(audioFocusChangeListener,
@ -1270,7 +1269,7 @@ public class ReactExoplayerView extends FrameLayout implements
*/ */
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) { private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, 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. * @return A new HttpDataSource factory.
*/ */
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { 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 // AudioBecomingNoisyListener implementation
@ -1486,7 +1485,7 @@ public class ReactExoplayerView extends FrameLayout implements
private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) { private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) {
ExecutorService es = Executors.newSingleThreadExecutor(); ExecutorService es = Executors.newSingleThreadExecutor();
final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); 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 final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset
Future<ArrayList<VideoTrack>> result = es.submit(new Callable() { Future<ArrayList<VideoTrack>> result = es.submit(new Callable() {
@ -1729,34 +1728,29 @@ public class ReactExoplayerView extends FrameLayout implements
// ReactExoplayerViewManager public api // 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) { public void setSrc(Source source) {
if (source.getUri() != null) {
clearResumePosition();
boolean isSourceEqual = source.isEquals(this.source);
hasDrmFailed = false;
this.source = source;
this.mediaDataSourceFactory =
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
source.getHeaders());
if (!Util.areEqual(this.customMetadata, customMetadata) && player != null) { // 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(); MediaItem currentMediaItem = player.getCurrentMediaItem();
if (currentMediaItem != null) { if (currentMediaItem != null) {
MediaItem newMediaItem = currentMediaItem.buildUpon().setMediaMetadata(customMetadata).build(); MediaItem newMediaItem = currentMediaItem.buildUpon().setMediaMetadata(customMetadata).build();
// This will cause video blink/reload but won't louse progress // This will cause video blink/reload but won't louse progress
player.setMediaItem(newMediaItem, false); player.setMediaItem(newMediaItem, false);
} }
} }
if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri) && cropStartMs == this.cropStartMs && cropEndMs == this.cropEndMs;
hasDrmFailed = false;
this.srcUri = uri;
this.startPositionMs = startPositionMs;
this.cropStartMs = cropStartMs;
this.cropEndMs = cropEndMs;
this.extension = extension;
this.requestHeaders = headers;
this.mediaDataSourceFactory =
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
this.requestHeaders);
this.customMetadata = customMetadata;
if (!isSourceEqual) { if (!isSourceEqual) {
reloadSource(); reloadSource();
} }
@ -1764,19 +1758,13 @@ public class ReactExoplayerView extends FrameLayout implements
} }
public void clearSrc() { public void clearSrc() {
if (srcUri != null) { if (source.getUri() != null) {
if (player != null) { if (player != null) {
player.stop(); player.stop();
player.clearMediaItems(); player.clearMediaItems();
} }
this.srcUri = null; this.source = new Source();
this.startPositionMs = -1;
this.cropStartMs = -1;
this.cropEndMs = -1;
this.extension = null;
this.requestHeaders = null;
this.mediaDataSourceFactory = null; this.mediaDataSourceFactory = null;
customMetadata = null;
clearResumePosition(); clearResumePosition();
} }
} }
@ -1793,22 +1781,9 @@ public class ReactExoplayerView extends FrameLayout implements
adTagUrl = uri; 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) { public void setTextTracks(SideLoadedTextTrackList textTracks) {
this.textTracks = textTracks; this.textTracks = textTracks;
reloadSource(); reloadSource(); // FIXME Shall be moved inside source
} }
private void reloadSource() { private void reloadSource() {

View File

@ -1,23 +1,20 @@
package com.brentvatne.exoplayer; package com.brentvatne.exoplayer;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.RawResourceDataSource;
import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.BufferConfig;
import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.BufferingStrategy;
import com.brentvatne.common.api.ControlsConfig; import com.brentvatne.common.api.ControlsConfig;
import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrackList; import com.brentvatne.common.api.SideLoadedTextTrackList;
import com.brentvatne.common.api.Source;
import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog; 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.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.HashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -41,18 +37,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String TAG = "ExoViewManager"; private static final String TAG = "ExoViewManager";
private static final String REACT_CLASS = "RCTVideo"; private static final String REACT_CLASS = "RCTVideo";
private static final String PROP_SRC = "src"; 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_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 = "drm";
private static final String PROP_DRM_TYPE = "type"; private static final String PROP_DRM_TYPE = "type";
private static final String PROP_DRM_LICENSE_SERVER = "licenseServer"; private static final String PROP_DRM_LICENSE_SERVER = "licenseServer";
private static final String PROP_DRM_HEADERS = "headers"; 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_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat"; private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack"; 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_DEBUG = "debug";
private static final String PROP_CONTROLS_STYLES = "controlsStyles"; private static final String PROP_CONTROLS_STYLES = "controlsStyles";
private final ReactExoplayerConfig config; private final ReactExoplayerConfig config;
public ReactExoplayerViewManager(ReactExoplayerConfig config) { public ReactExoplayerViewManager(ReactExoplayerConfig config) {
@ -154,85 +144,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
@ReactProp(name = PROP_SRC) @ReactProp(name = PROP_SRC)
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext(); Context context = videoView.getContext().getApplicationContext();
String uriString = ReactBridgeUtils.safeGetString(src, PROP_SRC_URI, null); Source source = Source.parse(src, context);
int startPositionMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_START_POSITION, -1); if (source.getUri() == null) {
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)) {
videoView.clearSrc(); videoView.clearSrc();
return;
}
if (startsWithValidScheme(uriString)) {
Uri srcUri = Uri.parse(uriString);
if (srcUri != null) {
videoView.setSrc(srcUri, startPositionMs, cropStartMs, cropEndMs, extension, headers, customMetadata);
}
} else { } else {
Resources resources = context.getResources(); videoView.setSrc(source);
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();
}
} }
} }
@ -462,14 +378,4 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
ControlsConfig controlsConfig = ControlsConfig.parse(controlsStyles); ControlsConfig controlsConfig = ControlsConfig.parse(controlsStyles);
videoView.setControlsStyles(controlsConfig); 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://");
}
} }

View File

@ -130,6 +130,12 @@ class VideoPlayer extends Component {
description: 'local file landscape', description: 'local file landscape',
uri: require('./broadchurch.mp4'), uri: require('./broadchurch.mp4'),
}, },
{
description: 'local file landscape cropped',
uri: require('./broadchurch.mp4'),
cropStart: 3000,
cropEnd: 10000,
},
{ {
description: 'local file portrait', description: 'local file portrait',
uri: require('./portrait.mp4'), uri: require('./portrait.mp4'),