@stepincto/expo-video 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
package expo.modules.video.player
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.MediaMetadataRetriever
|
|
5
|
+
import androidx.media3.common.C
|
|
6
|
+
import android.webkit.URLUtil
|
|
7
|
+
import androidx.annotation.OptIn
|
|
8
|
+
import androidx.media3.common.Format
|
|
9
|
+
import androidx.media3.common.MediaItem
|
|
10
|
+
import androidx.media3.common.MimeTypes
|
|
11
|
+
import androidx.media3.common.PlaybackException
|
|
12
|
+
import androidx.media3.common.PlaybackParameters
|
|
13
|
+
import androidx.media3.common.Player
|
|
14
|
+
import androidx.media3.common.Player.STATE_BUFFERING
|
|
15
|
+
import androidx.media3.common.Timeline
|
|
16
|
+
import androidx.media3.common.TrackSelectionParameters
|
|
17
|
+
import androidx.media3.common.Tracks
|
|
18
|
+
import androidx.media3.common.util.UnstableApi
|
|
19
|
+
import androidx.media3.exoplayer.DecoderReuseEvaluation
|
|
20
|
+
import androidx.media3.exoplayer.DefaultRenderersFactory
|
|
21
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
22
|
+
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
|
23
|
+
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
|
24
|
+
import androidx.media3.ui.PlayerView
|
|
25
|
+
import expo.modules.kotlin.AppContext
|
|
26
|
+
import expo.modules.kotlin.sharedobjects.SharedObject
|
|
27
|
+
import expo.modules.video.IntervalUpdateClock
|
|
28
|
+
import expo.modules.video.IntervalUpdateEmitter
|
|
29
|
+
import expo.modules.video.VideoManager
|
|
30
|
+
import expo.modules.video.delegates.IgnoreSameSet
|
|
31
|
+
import expo.modules.video.enums.AudioMixingMode
|
|
32
|
+
import expo.modules.video.enums.PlayerStatus
|
|
33
|
+
import expo.modules.video.enums.PlayerStatus.*
|
|
34
|
+
import expo.modules.video.playbackService.ExpoVideoPlaybackService
|
|
35
|
+
import expo.modules.video.playbackService.PlaybackServiceConnection
|
|
36
|
+
import expo.modules.video.records.BufferOptions
|
|
37
|
+
import expo.modules.video.records.PlaybackError
|
|
38
|
+
import expo.modules.video.records.TimeUpdate
|
|
39
|
+
import expo.modules.video.records.VideoSource
|
|
40
|
+
import expo.modules.video.utils.MutableWeakReference
|
|
41
|
+
import expo.modules.video.records.VideoTrack
|
|
42
|
+
import kotlinx.coroutines.launch
|
|
43
|
+
import java.io.FileInputStream
|
|
44
|
+
import java.lang.ref.WeakReference
|
|
45
|
+
|
|
46
|
+
// https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
|
|
47
|
+
@UnstableApi
|
|
48
|
+
class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSource?) : AutoCloseable, SharedObject(appContext), IntervalUpdateEmitter {
|
|
49
|
+
// This improves the performance of playing DRM-protected content
|
|
50
|
+
private var renderersFactory = DefaultRenderersFactory(context)
|
|
51
|
+
.forceEnableMediaCodecAsynchronousQueueing()
|
|
52
|
+
.setEnableDecoderFallback(true)
|
|
53
|
+
private var listeners: MutableList<WeakReference<VideoPlayerListener>> = mutableListOf()
|
|
54
|
+
private var currentPlayerView = MutableWeakReference<PlayerView?>(null)
|
|
55
|
+
val loadControl: VideoPlayerLoadControl = VideoPlayerLoadControl.Builder().build()
|
|
56
|
+
val subtitles: VideoPlayerSubtitles = VideoPlayerSubtitles(this)
|
|
57
|
+
val audioTracks: VideoPlayerAudioTracks = VideoPlayerAudioTracks(this)
|
|
58
|
+
val trackSelector = DefaultTrackSelector(context)
|
|
59
|
+
|
|
60
|
+
val player = ExoPlayer
|
|
61
|
+
.Builder(context, renderersFactory)
|
|
62
|
+
.setLooper(context.mainLooper)
|
|
63
|
+
.setLoadControl(loadControl)
|
|
64
|
+
.build()
|
|
65
|
+
|
|
66
|
+
private val firstFrameEventGenerator = createFirstFrameEventGenerator()
|
|
67
|
+
val serviceConnection = PlaybackServiceConnection(WeakReference(this))
|
|
68
|
+
val intervalUpdateClock = IntervalUpdateClock(this)
|
|
69
|
+
|
|
70
|
+
var playing by IgnoreSameSet(false) { new, old ->
|
|
71
|
+
sendEvent(PlayerEvent.IsPlayingChanged(new, old))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var uncommittedSource: VideoSource? = source
|
|
75
|
+
private var commitedSource by IgnoreSameSet<VideoSource?>(null) { new, old ->
|
|
76
|
+
sendEvent(PlayerEvent.SourceChanged(new, old))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Volume of the player if there was no mute applied.
|
|
80
|
+
var userVolume = 1f
|
|
81
|
+
var status: PlayerStatus = IDLE
|
|
82
|
+
var requiresLinearPlayback = false
|
|
83
|
+
var staysActiveInBackground = false
|
|
84
|
+
var preservesPitch = false
|
|
85
|
+
set(preservesPitch) {
|
|
86
|
+
field = preservesPitch
|
|
87
|
+
playbackParameters = applyPitchCorrection(playbackParameters)
|
|
88
|
+
}
|
|
89
|
+
var showNowPlayingNotification = false
|
|
90
|
+
set(value) {
|
|
91
|
+
field = value
|
|
92
|
+
serviceConnection.playbackServiceBinder?.service?.setShowNotification(value, this.player)
|
|
93
|
+
}
|
|
94
|
+
var duration = 0f
|
|
95
|
+
var isLive = false
|
|
96
|
+
|
|
97
|
+
var volume: Float by IgnoreSameSet(1f) { new: Float, old: Float ->
|
|
98
|
+
player.volume = if (muted) 0f else new
|
|
99
|
+
userVolume = volume
|
|
100
|
+
sendEvent(PlayerEvent.VolumeChanged(new, old))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var muted: Boolean by IgnoreSameSet(false) { new: Boolean, old: Boolean ->
|
|
104
|
+
player.volume = if (new) 0f else userVolume
|
|
105
|
+
sendEvent(PlayerEvent.MutedChanged(new, old))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
var playbackParameters by IgnoreSameSet(
|
|
109
|
+
PlaybackParameters.DEFAULT,
|
|
110
|
+
propertyMapper = { applyPitchCorrection(it) }
|
|
111
|
+
) { new: PlaybackParameters, old: PlaybackParameters ->
|
|
112
|
+
player.playbackParameters = new
|
|
113
|
+
|
|
114
|
+
if (old.speed != new.speed) {
|
|
115
|
+
sendEvent(PlayerEvent.PlaybackRateChanged(new.speed, old.speed))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
val currentOffsetFromLive: Float?
|
|
120
|
+
get() {
|
|
121
|
+
return if (player.currentLiveOffset == C.TIME_UNSET) {
|
|
122
|
+
null
|
|
123
|
+
} else {
|
|
124
|
+
player.currentLiveOffset / 1000f
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
val currentLiveTimestamp: Long?
|
|
129
|
+
get() {
|
|
130
|
+
val window = Timeline.Window()
|
|
131
|
+
if (!player.currentTimeline.isEmpty) {
|
|
132
|
+
player.currentTimeline.getWindow(player.currentMediaItemIndex, window)
|
|
133
|
+
}
|
|
134
|
+
if (window.windowStartTimeMs == C.TIME_UNSET) {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
return window.windowStartTimeMs + player.currentPosition
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var bufferOptions: BufferOptions = BufferOptions()
|
|
141
|
+
set(value) {
|
|
142
|
+
field = value
|
|
143
|
+
loadControl.applyBufferOptions(value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
val bufferedPosition: Double
|
|
147
|
+
get() {
|
|
148
|
+
if (player.currentMediaItem == null) {
|
|
149
|
+
return -1.0
|
|
150
|
+
}
|
|
151
|
+
if (player.playbackState == STATE_BUFFERING) {
|
|
152
|
+
return 0.0
|
|
153
|
+
}
|
|
154
|
+
return player.bufferedPosition / 1000.0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var audioMixingMode: AudioMixingMode = AudioMixingMode.AUTO
|
|
158
|
+
set(value) {
|
|
159
|
+
val old = field
|
|
160
|
+
field = value
|
|
161
|
+
sendEvent(PlayerEvent.AudioMixingModeChanged(value, old))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var isLoadingNewSource = false
|
|
165
|
+
private set
|
|
166
|
+
|
|
167
|
+
var currentVideoTrack: VideoTrack? = null
|
|
168
|
+
private set(value) {
|
|
169
|
+
val old = field
|
|
170
|
+
field = value
|
|
171
|
+
sendEvent(PlayerEvent.VideoTrackChanged(value, old))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var availableVideoTracks: List<VideoTrack> = emptyList()
|
|
175
|
+
private set
|
|
176
|
+
|
|
177
|
+
private val playerListener = object : Player.Listener {
|
|
178
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
179
|
+
this@VideoPlayer.playing = isPlaying
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override fun onTracksChanged(tracks: Tracks) {
|
|
183
|
+
val oldSubtitleTracks = ArrayList(subtitles.availableSubtitleTracks)
|
|
184
|
+
val oldAudioTracks = ArrayList(audioTracks.availableAudioTracks)
|
|
185
|
+
val oldCurrentTrack = subtitles.currentSubtitleTrack
|
|
186
|
+
val oldCurrentAudioTrack = audioTracks.currentAudioTrack
|
|
187
|
+
|
|
188
|
+
// Emit the tracks change event to update the subtitles
|
|
189
|
+
sendEvent(PlayerEvent.TracksChanged(tracks))
|
|
190
|
+
|
|
191
|
+
val newSubtitleTracks = subtitles.availableSubtitleTracks
|
|
192
|
+
val newAudioTracks = audioTracks.availableAudioTracks
|
|
193
|
+
val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
|
|
194
|
+
val newCurrentAudioTrack = audioTracks.currentAudioTrack
|
|
195
|
+
availableVideoTracks = tracks.toVideoTracks()
|
|
196
|
+
|
|
197
|
+
if (isLoadingNewSource) {
|
|
198
|
+
sendEvent(
|
|
199
|
+
PlayerEvent.VideoSourceLoaded(
|
|
200
|
+
commitedSource,
|
|
201
|
+
this@VideoPlayer.player.duration / 1000.0,
|
|
202
|
+
availableVideoTracks,
|
|
203
|
+
newSubtitleTracks,
|
|
204
|
+
newAudioTracks
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
isLoadingNewSource = false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!oldSubtitleTracks.toArray().contentEquals(newSubtitleTracks.toArray())) {
|
|
211
|
+
sendEvent(PlayerEvent.AvailableSubtitleTracksChanged(newSubtitleTracks, oldSubtitleTracks))
|
|
212
|
+
}
|
|
213
|
+
if (!oldAudioTracks.toArray().contentEquals(newAudioTracks.toArray())) {
|
|
214
|
+
sendEvent(PlayerEvent.AvailableAudioTracksChanged(newAudioTracks, oldAudioTracks))
|
|
215
|
+
}
|
|
216
|
+
if (oldCurrentTrack != newCurrentSubtitleTrack) {
|
|
217
|
+
sendEvent(PlayerEvent.SubtitleTrackChanged(newCurrentSubtitleTrack, oldCurrentTrack))
|
|
218
|
+
}
|
|
219
|
+
if (oldCurrentAudioTrack != newCurrentAudioTrack) {
|
|
220
|
+
sendEvent(PlayerEvent.AudioTrackChanged(newCurrentAudioTrack, oldCurrentAudioTrack))
|
|
221
|
+
}
|
|
222
|
+
super.onTracksChanged(tracks)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {
|
|
226
|
+
val oldTrack = subtitles.currentSubtitleTrack
|
|
227
|
+
val oldAudioTrack = audioTracks.currentAudioTrack
|
|
228
|
+
sendEvent(PlayerEvent.TrackSelectionParametersChanged(parameters))
|
|
229
|
+
|
|
230
|
+
val newTrack = subtitles.currentSubtitleTrack
|
|
231
|
+
val newAudioTrack = audioTracks.currentAudioTrack
|
|
232
|
+
sendEvent(PlayerEvent.SubtitleTrackChanged(newTrack, oldTrack))
|
|
233
|
+
sendEvent(PlayerEvent.AudioTrackChanged(newAudioTrack, oldAudioTrack))
|
|
234
|
+
super.onTrackSelectionParametersChanged(parameters)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
238
|
+
this@VideoPlayer.duration = 0f
|
|
239
|
+
this@VideoPlayer.isLive = false
|
|
240
|
+
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) {
|
|
241
|
+
sendEvent(PlayerEvent.PlayedToEnd())
|
|
242
|
+
}
|
|
243
|
+
subtitles.setSubtitlesEnabled(false)
|
|
244
|
+
super.onMediaItemTransition(mediaItem, reason)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
|
|
248
|
+
if (playbackState == Player.STATE_IDLE && player.playerError != null) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (playbackState == Player.STATE_READY) {
|
|
252
|
+
this@VideoPlayer.duration = this@VideoPlayer.player.duration / 1000f
|
|
253
|
+
this@VideoPlayer.isLive = this@VideoPlayer.player.isCurrentMediaItemLive
|
|
254
|
+
}
|
|
255
|
+
setStatus(playerStateToPlayerStatus(playbackState), null)
|
|
256
|
+
super.onPlaybackStateChanged(playbackState)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
override fun onVolumeChanged(volume: Float) {
|
|
260
|
+
if (!muted) {
|
|
261
|
+
this@VideoPlayer.volume = volume
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
|
|
266
|
+
this@VideoPlayer.playbackParameters = playbackParameters
|
|
267
|
+
super.onPlaybackParametersChanged(playbackParameters)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override fun onPlayerErrorChanged(error: PlaybackException?) {
|
|
271
|
+
error?.let {
|
|
272
|
+
this@VideoPlayer.duration = 0f
|
|
273
|
+
this@VideoPlayer.isLive = false
|
|
274
|
+
setStatus(ERROR, error)
|
|
275
|
+
} ?: run {
|
|
276
|
+
setStatus(playerStateToPlayerStatus(player.playbackState), null)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
super.onPlayerErrorChanged(error)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private val analyticsListener = object : AnalyticsListener {
|
|
284
|
+
override fun onVideoInputFormatChanged(eventTime: AnalyticsListener.EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) {
|
|
285
|
+
currentVideoTrack = availableVideoTracks.firstOrNull { it.format?.id == format.id }
|
|
286
|
+
super.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
init {
|
|
291
|
+
ExpoVideoPlaybackService.startService(appContext, context, serviceConnection)
|
|
292
|
+
player.addListener(playerListener)
|
|
293
|
+
player.addAnalyticsListener(analyticsListener)
|
|
294
|
+
VideoManager.registerVideoPlayer(this)
|
|
295
|
+
|
|
296
|
+
// ExoPlayer will enable subtitles automatically at the start, we want them disabled by default
|
|
297
|
+
appContext.mainQueue.launch {
|
|
298
|
+
subtitles.setSubtitlesEnabled(false)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override fun close() {
|
|
303
|
+
appContext?.reactContext?.unbindService(serviceConnection)
|
|
304
|
+
serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(player)
|
|
305
|
+
VideoManager.unregisterVideoPlayer(this@VideoPlayer)
|
|
306
|
+
|
|
307
|
+
appContext?.mainQueue?.launch {
|
|
308
|
+
player.removeListener(playerListener)
|
|
309
|
+
player.release()
|
|
310
|
+
}
|
|
311
|
+
uncommittedSource = null
|
|
312
|
+
commitedSource = null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
override fun deallocate() {
|
|
316
|
+
super.deallocate()
|
|
317
|
+
close()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fun changePlayerView(playerView: PlayerView?) {
|
|
321
|
+
PlayerView.switchTargetView(player, currentPlayerView.get(), playerView)
|
|
322
|
+
currentPlayerView.set(playerView)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fun prepare() {
|
|
326
|
+
availableVideoTracks = listOf()
|
|
327
|
+
currentVideoTrack = null
|
|
328
|
+
|
|
329
|
+
val newSource = uncommittedSource
|
|
330
|
+
val mediaSource = newSource?.toMediaSource(context)
|
|
331
|
+
|
|
332
|
+
mediaSource?.let {
|
|
333
|
+
player.setMediaSource(it)
|
|
334
|
+
player.prepare()
|
|
335
|
+
commitedSource = newSource
|
|
336
|
+
uncommittedSource = null
|
|
337
|
+
isLoadingNewSource = true
|
|
338
|
+
} ?: run {
|
|
339
|
+
player.clearMediaItems()
|
|
340
|
+
player.prepare()
|
|
341
|
+
isLoadingNewSource = false
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun applyPitchCorrection(playbackParameters: PlaybackParameters): PlaybackParameters {
|
|
346
|
+
val speed = playbackParameters.speed
|
|
347
|
+
val pitch = if (preservesPitch) 1f else speed
|
|
348
|
+
return PlaybackParameters(speed, pitch)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private fun playerStateToPlayerStatus(@Player.State state: Int): PlayerStatus {
|
|
352
|
+
return when (state) {
|
|
353
|
+
Player.STATE_IDLE -> IDLE
|
|
354
|
+
Player.STATE_BUFFERING -> LOADING
|
|
355
|
+
Player.STATE_READY -> READY_TO_PLAY
|
|
356
|
+
Player.STATE_ENDED -> {
|
|
357
|
+
// When an error occurs, the player state changes to ENDED.
|
|
358
|
+
if (player.playerError != null) {
|
|
359
|
+
ERROR
|
|
360
|
+
} else {
|
|
361
|
+
IDLE
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
else -> IDLE
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun setStatus(status: PlayerStatus, error: PlaybackException?) {
|
|
370
|
+
val oldStatus = this.status
|
|
371
|
+
this.status = status
|
|
372
|
+
|
|
373
|
+
val playbackError = error?.let {
|
|
374
|
+
PlaybackError(it)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (playbackError == null && player.playbackState == Player.STATE_ENDED) {
|
|
378
|
+
sendEvent(PlayerEvent.PlayedToEnd())
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.status != oldStatus) {
|
|
382
|
+
sendEvent(PlayerEvent.StatusChanged(status, oldStatus, playbackError))
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fun addListener(videoPlayerListener: VideoPlayerListener) {
|
|
387
|
+
if (listeners.all { it.get() != videoPlayerListener }) {
|
|
388
|
+
listeners.add(WeakReference(videoPlayerListener))
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fun removeListener(videoPlayerListener: VideoPlayerListener) {
|
|
393
|
+
listeners.removeAll { it.get() == videoPlayerListener }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private fun sendEvent(event: PlayerEvent) {
|
|
397
|
+
// Emits to the native listeners
|
|
398
|
+
val listenersSnapshot = listeners.toList().mapNotNull { it.get() }
|
|
399
|
+
|
|
400
|
+
event.emit(this, listenersSnapshot)
|
|
401
|
+
|
|
402
|
+
// Emits to the JS side
|
|
403
|
+
if (event.emitToJS) {
|
|
404
|
+
emit(event.name, event.jsEventPayload)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private fun createFirstFrameEventGenerator(): FirstFrameEventGenerator {
|
|
409
|
+
return FirstFrameEventGenerator(player, currentPlayerView) {
|
|
410
|
+
sendEvent(PlayerEvent.RenderedFirstFrame())
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// IntervalUpdateEmitter
|
|
415
|
+
override fun emitTimeUpdate() {
|
|
416
|
+
appContext?.mainQueue?.launch {
|
|
417
|
+
val updatePayload = TimeUpdate(player.currentPosition / 1000.0, currentOffsetFromLive, currentLiveTimestamp, bufferedPosition)
|
|
418
|
+
sendEvent(PlayerEvent.TimeUpdated(updatePayload))
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fun toMetadataRetriever(): MediaMetadataRetriever {
|
|
423
|
+
val source = uncommittedSource ?: commitedSource
|
|
424
|
+
val uri = source?.uri ?: throw IllegalStateException("Video source is not set")
|
|
425
|
+
val stringUri = uri.toString()
|
|
426
|
+
|
|
427
|
+
val mediaMetadataRetriever = MediaMetadataRetriever()
|
|
428
|
+
if (URLUtil.isFileUrl(stringUri)) {
|
|
429
|
+
mediaMetadataRetriever.setDataSource(stringUri.replace("file://", ""))
|
|
430
|
+
} else if (URLUtil.isContentUrl(stringUri)) {
|
|
431
|
+
context.contentResolver.openFileDescriptor(uri, "r")?.use { parcelFileDescriptor ->
|
|
432
|
+
FileInputStream(parcelFileDescriptor.fileDescriptor).use { inputStream ->
|
|
433
|
+
mediaMetadataRetriever.setDataSource(inputStream.fd)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
mediaMetadataRetriever.setDataSource(stringUri, source.headers ?: emptyMap())
|
|
438
|
+
}
|
|
439
|
+
return mediaMetadataRetriever
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Extension functions
|
|
444
|
+
|
|
445
|
+
@OptIn(UnstableApi::class)
|
|
446
|
+
private fun Tracks.toVideoTracks(): List<VideoTrack> {
|
|
447
|
+
val videoTracks = mutableListOf<VideoTrack?>()
|
|
448
|
+
for (group in this.groups) {
|
|
449
|
+
for (i in 0 until group.length) {
|
|
450
|
+
val format = group.getTrackFormat(i)
|
|
451
|
+
val isSupported = group.isTrackSupported(i)
|
|
452
|
+
|
|
453
|
+
if (!MimeTypes.isVideo(format.sampleMimeType)) {
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
videoTracks.add(VideoTrack.fromFormat(format, isSupported))
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return videoTracks.filterNotNull()
|
|
460
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package expo.modules.video.player
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.C
|
|
5
|
+
import androidx.media3.common.Format
|
|
6
|
+
import androidx.media3.common.MimeTypes
|
|
7
|
+
import androidx.media3.common.TrackGroup
|
|
8
|
+
import androidx.media3.common.TrackSelectionOverride
|
|
9
|
+
import androidx.media3.common.TrackSelectionParameters
|
|
10
|
+
import androidx.media3.common.Tracks
|
|
11
|
+
import androidx.media3.common.util.UnstableApi
|
|
12
|
+
import expo.modules.video.records.AudioTrack
|
|
13
|
+
import java.lang.ref.WeakReference
|
|
14
|
+
|
|
15
|
+
@OptIn(UnstableApi::class)
|
|
16
|
+
class VideoPlayerAudioTracks(owner: VideoPlayer) : VideoPlayerListener {
|
|
17
|
+
private val owner = WeakReference(owner)
|
|
18
|
+
private val videoPlayer: VideoPlayer?
|
|
19
|
+
get() {
|
|
20
|
+
return owner.get()
|
|
21
|
+
}
|
|
22
|
+
private val formatsToGroups = mutableMapOf<Format, Pair<TrackGroup, Int>>()
|
|
23
|
+
private var currentAudioTrackFormat: Format? = null
|
|
24
|
+
private var currentOverride: TrackSelectionOverride? = null
|
|
25
|
+
|
|
26
|
+
var currentAudioTrack: AudioTrack?
|
|
27
|
+
get() {
|
|
28
|
+
return AudioTrack.fromFormat(currentAudioTrackFormat)
|
|
29
|
+
}
|
|
30
|
+
set(value) {
|
|
31
|
+
applyAudioTrack(value)
|
|
32
|
+
}
|
|
33
|
+
val availableAudioTracks = arrayListOf<AudioTrack>()
|
|
34
|
+
|
|
35
|
+
init {
|
|
36
|
+
owner.addListener(this)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fun setAudioTracksEnabled(enabled: Boolean) {
|
|
40
|
+
val currentParams = videoPlayer?.player?.trackSelectionParameters ?: return
|
|
41
|
+
var params = currentParams.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, !enabled).build()
|
|
42
|
+
if (!enabled) {
|
|
43
|
+
params = params.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
|
|
44
|
+
}
|
|
45
|
+
videoPlayer?.player?.trackSelectionParameters = params
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// VideoPlayerListener
|
|
49
|
+
override fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {
|
|
50
|
+
currentAudioTrackFormat = findSelectedAudioFormat()
|
|
51
|
+
super.onTrackSelectionParametersChanged(player, trackSelectionParameters)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
|
|
55
|
+
formatsToGroups.clear()
|
|
56
|
+
availableAudioTracks.clear()
|
|
57
|
+
for (group in tracks.groups) {
|
|
58
|
+
for (i in 0..<group.length) {
|
|
59
|
+
val format: Format = group.getTrackFormat(i)
|
|
60
|
+
|
|
61
|
+
if (MimeTypes.isAudio(format.sampleMimeType)) {
|
|
62
|
+
formatsToGroups[format] = group.mediaTrackGroup to i
|
|
63
|
+
val track = AudioTrack.fromFormat(format) ?: continue
|
|
64
|
+
availableAudioTracks.add(track)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
currentAudioTrackFormat = findSelectedAudioFormat()
|
|
69
|
+
super.onTracksChanged(player, tracks)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Private methods
|
|
73
|
+
private fun applyAudioTrack(audioTrack: AudioTrack?) {
|
|
74
|
+
val player = videoPlayer?.player ?: return
|
|
75
|
+
var newParameters: TrackSelectionParameters = player.trackSelectionParameters
|
|
76
|
+
currentOverride?.let { override ->
|
|
77
|
+
newParameters = newParameters.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
|
|
78
|
+
}
|
|
79
|
+
if (audioTrack == null) {
|
|
80
|
+
player.trackSelectionParameters = newParameters
|
|
81
|
+
setAudioTracksEnabled(false)
|
|
82
|
+
currentOverride = null
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
val format = formatsToGroups.keys.firstOrNull {
|
|
86
|
+
it.id == audioTrack.id
|
|
87
|
+
}
|
|
88
|
+
format?.let {
|
|
89
|
+
formatsToGroups[it]?.let { subtitlePair ->
|
|
90
|
+
val trackSelectionOverride = TrackSelectionOverride(subtitlePair.first, subtitlePair.second)
|
|
91
|
+
newParameters = newParameters.buildUpon().addOverride(trackSelectionOverride).build()
|
|
92
|
+
player.trackSelectionParameters = newParameters
|
|
93
|
+
setAudioTracksEnabled(true)
|
|
94
|
+
currentOverride = trackSelectionOverride
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun findSelectedAudioFormat(): Format? {
|
|
100
|
+
val trackSelectionParameters = videoPlayer?.player?.trackSelectionParameters
|
|
101
|
+
val preferredAudioLanguages = trackSelectionParameters?.preferredAudioLanguages
|
|
102
|
+
val overriddenFormat: Format? = trackSelectionParameters?.overrides?.let {
|
|
103
|
+
for ((group, trackSelectionOverride) in it) {
|
|
104
|
+
if (group.type == C.TRACK_TYPE_AUDIO) {
|
|
105
|
+
// For audioTracks only one index will be replaced
|
|
106
|
+
return@let trackSelectionOverride.trackIndices.firstOrNull()?.let { index ->
|
|
107
|
+
group.getFormat(index)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return@let null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
val preferredFormat: Format? = preferredAudioLanguages?.let { preferredAudioLanguages ->
|
|
115
|
+
for (preferredLanguage in preferredAudioLanguages) {
|
|
116
|
+
return@let formatsToGroups.keys.firstOrNull {
|
|
117
|
+
it.language == preferredLanguage
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return@let null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return overriddenFormat ?: preferredFormat
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package expo.modules.video.player
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.TrackSelectionParameters
|
|
5
|
+
import androidx.media3.common.Tracks
|
|
6
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
|
+
import expo.modules.video.enums.AudioMixingMode
|
|
8
|
+
import expo.modules.video.enums.PlayerStatus
|
|
9
|
+
import expo.modules.video.records.AudioTrack
|
|
10
|
+
import expo.modules.video.records.PlaybackError
|
|
11
|
+
import expo.modules.video.records.SubtitleTrack
|
|
12
|
+
import expo.modules.video.records.VideoSource
|
|
13
|
+
import expo.modules.video.records.TimeUpdate
|
|
14
|
+
import expo.modules.video.records.VideoTrack
|
|
15
|
+
|
|
16
|
+
@OptIn(UnstableApi::class)
|
|
17
|
+
interface VideoPlayerListener {
|
|
18
|
+
fun onStatusChanged(player: VideoPlayer, status: PlayerStatus, oldStatus: PlayerStatus?, error: PlaybackError?) {}
|
|
19
|
+
fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {}
|
|
20
|
+
fun onVolumeChanged(player: VideoPlayer, volume: Float, oldVolume: Float?) {}
|
|
21
|
+
fun onMutedChanged(player: VideoPlayer, muted: Boolean, oldMuted: Boolean?) {}
|
|
22
|
+
fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {}
|
|
23
|
+
fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {}
|
|
24
|
+
fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {}
|
|
25
|
+
fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {}
|
|
26
|
+
fun onTimeUpdate(player: VideoPlayer, timeUpdate: TimeUpdate) {}
|
|
27
|
+
fun onPlayedToEnd(player: VideoPlayer) {}
|
|
28
|
+
fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {}
|
|
29
|
+
fun onVideoTrackChanged(player: VideoPlayer, videoTrack: VideoTrack?, oldVideoTrack: VideoTrack?) {}
|
|
30
|
+
fun onVideoSourceLoaded(player: VideoPlayer, videoSource: VideoSource?, duration: Double?, availableVideoTracks: List<VideoTrack>, availableSubtitleTracks: List<SubtitleTrack>, availableAudioTracks: List<AudioTrack>) {}
|
|
31
|
+
fun onRenderedFirstFrame(player: VideoPlayer) {}
|
|
32
|
+
}
|