[feat] Video Group

This commit is contained in:
acite
2025-09-26 03:09:14 +08:00
parent 756c2ea9f8
commit e38d77b2f6
5 changed files with 244 additions and 186 deletions

View File

@@ -10,5 +10,6 @@ data class VideoResponse(
val comment: List<Comment>,
val star: Boolean,
val like: Int,
val author: String
val author: String,
val group: String?
)

View File

@@ -3,7 +3,6 @@ package com.acitelight.aether.service
import android.content.Context
import com.acitelight.aether.model.BookMark
import com.acitelight.aether.model.Comic
import com.acitelight.aether.model.ComicResponse
import com.acitelight.aether.model.Video
import com.tonyodev.fetch2.Status
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -27,7 +26,7 @@ class MediaManager @Inject constructor(
{
val j = ApiClient.api!!.getVideoClasses(token)
return j.toList()
}catch(e: Exception)
}catch(_: Exception)
{
return listOf()
}
@@ -39,7 +38,7 @@ class MediaManager @Inject constructor(
{
val j = ApiClient.api!!.queryVideoClasses(klass, token)
return j.toList()
}catch(e: Exception)
}catch(_: Exception)
{
return listOf()
}
@@ -65,7 +64,7 @@ class MediaManager @Inject constructor(
try {
val j = ApiClient.api!!.queryVideo(klass, id, token)
return Video(klass = klass, id = id, token=token, isLocal = false, localBase = "", video = j)
}catch (e: Exception)
}catch (_: Exception)
{
return null
}
@@ -100,7 +99,7 @@ class MediaManager @Inject constructor(
Json.decodeFromString<Video>(jsonString).toLocal(
context.getExternalFilesDir(null)?.path ?: ""
)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
} else {
@@ -125,7 +124,7 @@ class MediaManager @Inject constructor(
}
localVideos + remoteVideos
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
@@ -135,7 +134,7 @@ class MediaManager @Inject constructor(
try{
val j = ApiClient.api!!.getComics(token)
return j
}catch (e: Exception)
}catch (_: Exception)
{
return listOf()
}
@@ -146,7 +145,7 @@ class MediaManager @Inject constructor(
try{
val j = ApiClient.api!!.queryComicInfo(id, token)
return Comic(id = id, comic = j, token = token)
}catch (e: Exception)
}catch (_: Exception)
{
return null
}
@@ -157,7 +156,7 @@ class MediaManager @Inject constructor(
try{
val j = ApiClient.api!!.queryComicInfoBulk(id, token)
return j.zip(id).map { Comic(id = it.second, comic = it.first, token = token) }
}catch (e: Exception)
}catch (_: Exception)
{
return null
}
@@ -166,9 +165,9 @@ class MediaManager @Inject constructor(
suspend fun postBookmark(id: String, bookMark: BookMark): Boolean
{
try{
val j = ApiClient.api!!.postBookmark(id, token, bookMark)
ApiClient.api!!.postBookmark(id, token, bookMark)
return true
}catch (e: Exception)
}catch (_: Exception)
{
return false
}

View File

@@ -2,11 +2,9 @@ package com.acitelight.aether.view
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.media.AudioManager
import android.view.View
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -27,7 +25,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import androidx.navigation.NavHostController
@@ -54,11 +51,9 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Brightness4
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
@@ -66,9 +61,6 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
@@ -76,12 +68,9 @@ import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@@ -97,7 +86,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
@@ -110,9 +98,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.text.Cue
import androidx.media3.common.util.UnstableApi
import androidx.wear.compose.materialcore.screenHeightDp
import coil3.ImageLoader
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
@@ -120,6 +108,7 @@ import com.acitelight.aether.Global
import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.model.KeyImage
import com.acitelight.aether.model.Video
import kotlinx.coroutines.launch
import kotlin.math.abs
fun formatTime(ms: Long): String {
@@ -147,7 +136,6 @@ fun BiliStyleSlider(
valueRange: ClosedFloatingPointRange<Float> = 0f..1f
) {
val colorScheme = MaterialTheme.colorScheme
val thumbRadius = 6.dp
val trackHeight = 3.dp
Slider(
@@ -189,7 +177,6 @@ fun BiliMiniSlider(
valueRange: ClosedFloatingPointRange<Float> = 0f..1f
) {
val colorScheme = MaterialTheme.colorScheme
val thumbRadius = 6.dp
val trackHeight = 3.dp
Slider(
@@ -337,7 +324,7 @@ fun VideoPlayer(
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float) {
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
val context = LocalContext.current
val activity = context as? Activity
@@ -379,16 +366,16 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
onDragStart = { offset ->
if (videoPlayerViewModel.locked) return@detectDragGestures
if (offset.x < size.width / 2) {
videoPlayerViewModel.draggingPurpose = -1;
videoPlayerViewModel.draggingPurpose = -1
} else {
videoPlayerViewModel.draggingPurpose = -2;
videoPlayerViewModel.draggingPurpose = -2
}
},
onDragEnd = {
if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
videoPlayerViewModel.draggingPurpose = -1
},
onDrag = { change, dragAmount ->
if (videoPlayerViewModel.locked) return@detectDragGestures
@@ -412,15 +399,15 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
.toFloat() / maxVolume.toFloat()
if (dragAmount.y < 0)
setVolume(cu + 1);
setVolume(cu + 1)
else if (dragAmount.y > 0)
setVolume(cu - 1);
setVolume(cu - 1)
} else if (videoPlayerViewModel.draggingPurpose == 1) {
videoPlayerViewModel.brit =
(videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn(
0f,
1f
);
)
activity?.window?.attributes = activity.window.attributes.apply {
screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f)
@@ -514,7 +501,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime(
(exoPlayer.duration).toLong()
(exoPlayer.duration)
)
}",
fontWeight = FontWeight.Bold,
@@ -719,7 +706,7 @@ fun VideoPlayerPortal(
) {
val colorScheme = MaterialTheme.colorScheme
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp;
val screenHeight = configuration.screenHeightDp.dp
val minHeight = 42.dp
var coverAlpha by remember { mutableFloatStateOf(0.0f) }
@@ -752,7 +739,7 @@ fun VideoPlayerPortal(
Offset.Zero
}
val dh = playerHeight - minHeight;
val dh = playerHeight - minHeight
coverAlpha = (if (dh > 10.dp)
0f
else
@@ -763,6 +750,11 @@ fun VideoPlayerPortal(
}
}
val klass by videoPlayerViewModel.currentKlass
val id by videoPlayerViewModel.currentId
val name by videoPlayerViewModel.currentName
val duration by videoPlayerViewModel.currentDuration
ToggleFullScreen(false)
Column(Modifier
.nestedScroll(nestedScrollConnection)
@@ -844,7 +836,7 @@ fun VideoPlayerPortal(
.align(Alignment.Start)
.padding(horizontal = 12.dp)
.padding(top = 12.dp),
text = videoPlayerViewModel.video?.video?.name ?: "",
text = name,
fontSize = 16.sp,
maxLines = 2,
fontWeight = FontWeight.Bold,
@@ -856,7 +848,7 @@ fun VideoPlayerPortal(
.alpha(0.5f)) {
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = videoPlayerViewModel.video?.klass ?: "",
text = klass,
fontSize = 14.sp,
maxLines = 1,
fontWeight = FontWeight.Bold,
@@ -864,7 +856,7 @@ fun VideoPlayerPortal(
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = formatTime(videoPlayerViewModel.video?.video?.duration ?: 0),
text = formatTime(duration),
fontSize = 14.sp,
maxLines = 1,
fontWeight = FontWeight.Bold,
@@ -873,10 +865,8 @@ fun VideoPlayerPortal(
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
SocialPanel(
Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
PlaylistPanel(
Modifier,
videoPlayerViewModel = videoPlayerViewModel
)
@@ -886,7 +876,7 @@ fun VideoPlayerPortal(
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
for (i in Global.sameClassVideos ?: listOf()) {
if (i.id == videoPlayerViewModel.video?.id) continue
if (i.id == id) continue
MiniVideoCard(
modifier = Modifier
@@ -894,7 +884,7 @@ fun VideoPlayerPortal(
i,
{
videoPlayerViewModel.isPlaying = false
videoPlayerViewModel._player?.pause()
videoPlayerViewModel.player?.pause()
val route = "video_player_route/${"${i.klass}/${i.id}".toHex()}"
navController.navigate(route)
}, videoPlayerViewModel.imageLoader!!
@@ -1023,8 +1013,43 @@ fun SocialPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel)
}
}
@Composable
fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel)
{
val name by videoPlayerViewModel.currentName
LazyRow(
modifier = modifier
.fillMaxWidth()
.height(80.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
contentPadding = PaddingValues(horizontal = 24.dp)
) {
items(videoPlayerViewModel.videos) { it ->
// SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!)
Card(
modifier = Modifier.fillMaxHeight().width(140.dp),
onClick = {
if(name == it.video.name)
return@Card
videoPlayerViewModel.viewModelScope.launch {
videoPlayerViewModel.startPlay(it)
}
}
) {
Box(Modifier.padding(8.dp).fillMaxSize())
{
Text(modifier = Modifier.align(Alignment.Center), text = it.video.name, maxLines = 4, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 13.sp)
}
}
}
}
}
@Composable
fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel) {
val gallery by videoPlayerViewModel.currentGallery
LazyRow(
modifier = Modifier
.fillMaxWidth()
@@ -1032,7 +1057,7 @@ fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel) {
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 24.dp)
) {
items(videoPlayerViewModel.video?.getGallery() ?: listOf()) { it ->
items(gallery) { it ->
SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!)
}
}
@@ -1060,7 +1085,7 @@ fun SingleImageItem(img: KeyImage, imageLoader: ImageLoader) {
fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
val context = LocalContext.current
val activity = context as? Activity
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!;
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
@@ -1070,6 +1095,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
)
}
val name by videoPlayerViewModel.currentName
fun setVolume(value: Int) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
@@ -1349,7 +1376,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
)
{
Text(
text = "${videoPlayerViewModel.video?.video?.name}",
text = name,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(horizontal = 46.dp).padding(top = 12.dp)
@@ -1437,7 +1464,6 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
@Composable
fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLoader: ImageLoader) {
var isImageLoaded by remember { mutableStateOf(false) }
Card(
modifier = modifier
.height(80.dp)
@@ -1460,7 +1486,6 @@ fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLo
.diskCacheKey("${video.klass}/${video.id}/cover")
.listener(
onStart = { },
onSuccess = { _, _ -> isImageLoaded = true },
onError = { _, _ -> }
)
.build(),

View File

@@ -92,6 +92,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.charset.Charset
import java.security.KeyPair
import kotlin.collections.sortedWith
fun videoTOView(v: List<Video>): Map<String?, List<Video>>
{
return v.map { if(it.video.group != null) it else Video(id=it.id, isLocal = it.isLocal, localBase = it.localBase,
klass = it.klass, token = it.token, video = it.video.copy(group = it.video.name)) }.groupBy { it.video.group }
}
fun String.toHex(): String {
return this.toByteArray().joinToString("") { "%02x".format(it) }
@@ -119,6 +126,13 @@ fun VideoScreen(
var menuVisibility by videoScreenViewModel.menuVisibility
var searchFilter by videoScreenViewModel.searchFilter
var doneInit by videoScreenViewModel.doneInit
val vb = videoTOView(videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
videoScreenViewModel.videoLibrary.classes.getOrNull(
tabIndex
), listOf()
).filter { it.video.name.contains(searchFilter) }).filter { it.key != null }
.map{ i -> Pair(i.key!!, i.value.sortedWith(compareBy(naturalOrder()) { it.video.name }) ) }
.toList()
if (doneInit)
CardPage(title = "Videos") {
@@ -225,19 +239,16 @@ fun VideoScreen(
modifier = Modifier.fillMaxSize()
) {
items(
items = videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
videoScreenViewModel.videoLibrary.classes.getOrNull(
tabIndex
), listOf()
).filter { it.video.name.contains(searchFilter) },
key = { "${it.klass}/${it.id}" }
items = vb,
key = { "${it.first}/${it.second}" }
) { video ->
androidx.compose.foundation.layout.Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
VideoCard(video, navController, videoScreenViewModel)
if(video.second.isNotEmpty())
VideoCard(video.second, navController, videoScreenViewModel)
}
}
}
@@ -312,11 +323,12 @@ fun CatalogueItemRow(
@Composable
fun VideoCard(
video: Video,
videos: List<Video>,
navController: NavHostController,
videoScreenViewModel: VideoScreenViewModel
) {
val tabIndex by videoScreenViewModel.tabIndex;
val video = videos.first()
Card(
modifier = Modifier
.fillMaxWidth()
@@ -327,7 +339,8 @@ fun VideoCard(
videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]]
?: mutableStateListOf(), video
)
val route = "video_player_route/${"${video.klass}/${video.id}".toHex()}"
val vg = videos.joinToString(",") { "${it.klass}/${it.id}" }.toHex()
val route = "video_player_route/$vg"
navController.navigate(route)
},
onLongClick = {
@@ -362,14 +375,6 @@ fun VideoCard(
imageLoader = videoScreenViewModel.imageLoader!!
)
Text(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(2.dp),
text = formatTime(video.video.duration),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Box(
Modifier
@@ -379,13 +384,35 @@ fun VideoCard(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.45f)
Color.Black.copy(alpha = 0.6f)
)
)
)
.align(Alignment.BottomCenter)
)
Text(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(horizontal = 2.dp),
text = "${videos.size} Videos",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
lineHeight = 13.sp,
color = Color.White
)
Text(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(horizontal = 2.dp),
text = formatTime(video.video.duration),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
lineHeight = 13.sp,
color = Color.White
)
if (video.isLocal)
Card(
Modifier
@@ -405,7 +432,7 @@ fun VideoCard(
}
}
Text(
text = video.video.name,
text = video.video.group ?: video.video.name,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
maxLines = 4,
@@ -423,14 +450,6 @@ fun VideoCard(
Text("Class: ", fontSize = 10.sp, maxLines = 1)
Text(video.klass, fontSize = 10.sp, maxLines = 1)
}
Row(
modifier = Modifier.padding(horizontal = 8.dp).padding(bottom = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Id: ", fontSize = 10.sp, maxLines = 1, lineHeight = 10.sp)
Text(video.id, fontSize = 10.sp, maxLines = 1, lineHeight = 10.sp)
}
}
}
}

View File

@@ -4,14 +4,13 @@ import android.content.Context
import android.net.Uri
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
@@ -19,7 +18,6 @@ import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.text.Cue
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
@@ -46,9 +44,9 @@ import okhttp3.Request
import java.io.File
import javax.inject.Inject
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.Tracks
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.acitelight.aether.model.KeyImage
@HiltViewModel
class VideoPlayerViewModel @Inject constructor(
@@ -67,26 +65,34 @@ class VideoPlayerViewModel @Inject constructor(
// 1 : Volume
// 2 : Brightness
var draggingPurpose by mutableIntStateOf(-1)
var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0)
var star by mutableStateOf(false)
var locked by mutableStateOf(false)
private var _init: Boolean = false;
private var _init: Boolean = false
var startPlaying by mutableStateOf(false)
var renderedFirst = false
var video: Video? = null
var videos: List<Video> = listOf()
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null;
var imageLoader: ImageLoader? = null
var brit by mutableFloatStateOf(0.5f)
val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context)
var cues by mutableStateOf(listOf<Cue>())
var currentKlass = mutableStateOf("")
var currentId = mutableStateOf("")
var currentName = mutableStateOf("")
var currentDuration = mutableLongStateOf(0)
var currentGallery = mutableStateOf(listOf<KeyImage>())
@OptIn(UnstableApi::class)
fun init(videoId: String) {
if (_init) return;
val v = videoId.hexToString()
if (_init)
return
_init = true
val vs = videoId.hexToString().split(",").map { it.split("/") }
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
@@ -94,94 +100,10 @@ class VideoPlayerViewModel @Inject constructor(
.build()
viewModelScope.launch {
video = mediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!!
recentManager.pushVideo(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1]))
val subtitleCandidate = video?.getSubtitle()?.trim()
val subtitleUri = tryResolveSubtitleUri(subtitleCandidate)
// decide whether we need network-capable media source factory:
val subtitleIsRemote = subtitleUri?.scheme?.startsWith("http", ignoreCase = true) == true
val videoIsRemote = !video!!.isLocal
val needNetworkFactory = videoIsRemote || subtitleIsRemote
val trackSelector = DefaultTrackSelector(context)
// build ExoPlayer with or without custom DefaultMediaSourceFactory
val builder = if (needNetworkFactory)
ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
else
ExoPlayer.Builder(context)
_player = builder.setTrackSelector(trackSelector).build().apply {
val url = video?.getVideo() ?: ""
val videoUri = if (video!!.isLocal) Uri.fromFile(File(url)) else url.toUri()
val mediaItem: MediaItem = if (subtitleUri != null) {
// prepare subtitle configuration with guessed mime type
val subConfig = MediaItem.SubtitleConfiguration.Builder(subtitleUri)
.setMimeType("text/vtt")
.build()
MediaItem.Builder()
.setUri(videoUri)
.setSubtitleConfigurations(listOf(subConfig))
.build()
} else {
MediaItem.fromUri(videoUri)
}
setMediaItem(mediaItem)
prepare()
playWhenReady = true
addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
super.onTracksChanged(tracks)
val trackSelector = _player?.trackSelector
if (trackSelector is DefaultTrackSelector) {
val parameters = trackSelector.buildUponParameters()
.setSelectUndeterminedTextLanguage(true)
.build()
trackSelector.parameters = parameters
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) {
startPlaying = true
}
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
if(!renderedFirst)
{
viewModelScope.launch {
val ii = database.userDao().get(video!!.id, video!!.klass)
if(ii != null)
{
_player!!.seekTo(ii.position)
Toast.makeText(context, "Recover from ${formatTime(ii.position)} ", Toast.LENGTH_SHORT).show()
}
}
}
renderedFirst = true
}
override fun onPlayerError(error: PlaybackException)
{
print(error.message)
}
override fun onCues(lcues: MutableList<Cue>) {
cues = lcues
}
})
}
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
startPlay(videos.first())
startListen()
}
_init = true;
}
/**
@@ -199,8 +121,8 @@ class VideoPlayerViewModel @Inject constructor(
try {
val client = createOkHttp()
var headReq = Request.Builder().url(trimmed).head().build()
var headResp = try { client.newCall(headReq).execute() } catch (e: Exception) { null }
val headReq = Request.Builder().url(trimmed).head().build()
val headResp = try { client.newCall(headReq).execute() } catch (_: Exception) { null }
headResp?.use { resp ->
val code = resp.code
@@ -217,7 +139,7 @@ class VideoPlayerViewModel @Inject constructor(
.get()
.build()
var rangeResp = try { client.newCall(rangeReq).execute() } catch (e: Exception) { null }
val rangeResp = try { client.newCall(rangeReq).execute() } catch (_: Exception) { null }
rangeResp?.use { resp ->
val code = resp.code
@@ -233,7 +155,7 @@ class VideoPlayerViewModel @Inject constructor(
return@withContext null
}
}
} catch (e: Exception) {
} catch (_: Exception) {
return@withContext null
}
return@withContext null
@@ -247,22 +169,114 @@ class VideoPlayerViewModel @Inject constructor(
@OptIn(UnstableApi::class)
fun startListen() {
CoroutineScope(Dispatchers.Main).launch {
while (_player?.isReleased != true) {
val __player = _player!!;
playProcess = __player.currentPosition.toFloat() / __player.duration.toFloat()
while (_init) {
player?.let { playProcess = it.currentPosition.toFloat() / it.duration.toFloat() }
delay(100)
}
}
}
var _player: ExoPlayer? = null;
@OptIn(UnstableApi::class)
suspend fun startPlay(video: Video) {
currentId.value = video.id
currentKlass.value = video.klass
currentName.value = video.video.name
currentDuration.longValue = video.video.duration
currentGallery.value = video.getGallery()
player?.apply {
stop()
clearMediaItems()
}
recentManager.pushVideo(context, VideoQueryIndex(video.klass, video.id))
val subtitleCandidate = video.getSubtitle().trim()
val subtitleUri = tryResolveSubtitleUri(subtitleCandidate)
if (player == null) {
val trackSelector = DefaultTrackSelector(context)
val builder = ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
player = builder.setTrackSelector(trackSelector).build().apply {
addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
val trackSelector = player?.trackSelector
if (trackSelector is DefaultTrackSelector) {
val parameters = trackSelector.buildUponParameters()
.setSelectUndeterminedTextLanguage(true)
.build()
trackSelector.parameters = parameters
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) {
startPlaying = true
}
}
override fun onRenderedFirstFrame() {
if (!renderedFirst) {
viewModelScope.launch {
val ii = database.userDao().get(video.id, video.klass)
if (ii != null) {
player?.seekTo(ii.position)
Toast.makeText(
context,
"Recover from ${formatTime(ii.position)} ",
Toast.LENGTH_SHORT
).show()
}
}
}
renderedFirst = true
}
override fun onPlayerError(error: PlaybackException) {
print(error.message)
}
override fun onCues(lcues: MutableList<Cue>) {
cues = lcues
}
})
}
}
val url = video.getVideo()
val videoUri = if (video.isLocal) Uri.fromFile(File(url)) else url.toUri()
val mediaItem: MediaItem = if (subtitleUri != null) {
val subConfig = MediaItem.SubtitleConfiguration.Builder(subtitleUri)
.setMimeType("text/vtt")
.build()
MediaItem.Builder()
.setUri(videoUri)
.setSubtitleConfigurations(listOf(subConfig))
.build()
} else {
MediaItem.fromUri(videoUri)
}
player?.apply {
setMediaItem(mediaItem)
prepare()
playWhenReady = true
}
}
var player: ExoPlayer? = null
override fun onCleared() {
super.onCleared()
val p = _player!!.currentPosition
_player?.release()
_init = false
val p = player!!.currentPosition
player?.release()
CoroutineScope(Dispatchers.IO).launch {
database.userDao().insert(VideoRecord(video!!.id, video!!.klass, p))
if(currentId.value.isNotEmpty() && currentKlass.value.isNotEmpty())
database.userDao().insert(VideoRecord(currentId.value, currentKlass.value, p))
}
}
}