From 06eb1c57d8c0a56b46c2315f5e598e1c0440e1c2 Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Tue, 17 Jul 2018 14:14:21 -0700 Subject: [PATCH] Support selecting audio tracks Implements audio track selection on iOS & Android ExoPlayer. The prop mirrors the API for selectedTextTrack. --- README.md | 38 +++++- Video.js | 7 ++ .../exoplayer/ReactExoplayerView.java | 119 +++++++++++++----- .../exoplayer/ReactExoplayerViewManager.java | 17 +++ .../exoplayer/VideoEventEmitter.java | 4 +- ios/RCTVideo.m | 85 ++++++++++++- ios/RCTVideoManager.m | 1 + 7 files changed, 231 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index cac87ad2..c1625eee 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ var styles = StyleSheet.create({ * [rate](#rate) * [repeat](#repeat) * [resizeMode](#resizemode) +* [selectedAudioTrack](#selectedaudiotrack) * [selectedTextTrack](#selectedtexttrack) * [stereoPan](#stereopan) * [textTracks](#texttracks) @@ -356,6 +357,36 @@ Determines how to resize the video when the frame doesn't match the raw video di Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP +#### selectedAudioTrack +Configure which audio track, if any, is played. + +``` +selectedAudioTrack={{ + type: Type, + value: Value +}} +``` + +Example: +``` +selectedAudioTrack={{ + type: "title", + value: "Dubbing" +}} +``` + +Type | Value | Description +--- | --- | --- +"system" (default) | N/A | Play the audio track that matches the system language. If none match, play the first track. +"disabled" | N/A | Turn off audio +"title" | string | Play the audio track with the title specified as the Value, e.g. "French" +"language" | string | Play the audio track with the language specified as the Value, e.g. "fr" +"index" | number | Play the audio track with the index specified as the value, e.g. 0 + +If a track matching the specified Type (and Value if appropriate) is unavailable, the first audio track will be played. If multiple tracks match the criteria, the first match will be used. + +Platforms: Android ExoPlayer, iOS + #### selectedTextTrack Configure which text track (caption or subtitle), if any, is shown. @@ -470,7 +501,8 @@ Property | Type | Description currentPosition | number | Time in seconds where the media will start duration | number | Length of the media in seconds naturalSize | object | Properties:
* width - Width in pixels that the video was encoded at
* height - Height in pixels that the video was encoded at
* orientation - "portrait" or "landscape" -textTracks | array | An array of text track info objects with the following properties:
* index - Index number
* title - Description of the track
* language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code
* type - Mime type of track +audioTracks | array | An array of audio track info objects with the following properties:
* index - Index number
* title - Description of the track
* language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code
* type - Mime type of track +textTracks | array | An array of text track info objects with the following properties:
* index - Index number
* title - Description of the track
* language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code
* type - Mime type of track Example: ``` @@ -488,6 +520,10 @@ Example: orientation: 'landscape' width: '1920' }, + audioTracks: [ + { language: 'es', title: 'Spanish', type: 'audio/mpeg', index: 0 }, + { language: 'en', title: 'English', type: 'audio/mpeg', index: 1 } ], + ], textTracks: [ { title: '#1 French', language: 'fr', index: 0, type: 'text/vtt' }, { title: '#2 English CC', language: 'en', index: 1, type: 'text/vtt' }, diff --git a/Video.js b/Video.js index dda530d3..f32725f3 100644 --- a/Video.js +++ b/Video.js @@ -316,6 +316,13 @@ Video.propTypes = { posterResizeMode: Image.propTypes.resizeMode, repeat: PropTypes.bool, allowsExternalPlayback: PropTypes.bool, + selectedAudioTrack: PropTypes.shape({ + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]) + }), selectedTextTrack: PropTypes.shape({ type: PropTypes.string.isRequired, value: PropTypes.oneOfType([ 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 9a036c25..487da343 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -113,6 +113,9 @@ class ReactExoplayerView extends FrameLayout implements private Uri srcUri; private String extension; private boolean repeat; + private String audioTrackType; + private Dynamic audioTrackValue; + private ReadableArray audioTracks; private String textTrackType; private Dynamic textTrackValue; private ReadableArray textTracks; @@ -501,20 +504,43 @@ class ReactExoplayerView extends FrameLayout implements private void videoLoaded() { if (loadVideoStarted) { loadVideoStarted = false; + setSelectedAudioTrack(audioTrackType, audioTrackValue); setSelectedTextTrack(textTrackType, textTrackValue); Format videoFormat = player.getVideoFormat(); int width = videoFormat != null ? videoFormat.width : 0; int height = videoFormat != null ? videoFormat.height : 0; eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height, - getTextTrackInfo()); + getAudioTrackInfo(), getTextTrackInfo()); } } + private WritableArray getAudioTrackInfo() { + WritableArray audioTracks = Arguments.createArray(); + + MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); + int index = getTrackRendererIndex(C.TRACK_TYPE_AUDIO); + if (info == null || index == C.INDEX_UNSET) { + return audioTracks; + } + + TrackGroupArray groups = info.getTrackGroups(index); + for (int i = 0; i < groups.length; ++i) { + Format format = groups.get(i).getFormat(0); + WritableMap textTrack = Arguments.createMap(); + textTrack.putInt("index", i); + textTrack.putString("title", format.id != null ? format.id : ""); + textTrack.putString("type", format.sampleMimeType); + textTrack.putString("language", format.language != null ? format.language : ""); + audioTracks.pushMap(textTrack); + } + return audioTracks; + } + private WritableArray getTextTrackInfo() { WritableArray textTracks = Arguments.createArray(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); - int index = getTextTrackRendererIndex(); + int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT); if (info == null || index == C.INDEX_UNSET) { return textTracks; } @@ -647,10 +673,10 @@ class ReactExoplayerView extends FrameLayout implements return false; } - public int getTextTrackRendererIndex() { + public int getTrackRendererIndex(int trackType) { int rendererCount = player.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - if (player.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { + if (player.getRendererType(rendererIndex) == trackType) { return rendererIndex; } } @@ -724,12 +750,9 @@ 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) { + public void setSelectedTrack(int trackType, String type, Dynamic value) { + int rendererIndex = getTrackRendererIndex(trackType); + if (rendererIndex == C.INDEX_UNSET) { return; } MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); @@ -737,13 +760,15 @@ class ReactExoplayerView extends FrameLayout implements return; } - TrackGroupArray groups = info.getTrackGroups(index); + TrackGroupArray groups = info.getTrackGroups(rendererIndex); int trackIndex = C.INDEX_UNSET; - trackSelector.setSelectionOverride(index, groups, null); if (TextUtils.isEmpty(type)) { - // Do nothing - } else if (type.equals("disabled")) { + type = "default"; + } + + if (type.equals("disabled")) { + trackSelector.setSelectionOverride(rendererIndex, groups, null); return; } else if (type.equals("language")) { for (int i = 0; i < groups.length; ++i) { @@ -762,26 +787,25 @@ class ReactExoplayerView extends FrameLayout implements } } } else if (type.equals("index")) { - trackIndex = value.asInt(); - } else { // default. Use system settings if possible - int sdk = android.os.Build.VERSION.SDK_INT; - if (sdk>18 && groups.length>0) { - CaptioningManager captioningManager = (CaptioningManager) themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); - if (captioningManager.isEnabled()) { - // default is to take the first object - trackIndex = 0; - - String locale = Locale.getDefault().getDisplayLanguage(); - for (int i = 0; i < groups.length; ++i) { - Format format = groups.get(i).getFormat(0); - if (format.language != null && format.language.equals(locale)) { - trackIndex = i; - break; - } + if (value.asInt() < groups.length) { + trackIndex = value.asInt(); + } + } else { // default + if (rendererIndex == C.TRACK_TYPE_TEXT) { // Use system settings if possible + int sdk = android.os.Build.VERSION.SDK_INT; + if (sdk > 18 && groups.length > 0) { + CaptioningManager captioningManager + = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); + if (captioningManager != null && captioningManager.isEnabled()) { + trackIndex = getTrackIndexForDefaultLocale(groups); } + } else { + trackSelector.setSelectionOverride(rendererIndex, groups, null); + return; } - } else return; - + } else if (rendererIndex == C.TRACK_TYPE_AUDIO) { + trackIndex = getTrackIndexForDefaultLocale(groups); + } } if (trackIndex == C.INDEX_UNSET) { @@ -791,8 +815,35 @@ class ReactExoplayerView extends FrameLayout implements MappingTrackSelector.SelectionOverride override = new MappingTrackSelector.SelectionOverride( - new FixedTrackSelection.Factory(), trackIndex, 0); - trackSelector.setSelectionOverride(index, groups, override); + new FixedTrackSelection.Factory(), trackIndex, 0); + trackSelector.setSelectionOverride(rendererIndex, groups, override); + } + + private int getTrackIndexForDefaultLocale(TrackGroupArray groups) { + int trackIndex = 0; // default if no match + String locale2 = Locale.getDefault().getLanguage(); // 2 letter code + String locale3 = Locale.getDefault().getISO3Language(); // 3 letter code + for (int i = 0; i < groups.length; ++i) { + Format format = groups.get(i).getFormat(0); + String language = format.language; + if (language != null && (language.equals(locale2) || language.equals(locale3))) { + trackIndex = i; + break; + } + } + return trackIndex; + } + + public void setSelectedAudioTrack(String type, Dynamic value) { + audioTrackType = type; + audioTrackValue = value; + setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue); + } + + public void setSelectedTextTrack(String type, Dynamic value) { + textTrackType = type; + textTrackValue = value; + setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue); } public void setPausedModifier(boolean paused) { 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 e3775a6e..fbc8a9ad 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -28,6 +28,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager index) { + mediaOption = [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:mediaOption inMediaSelectionGroup:group]; +} + +- (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; +} + - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { _selectedTextTrack = selectedTextTrack; - if (_textTracks) { + if (_textTracks) { // sideloaded text tracks [self setSideloadedText]; - } else { - [self setStreamingText]; + } else { // text tracks included in the HLS playlist + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicLegible + withCriteria:_selectedTextTrack]; } } @@ -869,9 +924,31 @@ static NSString *const timedMetadata = @"timedMetadata"; if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; } +- (NSArray *)getAudioTrackInfo +{ + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; + } + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; +} + - (NSArray *)getTextTrackInfo { - // if sideloaded, textTracks will already be set if (_textTracks) return _textTracks; diff --git a/ios/RCTVideoManager.m b/ios/RCTVideoManager.m index 8e566602..e0e0162e 100644 --- a/ios/RCTVideoManager.m +++ b/ios/RCTVideoManager.m @@ -25,6 +25,7 @@ RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL); RCT_EXPORT_VIEW_PROPERTY(textTracks, NSArray); RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(selectedAudioTrack, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(paused, BOOL); RCT_EXPORT_VIEW_PROPERTY(muted, BOOL); RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);