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:
Olivier Bouillet 2024-05-22 14:01:55 +02:00 committed by GitHub
parent dbd7d7aa2c
commit cad5c4624c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 530 additions and 288 deletions

View File

@ -9,7 +9,7 @@ class VideoTrack {
var height = 0 var height = 0
var bitrate = 0 var bitrate = 0
var codecs = "" var codecs = ""
var id = -1 var index = -1
var trackId = "" var trackId = ""
var isSelected = false var isSelected = false
} }

View File

@ -194,9 +194,15 @@ public class VideoEventEmitter {
WritableMap audioTrack = Arguments.createMap(); WritableMap audioTrack = Arguments.createMap();
audioTrack.putInt("index", i); audioTrack.putInt("index", i);
audioTrack.putString("title", format.getTitle()); audioTrack.putString("title", format.getTitle());
audioTrack.putString("type", format.getMimeType()); if (format.getMimeType() != null) {
audioTrack.putString("language", format.getLanguage()); audioTrack.putString("type", format.getMimeType());
audioTrack.putInt("bitrate", format.getBitrate()); }
if (format.getLanguage() != null) {
audioTrack.putString("language", format.getLanguage());
}
if (format.getBitrate() > 0) {
audioTrack.putInt("bitrate", format.getBitrate());
}
audioTrack.putBoolean("selected", format.isSelected()); audioTrack.putBoolean("selected", format.isSelected());
waAudioTracks.pushMap(audioTrack); waAudioTracks.pushMap(audioTrack);
} }
@ -214,7 +220,8 @@ public class VideoEventEmitter {
videoTrack.putInt("height",vTrack.getHeight()); videoTrack.putInt("height",vTrack.getHeight());
videoTrack.putInt("bitrate", vTrack.getBitrate()); videoTrack.putInt("bitrate", vTrack.getBitrate());
videoTrack.putString("codecs", vTrack.getCodecs()); 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()); videoTrack.putBoolean("selected", vTrack.isSelected());
waVideoTracks.pushMap(videoTrack); waVideoTracks.pushMap(videoTrack);
} }

View File

@ -36,8 +36,8 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private final AspectRatioFrameLayout layout; private final AspectRatioFrameLayout layout;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private ExoPlayer player; private ExoPlayer player;
private Context context; private final Context context;
private ViewGroup.LayoutParams layoutParams; private final ViewGroup.LayoutParams layoutParams;
private final FrameLayout adOverlayFrameLayout; private final FrameLayout adOverlayFrameLayout;
private boolean useTextureView = true; private boolean useTextureView = true;

View File

