From 0aceefb39410ef82eec84fe7b6109b1728e79416 Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 26 Apr 2023 08:57:09 +0900 Subject: [PATCH 01/58] feat: :zap: added gitignore From 649b2e944fb37462269c79b190856e2efef50948 Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 26 Apr 2023 08:57:31 +0900 Subject: [PATCH 02/58] feat: :zap: turn on newarch enabled on example app From 5c371bbe59814fa7af7745e489e943a1e6b7d0ba Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 26 Apr 2023 08:58:57 +0900 Subject: [PATCH 03/58] feat: :zap: setup example app new arch From 8782d7aa396cc2ab5c603264b0ba74da6cd5ed12 Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 26 Apr 2023 10:11:17 +0900 Subject: [PATCH 04/58] feat: :zap: setup android example app for fabric android From 6bd248986d8abbdb8c9083dc67f752ca6ff7a1c8 Mon Sep 17 00:00:00 2001 From: yungblud Date: Mon, 1 May 2023 18:09:58 +0900 Subject: [PATCH 05/58] feat: :zap: setup kotlin From d851d56344e57280e9e818d1a925776ca97d15cd Mon Sep 17 00:00:00 2001 From: yungblud Date: Mon, 1 May 2023 18:12:44 +0900 Subject: [PATCH 06/58] feat: :zap: migrate to fabric --- .../exoplayer/ReactExoplayerViewManager.kt | 490 ++++++++++++++++++ .../exoplayer/VideoEventEmitter.java | 281 ++++------ .../events/OnAudioFocusChangedEvent.kt | 22 + .../exoplayer/events/OnAudioTracksEvent.kt | 47 ++ .../events/OnPlaybackRateChangeEvent.kt | 22 + .../events/OnReadyForDisplayEvent.kt | 18 + .../exoplayer/events/OnReceiveAdEventEvent.kt | 21 + .../exoplayer/events/OnTextTracksEvent.kt | 46 ++ .../exoplayer/events/OnTimedMetadataEvent.kt | 49 ++ .../events/OnVideoAudioBecomingNoisyEvent.kt | 18 + .../events/OnVideoBandwidthUpdateEvent.kt | 34 ++ .../exoplayer/events/OnVideoBufferEvent.kt | 22 + .../exoplayer/events/OnVideoEndEvent.kt | 18 + .../exoplayer/events/OnVideoErrorEvent.kt | 46 ++ .../OnVideoFullscreenPlayerDidDismissEvent.kt | 18 + .../OnVideoFullscreenPlayerDidPresentEvent.kt | 18 + ...OnVideoFullscreenPlayerWillDismissEvent.kt | 18 + ...OnVideoFullscreenPlayerWillPresentEvent.kt | 18 + .../exoplayer/events/OnVideoIdleEvent.kt | 18 + .../exoplayer/events/OnVideoLoadEvent.kt | 103 ++++ .../exoplayer/events/OnVideoLoadStartEvent.kt | 18 + .../OnVideoPlaybackStateChangedEvent.kt | 22 + .../exoplayer/events/OnVideoProgressEvent.kt | 69 +++ .../exoplayer/events/OnVideoSeekEvent.kt | 24 + .../exoplayer/events/OnVideoTracksEvent.kt | 47 ++ .../brentvatne/react/ReactVideoPackage.java | 5 +- .../exoplayer/ReactExoplayerViewManager.java | 2 +- src/MyVideo.tsx | 380 ++++++++++++++ src/fabric/VideoNativeComponent.tsx | 273 ++++++++++ src/types/events.ts | 31 ++ src/types/video.ts | 135 +++++ 31 files changed, 2146 insertions(+), 187 deletions(-) create mode 100644 android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnAudioFocusChangedEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnAudioTracksEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnPlaybackRateChangeEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnReadyForDisplayEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnReceiveAdEventEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnTextTracksEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnTimedMetadataEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoAudioBecomingNoisyEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBandwidthUpdateEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBufferEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoEndEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoErrorEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidDismissEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidPresentEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillDismissEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillPresentEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoIdleEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadStartEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoPlaybackStateChangedEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoProgressEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoSeekEvent.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/events/OnVideoTracksEvent.kt rename android/src/{main => oldarch}/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java (99%) create mode 100644 src/MyVideo.tsx create mode 100644 src/fabric/VideoNativeComponent.tsx create mode 100644 src/types/events.ts create mode 100644 src/types/video.ts diff --git a/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt b/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt new file mode 100644 index 0000000000..b056aef2ba --- /dev/null +++ b/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt @@ -0,0 +1,490 @@ +package com.brentvatne.exoplayer; + +import android.net.Uri +import android.text.TextUtils +import com.brentvatne.exoplayer.events.* +import com.facebook.react.bridge.Dynamic +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableMapKeySetIterator +import com.facebook.react.common.MapBuilder +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.RNCVideoManagerDelegate +import com.facebook.react.viewmanagers.RNCVideoManagerInterface +import com.google.android.exoplayer2.DefaultLoadControl +import com.google.android.exoplayer2.upstream.RawResourceDataSource +import com.google.android.exoplayer2.util.Util +import java.util.* +import javax.annotation.Nullable + + +@ReactModule(name = "RNCVideo") +internal class ReactExoplayerViewManager() : ViewGroupManager(), RNCVideoManagerInterface { + private var config: ReactExoplayerConfig? = null + + private val mDelegate: ViewManagerDelegate = RNCVideoManagerDelegate(this) + + private val REACT_CLASS = "RNCVideo" + + private val PROP_SRC_URI = "uri" + private val PROP_SRC_START_TIME = "startTime" + private val PROP_SRC_END_TIME = "endTime" + private val PROP_SRC_TYPE = "type" + private val PROP_SRC_HEADERS = "requestHeaders" + + private val PROP_DRM_TYPE = "type" + private val PROP_DRM_LICENSESERVER = "licenseServer" + private val PROP_DRM_HEADERS = "headers" + + private val PROP_SELECTED_TEXT_TRACK_TYPE = "type" + private val PROP_SELECTED_TEXT_TRACK_VALUE = "value" + + private val PROP_SELECTED_AUDIO_TRACK_TYPE = "type" + private val PROP_SELECTED_AUDIO_TRACK_VALUE = "value" + + private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs" + private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs" + private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs" + private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs" + private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent" + private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent" + private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent" + + private val PROP_SELECTED_VIDEO_TRACK_TYPE = "type" + private val PROP_SELECTED_VIDEO_TRACK_VALUE = "value" + + constructor(config: ReactExoplayerConfig) : this() { + this.config = config + } + + override fun getName(): String { + return this.REACT_CLASS + } + + override fun createViewInstance(p0: ThemedReactContext): ReactExoplayerView { + val view = ReactExoplayerView(p0, config) + return view + } + + override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { + return mutableMapOf( + OnVideoLoadEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoLoad"), + OnVideoProgressEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoProgress"), + OnVideoLoadStartEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoLoadStart"), + OnAudioTracksEvent.EVENT_NAME to MapBuilder.of("registrationName", "onAudioTracks"), + OnTextTracksEvent.EVENT_NAME to MapBuilder.of("registrationName", "onTextTracks"), + OnVideoTracksEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoTracks"), + OnVideoBandwidthUpdateEvent.EVENT_NAME to MapBuilder.of("registrationName", "onBandwidthUpdate"), + OnVideoSeekEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoSeek"), + OnReadyForDisplayEvent.EVENT_NAME to MapBuilder.of("registrationName", "onReadyForDisplay"), + OnVideoBufferEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoBuffer"), + OnVideoPlaybackStateChangedEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoPlaybackStateChanged"), + OnVideoIdleEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoIdle"), + OnVideoEndEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoEnd"), + OnVideoFullscreenPlayerWillPresentEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoFullscreenPlayerWillPresent"), + OnVideoFullscreenPlayerDidPresentEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoFullscreenPlayerDidPresent"), + OnVideoFullscreenPlayerWillDismissEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoFullscreenPlayerWillDismiss"), + OnVideoFullscreenPlayerDidDismissEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoFullscreenPlayerDidDismiss"), + OnVideoErrorEvent.EVENT_NAME to MapBuilder.of("registrationName", "onVideoError"), + OnPlaybackRateChangeEvent.EVENT_NAME to MapBuilder.of("registrationName", "onPlaybackRateChange"), + OnTimedMetadataEvent.EVENT_NAME to MapBuilder.of("registrationName", "onTimedMetadata"), + OnAudioFocusChangedEvent.EVENT_NAME to MapBuilder.of("registrationName", "onAudioFocusChanged"), + OnVideoAudioBecomingNoisyEvent.EVENT_NAME to MapBuilder.of("registrationName", "onAudioBecomingNoisy"), + OnReceiveAdEventEvent.EVENT_NAME to MapBuilder.of("registrationName", "onReceiveAdEvent"), + ) + } + + override fun receiveCommand(root: ReactExoplayerView, commandId: String?, args: ReadableArray?) { + mDelegate.receiveCommand(root, commandId, args) + } + + @ReactProp(name = "src") + override fun setSrc(view: ReactExoplayerView?, src: ReadableMap?) { + if (view != null && src != null) { + val uriString: String? = if (src.hasKey(PROP_SRC_URI)) src.getString(PROP_SRC_URI) else null + val startTimeMs = if (src.hasKey(PROP_SRC_START_TIME)) src.getInt(PROP_SRC_START_TIME) else -1 + val endTimeMs = if (src.hasKey(PROP_SRC_END_TIME)) src.getInt(PROP_SRC_END_TIME) else -1 + val extension: String? = if (src.hasKey(PROP_SRC_TYPE)) src.getString(PROP_SRC_TYPE) else null + val headers: MutableMap = mutableMapOf() + val propSrcHeadersArray = if (src.hasKey(PROP_SRC_HEADERS)) src.getArray(PROP_SRC_HEADERS) else null + propSrcHeadersArray?.let { + if (it.size() > 0) { + for (i in 0 until it.size()) { + val current = it.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) { + headers.put(key, value) + } + } + } + } + if (TextUtils.isEmpty(uriString)) { + view.clearSrc(); + return; + } + if (startsWithValidScheme(uriString ?: "")) { + val srcUri: Uri = Uri.parse(uriString) + if (srcUri != null) { + view.setSrc(srcUri, startTimeMs.toLong(), endTimeMs.toLong(), extension, headers) + } + } else { + val context = view.context + var identifier = context.getResources().getIdentifier( + uriString, + "drawable", + context.getPackageName() + ); + if (identifier == 0) { + identifier = context.getResources().getIdentifier( + uriString, + "raw", + context.getPackageName() + ); + } + if (identifier > 0) { + val srcUri = RawResourceDataSource.buildRawResourceUri(identifier); + if (srcUri != null) { + view.setRawSrc(srcUri, extension); + } + } else { + view.clearSrc(); + } + } + } + } + + @ReactProp(name = "drm") + override fun setDrm(view: ReactExoplayerView?, drm: ReadableMap?) { + if (view != null && drm != null && drm.hasKey(PROP_DRM_TYPE)) { + val drmType: String? = if (drm.hasKey(PROP_DRM_TYPE)) drm.getString(PROP_DRM_TYPE) else null + val drmLicenseServer: String? = if (drm.hasKey(PROP_DRM_LICENSESERVER)) drm.getString(PROP_DRM_LICENSESERVER) else null + val drmHeaders: ReadableMap? = if (drm.hasKey(PROP_DRM_HEADERS)) drm.getMap(PROP_DRM_HEADERS) else null + if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { + val drmUUID: UUID? = Util.getDrmUuid(drmType) + view.setDrmType(drmUUID) + view.setDrmLicenseUrl(drmLicenseServer) + if (drmHeaders != null) { + val drmKeyRequestPropertiesList: ArrayList = ArrayList() + val itr: ReadableMapKeySetIterator = drmHeaders.keySetIterator() + while (itr.hasNextKey()) { + val key: String = itr.nextKey() + drmKeyRequestPropertiesList.add(key) + drmKeyRequestPropertiesList.add(drmHeaders.getString(key)) + } + view.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(arrayOfNulls(0))) + } + view.setUseTextureView(false) + } + } + } + + @ReactProp(name = "adTagUrl") + override fun setAdTagUrl(view: ReactExoplayerView?, uriString: String?) { + if (view != null && uriString != null) { + if (TextUtils.isEmpty(uriString)) { + return + } + val adTagUrl = Uri.parse(uriString) + view.setAdTagUrl(adTagUrl) + } + } + + override fun setAllowsExternalPlayback(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + @ReactProp(name = "maxBitRate") + override fun setMaxBitRate(view: ReactExoplayerView?, maxBitRate: Float) { + view?.setMaxBitRateModifier(maxBitRate.toInt()); + } + + @ReactProp(name = "resizeMode") + override fun setResizeMode(view: ReactExoplayerView?, resizeModeOrdinalString: String?) { + if (view != null && resizeModeOrdinalString != null) { + view.setResizeModeModifier(convertToIntDef(resizeModeOrdinalString)); + } + } + + @ReactProp(name = "repeat", defaultBoolean = false) + override fun setRepeat(view: ReactExoplayerView?, repeat: Boolean) { + view?.setRepeatModifier(repeat) + } + + override fun setAutomaticallyWaitsToMinimizeStalling(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + @ReactProp(name = "textTracks") + override fun setTextTracks(view: ReactExoplayerView?, textTracks: ReadableArray?) { + view?.setTextTracks(textTracks); + } + + @ReactProp(name = "selectedTextTrack") + override fun setSelectedTextTrack(view: ReactExoplayerView?, selectedTextTrack: ReadableMap?) { + var typeString: String? = null + var value: Dynamic? = null + if (selectedTextTrack != null) { + typeString = if (selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)) selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) else null + value = if (selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)) selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) else null + } + view?.setSelectedTextTrack(typeString, value) + } + + @ReactProp(name = "selectedAudioTrack") + override fun setSelectedAudioTrack(view: ReactExoplayerView?, selectedAudioTrack: ReadableMap?) { + var typeString: String? = null + var value: Dynamic? = null + if (selectedAudioTrack != null) { + typeString = if (selectedAudioTrack.hasKey(PROP_SELECTED_AUDIO_TRACK_TYPE)) selectedAudioTrack.getString(PROP_SELECTED_AUDIO_TRACK_TYPE) else null + value = if (selectedAudioTrack.hasKey(PROP_SELECTED_AUDIO_TRACK_VALUE)) selectedAudioTrack.getDynamic(PROP_SELECTED_AUDIO_TRACK_VALUE) else null + } + view?.setSelectedAudioTrack(typeString, value) + } + + @ReactProp(name = "paused", defaultBoolean = false) + override fun setPaused(view: ReactExoplayerView?, paused: Boolean) { + view?.setPausedModifier(paused) + } + + @ReactProp(name = "muted", defaultBoolean = false) + override fun setMuted(view: ReactExoplayerView?, muted: Boolean) { + view?.setMutedModifier(muted); + } + + @ReactProp(name = "controls", defaultBoolean = false) + override fun setControls(view: ReactExoplayerView?, controls: Boolean) { + view?.setControls(controls) + } + + override fun setFilter(view: ReactExoplayerView?, value: String?) { + // do nothing, ios only + } + + override fun setFilterEnabled(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + @ReactProp(name = "volume", defaultFloat = 1.0f) + override fun setVolume(view: ReactExoplayerView?, volume: Float) { + view?.setVolumeModifier(volume); + } + + @ReactProp(name = "playInBackground", defaultBoolean = false) + override fun setPlayInBackground(view: ReactExoplayerView?, playInBackground: Boolean) { + view?.setPlayInBackground(playInBackground); + } + + @ReactProp(name = "preventsDisplaySleepDuringVideoPlayback", defaultBoolean = false) + override fun setPreventsDisplaySleepDuringVideoPlayback(view: ReactExoplayerView?, preventsSleep: Boolean) { + view?.setPreventsDisplaySleepDuringVideoPlayback(preventsSleep); + } + + override fun setPreferredForwardBufferDuration(view: ReactExoplayerView?, value: Float) { + // do nothing, ios only + } + + override fun setPlayWhenInactive(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + override fun setPictureInPicture(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + override fun setIgnoreSilentSwitch(view: ReactExoplayerView?, value: String?) { + // do nothing, ios only + } + + override fun setMixWithOthers(view: ReactExoplayerView?, value: String?) { + // do nothing, ios only + } + + @ReactProp(name = "rate") + override fun setRate(view: ReactExoplayerView?, rate: Float) { + view?.setRateModifier(rate) + } + + @ReactProp(name = "fullscreen", defaultBoolean = false) + override fun setFullscreen(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + override fun setFullscreenAutorotate(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + override fun setFullscreenOrientation(view: ReactExoplayerView?, value: String?) { + // do nothing, ios only + } + + @ReactProp(name = "progressUpdateInterval", defaultFloat = 250.0f) + override fun setProgressUpdateInterval(view: ReactExoplayerView?, progressUpdateInterval: Float) { + view?.setProgressUpdateInterval(progressUpdateInterval); + } + + override fun setRestoreUserInterfaceForPIPStopCompletionHandler(view: ReactExoplayerView?, value: Boolean) { + // do nothing, ios only + } + + override fun setLocalSourceEncryptionKeyScheme(view: ReactExoplayerView?, value: String?) { + // do nothing, ios only + } + + override fun save(view: ReactExoplayerView?) { + // do nothing, ios only + } + + override fun seek(view: ReactExoplayerView?, time: Float, tolerance: Float) { + // todo: what is tolerance for? + val to = Math.round(time * 1000f) + view?.seekTo(to.toLong()); + } + + override fun setLicenseResult(view: ReactExoplayerView?, result: String?) { + // do nothing, ios only + } + + override fun setLicenseResultError(view: ReactExoplayerView?, error: String?) { + // do nothing, ios only + } + + private fun startsWithValidScheme(uriString: String): Boolean { + val lowerCaseUri = uriString.lowercase(Locale.getDefault()) + return (lowerCaseUri.startsWith("http://") + || lowerCaseUri.startsWith("https://") + || lowerCaseUri.startsWith("content://") + || lowerCaseUri.startsWith("file://") + || lowerCaseUri.startsWith("asset://")) + } + + /** + * toStringMap converts a [ReadableMap] into a HashMap. + * + * @param readableMap The ReadableMap to be conveted. + * @return A HashMap containing the data that was in the ReadableMap. + * @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java' + */ + fun toStringMap(@Nullable readableMap: ReadableMap?): Map? { + if (readableMap == null) return null + val iterator = readableMap.keySetIterator() + if (!iterator.hasNextKey()) return null + val result: MutableMap = HashMap() + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + result[key] = readableMap.getString(key) + } + return result + } + + @ResizeMode.Mode + private fun convertToIntDef(resizeModeOrdinalString: String): Int { + if (!TextUtils.isEmpty(resizeModeOrdinalString)) { + if (resizeModeOrdinalString == "none") { + return ResizeMode.toResizeMode(ResizeMode.RESIZE_MODE_FIT) + } else if (resizeModeOrdinalString == "contain") { + return ResizeMode.toResizeMode(ResizeMode.RESIZE_MODE_FIT) + } else if (resizeModeOrdinalString == "cover") { + return ResizeMode.toResizeMode(ResizeMode.RESIZE_MODE_CENTER_CROP) + } else if (resizeModeOrdinalString == "stretch") { + return ResizeMode.toResizeMode(ResizeMode.RESIZE_MODE_FILL) + } + } + return ResizeMode.RESIZE_MODE_FIT + } + + @ReactProp(name = "backBufferDurationMs", defaultInt = 0) + override fun setBackBufferDurationMs(view: ReactExoplayerView?, value: Int) { + view?.setBackBufferDurationMs(value) + } + + @ReactProp(name = "bufferConfig") + override fun setBufferConfig(view: ReactExoplayerView?, bufferConfig: ReadableMap?) { + var minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS + var maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS + var bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS + var bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS + var maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT + var minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE + var minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE + + if (bufferConfig != null) { + minBufferMs = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MS)) bufferConfig.getInt(PROP_BUFFER_CONFIG_MIN_BUFFER_MS) else minBufferMs + maxBufferMs = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_BUFFER_MS)) bufferConfig.getInt(PROP_BUFFER_CONFIG_MAX_BUFFER_MS) else maxBufferMs + bufferForPlaybackMs = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS)) bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS) else bufferForPlaybackMs + bufferForPlaybackAfterRebufferMs = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)) bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) else bufferForPlaybackAfterRebufferMs + maxHeapAllocationPercent = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT)) bufferConfig.getDouble(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT) else maxHeapAllocationPercent + minBackBufferMemoryReservePercent = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT)) bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT) else minBackBufferMemoryReservePercent + minBufferMemoryReservePercent = if (bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT)) bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT) else minBufferMemoryReservePercent + view?.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent) + } + } + + @ReactProp(name = "contentStartTime", defaultInt = -1) + override fun setContentStartTime(view: ReactExoplayerView?, value: Int) { + view?.setContentStartTime(value) + } + + override fun setCurrentPlaybackTime(view: ReactExoplayerView?, value: Double) { + // do nothing + } + + @ReactProp(name = "disableDisconnectError", defaultBoolean = false) + override fun setDisableDisconnectError(view: ReactExoplayerView?, value: Boolean) { + view?.setDisableDisconnectError(value) + } + + @ReactProp(name = "focusable", defaultBoolean = true) + override fun setFocusable(view: ReactExoplayerView?, value: Boolean) { + view?.setFocusable(value) + } + + @ReactProp(name = "hideShutterView", defaultBoolean = false) + override fun setHideShutterView(view: ReactExoplayerView?, value: Boolean) { + view?.setHideShutterView(value) + } + + @ReactProp(name = "minLoadRetryCount") + override fun setMinLoadRetryCount(view: ReactExoplayerView?, value: Int) { + view?.setMinLoadRetryCountModifier(value) + } + + @ReactProp(name = "reportBandwidth", defaultBoolean = false) + override fun setReportBandwidth(view: ReactExoplayerView?, value: Boolean) { + view?.setReportBandwidth(value) + } + + @ReactProp(name = "selectedVideoTrack") + override fun setSelectedVideoTrack(view: ReactExoplayerView?, selectedVideoTrack: ReadableMap?) { + var typeString: String? = null + var value: Dynamic? = null + if (selectedVideoTrack != null) { + typeString = if (selectedVideoTrack.hasKey(PROP_SELECTED_VIDEO_TRACK_TYPE)) selectedVideoTrack.getString(PROP_SELECTED_VIDEO_TRACK_TYPE) else null + value = if (selectedVideoTrack.hasKey(PROP_SELECTED_VIDEO_TRACK_VALUE)) selectedVideoTrack.getDynamic(PROP_SELECTED_VIDEO_TRACK_VALUE) else null + } + view?.setSelectedVideoTrack(typeString, value) + } + + @ReactProp(name = "subtitleStyle") + override fun setSubtitleStyle(view: ReactExoplayerView?, subtitleStyle: ReadableMap?) { + view?.setSubtitleStyle(SubtitleStyle.parse(subtitleStyle)); + } + + override fun setTrackId(view: ReactExoplayerView?, value: String?) { + // do nothing + } + + @ReactProp(name = "useTextureView", defaultBoolean = true) + override fun setUseTextureView(view: ReactExoplayerView?, value: Boolean) { + view?.setUseTextureView(value) + } + + @ReactProp(name = "useSecureView", defaultBoolean = true) + override fun setUseSecureView(view: ReactExoplayerView?, value: Boolean) { + view?.useSecureView(value) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index 24d8eafb07..6c750138a6 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -5,20 +5,40 @@ import com.brentvatne.common.Track; import com.brentvatne.common.VideoTrack; +import com.brentvatne.exoplayer.events.OnAudioFocusChangedEvent; +import com.brentvatne.exoplayer.events.OnAudioTracksEvent; +import com.brentvatne.exoplayer.events.OnPlaybackRateChangeEvent; +import com.brentvatne.exoplayer.events.OnReadyForDisplayEvent; +import com.brentvatne.exoplayer.events.OnReceiveAdEventEvent; +import com.brentvatne.exoplayer.events.OnTextTracksEvent; +import com.brentvatne.exoplayer.events.OnTimedMetadataEvent; +import com.brentvatne.exoplayer.events.OnVideoAudioBecomingNoisyEvent; +import com.brentvatne.exoplayer.events.OnVideoBandwidthUpdateEvent; +import com.brentvatne.exoplayer.events.OnVideoBufferEvent; +import com.brentvatne.exoplayer.events.OnVideoEndEvent; +import com.brentvatne.exoplayer.events.OnVideoErrorEvent; +import com.brentvatne.exoplayer.events.OnVideoFullscreenPlayerDidDismissEvent; +import com.brentvatne.exoplayer.events.OnVideoFullscreenPlayerDidPresentEvent; +import com.brentvatne.exoplayer.events.OnVideoFullscreenPlayerWillDismissEvent; +import com.brentvatne.exoplayer.events.OnVideoFullscreenPlayerWillPresentEvent; +import com.brentvatne.exoplayer.events.OnVideoIdleEvent; +import com.brentvatne.exoplayer.events.OnVideoLoadEvent; +import com.brentvatne.exoplayer.events.OnVideoLoadStartEvent; +import com.brentvatne.exoplayer.events.OnVideoPlaybackStateChangedEvent; +import com.brentvatne.exoplayer.events.OnVideoProgressEvent; +import com.brentvatne.exoplayer.events.OnVideoSeekEvent; +import com.brentvatne.exoplayer.events.OnVideoTracksEvent; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.RCTEventEmitter; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.emsg.EventMessage; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.io.StringWriter; -import java.io.PrintWriter; import java.util.ArrayList; class VideoEventEmitter { @@ -26,9 +46,11 @@ class VideoEventEmitter { private final RCTEventEmitter eventEmitter; private int viewId = View.NO_ID; + private ReactContext context = null; VideoEventEmitter(ReactContext reactContext) { this.eventEmitter = reactContext.getJSModule(RCTEventEmitter.class); + this.context = reactContext; } private static final String EVENT_LOAD_START = "onVideoLoadStart"; @@ -117,65 +139,14 @@ class VideoEventEmitter { @interface VideoEvents { } - private static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward"; - private static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward"; - private static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse"; - private static final String EVENT_PROP_REVERSE = "canPlayReverse"; - private static final String EVENT_PROP_STEP_FORWARD = "canStepForward"; - private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward"; - - private static final String EVENT_PROP_BUFFER_START = "bufferStart"; - private static final String EVENT_PROP_BUFFER_END = "bufferEnd"; - private static final String EVENT_PROP_DURATION = "duration"; - private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration"; - private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration"; - private static final String EVENT_PROP_CURRENT_TIME = "currentTime"; - private static final String EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime"; - private static final String EVENT_PROP_SEEK_TIME = "seekTime"; - private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize"; - private static final String EVENT_PROP_TRACK_ID = "trackId"; - private static final String EVENT_PROP_WIDTH = "width"; - private static final String EVENT_PROP_HEIGHT = "height"; - private static final String EVENT_PROP_ORIENTATION = "orientation"; - private static final String EVENT_PROP_VIDEO_TRACKS = "videoTracks"; - private static final String EVENT_PROP_AUDIO_TRACKS = "audioTracks"; - private static final String EVENT_PROP_TEXT_TRACKS = "textTracks"; - private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus"; - private static final String EVENT_PROP_IS_BUFFERING = "isBuffering"; - private static final String EVENT_PROP_PLAYBACK_RATE = "playbackRate"; - - private static final String EVENT_PROP_ERROR = "error"; - private static final String EVENT_PROP_ERROR_STRING = "errorString"; - private static final String EVENT_PROP_ERROR_EXCEPTION = "errorException"; - private static final String EVENT_PROP_ERROR_TRACE = "errorStackTrace"; - private static final String EVENT_PROP_ERROR_CODE = "errorCode"; - - private static final String EVENT_PROP_TIMED_METADATA = "metadata"; - - private static final String EVENT_PROP_BITRATE = "bitrate"; - - private static final String EVENT_PROP_IS_PLAYING = "isPlaying"; - void setViewId(int viewId) { this.viewId = viewId; } void loadStart() { - receiveEvent(EVENT_LOAD_START, null); - } - - WritableMap aspectRatioToNaturalSize(int videoWidth, int videoHeight) { - WritableMap naturalSize = Arguments.createMap(); - naturalSize.putInt(EVENT_PROP_WIDTH, videoWidth); - naturalSize.putInt(EVENT_PROP_HEIGHT, videoHeight); - if (videoWidth > videoHeight) { - naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape"); - } else if (videoWidth < videoHeight) { - naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait"); - } else { - naturalSize.putString(EVENT_PROP_ORIENTATION, "square"); - } - return naturalSize; + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoLoadStartEvent(viewId)); } WritableArray audioTracksToArray(ArrayList audioTracks) { @@ -237,37 +208,17 @@ public void load(double duration, double currentPosition, int videoWidth, int vi WritableArray waVideoTracks = videoTracksToArray(videoTracks); WritableArray waTextTracks = textTracksToArray(textTracks); - load( duration, currentPosition, videoWidth, videoHeight, waAudioTracks, waTextTracks, waVideoTracks, trackId); + load(duration, currentPosition, videoWidth, videoHeight, waAudioTracks, waTextTracks, waVideoTracks, trackId); } private void load(double duration, double currentPosition, int videoWidth, int videoHeight, WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks, String trackId) { - WritableMap event = Arguments.createMap(); - event.putDouble(EVENT_PROP_DURATION, duration / 1000D); - event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D); - - WritableMap naturalSize = aspectRatioToNaturalSize(videoWidth, videoHeight); - event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize); - event.putString(EVENT_PROP_TRACK_ID, trackId); - event.putArray(EVENT_PROP_VIDEO_TRACKS, videoTracks); - event.putArray(EVENT_PROP_AUDIO_TRACKS, audioTracks); - event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks); - - // TODO: Actually check if you can. - event.putBoolean(EVENT_PROP_FAST_FORWARD, true); - event.putBoolean(EVENT_PROP_SLOW_FORWARD, true); - event.putBoolean(EVENT_PROP_SLOW_REVERSE, true); - event.putBoolean(EVENT_PROP_REVERSE, true); - event.putBoolean(EVENT_PROP_FAST_FORWARD, true); - event.putBoolean(EVENT_PROP_STEP_BACKWARD, true); - event.putBoolean(EVENT_PROP_STEP_FORWARD, true); - - receiveEvent(EVENT_LOAD, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoLoadEvent(viewId, duration, currentPosition, videoWidth, videoHeight, audioTracks, textTracks, videoTracks, trackId)); } - - WritableMap arrayToObject(String field, WritableArray array) { WritableMap event = Arguments.createMap(); event.putArray(field, array); @@ -275,80 +226,93 @@ WritableMap arrayToObject(String field, WritableArray array) { } public void audioTracks(ArrayList audioTracks){ - receiveEvent(EVENT_AUDIO_TRACKS, arrayToObject(EVENT_PROP_AUDIO_TRACKS, audioTracksToArray(audioTracks))); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnAudioTracksEvent(viewId, audioTracks)); } public void textTracks(ArrayList textTracks){ - receiveEvent(EVENT_TEXT_TRACKS, arrayToObject(EVENT_PROP_TEXT_TRACKS, textTracksToArray(textTracks))); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnTextTracksEvent(viewId, textTracks)); } public void videoTracks(ArrayList videoTracks){ - receiveEvent(EVENT_VIDEO_TRACKS, arrayToObject(EVENT_PROP_VIDEO_TRACKS, videoTracksToArray(videoTracks))); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoTracksEvent(viewId, videoTracks)); } void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration, double currentPlaybackTime) { - WritableMap event = Arguments.createMap(); - event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D); - event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D); - event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000D); - event.putDouble(EVENT_PROP_CURRENT_PLAYBACK_TIME, currentPlaybackTime); - receiveEvent(EVENT_PROGRESS, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoProgressEvent(viewId, currentPosition, bufferedDuration, seekableDuration, currentPlaybackTime)); } void bandwidthReport(double bitRateEstimate, int height, int width, String id) { - WritableMap event = Arguments.createMap(); - event.putDouble(EVENT_PROP_BITRATE, bitRateEstimate); - event.putInt(EVENT_PROP_WIDTH, width); - event.putInt(EVENT_PROP_HEIGHT, height); - event.putString(EVENT_PROP_TRACK_ID, id); - receiveEvent(EVENT_BANDWIDTH, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoBandwidthUpdateEvent(viewId, bitRateEstimate, height, width, id)); } void seek(long currentPosition, long seekTime) { - WritableMap event = Arguments.createMap(); - event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D); - event.putDouble(EVENT_PROP_SEEK_TIME, seekTime / 1000D); - receiveEvent(EVENT_SEEK, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoSeekEvent(viewId, currentPosition, seekTime)); } void ready() { - receiveEvent(EVENT_READY, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnReadyForDisplayEvent(viewId)); } void buffering(boolean isBuffering) { - WritableMap map = Arguments.createMap(); - map.putBoolean(EVENT_PROP_IS_BUFFERING, isBuffering); - receiveEvent(EVENT_BUFFER, map); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoBufferEvent(viewId, isBuffering)); } void playbackStateChanged(boolean isPlaying) { - WritableMap map = Arguments.createMap(); - map.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying); - receiveEvent(EVENT_PLAYBACK_STATE_CHANGED, map); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoPlaybackStateChangedEvent(viewId, isPlaying)); } void idle() { - receiveEvent(EVENT_IDLE, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoIdleEvent(viewId)); } void end() { - receiveEvent(EVENT_END, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoEndEvent(viewId)); } void fullscreenWillPresent() { - receiveEvent(EVENT_FULLSCREEN_WILL_PRESENT, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoFullscreenPlayerWillPresentEvent(viewId)); } void fullscreenDidPresent() { - receiveEvent(EVENT_FULLSCREEN_DID_PRESENT, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoFullscreenPlayerDidPresentEvent(viewId)); } void fullscreenWillDismiss() { - receiveEvent(EVENT_FULLSCREEN_WILL_DISMISS, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoFullscreenPlayerWillDismissEvent(viewId)); } void fullscreenDidDismiss() { - receiveEvent(EVENT_FULLSCREEN_DID_DISMISS, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoFullscreenPlayerDidDismissEvent(viewId)); } void error(String errorString, Exception exception) { @@ -360,89 +324,38 @@ void error(String errorString, Exception exception, String errorCode) { } void _error(String errorString, Exception exception, String errorCode) { - // Prepare stack trace - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - exception.printStackTrace(pw); - String stackTrace = sw.toString(); - - WritableMap error = Arguments.createMap(); - error.putString(EVENT_PROP_ERROR_STRING, errorString); - error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString()); - error.putString(EVENT_PROP_ERROR_CODE, errorCode); - error.putString(EVENT_PROP_ERROR_TRACE, stackTrace); - WritableMap event = Arguments.createMap(); - event.putMap(EVENT_PROP_ERROR, error); - receiveEvent(EVENT_ERROR, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoErrorEvent(viewId, errorString, exception, errorCode)); } void playbackRateChange(float rate) { - WritableMap map = Arguments.createMap(); - map.putDouble(EVENT_PROP_PLAYBACK_RATE, (double)rate); - receiveEvent(EVENT_PLAYBACK_RATE_CHANGE, map); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnPlaybackRateChangeEvent(viewId, rate)); } void timedMetadata(Metadata metadata) { - WritableArray metadataArray = Arguments.createArray(); - - for (int i = 0; i < metadata.length(); i++) { - - Metadata.Entry entry = metadata.get(i); - - if (entry instanceof Id3Frame) { - - Id3Frame frame = (Id3Frame) entry; - - String value = ""; - - if (frame instanceof TextInformationFrame) { - TextInformationFrame txxxFrame = (TextInformationFrame) frame; - value = txxxFrame.value; - } - - String identifier = frame.id; - - WritableMap map = Arguments.createMap(); - map.putString("identifier", identifier); - map.putString("value", value); - - metadataArray.pushMap(map); - - } else if (entry instanceof EventMessage) { - - EventMessage eventMessage = (EventMessage) entry; - - WritableMap map = Arguments.createMap(); - map.putString("identifier", eventMessage.schemeIdUri); - map.putString("value", eventMessage.value); - metadataArray.pushMap(map); - - } - } - - WritableMap event = Arguments.createMap(); - event.putArray(EVENT_PROP_TIMED_METADATA, metadataArray); - receiveEvent(EVENT_TIMED_METADATA, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnTimedMetadataEvent(viewId, metadata)); } void audioFocusChanged(boolean hasFocus) { - WritableMap map = Arguments.createMap(); - map.putBoolean(EVENT_PROP_HAS_AUDIO_FOCUS, hasFocus); - receiveEvent(EVENT_AUDIO_FOCUS_CHANGE, map); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnAudioFocusChangedEvent(viewId, hasFocus)); } void audioBecomingNoisy() { - receiveEvent(EVENT_AUDIO_BECOMING_NOISY, null); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnVideoAudioBecomingNoisyEvent(viewId)); } void receiveAdEvent(String event) { - WritableMap map = Arguments.createMap(); - map.putString("event", event); - - receiveEvent(EVENT_ON_RECEIVE_AD_EVENT, map); - } - - private void receiveEvent(@VideoEvents String type, WritableMap event) { - eventEmitter.receiveEvent(viewId, type, event); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(this.context, viewId); + eventDispatcher.dispatchEvent(new OnReceiveAdEventEvent(viewId, event)); } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioFocusChangedEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioFocusChangedEvent.kt new file mode 100644 index 0000000000..045e304c62 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioFocusChangedEvent.kt @@ -0,0 +1,22 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnAudioFocusChangedEvent(viewTag: Int, private val hasFocus: Boolean): Event(viewTag) { + private val EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnAudioFocusChanged" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putBoolean(EVENT_PROP_HAS_AUDIO_FOCUS, hasFocus) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioTracksEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioTracksEvent.kt new file mode 100644 index 0000000000..435cbb82f1 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnAudioTracksEvent.kt @@ -0,0 +1,47 @@ +package com.brentvatne.exoplayer.events + +import com.brentvatne.common.Track +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnAudioTracksEvent(viewTag: Int, private val audioTracks: ArrayList) : Event(viewTag) { + private val EVENT_PROP_AUDIO_TRACKS = "audioTracks" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnAudioTracks" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), arrayToObject(EVENT_PROP_AUDIO_TRACKS, audioTracksToArray(audioTracks))) + } + + fun arrayToObject(field: String?, array: WritableArray?): WritableMap? { + val event = Arguments.createMap() + event.putArray(field!!, array) + return event + } + + fun audioTracksToArray(audioTracks: java.util.ArrayList?): WritableArray? { + val waAudioTracks = Arguments.createArray() + if (audioTracks != null) { + for (i in audioTracks.indices) { + val format = audioTracks[i] + val audioTrack = Arguments.createMap() + audioTrack.putInt("index", i) + audioTrack.putString("title", if (format.m_title != null) format.m_title else "") + audioTrack.putString("type", if (format.m_mimeType != null) format.m_mimeType else "") + audioTrack.putString("language", if (format.m_language != null) format.m_language else "") + audioTrack.putInt("bitrate", format.m_bitrate) + audioTrack.putBoolean("selected", format.m_isSelected) + waAudioTracks.pushMap(audioTrack) + } + } + return waAudioTracks + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnPlaybackRateChangeEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnPlaybackRateChangeEvent.kt new file mode 100644 index 0000000000..5448a12239 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnPlaybackRateChangeEvent.kt @@ -0,0 +1,22 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnPlaybackRateChangeEvent(viewTag: Int, private val rate: Float): Event(viewTag) { + private val EVENT_PROP_PLAYBACK_RATE = "playbackRate" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnPlaybackRateChange" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putDouble(EVENT_PROP_PLAYBACK_RATE, rate.toDouble()) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnReadyForDisplayEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnReadyForDisplayEvent.kt new file mode 100644 index 0000000000..2d5692ce9c --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnReadyForDisplayEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnReadyForDisplayEvent(viewTag: Int) : Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnReadyForDisplay" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnReceiveAdEventEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnReceiveAdEventEvent.kt new file mode 100644 index 0000000000..b65085db0a --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnReceiveAdEventEvent.kt @@ -0,0 +1,21 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnReceiveAdEventEvent(viewTag: Int, private val event: String): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnReceiveAdEventEvent" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val map = Arguments.createMap() + map.putString("event", event) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), map) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnTextTracksEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnTextTracksEvent.kt new file mode 100644 index 0000000000..66ef4524e5 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnTextTracksEvent.kt @@ -0,0 +1,46 @@ +package com.brentvatne.exoplayer.events + +import com.brentvatne.common.Track +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnTextTracksEvent(viewTag: Int, private val textTracks: ArrayList) : Event(viewTag) { + private val EVENT_PROP_TEXT_TRACKS = "textTracks" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnTextTracks" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), arrayToObject(EVENT_PROP_TEXT_TRACKS, textTracksToArray(textTracks))) + } + + fun arrayToObject(field: String?, array: WritableArray?): WritableMap? { + val event = Arguments.createMap() + event.putArray(field!!, array) + return event + } + + fun textTracksToArray(textTracks: ArrayList?): WritableArray? { + val waTextTracks = Arguments.createArray() + if (textTracks != null) { + for (i in textTracks.indices) { + val format = textTracks[i] + val textTrack = Arguments.createMap() + textTrack.putInt("index", i) + textTrack.putString("title", if (format.m_title != null) format.m_title else "") + textTrack.putString("type", if (format.m_mimeType != null) format.m_mimeType else "") + textTrack.putString("language", if (format.m_language != null) format.m_language else "") + textTrack.putBoolean("selected", format.m_isSelected) + waTextTracks.pushMap(textTrack) + } + } + return waTextTracks + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnTimedMetadataEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnTimedMetadataEvent.kt new file mode 100644 index 0000000000..019eb98ae7 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnTimedMetadataEvent.kt @@ -0,0 +1,49 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter +import com.google.android.exoplayer2.metadata.emsg.EventMessage +import com.google.android.exoplayer2.metadata.id3.Id3Frame +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame + +class OnTimedMetadataEvent(viewTag: Int, private val metadata: com.google.android.exoplayer2.metadata.Metadata): Event(viewTag) { + private val EVENT_PROP_TIMED_METADATA = "metadata" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnTimedMetadata" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val metadataArray = Arguments.createArray() + + for (i in 0 until metadata.length()) { + val entry = metadata[i] + if (entry is Id3Frame) { + val frame = entry + var value = "" + if (frame is TextInformationFrame) { + value = frame.value + } + val identifier = frame.id + val map = Arguments.createMap() + map.putString("identifier", identifier) + map.putString("value", value) + metadataArray.pushMap(map) + } else if (entry is EventMessage) { + val eventMessage = entry + val map = Arguments.createMap() + map.putString("identifier", eventMessage.schemeIdUri) + map.putString("value", eventMessage.value) + metadataArray.pushMap(map) + } + } + + val event = Arguments.createMap() + event.putArray(EVENT_PROP_TIMED_METADATA, metadataArray) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoAudioBecomingNoisyEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoAudioBecomingNoisyEvent.kt new file mode 100644 index 0000000000..2ea1078edc --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoAudioBecomingNoisyEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoAudioBecomingNoisyEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoAudioBecomingNoisy" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBandwidthUpdateEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBandwidthUpdateEvent.kt new file mode 100644 index 0000000000..ea97205116 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBandwidthUpdateEvent.kt @@ -0,0 +1,34 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoBandwidthUpdateEvent( + viewTag: Int, + private val bitRateEstimate: Double, + private val height: Int, + private val width: Int, + private val id: String +) : Event(viewTag) { + private val EVENT_PROP_BITRATE = "bitrate" + private val EVENT_PROP_WIDTH = "width" + private val EVENT_PROP_HEIGHT = "height" + private val EVENT_PROP_TRACK_ID = "trackId" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoBandwidthUpdate" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putDouble(EVENT_PROP_BITRATE, bitRateEstimate) + event.putInt(EVENT_PROP_WIDTH, width) + event.putInt(EVENT_PROP_HEIGHT, height) + event.putString(EVENT_PROP_TRACK_ID, id) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBufferEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBufferEvent.kt new file mode 100644 index 0000000000..110c92758a --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoBufferEvent.kt @@ -0,0 +1,22 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoBufferEvent(viewTag: Int, private val isBuffering: Boolean) : Event(viewTag) { + private val EVENT_PROP_IS_BUFFERING = "isBuffering" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoBuffer" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putBoolean(EVENT_PROP_IS_BUFFERING, isBuffering) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoEndEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoEndEvent.kt new file mode 100644 index 0000000000..c5495ee03c --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoEndEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoEndEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoEnd" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoErrorEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoErrorEvent.kt new file mode 100644 index 0000000000..bac4df8a8c --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoErrorEvent.kt @@ -0,0 +1,46 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter +import java.io.PrintWriter +import java.io.StringWriter + +class OnVideoErrorEvent( + viewTag: Int, + private val errorString: String, + private val exception: Exception, + private val errorCode: String +): Event(viewTag) { + private val EVENT_PROP_ERROR = "error" + private val EVENT_PROP_ERROR_STRING = "errorString" + private val EVENT_PROP_ERROR_EXCEPTION = "errorException" + private val EVENT_PROP_ERROR_TRACE = "errorStackTrace" + private val EVENT_PROP_ERROR_CODE = "errorCode" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoErrorEvent" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + // Prepare stack trace + + // Prepare stack trace + val sw = StringWriter() + val pw = PrintWriter(sw) + exception.printStackTrace(pw) + val stackTrace = sw.toString() + + val error = Arguments.createMap() + error.putString(EVENT_PROP_ERROR_STRING, errorString) + error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString()) + error.putString(EVENT_PROP_ERROR_CODE, errorCode) + error.putString(EVENT_PROP_ERROR_TRACE, stackTrace) + val event = Arguments.createMap() + event.putMap(EVENT_PROP_ERROR, error) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidDismissEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidDismissEvent.kt new file mode 100644 index 0000000000..8e02f1026f --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidDismissEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoFullscreenPlayerDidDismissEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoFullscreenPlayerDidDismiss" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidPresentEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidPresentEvent.kt new file mode 100644 index 0000000000..397e52f94a --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerDidPresentEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoFullscreenPlayerDidPresentEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoFullscreenPlayerDidPresent" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillDismissEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillDismissEvent.kt new file mode 100644 index 0000000000..f4b7bdb98a --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillDismissEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoFullscreenPlayerWillDismissEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoFullscreenPlayerWillDismiss" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillPresentEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillPresentEvent.kt new file mode 100644 index 0000000000..1ba8f8d965 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoFullscreenPlayerWillPresentEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoFullscreenPlayerWillPresentEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoFullscreenPlayerWillPresent" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoIdleEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoIdleEvent.kt new file mode 100644 index 0000000000..273fe3286f --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoIdleEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoIdleEvent(viewTag: Int): Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoIdle" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadEvent.kt new file mode 100644 index 0000000000..ed383a4b53 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadEvent.kt @@ -0,0 +1,103 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoLoadEvent(viewTag: Int, + private val duration: Double, + private val currentPosition: Double, + private val videoWidth: Int, + private val videoHeight: Int, + private val audioTracks: WritableArray, + private val textTracks: WritableArray, + private val videoTracks: WritableArray, + private val trackId: String): Event(viewTag) { + private val EVENT_PROP_FAST_FORWARD = "canPlayFastForward" + private val EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward" + private val EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse" + private val EVENT_PROP_REVERSE = "canPlayReverse" + private val EVENT_PROP_STEP_FORWARD = "canStepForward" + private val EVENT_PROP_STEP_BACKWARD = "canStepBackward" + + private val EVENT_PROP_BUFFER_START = "bufferStart" + private val EVENT_PROP_BUFFER_END = "bufferEnd" + private val EVENT_PROP_DURATION = "duration" + private val EVENT_PROP_PLAYABLE_DURATION = "playableDuration" + private val EVENT_PROP_SEEKABLE_DURATION = "seekableDuration" + private val EVENT_PROP_CURRENT_TIME = "currentTime" + private val EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime" + private val EVENT_PROP_SEEK_TIME = "seekTime" + private val EVENT_PROP_NATURAL_SIZE = "naturalSize" + private val EVENT_PROP_TRACK_ID = "trackId" + private val EVENT_PROP_WIDTH = "width" + private val EVENT_PROP_HEIGHT = "height" + private val EVENT_PROP_ORIENTATION = "orientation" + private val EVENT_PROP_VIDEO_TRACKS = "videoTracks" + private val EVENT_PROP_AUDIO_TRACKS = "audioTracks" + private val EVENT_PROP_TEXT_TRACKS = "textTracks" + private val EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus" + private val EVENT_PROP_IS_BUFFERING = "isBuffering" + private val EVENT_PROP_PLAYBACK_RATE = "playbackRate" + + private val EVENT_PROP_ERROR = "error" + private val EVENT_PROP_ERROR_STRING = "errorString" + private val EVENT_PROP_ERROR_EXCEPTION = "errorException" + private val EVENT_PROP_ERROR_TRACE = "errorStackTrace" + private val EVENT_PROP_ERROR_CODE = "errorCode" + + private val EVENT_PROP_TIMED_METADATA = "metadata" + + private val EVENT_PROP_BITRATE = "bitrate" + + private val EVENT_PROP_IS_PLAYING = "isPlaying" + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putDouble(EVENT_PROP_DURATION, duration / 1000.0) + event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000.0) + + val naturalSize: WritableMap? = aspectRatioToNaturalSize(videoWidth, videoHeight) + event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize) + event.putString(EVENT_PROP_TRACK_ID, trackId) + event.putArray(EVENT_PROP_VIDEO_TRACKS, videoTracks) + event.putArray(EVENT_PROP_AUDIO_TRACKS, audioTracks) + event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks) + + // TODO: Actually check if you can. + + // TODO: Actually check if you can. + event.putBoolean(EVENT_PROP_FAST_FORWARD, true) + event.putBoolean(EVENT_PROP_SLOW_FORWARD, true) + event.putBoolean(EVENT_PROP_SLOW_REVERSE, true) + event.putBoolean(EVENT_PROP_REVERSE, true) + event.putBoolean(EVENT_PROP_FAST_FORWARD, true) + event.putBoolean(EVENT_PROP_STEP_BACKWARD, true) + event.putBoolean(EVENT_PROP_STEP_FORWARD, true) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } + + companion object { + const val EVENT_NAME = "topOnVideoLoad" + } + + fun aspectRatioToNaturalSize(videoWidth: Int, videoHeight: Int): WritableMap? { + val naturalSize = Arguments.createMap() + naturalSize.putInt(EVENT_PROP_WIDTH, videoWidth) + naturalSize.putInt(EVENT_PROP_HEIGHT, videoHeight) + if (videoWidth > videoHeight) { + naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape") + } else if (videoWidth < videoHeight) { + naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait") + } else { + naturalSize.putString(EVENT_PROP_ORIENTATION, "square") + } + return naturalSize + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadStartEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadStartEvent.kt new file mode 100644 index 0000000000..6384faca35 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoLoadStartEvent.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoLoadStartEvent(viewTag: Int) : Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), null) + } + + companion object { + const val EVENT_NAME = "topOnVideoLoadStart" + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoPlaybackStateChangedEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoPlaybackStateChangedEvent.kt new file mode 100644 index 0000000000..50e7b0832c --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoPlaybackStateChangedEvent.kt @@ -0,0 +1,22 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoPlaybackStateChangedEvent(viewTag: Int, private val isPlaying: Boolean) : Event(viewTag) { + private val EVENT_PROP_IS_PLAYING = "isPlaying" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoPlaybackStateChanged" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoProgressEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoProgressEvent.kt new file mode 100644 index 0000000000..5dec7cdc70 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoProgressEvent.kt @@ -0,0 +1,69 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoProgressEvent( + viewTag: Int, + private val currentPosition: Double, + private val bufferedDuration: Double, + private val seekableDuration: Double, + private val currentPlaybackTime: Double +): Event(viewTag) { + private val EVENT_PROP_FAST_FORWARD = "canPlayFastForward" + private val EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward" + private val EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse" + private val EVENT_PROP_REVERSE = "canPlayReverse" + private val EVENT_PROP_STEP_FORWARD = "canStepForward" + private val EVENT_PROP_STEP_BACKWARD = "canStepBackward" + + private val EVENT_PROP_BUFFER_START = "bufferStart" + private val EVENT_PROP_BUFFER_END = "bufferEnd" + private val EVENT_PROP_DURATION = "duration" + private val EVENT_PROP_PLAYABLE_DURATION = "playableDuration" + private val EVENT_PROP_SEEKABLE_DURATION = "seekableDuration" + private val EVENT_PROP_CURRENT_TIME = "currentTime" + private val EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime" + private val EVENT_PROP_SEEK_TIME = "seekTime" + private val EVENT_PROP_NATURAL_SIZE = "naturalSize" + private val EVENT_PROP_TRACK_ID = "trackId" + private val EVENT_PROP_WIDTH = "width" + private val EVENT_PROP_HEIGHT = "height" + private val EVENT_PROP_ORIENTATION = "orientation" + private val EVENT_PROP_VIDEO_TRACKS = "videoTracks" + private val EVENT_PROP_AUDIO_TRACKS = "audioTracks" + private val EVENT_PROP_TEXT_TRACKS = "textTracks" + private val EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus" + private val EVENT_PROP_IS_BUFFERING = "isBuffering" + private val EVENT_PROP_PLAYBACK_RATE = "playbackRate" + + private val EVENT_PROP_ERROR = "error" + private val EVENT_PROP_ERROR_STRING = "errorString" + private val EVENT_PROP_ERROR_EXCEPTION = "errorException" + private val EVENT_PROP_ERROR_TRACE = "errorStackTrace" + private val EVENT_PROP_ERROR_CODE = "errorCode" + + private val EVENT_PROP_TIMED_METADATA = "metadata" + + private val EVENT_PROP_BITRATE = "bitrate" + + private val EVENT_PROP_IS_PLAYING = "isPlaying" + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000.0) + event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000.0) + event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000.0) + event.putDouble(EVENT_PROP_CURRENT_PLAYBACK_TIME, currentPlaybackTime) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } + + companion object { + const val EVENT_NAME = "topOnVideoProgress" + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoSeekEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoSeekEvent.kt new file mode 100644 index 0000000000..2a8108665c --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoSeekEvent.kt @@ -0,0 +1,24 @@ +package com.brentvatne.exoplayer.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoSeekEvent(viewTag: Int, private val currentPosition: Long, private val seekTime: Long) : Event(viewTag) { + private val EVENT_PROP_CURRENT_TIME = "currentTime" + private val EVENT_PROP_SEEK_TIME = "seekTime" + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + val event = Arguments.createMap() + event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000.0) + event.putDouble(EVENT_PROP_SEEK_TIME, seekTime / 1000.0) + rctEventEmitter?.receiveEvent(viewTag, getEventName(), event) + } + + companion object { + const val EVENT_NAME = "topOnVideoSeek" + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoTracksEvent.kt b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoTracksEvent.kt new file mode 100644 index 0000000000..c272c724e7 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/events/OnVideoTracksEvent.kt @@ -0,0 +1,47 @@ +package com.brentvatne.exoplayer.events + +import com.brentvatne.common.VideoTrack +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnVideoTracksEvent(viewTag: Int, private val videoTracks: ArrayList) : Event(viewTag) { + private val EVENT_PROP_VIDEO_TRACKS = "videoTracks" + override fun getEventName(): String { + return EVENT_NAME + } + + companion object { + const val EVENT_NAME = "topOnVideoTracks" + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter?) { + rctEventEmitter?.receiveEvent(viewTag, getEventName(), arrayToObject(EVENT_PROP_VIDEO_TRACKS, videoTracksToArray(videoTracks))) + } + + fun arrayToObject(field: String?, array: WritableArray?): WritableMap? { + val event = Arguments.createMap() + event.putArray(field!!, array) + return event + } + + fun videoTracksToArray(videoTracks: ArrayList?): WritableArray? { + val waVideoTracks = Arguments.createArray() + if (videoTracks != null) { + for (i in videoTracks.indices) { + val vTrack = videoTracks[i] + val videoTrack = Arguments.createMap() + videoTrack.putInt("width", vTrack.m_width) + videoTrack.putInt("height", vTrack.m_height) + videoTrack.putInt("bitrate", vTrack.m_bitrate) + videoTrack.putString("codecs", vTrack.m_codecs) + videoTrack.putInt("trackId", vTrack.m_id) + videoTrack.putBoolean("selected", vTrack.m_isSelected) + waVideoTracks.pushMap(videoTrack) + } + } + return waVideoTracks + } +} \ No newline at end of file diff --git a/android/src/main/java/com/brentvatne/react/ReactVideoPackage.java b/android/src/main/java/com/brentvatne/react/ReactVideoPackage.java index 23bedad69e..e54ad8f0aa 100644 --- a/android/src/main/java/com/brentvatne/react/ReactVideoPackage.java +++ b/android/src/main/java/com/brentvatne/react/ReactVideoPackage.java @@ -1,14 +1,15 @@ package com.brentvatne.react; import com.brentvatne.exoplayer.DefaultReactExoplayerConfig; -import com.brentvatne.exoplayer.ReactExoplayerConfig; import com.brentvatne.exoplayer.ReactExoplayerViewManager; import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; +import com.brentvatne.exoplayer.ReactExoplayerConfig; +import com.facebook.react.bridge.JavaScriptModule; + import java.util.Collections; import java.util.List; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java similarity index 99% rename from android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java rename to android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index ed810ca66a..4b76f4e917 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -26,7 +26,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager { - private static final String REACT_CLASS = "RCTVideo"; + private static final String REACT_CLASS = "RNCVideo"; private static final String PROP_SRC = "src"; private static final String PROP_SRC_URI = "uri"; private static final String PROP_SRC_START_TIME = "startTime"; diff --git a/src/MyVideo.tsx b/src/MyVideo.tsx new file mode 100644 index 0000000000..7cf7cecfef --- /dev/null +++ b/src/MyVideo.tsx @@ -0,0 +1,380 @@ +import React, { + useState, + useCallback, + useMemo, + useRef, + forwardRef, + useImperativeHandle, +} from "react"; +import { + View, + StyleSheet, + Image, + Platform, +} from "react-native"; +import RNCVideoComponent, { Commands, OnAudioFocusChangedData, OnPlaybackStateChangedData, OnVideoErrorData } from "./fabric/VideoNativeComponent"; + +import type { StyleProp, ImageStyle, NativeSyntheticEvent } from "react-native"; +import type { + VideoComponentType, + NativeCommands, + OnLoadData, + OnGetLicenseData, + OnLoadStartData, + OnProgressData, + OnSeekData, + OnPictureInPictureStatusChangedData, + OnBandwidthUpdateData, + OnBufferData, + OnExternalPlaybackChangeData, + OnReceiveAdEventData, +} from "./fabric/VideoNativeComponent"; +import type { ReactVideoProps } from "./types/video"; +import { generateHeaderForNative, resolveAssetSourceForVideo } from "./utils"; + +export interface VideoRef extends Omit { + seek: (time: number, tolerance?: number) => void; + presentFullscreenPlayer: () => void; + dismissFullscreenPlayer: () => void; + restoreUserInterfaceForPictureInPictureStopCompleted: (restore: boolean) => void; +} + +const Video = forwardRef( + ( + { + source, + style, + resizeMode, + posterResizeMode, + poster, + fullscreen, + drm, + textTracks, + selectedAudioTrack, + selectedTextTrack, + onLoadStart, + onLoad, + onError, + onProgress, + onSeek, + onEnd, + onBuffer, + onBandwidthUpdate, + onTimedMetadata, + onExternalPlaybackChange, + onFullscreenPlayerWillPresent, + onFullscreenPlayerDidPresent, + onFullscreenPlayerWillDismiss, + onFullscreenPlayerDidDismiss, + onReadyForDisplay, + onPlaybackRateChange, + onAudioBecomingNoisy, + onPictureInPictureStatusChanged, + onRestoreUserInterfaceForPictureInPictureStop, + onReceiveAdEvent, + onPlaybackStateChanged, + onAudioFocusChanged, + onIdle, + ...rest + }, + ref + ) => { + const videoRef = useRef>(null); + const [showPoster, setShowPoster] = useState(!!poster); + const [isFullscreen, setIsFullscreen] = useState(fullscreen); + const [ + _restoreUserInterfaceForPIPStopCompletionHandler, + setRestoreUserInterfaceForPIPStopCompletionHandler, + ] = useState(); + + const posterStyle = useMemo>( + () => ({ + ...StyleSheet.absoluteFillObject, + resizeMode: + posterResizeMode && posterResizeMode !== "none" + ? posterResizeMode + : "contain", + }), + [posterResizeMode] + ); + + const src = useMemo(() => { + const resolvedSource = resolveAssetSourceForVideo(source); + let uri = resolvedSource.uri || ""; + if (uri && uri.match(/^\//)) uri = `file://${uri}`; + if (!uri) console.warn("Trying to load empty source"); + const isNetwork = !!(uri && uri.match(/^https?:/)); + const isAsset = !!( + uri && + uri.match( + /^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/ + ) + ); + + return { + uri, + isNetwork, + isAsset, + shouldCache: resolvedSource.shouldCache || false, + type: resolvedSource.type || "", + mainVer: resolvedSource.mainVer || 0, + patchVer: resolvedSource.patchVer || 0, + requestHeaders: generateHeaderForNative(resolvedSource?.headers), + }; + }, [source]); + + const _drm = useMemo(() => { + if (!drm) return; + return { + drmType: drm.type, + licenseServer: drm.licenseServer, + headers: generateHeaderForNative(drm.headers), + contentId: drm.contentId, + certificateUrl: drm.certificateUrl, + base64Certificate: drm.base64Certificate, + }; + }, [drm]); + + + const _selectedTextTrack = useMemo(() => { + if (!selectedTextTrack) return; + if (typeof selectedTextTrack?.value === 'number') return { + seletedTextType: selectedTextTrack?.type, + index: selectedTextTrack?.value, + } + return { + seletedTextType: selectedTextTrack?.type, + value: selectedTextTrack?.value, + } + }, [selectedTextTrack]); + + const _selectedAudioTrack = useMemo(() => { + if (!selectedAudioTrack) return; + if (typeof selectedAudioTrack?.value === 'number') return { + seletedAudioType: selectedAudioTrack?.type, + index: selectedAudioTrack?.value, + } + return { + seletedAudioType: selectedAudioTrack?.type, + value: selectedAudioTrack?.value, + } + }, [selectedAudioTrack]); + + + const seek = useCallback( + (time: number, tolerance?: number) => { + if (isNaN(time)) throw new Error("Specified time is not a number"); + if (!videoRef.current) return; + Commands.seek(videoRef.current, time, tolerance ?? 100); + }, + [] + ); + + const presentFullscreenPlayer = useCallback(() => { + setIsFullscreen(true); + }, [setIsFullscreen]); + + const dismissFullscreenPlayer = useCallback(() => { + setIsFullscreen(false); + }, [setIsFullscreen]); + + const save = useCallback(async () => { + if(!videoRef.current) return; + Commands.save(videoRef.current); + }, []); + + + const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback( + (restored: boolean) => { + setRestoreUserInterfaceForPIPStopCompletionHandler(restored); + }, + [setRestoreUserInterfaceForPIPStopCompletionHandler] + ); + + const onVideoLoadStart = useCallback( + (e: NativeSyntheticEvent) => { + onLoadStart?.(e.nativeEvent); + }, + [onLoadStart] + ); + + const onVideoLoad = useCallback( + (e: NativeSyntheticEvent) => { + if (Platform.OS === "windows") setShowPoster(false); + onLoad?.(e.nativeEvent); + }, + [onLoad, setShowPoster] + ); + + const onVideoError = useCallback( + (e: NativeSyntheticEvent) => { + onError?.(e.nativeEvent); + }, + [onError] + ); + + const onVideoProgress = useCallback( + (e: NativeSyntheticEvent) => { + onProgress?.(e.nativeEvent); + }, + [onProgress] + ); + + const onVideoSeek = useCallback( + (e: NativeSyntheticEvent) => { + onSeek?.(e.nativeEvent); + }, + [onSeek] + ); + + // android only + const onVideoPlaybackStateChanged = useCallback((e: NativeSyntheticEvent) => { + onPlaybackStateChanged?.(e.nativeEvent); + }, [onPlaybackStateChanged]) + + // android only + const onVideoIdle = useCallback(() => { + onIdle?.() + }, [onIdle]) + + /** @todo: fix type */ + const _onTimedMetadata = useCallback( + () => { + onTimedMetadata?.(); + }, + [onTimedMetadata] + ); + + const _onPlaybackRateChange = useCallback( + (e: NativeSyntheticEvent>) => { + onPlaybackRateChange?.(e.nativeEvent); + }, + [onPlaybackRateChange] + ); + + const _onReadyForDisplay = useCallback(() => { + setShowPoster(false); + onReadyForDisplay?.(); + }, [setShowPoster, onReadyForDisplay]); + + const _onPictureInPictureStatusChanged = useCallback( + (e: NativeSyntheticEvent) => { + onPictureInPictureStatusChanged?.(e.nativeEvent); + }, + [onPictureInPictureStatusChanged] + ); + + const _onAudioFocusChanged = useCallback((e: NativeSyntheticEvent) => { + onAudioFocusChanged?.(e.nativeEvent) + }, [onAudioFocusChanged]) + + const onVideoBuffer = useCallback((e: NativeSyntheticEvent) => { + onBuffer?.(e.nativeEvent); + }, [onBuffer]); + + const onVideoExternalPlaybackChange = useCallback((e: NativeSyntheticEvent) => { + onExternalPlaybackChange?.(e.nativeEvent); + }, [onExternalPlaybackChange]) + + const _onBandwidthUpdate = useCallback((e: NativeSyntheticEvent) => { + onBandwidthUpdate?.(e.nativeEvent); + }, [onBandwidthUpdate]); + + const _onReceiveAdEvent = useCallback((e: NativeSyntheticEvent) => { + onReceiveAdEvent?.(e.nativeEvent); + }, [onReceiveAdEvent]); + + const onGetLicense = useCallback( + (event: NativeSyntheticEvent) => { + if (drm && drm.getLicense instanceof Function) { + const data = event.nativeEvent; + if (data && data.spcBase64) { + const getLicenseOverride = drm.getLicense(data.spcBase64, data.contentId, data.licenseUrl); + const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. + getLicensePromise.then((result => { + if (result !== undefined) { + if (videoRef.current) Commands.setLicenseResult(videoRef.current, result); + } else { + if (videoRef.current) Commands.setLicenseResultError(videoRef.current, 'Empty license result'); + } + })).catch(() => { + if (videoRef.current) Commands.setLicenseResultError(videoRef.current, 'fetch error'); + }); + } else { + if (videoRef.current) Commands.setLicenseResultError(videoRef.current, 'No spc received'); + } + } + }, + [drm] + ); + + useImperativeHandle( + ref, + () => ({ + seek, + presentFullscreenPlayer, + dismissFullscreenPlayer, + save, + restoreUserInterfaceForPictureInPictureStopCompleted, + }), + [ + seek, + presentFullscreenPlayer, + dismissFullscreenPlayer, + save, + restoreUserInterfaceForPictureInPictureStopCompleted, + ] + ); + + return ( + + + {showPoster ? ( + + ) : null} + + ); + } +); + +export default Video; diff --git a/src/fabric/VideoNativeComponent.tsx b/src/fabric/VideoNativeComponent.tsx new file mode 100644 index 0000000000..9fed0fb787 --- /dev/null +++ b/src/fabric/VideoNativeComponent.tsx @@ -0,0 +1,273 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import type { HostComponent, ViewProps } from 'react-native'; + +import type { Float, Int32, WithDefault, DirectEventHandler, Double } from 'react-native/Libraries/Types/CodegenTypes'; + +type Headers = ReadonlyArray> + +type VideoSrc = Readonly<{ + uri?: string; + isNetwork?: boolean; + isAsset?: boolean; + shouldCache?: boolean; + type?: string; + mainVer?: Int32; + patchVer?: Int32; + requestHeaders?: Headers; + startTime?: Float; + endTime?: Float; +}> + + +export type Filter = WithDefault<'None' | +'CIColorInvert' | +'CIColorMonochrome' | +'CIColorPosterize' | +'CIFalseColor' | +'CIMaximumComponent' | +'CIMinimumComponent' | +'CIPhotoEffectChrome' | +'CIPhotoEffectFade' | +'CIPhotoEffectInstant' | +'CIPhotoEffectMono' | +'CIPhotoEffectNoir' | +'CIPhotoEffectProcess' | +'CIPhotoEffectTonal' | +'CIPhotoEffectTransfer' | +'CISepiaTone', 'None'>; + +export type DrmType = WithDefault<'widevine' | 'playready' | 'clearkey' | 'fairplay', 'widevine'>; + +type Drm = Readonly<{ + drmType?: DrmType; + licenseServer?: string; + headers?: Headers; + contentId?: string; // ios + certificateUrl?: string; // ios + base64Certificate?: boolean; // ios default: false +}> + +type TextTracks = ReadonlyArray> + +type TextTrackType = WithDefault<'system' | 'disabled' | 'title' | 'language' | 'index', 'system'>; + +type SelectedTextTrack = Readonly<{ + selectedTextType?: TextTrackType; + value?: string; + index?: Int32; +}> + +type AudioTrackType = WithDefault<'system' | 'disabled' | 'title' | 'language' | 'index', 'system'>; + +type SelectedAudioTrack = Readonly<{ + selectedAudioType?: AudioTrackType; + value?: string; + index?: Int32; +}> + +export type Seek = Readonly<{ + time: Float; + tolerance?: Float; +}> + +type BufferConfig = Readonly<{ + minBufferMs?: Float; + maxBufferMs?: Float; + bufferForPlaybackMs?: Float; + bufferForPlaybackAfterRebufferMs?: Float; + maxHeapAllocationPercent?: Float; + minBackBufferMemoryReservePercent?: Float; + minBufferMemoryReservePercent?: Float; +}> + +type SelectedVideoTrack = Readonly<{ + type?: WithDefault<'auto' | 'disabled' | 'resolution' | 'index', 'auto'> + value?: Int32; +}> + +type SubtitleStyle = Readonly<{ + fontSize?: Float; + paddingTop?: WithDefault; + paddingBottom?: WithDefault; + paddingLeft?: WithDefault; + paddingRight?: WithDefault; +}> + +export type OnLoadData = Readonly<{ + currentTime: Float; + duration: Float; + naturalSize: Readonly<{ + width: Float; + height: Float; + orientation: string; + }>; +}> + +export type OnLoadStartData = Readonly<{ + isNetwork: boolean; + type: string; + uri: string; +}> + +export type OnBufferData = Readonly<{ isBuffering: boolean }>; + + +export type OnProgressData = Readonly<{ + currentTime: Float; + playableDuration: Float; + seekableDuration: Float; +}> + +export type OnBandwidthUpdateData = Readonly<{ + bitrate: Int32; +}>; + +export type OnSeekData = Readonly<{ + currentTime: Float; + seekTime: Float; +}> + +export type OnPlaybackStateChangedData = Readonly<{ + isPlaying: boolean; +}> + +export type OnTimedMetadataData = Readonly<{}> + +export type OnPlaybackData = Readonly<{ + playbackRate: Float; +}>; + +export type OnExternalPlaybackChangeData = Readonly<{ + isExternalPlaybackActive: boolean; +}> + +export type OnGetLicenseData = Readonly<{ + licenseUrl: string; + contentId: string; + spcBase64: string; +}> + +export type OnPictureInPictureStatusChangedData = Readonly<{ + isActive: boolean; +}> + +export type OnReceiveAdEventData = Readonly<{ + event: string; +}> + +export type OnVideoErrorData = Readonly<{ + error: string; +}> + +export type OnAudioFocusChangedData = Readonly<{ + hasFocus: boolean; +}> + +export interface VideoNativeProps extends ViewProps { + src: VideoSrc; + drm?: Drm; + adTagUrl?: string; + allowsExternalPlayback?: boolean; // ios, true + maxBitRate?: Float; + resizeMode?: WithDefault<'none' | 'contain' | 'cover' | 'stretch', 'none'>; + repeat?: boolean; + automaticallyWaitsToMinimizeStalling?: boolean + textTracks?: TextTracks; + selectedTextTrack?: SelectedTextTrack; + selectedAudioTrack?: SelectedAudioTrack; + paused?: boolean; + muted?: boolean; + controls?: boolean; + filter?: Filter; + filterEnabled?: boolean; + volume?: Float; // default 1.0 + playInBackground?: boolean; + preventsDisplaySleepDuringVideoPlayback?: boolean; + preferredForwardBufferDuration?: Float; //ios, 0 + playWhenInactive?: boolean; // ios, false + pictureInPicture?: boolean; // ios, false + ignoreSilentSwitch?: WithDefault<'inherit' | 'ignore' | 'obey', 'inherit'>; // ios, 'inherit' + mixWithOthers?: WithDefault<'inherit' | 'mix' | 'duck', 'inherit'>; // ios, 'inherit' + rate?: Float; + fullscreen?: boolean; // ios, false + fullscreenAutorotate?: boolean; + fullscreenOrientation?: WithDefault<'all' | 'landscape' | 'portrait', 'all'>; + progressUpdateInterval?: Float; + restoreUserInterfaceForPIPStopCompletionHandler?: boolean; + localSourceEncryptionKeyScheme?: string; + + backBufferDurationMs?: Int32; // Android + bufferConfig?: BufferConfig; // Android + contentStartTime?: Int32; // Android + currentPlaybackTime?: Double; // Android + disableDisconnectError?: boolean; // Android + focusable?: boolean; // Android + hideShutterView?: boolean; // Android + minLoadRetryCount?: Int32; // Android + reportBandwidth?: boolean; //Android + selectedVideoTrack?: SelectedVideoTrack; // android + subtitleStyle?: SubtitleStyle // android + trackId?: string; // Android + useTextureView?: boolean; // Android + useSecureView?: boolean; // Android + + onVideoLoad?: DirectEventHandler; + onVideoLoadStart?: DirectEventHandler; + onVideoBuffer?: DirectEventHandler; + onVideoError?: DirectEventHandler; + onVideoProgress?: DirectEventHandler; + onBandwidthUpdate?: DirectEventHandler + onVideoSeek?: DirectEventHandler; + onVideoEnd?: DirectEventHandler>; // all + onTimedMetadata?: DirectEventHandler; // ios, android + onVideoAudioBecomingNoisy?: DirectEventHandler>; + onVideoFullscreenPlayerWillPresent?: DirectEventHandler>; // ios, android + onVideoFullscreenPlayerDidPresent?: DirectEventHandler>; // ios, android + onVideoFullscreenPlayerWillDismiss?: DirectEventHandler>; // ios, android + onVideoFullscreenPlayerDidDismiss?: DirectEventHandler>; // ios, android + onReadyForDisplay?: DirectEventHandler>; + onPlaybackRateChange?: DirectEventHandler; // all + onVideoExternalPlaybackChange?: DirectEventHandler; + onGetLicense?: DirectEventHandler; + onPictureInPictureStatusChanged?: DirectEventHandler; + onRestoreUserInterfaceForPictureInPictureStop?: DirectEventHandler>; + onReceiveAdEvent?: DirectEventHandler; + onVideoPlaybackStateChanged?: DirectEventHandler; // android only + onVideoIdle?: DirectEventHandler<{}>; // android only (nowhere in document, so do not use as props. just type declaration) + onAudioFocusChanged?: DirectEventHandler; // android only (nowhere in document, so do not use as props. just type declaration) + // @todo: fix type + // onAudioTracks: DirectEventHandler; // android + // onTextTracks: DirectEventHandler; // android + // onVideoTracks: DirectEventHandler; // android + // onTimedMetadata: DirectEventHandler; // android +} + +export type VideoComponentType = HostComponent +export interface NativeCommands { + save: (viewRef: React.ElementRef) => void; + seek: (viewRef: React.ElementRef, time: Float, tolerance?: Float) => void; + setLicenseResult: (viewRef: React.ElementRef, result: string) => void; + setLicenseResultError: (viewRef: React.ElementRef, error: string) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: [ + 'save', + 'seek', + 'setLicenseResult', + 'setLicenseResultError', + ], +}); + +export default codegenNativeComponent('RNCVideo') as VideoComponentType; + + diff --git a/src/types/events.ts b/src/types/events.ts new file mode 100644 index 0000000000..90217fb7fe --- /dev/null +++ b/src/types/events.ts @@ -0,0 +1,31 @@ +import type { OnBandwidthUpdateData, OnBufferData, OnLoadData, OnLoadStartData, OnProgressData, OnSeekData, OnPlaybackData, OnExternalPlaybackChangeData, OnPictureInPictureStatusChangedData, OnReceiveAdEventData, OnVideoErrorData, OnPlaybackStateChangedData, OnAudioFocusChangedData } from "src/fabric/VideoNativeComponent"; + +export interface ReactVideoEvents { + onAudioBecomingNoisy?: () => void //Android, iOS + onAudioTracks?: () => void // Android + onAudioFocusChanged?: (e: OnAudioFocusChangedData) => void // Android + onIdle?: () => void // Android + onBandwidthUpdate?: (e: OnBandwidthUpdateData) => void //Android + onBuffer?: (e: OnBufferData) => void //Android, iOS + onEnd?: () => void //All + onError?: (e: OnVideoErrorData) => void //Android, iOS + onExternalPlaybackChange?: (e: OnExternalPlaybackChangeData) => void //iOS + onFullscreenPlayerWillPresent?: () => void //Android, iOS + onFullscreenPlayerDidPresent?: () => void //Android, iOS + onFullscreenPlayerWillDismiss?: () => void //Android, iOS + onFullscreenPlayerDidDismiss?: () => void //Android, iOS + onLoad?: (e: OnLoadData) => void //All + onLoadStart?: (e: OnLoadStartData) => void //All + onPictureInPictureStatusChanged?: (e: OnPictureInPictureStatusChangedData) => void //iOS + onPlaybackRateChange?: (e: OnPlaybackData) => void //All + onProgress?: (e: OnProgressData) => void //All + onReadyForDisplay?: () => void //Android, iOS, Web + onReceiveAdEvent?: (e: OnReceiveAdEventData) => void //Android, iOS + onRestoreUserInterfaceForPictureInPictureStop?: () => void //iOS + onSeek?: (e: OnSeekData) => void //Android, iOS, Windows UWP + onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void // Android + // @todo: fix type + onTimedMetadata?: () => void //Android, iOS + onTextTracks?: () => void //Android + onVideoTracks?: () => void //Android +} diff --git a/src/types/video.ts b/src/types/video.ts new file mode 100644 index 0000000000..e91a0f1165 --- /dev/null +++ b/src/types/video.ts @@ -0,0 +1,135 @@ +import type { ISO639_1 } from './language'; +import type { ReactVideoEvents } from './events'; +import type { StyleProp, ViewStyle } from 'react-native' + +type Filter = | 'None' + | 'CIColorInvert' + | 'CIColorMonochrome' + | 'CIColorPosterize' + | 'CIFalseColor' + | 'CIMaximumComponent' + | 'CIMinimumComponent' + | 'CIPhotoEffectChrome' + | 'CIPhotoEffectFade' + | 'CIPhotoEffectInstant' + | 'CIPhotoEffectMono' + | 'CIPhotoEffectNoir' + | 'CIPhotoEffectProcess' + | 'CIPhotoEffectTonal' + | 'CIPhotoEffectTransfer' + | 'CISepiaTone' + + + +type Headers = Record; + +export type ReactVideoSource = Readonly<{ + uri?: string; + isNetwork?: boolean; + isAsset?: boolean; + shouldCache?: boolean; + type?: string; + mainVer?: number; + patchVer?: number; + headers?: Headers; + startTime?: number; + endTime?: number; +}>; + +export type ReactVideoDrm = Readonly<{ + type?: 'widevine' | 'playready' | 'clearkey' | 'fairplay'; + licenseServer?: string; + headers?: Headers; + contentId?: string; // ios + certificateUrl?: string; // ios + base64Certificate?: boolean; // ios default: false + getLicense?: (licenseUrl: string, contentId: string, spcBase64: string) => void; // ios +}> + +type BufferConfig = { + minBufferMs?: number; + maxBufferMs?: number; + bufferForPlaybackMs?: number; + bufferForPlaybackAfterRebufferMs?: number; + maxHeapAllocationPercent?: number; + minBackBufferMemoryReservePercent?: number; + minBufferMemoryReservePercent?: number; +} + +type SelectedTrack = { + type: 'system' | 'disabled' | 'title' | 'language' | 'index'; + value?: string | number; +} + +type SelectedVideoTrack = { + type: 'auto' | 'disabled' | 'resolution' | 'index' + value: number; +} + +type SubtitleStyle = { + fontSize?: number; + paddingTop?: number; + paddingBottom?: number; + paddingLeft?: number; + paddingRight?: number; +} + +type TextTracks = { + title: string; + language: ISO639_1; + type: | 'application/x-subrip' + | 'application/ttml+xml' + | 'text/vtt'; + uri: string; +}[] + +export interface ReactVideoProps extends ReactVideoEvents { + source: ReactVideoSource; + drm: ReactVideoDrm; + style: StyleProp; + adTagUrl?: string; // iOS + audioOnly?: boolean; + automaticallyWaitsToMinimizeStalling?: boolean; // iOS + backBufferDurationMs?: number; // Android + bufferConfig?: BufferConfig; // Android + contentStartTime?: number; // Android + controls?: boolean; + currentPlaybackTime?: number; // Android + disableFocus?: boolean; + disableDisconnectError?: boolean; // Android + filter?: Filter; // iOS + filterEnabled?: boolean; // iOS + focusable?: boolean; // Android + fullscreen?: boolean; // iOS + fullscreenAutorotate?: boolean; // iOS + fullscreenOrientation?: 'all' | 'landscape' | 'portrait'; // iOS + hideShutterView?: boolean; // Android + ignoreSilentSwitch?: 'inherit' | 'ignore' | 'obey' // iOS + minLoadRetryCount?: number; // Android + maxBitRate?: number; + mixWithOthers?: 'inherit' | 'mix' | 'duck'; // iOS + muted?: boolean; + paused?: boolean; + pictureInPicture?: boolean // iOS + playInBackground?: boolean; + playWhenInactive?: boolean // iOS + poster?: string; + posterResizeMode?: 'contain' | 'center' | 'cover' | 'none' | 'repeat' | 'stretch'; + preferredForwardBufferDuration?: number// iOS + preventsDisplaySleepDuringVideoPlayback?: boolean; + progressUpdateInterval?: number; + rate?: number; + repeat?: boolean; + reportBandwidth?: boolean; //Android + resizeMode?: 'none' | 'contain' | 'cover' | 'stretch'; + selectedAudioTrack?: SelectedTrack; + selectedTextTrack?: SelectedTrack; + selectedVideoTrack?: SelectedVideoTrack; // android + subtitleStyle?: SubtitleStyle // android + textTracks?: TextTracks; + trackId?: string; // Android + useTextureView?: boolean; // Android + useSecureView?: boolean; // Android + volume?: number; + localSourceEncryptionKeyScheme?: string; +} From ddcb2527d36f697fe61dfb25e8e2500d4a71025d Mon Sep 17 00:00:00 2001 From: jiyong1 Date: Sun, 16 Apr 2023 00:17:07 +0900 Subject: [PATCH 07/58] refactor: js to ts --- VideoResizeMode.js | 7 ------- DRMType.js => src/lib/DRMType.ts | 0 FilterType.js => src/lib/FilterType.ts | 2 +- src/lib/TextTrackType.ts | 5 +++++ src/lib/VideoResizeMode.ts | 5 +++++ 5 files changed, 11 insertions(+), 8 deletions(-) delete mode 100644 VideoResizeMode.js rename DRMType.js => src/lib/DRMType.ts (100%) rename FilterType.js => src/lib/FilterType.ts (96%) create mode 100644 src/lib/TextTrackType.ts create mode 100644 src/lib/VideoResizeMode.ts diff --git a/VideoResizeMode.js b/VideoResizeMode.js deleted file mode 100644 index 15a897739e..0000000000 --- a/VideoResizeMode.js +++ /dev/null @@ -1,7 +0,0 @@ -import keyMirror from 'keymirror'; - -export default keyMirror({ - contain: null, - cover: null, - stretch: null, -}); diff --git a/DRMType.js b/src/lib/DRMType.ts similarity index 100% rename from DRMType.js rename to src/lib/DRMType.ts diff --git a/FilterType.js b/src/lib/FilterType.ts similarity index 96% rename from FilterType.js rename to src/lib/FilterType.ts index 3d8d9cb40c..5616c43ca1 100644 --- a/FilterType.js +++ b/src/lib/FilterType.ts @@ -1,5 +1,5 @@ export default { - NONE: '', + NONE: 'None', INVERT: 'CIColorInvert', MONOCHROME: 'CIColorMonochrome', POSTERIZE: 'CIColorPosterize', diff --git a/src/lib/TextTrackType.ts b/src/lib/TextTrackType.ts new file mode 100644 index 0000000000..9f06e5dd58 --- /dev/null +++ b/src/lib/TextTrackType.ts @@ -0,0 +1,5 @@ +export default { + SRT: 'application/x-subrip', + TTML: 'application/ttml+xml', + VTT: 'text/vtt', +}; diff --git a/src/lib/VideoResizeMode.ts b/src/lib/VideoResizeMode.ts new file mode 100644 index 0000000000..99955a062f --- /dev/null +++ b/src/lib/VideoResizeMode.ts @@ -0,0 +1,5 @@ +export default { + contain: 'contain', + cover: 'cover', + stretch: 'stretch', +}; From 37c5a02e2c1c49a5a9037fd59bf8665953a4956a Mon Sep 17 00:00:00 2001 From: jiyong1 Date: Sun, 16 Apr 2023 00:17:30 +0900 Subject: [PATCH 08/58] chore: update pacakge --- package.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 53eabded6b..5171f7eed9 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ }, "devDependencies": { "@react-native-community/eslint-config": "^0.0.5", + "@types/react": "^18.0.35", "eslint": "^6.5.1", - "react": "16.9.0", - "react-native": "0.61.5", - "react-native-windows": "^0.61.0-0" + "react": "^18.2.0", + "react-native": "^0.71.6", + "react-native-windows": "^0.61.0-0", + "typescript": "^5.0.4" }, "dependencies": { "deprecated-react-native-prop-types": "^2.2.0", @@ -36,10 +38,7 @@ "android", "ios", "windows", - "FilterType.js", - "DRMType.js", - "TextTrackType.js", - "VideoResizeMode.js", + "src", "react-native-video.podspec", "docs" ] From 43a8645dc00bd720986d74d47a9958126c898bf3 Mon Sep 17 00:00:00 2001 From: jiyong1 Date: Thu, 20 Apr 2023 19:44:18 +0900 Subject: [PATCH 09/58] fabric support for js --- TextTrackType.js | 5 - Video.js | 563 ------------------------- examples/basic/src/VideoPlayer.ios.tsx | 313 ++++++++------ src/MyVideo.tsx | 1 - src/VideoView.tsx | 43 ++ src/index.ts | 7 + src/types/language.ts | 184 ++++++++ src/utils.ts | 19 + 8 files changed, 446 insertions(+), 689 deletions(-) delete mode 100644 TextTrackType.js delete mode 100644 Video.js create mode 100644 src/VideoView.tsx create mode 100644 src/index.ts create mode 100644 src/types/language.ts create mode 100644 src/utils.ts diff --git a/TextTrackType.js b/TextTrackType.js deleted file mode 100644 index 9f06e5dd58..0000000000 --- a/TextTrackType.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - SRT: 'application/x-subrip', - TTML: 'application/ttml+xml', - VTT: 'text/vtt', -}; diff --git a/Video.js b/Video.js deleted file mode 100644 index 86b75dae8e..0000000000 --- a/Video.js +++ /dev/null @@ -1,563 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native'; -import { ViewPropTypes, ImagePropTypes } from 'deprecated-react-native-prop-types'; -import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; -import TextTrackType from './TextTrackType'; -import FilterType from './FilterType'; -import DRMType from './DRMType'; -import VideoResizeMode from './VideoResizeMode.js'; - -const styles = StyleSheet.create({ - base: { - overflow: 'hidden', - }, -}); - -const { VideoDecoderProperties } = NativeModules -export { TextTrackType, FilterType, DRMType, VideoDecoderProperties } - -export default class Video extends Component { - - constructor(props) { - super(props); - - this.state = { - showPoster: !!props.poster, - }; - } - - setNativeProps(nativeProps) { - this._root.setNativeProps(nativeProps); - } - - toTypeString(x) { - switch (typeof x) { - case 'object': - return x instanceof Date - ? x.toISOString() - : JSON.stringify(x); // object, null - case 'undefined': - return ''; - default: // boolean, number, string - return x.toString(); - } - } - - stringsOnlyObject(obj) { - const strObj = {}; - - Object.keys(obj).forEach(x => { - strObj[x] = this.toTypeString(obj[x]); - }); - - return strObj; - } - - seek = (time, tolerance = 100) => { - if (isNaN(time)) {throw new Error('Specified time is not a number');} - - if (Platform.OS === 'ios') { - this.setNativeProps({ - seek: { - time, - tolerance, - }, - }); - } else { - this.setNativeProps({ seek: time }); - } - }; - - presentFullscreenPlayer = () => { - this.setNativeProps({ fullscreen: true }); - }; - - dismissFullscreenPlayer = () => { - this.setNativeProps({ fullscreen: false }); - }; - - save = async (options) => { - return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); - } - - restoreUserInterfaceForPictureInPictureStopCompleted = (restored) => { - this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restored }); - }; - - _assignRoot = (component) => { - this._root = component; - }; - - _hidePoster = () => { - if (this.state.showPoster) { - this.setState({ showPoster: false }); - } - } - - _onLoadStart = (event) => { - if (this.props.onLoadStart) { - this.props.onLoadStart(event.nativeEvent); - } - }; - - _onPlaybackStateChanged = (event) => { - if (this.props.onPlaybackStateChanged) { - this.props.onPlaybackStateChanged(event.nativeEvent); - } - }; - - _onLoad = (event) => { - // Need to hide poster here for windows as onReadyForDisplay is not implemented - if (Platform.OS === 'windows') { - this._hidePoster(); - } - if (this.props.onLoad) { - this.props.onLoad(event.nativeEvent); - } - }; - - _onAudioTracks = (event) => { - if (this.props.onAudioTracks) { - this.props.onAudioTracks(event.nativeEvent); - } - }; - - _onTextTracks = (event) => { - if (this.props.onTextTracks) { - this.props.onTextTracks(event.nativeEvent); - } - }; - - _onVideoTracks = (event) => { - if (this.props.onVideoTracks) { - this.props.onVideoTracks(event.nativeEvent); - } - }; - - _onError = (event) => { - if (this.props.onError) { - this.props.onError(event.nativeEvent); - } - }; - - _onProgress = (event) => { - if (this.props.onProgress) { - this.props.onProgress(event.nativeEvent); - } - }; - - _onBandwidthUpdate = (event) => { - if (this.props.onBandwidthUpdate) { - this.props.onBandwidthUpdate(event.nativeEvent); - } - }; - - _onSeek = (event) => { - if (this.props.onSeek) { - this.props.onSeek(event.nativeEvent); - } - }; - - _onEnd = (event) => { - if (this.props.onEnd) { - this.props.onEnd(event.nativeEvent); - } - }; - - _onTimedMetadata = (event) => { - if (this.props.onTimedMetadata) { - this.props.onTimedMetadata(event.nativeEvent); - } - }; - - _onFullscreenPlayerWillPresent = (event) => { - if (this.props.onFullscreenPlayerWillPresent) { - this.props.onFullscreenPlayerWillPresent(event.nativeEvent); - } - }; - - _onFullscreenPlayerDidPresent = (event) => { - if (this.props.onFullscreenPlayerDidPresent) { - this.props.onFullscreenPlayerDidPresent(event.nativeEvent); - } - }; - - _onFullscreenPlayerWillDismiss = (event) => { - if (this.props.onFullscreenPlayerWillDismiss) { - this.props.onFullscreenPlayerWillDismiss(event.nativeEvent); - } - }; - - _onFullscreenPlayerDidDismiss = (event) => { - if (this.props.onFullscreenPlayerDidDismiss) { - this.props.onFullscreenPlayerDidDismiss(event.nativeEvent); - } - }; - - _onReadyForDisplay = (event) => { - if (!this.props.audioOnly) { - this._hidePoster(); - } - - if (this.props.onReadyForDisplay) { - this.props.onReadyForDisplay(event.nativeEvent); - } - }; - - _onPlaybackStalled = (event) => { - if (this.props.onPlaybackStalled) { - this.props.onPlaybackStalled(event.nativeEvent); - } - }; - - _onPlaybackResume = (event) => { - if (this.props.onPlaybackResume) { - this.props.onPlaybackResume(event.nativeEvent); - } - }; - - _onPlaybackRateChange = (event) => { - if (this.props.onPlaybackRateChange) { - this.props.onPlaybackRateChange(event.nativeEvent); - } - }; - - _onExternalPlaybackChange = (event) => { - if (this.props.onExternalPlaybackChange) { - this.props.onExternalPlaybackChange(event.nativeEvent); - } - } - - _onAudioBecomingNoisy = () => { - if (this.props.onAudioBecomingNoisy) { - this.props.onAudioBecomingNoisy(); - } - }; - - _onPictureInPictureStatusChanged = (event) => { - if (this.props.onPictureInPictureStatusChanged) { - this.props.onPictureInPictureStatusChanged(event.nativeEvent); - } - }; - - _onRestoreUserInterfaceForPictureInPictureStop = (event) => { - if (this.props.onRestoreUserInterfaceForPictureInPictureStop) { - this.props.onRestoreUserInterfaceForPictureInPictureStop(); - } - }; - - _onAudioFocusChanged = (event) => { - if (this.props.onAudioFocusChanged) { - this.props.onAudioFocusChanged(event.nativeEvent); - } - }; - - _onBuffer = (event) => { - if (this.props.onBuffer) { - this.props.onBuffer(event.nativeEvent); - } - }; - - _onGetLicense = (event) => { - if (this.props.drm && this.props.drm.getLicense instanceof Function) { - const data = event.nativeEvent; - if (data && data.spcBase64) { - const getLicenseOverride = this.props.drm.getLicense(data.spcBase64, data.contentId, data.licenseUrl); - const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. - getLicensePromise.then((result => { - if (result !== undefined) { - NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); - } else { - NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); - } - })).catch((error) => { - NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); - }); - } else { - NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', findNodeHandle(this._root)); - } - } - } - - _onReceiveAdEvent = (event) => { - if (this.props.onReceiveAdEvent) { - this.props.onReceiveAdEvent(event.nativeEvent); - } - }; - - getViewManagerConfig = viewManagerName => { - if (!UIManager.getViewManagerConfig) { - return UIManager[viewManagerName]; - } - return UIManager.getViewManagerConfig(viewManagerName); - }; - - render() { - const resizeMode = this.props.resizeMode; - const source = resolveAssetSource(this.props.source) || {}; - const shouldCache = !source.__packager_asset; - - let uri = source.uri || ''; - if (uri && uri.match(/^\//)) { - uri = `file://${uri}`; - } - - if (!uri) { - console.log('Trying to load empty source.'); - } - - const isNetwork = !!(uri && uri.match(/^https?:/i)); - const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/i)); - - if ((uri || uri === '') && !isNetwork && !isAsset) { - if (this.props.onError) { - this.props.onError({error: {errorString: 'invalid url, player will stop', errorCode: 'INVALID_URL'}}); - } - } - - let nativeResizeMode; - const RCTVideoInstance = this.getViewManagerConfig('RCTVideo'); - - if (resizeMode === VideoResizeMode.stretch) { - nativeResizeMode = RCTVideoInstance.Constants.ScaleToFill; - } else if (resizeMode === VideoResizeMode.contain) { - nativeResizeMode = RCTVideoInstance.Constants.ScaleAspectFit; - } else if (resizeMode === VideoResizeMode.cover) { - nativeResizeMode = RCTVideoInstance.Constants.ScaleAspectFill; - } else { - nativeResizeMode = RCTVideoInstance.Constants.ScaleNone; - } - - const nativeProps = Object.assign({}, this.props); - Object.assign(nativeProps, { - style: [styles.base, nativeProps.style], - resizeMode: nativeResizeMode, - src: { - uri, - isNetwork, - isAsset, - shouldCache, - type: source.type || '', - mainVer: source.mainVer || 0, - patchVer: source.patchVer || 0, - requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, - startTime: source.startTime || 0, - endTime: source.endTime - }, - onVideoLoadStart: this._onLoadStart, - onVideoPlaybackStateChanged: this._onPlaybackStateChanged, - onVideoLoad: this._onLoad, - onAudioTracks: this._onAudioTracks, - onTextTracks: this._onTextTracks, - onVideoTracks: this._onVideoTracks, - onVideoError: this._onError, - onVideoProgress: this._onProgress, - onVideoSeek: this._onSeek, - onVideoEnd: this._onEnd, - onVideoBuffer: this._onBuffer, - onVideoBandwidthUpdate: this._onBandwidthUpdate, - onTimedMetadata: this._onTimedMetadata, - onVideoAudioBecomingNoisy: this._onAudioBecomingNoisy, - onVideoExternalPlaybackChange: this._onExternalPlaybackChange, - onVideoFullscreenPlayerWillPresent: this._onFullscreenPlayerWillPresent, - onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent, - onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, - onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, - onReadyForDisplay: this._onReadyForDisplay, - onPlaybackStalled: this._onPlaybackStalled, - onPlaybackResume: this._onPlaybackResume, - onPlaybackRateChange: this._onPlaybackRateChange, - onAudioFocusChanged: this._onAudioFocusChanged, - onAudioBecomingNoisy: this._onAudioBecomingNoisy, - onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense, - onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, - onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, - onReceiveAdEvent: this._onReceiveAdEvent, - }); - - const posterStyle = { - ...StyleSheet.absoluteFillObject, - resizeMode: this.props.posterResizeMode || 'contain', - }; - - return ( - - - {this.state.showPoster && ( - - )} - - ); - } -} - -Video.propTypes = { - filter: PropTypes.oneOf([ - FilterType.NONE, - FilterType.INVERT, - FilterType.MONOCHROME, - FilterType.POSTERIZE, - FilterType.FALSE, - FilterType.MAXIMUMCOMPONENT, - FilterType.MINIMUMCOMPONENT, - FilterType.CHROME, - FilterType.FADE, - FilterType.INSTANT, - FilterType.MONO, - FilterType.NOIR, - FilterType.PROCESS, - FilterType.TONAL, - FilterType.TRANSFER, - FilterType.SEPIA, - ]), - filterEnabled: PropTypes.bool, - onVideoLoadStart: PropTypes.func, - onVideoLoad: PropTypes.func, - onVideoBuffer: PropTypes.func, - onVideoError: PropTypes.func, - onVideoProgress: PropTypes.func, - onVideoBandwidthUpdate: PropTypes.func, - onVideoSeek: PropTypes.func, - onVideoEnd: PropTypes.func, - onTimedMetadata: PropTypes.func, - onVideoAudioBecomingNoisy: PropTypes.func, - onVideoExternalPlaybackChange: PropTypes.func, - onVideoFullscreenPlayerWillPresent: PropTypes.func, - onVideoFullscreenPlayerDidPresent: PropTypes.func, - onVideoFullscreenPlayerWillDismiss: PropTypes.func, - onVideoFullscreenPlayerDidDismiss: PropTypes.func, - - /* Wrapper component */ - source: PropTypes.oneOfType([ - PropTypes.shape({ - uri: PropTypes.string, - }), - // Opaque type returned by require('./video.mp4') - PropTypes.number, - ]), - drm: PropTypes.shape({ - type: PropTypes.oneOf([ - DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY, - ]), - licenseServer: PropTypes.string, - headers: PropTypes.shape({}), - base64Certificate: PropTypes.bool, - certificateUrl: PropTypes.string, - getLicense: PropTypes.func, - }), - localSourceEncryptionKeyScheme: PropTypes.string, - minLoadRetryCount: PropTypes.number, - maxBitRate: PropTypes.number, - resizeMode: PropTypes.string, - poster: PropTypes.string, - posterResizeMode: ImagePropTypes.resizeMode, - repeat: PropTypes.bool, - automaticallyWaitsToMinimizeStalling: PropTypes.bool, - allowsExternalPlayback: PropTypes.bool, - selectedAudioTrack: PropTypes.shape({ - type: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - }), - selectedVideoTrack: PropTypes.shape({ - type: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - }), - selectedTextTrack: PropTypes.shape({ - type: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - }), - textTracks: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string, - uri: PropTypes.string.isRequired, - type: PropTypes.oneOf([ - TextTrackType.SRT, - TextTrackType.TTML, - TextTrackType.VTT, - ]), - language: PropTypes.string.isRequired, - }) - ), - paused: PropTypes.bool, - muted: PropTypes.bool, - volume: PropTypes.number, - bufferConfig: PropTypes.shape({ - minBufferMs: PropTypes.number, - maxBufferMs: PropTypes.number, - bufferForPlaybackMs: PropTypes.number, - bufferForPlaybackAfterRebufferMs: PropTypes.number, - maxHeapAllocationPercent: PropTypes.number, - }), - rate: PropTypes.number, - pictureInPicture: PropTypes.bool, - playInBackground: PropTypes.bool, - preferredForwardBufferDuration: PropTypes.number, - playWhenInactive: PropTypes.bool, - ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), - reportBandwidth: PropTypes.bool, - contentStartTime: PropTypes.number, - disableFocus: PropTypes.bool, - focusable: PropTypes.bool, - disableBuffering: PropTypes.bool, - controls: PropTypes.bool, - audioOnly: PropTypes.bool, - fullscreenAutorotate: PropTypes.bool, - fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), - progressUpdateInterval: PropTypes.number, - subtitleStyle: PropTypes.shape({ - paddingTop: PropTypes.number, - paddingBottom: PropTypes.number, - paddingLeft: PropTypes.number, - paddingRight: PropTypes.number, - fontSize: PropTypes.number, - }), - useTextureView: PropTypes.bool, - useSecureView: PropTypes.bool, - hideShutterView: PropTypes.bool, - shutterColor: PropTypes.string, - onLoadStart: PropTypes.func, - onPlaybackStateChanged: PropTypes.func, - onLoad: PropTypes.func, - onAudioTracks: PropTypes.func, - onTextTracks: PropTypes.func, - onVideoTracks: PropTypes.func, - onBuffer: PropTypes.func, - onError: PropTypes.func, - onProgress: PropTypes.func, - onBandwidthUpdate: PropTypes.func, - onSeek: PropTypes.func, - onEnd: PropTypes.func, - onFullscreenPlayerWillPresent: PropTypes.func, - onFullscreenPlayerDidPresent: PropTypes.func, - onFullscreenPlayerWillDismiss: PropTypes.func, - onFullscreenPlayerDidDismiss: PropTypes.func, - onReadyForDisplay: PropTypes.func, - onPlaybackStalled: PropTypes.func, - onPlaybackResume: PropTypes.func, - onPlaybackRateChange: PropTypes.func, - onAudioFocusChanged: PropTypes.func, - onAudioBecomingNoisy: PropTypes.func, - onPictureInPictureStatusChanged: PropTypes.func, - onExternalPlaybackChange: PropTypes.func, - adTagUrl: PropTypes.string, - onReceiveAdEvent: PropTypes.func, - - /* Required by react-native */ - ...ViewPropTypes, -}; - -const RCTVideo = requireNativeComponent('RCTVideo'); diff --git a/examples/basic/src/VideoPlayer.ios.tsx b/examples/basic/src/VideoPlayer.ios.tsx index 740251047a..08a72220a2 100644 --- a/examples/basic/src/VideoPlayer.ios.tsx +++ b/examples/basic/src/VideoPlayer.ios.tsx @@ -1,7 +1,5 @@ 'use strict'; -import React, { - Component -} from 'react'; +import React, {Component} from 'react'; import { Alert, @@ -12,25 +10,25 @@ import { View, } from 'react-native'; -import Video,{FilterType} from 'react-native-video'; +import Video, {FilterType} from 'react-native-video'; const filterTypes = [ - FilterType.NONE, - FilterType.INVERT, - FilterType.MONOCHROME, - FilterType.POSTERIZE, - FilterType.FALSE, - FilterType.MAXIMUMCOMPONENT, - FilterType.MINIMUMCOMPONENT, - FilterType.CHROME, - FilterType.FADE, - FilterType.INSTANT, - FilterType.MONO, - FilterType.NOIR, - FilterType.PROCESS, - FilterType.TONAL, - FilterType.TRANSFER, - FilterType.SEPIA + FilterType.NONE, + FilterType.INVERT, + FilterType.MONOCHROME, + FilterType.POSTERIZE, + FilterType.FALSE, + FilterType.MAXIMUMCOMPONENT, + FilterType.MINIMUMCOMPONENT, + FilterType.CHROME, + FilterType.FADE, + FilterType.INSTANT, + FilterType.MONO, + FilterType.NOIR, + FilterType.PROCESS, + FilterType.TONAL, + FilterType.TRANSFER, + FilterType.SEPIA, ]; class VideoPlayer extends Component { @@ -54,7 +52,7 @@ class VideoPlayer extends Component { mixWithOthers: null, isBuffering: false, filter: FilterType.NONE, - filterEnabled: true + filterEnabled: true, }; onLoad(data: any) { @@ -66,8 +64,8 @@ class VideoPlayer extends Component { this.setState({currentTime: data.currentTime}); } - onBuffer({ isBuffering }: { isBuffering: boolean }) { - this.setState({ isBuffering }); + onBuffer({isBuffering}: {isBuffering: boolean}) { + this.setState({isBuffering}); } getCurrentTimePercentage() { @@ -88,19 +86,28 @@ class VideoPlayer extends Component { } this.setState({ - filter: filterTypes[index] - }) + filter: filterTypes[index], + }); } renderSkinControl(skin) { const isSelected = this.state.skin == skin; const selectControls = skin == 'native' || skin == 'embed'; return ( - { this.setState({ - controls: selectControls, - skin: skin - }) }}> - + { + this.setState({ + controls: selectControls, + skin: skin, + }); + }} + > + {skin} @@ -108,63 +115,108 @@ class VideoPlayer extends Component { } renderRateControl(rate: number) { - const isSelected = (this.state.rate == rate); + const isSelected = this.state.rate == rate; return ( - { this.setState({rate: rate}) }}> - + { + this.setState({rate: rate}); + }} + > + {rate}x - ) + ); } renderResizeModeControl(resizeMode: string) { - const isSelected = (this.state.resizeMode == resizeMode); + const isSelected = this.state.resizeMode == resizeMode; return ( - { this.setState({resizeMode: resizeMode}) }}> - + { + this.setState({resizeMode: resizeMode}); + }} + > + {resizeMode} - ) + ); } renderVolumeControl(volume: number) { - const isSelected = (this.state.volume == volume); + const isSelected = this.state.volume == volume; return ( - { this.setState({volume: volume}) }}> - + { + this.setState({volume: volume}); + }} + > + {volume * 100}% - ) + ); } renderIgnoreSilentSwitchControl(ignoreSilentSwitch: string) { - const isSelected = (this.state.ignoreSilentSwitch == ignoreSilentSwitch); + const isSelected = this.state.ignoreSilentSwitch == ignoreSilentSwitch; return ( - { this.setState({ignoreSilentSwitch: ignoreSilentSwitch}) }}> - + { + this.setState({ignoreSilentSwitch: ignoreSilentSwitch}); + }} + > + {ignoreSilentSwitch} - ) + ); } renderMixWithOthersControl(mixWithOthers: string) { - const isSelected = (this.state.mixWithOthers == mixWithOthers); + const isSelected = this.state.mixWithOthers == mixWithOthers; return ( - { this.setState({mixWithOthers: mixWithOthers}) }}> - + { + this.setState({mixWithOthers: mixWithOthers}); + }} + > + {mixWithOthers} - ) + ); } renderCustomSkin() { @@ -173,7 +225,12 @@ class VideoPlayer extends Component { return ( - {this.setState({paused: !this.state.paused})}}> + { + this.setState({paused: !this.state.paused}); + }} + > - { - (this.state.filterEnabled) ? - - { - this.setFilter(-1) - }}> - Previous Filter - - { - this.setFilter(1) - }}> - Next Filter - - : null - } + {this.state.filterEnabled ? ( + + { + this.setFilter(-1); + }} + > + Previous Filter + + { + this.setFilter(1); + }} + > + Next Filter + + + ) : null} @@ -237,25 +299,28 @@ class VideoPlayer extends Component { - { - (Platform.OS === 'ios') ? - <> - - {this.renderIgnoreSilentSwitchControl('ignore')} - {this.renderIgnoreSilentSwitchControl('obey')} - - - {this.renderMixWithOthersControl('mix')} - {this.renderMixWithOthersControl('duck')} - - : null - } + {Platform.OS === 'ios' ? ( + <> + + {this.renderIgnoreSilentSwitchControl('ignore')} + {this.renderIgnoreSilentSwitchControl('obey')} + + + {this.renderMixWithOthersControl('mix')} + {this.renderMixWithOthersControl('duck')} + + + ) : null} - - + + @@ -264,7 +329,10 @@ class VideoPlayer extends Component { } renderNativeSkin() { - const videoStyle = this.state.skin == 'embed' ? styles.nativeVideoControls : styles.fullScreen; + const videoStyle = + this.state.skin == 'embed' + ? styles.nativeVideoControls + : styles.fullScreen; return ( @@ -281,7 +349,9 @@ class VideoPlayer extends Component { onLoad={this.onLoad} onBuffer={this.onBuffer} onProgress={this.onProgress} - onEnd={() => { Alert.alert('Done!') }} + onEnd={() => { + Alert.alert('Done!'); + }} repeat={true} controls={this.state.controls} filter={this.state.filter} @@ -295,21 +365,24 @@ class VideoPlayer extends Component { {this.renderSkinControl('native')} {this.renderSkinControl('embed')} - { - (this.state.filterEnabled) ? - - { - this.setFilter(-1) - }}> - Previous Filter - - { - this.setFilter(1) - }}> - Next Filter - - : null - } + {this.state.filterEnabled ? ( + + { + this.setFilter(-1); + }} + > + Previous Filter + + { + this.setFilter(1); + }} + > + Next Filter + + + ) : null} @@ -331,28 +404,28 @@ class VideoPlayer extends Component { - { - (Platform.OS === 'ios') ? - <> - - {this.renderIgnoreSilentSwitchControl('ignore')} - {this.renderIgnoreSilentSwitchControl('obey')} - - - {this.renderMixWithOthersControl('mix')} - {this.renderMixWithOthersControl('duck')} - - : null - } + {Platform.OS === 'ios' ? ( + <> + + {this.renderIgnoreSilentSwitchControl('ignore')} + {this.renderIgnoreSilentSwitchControl('obey')} + + + {this.renderMixWithOthersControl('mix')} + {this.renderMixWithOthersControl('duck')} + + + ) : null} - ); } render() { - return this.state.controls ? this.renderNativeSkin() : this.renderCustomSkin(); + return this.state.controls + ? this.renderNativeSkin() + : this.renderCustomSkin(); } } @@ -371,7 +444,7 @@ const styles = StyleSheet.create({ right: 0, }, controls: { - backgroundColor: "transparent", + backgroundColor: 'transparent', borderRadius: 5, position: 'absolute', bottom: 44, @@ -417,24 +490,24 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }, ignoreSilentSwitchControl: { flex: 1, flexDirection: 'row', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }, mixWithOthersControl: { flex: 1, flexDirection: 'row', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }, controlOption: { alignSelf: 'center', fontSize: 11, - color: "white", + color: 'white', paddingLeft: 2, paddingRight: 2, lineHeight: 12, @@ -450,4 +523,4 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, }); -export default VideoPlayer \ No newline at end of file +export default VideoPlayer; diff --git a/src/MyVideo.tsx b/src/MyVideo.tsx index 7cf7cecfef..6d80113740 100644 --- a/src/MyVideo.tsx +++ b/src/MyVideo.tsx @@ -13,7 +13,6 @@ import { Platform, } from "react-native"; import RNCVideoComponent, { Commands, OnAudioFocusChangedData, OnPlaybackStateChangedData, OnVideoErrorData } from "./fabric/VideoNativeComponent"; - import type { StyleProp, ImageStyle, NativeSyntheticEvent } from "react-native"; import type { VideoComponentType, diff --git a/src/VideoView.tsx b/src/VideoView.tsx new file mode 100644 index 0000000000..2300a642a3 --- /dev/null +++ b/src/VideoView.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import type { VideoNativeProps } from './fabric/VideoNativeComponent'; +import RNCVideoComponent from './fabric/VideoNativeComponent' +import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource' +import type { ResolvedAssetSource } from 'react-native/Libraries/Image/AssetSourceResolver'; + +export interface VideoViewProps extends Omit { + source: { + uri: string + } | number +} + +export default function VideoView({ source: sourceProp, ...props }: VideoViewProps) { + const source = resolveAssetSource(sourceProp) || {} as ResolvedAssetSource; + const shouldCache = !(source as any).__packager_asset; + + let uri = source.uri || ''; + if (uri && uri.match(/^\//)) { + uri = `file://${uri}`; + } + + const isNetwork = !!(uri && uri.match(/^https?:/i)); + const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/i)); + + + return ( + + ) + } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..936c3a0e29 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +import Video from './MyVideo'; +export { default as FilterType } from './lib/FilterType'; +export { default as VideoResizeMode } from './lib/VideoResizeMode'; +export { default as TextTrackType } from './lib/TextTrackType'; +export { default as DRMType } from './lib/DRMType'; +export { VideoRef } from './MyVideo'; +export default Video; diff --git a/src/types/language.ts b/src/types/language.ts new file mode 100644 index 0000000000..8aa3b3116b --- /dev/null +++ b/src/types/language.ts @@ -0,0 +1,184 @@ +export type ISO639_1 = + | 'aa' + | 'ab' + | 'ae' + | 'af' + | 'ak' + | 'am' + | 'an' + | 'ar' + | 'as' + | 'av' + | 'ay' + | 'az' + | 'ba' + | 'be' + | 'bg' + | 'bi' + | 'bm' + | 'bn' + | 'bo' + | 'br' + | 'bs' + | 'ca' + | 'ce' + | 'ch' + | 'co' + | 'cr' + | 'cs' + | 'cu' + | 'cv' + | 'cy' + | 'da' + | 'de' + | 'dv' + | 'dz' + | 'ee' + | 'el' + | 'en' + | 'eo' + | 'es' + | 'et' + | 'eu' + | 'fa' + | 'ff' + | 'fi' + | 'fj' + | 'fo' + | 'fr' + | 'fy' + | 'ga' + | 'gd' + | 'gl' + | 'gn' + | 'gu' + | 'gv' + | 'ha' + | 'he' + | 'hi' + | 'ho' + | 'hr' + | 'ht' + | 'hu' + | 'hy' + | 'hz' + | 'ia' + | 'id' + | 'ie' + | 'ig' + | 'ii' + | 'ik' + | 'io' + | 'is' + | 'it' + | 'iu' + | 'ja' + | 'jv' + | 'ka' + | 'kg' + | 'ki' + | 'kj' + | 'kk' + | 'kl' + | 'km' + | 'kn' + | 'ko' + | 'kr' + | 'ks' + | 'ku' + | 'kv' + | 'kw' + | 'ky' + | 'la' + | 'lb' + | 'lg' + | 'li' + | 'ln' + | 'lo' + | 'lt' + | 'lu' + | 'lv' + | 'mg' + | 'mh' + | 'mi' + | 'mk' + | 'ml' + | 'mn' + | 'mr' + | 'ms' + | 'mt' + | 'my' + | 'na' + | 'nb' + | 'nd' + | 'ne' + | 'ng' + | 'nl' + | 'nn' + | 'no' + | 'nr' + | 'nv' + | 'ny' + | 'oc' + | 'oj' + | 'om' + | 'or' + | 'os' + | 'pa' + | 'pi' + | 'pl' + | 'ps' + | 'pt' + | 'qu' + | 'rm' + | 'rn' + | 'ro' + | 'ru' + | 'rw' + | 'sa' + | 'sc' + | 'sd' + | 'se' + | 'sg' + | 'si' + | 'sk' + | 'sl' + | 'sm' + | 'sn' + | 'so' + | 'sq' + | 'sr' + | 'ss' + | 'st' + | 'su' + | 'sv' + | 'sw' + | 'ta' + | 'te' + | 'tg' + | 'th' + | 'ti' + | 'tk' + | 'tl' + | 'tn' + | 'to' + | 'tr' + | 'ts' + | 'tt' + | 'tw' + | 'ty' + | 'ug' + | 'uk' + | 'ur' + | 'uz' + | 've' + | 'vi' + | 'vo' + | 'wa' + | 'wo' + | 'xh' + | 'yi' + | 'yo' + | 'za' + | 'zh' + | 'zu'; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000000..fb2a110225 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,19 @@ +import { Image } from "react-native"; +import type { ImageSourcePropType } from 'react-native'; +import type { ReactVideoSource } from './types/video'; + +type Source = ImageSourcePropType | ReactVideoSource; + +export function generateHeaderForNative(obj?: Record) { + if (!obj) return []; + return Object.entries(obj).map(([ key, value]) => ({ key, value })); +} + +export function resolveAssetSourceForVideo(source: Source): ReactVideoSource { + if (typeof source === 'number') { + return { + uri: Image.resolveAssetSource(source).uri, + }; + } + return source as ReactVideoSource; +} \ No newline at end of file From 8ba795094603557d0d13988596f302cecc5f951d Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 26 Apr 2023 08:57:09 +0900 Subject: [PATCH 10/58] feat: :zap: added gitignore From 68320b0881d0c9f05e5911192f07520fed6c775f Mon Sep 17 00:00:00 2001 From: yungblud Date: Tue, 2 May 2023 20:47:31 +0900 Subject: [PATCH 11/58] feat: modified seek, headers --- .../exoplayer/ReactExoplayerViewManager.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 4b76f4e917..8050e8783e 100644 --- a/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -5,6 +5,8 @@ import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -123,6 +125,17 @@ public void onDropViewInstance(ReactExoplayerView view) { ); } + @Override + public void receiveCommand(@NonNull ReactExoplayerView root, String commandId, @androidx.annotation.Nullable ReadableArray args) { + switch (commandId) { + case "seek": + this.setSeek(root, args.getInt(0)); + break; + default: + break; + } + } + @ReactProp(name = PROP_DRM) public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) { if (drm != null && drm.hasKey(PROP_DRM_TYPE)) { @@ -155,7 +168,21 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src int startTimeMs = src.hasKey(PROP_SRC_START_TIME) ? src.getInt(PROP_SRC_START_TIME) : -1; int endTimeMs = src.hasKey(PROP_SRC_END_TIME) ? src.getInt(PROP_SRC_END_TIME) : -1; String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; - Map headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; + + Map headers = new HashMap<>(); + ReadableArray propSrcHeadersArray = (src.hasKey(PROP_SRC_HEADERS)) ? src.getArray(PROP_SRC_HEADERS) : null; + 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); + } + } + } + } if (TextUtils.isEmpty(uriString)) { videoView.clearSrc(); From 153186dfec46aae52b855ade02b9389273a8d979 Mon Sep 17 00:00:00 2001 From: yungblud Date: Tue, 2 May 2023 21:51:53 +0900 Subject: [PATCH 12/58] fix: fixed drm headers type android --- .../exoplayer/ReactExoplayerViewManager.kt | 30 +++++++++---------- .../exoplayer/ReactExoplayerViewManager.java | 14 +++++---- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt b/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt index b056aef2ba..837f588944 100644 --- a/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt +++ b/android/src/fabric/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt @@ -6,7 +6,6 @@ import com.brentvatne.exoplayer.events.* import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.ReadableMapKeySetIterator import com.facebook.react.common.MapBuilder import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext @@ -160,25 +159,26 @@ internal class ReactExoplayerViewManager() : ViewGroupManager = ArrayList() - val itr: ReadableMapKeySetIterator = drmHeaders.keySetIterator() - while (itr.hasNextKey()) { - val key: String = itr.nextKey() + for (i in 0 until drmHeadersArray.size()) { + val current = drmHeadersArray.getMap(i) + val key = if (current.hasKey("key")) current.getString("key") else null + val value = if (current.hasKey("value")) current.getString("value") else null drmKeyRequestPropertiesList.add(key) - drmKeyRequestPropertiesList.add(drmHeaders.getString(key)) + drmKeyRequestPropertiesList.add(value) } - view.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(arrayOfNulls(0))) + view?.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(arrayOfNulls(0))) } - view.setUseTextureView(false) + view?.setUseTextureView(false) } } } diff --git a/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 8050e8783e..31a1d86d66 100644 --- a/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/oldarch/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -141,18 +141,20 @@ public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm if (drm != null && drm.hasKey(PROP_DRM_TYPE)) { String drmType = drm.hasKey(PROP_DRM_TYPE) ? drm.getString(PROP_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_DRM_LICENSESERVER) ? drm.getString(PROP_DRM_LICENSESERVER) : null; - ReadableMap drmHeaders = drm.hasKey(PROP_DRM_HEADERS) ? drm.getMap(PROP_DRM_HEADERS) : null; + ReadableArray drmHeadersArray = (drm.hasKey(PROP_DRM_HEADERS)) ? drm.getArray(PROP_DRM_HEADERS) : null; + if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { UUID drmUUID = Util.getDrmUuid(drmType); videoView.setDrmType(drmUUID); videoView.setDrmLicenseUrl(drmLicenseServer); - if (drmHeaders != null) { + if (drmHeadersArray != null) { ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); - while (itr.hasNextKey()) { - String key = itr.nextKey(); + for (int i = 0; i < drmHeadersArray.size(); i++) { + ReadableMap current = drmHeadersArray.getMap(i); + String key = current.hasKey("key") ? current.getString("key") : null; + String value = current.hasKey("value") ? current.getString("value") : null; drmKeyRequestPropertiesList.add(key); - drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + drmKeyRequestPropertiesList.add(value); } videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); } From 797f527c4c8a346c6de15cb711f7c922af475860 Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 3 May 2023 20:56:25 +0900 Subject: [PATCH 13/58] chore: modified type --- src/types/video.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/video.ts b/src/types/video.ts index e91a0f1165..f017851fa2 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -85,8 +85,8 @@ type TextTracks = { export interface ReactVideoProps extends ReactVideoEvents { source: ReactVideoSource; - drm: ReactVideoDrm; - style: StyleProp; + drm?: ReactVideoDrm; + style?: StyleProp; adTagUrl?: string; // iOS audioOnly?: boolean; automaticallyWaitsToMinimizeStalling?: boolean; // iOS From ace7e55c5a0d296f1a9ca7ecee2af9058a043f8d Mon Sep 17 00:00:00 2001 From: yungblud Date: Wed, 3 May 2023 20:56:34 +0900 Subject: [PATCH 14/58] feat: modified package json --- package.json | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5171f7eed9..19486a4e3a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "react-native-video", - "version": "6.0.0-alpha.7", + "version": "7.0.0-alpha.0", "description": "A