From cad5c4624cf593e02f0f3f0717ac609fd5e0153f Mon Sep 17 00:00:00 2001 From: Olivier Bouillet <62574056+freeboub@users.noreply.github.com> Date: Wed, 22 May 2024 14:01:55 +0200 Subject: [PATCH] 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 --- .../com/brentvatne/common/api/VideoTrack.kt | 2 +- .../common/react/VideoEventEmitter.java | 15 +- .../brentvatne/exoplayer/ExoPlayerView.java | 4 +- .../exoplayer/ReactExoplayerView.java | 18 +- docs/pages/component/events.mdx | 12 +- examples/basic/src/VideoPlayer.tsx | 391 ++++++------------ .../src/components/AudioTracksSelector.tsx | 53 +++ .../src/components/TextTracksSelector.tsx | 64 +++ .../src/components/VideoTracksSelector.tsx | 64 +++ examples/basic/src/styles.tsx | 167 ++++++++ examples/basic/tsconfig.json | 2 +- src/Video.tsx | 11 +- src/specs/VideoNativeComponent.ts | 3 +- src/types/events.ts | 10 + src/types/video.ts | 2 +- 15 files changed, 530 insertions(+), 288 deletions(-) create mode 100644 examples/basic/src/components/AudioTracksSelector.tsx create mode 100644 examples/basic/src/components/TextTracksSelector.tsx create mode 100644 examples/basic/src/components/VideoTracksSelector.tsx create mode 100644 examples/basic/src/styles.tsx diff --git a/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt b/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt index 60e5da26..6556414b 100644 --- a/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt +++ b/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt @@ -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 } diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java index 8010bf65..f923cf9f 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java @@ -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); } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index dc5b184c..9e5bb733 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -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; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 13f25366..ceece964 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -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 getVideoTrackInfo() { ArrayList 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); diff --git a/docs/pages/component/events.mdx b/docs/pages/component/events.mdx index d0cc94a3..4307ee41 100644 --- a/docs/pages/component/events.mdx +++ b/docs/pages/component/events.mdx @@ -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, diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index c83385b7..aafd8542 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -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; textTracks: Array; + videoTracks: Array; 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 ( - {this.srcList[this.state.srcListId]?.description || 'local file'} + {(this.srcList[this.state.srcListId] as AdditionnalSourceInfo) + ?.description || 'local file'} { + 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 { {this.renderSeekBar()} - AudioTrack - {this.state.audioTracks?.length <= 0 ? ( - empty - ) : ( - { - console.log('on audio value change ' + itemValue); - this.setState({ - selectedAudioTrack: { - type: 'language', - value: itemValue, - }, - }); - }}> - {this.state.audioTracks.map(track => { - if (!track) { - return; - } - return ( - - ); - })} - - )} - TextTrack - {this.state.textTracks?.length <= 0 ? ( - empty - ) : ( - { - console.log('on value change ' + itemValue); - this.setState({ - selectedTextTrack: { - type: this.textTracksSelectionBy === 'index' ? 'index': 'language', - value: itemValue, - }, - }); - }}> - - {this.state.textTracks.map(track => { - if (!track) { - return; - } - if (this.textTracksSelectionBy === 'index') { - return ( - ); - } else { - return ( - ); - } - })} - - )} + + + @@ -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 (