diff --git a/README.md b/README.md
index 9aac6495..98898b9a 100644
--- a/README.md
+++ b/README.md
@@ -191,8 +191,6 @@ using System.Collections.Generic;
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
- onLoadStart={this.loadStart} // Callback when video starts to load
- onLoad={this.setDuration} // Callback when video loads
onProgress={this.setTime} // Callback every ~250ms with currentTime
onTimedMetadata={this.onTimedMetadata} // Callback when the stream receive some metadata
style={styles.backgroundVideo} />
@@ -233,9 +231,14 @@ var styles = StyleSheet.create({
* [resizeMode](#resizemode)
* [selectedTextTrack](#selectedtexttrack)
* [stereoPan](#stereopan)
+* [textTracks](#texttracks)
* [useTextureView](#usetextureview)
* [volume](#volume)
+### Event props
+* [onLoad](#onload)
+* [onLoadStart](#onloadstart)
+
#### allowsExternalPlayback
Indicates whether the player allows switching to external playback mode such as AirPlay or HDMI.
* **true (default)** - allow switching to external playback mode
@@ -359,7 +362,7 @@ Type | Value | Description
"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.
+Both iOS & Android (only 4.4 and higher) 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.
@@ -373,6 +376,40 @@ Adjust the balance of the left and right audio channels. Any value between –1
Platforms: Android MediaPlayer
+#### textTracks
+Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
+
+Property | Description
+--- | ---
+title | Descriptive name for the track
+language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language
+type | Mime type of the track
* TextTrackType.SRT - .srt SubRip Subtitle
* TextTrackType.TTML - .ttml TTML
* TextTrackType.VTT - .vtt WebVTT
+uri | URL for the text track. Currently, only tracks hosted on a webserver are supported
+
+Example:
+```
+import { TextTrackType }, Video from 'react-native-video';
+
+textTracks={[
+ {
+ title: "English CC",
+ language: "en",
+ type: "text/vtt", TextTrackType.VTT,
+ uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt"
+ },
+ {
+ title: "Spanish Subtitles",
+ language: "es",
+ type: "application/x-subrip", TextTrackType.SRT,
+ uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
+ }
+]}
+```
+
+This isn't support on iOS because AVPlayer doesn't support it. Text tracks must be loaded as part of an HLS playlist.
+
+Platforms: Android ExoPlayer
+
#### useTextureView
Output to a TextureView instead of the default SurfaceView. In general, you will want to use SurfaceView because it is more efficient and provides better performance. However, SurfaceViews has two limitations:
* It can't be animated, transformed or scaled
@@ -393,6 +430,68 @@ Adjust the volume.
Platforms: all
+### Event props
+
+#### onLoad
+Callback function that is called when the media is loaded and ready to play.
+
+Payload:
+
+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
+
+Example:
+```
+{
+ canPlaySlowForward: true,
+ canPlayReverse: false,
+ canPlaySlowReverse: false,
+ canPlayFastForward: false,
+ canStepForward: false,
+ canStepBackward: false,
+ currentTime: 0,
+ duration: 5910.208984375,
+ naturalSize: {
+ height: 1080
+ orientation: 'landscape'
+ width: '1920'
+ },
+ textTracks: [
+ { title: '#1 French', language: 'fr', index: 0, type: 'text/vtt' },
+ { title: '#2 English CC', language: 'en', index: 1, type: 'text/vtt' },
+ { title: '#3 English Director Commentary', language: 'en', index: 2, type: 'text/vtt' }
+ ]
+}
+```
+
+Platforms: all
+
+#### onLoadStart
+Callback function that is called when the media starts loading.
+
+Payload:
+
+Property | Description
+--- | ---
+isNetwork | Boolean indicating if the media is being loaded from the network
+type | Type of the media. Not available on Windows
+uri | URI for the media source. Not available on Windows
+
+Example:
+```
+{
+ isNetwork: true,
+ type: '',
+ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
+}
+```
+
+Platforms: all
+
### Additional props
To see the full list of available props, you can check the [propTypes](https://github.com/react-native-community/react-native-video/blob/master/Video.js#L246) of the Video.js component.
diff --git a/TextTrackType.js b/TextTrackType.js
new file mode 100644
index 00000000..caff65a9
--- /dev/null
+++ b/TextTrackType.js
@@ -0,0 +1,7 @@
+import keyMirror from 'keymirror';
+
+export default {
+ SRT: 'application/x-subrip',
+ TTML: 'application/ttml+xml',
+ VTT: 'text/vtt'
+};
diff --git a/Video.js b/Video.js
index 5d11936c..ddee232a 100644
--- a/Video.js
+++ b/Video.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image} from 'react-native';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
+import TextTrackType from './TextTrackType';
import VideoResizeMode from './VideoResizeMode.js';
const styles = StyleSheet.create({
@@ -10,6 +11,8 @@ const styles = StyleSheet.create({
},
});
+export { TextTrackType };
+
export default class Video extends Component {
constructor(props) {
@@ -282,6 +285,18 @@ Video.propTypes = {
PropTypes.number
])
}),
+ textTracks: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ uri: PropTypes.string.isRequired,
+ type: PropTypes.oneOf([
+ TextTrackType.SRT,
+ TextTrackType.TTML,
+ TextTrackType.VTT,
+ ]),
+ language: PropTypes.string.isRequired
+ })
+ ),
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 22f14759..04c4bb2c 100644
--- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
+++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
@@ -16,8 +16,13 @@ import android.widget.FrameLayout;
import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
+import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.LifecycleEventListener;
+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.uimanager.ThemedReactContext;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
@@ -37,6 +42,8 @@ import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
@@ -51,6 +58,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
@@ -58,6 +66,7 @@ import java.net.CookieManager;
import java.net.CookiePolicy;
import java.lang.Math;
import java.lang.Object;
+import java.util.ArrayList;
@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
@@ -103,6 +112,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean repeat;
private String textTrackType;
private Dynamic textTrackValue;
+ private ReadableArray textTracks;
private boolean disableFocus;
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
@@ -229,7 +239,19 @@ class ReactExoplayerView extends FrameLayout implements
player.setPlaybackParameters(params);
}
if (playerNeedsSource && srcUri != null) {
- MediaSource mediaSource = buildMediaSource(srcUri, extension);
+ ArrayList mediaSourceList = buildTextSources();
+ MediaSource videoSource = buildMediaSource(srcUri, extension);
+ MediaSource mediaSource;
+ if (mediaSourceList.size() == 0) {
+ mediaSource = videoSource;
+ } else {
+ mediaSourceList.add(0, videoSource);
+ MediaSource[] textSourceArray = mediaSourceList.toArray(
+ new MediaSource[mediaSourceList.size()]
+ );
+ mediaSource = new MergingMediaSource(textSourceArray);
+ }
+
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
@@ -263,6 +285,32 @@ class ReactExoplayerView extends FrameLayout implements
}
}
+ private ArrayList buildTextSources() {
+ ArrayList textSources = new ArrayList<>();
+ if (textTracks == null) {
+ return textSources;
+ }
+
+ for (int i = 0; i < textTracks.size(); ++i) {
+ ReadableMap textTrack = textTracks.getMap(i);
+ String language = textTrack.getString("language");
+ String title = textTrack.hasKey("title")
+ ? textTrack.getString("title") : language + " " + i;
+ Uri uri = Uri.parse(textTrack.getString("uri"));
+ MediaSource textSource = buildTextSource(title, uri, textTrack.getString("type"),
+ language);
+ if (textSource != null) {
+ textSources.add(textSource);
+ }
+ }
+ return textSources;
+ }
+
+ private MediaSource buildTextSource(String title, Uri uri, String mimeType, String language) {
+ Format textFormat = Format.createTextSampleFormat(title, mimeType, Format.NO_VALUE, language);
+ return new SingleSampleMediaSource(uri, mediaDataSourceFactory, textFormat, C.TIME_UNSET);
+ }
+
private void releasePlayer() {
if (player != null) {
isPaused = player.getPlayWhenReady();
@@ -453,10 +501,33 @@ class ReactExoplayerView extends FrameLayout implements
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);
+ eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height,
+ getTextTrackInfo());
}
}
+ private WritableArray getTextTrackInfo() {
+ WritableArray textTracks = Arguments.createArray();
+
+ MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
+ int index = getTextTrackRendererIndex();
+ if (info == null || index == C.INDEX_UNSET) {
+ return textTracks;
+ }
+
+ 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);
+ textTrack.putString("type", format.sampleMimeType);
+ textTrack.putString("language", format.language);
+ textTracks.pushMap(textTrack);
+ }
+ return textTracks;
+ }
+
private void onBuffering(boolean buffering) {
if (isBuffering == buffering) {
return;
@@ -623,6 +694,11 @@ class ReactExoplayerView extends FrameLayout implements
}
}
+ public void setTextTracks(ReadableArray textTracks) {
+ this.textTracks = textTracks;
+ reloadSource();
+ }
+
private void reloadSource() {
playerNeedsSource = true;
initializePlayer();
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 4611c078..e204a4e7 100644
--- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java
+++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java
@@ -5,6 +5,7 @@ import android.net.Uri;
import android.text.TextUtils;
import com.facebook.react.bridge.Dynamic;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
@@ -28,6 +29,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager