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:
parent
1b51c15348
commit
bdf3e556d8
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
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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://");
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user