diff --git a/README.md b/README.md index 9e09e01f..b1ee113e 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,8 @@ var styles = StyleSheet.create({ * [progressUpdateInterval](#progressupdateinterval) * [rate](#rate) * [repeat](#repeat) -* [resizeMode](#resize-mode) +* [resizeMode](#resizemode) +* [selectedTextTrack](#selectedtexttrack) * [volume](#volume) #### ignoreSilentSwitch @@ -304,6 +305,38 @@ Determines how to resize the video when the frame doesn't match the raw video di Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP +#### selectedTextTrack +Configure which text track (caption or subtitle), if any, is shown. + +``` +selectedTextTrack={{ + type: Type, + value: Value +}} +``` + +Example: +``` +selectedTextTrack={{ + type: "title", + value: "English Subtitles" +}} +``` + +Type | Value | Description +--- | --- | --- +"system" (default) | N/A | Display captions only if the system preference for captions is enabled +"disabled" | N/A | Don't display a text track +"title" | string | Display the text track with the title specified as the Value, e.g. "French 1" +"language" | string | Display the text track with the language specified as the Value, e.g. "fr" +"index" | number | Display the text track with the index specified as the value, e.g. 0 + +Both iOS & Android offer Settings to enable Captions for hearing impaired people. If "system" is selected and the Captions Setting is enabled, iOS/Android will look for a caption that matches that customer's language and display it. + +If a track matching the specified Type (and Value if appropriate) is unavailable, no text track will be displayed. If multiple tracks match the criteria, the first match will be used. + +Platforms: Android ExoPlayer, iOS + #### volume Adjust the volume. * **1.0 (default)** - Play at full volume diff --git a/Video.js b/Video.js index 80187e95..3df7100f 100644 --- a/Video.js +++ b/Video.js @@ -275,6 +275,13 @@ Video.propTypes = { posterResizeMode: Image.propTypes.resizeMode, repeat: PropTypes.bool, allowsExternalPlayback: PropTypes.bool, + selectedTextTrack: PropTypes.shape({ + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]) + }), paused: PropTypes.bool, muted: PropTypes.bool, volume: PropTypes.number, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index aa60ac14..a6e88409 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -5,17 +5,20 @@ import android.app.Activity; import android.content.Context; import android.media.AudioManager; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.Window; +import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.BecomingNoisyListener; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.uimanager.ThemedReactContext; import com.google.android.exoplayer2.C; @@ -45,6 +48,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -57,6 +61,7 @@ import java.net.CookieManager; import java.net.CookiePolicy; import java.lang.Math; import java.lang.Object; +import java.util.Locale; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -99,6 +104,8 @@ class ReactExoplayerView extends FrameLayout implements private Uri srcUri; private String extension; private boolean repeat; + private String textTrackType; + private Dynamic textTrackValue; private boolean disableFocus; private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; @@ -444,6 +451,7 @@ class ReactExoplayerView extends FrameLayout implements private void videoLoaded() { if (loadVideoStarted) { loadVideoStarted = false; + setSelectedTextTrack(textTrackType, textTrackValue); Format videoFormat = player.getVideoFormat(); int width = videoFormat != null ? videoFormat.width : 0; int height = videoFormat != null ? videoFormat.height : 0; @@ -535,7 +543,8 @@ class ReactExoplayerView extends FrameLayout implements decoderInitializationException.decoderName); } } - } else if (e.type == ExoPlaybackException.TYPE_SOURCE) { + } + else if (e.type == ExoPlaybackException.TYPE_SOURCE) { ex = e.getSourceException(); errorString = getResources().getString(R.string.unrecognized_media_format); } @@ -565,6 +574,16 @@ class ReactExoplayerView extends FrameLayout implements return false; } + public int getTextTrackRendererIndex() { + int rendererCount = player.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (player.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { + return rendererIndex; + } + } + return C.INDEX_UNSET; + } + @Override public void onMetadata(Metadata metadata) { eventEmitter.timedMetadata(metadata); @@ -626,6 +645,60 @@ class ReactExoplayerView extends FrameLayout implements this.repeat = repeat; } + public void setSelectedTextTrack(String type, Dynamic value) { + textTrackType = type; + textTrackValue = value; + + int index = getTextTrackRendererIndex(); + if (index == C.INDEX_UNSET) { + return; + } + MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); + if (info == null) { + return; + } + + TrackGroupArray groups = info.getTrackGroups(index); + int trackIndex = C.INDEX_UNSET; + if (TextUtils.isEmpty(type)) { + // Do nothing + } else if (type.equals("disabled")) { + trackSelector.setSelectionOverride(index, groups, null); + return; + } else if (type.equals("language")) { + for (int i = 0; i < groups.length; ++i) { + Format format = groups.get(i).getFormat(0); + if (format.language != null && format.language.equals(value.asString())) { + trackIndex = i; + break; + } + } + } else if (type.equals("title")) { + for (int i = 0; i < groups.length; ++i) { + Format format = groups.get(i).getFormat(0); + if (format.id != null && format.id.equals(value.asString())) { + trackIndex = i; + break; + } + } + } else if (type.equals("index")) { + trackIndex = value.asInt(); + } else { // default. invalid type or "system" + trackSelector.clearSelectionOverrides(index); + return; + } + + if (trackIndex == C.INDEX_UNSET) { + trackSelector.clearSelectionOverrides(trackIndex); + return; + } + + MappingTrackSelector.SelectionOverride override + = new MappingTrackSelector.SelectionOverride( + new FixedTrackSelection.Factory(), trackIndex, 0); + trackSelector.setSelectionOverride(index, groups, override); + } + public void setPausedModifier(boolean paused) { isPaused = paused; if (player != null) { diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index b500f400..1f42d5ca 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.net.Uri; import android.text.TextUtils; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; @@ -24,6 +25,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager index) { + option = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } + + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:option inMediaSelectionGroup:group]; +} + - (BOOL)getFullscreen { return _fullscreenPlayerPresented; diff --git a/ios/RCTVideoManager.m b/ios/RCTVideoManager.m index 4fa1a1f1..e801447d 100644 --- a/ios/RCTVideoManager.m +++ b/ios/RCTVideoManager.m @@ -23,6 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL); +RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(paused, BOOL); RCT_EXPORT_VIEW_PROPERTY(muted, BOOL); RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);