diff --git a/README.md b/README.md index 0360af30..9994b7e8 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ const playlistItem = { title: 'Track', mediaId: -1, image: 'http://image.com/image.png', - desc: 'My beautiful track', + description: 'My beautiful track', startTime: 0, file: 'http://file.com/file.mp3', autostart: true, @@ -182,26 +182,30 @@ Running the example project: ##### Config -| Prop | Description | Type | Platform | Default | -| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------- | ------- | -| **`offlineImage`** | The url for the player offline thumbnail. | `String` | `iOS` | `none` | -| **`offlineMessage`** | The message when the player is offline. | `String` | `iOS` | `none` | -| **`autostart`** | Should the tracks auto start. | `Boolean` | `iOS && Android` | `false` | -| **`controls`** | Should the control buttons show. | `Boolean` | `Android` | `true` | -| **`repeat`** | Should the track repeat. | `Boolean` | `iOS && Android` | `false` | -| **`playlist`** | An array of playlistItems. | `[playlistItem]` see [PlaylistItem](#PlaylistItem)] | `iOS && Android` | `none` | -| **`nextUpStyle`** | How the next up videos should be presented. | `{offsetSeconds: Int, offsetPercentage, Int}` | `iOS && Android` | `none` | -| **`styling`** | All the stylings for the player see [Styling](#Styling) section. | `Object` | `iOS && Android` | `none` | -| **`advertising`** | General Advertising settings on the player see [Advertising](#Advertising) section. | `Object` | `iOS && Android` | `none` | -| **`fullScreenOnLandscape`** | When this is true the player will go into full screen on rotate of phone to landscape | `Boolean` | `iOS && Android` | `false` | -| **`landscapeOnFullScreen`** | When this is true the player will go into landscape orientation when on full screen | `Boolean` | `iOS && Android` | `false` | -| **`portraitOnExitFullScreen`** | When this is true the player will go into portrait orientation when exiting full screen | `Boolean` | `Android` | `false` | -| **`exitFullScreenOnPortrait`** | When this is true the player will exit full screen when the phone goes into portrait | `Boolean` | `Android` | `false` | -| **`enableLockScreenControls`** | When this is true the player will show media controls on lock screen | `Boolean` | `iOS` | `true` | -| **`stretching`** | Resize images and video to fit player dimensions. See below [Stretching](#Stretching) section. | `String` | `Android` | `none` | -| **`backgroundAudioEnabled`** | Should the player continue playing in the background and handle interruptions. | `Boolean` | `iOS && Android` | `false` | -| **`viewOnly`** | When true the player will not have any controls it will show only the video. | `Boolean` | `iOS` | `false` | -| **`pipEnabled`** | When true the player will be able to go into Picture in Picture mode. **Note: This is true by default for iOS PlayerViewController**. **For Android you will also need to follow the instruction mentioned [here](https://developer.jwplayer.com/jwplayer/docs/android-invoke-picture-in-picture-playback) && below [Picture in picture](Picture-in-picture) section.** | `Boolean` | `iOS when viewOnly prop is true && Android` | `false` | +| Prop | Description | Type | Platform | Default | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -------- | +| **`offlineImage`** | The url for the player offline thumbnail. | `String` | `iOS` | `none` | +| **`offlineMessage`** | The message when the player is offline. | `String` | `iOS` | `none` | +| **`autostart`** | Should the tracks auto start. | `Boolean` | `iOS && Android` | `false` | +| **`controls`** | Should the control buttons show. | `Boolean` | `Android` | `true` | +| **`repeat`** | Should the track repeat. | `Boolean` | `iOS && Android` | `false` | +| **`playlist`** | An array of playlistItems. | `[playlistItem]` see [PlaylistItem](#PlaylistItem)] | `iOS && Android` | `none` | +| **`nextUpStyle`** | How the next up videos should be presented. | `{offsetSeconds: Int, offsetPercentage, Int}` | `iOS && Android` | `none` | +| **`styling`** | All the stylings for the player see [Styling](#Styling) section. | `Object` | `iOS && Android` | `none` | +| **`advertising`** | General Advertising settings on the player see [Advertising](#Advertising) section. | `Object` | `iOS && Android` | `none` | +| **`fullScreenOnLandscape`** | When this is true the player will go into full screen on rotate of phone to landscape | `Boolean` | `iOS && Android` | `false` | +| **`landscapeOnFullScreen`** | When this is true the player will go into landscape orientation when on full screen | `Boolean` | `iOS && Android` | `false` | +| **`portraitOnExitFullScreen`** | When this is true the player will go into portrait orientation when exiting full screen | `Boolean` | `Android` | `false` | +| **`exitFullScreenOnPortrait`** | When this is true the player will exit full screen when the phone goes into portrait | `Boolean` | `Android` | `false` | +| **`enableLockScreenControls`** | When this is true the player will show media controls on lock screen | `Boolean` | `iOS` | `true` | +| **`stretching`** | Resize images and video to fit player dimensions. See below [Stretching](#Stretching) section. | `String` | `Android` | `none` | +| **`backgroundAudioEnabled`** | Should the player continue playing in the background and handle interruptions. | `Boolean` | `iOS && Android` | `false` | +| **`viewOnly`** | When true the player will not have any controls it will show only the video. | `Boolean` | `iOS` | `false` | +| **`pipEnabled`** | When true the player will be able to go into Picture in Picture mode. **Note: This is true by default for iOS PlayerViewController**. **For Android you will also need to follow the instruction mentioned [here](https://developer.jwplayer.com/jwplayer/docs/android-invoke-picture-in-picture-playback) && below [Picture in picture](Picture-in-picture) section.** | `Boolean` | `iOS when viewOnly prop is true && Android` | `false` | +| **`interfaceBehavior`** | The behavior of the player interface. | `'normal', 'hidden', 'onscreen'` | `iOS` | `normal` | +| **`preload`** | The behavior of the preload. | `'auto', 'none'` | `iOS` | `auto` | +| **`related`** | The related videos behaviors. Check out the [Related](#Related) section. | `Object` | `iOS` | `none` | +| **`hideUIGroup`** | A way to hide a certain UI group in the player. | `'overlay', 'control_bar', 'center_controls', 'next_up', 'error', 'playlist', 'controls_container', 'settings_menu', 'quality_submenu', 'captions_submenu', 'playback_submenu', 'audiotracks_submenu', 'casting_menu'` | `Android` | `none` | ##### PlaylistItem | Prop | Description | Type | @@ -210,7 +214,7 @@ Running the example project: | **`startTime`** | the player should start from a certain second. | `Int` | | **`adVmap`** | The url of ads VMAP xml. | `String` | | **`adSchedule`** | Array of tags and and offsets for ads. | `{tag: String, offset: String}` | -| **`desc`** | Description of the track. | `String` | +| **`description`** | Description of the track. | `String` | | **`file`** | The url of the file to play. | `String` | | **`tracks`** | Array of caption tracks. | `{file: String, label: String}` | | **`sources`** | Array of media sources. | `{file: String, label: String, default: Boolean}` | @@ -251,14 +255,14 @@ Running the example project: ##### Styling -| Prop | Description | Type | Platform | Default | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- | -| **`displayDescription`** | Should the player show the description. | `Boolean` | `iOS && Android` | `true` | -| **`displayTitle`** | Should the player show the title. | `Boolean` | `iOS && Android` | `true` | -| **`colors`** | Object with colors in hex format (without hashtag), for the icons and progress bar See below [Colors](#Colors) section. | `Object` | -| **`font`** | Name and size of the fonts for all texts in the player. **Note: the font must be added properly in your native project** | `{name: String, size: Int}` | `iOS` | `System` | -| **`captionsStyle`** | Style of the captions: name and size of the fonts, backgroundColor, edgeStyle and highlightColor. **Note: the font must be added properly in your native project** | `{font: {name: String, size: Int}, backgroundColor: String, highlightColor: String, edgeStyle: Int}` See the [edgeStyle](#EdgeStyle) enum below | `iOS` | `System` | -| **`menuStyle`** | Style of the menu: name and size of the fonts, backgroundColor and fontColor. **Note: the font must be added properly in your native project** | `{font: {name: String, size: Int}, backgroundColor: String, fontColor: String}` | `iOS` | `System` | +| Prop | Description | Type | Platform | Default | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- | +| **`displayDescription`** | Should the player show the description. | `Boolean` | `iOS && Android` | `true` | +| **`displayTitle`** | Should the player show the title. | `Boolean` | `iOS && Android` | `true` | +| **`colors`** | Object with colors in hex format (without hashtag), for the icons and progress bar See below [Colors](#Colors) section. | `Object` | +| **`font`** | Name and size of the fonts for all texts in the player. **Note: the font must be added properly in your native project** | `{name: String, size: Int}` | `iOS` | `System` | +| **`captionsStyle`** | Style of the captions: name and size of the fonts, backgroundColor, edgeStyle and highlightColor. **Note: the font must be added properly in your native project** | `{font: {name: String, size: Int}, backgroundColor: String, highlightColor: String, edgeStyle: 'none', 'dropshadow', 'raised', 'depressed', 'uniform'}` See the [edgeStyle](#EdgeStyle) enum below | `iOS` | `System` | +| **`menuStyle`** | Style of the menu: name and size of the fonts, backgroundColor and fontColor. **Note: the font must be added properly in your native project** | `{font: {name: String, size: Int}, backgroundColor: String, fontColor: String}` | `iOS` | `System` | ##### Colors @@ -291,6 +295,28 @@ colors: PropTypes.shape({ | **`JWCaptionEdgeStyleDepressed`** | 5 | | **`JWCaptionEdgeStyleUniform`** | 6 | +### AudioTrack + +Each AudioTrack object has the following keys: + +`autoSelect`: boolean + +`defaultTrack`: boolean + +`groupId`: string + +`name`: string + +`language`: string + +A video file can include multiple audio tracks. The onAudioTracks event is fired when the list of available AudioTracks is updated (happens shortly after a playlist item starts playing). + +Once the AudioTracks list is available, use getAudioTracks to return an array of available AudioTracks. + +Then use getCurrentAudioTrack or setCurrentAudioTrack(index) to view or change the current AudioTrack. + +This is all handled automatically if using the default player controls, but these functions are helpful if you're implementing custom controls. + ### Stretching `uniform`: (default) Fits JW Player dimensions while maintaining aspect ratio @@ -314,7 +340,18 @@ colors: PropTypes.shape({ | **`adVmap`** | The url of ads VMAP xml. | `String` | | **`adSchedule`** | Array of tags and and offsets for ads. | `{tag: String, offset: String}` | | **`openBrowserOnAdClick`** | Should the player open the browser when clicking on an ad. | `Boolean` | -| **`adClient`** | The ad client. One of [JWPlayerAdClients](#JWPlayerAdClients), defaults to JWAdClientVast | `Int` | +| **`adClient`** | The ad client. One of [JWPlayerAdClients](#JWPlayerAdClients), defaults to JWAdClientVast | `'vast', 'ima", 'ima_dai'` | + +##### Related + +| Prop | Description | Type | +| --------------------- | ------------------------------------------------------------------------------------------- | ---------------------------- | +| **`onClick`** | Sets the related content onClick action using a JWRelatedOnClick. Defaults to `play` | `'play', 'link'` | +| **`onComplete`** | Sets the related content onComplete action using a JWRelatedOnComplete. Defaults to `show` | `'show', 'hide', 'autoplay'` | +| **`heading`** | Sets the related content heading using a String. Defaults to “Next up”. | `String` | +| **`url`** | Sets the related content url using a URL. | `String` | +| **`autoplayMessage`** | Sets the related content autoplayMessage using a String. Defaults to `title` | `String` | +| **`autoplayTimer`** | Sets the related content autoplayTimer using a Int. Defaults to 10 seconds. | `Int` | ##### Picture-in-picture @@ -335,21 +372,25 @@ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Conf ## Available methods -| Func | Description | Argument | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | -| **`seekTo`** | Tells the player to seek to position, use in onPlaylistItem callback so player finishes buffering file. | `Int` | -| **`togglePIP`** | Enter or exist Picture in picture mode. | `none` | -| **`play`** | Starts playing. | `none` | -| **`pause`** | Pauses playing. | `none` | -| **`stop`** | Stops the player completely. | `none` | -| **`playerState`** | Returns promise that then returns the current state of the player. Check out the [JWPlayerState](#JWPlayerState) Object. | `none` | -| **`position`** | Returns promise that then returns the current position of the player in seconds. | `none` | -| **`toggleSpeed`** | Toggles the player speed one of `0.5`, `1.0`, `1.5`, `2.0`. | `none` | -| **`setPlaylistIndex`** | Sets the current playing item in the loaded playlist. | `Int` | -| **`setControls`** | Sets the display of the control buttons on the player. | `Boolean` | -| **`setFullScreen`** | Set full screen. | `Boolean` | -| **`loadPlaylist`** | Loads a playlist. (Using this function before the player has finished initializing may result in assert crash or blank screen, put in a timeout to make sure JWPlayer is mounted). | `[PlaylistItems]` | -| **`loadPlaylistItem`** | Loads a playlist item. (Using this function before the player has finished initializing may result in assert crash or blank screen, put in a timeout to make sure JWPlayer is mounted). | [PlaylistItem](#PlaylistItem) | +| Func | Description | Argument | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| **`seekTo`** | Tells the player to seek to position, use in onPlaylistItem callback so player finishes buffering file. | `Int` | +| **`togglePIP`** | Enter or exist Picture in picture mode. | `none` | +| **`play`** | Starts playing. | `none` | +| **`pause`** | Pauses playing. | `none` | +| **`stop`** | Stops the player completely. | `none` | +| **`playerState`** | Returns promise that then returns the current state of the player. Check out the [JWPlayerState](#JWPlayerState) Object. | `none` | +| **`position`** | Returns promise that then returns the current position of the player in seconds. | `none` | +| **`toggleSpeed`** | Toggles the player speed one of `0.5`, `1.0`, `1.5`, `2.0`. | `none` | +| **`setSpeed`** | Sets the player speed. | `Double` | +| **`setPlaylistIndex`** | Sets the current playing item in the loaded playlist. | `Int` | +| **`setControls`** | Sets the display of the control buttons on the player. | `Boolean` | +| **`setFullScreen`** | Set full screen. | `Boolean` | +| **`loadPlaylist`** | Loads a playlist. (Using this function before the player has finished initializing may result in assert crash or blank screen, put in a timeout to make sure JWPlayer is mounted). | `[PlaylistItems]` | +| **`loadPlaylistItem`** | Loads a playlist item. (Using this function before the player has finished initializing may result in assert crash or blank screen, put in a timeout to make sure JWPlayer is mounted). | [PlaylistItem](#PlaylistItem) | +| **`getAudioTracks`** | Returns promise that returns an array of [AudioTracks](#AudioTrack) | `none` | +| **`getCurrentAudioTrack`** | Returns promise that returns the index of the current audio track in array returned by getAudioTracks | `none` | +| **`setCurrentAudioTrack`** | Sets the current audio track to the audio track at the specified index in the array returned by getAudioTracks | `Int` | ## Available callbacks @@ -374,6 +415,7 @@ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Conf | **`onFullScreenExit`** | Player exited fullscreen. | `none` | | **`onPlaylistComplete`** | Player finished playing playlist items. | `none` | | **`onPlaylistItem`** | When starting to play a playlist item. | JW type playlist item see docs [ios](https://developer.jwplayer.com/sdk/ios/reference/Protocols/JWPlaylistItemEvent.html), [android](https://developer.jwplayer.com/sdk/android/reference/com/longtailvideo/jwplayer/events/PlaylistItemEvent.html) contains additional index of current playing item in playlist 0 for default | +| **`onAudioTracks`** | The list of available audio tracks is updated (happens shortly after a playlist item starts playing). | `none` | ### Background Audio diff --git a/android/build.gradle b/android/build.gradle index 38cb4f4b..eee7a31e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -48,10 +48,10 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.2' // JWPlayer SDK - implementation 'com.jwplayer:jwplayer-core:4.0.0' - implementation 'com.jwplayer:jwplayer-common:4.0.0' - implementation 'com.jwplayer:jwplayer-chromecast:4.0.0' - implementation 'com.jwplayer:jwplayer-ima:4.0.0' + implementation 'com.jwplayer:jwplayer-core:4.1.0' + implementation 'com.jwplayer:jwplayer-common:4.1.0' + implementation 'com.jwplayer:jwplayer-chromecast:4.1.0' + implementation 'com.jwplayer:jwplayer-ima:4.1.0' // Ad dependencies implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.24.0' diff --git a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerModule.java b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerModule.java index d06b9c25..29c26ad7 100644 --- a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerModule.java +++ b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerModule.java @@ -14,8 +14,10 @@ import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.UIBlock; import com.facebook.react.uimanager.UIManagerModule; -import com.google.android.gms.cast.CastDevice; import com.jwplayer.pub.api.PlayerState; +import com.jwplayer.pub.api.media.audio.AudioTrack; + +import java.util.List; public class RNJWPlayerModule extends ReactContextBaseJavaModule { @@ -75,6 +77,28 @@ public void execute (NativeViewHierarchyManager nvhm) { } } + @ReactMethod + public void togglePIP(final int reactTag) { + try { + UIManagerModule uiManager = mReactContext.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() { + public void execute (NativeViewHierarchyManager nvhm) { + RNJWPlayerView playerView = (RNJWPlayerView) nvhm.resolveView(reactTag); + + if (playerView != null && playerView.mPlayerView != null) { + if (playerView.mPlayerView.getPlayer().isInPictureInPictureMode()) { + playerView.mPlayerView.getPlayer().exitPictureInPictureMode(); + } else { + playerView.mPlayerView.getPlayer().enterPictureInPictureMode(); + } + } + } + }); + } catch (IllegalViewOperationException e) { + throw e; + } + } + @ReactMethod public void setSpeed(final int reactTag, final float speed) { try { @@ -267,6 +291,76 @@ public void execute (NativeViewHierarchyManager nvhm) { } } + @ReactMethod + public void getAudioTracks(final int reactTag, final Promise promise) { + try { + UIManagerModule uiManager = mReactContext.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() { + public void execute (NativeViewHierarchyManager nvhm) { + RNJWPlayerView playerView = (RNJWPlayerView) nvhm.resolveView(reactTag); + + if (playerView != null && playerView.mPlayer != null) { + List audioTrackList = playerView.mPlayer.getAudioTracks(); + WritableArray audioTracks = Arguments.createArray(); + for (int i = 0; i < audioTrackList.size(); i++) { + WritableMap audioTrack = Arguments.createMap(); + AudioTrack track = audioTrackList.get(i); + audioTrack.putString("name", track.getName()); + audioTrack.putString("language", track.getLanguage()); + audioTrack.putString("groupId", track.getGroupId()); + audioTrack.putBoolean("defaultTrack", track.isDefaultTrack()); + audioTrack.putBoolean("autoSelect", track.isAutoSelect()); + audioTracks.pushMap(audioTrack); + } + promise.resolve(audioTracks); + } else { + promise.reject("RNJW Error", "Player is null"); + } + } + }); + } catch (IllegalViewOperationException e) { + promise.reject("RNJW Error", e); + } + } + + @ReactMethod + public void getCurrentAudioTrack(final int reactTag, final Promise promise) { + try { + UIManagerModule uiManager = mReactContext.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() { + public void execute (NativeViewHierarchyManager nvhm) { + RNJWPlayerView playerView = (RNJWPlayerView) nvhm.resolveView(reactTag); + + if (playerView != null && playerView.mPlayer != null) { + promise.resolve(playerView.mPlayer.getCurrentAudioTrack()); + } else { + promise.reject("RNJW Error", "Player is null"); + } + } + }); + } catch (IllegalViewOperationException e) { + promise.reject("RNJW Error", e); + } + } + + @ReactMethod + public void setCurrentAudioTrack(final int reactTag, final int index) { + try { + UIManagerModule uiManager = mReactContext.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() { + public void execute (NativeViewHierarchyManager nvhm) { + RNJWPlayerView playerView = (RNJWPlayerView) nvhm.resolveView(reactTag); + + if (playerView != null && playerView.mPlayer != null) { + playerView.mPlayer.setCurrentAudioTrack(index); + } + } + }); + } catch (IllegalViewOperationException e) { + throw e; + } + } + private int stateToInt(PlayerState playerState) { switch (playerState) { case IDLE: diff --git a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerView.java b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerView.java index 42f0d512..0c572244 100755 --- a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerView.java +++ b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerView.java @@ -26,10 +26,13 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.jwplayer.pub.api.JWPlayer; +import com.jwplayer.pub.api.UiGroup; import com.jwplayer.pub.api.background.MediaServiceController; import com.jwplayer.pub.api.configuration.PlayerConfig; import com.jwplayer.pub.api.configuration.UiConfig; @@ -57,6 +60,7 @@ import com.jwplayer.pub.api.events.FirstFrameEvent; import com.jwplayer.pub.api.events.FullscreenEvent; import com.jwplayer.pub.api.events.IdleEvent; +import com.jwplayer.pub.api.events.MetaEvent; import com.jwplayer.pub.api.events.PauseEvent; import com.jwplayer.pub.api.events.PipCloseEvent; import com.jwplayer.pub.api.events.PipOpenEvent; @@ -86,6 +90,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; public class RNJWPlayerView extends RelativeLayout implements VideoPlayerEvents.OnFullscreenListener, @@ -111,6 +116,7 @@ public class RNJWPlayerView extends RelativeLayout implements VideoPlayerEvents.OnSeekedListener, VideoPlayerEvents.OnCaptionsListListener, VideoPlayerEvents.OnCaptionsChangedListener, + VideoPlayerEvents.OnMetaListener, AdvertisingEvents.OnBeforePlayListener, AdvertisingEvents.OnBeforeCompleteListener, @@ -239,7 +245,42 @@ public void destroyPlayer() { if (mPlayerView != null) { mPlayer.stop(); - mPlayer.removeAllListeners(this); + mPlayer.removeListeners(this, + // VideoPlayerEvents + EventType.READY, + EventType.PLAY, + EventType.PAUSE, + EventType.COMPLETE, + EventType.IDLE, + EventType.ERROR, + EventType.SETUP_ERROR, + EventType.BUFFER, + EventType.TIME, + EventType.PLAYLIST, + EventType.PLAYLIST_ITEM, + EventType.PLAYLIST_COMPLETE, + EventType.FIRST_FRAME, + EventType.CONTROLS, + EventType.CONTROLBAR_VISIBILITY, + EventType.DISPLAY_CLICK, + EventType.FULLSCREEN, + EventType.SEEK, + EventType.SEEKED, + EventType.CAPTIONS_LIST, + EventType.CAPTIONS_CHANGED, + EventType.META, + // Ad events + EventType.BEFORE_PLAY, + EventType.BEFORE_COMPLETE, + EventType.AD_PLAY, + EventType.AD_PAUSE, + // Cast event + EventType.CAST, + // Pip events + EventType.PIP_CLOSE, + EventType.PIP_OPEN + ); + mPlayerView = null; getReactContext().removeLifecycleEventListener(this); @@ -253,41 +294,41 @@ public void destroyPlayer() { public void setupPlayerView(Boolean backgroundAudioEnabled) { if (mPlayerView != null) { - // VideoPlayerEvents - mPlayer.addListener(EventType.READY, this); - mPlayer.addListener(EventType.PLAY, this); - mPlayer.addListener(EventType.PAUSE, this); - mPlayer.addListener(EventType.COMPLETE, this); - mPlayer.addListener(EventType.IDLE, this); - mPlayer.addListener(EventType.ERROR, this); - mPlayer.addListener(EventType.SETUP_ERROR, this); - mPlayer.addListener(EventType.BUFFER, this); - mPlayer.addListener(EventType.TIME, this); - mPlayer.addListener(EventType.PLAYLIST, this); - mPlayer.addListener(EventType.PLAYLIST_ITEM, this); - mPlayer.addListener(EventType.PLAYLIST_COMPLETE, this); - mPlayer.addListener(EventType.FIRST_FRAME, this); - mPlayer.addListener(EventType.CONTROLS, this); - mPlayer.addListener(EventType.CONTROLBAR_VISIBILITY, this); - mPlayer.addListener(EventType.DISPLAY_CLICK, this); - mPlayer.addListener(EventType.FULLSCREEN, this); - mPlayer.addListener(EventType.SEEK, this); - mPlayer.addListener(EventType.SEEKED, this); - mPlayer.addListener(EventType.CAPTIONS_LIST, this); - mPlayer.addListener(EventType.CAPTIONS_CHANGED, this); - - // Ad events - mPlayer.addListener(EventType.BEFORE_PLAY, this); - mPlayer.addListener(EventType.BEFORE_COMPLETE, this); - mPlayer.addListener(EventType.AD_PLAY, this); - mPlayer.addListener(EventType.AD_PAUSE, this); - - // Cast event - mPlayer.addListener(EventType.CAST, this); - - // Pip events - mPlayer.addListener(EventType.PIP_CLOSE, this); - mPlayer.addListener(EventType.PIP_OPEN, this); + mPlayer.addListeners(this, + // VideoPlayerEvents + EventType.READY, + EventType.PLAY, + EventType.PAUSE, + EventType.COMPLETE, + EventType.IDLE, + EventType.ERROR, + EventType.SETUP_ERROR, + EventType.BUFFER, + EventType.TIME, + EventType.PLAYLIST, + EventType.PLAYLIST_ITEM, + EventType.PLAYLIST_COMPLETE, + EventType.FIRST_FRAME, + EventType.CONTROLS, + EventType.CONTROLBAR_VISIBILITY, + EventType.DISPLAY_CLICK, + EventType.FULLSCREEN, + EventType.SEEK, + EventType.SEEKED, + EventType.CAPTIONS_LIST, + EventType.CAPTIONS_CHANGED, + EventType.META, + // Ad events + EventType.BEFORE_PLAY, + EventType.BEFORE_COMPLETE, + EventType.AD_PLAY, + EventType.AD_PAUSE, + // Cast event + EventType.CAST, + // Pip events + EventType.PIP_CLOSE, + EventType.PIP_OPEN + ); mPlayer.setFullscreenHandler(new fullscreenHandler()); @@ -430,8 +471,8 @@ public PlaylistItem getPlaylistItem (ReadableMap playlistItem) { itemBuilder.title(title); } - if (playlistItem.hasKey("desc")) { - String desc = playlistItem.getString("desc"); + if (playlistItem.hasKey("description")) { + String desc = playlistItem.getString("description"); itemBuilder.description(desc); } @@ -595,8 +636,11 @@ private void setupPlayer(ReadableMap prop) { adScheduleList.add(adBreak); } - if (ads.hasKey("adClient")) { - switch (ads.getInt("adClient")) { + if (ads.hasKey("adClient") && + ads.getString("adClient") != null && + CLIENT_TYPES.get(ads.getString("adClient")) != null) { + Integer clientType = CLIENT_TYPES.get(ads.getString("adClient")); + switch (clientType) { case 1: client = AdClient.IMA; advertisingConfig = new ImaAdvertisingConfig.Builder().schedule(adScheduleList).build(); @@ -639,11 +683,17 @@ private void setupPlayer(ReadableMap prop) { UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build(); configBuilder.uiConfig(uiConfig); } + } - // in future support hiding showing individual ui groups -// UiConfig hideJwControlbarUiConfig = new UiConfig.Builder() -// .hide(UiGroup.CONTROLBAR) -// .build(); + if (prop.hasKey("hideUIGroup")) { + UiGroup uiGroup = GROUP_TYPES.get(prop.getString("hideUIGroup")); + if (uiGroup != null) { + UiConfig hideJwControlbarUiConfig = new UiConfig.Builder() + .displayAllControls() + .hide(uiGroup) + .build(); + configBuilder.uiConfig(hideJwControlbarUiConfig); + } } PlayerConfig playerConfig = configBuilder.build(); @@ -772,18 +822,22 @@ public void onBeforePlay(BeforePlayEvent beforePlayEvent) { getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topBeforePlay", event); } - // VideoPlayerEvents + // Audio Events @Override - public void onAudioTrackChanged(AudioTrackChangedEvent audioTrackChangedEvent) { - + public void onAudioTracks(AudioTracksEvent audioTracksEvent) { + WritableMap event = Arguments.createMap(); + event.putString("message", "onAudioTracks"); + getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topAudioTracks", event); } @Override - public void onAudioTracks(AudioTracksEvent audioTracksEvent) { + public void onAudioTrackChanged(AudioTrackChangedEvent audioTrackChangedEvent) { } + // Player Events + @Override public void onBuffer(BufferEvent bufferEvent) { WritableMap event = Arguments.createMap(); @@ -979,6 +1033,11 @@ public void onCaptionsList(CaptionsListEvent captionsListEvent) { } + @Override + public void onMeta(MetaEvent metaEvent) { + + } + // Picture in Picture events @Override @@ -1019,6 +1078,28 @@ public void onHostPause() { public void onHostDestroy() { this.destroyPlayer(); } + + // utils + private final Map CLIENT_TYPES = MapBuilder.of( + "vast", 0, + "ima", 1, + "ima_dai", 2 + ); + + private final Map GROUP_TYPES = ImmutableMap.builder() + .put("overlay", UiGroup.OVERLAY) + .put("control_bar", UiGroup.CONTROLBAR) + .put("center_controls", UiGroup.CENTER_CONTROLS) + .put("next_up", UiGroup.NEXT_UP) + .put("error", UiGroup.ERROR) + .put("playlist", UiGroup.PLAYLIST) + .put("controls_container", UiGroup.PLAYER_CONTROLS_CONTAINER) + .put("settings_menu", UiGroup.SETTINGS_MENU) + .put("quality_submenu", UiGroup.SETTINGS_QUALITY_SUBMENU) + .put("captions_submenu", UiGroup.SETTINGS_CAPTIONS_SUBMENU) + .put("playback_submenu", UiGroup.SETTINGS_PLAYBACK_SUBMENU) + .put("audiotracks_submenu", UiGroup.SETTINGS_AUDIOTRACKS_SUBMENU) + .put("casting_menu", UiGroup.CASTING_MENU).build(); } diff --git a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerViewManager.java b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerViewManager.java index 6bab0fd4..2b9e59d8 100644 --- a/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerViewManager.java +++ b/android/src/main/java/com/appgoalz/rnjwplayer/RNJWPlayerViewManager.java @@ -135,6 +135,10 @@ public Map getExportedCustomBubblingEventTypeConstants() { MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onAdPause"))) + .put("topAudioTracks", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onAudioTracks"))) .build(); } diff --git a/index.d.ts b/index.d.ts index 609acd8c..6d6cf0b5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,6 +2,13 @@ declare module "react-native-jw-media-player" { import React from "react"; import { ViewStyle } from "react-native"; + interface AudioTrack { + autoSelect: boolean; + defaultTrack: boolean; + groupId: string; + language: string; + name: string; + } interface CastingDevice { name?: string; identifier?: string; @@ -19,18 +26,23 @@ declare module "react-native-jw-media-player" { tag: string; offset: string; } + type ClientTypes = + | 'vast' + | 'ima' + | 'ima_dai'; interface Advertising { adSchedule?: AdSchedule; adVmap?: string; tag?: string; openBrowserOnAdClick?: boolean; + adClient?: ClientTypes; } interface PlaylistItem { file: string; sources?: Source[]; image?: string; title?: string; - desc?: string; + description?: string; mediaId?: string; adSchedule?: AdSchedule; adVmap?: string; @@ -39,18 +51,31 @@ declare module "react-native-jw-media-player" { startTime?: number; autostart?: boolean; } + type RelatedOnClicks = + | 'play' + | 'link'; + type RelatedOnCompletes = + | 'show' + | 'hide' + | 'autoplay'; interface Related { - onClick?: string; - onComplete?: string; + onClick?: RelatedOnClicks; + onComplete?: RelatedOnCompletes; heading?: string; url?: string; autoplayMessage?: string; - autoplayTimer?: string; + autoplayTimer?: number; } interface Font { name?: string; size?: number; } + type EdgeStyles = + | 'none' + | 'dropshadow' + | 'raised' + | 'depressed' + | 'uniform'; interface Styling { colors?: { buttons?: string; @@ -66,7 +91,7 @@ declare module "react-native-jw-media-player" { fontColor?: string; backgroundColor?: string; highlightColor?: string; - edgeStyle?: number; + edgeStyle?: EdgeStyles; }; menuStyle: { font?: Font; @@ -74,10 +99,30 @@ declare module "react-native-jw-media-player" { backgroundColor?: string; }; } + type Preloads = + | 'auto' + | 'none'; + type InterfaceBehaviors = + | 'normal' + | 'hidden' + | 'onscreen'; + type UIGroups = + | 'overlay' + | 'control_bar' + | 'center_controls' + | 'next_up' + | 'error' + | 'playlist' + | 'controls_container' + | 'settings_menu' + | 'quality_submenu' + | 'captions_submenu' + | 'playback_submenu' + | 'audiotracks_submenu' + | 'casting_menu'; interface Config { license: string, advertising?: Advertising; - adClient?: string; autostart?: boolean; controls?: boolean; repeat?: boolean; @@ -91,8 +136,9 @@ declare module "react-native-jw-media-player" { playlist?: PlaylistItem[]; stretching?: string; related?: Related; - preload?: string; - interfaceBehavior: number; + preload?: Preloads; + interfaceBehavior?: InterfaceBehaviors; + hideUIGroup?: UIGroups; } interface PropsType { config: Config; @@ -116,6 +162,7 @@ declare module "react-native-jw-media-player" { onPlaylistItem?: (playlistItem: PlaylistItem) => void; onControlBarVisible?: (event: any) => void; onPlaylistComplete?: (event: any) => void; + onAudioTracks?: (event: any) => void; style?: ViewStyle; } @@ -138,5 +185,9 @@ declare module "react-native-jw-media-player" { availableDevices(): Promise; castState(): Promise; playerState(): Promise; + getAudioTracks(): Promise; + getCurrentAudioTrack(): Promise; + setCurrentAudioTrack(index: number): void; + setCurrentCaptions(index: number): void; } } diff --git a/index.js b/index.js index 6ba7884b..8e7ac20d 100644 --- a/index.js +++ b/index.js @@ -56,7 +56,7 @@ export default class JWPlayer extends Component { autostart: PropTypes.bool, controls: PropTypes.bool, repeat: PropTypes.bool, - preload: PropTypes.oneOf(["0", "1"]), + preload: PropTypes.oneOf(["auto", "none"]), playlist: PropTypes.arrayOf( PropTypes.shape({ file: PropTypes.string, @@ -103,7 +103,7 @@ export default class JWPlayer extends Component { }), // controller only - interfaceBehavior: PropTypes.oneOf(["0", "1", "2"]), + interfaceBehavior: PropTypes.oneOf(["normal", "hidden", "onscreen"]), styling: PropTypes.shape({ colors: PropTypes.shape({ buttons: PropTypes.string, @@ -126,7 +126,7 @@ export default class JWPlayer extends Component { backgroundColor: PropTypes.string, fontColor: PropTypes.string, highlightColor: PropTypes.string, - edgeStyle: PropTypes.oneOf(["1", "2" ,"3", "4", "5", "6"]) + edgeStyle: PropTypes.oneOf(['none', 'dropshadow', 'raised', 'depressed', 'uniform']) }), menuStyle: PropTypes.shape({ font: PropTypes.shape({ @@ -160,10 +160,6 @@ export default class JWPlayer extends Component { setPlaylistIndex: PropTypes.func, setControls: PropTypes.func, setFullscreen: PropTypes.func, - showAirPlayButton: PropTypes.func, - hideAirPlayButton: PropTypes.func, - showCastButton: PropTypes.func, - hideCastButton: PropTypes.func, setUpCastController: PropTypes.func, presentCastDialog: PropTypes.func, connectedDevice: PropTypes.func, @@ -191,6 +187,10 @@ export default class JWPlayer extends Component { onControlBarVisible: PropTypes.func, onControlBarVisible: PropTypes.func, onPlaylistComplete: PropTypes.func, + getAudioTracks: PropTypes.func, + getCurrentAudioTrack: PropTypes.func, + setCurrentAudioTrack: PropTypes.func, + onAudioTracks: PropTypes.func, }; pause() { @@ -244,6 +244,20 @@ export default class JWPlayer extends Component { ); } + async time() { + if (RNJWPlayerManager) { + try { + var time = await RNJWPlayerManager.time( + this.getRNJWPlayerBridgeHandle() + ); + return time; + } catch (e) { + console.error(e); + return null; + } + } + } + async position() { if (RNJWPlayerManager) { try { @@ -329,6 +343,48 @@ export default class JWPlayer extends Component { } } + async getAudioTracks() { + if (RNJWPlayerManager) { + try { + var audioTracks = await RNJWPlayerManager.getAudioTracks( + this.getRNJWPlayerBridgeHandle() + ); + // iOS sends autoSelect as 0 or 1 instead of a boolean + // couldn't figure out how to send autoSelect as a boolean from Objective C + return audioTracks.map((audioTrack) => { + audioTrack.autoSelect = !!audioTrack.autoSelect; + return audioTrack; + }); + } catch (e) { + console.error(e); + return null; + } + } + } + + async getCurrentAudioTrack() { + if (RNJWPlayerManager) { + try { + var currentAudioTrack = await RNJWPlayerManager.getCurrentAudioTrack( + this.getRNJWPlayerBridgeHandle() + ); + return currentAudioTrack; + } catch (e) { + console.error(e); + return null; + } + } + } + + setCurrentAudioTrack(index) { + if (RNJWPlayerManager) { + RNJWPlayerManager.setCurrentAudioTrack( + this.getRNJWPlayerBridgeHandle(), + index + ); + } + } + getRNJWPlayerBridgeHandle() { return ReactNative.findNodeHandle(this.refs[RCT_RNJWPLAYER_REF]); } @@ -345,14 +401,14 @@ export default class JWPlayer extends Component { controls, repeat, mute, - displayTitle, - displayDesc, + styling, nextUpDisplay, playlistItem, playlist, style, stretching, } = config || {}; + var {displayTitle, displayDescription} = styling || {} var thisConfig = this.props.config || {}; diff --git a/ios/RNJWPlayer.podspec b/ios/RNJWPlayer.podspec index a354560a..cbed1dad 100644 --- a/ios/RNJWPlayer.podspec +++ b/ios/RNJWPlayer.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.platform = :ios, "10.0" s.source = { :git => "https://github.com/chaimPaneth/react-native-jw-media-player.git", :tag => "v#{s.version}" } s.source_files = "RNJWPlayer/*.{h,m}" - s.dependency 'JWPlayerKit', '~> 4.0.1' + s.dependency 'JWPlayerKit', '~> 4.1.1' s.dependency 'google-cast-sdk', '~> 4.5.1' s.dependency 'React' # s.static_framework = true diff --git a/ios/RNJWPlayer.xcodeproj/project.pbxproj b/ios/RNJWPlayer.xcodeproj/project.pbxproj index d1db1384..5077f25e 100644 --- a/ios/RNJWPlayer.xcodeproj/project.pbxproj +++ b/ios/RNJWPlayer.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2A5DC7C0272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A5DC7BF272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.m */; }; 2A9166FF21064ECE00152DD3 /* JWPlayer_iOS_SDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A9166FD21064DE100152DD3 /* JWPlayer_iOS_SDK.framework */; }; 2AA52BE726C144B200AD26AE /* RNJWPlayerViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AA52BE226C144B200AD26AE /* RNJWPlayerViewManager.m */; }; 2AA52BE826C144B200AD26AE /* RNJWPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AA52BE426C144B200AD26AE /* RNJWPlayerView.m */; }; @@ -28,6 +29,8 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRNJWPlayer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNJWPlayer.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A5DC7BE272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCTConvert+RNJWPlayer.h"; path = "RNJWPlayer/RCTConvert+RNJWPlayer.h"; sourceTree = ""; }; + 2A5DC7BF272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+RNJWPlayer.m"; path = "RNJWPlayer/RCTConvert+RNJWPlayer.m"; sourceTree = ""; }; 2A9166FD21064DE100152DD3 /* JWPlayer_iOS_SDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JWPlayer_iOS_SDK.framework; path = "../../../ios/Pods/JWPlayer-SDK/JWPlayer_iOS_SDK.framework"; sourceTree = ""; }; 2AA52BE126C144B200AD26AE /* RNJWPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNJWPlayerView.h; path = RNJWPlayer/RNJWPlayerView.h; sourceTree = ""; }; 2AA52BE226C144B200AD26AE /* RNJWPlayerViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNJWPlayerViewManager.m; path = RNJWPlayer/RNJWPlayerViewManager.m; sourceTree = ""; }; @@ -76,6 +79,8 @@ 2AA52BE426C144B200AD26AE /* RNJWPlayerView.m */, 2AA52BE326C144B200AD26AE /* RNJWPlayerViewManager.h */, 2AA52BE226C144B200AD26AE /* RNJWPlayerViewManager.m */, + 2A5DC7BE272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.h */, + 2A5DC7BF272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.m */, 134814211AA4EA7D00B7C361 /* Products */, 3BC75FCA1E43B1DB0011FBAA /* Frameworks */, ); @@ -139,6 +144,7 @@ buildActionMask = 2147483647; files = ( 2AA52BE826C144B200AD26AE /* RNJWPlayerView.m in Sources */, + 2A5DC7C0272B5DB6003BF3E4 /* RCTConvert+RNJWPlayer.m in Sources */, 2AA52BE726C144B200AD26AE /* RNJWPlayerViewManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/RNJWPlayer/RCTConvert+RNJWPlayer.h b/ios/RNJWPlayer/RCTConvert+RNJWPlayer.h new file mode 100644 index 00000000..8d01be65 --- /dev/null +++ b/ios/RNJWPlayer/RCTConvert+RNJWPlayer.h @@ -0,0 +1,25 @@ +// +// RCTConvert+RNJWPlayer.m +// RNJWPlayer +// +// Created by Chaim Paneth on 10/29/21. +// + +#import +#import + +@interface RCTConvert (RNJWPlayer) + ++ (JWAdClient)JWAdClient:(id)json; + ++ (JWInterfaceBehavior)JWInterfaceBehavior:(id)json; + ++ (JWCaptionEdgeStyle)JWCaptionEdgeStyle:(id)json; + ++ (JWPreload)JWPreload:(id)json; + ++ (JWRelatedOnClick)JWRelatedOnClick:(id)json; + ++ (JWRelatedOnComplete)JWRelatedOnComplete:(id)json; + +@end diff --git a/ios/RNJWPlayer/RCTConvert+RNJWPlayer.m b/ios/RNJWPlayer/RCTConvert+RNJWPlayer.m new file mode 100644 index 00000000..bd85b09e --- /dev/null +++ b/ios/RNJWPlayer/RCTConvert+RNJWPlayer.m @@ -0,0 +1,48 @@ +// +// RCTConvert+RNJWPlayer.m +// RNJWPlayer +// +// Created by Chaim Paneth on 10/29/21. +// + +#import "RCTConvert+RNJWPlayer.h" + +@implementation RCTConvert (RNJWPlayer) + +RCT_ENUM_CONVERTER(JWAdClient, (@{ + @"vast": @(JWAdClientJWPlayer), + @"ima": @(JWAdClientGoogleIMA), + @"ima_dai": @(JWAdClientGoogleIMADAI), +}), JWAdClientUnknown, integerValue) + +RCT_ENUM_CONVERTER(JWInterfaceBehavior, (@{ + @"normal": @(JWInterfaceBehaviorNormal), + @"hidden": @(JWInterfaceBehaviorHidden), + @"onscreen": @(JWInterfaceBehaviorAlwaysOnScreen), +}), JWInterfaceBehaviorNormal, integerValue) + +RCT_ENUM_CONVERTER(JWCaptionEdgeStyle, (@{ + @"none": @(JWCaptionEdgeStyleNone), + @"dropshadow": @(JWCaptionEdgeStyleDropshadow), + @"raised": @(JWCaptionEdgeStyleRaised), + @"depressed": @(JWCaptionEdgeStyleDepressed), + @"uniform": @(JWCaptionEdgeStyleUniform), +}), JWCaptionEdgeStyleUndefined, integerValue) + +RCT_ENUM_CONVERTER(JWPreload, (@{ + @"auto": @(JWPreloadAuto), + @"none": @(JWPreloadNone), +}), JWPreloadNone, integerValue) + +RCT_ENUM_CONVERTER(JWRelatedOnClick, (@{ + @"play": @(JWRelatedOnClickPlay), + @"link": @(JWRelatedOnClickLink), +}), JWRelatedOnClickPlay, integerValue) + +RCT_ENUM_CONVERTER(JWRelatedOnComplete, (@{ + @"show": @(JWRelatedOnCompleteShow), + @"hide": @(JWRelatedOnCompleteHide), + @"autoplay": @(JWRelatedOnCompleteAutoplay), +}), JWRelatedOnCompleteShow, integerValue) + +@end diff --git a/ios/RNJWPlayer/RNJWPlayerView.h b/ios/RNJWPlayer/RNJWPlayerView.h index 33a5338a..5f647944 100644 --- a/ios/RNJWPlayer/RNJWPlayerView.h +++ b/ios/RNJWPlayer/RNJWPlayerView.h @@ -44,6 +44,9 @@ @property(nonatomic, copy)RCTBubblingEventBlock onBeforeComplete; @property(nonatomic, copy)RCTBubblingEventBlock onComplete; +/* av events */ +@property(nonatomic, copy)RCTBubblingEventBlock onAudioTracks; + /* player events */ @property(nonatomic, copy)RCTBubblingEventBlock onPlayerReady; @property(nonatomic, copy)RCTBubblingEventBlock onSetupPlayerError; diff --git a/ios/RNJWPlayer/RNJWPlayerView.m b/ios/RNJWPlayer/RNJWPlayerView.m index f587ca0b..9600810b 100644 --- a/ios/RNJWPlayer/RNJWPlayerView.m +++ b/ios/RNJWPlayer/RNJWPlayerView.m @@ -2,6 +2,7 @@ #import #import #import +#import "RCTConvert+RNJWPlayer.h" @implementation RNJWPlayerView @@ -47,6 +48,7 @@ - (void)layoutSubviews if (self.playerViewController != nil) { self.playerViewController.view.frame = self.frame; +// [_playerViewController.view.subviews[0].subviews[2] setHidden:YES]; // this is overlay with controls } } @@ -228,33 +230,7 @@ -(void)setStyling:styling id edgeStyle = capStyle[@"edgeStyle"]; if (edgeStyle != nil && (edgeStyle != (id)[NSNull null])) { - JWCaptionEdgeStyle finalEdgeStyle; - switch ([edgeStyle intValue]) { - case 1: - finalEdgeStyle = JWCaptionEdgeStyleUndefined; - break; - case 2: - finalEdgeStyle = JWCaptionEdgeStyleNone; - break; - case 3: - finalEdgeStyle = JWCaptionEdgeStyleDropshadow; - break; - case 4: - finalEdgeStyle = JWCaptionEdgeStyleRaised; - break; - case 5: - finalEdgeStyle = JWCaptionEdgeStyleDepressed; - break; - case 6: - finalEdgeStyle = JWCaptionEdgeStyleUniform; - break; - - default: - finalEdgeStyle = JWCaptionEdgeStyleUndefined; - break; - } - - [capStyleBuilder edgeStyle:finalEdgeStyle]; + [capStyleBuilder edgeStyle:[RCTConvert JWCaptionEdgeStyle:edgeStyle]]; } JWCaptionStyle* captionStyle = [capStyleBuilder buildAndReturnError:&error]; @@ -355,7 +331,7 @@ -(JWPlayerItem*)getPlayerItem:item [itemBuilder title:title]; } - id desc = item[@"desc"]; + id desc = item[@"description"]; if ((desc != nil) && (desc != (id)[NSNull null])) { [itemBuilder description:desc]; } @@ -376,11 +352,6 @@ -(JWPlayerItem*)getPlayerItem:item NSURL* recUrl = [NSURL URLWithString:recommendations]; [itemBuilder recommendations:recUrl]; } - - id autostart = item[@"autostart"]; - if (autostart != nil && (autostart != (id)[NSNull null])) { - [itemBuilder autostart:autostart]; - } id tracksItem = item[@"tracks"]; if(tracksItem != nil && (tracksItem != (id)[NSNull null])) { @@ -473,16 +444,7 @@ -(JWPlayerConfiguration*)getPlayerConfiguration:config id preload = config[@"preload"]; if (preload != nil && (preload != (id)[NSNull null])) { - switch ([preload intValue]) { - case 0: - [configBuilder preload:JWPreloadAuto]; - break; - case 1: - [configBuilder preload:JWPreloadNone]; - break; - default: - break; - } + [configBuilder preload:[RCTConvert JWPreload:preload]]; } id related = config[@"related"]; @@ -491,35 +453,13 @@ -(JWPlayerConfiguration*)getPlayerConfiguration:config id onClick = related[@"onClick"]; if ((onClick != nil) && (onClick != (id)[NSNull null])) { - switch ([onClick intValue]) { - case 0: - [relatedBuilder onClick:JWRelatedOnClickPlay]; - break; - case 1: - [relatedBuilder onClick:JWRelatedOnClickLink]; - break; - default: - [relatedBuilder onClick:JWRelatedOnClickPlay]; - break; - } + [relatedBuilder onClick:[RCTConvert JWRelatedOnClick:onClick]]; } id onComplete = related[@"onComplete"]; if ((onComplete != nil) && (onComplete != (id)[NSNull null])) { - switch ([onComplete intValue]) { - case 0: - [relatedBuilder onComplete:JWRelatedOnCompleteShow]; - break; - case 1: - [relatedBuilder onComplete:JWRelatedOnCompleteHide]; - break; - case 2: - [relatedBuilder onComplete:JWRelatedOnCompleteAutoplay]; - break; - default: - [relatedBuilder onComplete:JWRelatedOnCompleteAutoplay]; - break; - } + + [relatedBuilder onComplete:[RCTConvert JWRelatedOnComplete:onComplete]]; } id heading = related[@"heading"]; @@ -558,10 +498,11 @@ -(JWPlayerConfiguration*)getPlayerConfiguration:config JWAdvertisingConfig* advertising; JWAdsAdvertisingConfigBuilder* adConfigBuilder = [[JWAdsAdvertisingConfigBuilder alloc] init]; - id adClient = config[@"adClient"]; + id adClient = ads[@"adClient"]; if ((adClient != nil) && (adClient != (id)[NSNull null])) { + int clientType = (int)[RCTConvert JWAdClient:adClient]; JWAdClient jwAdClient; - switch ([adClient intValue]) { + switch (clientType) { case 0: jwAdClient = JWAdClientJWPlayer; break; @@ -587,7 +528,7 @@ -(JWPlayerConfiguration*)getPlayerConfiguration:config // [adConfigBuilder adRules:(JWAdRules * _Nonnull)]; - id schedule = config[@"adSchedule"]; + id schedule = ads[@"adSchedule"]; if(schedule != nil && (schedule != (id)[NSNull null])) { NSArray* scheduleAr = (NSArray*)schedule; if (scheduleAr.count > 0) { @@ -622,13 +563,13 @@ -(JWPlayerConfiguration*)getPlayerConfiguration:config [adConfigBuilder tag:tagUrl]; } - id adVmap = config[@"adVmap"]; + id adVmap = ads[@"adVmap"]; if (adVmap != nil && (adVmap != (id)[NSNull null])) { NSURL* adVmapUrl = [NSURL URLWithString:adVmap]; [adConfigBuilder vmapURL:adVmapUrl]; } - id openBrowserOnAdClick = config[@"openBrowserOnAdClick"]; + id openBrowserOnAdClick = ads[@"openBrowserOnAdClick"]; if (openBrowserOnAdClick != nil && (openBrowserOnAdClick != (id)[NSNull null])) { [adConfigBuilder openBrowserOnAdClick:openBrowserOnAdClick]; } @@ -651,22 +592,10 @@ -(void)setupPlayerViewController:config :(JWPlayerConfiguration*)playerConfig _playerViewController = [JWPlayerViewController new]; _playerViewController.delegate = self; - id interfaceBehavior = config[@"interfaceBehavior"]; - if ((interfaceBehavior != nil) && (interfaceBehavior != (id)[NSNull null])) { - switch ([interfaceBehavior intValue]) { - case 0: - _playerViewController.interfaceBehavior = JWInterfaceBehaviorNormal; - break; - case 1: - _playerViewController.interfaceBehavior = JWInterfaceBehaviorHidden; - break; - case 2: - _playerViewController.interfaceBehavior = JWInterfaceBehaviorAlwaysOnScreen; - break; - default: - break; - } - } +// id interfaceBehavior = config[@"interfaceBehavior"]; +// if ((interfaceBehavior != nil) && (interfaceBehavior != (id)[NSNull null])) { +// _playerViewController.interfaceBehavior = [RCTConvert JWInterfaceBehavior:interfaceBehavior]; +// } id forceFullScreenOnLandscape = config[@"fullScreenOnLandscape"]; if (forceFullScreenOnLandscape != nil && forceFullScreenOnLandscape != (id)[NSNull null]) { @@ -1107,18 +1036,38 @@ - (void)jwplayer:(id)player didLoadPlaylistItem:(JWPlayerItem *)item a } if (self.onPlaylistItem) { + NSMutableDictionary* sourceDict = [[NSMutableDictionary alloc] init]; + for (JWVideoSource* source in item.videoSources) { + [sourceDict setObject:source.file forKey:@"file"]; + [sourceDict setObject:source.label forKey:@"label"]; + [sourceDict setObject:@(source.defaultVideo) forKey:@"default"]; + } + + NSMutableDictionary* schedDict = [[NSMutableDictionary alloc] init]; + for (JWAdBreak* sched in item.adSchedule) { + [schedDict setObject:sched.offset forKey:@"offset"]; + [schedDict setObject:sched.tagArray forKey:@"tags"]; + [schedDict setObject:@(sched.type) forKey:@"type"]; + } + + NSMutableDictionary* trackDict = [[NSMutableDictionary alloc] init]; + for (JWMediaTrack* track in item.mediaTracks) { + [trackDict setObject:track.file forKey:@"file"]; + [trackDict setObject:track.label forKey:@"label"]; + [trackDict setObject:@(track.defaultTrack) forKey:@"default"]; + } + NSDictionary* itemDict = [NSDictionary dictionaryWithObjectsAndKeys: item.mediaId, @"mediaId", -// item.posterImage, @"image", -// item.title, @"title", -// item.description, @"desc", -// item.vmapURL, @"adVmap", -// item.recommendations, @"recommendations", -// item.startTime, @"startTime", -// item.autostart, @"autostart", -// item.videoSources, @"sources", -// item.adSchedule, @"adSchedule", -// item.mediaTracks, @"tracks", + item.title, @"title", + item.description, @"description", + item.posterImage.absoluteString, @"image", + @(item.startTime), @"startTime", + item.vmapURL.absoluteString, @"adVmap", + item.recommendations.absoluteString, @"recommendations", + sourceDict, @"sources", + schedDict, @"adSchedule", + trackDict, @"tracks", nil]; NSError *error; @@ -1138,19 +1087,40 @@ - (void)jwplayer:(id)player didLoadPlaylist:(NSArray * NSMutableArray* playlistArray = [[NSMutableArray alloc] init]; for (JWPlayerItem* item in playlist) { + NSMutableDictionary* sourceDict = [[NSMutableDictionary alloc] init]; + for (JWVideoSource* source in item.videoSources) { + [sourceDict setObject:source.file forKey:@"file"]; + [sourceDict setObject:source.label forKey:@"label"]; + [sourceDict setObject:@(source.defaultVideo) forKey:@"default"]; + } + + NSMutableDictionary* schedDict = [[NSMutableDictionary alloc] init]; + for (JWAdBreak* sched in item.adSchedule) { + [schedDict setObject:sched.offset forKey:@"offset"]; + [schedDict setObject:sched.tagArray forKey:@"tags"]; + [schedDict setObject:@(sched.type) forKey:@"type"]; + } + + NSMutableDictionary* trackDict = [[NSMutableDictionary alloc] init]; + for (JWMediaTrack* track in item.mediaTracks) { + [trackDict setObject:track.file forKey:@"file"]; + [trackDict setObject:track.label forKey:@"label"]; + [trackDict setObject:@(track.defaultTrack) forKey:@"default"]; + } + NSDictionary* itemDict = [NSDictionary dictionaryWithObjectsAndKeys: item.mediaId, @"mediaId", -// item.posterImage, @"image", -// item.title, @"title", -// item.description, @"desc", -// item.vmapURL, @"adVmap", -// item.recommendations, @"recommendations", -// item.startTime, @"startTime", -// item.autostart, @"autostart", -// item.videoSources, @"sources", -// item.adSchedule, @"adSchedule", -// item.mediaTracks, @"tracks", + item.title, @"title", + item.description, @"description", + item.posterImage.absoluteString, @"image", + @(item.startTime), @"startTime", + item.vmapURL.absoluteString, @"adVmap", + item.recommendations.absoluteString, @"recommendations", + sourceDict, @"sources", + schedDict, @"adSchedule", + trackDict, @"tracks", nil]; + [playlistArray addObject:itemDict]; } @@ -1394,11 +1364,13 @@ - (void)castController:(JWCastController * _Nonnull)controller disconnectedWithE #pragma mark - JWPlayer AV Delegate -- (void)jwplayer:(id _Nonnull)player audioTrackChanged:(NSInteger)currentLevel { - +- (void)jwplayer:(id _Nonnull)player audioTracksUpdated:(NSArray * _Nonnull)levels { + if (self.onAudioTracks) { + self.onAudioTracks(@{}); + } } -- (void)jwplayer:(id _Nonnull)player audioTracksUpdated:(NSArray * _Nonnull)levels { +- (void)jwplayer:(id _Nonnull)player audioTrackChanged:(NSInteger)currentLevel { } diff --git a/ios/RNJWPlayer/RNJWPlayerViewManager.m b/ios/RNJWPlayer/RNJWPlayerViewManager.m index 527a7d15..6e9160b1 100644 --- a/ios/RNJWPlayer/RNJWPlayerViewManager.m +++ b/ios/RNJWPlayer/RNJWPlayerViewManager.m @@ -37,6 +37,9 @@ - (UIView*)view RCT_EXPORT_VIEW_PROPERTY(onIdle, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaylistItem, RCTBubblingEventBlock); +/* av events */ +RCT_EXPORT_VIEW_PROPERTY(onAudioTracks, RCTBubblingEventBlock); + /* player events */ RCT_EXPORT_VIEW_PROPERTY(onPlayerReady, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onSetupPlayerError, RCTBubblingEventBlock); @@ -147,9 +150,9 @@ - (UIView*)view } -RCT_REMAP_METHOD(time, +RCT_REMAP_METHOD(position, tag:(nonnull NSNumber *)reactTag - resolver:(RCTPromiseResolveBlock)resolve + positionResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RNJWPlayerView *view = viewRegistry[reactTag]; @@ -160,9 +163,9 @@ - (UIView*)view reject(@"no_player", @"There is no playerView", error); } else { if (view.playerView) { - resolve(@{@"time": view.playerView.player.time}); + resolve(@(view.playerView.player.time.position)); } else if (view.playerViewController) { - resolve(@{@"time": view.playerViewController.player.time}); + resolve(@(view.playerViewController.player.time.position)); } } }]; @@ -380,4 +383,85 @@ - (UIView*)view }]; } +RCT_REMAP_METHOD(getAudioTracks, + tag:(nonnull NSNumber *)reactTag + resolve:(RCTPromiseResolveBlock)resolve + eject:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNJWPlayerView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RNJWPlayerView class]] || (view.playerView == nil && view.playerViewController == nil)) { + RCTLogError(@"Invalid view returned from registry, expecting RNJWPlayerView, got: %@", view); + + NSError *error = [[NSError alloc] init]; + reject(@"no_player", @"There is no player", error); + } else { + NSArray *audioTracks; + if (view.playerView) { + audioTracks = [view.playerView.player audioTracks]; + } else if (view.playerViewController) { + audioTracks = [view.playerViewController.player audioTracks]; + } + + if (audioTracks) { + NSMutableArray *results = [[NSMutableArray alloc] init]; + for (int i = 0; i < audioTracks.count; i++) { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + id audioTrack = [audioTracks objectAtIndex:i]; + [dict setObject:audioTrack[@"language"] forKey:@"language"]; + [dict setObject:audioTrack[@"autoselect"] forKey:@"autoSelect"]; + [dict setObject:audioTrack[@"defaulttrack"] forKey:@"defaultTrack"]; + [dict setObject:audioTrack[@"name"] forKey:@"name"]; + [dict setObject:audioTrack[@"groupid"] forKey:@"groupId"]; + [results addObject:dict]; + } + resolve(results); + } else { + NSError *error = [[NSError alloc] init]; + reject(@"no_audio_tracks", @"There are no audio tracks.", error); + } + } + }]; +} + +RCT_REMAP_METHOD(getCurrentAudioTrack, + tag:(nonnull NSNumber *)reactTag + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNJWPlayerView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RNJWPlayerView class]] || (view.playerView == nil && view.playerViewController == nil)) { + RCTLogError(@"Invalid view returned from registry, expecting RNJWPlayerView, got: %@", view); + + NSError *error = [[NSError alloc] init]; + reject(@"no_player", @"There is no player", error); + } else { + if (view.playerView) { + resolve([NSNumber numberWithInteger:[view.playerView.player currentAudioTrack]]); + } else if (view.playerViewController) { + resolve([NSNumber numberWithInteger:[view.playerViewController.player currentAudioTrack]]); + } else { + NSError *error = [[NSError alloc] init]; + reject(@"no_player", @"There is no player", error); + } + } + }]; +} + +RCT_EXPORT_METHOD(setCurrentAudioTrack: (nonnull NSNumber *)reactTag: (nonnull NSNumber *)index) { + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNJWPlayerView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RNJWPlayerView class]] || (view.playerView == nil && view.playerViewController == nil)) { + RCTLogError(@"Invalid view returned from registry, expecting RNJWPlayerView, got: %@", view); + } else { + if (view.playerView) { + [view.playerView.player setCurrentAudioTrack:[index integerValue]]; + } else if (view.playerViewController) { + [view.playerViewController.player setCurrentAudioTrack:[index integerValue]]; + } + } + }]; +} + @end diff --git a/package.json b/package.json index a1a94625..41ce2237 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-jw-media-player", - "version": "0.2.1", + "version": "0.2.2", "description": "React-native Android/iOS plugin for JWPlayer SDK (https://www.jwplayer.com/)", "main": "index.js", "types": "./index.d.ts",