diff --git a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java index aea3db7..1ec0db3 100644 --- a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +++ b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java @@ -505,6 +505,28 @@ public void execute (NativeViewHierarchyManager nvhm) { } } + + @ReactMethod + public void getCurrentCaptions(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.getCurrentCaptions()); + } else { + promise.reject("RNJW Error", "Player is null"); + } + } + }); + } catch (IllegalViewOperationException e) { + throw e; + } + } + + private int stateToInt(PlayerState playerState) { switch (playerState) { case IDLE: diff --git a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java index 50c3993..46ff9e9 100755 --- a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +++ b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java @@ -35,6 +35,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; @@ -110,6 +111,7 @@ import com.jwplayer.pub.api.fullscreen.delegates.DialogLayoutDelegate; import com.jwplayer.pub.api.fullscreen.delegates.SystemUiDelegate; import com.jwplayer.pub.api.license.LicenseUtil; +import com.jwplayer.pub.api.media.captions.Caption; import com.jwplayer.pub.api.media.playlists.PlaylistItem; import com.jwplayer.ui.views.CueMarkerSeekbar; @@ -1437,6 +1439,38 @@ public void onAudioTrackChanged(AudioTrackChangedEvent audioTrackChangedEvent) { } + // Captions Events + + @Override + public void onCaptionsChanged(CaptionsChangedEvent captionsChangedEvent) { + WritableMap event = Arguments.createMap(); + event.putString("message", "onCaptionsChanged"); + event.putInt("index", captionsChangedEvent.getCurrentTrack()); + getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topCaptionsChanged", event); + } + + @Override + public void onCaptionsList(CaptionsListEvent captionsListEvent) { + WritableMap event = Arguments.createMap(); + List captionTrackList = captionsListEvent.getCaptions(); + WritableArray captionTracks = Arguments.createArray(); + if (captionTrackList != null) { + for(int i = 0; i < captionTrackList.size(); i++) { + WritableMap captionTrack = Arguments.createMap(); + Caption track = captionTrackList.get(i); + captionTrack.putString("file", track.getFile()); + captionTrack.putString("label", track.getLabel()); + captionTrack.putBoolean("default", track.isDefault()); + captionTracks.pushMap(captionTrack); + } + } + event.putString("message", "onCaptionsList"); + event.putInt("index", captionsListEvent.getCurrentCaptionIndex()); + event.putArray("tracks", captionTracks); + getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topCaptionsList", event); + + } + // Player Events @Override @@ -1652,16 +1686,6 @@ public void onTime(TimeEvent timeEvent) { getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topTime", event); } - @Override - public void onCaptionsChanged(CaptionsChangedEvent captionsChangedEvent) { - - } - - @Override - public void onCaptionsList(CaptionsListEvent captionsListEvent) { - - } - @Override public void onMeta(MetaEvent metaEvent) { diff --git a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java index e0b9e06..11f14d8 100644 --- a/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +++ b/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java @@ -155,6 +155,14 @@ public Map getExportedCustomBubblingEventTypeConstants() { MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onAudioTracks"))) + .put("topCaptionsChanged", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onCaptionsChanged"))) + .put("topCaptionsList", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onCaptionsList"))) .put("topCasting", MapBuilder.of( "phasedRegistrationNames", diff --git a/index.d.ts b/index.d.ts index 9a8a866..63f11ee 100644 --- a/index.d.ts +++ b/index.d.ts @@ -523,6 +523,15 @@ declare module "@jwplayer/jwplayer-react-native" { type: number; } // Overloaded type to be used in multiple error events + interface CaptionsChangedEventProps { + index?: number; + } + interface CaptionsListEventProps { + index: number; + file?: string; + label: string; + default: string; + } type NativeError = (event: BaseEvent | BaseEvent | BaseEvent) => void; type NativeWarning = (event: BaseEvent) => void; interface PropsType { @@ -557,6 +566,8 @@ declare module "@jwplayer/jwplayer-react-native" { onControlBarVisible?: (event: BaseEvent) => void; onPlaylistComplete?: () => void; onPlaylistItem?: (event: BaseEvent) => void; + onCaptionsChanged?: (event: BaseEvent) => void; + onCaptionsList?: (event: BaseEvent) => void; onAudioTracks?: () => void; shouldComponentUpdate?: (nextProps: any, nextState: any) => boolean; } @@ -588,6 +599,7 @@ declare module "@jwplayer/jwplayer-react-native" { getCurrentAudioTrack(): Promise; setCurrentAudioTrack(index: number): void; setCurrentCaptions(index: number): void; + getCurrentCaptions(): Promise; setVisibility(visibility: boolean, controls: JWControlType[]): void; } } diff --git a/index.js b/index.js index 12852af..7a8fc96 100644 --- a/index.js +++ b/index.js @@ -370,6 +370,9 @@ export default class JWPlayer extends Component { getCurrentAudioTrack: PropTypes.func, setCurrentAudioTrack: PropTypes.func, setCurrentCaptions: PropTypes.func, + getCurrentCaptions: PropTypes.func, + onCaptionsChanged: PropTypes.func, + onCaptionsList: PropTypes.func, onAudioTracks: PropTypes.func, }; @@ -688,6 +691,21 @@ export default class JWPlayer extends Component { ); } } + + async getCurrentCaptions() { + if (RNJWPlayerManager) { + try { + var currentCaptionTrack = + await RNJWPlayerManager.getCurrentCaptions( + this.getRNJWPlayerBridgeHandle() + ); + return currentCaptionTrack; + } catch (e) { + console.error(e); + return null; + } + } + } getRNJWPlayerBridgeHandle() { return findNodeHandle(this[this.ref_key]); diff --git a/ios/RNJWPlayer/RNJWPlayerView.swift b/ios/RNJWPlayer/RNJWPlayerView.swift index 8beae89..87e106d 100644 --- a/ios/RNJWPlayer/RNJWPlayerView.swift +++ b/ios/RNJWPlayer/RNJWPlayerView.swift @@ -85,6 +85,8 @@ class RNJWPlayerView : UIView, JWPlayerDelegate, JWPlayerStateDelegate, JWAdDele @objc var onCasting: RCTDirectEventBlock? @objc var onCastingEnded: RCTDirectEventBlock? @objc var onCastingFailed: RCTDirectEventBlock? + @objc var onCaptionsChanged: RCTDirectEventBlock? + @objc var onCaptionsList: RCTDirectEventBlock? init() { super.init(frame: CGRect(x: 20, y: 0, width: UIScreen.main.bounds.width - 40, height: 300)) @@ -1400,7 +1402,7 @@ class RNJWPlayerView : UIView, JWPlayerDelegate, JWPlayerStateDelegate, JWAdDele } func jwplayer(_ player:JWPlayer, captionTrackChanged index:Int) { - + self.onCaptionsChanged?(["index": index]) } func jwplayer(_ player: JWPlayer, visualQualityChanged currentVisualQuality: JWVisualQuality) { @@ -1416,7 +1418,15 @@ class RNJWPlayerView : UIView, JWPlayerDelegate, JWPlayerStateDelegate, JWAdDele } func jwplayer(_ player:JWPlayer, updatedCaptionList options:[JWMediaSelectionOption]) { - + var tracks: [[String: Any]] = [] + for track in player.captionsTracks { + var dict: [String: Any] = [:] + dict["label"] = track.name + dict["default"] = track.defaultOption + tracks.append(dict) + } + let currentIndex = player.currentCaptionsTrack + self.onCaptionsList?(["index": currentIndex, "tracks": tracks]) } // MARK: - JWPlayer audio session && interruption handling diff --git a/ios/RNJWPlayer/RNJWPlayerViewController.swift b/ios/RNJWPlayer/RNJWPlayerViewController.swift index a9f257f..b4587e8 100644 --- a/ios/RNJWPlayer/RNJWPlayerViewController.swift +++ b/ios/RNJWPlayer/RNJWPlayerViewController.swift @@ -588,6 +588,7 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerD override func jwplayer(_ player:JWPlayer, captionTrackChanged index:Int) { super.jwplayer(player, captionTrackChanged:index) + parentView.onCaptionsChanged?(["index": index]) } override func jwplayer(_ player:JWPlayer, qualityLevelChanged currentLevel:Int) { @@ -600,6 +601,16 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerD override func jwplayer(_ player:JWPlayer, updatedCaptionList options:[JWMediaSelectionOption]) { super.jwplayer(player, updatedCaptionList:options) + + var tracks: [[String: Any]] = [] + for track in player.captionsTracks { + var dict: [String: Any] = [:] + dict["label"] = track.name + dict["default"] = track.defaultOption + tracks.append(dict) + } + let currentIndex = player.currentCaptionsTrack + parentView.onCaptionsList?(["index": currentIndex, "tracks": tracks]) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/ios/RNJWPlayer/RNJWPlayerViewManager.m b/ios/RNJWPlayer/RNJWPlayerViewManager.m index c47d6d5..887b63c 100644 --- a/ios/RNJWPlayer/RNJWPlayerViewManager.m +++ b/ios/RNJWPlayer/RNJWPlayerViewManager.m @@ -32,6 +32,8 @@ @interface RCT_EXTERN_MODULE(RNJWPlayerViewManager, RCTViewManager) /* av events */ RCT_EXPORT_VIEW_PROPERTY(onAudioTracks, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onCaptionsChanged, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onCaptionsList, RCTDirectEventBlock); /* player events */ RCT_EXPORT_VIEW_PROPERTY(onPlayerReady, RCTDirectEventBlock); @@ -121,7 +123,7 @@ @interface RCT_EXTERN_MODULE(RNJWPlayerViewManager, RCTViewManager) RCT_EXTERN_METHOD(setCurrentCaptions: (nonnull NSNumber *)reactTag: (nonnull NSNumber *)index) -RCT_EXTERN_METHOD(setCurrentCaptions: (nonnull NSNumber *)reactTag: (nonnull NSNumber *)index) +RCT_EXTERN_METHOD(getCurrentCaptions: (nonnull NSNumber *)reactTag :(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(setLicenseKey: (nonnull NSNumber *)reactTag: (nonnull NSString *)license) diff --git a/ios/RNJWPlayer/RNJWPlayerViewManager.swift b/ios/RNJWPlayer/RNJWPlayerViewManager.swift index a8f6107..024efe9 100644 --- a/ios/RNJWPlayer/RNJWPlayerViewManager.swift +++ b/ios/RNJWPlayer/RNJWPlayerViewManager.swift @@ -420,6 +420,25 @@ class RNJWPlayerViewManager: RCTViewManager { } } + @objc func getCurrentCaptions(_ reactTag: NSNumber, _ resolve: @escaping RCTPromiseResolveBlock, _ reject: @escaping RCTPromiseRejectBlock) { + self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in + guard let view = viewRegistry?[reactTag] as? RNJWPlayerView else { + let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "There is no player"]) + reject("no_player", "Invalid view returned from registry, expecting RNJWPlayerView", error) + return + } + + if let playerView = view.playerView { + resolve(NSNumber(value: playerView.player.currentCaptionsTrack)) + } else if let playerViewController = view.playerViewController { + resolve(NSNumber(value: playerViewController.player.currentCaptionsTrack)) + } else { + let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "There is no player"]) + reject("no_player", "There is no player", error) + } + } + } + @objc func setLicenseKey(_ reactTag: NSNumber, _ license: String) { self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in guard let view = viewRegistry?[reactTag] as? RNJWPlayerView else {