A cheap attempt at a native Bluesky client for Android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

*: vendor `dsa28s/compose-video` due to forced landscape logic, we don't force anything like that

I opened an issue on the repo asking for that param to be configurable.

geesawra 8f275441 7a09dc35

+1279 -7
+4 -2
app/build.gradle.kts
··· 55 55 implementation("io.coil-kt.coil3:coil-compose:3.0.1") 56 56 implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.1") 57 57 implementation("io.github.fornewid:placeholder-material3:2.0.0") 58 - implementation("androidx.media3:media3-exoplayer:1.8.0") 59 - implementation("androidx.media3:media3-ui:1.8.0") 58 + implementation("androidx.media3:media3-exoplayer:1.8.0") // [Required] androidx.media3 ExoPlayer dependency 59 + implementation("androidx.media3:media3-session:1.8.0") // [Required] MediaSession Extension dependency 60 + implementation("androidx.media3:media3-ui:1.8.0") // [Required] Base Player UI 60 61 implementation("androidx.media3:media3-exoplayer-hls:1.8.0") 62 + implementation("androidx.media3:media3-exoplayer-dash:1.8.0") 61 63 implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4") 62 64 implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4") 63 65 implementation("com.google.dagger:hilt-android:2.57.1")
+2
app/src/main/AndroidManifest.xml
··· 5 5 <application 6 6 android:name=".Application" 7 7 android:allowBackup="true" 8 + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" 8 9 android:dataExtractionRules="@xml/data_extraction_rules" 9 10 android:fullBackupContent="@xml/backup_rules" 10 11 android:icon="@mipmap/ic_launcher" 11 12 android:label="@string/app_name" 12 13 android:roundIcon="@mipmap/ic_launcher_round" 14 + android:screenOrientation="portrait" 13 15 android:supportsRtl="true" 14 16 android:theme="@style/Theme.JerryNo"> 15 17 <activity
+7 -1
app/src/main/java/industries/geesawra/jerryno/PostImageGallery.kt
··· 11 11 import androidx.compose.ui.Modifier 12 12 import androidx.compose.ui.draw.clip 13 13 import androidx.compose.ui.layout.ContentScale 14 + import androidx.compose.ui.platform.LocalContext 14 15 import androidx.compose.ui.unit.dp 15 16 import coil3.compose.AsyncImage 17 + import coil3.request.ImageRequest 18 + import coil3.request.crossfade 16 19 17 20 data class Image( 18 21 val url: String, ··· 43 46 // We take the first 4 images and give them each a weight 44 47 images.take(4).forEachIndexed { idx, image -> 45 48 AsyncImage( 46 - model = image.url, 49 + model = ImageRequest.Builder(LocalContext.current) 50 + .data(image.url) 51 + .crossfade(true) 52 + .build(), 47 53 contentDescription = image.alt, 48 54 contentScale = ContentScale.Crop, // Fills the space 49 55 modifier = Modifier
+40 -4
app/src/main/java/industries/geesawra/jerryno/SkeetRowView.kt
··· 23 23 import androidx.compose.ui.platform.LocalContext 24 24 import androidx.compose.ui.text.font.FontWeight 25 25 import androidx.compose.ui.unit.dp 26 + import androidx.media3.common.MimeTypes 26 27 import app.bsky.feed.FeedViewPost 27 28 import app.bsky.feed.FeedViewPostReasonUnion 28 29 import app.bsky.feed.Post ··· 32 33 import coil3.request.ImageRequest 33 34 import coil3.request.crossfade 34 35 import industries.geesawra.jerryno.datalayer.TimelineViewModel 36 + import io.sanghun.compose.video.RepeatMode 37 + import io.sanghun.compose.video.VideoPlayer 38 + import io.sanghun.compose.video.controller.VideoPlayerControllerConfig 39 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem 35 40 import kotlinx.serialization.json.decodeFromJsonElement 36 41 import sh.christian.ozone.BlueskyJson 37 42 ··· 125 130 126 131 Card( 127 132 modifier = Modifier 128 - .heightIn(max = 180.dp) 133 + .heightIn(max = 250.dp) 129 134 .fillMaxWidth() 130 135 .padding(8.dp) 131 136 ) { ··· 144 149 } 145 150 146 151 is PostViewEmbedUnion.VideoView -> { 147 - embed.value 148 - Text("Videos TBD") 149 - } // TODO: build this 152 + VideoPlayer( 153 + mediaItems = listOf( 154 + VideoPlayerMediaItem.NetworkMediaItem( 155 + url = embed.value.playlist.uri, 156 + mimeType = MimeTypes.APPLICATION_M3U8, 157 + ) 158 + ), 159 + handleLifecycle = false, 160 + autoPlay = false, 161 + usePlayerController = true, 162 + enablePip = false, 163 + handleAudioFocus = true, 164 + controllerConfig = VideoPlayerControllerConfig( 165 + showSpeedAndPitchOverlay = false, 166 + showSubtitleButton = false, 167 + showCurrentTimeAndTotalTime = true, 168 + showBufferingProgress = false, 169 + showForwardIncrementButton = true, 170 + showBackwardIncrementButton = true, 171 + showBackTrackButton = false, 172 + showNextTrackButton = false, 173 + showRepeatModeButton = true, 174 + controllerShowTimeMilliSeconds = 5_000, 175 + controllerAutoShow = true, 176 + showFullScreenButton = true, 177 + ), 178 + volume = 0.5f, // volume 0.0f to 1.0f 179 + repeatMode = RepeatMode.NONE, // or RepeatMode.ALL, RepeatMode.ONE 180 + modifier = Modifier 181 + .fillMaxSize() 182 + .padding(8.dp), 183 + ) 184 + } 185 + 150 186 else -> {} 151 187 } 152 188 }
+70
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/RepeatMode.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video 17 + 18 + import androidx.compose.runtime.Stable 19 + import androidx.media3.common.Player 20 + 21 + /** 22 + * VideoPlayer repeat mode. 23 + */ 24 + @Stable 25 + @Suppress("UNUSED_PARAMETER") 26 + enum class RepeatMode(rawValue: String) { 27 + /** 28 + * No repeat. 29 + */ 30 + NONE("none"), 31 + 32 + /** 33 + * Repeat current media only. 34 + */ 35 + ONE("one"), 36 + 37 + /** 38 + * Repeat all track. 39 + */ 40 + ALL("all"), 41 + } 42 + 43 + /** 44 + * Convert [RepeatMode] to exoplayer repeat mode. 45 + * 46 + * @return [Player.REPEAT_MODE_ALL] or [Player.REPEAT_MODE_OFF] or [Player.REPEAT_MODE_ONE] or 47 + */ 48 + internal fun RepeatMode.toExoPlayerRepeatMode(): Int = 49 + when (this) { 50 + RepeatMode.NONE -> Player.REPEAT_MODE_OFF 51 + RepeatMode.ALL -> Player.REPEAT_MODE_ALL 52 + RepeatMode.ONE -> Player.REPEAT_MODE_ONE 53 + } 54 + 55 + /** 56 + * Convert exoplayer repeat mode to [RepeatMode]. 57 + * 58 + * @return [RepeatMode.ALL] or [RepeatMode.NONE] or [RepeatMode.ONE] 59 + */ 60 + fun Int.toRepeatMode(): RepeatMode = 61 + if (this in 0 until 3) { 62 + when (this) { 63 + 0 -> RepeatMode.NONE 64 + 1 -> RepeatMode.ONE 65 + 2 -> RepeatMode.ALL 66 + else -> throw IllegalStateException("This is not ExoPlayer repeat mode.") 67 + } 68 + } else { 69 + throw IllegalStateException("This is not ExoPlayer repeat mode.") 70 + }
+90
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/ResizeMode.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video 17 + 18 + import androidx.annotation.OptIn 19 + import androidx.compose.runtime.Stable 20 + import androidx.media3.common.util.UnstableApi 21 + import androidx.media3.ui.AspectRatioFrameLayout 22 + 23 + /** 24 + * VideoPlayer resize mode. 25 + */ 26 + @Stable 27 + @Suppress("UNUSED_PARAMETER") 28 + enum class ResizeMode(rawValue: String) { 29 + /** 30 + * Either the width or height is decreased to obtain the desired aspect ratio. 31 + */ 32 + FIT("fit"), 33 + 34 + /** 35 + * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio. 36 + */ 37 + FIXED_WIDTH("fixed_width"), 38 + 39 + /** 40 + * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. 41 + */ 42 + FIXED_HEIGHT("fixed_height"), 43 + 44 + /** 45 + * The specified aspect ratio is ignored. 46 + */ 47 + FILL("fill"), 48 + 49 + /** 50 + * Either the width or height is increased to obtain the desired aspect ratio. 51 + */ 52 + ZOOM("zoom"), 53 + } 54 + 55 + /** 56 + * Convert [ResizeMode] to playerview resize mode. 57 + * 58 + * @return [AspectRatioFrameLayout.RESIZE_MODE_FIT] or [AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH] 59 + * or [AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT] or [AspectRatioFrameLayout.RESIZE_MODE_FILL] 60 + * or [AspectRatioFrameLayout.RESIZE_MODE_ZOOM] 61 + */ 62 + @OptIn(UnstableApi::class) 63 + internal fun ResizeMode.toPlayerViewResizeMode(): Int = 64 + when (this) { 65 + ResizeMode.FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT 66 + ResizeMode.FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH 67 + ResizeMode.FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT 68 + ResizeMode.FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL 69 + ResizeMode.ZOOM -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM 70 + } 71 + 72 + /** 73 + * Convert playerview resize mode to [ResizeMode]. 74 + * 75 + * @return [ResizeMode.FIT] or [ResizeMode.FIXED_WIDTH] or [ResizeMode.FIXED_HEIGHT] 76 + * or [ResizeMode.FILL] or [ResizeMode.ZOOM] 77 + */ 78 + fun Int.toResizeMode(): ResizeMode = 79 + if (this in 0 until 5) { 80 + when (this) { 81 + 0 -> ResizeMode.FIT 82 + 1 -> ResizeMode.FIXED_WIDTH 83 + 2 -> ResizeMode.FIXED_HEIGHT 84 + 3 -> ResizeMode.FILL 85 + 4 -> ResizeMode.ZOOM 86 + else -> throw IllegalStateException("This is not PlayerView resize mode.") 87 + } 88 + } else { 89 + throw IllegalStateException("This is not PlayerView resize mode.") 90 + }
+383
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/VideoPlayer.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video 17 + 18 + import android.annotation.SuppressLint 19 + import android.content.pm.ActivityInfo 20 + import android.graphics.Color 21 + import android.os.Handler 22 + import android.os.Looper 23 + import android.widget.ImageButton 24 + import androidx.activity.compose.BackHandler 25 + import androidx.annotation.FloatRange 26 + import androidx.compose.runtime.Composable 27 + import androidx.compose.runtime.DisposableEffect 28 + import androidx.compose.runtime.LaunchedEffect 29 + import androidx.compose.runtime.getValue 30 + import androidx.compose.runtime.mutableStateOf 31 + import androidx.compose.runtime.remember 32 + import androidx.compose.runtime.rememberUpdatedState 33 + import androidx.compose.runtime.setValue 34 + import androidx.compose.ui.Modifier 35 + import androidx.compose.ui.platform.LocalContext 36 + import androidx.compose.ui.viewinterop.AndroidView 37 + import androidx.compose.ui.window.SecureFlagPolicy 38 + import androidx.lifecycle.Lifecycle 39 + import androidx.lifecycle.LifecycleEventObserver 40 + import androidx.media3.common.AudioAttributes 41 + import androidx.media3.common.C 42 + import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE 43 + import androidx.media3.common.ForwardingPlayer 44 + import androidx.media3.common.MediaItem 45 + import androidx.media3.common.util.RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL 46 + import androidx.media3.common.util.RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE 47 + import androidx.media3.common.util.RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE 48 + import androidx.media3.datasource.DefaultDataSource 49 + import androidx.media3.datasource.DefaultHttpDataSource 50 + import androidx.media3.datasource.cache.CacheDataSource 51 + import androidx.media3.exoplayer.ExoPlayer 52 + import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 53 + import androidx.media3.session.MediaSession 54 + import androidx.media3.ui.PlayerView 55 + import io.sanghun.compose.video.cache.VideoPlayerCacheManager 56 + import io.sanghun.compose.video.controller.VideoPlayerControllerConfig 57 + import io.sanghun.compose.video.controller.applyToExoPlayerView 58 + import io.sanghun.compose.video.pip.enterPIPMode 59 + import io.sanghun.compose.video.pip.isActivityStatePipMode 60 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem 61 + import io.sanghun.compose.video.uri.toUri 62 + import io.sanghun.compose.video.util.findActivity 63 + import io.sanghun.compose.video.util.setFullScreen 64 + import kotlinx.coroutines.Dispatchers 65 + import kotlinx.coroutines.delay 66 + import kotlinx.coroutines.withContext 67 + import java.util.UUID 68 + 69 + /** 70 + * [VideoPlayer] is UI component that can play video in Jetpack Compose. It works based on ExoPlayer. 71 + * You can play local (e.g. asset files, resource files) files and all video files in the network environment. 72 + * For all video formats supported by the [VideoPlayer] component, see the ExoPlayer link below. 73 + * 74 + * If you rotate the screen, the default action is to reset the player state. 75 + * To prevent this happening, put the following options in the `android:configChanges` option of your app's AndroidManifest.xml to keep the settings. 76 + * ``` 77 + * keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode 78 + * ``` 79 + * 80 + * This component is linked with Compose [androidx.compose.runtime.DisposableEffect]. 81 + * This means that it move out of the Composable Scope, the ExoPlayer instance will automatically be destroyed as well. 82 + * 83 + * @see <a href="https://exoplayer.dev/supported-formats.html">Exoplayer Support Formats</a> 84 + * 85 + * @param modifier Modifier to apply to this layout node. 86 + * @param mediaItems [VideoPlayerMediaItem] to be played by the video player. The reason for receiving media items as an array is to configure multi-track. If it's a single track, provide a single list (e.g. listOf(mediaItem)). 87 + * @param handleLifecycle Sets whether to automatically play/stop the player according to the activity lifecycle. Default is true. 88 + * @param autoPlay Autoplay when media item prepared. Default is true. 89 + * @param usePlayerController Using player controller. Default is true. 90 + * @param controllerConfig Player controller config. You can customize the Video Player Controller UI. 91 + * @param seekBeforeMilliSeconds The seek back increment, in milliseconds. Default is 10sec (10000ms). Read-only props (Changes in values do not take effect.) 92 + * @param seekAfterMilliSeconds The seek forward increment, in milliseconds. Default is 10sec (10000ms). Read-only props (Changes in values do not take effect.) 93 + * @param repeatMode Sets the content repeat mode. 94 + * @param volume Sets thie player volume. It's possible from 0.0 to 1.0. 95 + * @param onCurrentTimeChanged A callback that returned once every second for player current time when the player is playing. 96 + * @param fullScreenSecurePolicy Windows security settings to apply when full screen. Default is off. (For example, avoid screenshots that are not DRM-applied.) 97 + * @param onFullScreenEnter A callback that occurs when the player is full screen. (The [VideoPlayerControllerConfig.showFullScreenButton] must be true to trigger a callback.) 98 + * @param onFullScreenExit A callback that occurs when the full screen is turned off. (The [VideoPlayerControllerConfig.showFullScreenButton] must be true to trigger a callback.) 99 + * @param enablePip Enable PIP (Picture-in-Picture). 100 + * @param enablePipWhenBackPressed With [enablePip] is `true`, set whether to enable PIP mode even when you press Back. Default is false. 101 + * @param handleAudioFocus Set whether to handle the video playback control automatically when it is playing in PIP mode and media is played in another app. Default is true. 102 + * @param playerBuilder Return exoplayer builder. This instance allows you to customise the ExoPlayer while in the Building process. Used to add various components like other RenderFactories. 103 + * @param playerInstance Return exoplayer instance. This instance allows you to add [androidx.media3.exoplayer.analytics.AnalyticsListener] to receive various events from the player. 104 + */ 105 + @SuppressLint("SourceLockedOrientationActivity", "UnsafeOptInUsageError") 106 + @Composable 107 + fun VideoPlayer( 108 + modifier: Modifier = Modifier, 109 + mediaItems: List<VideoPlayerMediaItem>, 110 + handleLifecycle: Boolean = true, 111 + autoPlay: Boolean = true, 112 + usePlayerController: Boolean = true, 113 + controllerConfig: VideoPlayerControllerConfig = VideoPlayerControllerConfig.Default, 114 + seekBeforeMilliSeconds: Long = 10000L, 115 + seekAfterMilliSeconds: Long = 10000L, 116 + repeatMode: RepeatMode = RepeatMode.NONE, 117 + resizeMode: ResizeMode = ResizeMode.FIT, 118 + @FloatRange(from = 0.0, to = 1.0) volume: Float = 1f, 119 + onCurrentTimeChanged: (Long) -> Unit = {}, 120 + fullScreenSecurePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, 121 + onFullScreenEnter: () -> Unit = {}, 122 + onFullScreenExit: () -> Unit = {}, 123 + enablePip: Boolean = false, 124 + defaultFullScreeen: Boolean = false, 125 + enablePipWhenBackPressed: Boolean = false, 126 + handleAudioFocus: Boolean = true, 127 + playerBuilder: ExoPlayer.Builder.() -> ExoPlayer.Builder = { this }, 128 + playerInstance: ExoPlayer.() -> Unit = {}, 129 + ) { 130 + val context = LocalContext.current 131 + var currentTime by remember { mutableStateOf(0L) } 132 + 133 + var mediaSession = remember<MediaSession?> { null } 134 + 135 + val player = remember { 136 + val httpDataSourceFactory = DefaultHttpDataSource.Factory() 137 + 138 + ExoPlayer.Builder(context) 139 + .setSeekBackIncrementMs(seekBeforeMilliSeconds) 140 + .setSeekForwardIncrementMs(seekAfterMilliSeconds) 141 + .setAudioAttributes( 142 + AudioAttributes.Builder() 143 + .setContentType(AUDIO_CONTENT_TYPE_MOVIE) 144 + .setUsage(C.USAGE_MEDIA) 145 + .build(), 146 + handleAudioFocus, 147 + ) 148 + .apply { 149 + val cache = VideoPlayerCacheManager.getCache() 150 + if (cache != null) { 151 + val cacheDataSourceFactory = CacheDataSource.Factory() 152 + .setCache(cache) 153 + .setUpstreamDataSourceFactory( 154 + DefaultDataSource.Factory( 155 + context, 156 + httpDataSourceFactory 157 + ) 158 + ) 159 + setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory)) 160 + } 161 + } 162 + .playerBuilder() 163 + .build() 164 + .also(playerInstance) 165 + } 166 + 167 + val defaultPlayerView = remember { 168 + PlayerView(context) 169 + } 170 + 171 + BackHandler(enablePip && enablePipWhenBackPressed) { 172 + enterPIPMode(context, defaultPlayerView) 173 + player.play() 174 + } 175 + 176 + LaunchedEffect(Unit) { 177 + while (true) { 178 + delay(1000) 179 + 180 + if (currentTime != player.currentPosition) { 181 + onCurrentTimeChanged(currentTime) 182 + } 183 + 184 + currentTime = player.currentPosition 185 + } 186 + } 187 + 188 + LaunchedEffect(usePlayerController) { 189 + defaultPlayerView.useController = usePlayerController 190 + } 191 + 192 + LaunchedEffect(player) { 193 + defaultPlayerView.player = player 194 + } 195 + 196 + LaunchedEffect(mediaItems, player) { 197 + mediaSession?.release() 198 + mediaSession = MediaSession.Builder(context, ForwardingPlayer(player)) 199 + .setId( 200 + "VideoPlayerMediaSession_${ 201 + UUID.randomUUID().toString().lowercase().split("-").first() 202 + }" 203 + ) 204 + .build() 205 + val exoPlayerMediaItems = withContext(Dispatchers.IO) { 206 + mediaItems.map { 207 + val uri = it.toUri(context) 208 + 209 + MediaItem.Builder().apply { 210 + setUri(uri) 211 + setMediaMetadata(it.mediaMetadata) 212 + setMimeType(it.mimeType) 213 + setDrmConfiguration( 214 + if (it is VideoPlayerMediaItem.NetworkMediaItem) { 215 + it.drmConfiguration 216 + } else { 217 + null 218 + }, 219 + ) 220 + }.build() 221 + } 222 + } 223 + 224 + player.setMediaItems(exoPlayerMediaItems) 225 + player.prepare() 226 + 227 + if (autoPlay) { 228 + player.play() 229 + } 230 + } 231 + 232 + var isFullScreenModeEntered by remember { mutableStateOf(defaultFullScreeen) } 233 + 234 + LaunchedEffect(controllerConfig) { 235 + controllerConfig.applyToExoPlayerView(defaultPlayerView) { 236 + isFullScreenModeEntered = it 237 + 238 + if (it) { 239 + onFullScreenEnter() 240 + } 241 + } 242 + } 243 + 244 + LaunchedEffect(controllerConfig, repeatMode) { 245 + defaultPlayerView.setRepeatToggleModes( 246 + if (controllerConfig.showRepeatModeButton) { 247 + REPEAT_TOGGLE_MODE_ALL or REPEAT_TOGGLE_MODE_ONE 248 + } else { 249 + REPEAT_TOGGLE_MODE_NONE 250 + }, 251 + ) 252 + player.repeatMode = repeatMode.toExoPlayerRepeatMode() 253 + } 254 + 255 + LaunchedEffect(volume) { 256 + player.volume = volume 257 + } 258 + 259 + VideoPlayerSurface( 260 + modifier = modifier, 261 + defaultPlayerView = defaultPlayerView, 262 + player = player, 263 + usePlayerController = usePlayerController, 264 + handleLifecycle = handleLifecycle, 265 + enablePip = enablePip, 266 + surfaceResizeMode = resizeMode 267 + ) 268 + 269 + if (isFullScreenModeEntered) { 270 + var fullScreenPlayerView by remember { mutableStateOf<PlayerView?>(null) } 271 + 272 + VideoPlayerFullScreenDialog( 273 + player = player, 274 + currentPlayerView = defaultPlayerView, 275 + controllerConfig = controllerConfig, 276 + repeatMode = repeatMode, 277 + resizeMode = resizeMode, 278 + onDismissRequest = { 279 + fullScreenPlayerView?.let { 280 + PlayerView.switchTargetView(player, it, defaultPlayerView) 281 + defaultPlayerView.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_fullscreen) 282 + .performClick() 283 + val currentActivity = context.findActivity() 284 + currentActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 285 + currentActivity.setFullScreen(false) 286 + onFullScreenExit() 287 + } 288 + 289 + isFullScreenModeEntered = false 290 + }, 291 + securePolicy = fullScreenSecurePolicy, 292 + enablePip = enablePip, 293 + fullScreenPlayerView = { 294 + fullScreenPlayerView = this 295 + }, 296 + ) 297 + } 298 + } 299 + 300 + @SuppressLint("UnsafeOptInUsageError") 301 + @Composable 302 + internal fun VideoPlayerSurface( 303 + modifier: Modifier = Modifier, 304 + defaultPlayerView: PlayerView, 305 + player: ExoPlayer, 306 + usePlayerController: Boolean, 307 + handleLifecycle: Boolean, 308 + enablePip: Boolean, 309 + surfaceResizeMode: ResizeMode, 310 + onPipEntered: () -> Unit = {}, 311 + autoDispose: Boolean = true, 312 + ) { 313 + val lifecycleOwner = 314 + rememberUpdatedState(androidx.lifecycle.compose.LocalLifecycleOwner.current) 315 + val context = LocalContext.current 316 + 317 + var isPendingPipMode by remember { mutableStateOf(false) } 318 + 319 + DisposableEffect( 320 + AndroidView( 321 + modifier = modifier, 322 + factory = { 323 + defaultPlayerView.apply { 324 + useController = usePlayerController 325 + resizeMode = surfaceResizeMode.toPlayerViewResizeMode() 326 + setBackgroundColor(Color.BLACK) 327 + } 328 + }, 329 + ), 330 + ) { 331 + val observer = LifecycleEventObserver { _, event -> 332 + when (event) { 333 + Lifecycle.Event.ON_PAUSE -> { 334 + if (handleLifecycle) { 335 + player.pause() 336 + } 337 + 338 + if (enablePip && player.playWhenReady) { 339 + isPendingPipMode = true 340 + 341 + Handler(Looper.getMainLooper()).post { 342 + enterPIPMode(context, defaultPlayerView) 343 + onPipEntered() 344 + 345 + Handler(Looper.getMainLooper()).postDelayed({ 346 + isPendingPipMode = false 347 + }, 500) 348 + } 349 + } 350 + } 351 + 352 + Lifecycle.Event.ON_RESUME -> { 353 + if (handleLifecycle) { 354 + player.play() 355 + } 356 + 357 + if (enablePip && player.playWhenReady) { 358 + defaultPlayerView.useController = usePlayerController 359 + } 360 + } 361 + 362 + Lifecycle.Event.ON_STOP -> { 363 + val isPipMode = context.isActivityStatePipMode() 364 + 365 + if (handleLifecycle || (enablePip && isPipMode && !isPendingPipMode)) { 366 + player.stop() 367 + } 368 + } 369 + 370 + else -> {} 371 + } 372 + } 373 + val lifecycle = lifecycleOwner.value.lifecycle 374 + lifecycle.addObserver(observer) 375 + 376 + onDispose { 377 + if (autoDispose) { 378 + player.release() 379 + lifecycle.removeObserver(observer) 380 + } 381 + } 382 + } 383 + }
+169
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/VideoPlayerFullScreenDialog.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video 17 + 18 + import android.annotation.SuppressLint 19 + import android.content.pm.ActivityInfo 20 + import android.view.Window 21 + import android.view.WindowManager 22 + import android.widget.ImageButton 23 + import androidx.compose.foundation.background 24 + import androidx.compose.foundation.layout.Box 25 + import androidx.compose.foundation.layout.fillMaxSize 26 + import androidx.compose.runtime.Composable 27 + import androidx.compose.runtime.LaunchedEffect 28 + import androidx.compose.runtime.SideEffect 29 + import androidx.compose.runtime.getValue 30 + import androidx.compose.runtime.mutableStateOf 31 + import androidx.compose.runtime.remember 32 + import androidx.compose.runtime.setValue 33 + import androidx.compose.ui.Alignment 34 + import androidx.compose.ui.Modifier 35 + import androidx.compose.ui.graphics.Color 36 + import androidx.compose.ui.platform.LocalContext 37 + import androidx.compose.ui.platform.LocalView 38 + import androidx.compose.ui.window.Dialog 39 + import androidx.compose.ui.window.DialogProperties 40 + import androidx.compose.ui.window.DialogWindowProvider 41 + import androidx.compose.ui.window.SecureFlagPolicy 42 + import androidx.media3.common.util.RepeatModeUtil 43 + import androidx.media3.exoplayer.ExoPlayer 44 + import androidx.media3.ui.PlayerView 45 + import io.sanghun.compose.video.controller.VideoPlayerControllerConfig 46 + import io.sanghun.compose.video.controller.applyToExoPlayerView 47 + import io.sanghun.compose.video.util.findActivity 48 + import io.sanghun.compose.video.util.setFullScreen 49 + 50 + /** 51 + * ExoPlayer does not support full screen views by default. 52 + * So create a full screen modal that wraps the Compose Dialog. 53 + * 54 + * Delegate all functions of the video controller that were used just before 55 + * the full screen to the video controller managed by that component. 56 + * Conversely, if the full screen dismissed, it will restore all the functions it delegated 57 + * for synchronization with the video controller on the full screen and the video controller on the previous screen. 58 + * 59 + * @param player Exoplayer instance. 60 + * @param currentPlayerView [androidx.media3.ui.PlayerView] instance currently in use for playback. 61 + * @param fullScreenPlayerView Callback to return all features to existing video player controller. 62 + * @param controllerConfig Player controller config. You can customize the Video Player Controller UI. 63 + * @param repeatMode Sets the content repeat mode. 64 + * @param enablePip Enable PIP. 65 + * @param onDismissRequest Callback that occurs when modals are closed. 66 + * @param securePolicy Policy on setting [android.view.WindowManager.LayoutParams.FLAG_SECURE] on a full screen dialog window. 67 + */ 68 + @SuppressLint("UnsafeOptInUsageError") 69 + @Composable 70 + internal fun VideoPlayerFullScreenDialog( 71 + player: ExoPlayer, 72 + currentPlayerView: PlayerView, 73 + fullScreenPlayerView: PlayerView.() -> Unit, 74 + controllerConfig: VideoPlayerControllerConfig, 75 + repeatMode: RepeatMode, 76 + resizeMode: ResizeMode, 77 + enablePip: Boolean, 78 + onDismissRequest: () -> Unit, 79 + securePolicy: SecureFlagPolicy, 80 + ) { 81 + val context = LocalContext.current 82 + val internalFullScreenPlayerView = remember { 83 + PlayerView(context) 84 + .also(fullScreenPlayerView) 85 + } 86 + var isFullScreenModeEntered by remember { 87 + mutableStateOf(false) 88 + } 89 + 90 + Dialog( 91 + onDismissRequest = onDismissRequest, 92 + properties = DialogProperties( 93 + dismissOnClickOutside = false, 94 + usePlatformDefaultWidth = false, 95 + securePolicy = securePolicy, 96 + decorFitsSystemWindows = false, 97 + ), 98 + ) { 99 + 100 + LaunchedEffect(Unit) { 101 + PlayerView.switchTargetView(player, currentPlayerView, internalFullScreenPlayerView) 102 + } 103 + 104 + val activityWindow = getActivityWindow() 105 + val dialogWindow = getDialogWindow() 106 + 107 + SideEffect { 108 + if (activityWindow != null && dialogWindow != null && !isFullScreenModeEntered) { 109 + activityWindow.setFullScreen(true) 110 + dialogWindow.setFullScreen(true) 111 + // dialogWindow has extra padding but activityWindow doesn't; 112 + // copy the attributes from activity to dialog 113 + WindowManager.LayoutParams().apply { 114 + copyFrom(activityWindow.attributes) 115 + type = dialogWindow.attributes.type 116 + dialogWindow.attributes = this 117 + } 118 + isFullScreenModeEntered = true 119 + } 120 + } 121 + 122 + LaunchedEffect(controllerConfig) { 123 + controllerConfig.applyToExoPlayerView(internalFullScreenPlayerView) { 124 + if (!it) { 125 + onDismissRequest() 126 + } 127 + } 128 + internalFullScreenPlayerView.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_fullscreen) 129 + .performClick() 130 + } 131 + 132 + LaunchedEffect(controllerConfig, repeatMode) { 133 + internalFullScreenPlayerView.setRepeatToggleModes( 134 + if (controllerConfig.showRepeatModeButton) { 135 + RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL or RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE 136 + } else { 137 + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE 138 + }, 139 + ) 140 + } 141 + 142 + Box( 143 + modifier = Modifier 144 + .fillMaxSize() 145 + .background(Color.Black), 146 + ) { 147 + VideoPlayerSurface( 148 + defaultPlayerView = internalFullScreenPlayerView, 149 + player = player, 150 + usePlayerController = true, 151 + handleLifecycle = !enablePip, 152 + autoDispose = false, 153 + enablePip = enablePip, 154 + surfaceResizeMode = resizeMode, 155 + onPipEntered = { onDismissRequest() }, 156 + modifier = Modifier 157 + .align(Alignment.Center) 158 + .fillMaxSize(), 159 + ) 160 + } 161 + } 162 + } 163 + 164 + 165 + @Composable 166 + internal fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window 167 + 168 + @Composable 169 + internal fun getActivityWindow(): Window? = LocalView.current.context.findActivity().window
+62
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/cache/VideoPlayerCacheManager.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.cache 17 + 18 + import android.annotation.SuppressLint 19 + import android.content.Context 20 + import androidx.media3.database.StandaloneDatabaseProvider 21 + import androidx.media3.datasource.cache.Cache 22 + import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor 23 + import androidx.media3.datasource.cache.SimpleCache 24 + import java.io.File 25 + 26 + /** 27 + * Manage video player cache. 28 + */ 29 + object VideoPlayerCacheManager { 30 + 31 + private lateinit var cacheInstance: Cache 32 + 33 + /** 34 + * Set the cache for video player. 35 + * It can only be set once in the app, and it is shared and used by multiple video players. 36 + * 37 + * @param context Current activity context. 38 + * @param maxCacheBytes Sets the maximum cache capacity in bytes. If the cache builds up as much as the set capacity, it is deleted from the oldest cache. 39 + */ 40 + @SuppressLint("UnsafeOptInUsageError") 41 + fun initialize(context: Context, maxCacheBytes: Long) { 42 + if (::cacheInstance.isInitialized) { 43 + return 44 + } 45 + 46 + cacheInstance = SimpleCache( 47 + File(context.cacheDir, "video"), 48 + LeastRecentlyUsedCacheEvictor(maxCacheBytes), 49 + StandaloneDatabaseProvider(context), 50 + ) 51 + } 52 + 53 + /** 54 + * Gets the ExoPlayer cache instance. If null, the cache to be disabled. 55 + */ 56 + internal fun getCache(): Cache? = 57 + if (::cacheInstance.isInitialized) { 58 + cacheInstance 59 + } else { 60 + null 61 + } 62 + }
+122
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/controller/VideoPlayerControllerConfig.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.controller 17 + 18 + import android.annotation.SuppressLint 19 + import android.view.View 20 + import androidx.compose.runtime.Immutable 21 + import androidx.core.view.isVisible 22 + import androidx.media3.ui.PlayerView 23 + 24 + /** 25 + * Sets the detailed properties of the [io.sanghun.compose.video.VideoPlayer] Controller. 26 + * 27 + * @param showSpeedAndPitchOverlay Visible speed, audio track select button. 28 + * @param showSubtitleButton Visible subtitle (CC) button. 29 + * @param showCurrentTimeAndTotalTime Visible currentTime, totalTime text. 30 + * @param showBufferingProgress Visible buffering progress. 31 + * @param showForwardIncrementButton Show forward increment button. 32 + * @param showBackwardIncrementButton Show backward increment button. 33 + * @param showBackTrackButton Show back track button. 34 + * @param showNextTrackButton Show next track button. 35 + * @param showRepeatModeButton Show repeat mode toggle button. 36 + * @param controllerShowTimeMilliSeconds Sets the playback controls timeout. 37 + * The playback controls are automatically hidden after this duration of time has elapsed without user input and with playback or buffering in progress. 38 + * (The timeout in milliseconds. A non-positive value will cause the controller to remain visible indefinitely.) 39 + * @param controllerAutoShow Sets whether the playback controls are automatically shown when playback starts, pauses, ends, or fails. 40 + * If set to false, the playback controls can be manually operated with {@link #showController()} and {@link #hideController()}. 41 + * (Whether the playback controls are allowed to show automatically.) 42 + * @param showFullScreenButton Show full screen button. 43 + */ 44 + @Immutable 45 + data class VideoPlayerControllerConfig( 46 + val showSpeedAndPitchOverlay: Boolean, 47 + val showSubtitleButton: Boolean, 48 + val showCurrentTimeAndTotalTime: Boolean, 49 + val showBufferingProgress: Boolean, 50 + val showForwardIncrementButton: Boolean, 51 + val showBackwardIncrementButton: Boolean, 52 + val showBackTrackButton: Boolean, 53 + val showNextTrackButton: Boolean, 54 + val showRepeatModeButton: Boolean, 55 + val showFullScreenButton: Boolean, 56 + 57 + val controllerShowTimeMilliSeconds: Int, 58 + val controllerAutoShow: Boolean, 59 + ) { 60 + 61 + companion object { 62 + /** 63 + * Default config for Controller. 64 + */ 65 + val Default = VideoPlayerControllerConfig( 66 + showSpeedAndPitchOverlay = false, 67 + showSubtitleButton = true, 68 + showCurrentTimeAndTotalTime = true, 69 + showBufferingProgress = false, 70 + showForwardIncrementButton = false, 71 + showBackwardIncrementButton = false, 72 + showBackTrackButton = true, 73 + showNextTrackButton = true, 74 + showRepeatModeButton = false, 75 + controllerShowTimeMilliSeconds = 5_000, 76 + controllerAutoShow = true, 77 + showFullScreenButton = true, 78 + ) 79 + } 80 + } 81 + 82 + /** 83 + * Apply the [VideoPlayerControllerConfig] to the ExoPlayer StyledViewPlayer. 84 + * 85 + * @param playerView [PlayerView] to which you want to apply settings. 86 + * @param onFullScreenStatusChanged Callback that occurs when the full screen status changes. 87 + */ 88 + @SuppressLint("UnsafeOptInUsageError") 89 + internal fun VideoPlayerControllerConfig.applyToExoPlayerView( 90 + playerView: PlayerView, 91 + onFullScreenStatusChanged: (Boolean) -> Unit, 92 + ) { 93 + val controllerView = playerView.rootView 94 + 95 + controllerView.findViewById<View>(androidx.media3.ui.R.id.exo_settings).isVisible = 96 + showSpeedAndPitchOverlay 97 + playerView.setShowSubtitleButton(showSubtitleButton) 98 + controllerView.findViewById<View>(androidx.media3.ui.R.id.exo_time).isVisible = 99 + showCurrentTimeAndTotalTime 100 + playerView.setShowBuffering( 101 + if (!showBufferingProgress) PlayerView.SHOW_BUFFERING_NEVER else PlayerView.SHOW_BUFFERING_ALWAYS, 102 + ) 103 + controllerView.findViewById<View>(androidx.media3.ui.R.id.exo_ffwd_with_amount).isVisible = 104 + showForwardIncrementButton 105 + controllerView.findViewById<View>(androidx.media3.ui.R.id.exo_rew_with_amount).isVisible = 106 + showBackwardIncrementButton 107 + playerView.setShowNextButton(showNextTrackButton) 108 + playerView.setShowPreviousButton(showBackTrackButton) 109 + playerView.setShowFastForwardButton(showForwardIncrementButton) 110 + playerView.setShowRewindButton(showBackwardIncrementButton) 111 + playerView.controllerShowTimeoutMs = controllerShowTimeMilliSeconds 112 + playerView.controllerAutoShow = controllerAutoShow 113 + 114 + @Suppress("DEPRECATION") 115 + if (showFullScreenButton) { 116 + playerView.setControllerOnFullScreenModeChangedListener { 117 + onFullScreenStatusChanged(it) 118 + } 119 + } else { 120 + playerView.setControllerOnFullScreenModeChangedListener(null) 121 + } 122 + }
+68
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/pip/PictureInPicture.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.pip 17 + 18 + import android.app.PictureInPictureParams 19 + import android.content.Context 20 + import android.content.pm.PackageManager 21 + import android.os.Build 22 + import android.util.Rational 23 + import androidx.media3.ui.PlayerView 24 + import io.sanghun.compose.video.util.findActivity 25 + 26 + /** 27 + * Enables PIP mode for the current activity. 28 + * 29 + * @param context Activity context. 30 + * @param defaultPlayerView Current video player controller. 31 + */ 32 + @Suppress("DEPRECATION") 33 + internal fun enterPIPMode(context: Context, defaultPlayerView: PlayerView) { 34 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && 35 + context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 36 + ) { 37 + defaultPlayerView.useController = false 38 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 39 + val params = PictureInPictureParams.Builder() 40 + 41 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 42 + params 43 + .setTitle("Video Player") 44 + .setAspectRatio(Rational(16, 9)) 45 + .setSeamlessResizeEnabled(true) 46 + } 47 + 48 + context.findActivity().enterPictureInPictureMode(params.build()) 49 + } else { 50 + context.findActivity().enterPictureInPictureMode() 51 + } 52 + } 53 + } 54 + 55 + /** 56 + * Check that the current activity is in PIP mode. 57 + * 58 + * @return `true` if the activity is in pip mode. (PIP mode is not supported in the version below Android N, so `false` is returned unconditionally.) 59 + */ 60 + internal fun Context.isActivityStatePipMode(): Boolean { 61 + val currentActivity = findActivity() 62 + 63 + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 64 + currentActivity.isInPictureInPictureMode 65 + } else { 66 + false 67 + } 68 + }
+93
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/uri/VideoPlayerMediaItem.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.uri 17 + 18 + import android.net.Uri 19 + import androidx.annotation.RawRes 20 + import androidx.media3.common.MediaItem.DrmConfiguration 21 + import androidx.media3.common.MediaMetadata 22 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem.AssetFileMediaItem 23 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem.NetworkMediaItem 24 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem.RawResourceMediaItem 25 + import io.sanghun.compose.video.uri.VideoPlayerMediaItem.StorageMediaItem 26 + 27 + interface BaseVideoPlayerMediaItem { 28 + val mediaMetadata: MediaMetadata 29 + val mimeType: String 30 + } 31 + 32 + /** 33 + * Representation of a media item for [io.sanghun.compose.video.VideoPlayer]. 34 + * 35 + * @see RawResourceMediaItem 36 + * @see AssetFileMediaItem 37 + * @see StorageMediaItem 38 + * @see NetworkMediaItem 39 + */ 40 + sealed interface VideoPlayerMediaItem : BaseVideoPlayerMediaItem { 41 + 42 + /** 43 + * A media item in the raw resource. 44 + * @param resourceId R.raw.xxxxx resource id 45 + * @param mediaMetadata Media Metadata. Default is empty. 46 + * @param mimeType Media mime type. 47 + */ 48 + data class RawResourceMediaItem( 49 + @RawRes val resourceId: Int, 50 + override val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, 51 + override val mimeType: String = "", 52 + ) : VideoPlayerMediaItem 53 + 54 + /** 55 + * A media item in the assets folder. 56 + * @param assetPath asset media file path (e.g If there is a test.mp4 file in the assets folder, test.mp4 becomes the assetPath.) 57 + * @throws androidx.media3.datasource.AssetDataSource.AssetDataSourceException asset file is not exist or load failed. 58 + * @param mediaMetadata Media Metadata. Default is empty. 59 + * @param mimeType Media mime type. 60 + */ 61 + data class AssetFileMediaItem( 62 + val assetPath: String, 63 + override val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, 64 + override val mimeType: String = "", 65 + ) : VideoPlayerMediaItem 66 + 67 + /** 68 + * A media item in the device internal / external storage. 69 + * @param storageUri storage file uri 70 + * @param mediaMetadata Media Metadata. Default is empty. 71 + * @param mimeType Media mime type. 72 + * @throws androidx.media3.datasource.FileDataSource.FileDataSourceException 73 + */ 74 + data class StorageMediaItem( 75 + val storageUri: Uri, 76 + override val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, 77 + override val mimeType: String = "", 78 + ) : VideoPlayerMediaItem 79 + 80 + /** 81 + * A media item in the internet 82 + * @param url network video url' 83 + * @param mediaMetadata Media Metadata. Default is empty. 84 + * @param mimeType Media mime type. 85 + * @param drmConfiguration Drm configuration for media. (Default is null) 86 + */ 87 + data class NetworkMediaItem( 88 + val url: String, 89 + override val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, 90 + override val mimeType: String = "", 91 + val drmConfiguration: DrmConfiguration? = null, 92 + ) : VideoPlayerMediaItem 93 + }
+68
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/uri/VideoPlayerMediaItemConverter.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.uri 17 + 18 + import android.annotation.SuppressLint 19 + import android.content.Context 20 + import android.net.Uri 21 + import androidx.media3.datasource.AssetDataSource 22 + import androidx.media3.datasource.DataSpec 23 + import androidx.media3.datasource.FileDataSource 24 + import androidx.media3.datasource.FileDataSource.FileDataSourceException 25 + import androidx.media3.datasource.RawResourceDataSource 26 + 27 + /** 28 + * Converts [VideoPlayerMediaItem] to [android.net.Uri]. 29 + * 30 + * @param context Pass application context or activity context. Use this context to load asset file using [android.content.res.AssetManager]. 31 + * @return [android.net.Uri] 32 + */ 33 + @SuppressLint("UnsafeOptInUsageError") 34 + internal fun VideoPlayerMediaItem.toUri( 35 + context: Context, 36 + ): Uri = when (this) { 37 + is VideoPlayerMediaItem.RawResourceMediaItem -> { 38 + RawResourceDataSource.buildRawResourceUri(resourceId) 39 + } 40 + 41 + is VideoPlayerMediaItem.AssetFileMediaItem -> { 42 + val dataSpec = DataSpec(Uri.parse("asset:///$assetPath")) 43 + val assetDataSource = AssetDataSource(context) 44 + try { 45 + assetDataSource.open(dataSpec) 46 + } catch (e: AssetDataSource.AssetDataSourceException) { 47 + e.printStackTrace() 48 + } 49 + 50 + assetDataSource.uri ?: Uri.EMPTY 51 + } 52 + 53 + is VideoPlayerMediaItem.NetworkMediaItem -> { 54 + Uri.parse(url) 55 + } 56 + 57 + is VideoPlayerMediaItem.StorageMediaItem -> { 58 + val dataSpec = DataSpec(storageUri) 59 + val fileDataSource = FileDataSource() 60 + try { 61 + fileDataSource.open(dataSpec) 62 + } catch (e: FileDataSourceException) { 63 + e.printStackTrace() 64 + } 65 + 66 + fileDataSource.uri ?: Uri.EMPTY 67 + } 68 + }
+33
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/util/ContextUtil.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.util 17 + 18 + import android.app.Activity 19 + import android.content.Context 20 + import android.content.ContextWrapper 21 + 22 + /** 23 + * The environment in which Compose is hosted may not be an activity unconditionally. 24 + * Gets the current activity that is open from various kinds of contexts such as Fragment, Dialog, etc. 25 + */ 26 + internal fun Context.findActivity(): Activity { 27 + var context = this 28 + while (context is ContextWrapper) { 29 + if (context is Activity) return context 30 + context = context.baseContext 31 + } 32 + throw IllegalStateException("Activity not found. Unknown error.") 33 + }
+60
app/src/main/java/industries/geesawra/jerryno/thirdpartyforks/compose-video/util/WindowUtil.kt
··· 1 + /* 2 + * Copyright 2023 Dora Lee 3 + * 4 + * Licensed under the Apache License, Version 2.0 (the "License"); 5 + * you may not use this file except in compliance with the License. 6 + * You may obtain a copy of the License at 7 + * 8 + * https://www.apache.org/licenses/LICENSE-2.0 9 + * 10 + * Unless required by applicable law or agreed to in writing, software 11 + * distributed under the License is distributed on an "AS IS" BASIS, 12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + * See the License for the specific language governing permissions and 14 + * limitations under the License. 15 + */ 16 + package io.sanghun.compose.video.util 17 + 18 + import android.app.Activity 19 + import android.view.View 20 + import android.view.Window 21 + import androidx.core.view.WindowCompat 22 + import androidx.core.view.WindowInsetsCompat 23 + import androidx.core.view.WindowInsetsControllerCompat 24 + 25 + /** 26 + * Bring the activity to the full screen. 27 + */ 28 + internal fun Activity.setFullScreen(fullscreen: Boolean) { 29 + window.setFullScreen(fullscreen) 30 + } 31 + 32 + /** 33 + * Bring the window to full screen. (Remove the status bar and navigation bar.) 34 + */ 35 + @Suppress("Deprecation") 36 + internal fun Window.setFullScreen(fullscreen: Boolean) { 37 + if (fullscreen) { 38 + this.hideSystemBars() 39 + } else { 40 + this.showSystemBars() 41 + } 42 + } 43 + 44 + private fun Window.hideSystemBars() { 45 + WindowCompat.setDecorFitsSystemWindows(this, false) 46 + // Without this deprecated systemUiVisibility, videos are not completely immersive mode in API 30. 47 + // ie. navigation bars are visible, if this is removed. 48 + this.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or 49 + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or 50 + View.SYSTEM_UI_FLAG_IMMERSIVE 51 + WindowCompat.getInsetsController(this, this.decorView).let { controller -> 52 + controller.hide(WindowInsetsCompat.Type.systemBars()) 53 + controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 54 + } 55 + } 56 + 57 + private fun Window.showSystemBars() { 58 + WindowCompat.setDecorFitsSystemWindows(this, true) 59 + WindowCompat.getInsetsController(this, this.decorView).show(WindowInsetsCompat.Type.systemBars()) 60 + }
+8
settings.gradle.kts
··· 19 19 } 20 20 } 21 21 22 + 23 + dependencyResolutionManagement { 24 + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 25 + repositories { 26 + mavenCentral() 27 + maven { url = uri("https://www.jitpack.io") } 28 + } 29 + } 22 30 rootProject.name = "Jerry No" 23 31 include(":app")