fix(android)!: rework video tracks management (#3778)
* fix: fix crash when invalid index type is provided and minor clean up * fix: review video track management. Fix index support and rework string vs int in tracks management * fix: ABR track selection check * fix: split track selector in sample and lint code * fix: ensure we don't report null fields * chore: improve tracks displayed * chore: start moving to selection by index only
This commit is contained in:
parent
dbd7d7aa2c
commit
cad5c4624c
@ -9,7 +9,7 @@ class VideoTrack {
|
||||
var height = 0
|
||||
var bitrate = 0
|
||||
var codecs = ""
|
||||
var id = -1
|
||||
var index = -1
|
||||
var trackId = ""
|
||||
var isSelected = false
|
||||
}
|
||||
|
@ -194,9 +194,15 @@ public class VideoEventEmitter {
|
||||
WritableMap audioTrack = Arguments.createMap();
|
||||
audioTrack.putInt("index", i);
|
||||
audioTrack.putString("title", format.getTitle());
|
||||
audioTrack.putString("type", format.getMimeType());
|
||||
audioTrack.putString("language", format.getLanguage());
|
||||
audioTrack.putInt("bitrate", format.getBitrate());
|
||||
if (format.getMimeType() != null) {
|
||||
audioTrack.putString("type", format.getMimeType());
|
||||
}
|
||||
if (format.getLanguage() != null) {
|
||||
audioTrack.putString("language", format.getLanguage());
|
||||
}
|
||||
if (format.getBitrate() > 0) {
|
||||
audioTrack.putInt("bitrate", format.getBitrate());
|
||||
}
|
||||
audioTrack.putBoolean("selected", format.isSelected());
|
||||
waAudioTracks.pushMap(audioTrack);
|
||||
}
|
||||
@ -214,7 +220,8 @@ public class VideoEventEmitter {
|
||||
videoTrack.putInt("height",vTrack.getHeight());
|
||||
videoTrack.putInt("bitrate", vTrack.getBitrate());
|
||||
videoTrack.putString("codecs", vTrack.getCodecs());
|
||||
videoTrack.putInt("trackId",vTrack.getId());
|
||||
videoTrack.putString("trackId", vTrack.getTrackId());
|
||||
videoTrack.putInt("index", vTrack.getIndex());
|
||||
videoTrack.putBoolean("selected", vTrack.isSelected());
|
||||
waVideoTracks.pushMap(videoTrack);
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
|
||||
private final AspectRatioFrameLayout layout;
|
||||
private final ComponentListener componentListener;
|
||||
private ExoPlayer player;
|
||||
private Context context;
|
||||
private ViewGroup.LayoutParams layoutParams;
|
||||
private final Context context;
|
||||
private final ViewGroup.LayoutParams layoutParams;
|
||||
private final FrameLayout adOverlayFrameLayout;
|
||||
|
||||
private boolean useTextureView = true;
|
||||
|
@ -1428,13 +1428,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||
if (format.codecs != null) videoTrack.setCodecs(format.codecs);
|
||||
videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
|
||||
videoTrack.setIndex(trackIndex);
|
||||
return videoTrack;
|
||||
}
|
||||
|
||||
private ArrayList<VideoTrack> getVideoTrackInfo() {
|
||||
ArrayList<VideoTrack> videoTracks = new ArrayList<>();
|
||||
if (trackSelector == null) {
|
||||
// Likely player is unmounting so no audio tracks are available anymore
|
||||
// Likely player is unmounting so no video tracks are available anymore
|
||||
return videoTracks;
|
||||
}
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
@ -1869,14 +1870,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
} else if ("index".equals(type)) {
|
||||
try {
|
||||
int iValue = Integer.parseInt(value);
|
||||
if (iValue < groups.length) {
|
||||
groupIndex = iValue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
DebugLog.e(TAG, "cannot parse index:" + value);
|
||||
int iValue = Integer.parseInt(value);
|
||||
|
||||
if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
|
||||
groupIndex = 0;
|
||||
if (iValue < groups.get(groupIndex).length) {
|
||||
tracks.set(0, iValue);
|
||||
}
|
||||
} else if (iValue < groups.length) {
|
||||
groupIndex = iValue;
|
||||
}
|
||||
} else if ("resolution".equals(type)) {
|
||||
int height = Integer.parseInt(value);
|
||||
|
@ -237,9 +237,9 @@ Example:
|
||||
{ title: '#3 English Director Commentary', language: 'en', index: 2, type: 'text/vtt' }
|
||||
],
|
||||
videoTracks: [
|
||||
{ bitrate: 3987904, codecs: "avc1.640028", height: 720, trackId: "f1-v1-x3", width: 1280 },
|
||||
{ bitrate: 7981888, codecs: "avc1.640028", height: 1080, trackId: "f2-v1-x3", width: 1920 },
|
||||
{ bitrate: 1994979, codecs: "avc1.4d401f", height: 480, trackId: "f3-v1-x3", width: 848 }
|
||||
{ index: 0, bitrate: 3987904, codecs: "avc1.640028", height: 720, trackId: "f1-v1-x3", width: 1280 },
|
||||
{ index: 1, bitrate: 7981888, codecs: "avc1.640028", height: 1080, trackId: "f2-v1-x3", width: 1920 },
|
||||
{ index: 2, bitrate: 1994979, codecs: "avc1.4d401f", height: 480, trackId: "f3-v1-x3", width: 848 }
|
||||
]
|
||||
}
|
||||
```
|
||||
@ -550,7 +550,8 @@ Payload:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ------- | ------------------------------------- |
|
||||
| trackId | number | Internal track ID |
|
||||
| index | number | index of the track |
|
||||
| trackId | string | Internal track ID |
|
||||
| codecs | string | MimeType of codec used for this track |
|
||||
| width | number | Track width |
|
||||
| height | number | Track height |
|
||||
@ -563,7 +564,8 @@ Example:
|
||||
{
|
||||
videoTracks: [
|
||||
{
|
||||
trackId: 0,
|
||||
index: O,
|
||||
trackId: "0",
|
||||
codecs: 'video/mp4',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
|
@ -3,7 +3,6 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@ -15,8 +14,6 @@ import {
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
|
||||
import Video, {
|
||||
AudioTrack,
|
||||
OnAudioTracksData,
|
||||
@ -39,12 +36,33 @@ import Video, {
|
||||
OnSeekData,
|
||||
OnPlaybackStateChangedData,
|
||||
OnPlaybackRateChangeData,
|
||||
OnVideoTracksData,
|
||||
VideoTrack,
|
||||
SelectedVideoTrackType,
|
||||
SelectedVideoTrack,
|
||||
BufferingStrategyType,
|
||||
ReactVideoSource,
|
||||
Drm,
|
||||
TextTracks,
|
||||
} from 'react-native-video';
|
||||
import ToggleControl from './ToggleControl';
|
||||
import MultiValueControl, {
|
||||
MultiValueControlPropType,
|
||||
} from './MultiValueControl';
|
||||
import styles from './styles';
|
||||
import AudioTrackSelector from './components/AudioTracksSelector';
|
||||
import TextTrackSelector from './components/TextTracksSelector';
|
||||
import VideoTrackSelector from './components/VideoTracksSelector';
|
||||
|
||||
type AdditionnalSourceInfo = {
|
||||
textTracks: TextTracks;
|
||||
adTagUrl: string;
|
||||
description: string;
|
||||
drm: Drm;
|
||||
noView: boolean;
|
||||
};
|
||||
|
||||
type SampleVideoSource = ReactVideoSource | AdditionnalSourceInfo;
|
||||
|
||||
interface StateType {
|
||||
rate: number;
|
||||
@ -65,8 +83,10 @@ interface StateType {
|
||||
seeking: boolean;
|
||||
audioTracks: Array<AudioTrack>;
|
||||
textTracks: Array<TextTrack>;
|
||||
videoTracks: Array<VideoTrack>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
selectedVideoTrack: SelectedVideoTrack;
|
||||
srcListId: number;
|
||||
loop: boolean;
|
||||
showRNVControls: boolean;
|
||||
@ -95,8 +115,12 @@ class VideoPlayer extends Component {
|
||||
seeking: false,
|
||||
audioTracks: [],
|
||||
textTracks: [],
|
||||
videoTracks: [],
|
||||
selectedAudioTrack: undefined,
|
||||
selectedTextTrack: undefined,
|
||||
selectedVideoTrack: {
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
},
|
||||
srcListId: 0,
|
||||
loop: false,
|
||||
showRNVControls: false,
|
||||
@ -108,7 +132,7 @@ class VideoPlayer extends Component {
|
||||
seekerWidth = 0;
|
||||
|
||||
// internal usage change to index if you want to select tracks by index instead of lang
|
||||
textTracksSelectionBy = 'lang';
|
||||
textTracksSelectionBy = 'index';
|
||||
|
||||
srcAllPlatformList = [
|
||||
{
|
||||
@ -123,8 +147,9 @@ class VideoPlayer extends Component {
|
||||
subtitle: 'Test Subtitle',
|
||||
artist: 'Test Artist',
|
||||
description: 'Test Description',
|
||||
imageUri: 'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png'
|
||||
}
|
||||
imageUri:
|
||||
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: '(hls|live) red bull tv',
|
||||
@ -134,8 +159,9 @@ class VideoPlayer extends Component {
|
||||
subtitle: 'Custom Subtitle',
|
||||
artist: 'Custom Artist',
|
||||
description: 'Custom Description',
|
||||
imageUri: 'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png'
|
||||
}
|
||||
imageUri:
|
||||
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'invalid URL',
|
||||
@ -239,7 +265,7 @@ class VideoPlayer extends Component {
|
||||
samplePoster =
|
||||
'https://upload.wikimedia.org/wikipedia/commons/1/18/React_Native_Logo.png';
|
||||
|
||||
srcList = this.srcAllPlatformList.concat(
|
||||
srcList: SampleVideoSource[] = this.srcAllPlatformList.concat(
|
||||
Platform.OS === 'android' ? this.srcAndroidList : this.srcIosList,
|
||||
);
|
||||
|
||||
@ -270,6 +296,7 @@ class VideoPlayer extends Component {
|
||||
this.setState({duration: data.duration, loading: false});
|
||||
this.onAudioTracks(data);
|
||||
this.onTextTracks(data);
|
||||
this.onVideoTracks(data);
|
||||
};
|
||||
|
||||
updateSeeker = () => {
|
||||
@ -301,12 +328,12 @@ class VideoPlayer extends Component {
|
||||
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
|
||||
return x.selected;
|
||||
});
|
||||
if (selectedTrack?.language) {
|
||||
if (selectedTrack?.index) {
|
||||
this.setState({
|
||||
audioTracks: data.audioTracks,
|
||||
selectedAudioTrack: {
|
||||
type: 'language',
|
||||
value: selectedTrack?.language,
|
||||
type: SelectedVideoTrackType.INDEX,
|
||||
value: selectedTrack?.index,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@ -316,6 +343,13 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onVideoTracks = (data: OnVideoTracksData) => {
|
||||
console.log('onVideoTracks', data.videoTracks);
|
||||
this.setState({
|
||||
videoTracks: data.videoTracks,
|
||||
});
|
||||
};
|
||||
|
||||
onTextTracks = (data: OnTextTracksData) => {
|
||||
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
|
||||
return x?.selected;
|
||||
@ -324,13 +358,16 @@ class VideoPlayer extends Component {
|
||||
if (selectedTrack?.language) {
|
||||
this.setState({
|
||||
textTracks: data.textTracks,
|
||||
selectedTextTrack: this.textTracksSelectionBy === 'index' ? {
|
||||
type: 'index',
|
||||
value: selectedTrack?.index,
|
||||
}: {
|
||||
type: 'language',
|
||||
value: selectedTrack?.language,
|
||||
},
|
||||
selectedTextTrack:
|
||||
this.textTracksSelectionBy === 'index'
|
||||
? {
|
||||
type: 'index',
|
||||
value: selectedTrack?.index,
|
||||
}
|
||||
: {
|
||||
type: 'language',
|
||||
value: selectedTrack?.language,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
@ -445,6 +482,9 @@ class VideoPlayer extends Component {
|
||||
textTracks: [],
|
||||
selectedAudioTrack: undefined,
|
||||
selectedTextTrack: undefined,
|
||||
selectedVideoTrack: {
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -641,7 +681,8 @@ class VideoPlayer extends Component {
|
||||
return (
|
||||
<View style={styles.topControlsContainer}>
|
||||
<Text style={styles.controlOption}>
|
||||
{this.srcList[this.state.srcListId]?.description || 'local file'}
|
||||
{(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)
|
||||
?.description || 'local file'}
|
||||
</Text>
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
@ -667,6 +708,50 @@ class VideoPlayer extends Component {
|
||||
this.setState({resizeMode: value});
|
||||
};
|
||||
|
||||
onSelectedAudioTrackChange = (itemValue: string) => {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
if (itemValue === 'none') {
|
||||
this.setState({
|
||||
selectedAudioTrack: SelectedVideoTrackType.DISABLED,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedAudioTrack: {
|
||||
type: SelectedVideoTrackType.INDEX,
|
||||
value: itemValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedTextTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
this.setState({
|
||||
selectedTextTrack: {
|
||||
type: this.textTracksSelectionBy === 'index' ? 'index' : 'language',
|
||||
value: itemValue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onSelectedVideoTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
if (itemValue === undefined || itemValue === 'auto') {
|
||||
this.setState({
|
||||
selectedVideoTrack: {
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedVideoTrack: {
|
||||
type: SelectedVideoTrackType.INDEX,
|
||||
value: itemValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderOverlay() {
|
||||
return (
|
||||
<>
|
||||
@ -809,77 +894,22 @@ class VideoPlayer extends Component {
|
||||
</View>
|
||||
{this.renderSeekBar()}
|
||||
<View style={styles.generalControls}>
|
||||
<Text style={styles.controlOption}>AudioTrack</Text>
|
||||
{this.state.audioTracks?.length <= 0 ? (
|
||||
<Text style={styles.emptyPickerItem}>empty</Text>
|
||||
) : (
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={this.state.selectedAudioTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
this.setState({
|
||||
selectedAudioTrack: {
|
||||
type: 'language',
|
||||
value: itemValue,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{this.state.audioTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={track.language}
|
||||
value={track.language}
|
||||
key={track.language}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
)}
|
||||
<Text style={styles.controlOption}>TextTrack</Text>
|
||||
{this.state.textTracks?.length <= 0 ? (
|
||||
<Text style={styles.emptyPickerItem}>empty</Text>
|
||||
) : (
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={this.state.selectedTextTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
console.log('on value change ' + itemValue);
|
||||
this.setState({
|
||||
selectedTextTrack: {
|
||||
type: this.textTracksSelectionBy === 'index' ? 'index': 'language',
|
||||
value: itemValue,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
{this.state.textTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
if (this.textTracksSelectionBy === 'index') {
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.index}`}
|
||||
value={track.index}
|
||||
key={track.index}
|
||||
/>);
|
||||
} else {
|
||||
return (
|
||||
<Picker.Item
|
||||
label={track.language}
|
||||
value={track.language}
|
||||
key={track.language}
|
||||
/>);
|
||||
}
|
||||
})}
|
||||
</Picker>
|
||||
)}
|
||||
<AudioTrackSelector
|
||||
audioTracks={this.state.audioTracks}
|
||||
selectedAudioTrack={this.state.selectedAudioTrack}
|
||||
onValueChange={this.onSelectedAudioTrackChange}
|
||||
/>
|
||||
<TextTrackSelector
|
||||
textTracks={this.state.textTracks}
|
||||
selectedTextTrack={this.state.selectedTextTrack}
|
||||
onValueChange={this.onSelectedTextTrackChange}
|
||||
textTracksSelectionBy={this.textTracksSelectionBy}
|
||||
/>
|
||||
<VideoTrackSelector
|
||||
videoTracks={this.state.videoTracks}
|
||||
selectedVideoTrack={this.state.selectedVideoTrack}
|
||||
onValueChange={this.onSelectedVideoTrackChange}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
@ -893,6 +923,9 @@ class VideoPlayer extends Component {
|
||||
? styles.fullScreen
|
||||
: styles.halfScreen;
|
||||
|
||||
const currentSrc = this.srcList[this.state.srcListId];
|
||||
const additionnal = currentSrc as AdditionnalSourceInfo;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={viewStyle}>
|
||||
<Video
|
||||
@ -900,10 +933,10 @@ class VideoPlayer extends Component {
|
||||
ref={(ref: VideoRef) => {
|
||||
this.video = ref;
|
||||
}}
|
||||
source={this.srcList[this.state.srcListId]}
|
||||
textTracks={this.srcList[this.state.srcListId]?.textTracks}
|
||||
adTagUrl={this.srcList[this.state.srcListId]?.adTagUrl}
|
||||
drm={this.srcList[this.state.srcListId]?.drm}
|
||||
source={currentSrc as ReactVideoSource}
|
||||
textTracks={additionnal?.textTracks}
|
||||
adTagUrl={additionnal?.adTagUrl}
|
||||
drm={additionnal?.drm}
|
||||
style={viewStyle}
|
||||
rate={this.state.rate}
|
||||
paused={this.state.paused}
|
||||
@ -915,6 +948,7 @@ class VideoPlayer extends Component {
|
||||
onLoad={this.onLoad}
|
||||
onAudioTracks={this.onAudioTracks}
|
||||
onTextTracks={this.onTextTracks}
|
||||
onVideoTracks={this.onVideoTracks}
|
||||
onTextTrackDataChanged={this.onTextTrackDataChanged}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
@ -930,6 +964,7 @@ class VideoPlayer extends Component {
|
||||
repeat={this.state.loop}
|
||||
selectedTextTrack={this.state.selectedTextTrack}
|
||||
selectedAudioTrack={this.state.selectedAudioTrack}
|
||||
selectedVideoTrack={this.state.selectedVideoTrack}
|
||||
playInBackground={false}
|
||||
bufferConfig={{
|
||||
minBufferMs: 15000,
|
||||
@ -955,7 +990,7 @@ class VideoPlayer extends Component {
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{this.srcList[this.state.srcListId]?.noView
|
||||
{(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)?.noView
|
||||
? null
|
||||
: this.renderVideoView()}
|
||||
{this.renderOverlay()}
|
||||
@ -963,168 +998,4 @@ class VideoPlayer extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
halfScreen: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: 50,
|
||||
bottom: 100,
|
||||
right: 100,
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
bottomControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
leftControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
},
|
||||
rightControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
},
|
||||
topControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftRightControlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
padding: 10,
|
||||
lineHeight: 12,
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
pickerContainer: {
|
||||
width: 100,
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
borderWidth: 1,
|
||||
borderColor: 'red',
|
||||
},
|
||||
IndicatorStyle: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
seekbarContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
height: 30,
|
||||
},
|
||||
seekbarTrack: {
|
||||
backgroundColor: '#333',
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
top: 14,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarFill: {
|
||||
backgroundColor: '#FFF',
|
||||
height: 1,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarHandle: {
|
||||
position: 'absolute',
|
||||
marginLeft: -7,
|
||||
height: 28,
|
||||
width: 28,
|
||||
},
|
||||
seekbarCircle: {
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
top: 8,
|
||||
left: 8,
|
||||
height: 12,
|
||||
width: 12,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
pickerItem: {
|
||||
color: 'white',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
emptyPickerItem: {
|
||||
color: 'white',
|
||||
marginTop: 20,
|
||||
marginLeft: 20,
|
||||
flex: 1,
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
topControlsContainer: {
|
||||
paddingTop: 30,
|
||||
}
|
||||
});
|
||||
|
||||
export default VideoPlayer;
|
||||
|
53
examples/basic/src/components/AudioTracksSelector.tsx
Normal file
53
examples/basic/src/components/AudioTracksSelector.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {AudioTrack, SelectedTrack} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface AudioTrackSelectorType {
|
||||
audioTracks: Array<AudioTrack>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
}
|
||||
|
||||
const AudioTrackSelector = ({
|
||||
audioTracks,
|
||||
selectedAudioTrack,
|
||||
onValueChange,
|
||||
}: AudioTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>AudioTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={selectedAudioTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
onValueChange(`${itemValue}`);
|
||||
}
|
||||
}}>
|
||||
{audioTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{audioTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.language} - ${track.title} - ${track.selected}`}
|
||||
value={`${track.index}`}
|
||||
key={`${track.index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioTrackSelector;
|
64
examples/basic/src/components/TextTracksSelector.tsx
Normal file
64
examples/basic/src/components/TextTracksSelector.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {TextTrack, SelectedTrack} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface TextTrackSelectorType {
|
||||
textTracks: Array<TextTrack>;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
textTracksSelectionBy: string;
|
||||
}
|
||||
|
||||
const TextTrackSelector = ({
|
||||
textTracks,
|
||||
selectedTextTrack,
|
||||
onValueChange,
|
||||
textTracksSelectionBy,
|
||||
}: TextTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>TextTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={`${selectedTextTrack?.value}`}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
{textTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{textTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
if (textTracksSelectionBy === 'index') {
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.index}`}
|
||||
value={track.index}
|
||||
key={track.index}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Picker.Item
|
||||
label={track.language}
|
||||
value={track.language}
|
||||
key={track.language}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextTrackSelector;
|
64
examples/basic/src/components/VideoTracksSelector.tsx
Normal file
64
examples/basic/src/components/VideoTracksSelector.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
SelectedVideoTrack,
|
||||
SelectedVideoTrackType,
|
||||
VideoTrack,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface VideoTrackSelectorType {
|
||||
videoTracks: Array<VideoTrack>;
|
||||
selectedVideoTrack: SelectedVideoTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
}
|
||||
|
||||
const VideoTrackSelector = ({
|
||||
videoTracks,
|
||||
selectedVideoTrack,
|
||||
onValueChange,
|
||||
}: VideoTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>VideoTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={
|
||||
selectedVideoTrack === undefined ||
|
||||
selectedVideoTrack?.type === SelectedVideoTrackType.AUTO
|
||||
? 'auto'
|
||||
: `${selectedVideoTrack?.value}`
|
||||
}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
<Picker.Item label={'auto'} value={'auto'} key={'auto'} />
|
||||
{videoTracks?.length <= 0 || videoTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{videoTracks?.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.width}x${track.height} ${Math.floor(
|
||||
(track.bitrate || 0) / 8 / 1024,
|
||||
)} Kbps`}
|
||||
value={`${track.index}`}
|
||||
key={track.index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoTrackSelector;
|
167
examples/basic/src/styles.tsx
Normal file
167
examples/basic/src/styles.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
halfScreen: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: 50,
|
||||
bottom: 100,
|
||||
right: 100,
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
bottomControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
leftControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
},
|
||||
rightControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
},
|
||||
topControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftRightControlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
padding: 10,
|
||||
lineHeight: 12,
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
pickerContainer: {
|
||||
width: 100,
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
borderWidth: 1,
|
||||
borderColor: 'red',
|
||||
},
|
||||
IndicatorStyle: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
seekbarContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
height: 30,
|
||||
},
|
||||
seekbarTrack: {
|
||||
backgroundColor: '#333',
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
top: 14,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarFill: {
|
||||
backgroundColor: '#FFF',
|
||||
height: 1,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarHandle: {
|
||||
position: 'absolute',
|
||||
marginLeft: -7,
|
||||
height: 28,
|
||||
width: 28,
|
||||
},
|
||||
seekbarCircle: {
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
top: 8,
|
||||
left: 8,
|
||||
height: 12,
|
||||
width: 12,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
color: 'white',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
pickerItem: {
|
||||
color: 'white',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
emptyPickerItem: {
|
||||
color: 'white',
|
||||
marginTop: 20,
|
||||
marginLeft: 20,
|
||||
flex: 1,
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
topControlsContainer: {
|
||||
paddingTop: 30,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
@ -6,6 +6,6 @@
|
||||
"react": ["./node_modules/@types/react"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "**/*.js"],
|
||||
"jsx": "react",
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
@ -222,13 +222,14 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
if (!selectedVideoTrack) {
|
||||
return;
|
||||
}
|
||||
const value = selectedVideoTrack?.value
|
||||
? `${selectedVideoTrack.value}`
|
||||
: undefined;
|
||||
|
||||
const type = typeof selectedVideoTrack.value;
|
||||
if (type !== 'number' && type !== 'string') {
|
||||
console.log('invalid type provided to selectedVideoTrack');
|
||||
return;
|
||||
}
|
||||
return {
|
||||
type: selectedVideoTrack?.type,
|
||||
value,
|
||||
value: `${selectedVideoTrack.value}`,
|
||||
};
|
||||
}, [selectedVideoTrack]);
|
||||
|
||||
|
@ -225,7 +225,8 @@ export type OnTextTrackDataChangedData = Readonly<{
|
||||
|
||||
export type OnVideoTracksData = Readonly<{
|
||||
videoTracks: {
|
||||
trackId: Int32;
|
||||
index: Int32;
|
||||
tracksId?: string;
|
||||
codecs?: string;
|
||||
width?: Float;
|
||||
height?: Float;
|
||||
|
@ -21,6 +21,7 @@ import type {
|
||||
|
||||
export type AudioTrack = OnAudioTracksData['audioTracks'][number];
|
||||
export type TextTrack = OnTextTracksData['textTracks'][number];
|
||||
export type VideoTrack = OnVideoTracksData['videoTracks'][number];
|
||||
|
||||
export type OnLoadData = Readonly<{
|
||||
currentTime: number;
|
||||
@ -48,6 +49,15 @@ export type OnLoadData = Readonly<{
|
||||
type?: WithDefault<'srt' | 'ttml' | 'vtt', 'srt'>;
|
||||
selected?: boolean;
|
||||
}[];
|
||||
videoTracks: {
|
||||
index: number;
|
||||
tracksID?: string;
|
||||
codecs?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
bitrate?: number;
|
||||
selected?: boolean;
|
||||
}[];
|
||||
}>;
|
||||
|
||||
export type OnTextTracksData = Readonly<{
|
||||
|
@ -117,7 +117,7 @@ export enum SelectedVideoTrackType {
|
||||
|
||||
export type SelectedVideoTrack = {
|
||||
type: SelectedVideoTrackType;
|
||||
value?: number;
|
||||
value?: string | number;
|
||||
};
|
||||
|
||||
export type SubtitleStyle = {
|
||||
|
Loading…
Reference in New Issue
Block a user