Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text track selection support for iOS & ExoPlayer #1049

Merged
merged 9 commits into from Jun 6, 2018
33 changes: 33 additions & 0 deletions README.md
Expand Up @@ -230,6 +230,7 @@ var styles = StyleSheet.create({
* [rate](#rate)
* [repeat](#repeat)
* [resizeMode](#resizemode)
* [selectedTextTrack](#selectedtexttrack)
* [volume](#volume)

#### ignoreSilentSwitch
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Video.js
Expand Up @@ -274,6 +274,13 @@ Video.propTypes = {
poster: PropTypes.string,
posterResizeMode: Image.propTypes.resizeMode,
repeat: 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,
Expand Down
Expand Up @@ -5,17 +5,20 @@
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;
Expand Down Expand Up @@ -45,6 +48,7 @@
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;
Expand All @@ -57,6 +61,7 @@
import java.net.CookiePolicy;
import java.lang.Math;
import java.lang.Object;
import java.util.Locale;

@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -444,6 +451,7 @@ private void startProgressHandler() {
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;
Expand Down Expand Up @@ -535,7 +543,8 @@ public void onPlayerError(ExoPlaybackException e) {
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);
}
Expand Down Expand Up @@ -565,6 +574,16 @@ private static boolean isBehindLiveWindow(ExoPlaybackException e) {
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);
Expand Down Expand Up @@ -626,6 +645,60 @@ public void setRepeatModifier(boolean repeat) {
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) {
Expand Down
Expand Up @@ -4,6 +4,7 @@
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;
Expand All @@ -24,6 +25,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_SRC_TYPE = "type";
private static final String PROP_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
private static final String PROP_SELECTED_TEXT_TRACK_TYPE = "type";
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
private static final String PROP_PAUSED = "paused";
private static final String PROP_MUTED = "muted";
private static final String PROP_VOLUME = "volume";
Expand Down Expand Up @@ -116,6 +120,16 @@ public void setRepeat(final ReactExoplayerView videoView, final boolean repeat)
videoView.setRepeatModifier(repeat);
}

@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
public void setSelectedTextTrack(final ReactExoplayerView videoView,
@Nullable ReadableMap selectedTextTrack) {
String typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
Dynamic value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
videoView.setSelectedTextTrack(typeString, value);
}

@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
videoView.setPausedModifier(paused);
Expand Down
46 changes: 46 additions & 0 deletions ios/RCTVideo.m
Expand Up @@ -40,6 +40,7 @@ @implementation RCTVideo
BOOL _muted;
BOOL _paused;
BOOL _repeat;
NSDictionary * _selectedTextTrack;
BOOL _playbackStalled;
BOOL _playInBackground;
BOOL _playWhenInactive;
Expand Down Expand Up @@ -629,6 +630,7 @@ - (void)applyModifiers
[_player setMuted:NO];
}

[self setSelectedTextTrack:_selectedTextTrack];
[self setResizeMode:_resizeMode];
[self setRepeat:_repeat];
[self setPaused:_paused];
Expand All @@ -639,6 +641,50 @@ - (void)setRepeat:(BOOL)repeat {
_repeat = repeat;
}

- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack {
_selectedTextTrack = selectedTextTrack;
NSString *type = selectedTextTrack[@"type"];
AVMediaSelectionGroup *group = [_player.currentItem.asset
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
AVMediaSelectionOption *option;

if ([type isEqualToString:@"disabled"]) {
// Do nothing. We want to ensure option is nil
} else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) {
NSString *value = selectedTextTrack[@"value"];
for (int i = 0; i < group.options.count; ++i) {
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
NSString *optionValue;
if ([type isEqualToString:@"language"]) {
optionValue = [currentOption extendedLanguageTag];
} else {
optionValue = [[[currentOption commonMetadata]
valueForKey:@"value"]
objectAtIndex:0];
}
if ([value isEqualToString:optionValue]) {
option = currentOption;
break;
}
}
//} else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if ([type isEqualToString:@"index"]) {
if ([selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) {
int index = [selectedTextTrack[@"value"] intValue];
if (group.options.count > 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;
Expand Down
1 change: 1 addition & 0 deletions ios/RCTVideoManager.m
Expand Up @@ -22,6 +22,7 @@ - (dispatch_queue_t)methodQueue
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
RCT_EXPORT_VIEW_PROPERTY(repeat, 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);
Expand Down