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:
		| @@ -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 = { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user