@ -1428,13 +1428,14 @@ public class ReactExoplayerView extends FrameLayout implements
videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
if (format.codecs != null) videoTrack.setCodecs(format.codecs); if (format.codecs != null) videoTrack.setCodecs(format.codecs);
videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id); videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
videoTrack.setIndex(trackIndex);
return videoTrack; return videoTrack;
} }
private ArrayList<VideoTrack> getVideoTrackInfo() { private ArrayList<VideoTrack> getVideoTrackInfo() {
ArrayList<VideoTrack> videoTracks = new ArrayList<>(); ArrayList<VideoTrack> videoTracks = new ArrayList<>();
if (trackSelector == null) { 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; return videoTracks;
} }
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
@ -1869,14 +1870,15 @@ public class ReactExoplayerView extends FrameLayout implements
} }
} }
} else if ("index".equals(type)) { } else if ("index".equals(type)) {
try { int iValue = Integer.parseInt(value);
int iValue = Integer.parseInt(value);
if (iValue < groups.length) { if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
groupIndex = iValue;
}
} catch (Exception e) {
DebugLog.e(TAG, "cannot parse index:" + value);
groupIndex = 0; groupIndex = 0;
if (iValue < groups.get(groupIndex).length) {
tracks.set(0, iValue);
}
} else if (iValue < groups.length) {
groupIndex = iValue;
} }
} else if ("resolution".equals(type)) { } else if ("resolution".equals(type)) {
int height = Integer.parseInt(value); int height = Integer.parseInt(value);

View File

@ -237,9 +237,9 @@ Example:
{ title: '#3 English Director Commentary', language: 'en', index: 2, type: 'text/vtt' } { title: '#3 English Director Commentary', language: 'en', index: 2, type: 'text/vtt' }
], ],
videoTracks: [ videoTracks: [
{ bitrate: 3987904, codecs: "avc1.640028", height: 720, trackId: "f1-v1-x3", width: 1280 }, { index: 0, 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 }, { index: 1, 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: 2, bitrate: 1994979, codecs: "avc1.4d401f", height: 480, trackId: "f3-v1-x3", width: 848 }
] ]
} }
``` ```
@ -550,7 +550,8 @@ Payload:
| Property | Type | Description | | 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 | | codecs | string | MimeType of codec used for this track |
| width | number | Track width | | width | number | Track width |
| height | number | Track height | | height | number | Track height |
@ -563,7 +564,8 @@ Example:
{ {
videoTracks: [ videoTracks: [
{ {
trackId: 0, index: O,
trackId: "0",
codecs: 'video/mp4', codecs: 'video/mp4',
width: 1920, width: 1920,
height: 1080, height: 1080,

View File

@ -3,7 +3,6 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import { import {
StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, View,
@ -15,8 +14,6 @@ import {
Alert, Alert,
} from 'react-native'; } from 'react-native';
import {Picker} from '@react-native-picker/picker';
import Video, { import Video, {
AudioTrack, AudioTrack,
OnAudioTracksData, OnAudioTracksData,
@ -39,12 +36,33 @@ import Video, {
OnSeekData, OnSeekData,
OnPlaybackStateChangedData, OnPlaybackStateChangedData,
OnPlaybackRateChangeData, OnPlaybackRateChangeData,
OnVideoTracksData,
VideoTrack,
SelectedVideoTrackType,
SelectedVideoTrack,
BufferingStrategyType, BufferingStrategyType,
ReactVideoSource,
Drm,
TextTracks,
} from 'react-native-video'; } from 'react-native-video';
import ToggleControl from './ToggleControl'; import ToggleControl from './ToggleControl';
import MultiValueControl, { import MultiValueControl, {
MultiValueControlPropType, MultiValueControlPropType,
} from './MultiValueControl'; } 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 { interface StateType {
rate: number; rate: number;
@ -65,8 +83,10 @@ interface StateType {
seeking: boolean; seeking: boolean;
audioTracks: Array<AudioTrack>; audioTracks: Array<AudioTrack>;
textTracks: Array<TextTrack>; textTracks: Array<TextTrack>;
videoTracks: Array<VideoTrack>;
selectedAudioTrack: SelectedTrack | undefined; selectedAudioTrack: SelectedTrack | undefined;
selectedTextTrack: SelectedTrack | undefined; selectedTextTrack: SelectedTrack | undefined;
selectedVideoTrack: SelectedVideoTrack;
srcListId: number; srcListId: number;
loop: boolean; loop: boolean;
showRNVControls: boolean; showRNVControls: boolean;
@ -95,8 +115,12 @@ class VideoPlayer extends Component {
seeking: false, seeking: false,
audioTracks: [], audioTracks: [],
textTracks: [], textTracks: [],
videoTracks: [],
selectedAudioTrack: undefined, selectedAudioTrack: undefined,
selectedTextTrack: undefined, selectedTextTrack: undefined,
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
srcListId: 0, srcListId: 0,
loop: false, loop: false,
showRNVControls: false, showRNVControls: false,
@ -108,7 +132,7 @@ class VideoPlayer extends Component {
seekerWidth = 0; seekerWidth = 0;
// internal usage change to index if you want to select tracks by index instead of lang // internal usage change to index if you want to select tracks by index instead of lang
textTracksSelectionBy = 'lang'; textTracksSelectionBy = 'index';
srcAllPlatformList = [ srcAllPlatformList = [
{ {
@ -123,8 +147,9 @@ class VideoPlayer extends Component {
subtitle: 'Test Subtitle', subtitle: 'Test Subtitle',
artist: 'Test Artist', artist: 'Test Artist',
description: 'Test Description', 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', description: '(hls|live) red bull tv',
@ -134,8 +159,9 @@ class VideoPlayer extends Component {
subtitle: 'Custom Subtitle', subtitle: 'Custom Subtitle',
artist: 'Custom Artist', artist: 'Custom Artist',
description: 'Custom Description', 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', description: 'invalid URL',
@ -239,7 +265,7 @@ class VideoPlayer extends Component {
samplePoster = samplePoster =
'https://upload.wikimedia.org/wikipedia/commons/1/18/React_Native_Logo.png'; '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, Platform.OS === 'android' ? this.srcAndroidList : this.srcIosList,
); );
@ -270,6 +296,7 @@ class VideoPlayer extends Component {
this.setState({duration: data.duration, loading: false}); this.setState({duration: data.duration, loading: false});
this.onAudioTracks(data); this.onAudioTracks(data);
this.onTextTracks(data); this.onTextTracks(data);
this.onVideoTracks(data);
}; };
updateSeeker = () => { updateSeeker = () => {
@ -301,12 +328,12 @@ class VideoPlayer extends Component {
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => { const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
return x.selected; return x.selected;
}); });
if (selectedTrack?.language) { if (selectedTrack?.index) {
this.setState({ this.setState({
audioTracks: data.audioTracks, audioTracks: data.audioTracks,
selectedAudioTrack: { selectedAudioTrack: {
type: 'language', type: SelectedVideoTrackType.INDEX,
value: selectedTrack?.language, value: selectedTrack?.index,
}, },
}); });
} else { } 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) => { onTextTracks = (data: OnTextTracksData) => {
const selectedTrack = data.textTracks?.find((x: TextTrack) => { const selectedTrack = data.textTracks?.find((x: TextTrack) => {
return x?.selected; return x?.selected;
@ -324,13 +358,16 @@ class VideoPlayer extends Component {
if (selectedTrack?.language) { if (selectedTrack?.language) {
this.setState({ this.setState({
textTracks: data.textTracks, textTracks: data.textTracks,
selectedTextTrack: this.textTracksSelectionBy === 'index' ? { selectedTextTrack:
type: 'index', this.textTracksSelectionBy === 'index'
value: selectedTrack?.index, ? {
}: { type: 'index',
type: 'language', value: selectedTrack?.index,
value: selectedTrack?.language, }
}, : {
type: 'language',
value: selectedTrack?.language,
},
}); });
} else { } else {
this.setState({ this.setState({
@ -445,6 +482,9 @@ class VideoPlayer extends Component {
textTracks: [], textTracks: [],
selectedAudioTrack: undefined, selectedAudioTrack: undefined,
selectedTextTrack: undefined, selectedTextTrack: undefined,
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
}); });
} }
@ -641,7 +681,8 @@ class VideoPlayer extends Component {
return ( return (
<View style={styles.topControlsContainer}> <View style={styles.topControlsContainer}>
<Text style={styles.controlOption}> <Text style={styles.controlOption}>
{this.srcList[this.state.srcListId]?.description || 'local file'} {(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)
?.description || 'local file'}
</Text> </Text>
<View> <View>
<TouchableOpacity <TouchableOpacity
@ -667,6 +708,50 @@ class VideoPlayer extends Component {
this.setState({resizeMode: value}); 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() { renderOverlay() {
return ( return (
<> <>
@ -809,77 +894,22 @@ class VideoPlayer extends Component {
</View> </View>
{this.renderSeekBar()} {this.renderSeekBar()}
<View style={styles.generalControls}> <View style={styles.generalControls}>
<Text style={styles.controlOption}>AudioTrack</Text> <AudioTrackSelector
{this.state.audioTracks?.length <= 0 ? ( audioTracks={this.state.audioTracks}
<Text style={styles.emptyPickerItem}>empty</Text> selectedAudioTrack={this.state.selectedAudioTrack}
) : ( onValueChange={this.onSelectedAudioTrackChange}
<Picker />
style={styles.picker} <TextTrackSelector
itemStyle={styles.pickerItem} textTracks={this.state.textTracks}
selectedValue={this.state.selectedAudioTrack?.value} selectedTextTrack={this.state.selectedTextTrack}
onValueChange={itemValue => { onValueChange={this.onSelectedTextTrackChange}
console.log('on audio value change ' + itemValue); textTracksSelectionBy={this.textTracksSelectionBy}
this.setState({ />
selectedAudioTrack: { <VideoTrackSelector
type: 'language', videoTracks={this.state.videoTracks}
value: itemValue, selectedVideoTrack={this.state.selectedVideoTrack}
}, onValueChange={this.onSelectedVideoTrackChange}
}); />
}}>
{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>
)}
</View> </View>
</View> </View>
</> </>
@ -893,6 +923,9 @@ class VideoPlayer extends Component {
? styles.fullScreen ? styles.fullScreen
: styles.halfScreen; : styles.halfScreen;
const currentSrc = this.srcList[this.state.srcListId];
const additionnal = currentSrc as AdditionnalSourceInfo;
return ( return (
<TouchableOpacity style={viewStyle}> <TouchableOpacity style={viewStyle}>
<Video <Video
@ -900,10 +933,10 @@ class VideoPlayer extends Component {
ref={(ref: VideoRef) => { ref={(ref: VideoRef) => {
this.video = ref; this.video = ref;
}} }}
source={this.srcList[this.state.srcListId]} source={currentSrc as ReactVideoSource}
textTracks={this.srcList[this.state.srcListId]?.textTracks} textTracks={additionnal?.textTracks}
adTagUrl={this.srcList[this.state.srcListId]?.adTagUrl} adTagUrl={additionnal?.adTagUrl}
drm={this.srcList[this.state.srcListId]?.drm} drm={additionnal?.drm}
style={viewStyle} style={viewStyle}
rate={this.state.rate} rate={this.state.rate}
paused={this.state.paused} paused={this.state.paused}
@ -915,6 +948,7 @@ class VideoPlayer extends Component {
onLoad={this.onLoad} onLoad={this.onLoad}
onAudioTracks={this.onAudioTracks} onAudioTracks={this.onAudioTracks}
onTextTracks={this.onTextTracks} onTextTracks={this.onTextTracks}
onVideoTracks={this.onVideoTracks}
onTextTrackDataChanged={this.onTextTrackDataChanged} onTextTrackDataChanged={this.onTextTrackDataChanged}
onProgress={this.onProgress} onProgress={this.onProgress}
onEnd={this.onEnd} onEnd={this.onEnd}
@ -930,6 +964,7 @@ class VideoPlayer extends Component {
repeat={this.state.loop} repeat={this.state.loop}
selectedTextTrack={this.state.selectedTextTrack} selectedTextTrack={this.state.selectedTextTrack}
selectedAudioTrack={this.state.selectedAudioTrack} selectedAudioTrack={this.state.selectedAudioTrack}
selectedVideoTrack={this.state.selectedVideoTrack}
playInBackground={false} playInBackground={false}
bufferConfig={{ bufferConfig={{
minBufferMs: 15000, minBufferMs: 15000,
@ -955,7 +990,7 @@ class VideoPlayer extends Component {
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
{this.srcList[this.state.srcListId]?.noView {(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)?.noView
? null ? null
: this.renderVideoView()} : this.renderVideoView()}
{this.renderOverlay()} {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; export default VideoPlayer;

View 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;

View 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;

View 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;

View 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;

View File

@ -6,6 +6,6 @@
"react": ["./node_modules/@types/react"] "react": ["./node_modules/@types/react"]
} }
}, },
"include": ["src", "**/*.js"], "jsx": "react",
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -222,13 +222,14 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
if (!selectedVideoTrack) { if (!selectedVideoTrack) {
return; return;
} }
const value = selectedVideoTrack?.value const type = typeof selectedVideoTrack.value;
? `${selectedVideoTrack.value}` if (type !== 'number' && type !== 'string') {
: undefined; console.log('invalid type provided to selectedVideoTrack');
return;
}
return { return {
type: selectedVideoTrack?.type, type: selectedVideoTrack?.type,
value, value: `${selectedVideoTrack.value}`,
}; };
}, [selectedVideoTrack]); }, [selectedVideoTrack]);

View File

@ -225,7 +225,8 @@ export type OnTextTrackDataChangedData = Readonly<{
export type OnVideoTracksData = Readonly<{ export type OnVideoTracksData = Readonly<{
videoTracks: { videoTracks: {
trackId: Int32; index: Int32;
tracksId?: string;
codecs?: string; codecs?: string;
width?: Float; width?: Float;
height?: Float; height?: Float;

View File

@ -21,6 +21,7 @@ import type {
export type AudioTrack = OnAudioTracksData['audioTracks'][number]; export type AudioTrack = OnAudioTracksData['audioTracks'][number];
export type TextTrack = OnTextTracksData['textTracks'][number]; export type TextTrack = OnTextTracksData['textTracks'][number];
export type VideoTrack = OnVideoTracksData['videoTracks'][number];
export type OnLoadData = Readonly<{ export type OnLoadData = Readonly<{
currentTime: number; currentTime: number;
@ -48,6 +49,15 @@ export type OnLoadData = Readonly<{
type?: WithDefault<'srt' | 'ttml' | 'vtt', 'srt'>; type?: WithDefault<'srt' | 'ttml' | 'vtt', 'srt'>;
selected?: boolean; selected?: boolean;
}[]; }[];
videoTracks: {
index: number;
tracksID?: string;
codecs?: string;
width?: number;
height?: number;
bitrate?: number;
selected?: boolean;
}[];
}>; }>;
export type OnTextTracksData = Readonly<{ export type OnTextTracksData = Readonly<{

View File

@ -117,7 +117,7 @@ export enum SelectedVideoTrackType {
export type SelectedVideoTrack = { export type SelectedVideoTrack = {
type: SelectedVideoTrackType; type: SelectedVideoTrackType;
value?: number; value?: string | number;
}; };
export type SubtitleStyle = { export type SubtitleStyle = {