desktop: video and audio players (#3052)
* desktop: video and audio players * making player working without preinstalled VLC * mac support * don't use vlc lib when not needed * updated jna version * changes in script * video player lazy loading * mac script changes * updated build gradle for preserving atrributes of file while copying * apply the same file stats on libs to make VLC checker happy * updated script * changes --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
6de0ed4766
commit
2d7655281f
@ -139,8 +139,6 @@ dependencies {
|
|||||||
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
|
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
|
||||||
//implementation("androidx.compose.ui:ui-util:$compose_version")
|
//implementation("androidx.compose.ui:ui-util:$compose_version")
|
||||||
|
|
||||||
implementation("com.google.accompanist:accompanist-pager:0.25.1")
|
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
@ -73,7 +73,7 @@ class MainActivity: FragmentActivity() {
|
|||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
VideoPlayer.stopAll()
|
VideoPlayerHolder.stopAll()
|
||||||
AppLock.appWasHidden()
|
AppLock.appWasHidden()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +97,7 @@ kotlin {
|
|||||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
||||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||||
|
implementation("uk.co.caprica:vlcj:4.7.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val desktopTest by getting
|
val desktopTest by getting
|
||||||
|
@ -27,7 +27,7 @@ actual class RecorderNative: RecorderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
||||||
VideoPlayer.stopAll()
|
VideoPlayerHolder.stopAll()
|
||||||
AudioPlayer.stop()
|
AudioPlayer.stop()
|
||||||
val rec: MediaRecorder
|
val rec: MediaRecorder
|
||||||
recorder = initRecorder().also { rec = it }
|
recorder = initRecorder().also { rec = it }
|
||||||
@ -140,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoPlayer.stopAll()
|
VideoPlayerHolder.stopAll()
|
||||||
RecorderInterface.stopRecording?.invoke()
|
RecorderInterface.stopRecording?.invoke()
|
||||||
val current = currentlyPlaying.value
|
val current = currentlyPlaying.value
|
||||||
if (current == null || current.first != fileSource.filePath) {
|
if (current == null || current.first != fileSource.filePath) {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import chat.simplex.common.helpers.toUri
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
@ -17,49 +20,15 @@ import kotlinx.coroutines.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
actual class VideoPlayer private constructor(
|
actual class VideoPlayer actual constructor(
|
||||||
private val uri: URI,
|
override val uri: URI,
|
||||||
private val gallery: Boolean,
|
override val gallery: Boolean,
|
||||||
private val defaultPreview: ImageBitmap,
|
private val defaultPreview: ImageBitmap,
|
||||||
defaultDuration: Long,
|
defaultDuration: Long,
|
||||||
soundEnabled: Boolean
|
soundEnabled: Boolean
|
||||||
): VideoPlayerInterface {
|
): VideoPlayerInterface {
|
||||||
actual companion object {
|
|
||||||
private val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
|
|
||||||
private val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
|
|
||||||
|
|
||||||
actual fun getOrCreate(
|
|
||||||
uri: URI,
|
|
||||||
gallery: Boolean,
|
|
||||||
defaultPreview: ImageBitmap,
|
|
||||||
defaultDuration: Long,
|
|
||||||
soundEnabled: Boolean
|
|
||||||
): VideoPlayer =
|
|
||||||
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
|
|
||||||
|
|
||||||
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
|
|
||||||
player(fileName, gallery)?.enableSound(enable) == true
|
|
||||||
|
|
||||||
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
|
|
||||||
fileName ?: return null
|
|
||||||
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) =
|
|
||||||
player(uri.path, gallery)?.release(remove).run { }
|
|
||||||
|
|
||||||
actual fun stopAll() {
|
|
||||||
players.values.forEach { it.stop() }
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun releaseAll() {
|
|
||||||
players.values.forEach { it.release(false) }
|
|
||||||
players.clear()
|
|
||||||
previewsAndDurations.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val currentVolume: Float
|
private val currentVolume: Float
|
||||||
|
|
||||||
override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
|
override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
|
||||||
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
||||||
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
||||||
@ -114,7 +83,7 @@ actual class VideoPlayer private constructor(
|
|||||||
RecorderInterface.stopRecording?.invoke()
|
RecorderInterface.stopRecording?.invoke()
|
||||||
}
|
}
|
||||||
AudioPlayer.stop()
|
AudioPlayer.stop()
|
||||||
stopAll()
|
VideoPlayerHolder.stopAll()
|
||||||
if (listener.value == null) {
|
if (listener.value == null) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
|
val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
|
||||||
@ -224,14 +193,14 @@ actual class VideoPlayer private constructor(
|
|||||||
override fun release(remove: Boolean) {
|
override fun release(remove: Boolean) {
|
||||||
player.release()
|
player.release()
|
||||||
if (remove) {
|
if (remove) {
|
||||||
players.remove(uri to gallery)
|
VideoPlayerHolder.players.remove(uri to gallery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPreviewAndDuration() {
|
private fun setPreviewAndDuration() {
|
||||||
// It freezes main thread, doing it in IO thread
|
// It freezes main thread, doing it in IO thread
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
|
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
preview.value = previewAndDuration.preview ?: defaultPreview
|
preview.value = previewAndDuration.preview ?: defaultPreview
|
||||||
duration.value = (previewAndDuration.duration ?: 0)
|
duration.value = (previewAndDuration.duration ?: 0)
|
||||||
|
@ -51,7 +51,7 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
|
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
StyledPlayerView(ctx).apply {
|
StyledPlayerView(ctx).apply {
|
||||||
|
@ -7,13 +7,12 @@ import androidx.compose.ui.text.SpanStyle
|
|||||||
import androidx.compose.ui.text.font.*
|
import androidx.compose.ui.text.font.*
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.chat.ComposeState
|
import chat.simplex.common.views.chat.ComposeState
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.common.platform.AudioPlayer
|
|
||||||
import chat.simplex.common.platform.chatController
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
@ -2113,6 +2112,23 @@ data class CryptoFile(
|
|||||||
val isAbsolutePath: Boolean
|
val isAbsolutePath: Boolean
|
||||||
get() = File(filePath).isAbsolute
|
get() = File(filePath).isAbsolute
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var tmpFile: File? = null
|
||||||
|
|
||||||
|
fun createTmpFileIfNeeded(): File {
|
||||||
|
if (tmpFile == null) {
|
||||||
|
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||||
|
tmpFile.deleteOnExit()
|
||||||
|
ChatModel.filesToDelete.add(tmpFile)
|
||||||
|
this.tmpFile = tmpFile
|
||||||
|
}
|
||||||
|
return tmpFile!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteTmpFile() {
|
||||||
|
tmpFile?.delete()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import java.net.URI
|
|||||||
interface VideoPlayerInterface {
|
interface VideoPlayerInterface {
|
||||||
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
|
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
|
||||||
|
|
||||||
|
val uri: URI
|
||||||
|
val gallery: Boolean
|
||||||
val soundEnabled: MutableState<Boolean>
|
val soundEnabled: MutableState<Boolean>
|
||||||
val brokenVideo: MutableState<Boolean>
|
val brokenVideo: MutableState<Boolean>
|
||||||
val videoPlaying: MutableState<Boolean>
|
val videoPlaying: MutableState<Boolean>
|
||||||
@ -20,18 +22,45 @@ interface VideoPlayerInterface {
|
|||||||
fun release(remove: Boolean)
|
fun release(remove: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
expect class VideoPlayer: VideoPlayerInterface {
|
expect class VideoPlayer(
|
||||||
companion object {
|
uri: URI,
|
||||||
fun getOrCreate(
|
gallery: Boolean,
|
||||||
uri: URI,
|
defaultPreview: ImageBitmap,
|
||||||
gallery: Boolean,
|
defaultDuration: Long,
|
||||||
defaultPreview: ImageBitmap,
|
soundEnabled: Boolean
|
||||||
defaultDuration: Long,
|
): VideoPlayerInterface
|
||||||
soundEnabled: Boolean
|
|
||||||
): VideoPlayer
|
object VideoPlayerHolder {
|
||||||
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean
|
val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
|
||||||
fun release(uri: URI, gallery: Boolean, remove: Boolean)
|
val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
|
||||||
fun stopAll()
|
|
||||||
fun releaseAll()
|
fun getOrCreate(
|
||||||
|
uri: URI,
|
||||||
|
gallery: Boolean,
|
||||||
|
defaultPreview: ImageBitmap,
|
||||||
|
defaultDuration: Long,
|
||||||
|
soundEnabled: Boolean
|
||||||
|
): VideoPlayer =
|
||||||
|
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
|
||||||
|
|
||||||
|
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
|
||||||
|
player(fileName, gallery)?.enableSound(enable) == true
|
||||||
|
|
||||||
|
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
|
||||||
|
fileName ?: return null
|
||||||
|
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release(uri: URI, gallery: Boolean, remove: Boolean) =
|
||||||
|
player(uri.path, gallery)?.release(remove).run { }
|
||||||
|
|
||||||
|
fun stopAll() {
|
||||||
|
players.values.forEach { it.stop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun releaseAll() {
|
||||||
|
players.values.forEach { it.release(false) }
|
||||||
|
players.clear()
|
||||||
|
previewsAndDurations.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -737,7 +737,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
DisposableEffectOnGone(
|
DisposableEffectOnGone(
|
||||||
whenGone = {
|
whenGone = {
|
||||||
VideoPlayer.releaseAll()
|
VideoPlayerHolder.releaseAll()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||||
|
@ -50,7 +50,7 @@ fun CIVideoView(
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Box {
|
Box {
|
||||||
ImageView(preview, showMenu, onClick = {
|
VideoPreviewImageView(preview, onClick = {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
when (file.fileStatus) {
|
when (file.fileStatus) {
|
||||||
CIFileStatus.RcvInvitation ->
|
CIFileStatus.RcvInvitation ->
|
||||||
@ -75,7 +75,10 @@ fun CIVideoView(
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
onLongClick = {
|
||||||
|
showMenu.value = true
|
||||||
|
})
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||||
}
|
}
|
||||||
@ -90,7 +93,7 @@ fun CIVideoView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
||||||
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
||||||
val progress = remember(uri.path) { player.progress }
|
val progress = remember(uri.path) { player.progress }
|
||||||
val duration = remember(uri.path) { player.duration }
|
val duration = remember(uri.path) { player.duration }
|
||||||
@ -111,6 +114,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
|||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val onLongClick = { showMenu.value = true }
|
||||||
Box {
|
Box {
|
||||||
val windowWidth = LocalWindowWidth()
|
val windowWidth = LocalWindowWidth()
|
||||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||||
@ -118,12 +122,12 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
|||||||
player,
|
player,
|
||||||
width,
|
width,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = { showMenu.value = true },
|
onLongClick = onLongClick,
|
||||||
stop
|
stop
|
||||||
)
|
)
|
||||||
if (showPreview.value) {
|
if (showPreview.value) {
|
||||||
ImageView(preview, showMenu, onClick)
|
VideoPreviewImageView(preview, onClick, onLongClick)
|
||||||
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
|
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
|
||||||
}
|
}
|
||||||
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
||||||
}
|
}
|
||||||
@ -201,7 +205,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) {
|
||||||
val windowWidth = LocalWindowWidth()
|
val windowWidth = LocalWindowWidth()
|
||||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||||
Image(
|
Image(
|
||||||
@ -210,10 +214,10 @@ private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onC
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(width)
|
.width(width)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { showMenu.value = true },
|
onLongClick = onLongClick,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
.onRightClick { showMenu.value = true },
|
.onRightClick(onLongClick),
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -46,9 +46,11 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
||||||
DisposableEffectOnGone(
|
DisposableEffectOnGone(
|
||||||
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
|
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
|
||||||
)
|
)
|
||||||
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
|
|
||||||
|
@Composable
|
||||||
|
fun Content(index: Int) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -127,7 +129,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
|||||||
FullScreenImageView(modifier, data, imageBitmap)
|
FullScreenImageView(modifier, data, imageBitmap)
|
||||||
} else if (media is ProviderMedia.Video) {
|
} else if (media is ProviderMedia.Video) {
|
||||||
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
|
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
|
||||||
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
|
VideoView(modifier, media.uri, preview, index == settledCurrentPage, close)
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { playersToRelease.add(media.uri) }
|
onDispose { playersToRelease.add(media.uri) }
|
||||||
}
|
}
|
||||||
@ -135,14 +137,19 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index -> Content(index) }
|
||||||
|
} else {
|
||||||
|
Content(pagerState.currentPage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
|
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) {
|
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) {
|
||||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
|
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) }
|
||||||
val isCurrentPage = rememberUpdatedState(currentPage)
|
val isCurrentPage = rememberUpdatedState(currentPage)
|
||||||
val play = {
|
val play = {
|
||||||
player.play(true)
|
player.play(true)
|
||||||
@ -154,13 +161,16 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
|
|||||||
player.enableSound(true)
|
player.enableSound(true)
|
||||||
snapshotFlow { isCurrentPage.value }
|
snapshotFlow { isCurrentPage.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { if (it) play() else stop() }
|
.collect {
|
||||||
|
// Do not autoplay on desktop because it needs workaround
|
||||||
|
if (it && appPlatform.isAndroid) play() else if (!it) stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
FullScreenVideoView(player, modifier)
|
FullScreenVideoView(player, modifier, close)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier)
|
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit)
|
||||||
|
@ -66,6 +66,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
if (chatModel.chatId.value != null) {
|
if (chatModel.chatId.value != null) {
|
||||||
ModalManager.end.closeModalsExceptFirst()
|
ModalManager.end.closeModalsExceptFirst()
|
||||||
}
|
}
|
||||||
|
AudioPlayer.stop()
|
||||||
|
VideoPlayerHolder.stopAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||||
|
@ -29,6 +29,10 @@ fun initApp() {
|
|||||||
//testCrypto()
|
//testCrypto()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun discoverVlcLibs(path: String) {
|
||||||
|
uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyAppLocale() {
|
private fun applyAppLocale() {
|
||||||
val lang = ChatController.appPrefs.appLanguage.get()
|
val lang = ChatController.appPrefs.appLanguage.get()
|
||||||
if (lang == null || lang == Locale.getDefault().language) return
|
if (lang == null || lang == Locale.getDefault().language) return
|
||||||
|
@ -21,6 +21,8 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
|
|||||||
|
|
||||||
actual val databaseExportDir: File = tmpDir
|
actual val databaseExportDir: File = tmpDir
|
||||||
|
|
||||||
|
val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() }
|
||||||
|
|
||||||
actual fun desktopOpenDatabaseDir() {
|
actual fun desktopOpenDatabaseDir() {
|
||||||
if (Desktop.isDesktopSupported()) {
|
if (Desktop.isDesktopSupported()) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
import chat.simplex.common.views.helpers.AlertManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import chat.simplex.common.views.helpers.generalGetString
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
||||||
|
import uk.co.caprica.vlcj.player.base.State
|
||||||
|
import uk.co.caprica.vlcj.player.component.AudioPlayerComponent
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
actual class RecorderNative: RecorderInterface {
|
actual class RecorderNative: RecorderInterface {
|
||||||
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
||||||
@ -18,36 +26,187 @@ actual class RecorderNative: RecorderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual object AudioPlayer: AudioPlayerInterface {
|
actual object AudioPlayer: AudioPlayerInterface {
|
||||||
override fun play(fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
|
val player by lazy { AudioPlayerComponent().mediaPlayer() }
|
||||||
showInDevelopingAlert()
|
|
||||||
|
// Filepath: String, onProgressUpdate
|
||||||
|
private val currentlyPlaying: MutableState<Pair<CryptoFile, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null)
|
||||||
|
private var progressJob: Job? = null
|
||||||
|
|
||||||
|
enum class TrackState {
|
||||||
|
PLAYING, PAUSED, REPLACED
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns real duration of the track
|
||||||
|
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
|
||||||
|
val absoluteFilePath = getAppFilePath(fileSource.filePath)
|
||||||
|
if (!File(absoluteFilePath).exists()) {
|
||||||
|
Log.e(TAG, "No such file: ${fileSource.filePath}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoPlayerHolder.stopAll()
|
||||||
|
RecorderInterface.stopRecording?.invoke()
|
||||||
|
val current = currentlyPlaying.value
|
||||||
|
if (current == null || current.first != fileSource) {
|
||||||
|
stopListener()
|
||||||
|
player.stop()
|
||||||
|
runCatching {
|
||||||
|
if (fileSource.cryptoArgs != null) {
|
||||||
|
val tmpFile = fileSource.createTmpFileIfNeeded()
|
||||||
|
decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath)
|
||||||
|
player.media().prepare("file://${tmpFile.absolutePath}")
|
||||||
|
} else {
|
||||||
|
player.media().prepare("file://$absoluteFilePath")
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Log.e(TAG, it.stackTraceToString())
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seek != null) player.seekTo(seek)
|
||||||
|
player.start()
|
||||||
|
currentlyPlaying.value = fileSource to onProgressUpdate
|
||||||
|
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||||
|
while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) {
|
||||||
|
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
||||||
|
// so help to make the playback stopped in UI immediately
|
||||||
|
if (player.currentPosition == player.duration) {
|
||||||
|
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delay(50)
|
||||||
|
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||||
|
}
|
||||||
|
onProgressUpdate(null, TrackState.PAUSED)
|
||||||
|
currentlyPlaying.value?.first?.deleteTmpFile()
|
||||||
|
}
|
||||||
|
return player.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pause(): Int {
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = null
|
||||||
|
val position = player.currentPosition
|
||||||
|
player.pause()
|
||||||
|
return position
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
/*LALAL*/
|
if (currentlyPlaying.value == null) return
|
||||||
|
player.stop()
|
||||||
|
stopListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop(item: ChatItem) {
|
override fun stop(item: ChatItem) = stop(item.file?.fileName)
|
||||||
/*LALAL*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// FileName or filePath are ok
|
||||||
override fun stop(fileName: String?) {
|
override fun stop(fileName: String?) {
|
||||||
TODO("Not yet implemented")
|
if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopListener() {
|
||||||
|
val afterCoroutineCancel: CompletionHandler = {
|
||||||
|
// Notify prev audio listener about stop
|
||||||
|
currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED)
|
||||||
|
currentlyPlaying.value?.first?.deleteTmpFile()
|
||||||
|
currentlyPlaying.value = null
|
||||||
|
}
|
||||||
|
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
|
||||||
|
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.REPLACED] (in this order)
|
||||||
|
* */
|
||||||
|
if (progressJob != null) {
|
||||||
|
progressJob?.invokeOnCompletion(afterCoroutineCancel)
|
||||||
|
} else {
|
||||||
|
afterCoroutineCancel(null)
|
||||||
|
}
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun play(
|
||||||
|
fileSource: CryptoFile,
|
||||||
|
audioPlaying: MutableState<Boolean>,
|
||||||
|
progress: MutableState<Int>,
|
||||||
|
duration: MutableState<Int>,
|
||||||
|
resetOnEnd: Boolean,
|
||||||
|
) {
|
||||||
|
if (progress.value == duration.value) {
|
||||||
|
progress.value = 0
|
||||||
|
}
|
||||||
|
val realDuration = start(fileSource, progress.value) { pro, state ->
|
||||||
|
if (pro != null) {
|
||||||
|
progress.value = pro
|
||||||
|
}
|
||||||
|
if (pro == null || pro == duration.value) {
|
||||||
|
audioPlaying.value = false
|
||||||
|
if (pro == duration.value) {
|
||||||
|
progress.value = if (resetOnEnd) 0 else duration.value
|
||||||
|
} else if (state == TrackState.REPLACED) {
|
||||||
|
progress.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioPlaying.value = realDuration != null
|
||||||
|
// Update to real duration instead of what was received in ChatInfo
|
||||||
|
realDuration?.let { duration.value = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
|
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
|
||||||
TODO("Not yet implemented")
|
pro.value = pause()
|
||||||
|
audioPlaying.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
|
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
|
||||||
/*LALAL*/
|
pro.value = ms
|
||||||
|
if (currentlyPlaying.value?.first?.filePath == filePath) {
|
||||||
|
player.seekTo(ms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun duration(unencryptedFilePath: String): Int? {
|
override fun duration(unencryptedFilePath: String): Int? {
|
||||||
/*LALAL*/
|
var res: Int? = null
|
||||||
return null
|
try {
|
||||||
|
val helperPlayer = AudioPlayerComponent().mediaPlayer()
|
||||||
|
helperPlayer.media().startPaused("file://$unencryptedFilePath")
|
||||||
|
res = helperPlayer.duration
|
||||||
|
helperPlayer.stop()
|
||||||
|
helperPlayer.release()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, e.stackTraceToString())
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MediaPlayer.isPlaying: Boolean
|
||||||
|
get() = status().isPlaying
|
||||||
|
|
||||||
|
fun MediaPlayer.seekTo(time: Int) {
|
||||||
|
controls().setTime(time.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaPlayer.start() {
|
||||||
|
controls().start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaPlayer.pause() {
|
||||||
|
controls().pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaPlayer.stop() {
|
||||||
|
controls().stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val MediaPlayer.currentPosition: Int
|
||||||
|
get() = max(0, status().time().toInt())
|
||||||
|
|
||||||
|
val MediaPlayer.duration: Int
|
||||||
|
get() = media().info().duration().toInt()
|
||||||
|
|
||||||
actual object SoundPlayer: SoundPlayerInterface {
|
actual object SoundPlayer: SoundPlayerInterface {
|
||||||
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
|
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
|
||||||
override fun stop() { /*LALAL*/ }
|
override fun stop() { /*LALAL*/ }
|
||||||
|
@ -3,51 +3,213 @@ package chat.simplex.common.platform
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
||||||
|
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||||
|
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
|
||||||
|
import java.awt.Component
|
||||||
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
actual class VideoPlayer: VideoPlayerInterface {
|
actual class VideoPlayer actual constructor(
|
||||||
actual companion object {
|
override val uri: URI,
|
||||||
actual fun getOrCreate(
|
override val gallery: Boolean,
|
||||||
uri: URI,
|
private val defaultPreview: ImageBitmap,
|
||||||
gallery: Boolean,
|
defaultDuration: Long,
|
||||||
defaultPreview: ImageBitmap,
|
soundEnabled: Boolean
|
||||||
defaultDuration: Long,
|
): VideoPlayerInterface {
|
||||||
soundEnabled: Boolean
|
|
||||||
): VideoPlayer = VideoPlayer().also {
|
|
||||||
it.preview.value = defaultPreview
|
|
||||||
it.duration.value = defaultDuration
|
|
||||||
it.soundEnabled.value = soundEnabled
|
|
||||||
}
|
|
||||||
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean { /*TODO*/ return false }
|
|
||||||
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) { /*TODO*/ }
|
|
||||||
actual fun stopAll() { /*LALAL*/ }
|
|
||||||
actual fun releaseAll() { /*LALAL*/ }
|
|
||||||
}
|
|
||||||
|
|
||||||
override val soundEnabled: MutableState<Boolean> = mutableStateOf(false)
|
override val soundEnabled: MutableState<Boolean> = mutableStateOf(false)
|
||||||
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
||||||
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
||||||
override val progress: MutableState<Long> = mutableStateOf(0L)
|
override val progress: MutableState<Long> = mutableStateOf(0L)
|
||||||
override val duration: MutableState<Long> = mutableStateOf(0L)
|
override val duration: MutableState<Long> = mutableStateOf(0L)
|
||||||
override val preview: MutableState<ImageBitmap> = mutableStateOf(ImageBitmap(0, 0))
|
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
|
||||||
|
|
||||||
|
val mediaPlayerComponent = initializeMediaPlayerComponent()
|
||||||
|
val player by lazy { mediaPlayerComponent.mediaPlayer() }
|
||||||
|
|
||||||
|
init {
|
||||||
|
withBGApi {
|
||||||
|
setPreviewAndDuration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentVolume: Int by lazy { player.audio().volume() }
|
||||||
|
private var isReleased: Boolean = false
|
||||||
|
|
||||||
|
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
||||||
|
private var progressJob: Job? = null
|
||||||
|
|
||||||
|
enum class TrackState {
|
||||||
|
PLAYING, PAUSED, STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||||
|
val filepath = getAppFilePath(uri)
|
||||||
|
if (filepath == null || !File(filepath).exists()) {
|
||||||
|
Log.e(TAG, "No such file: $uri")
|
||||||
|
brokenVideo.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundEnabled.value) {
|
||||||
|
RecorderInterface.stopRecording?.invoke()
|
||||||
|
}
|
||||||
|
AudioPlayer.stop()
|
||||||
|
VideoPlayerHolder.stopAll()
|
||||||
|
val playerFilePath = uri.toString().replaceFirst("file:", "file://")
|
||||||
|
if (listener.value == null) {
|
||||||
|
runCatching {
|
||||||
|
player.media().prepare(playerFilePath)
|
||||||
|
if (seek != null) {
|
||||||
|
player.seekTo(seek.toInt())
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Log.e(TAG, it.stackTraceToString())
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message)
|
||||||
|
brokenVideo.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.start()
|
||||||
|
if (seek != null) player.seekTo(seek.toInt())
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
// Can happen when video file is broken
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error))
|
||||||
|
brokenVideo.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
listener.value = onProgressUpdate
|
||||||
|
// Player can only be accessed in one specific thread
|
||||||
|
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
|
while (isActive && !isReleased && player.isPlaying) {
|
||||||
|
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
||||||
|
// so help to make the playback stopped in UI immediately
|
||||||
|
if (player.currentPosition == player.duration) {
|
||||||
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delay(50)
|
||||||
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
|
}
|
||||||
|
if (isActive && !isReleased) {
|
||||||
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
|
||||||
|
}
|
||||||
|
onProgressUpdate(null, TrackState.PAUSED)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
/*TODO*/
|
if (isReleased || !videoPlaying.value) return
|
||||||
|
player.controls().stop()
|
||||||
|
stopListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopListener() {
|
||||||
|
val afterCoroutineCancel: CompletionHandler = {
|
||||||
|
// Notify prev video listener about stop
|
||||||
|
listener.value?.invoke(null, TrackState.STOPPED)
|
||||||
|
}
|
||||||
|
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
|
||||||
|
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.STOPPED] (in this order)
|
||||||
|
* */
|
||||||
|
if (progressJob != null) {
|
||||||
|
progressJob?.invokeOnCompletion(afterCoroutineCancel)
|
||||||
|
} else {
|
||||||
|
afterCoroutineCancel(null)
|
||||||
|
}
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun play(resetOnEnd: Boolean) {
|
override fun play(resetOnEnd: Boolean) {
|
||||||
if (appPlatform.isDesktop) {
|
if (progress.value == duration.value) {
|
||||||
showInDevelopingAlert()
|
progress.value = 0
|
||||||
|
}
|
||||||
|
videoPlaying.value = start(progress.value) { pro, _ ->
|
||||||
|
if (pro != null) {
|
||||||
|
progress.value = pro
|
||||||
|
}
|
||||||
|
if ((pro == null || pro == duration.value) && duration.value != 0L) {
|
||||||
|
videoPlaying.value = false
|
||||||
|
if (pro == duration.value) {
|
||||||
|
progress.value = if (resetOnEnd) 0 else duration.value
|
||||||
|
}/* else if (state == TrackState.STOPPED) {
|
||||||
|
progress.value = 0 //
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableSound(enable: Boolean): Boolean {
|
override fun enableSound(enable: Boolean): Boolean {
|
||||||
/*TODO*/
|
if (isReleased) return false
|
||||||
return false
|
if (soundEnabled.value == enable) return false
|
||||||
|
soundEnabled.value = enable
|
||||||
|
player.audio().setVolume(if (enable) currentVolume else 0)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release(remove: Boolean) {
|
override fun release(remove: Boolean) { withApi {
|
||||||
/*TODO*/
|
if (isReleased) return@withApi
|
||||||
|
isReleased = true
|
||||||
|
// TODO
|
||||||
|
/** [player.release] freezes thread for some reason. It happens periodically. So doing this we don't see the freeze, but it's still there */
|
||||||
|
if (player.isPlaying) player.stop()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
||||||
|
if (remove) {
|
||||||
|
VideoPlayerHolder.players.remove(uri to gallery)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
private val MediaPlayer.currentPosition: Int
|
||||||
|
get() = if (isReleased) 0 else max(0, player.status().time().toInt())
|
||||||
|
|
||||||
|
private suspend fun setPreviewAndDuration() {
|
||||||
|
// It freezes main thread, doing it in IO thread
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo() }
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
preview.value = previewAndDuration.preview ?: defaultPreview
|
||||||
|
duration.value = (previewAndDuration.duration ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getBitmapFromVideo(): VideoPlayerInterface.PreviewAndDuration {
|
||||||
|
val player = CallbackMediaPlayerComponent().mediaPlayer()
|
||||||
|
val filepath = getAppFilePath(uri)
|
||||||
|
if (filepath == null || !File(filepath).exists()) {
|
||||||
|
return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||||
|
}
|
||||||
|
player.media().startPaused(filepath)
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) {
|
||||||
|
delay(10)
|
||||||
|
}
|
||||||
|
val preview = player.snapshots()?.get()?.toComposeImageBitmap()
|
||||||
|
val duration = player.duration.toLong()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
||||||
|
return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeMediaPlayerComponent(): Component {
|
||||||
|
return if (desktopPlatform.isMac()) {
|
||||||
|
CallbackMediaPlayerComponent()
|
||||||
|
} else {
|
||||||
|
EmbeddedMediaPlayerComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Component.mediaPlayer() = when (this) {
|
||||||
|
is CallbackMediaPlayerComponent -> mediaPlayer()
|
||||||
|
is EmbeddedMediaPlayerComponent -> mediaPlayer()
|
||||||
|
else -> error("mediaPlayer() can only be called on vlcj player components")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import chat.simplex.common.platform.VideoPlayer
|
import chat.simplex.common.platform.VideoPlayer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
|
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {}
|
||||||
/* LALAL */
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun LocalWindowWidth(): Dp {
|
actual fun LocalWindowWidth(): Dp {
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
package chat.simplex.common.views.chat.item
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.awt.SwingPanel
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import chat.simplex.common.platform.VideoPlayer
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.simplexWindowState
|
||||||
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
|
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
|
||||||
@ -20,6 +29,43 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
@Composable
|
@Composable
|
||||||
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
|
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
||||||
|
// Workaround. Without changing size of the window the screen flashes a lot even if it's not being recomposed
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width + 1.dp)
|
||||||
|
delay(50)
|
||||||
|
player.play(true)
|
||||||
|
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width - 1.dp)
|
||||||
|
}
|
||||||
|
Box {
|
||||||
|
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
||||||
|
val factory = remember { { player.mediaPlayerComponent } }
|
||||||
|
SwingPanel(
|
||||||
|
background = Color.Transparent,
|
||||||
|
modifier = Modifier,
|
||||||
|
factory = factory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Controls(player, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
|
||||||
|
val playing = remember(player) { player.videoPlaying }
|
||||||
|
val progress = remember(player) { player.progress }
|
||||||
|
val duration = remember(player) { player.duration }
|
||||||
|
Row(Modifier.fillMaxWidth().align(Alignment.BottomCenter).height(50.dp)) {
|
||||||
|
IconButton(onClick = { if (playing.value) player.player.pause() else player.play(true) },) {
|
||||||
|
Icon(painterResource(if (playing.value) MR.images.ic_pause_filled else MR.images.ic_play_arrow_filled), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
|
||||||
|
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = close,) {
|
||||||
|
Icon(painterResource(MR.images.ic_close), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,6 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
|
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
|
||||||
// LALAL
|
|
||||||
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L)
|
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
|
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
@ -22,6 +21,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation("net.java.dev.jna:jna:5.13.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jvmTest by getting
|
val jvmTest by getting
|
||||||
@ -33,16 +33,23 @@ compose {
|
|||||||
desktop {
|
desktop {
|
||||||
application {
|
application {
|
||||||
// For debugging via VisualVM
|
// For debugging via VisualVM
|
||||||
/*jvmArgs += listOf(
|
val debugJava = false
|
||||||
"-Dcom.sun.management.jmxremote.port=8080",
|
if (debugJava) {
|
||||||
"-Dcom.sun.management.jmxremote.ssl=false",
|
jvmArgs += listOf(
|
||||||
"-Dcom.sun.management.jmxremote.authenticate=false"
|
"-Dcom.sun.management.jmxremote.port=8080",
|
||||||
)*/
|
"-Dcom.sun.management.jmxremote.ssl=false",
|
||||||
|
"-Dcom.sun.management.jmxremote.authenticate=false"
|
||||||
|
)
|
||||||
|
}
|
||||||
mainClass = "chat.simplex.desktop.MainKt"
|
mainClass = "chat.simplex.desktop.MainKt"
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
// For debugging via VisualVM
|
// For debugging via VisualVM
|
||||||
//modules("jdk.zipfs", "jdk.management.agent")
|
if (debugJava) {
|
||||||
modules("jdk.zipfs")
|
modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent")
|
||||||
|
} else {
|
||||||
|
// 'jdk.unsupported' is for vlcj
|
||||||
|
modules("jdk.zipfs", "jdk.unsupported")
|
||||||
|
}
|
||||||
//includeAllModules = true
|
//includeAllModules = true
|
||||||
outputBaseDir.set(project.file("../release"))
|
outputBaseDir.set(project.file("../release"))
|
||||||
targetFormats(
|
targetFormats(
|
||||||
@ -145,57 +152,119 @@ tasks.named("compileJava") {
|
|||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
tasks.create("cmakeBuildAndCopy") {
|
tasks.create("cmakeBuildAndCopy") {
|
||||||
dependsOn("cmakeBuild")
|
dependsOn("cmakeBuild")
|
||||||
|
val copyDetails = mutableMapOf<String, ArrayList<FileCopyDetails>>()
|
||||||
|
copy {
|
||||||
|
from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps")
|
||||||
|
into("src/jvmMain/resources/libs/linux-x86_64")
|
||||||
|
include("*.so*")
|
||||||
|
eachFile {
|
||||||
|
path = name
|
||||||
|
}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc"
|
||||||
|
from("$cppPath/desktop/libs/linux-x86_64/deps/vlc")
|
||||||
|
into(destinationDir)
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
copyIfNeeded(destinationDir, copyDetails)
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps")
|
||||||
|
into("src/jvmMain/resources/libs/linux-aarch64")
|
||||||
|
include("*.so*")
|
||||||
|
eachFile {
|
||||||
|
path = name
|
||||||
|
}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc"
|
||||||
|
from("$cppPath/desktop/libs/linux-aarch64/deps/vlc")
|
||||||
|
into(destinationDir)
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
copyIfNeeded(destinationDir, copyDetails)
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
from("${project(":desktop").buildDir}/cmake/main/win-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps")
|
||||||
|
into("src/jvmMain/resources/libs/windows-x86_64")
|
||||||
|
include("*.dll")
|
||||||
|
eachFile {
|
||||||
|
path = name
|
||||||
|
}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc"
|
||||||
|
from("$cppPath/desktop/libs/windows-x86_64/deps/vlc")
|
||||||
|
into(destinationDir)
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
copyIfNeeded(destinationDir, copyDetails)
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps")
|
||||||
|
into("src/jvmMain/resources/libs/mac-x86_64")
|
||||||
|
include("*.dylib")
|
||||||
|
eachFile {
|
||||||
|
path = name
|
||||||
|
}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc"
|
||||||
|
from("$cppPath/desktop/libs/mac-x86_64/deps/vlc")
|
||||||
|
into(destinationDir)
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
copyIfNeeded(destinationDir, copyDetails)
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps")
|
||||||
|
into("src/jvmMain/resources/libs/mac-aarch64")
|
||||||
|
include("*.dylib")
|
||||||
|
eachFile {
|
||||||
|
path = name
|
||||||
|
}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
copy {
|
||||||
|
val destinationDir = "src/jvmMain/resources/libs/mac-aarch64/vlc"
|
||||||
|
from("$cppPath/desktop/libs/mac-aarch64/deps/vlc")
|
||||||
|
into(destinationDir)
|
||||||
|
includeEmptyDirs = false
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
copyIfNeeded(destinationDir, copyDetails)
|
||||||
|
}
|
||||||
doLast {
|
doLast {
|
||||||
copy {
|
copyDetails.forEach { (destinationDir, details) ->
|
||||||
from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps")
|
details.forEach { detail ->
|
||||||
into("src/jvmMain/resources/libs/linux-x86_64")
|
val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path)
|
||||||
include("*.so*")
|
if (target.exists()) {
|
||||||
eachFile {
|
target.setLastModified(detail.lastModified)
|
||||||
path = name
|
}
|
||||||
}
|
}
|
||||||
includeEmptyDirs = false
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps")
|
|
||||||
into("src/jvmMain/resources/libs/linux-aarch64")
|
|
||||||
include("*.so*")
|
|
||||||
eachFile {
|
|
||||||
path = name
|
|
||||||
}
|
|
||||||
includeEmptyDirs = false
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from("${project(":desktop").buildDir}/cmake/main/win-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps")
|
|
||||||
into("src/jvmMain/resources/libs/windows-x86_64")
|
|
||||||
include("*.dll")
|
|
||||||
eachFile {
|
|
||||||
path = name
|
|
||||||
}
|
|
||||||
includeEmptyDirs = false
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps")
|
|
||||||
into("src/jvmMain/resources/libs/mac-x86_64")
|
|
||||||
include("*.dylib")
|
|
||||||
eachFile {
|
|
||||||
path = name
|
|
||||||
}
|
|
||||||
includeEmptyDirs = false
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps")
|
|
||||||
into("src/jvmMain/resources/libs/mac-aarch64")
|
|
||||||
include("*.dylib")
|
|
||||||
eachFile {
|
|
||||||
path = name
|
|
||||||
}
|
|
||||||
includeEmptyDirs = false
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun CopySpec.copyIfNeeded(destinationDir: String, into: MutableMap<String, ArrayList<FileCopyDetails>>) {
|
||||||
|
val details = arrayListOf<FileCopyDetails>()
|
||||||
|
eachFile {
|
||||||
|
val targetFile = File(destinationDir, path)
|
||||||
|
if (file.lastModified() == targetFile.lastModified() && file.length() == targetFile.length()) {
|
||||||
|
exclude()
|
||||||
|
} else {
|
||||||
|
details.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
into[destinationDir] = details
|
||||||
|
}
|
||||||
|
@ -5,6 +5,8 @@ import chat.simplex.common.showApp
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
import kotlin.io.path.setLastModifiedTime
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
initHaskell()
|
initHaskell()
|
||||||
@ -20,6 +22,15 @@ private fun initHaskell() {
|
|||||||
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
|
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
|
||||||
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
|
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
|
||||||
System.load(File(libsTmpDir, libApp).absolutePath)
|
System.load(File(libsTmpDir, libApp).absolutePath)
|
||||||
|
|
||||||
|
vlcDir.deleteRecursively()
|
||||||
|
Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
// No picture without preloading it, only sound. However, with libs from AppImage it works without preloading
|
||||||
|
//val libXcb = "libvlc_xcb_events.so.0.0.0"
|
||||||
|
//System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath)
|
||||||
|
System.setProperty("jna.library.path", vlcDir.absolutePath)
|
||||||
|
//discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath)
|
||||||
|
|
||||||
libsTmpDir.deleteRecursively()
|
libsTmpDir.deleteRecursively()
|
||||||
initHS()
|
initHS()
|
||||||
}
|
}
|
||||||
@ -34,7 +45,12 @@ private fun copyResources(from: String, to: Path) {
|
|||||||
return FileVisitResult.CONTINUE
|
return FileVisitResult.CONTINUE
|
||||||
}
|
}
|
||||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
Files.copy(file, to.resolve(resPath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING)
|
val dest = to.resolve(resPath.relativize(file).toString())
|
||||||
|
Files.copy(file, dest, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
// Setting the same time on file as the time set in script that generates VLC libs
|
||||||
|
if (dest.toString().contains("." + desktopPlatform.libExtension)) {
|
||||||
|
dest.setLastModifiedTime(FileTime.fromMillis(0))
|
||||||
|
}
|
||||||
return FileVisitResult.CONTINUE
|
return FileVisitResult.CONTINUE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -23,3 +23,4 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
|||||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
scripts/desktop/prepare-vlc-linux.sh
|
||||||
|
@ -95,3 +95,4 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
|||||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
scripts/desktop/prepare-vlc-mac.sh
|
||||||
|
74
scripts/desktop/prepare-vlc-linux.sh
Executable file
74
scripts/desktop/prepare-vlc-linux.sh
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function readlink() {
|
||||||
|
echo "$(cd "$(dirname "$1")"; pwd -P)"
|
||||||
|
}
|
||||||
|
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
|
||||||
|
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/deps/vlc
|
||||||
|
|
||||||
|
mkdir $vlc_dir || exit 0
|
||||||
|
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
mkdir tmp 2>/dev/null || true
|
||||||
|
cd tmp
|
||||||
|
curl https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage
|
||||||
|
chmod +x appimage
|
||||||
|
./appimage --appimage-extract
|
||||||
|
cp -r squashfs-root/usr/lib/* $vlc_dir
|
||||||
|
cd ../
|
||||||
|
rm -rf tmp
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
|
||||||
|
# This is currently unneeded
|
||||||
|
cd /tmp
|
||||||
|
(
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc
|
||||||
|
ar p libvlc data.tar.xz > data.tar.xz
|
||||||
|
tar -xvf data.tar.xz
|
||||||
|
mv usr/lib/x86_64-linux-gnu/libvlc.so{.5,}
|
||||||
|
cp usr/lib/x86_64-linux-gnu/libvlc.so* $vlc_dir
|
||||||
|
cd ../
|
||||||
|
rm -rf tmp
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore
|
||||||
|
ar p libvlccore data.tar.xz > data.tar.xz
|
||||||
|
tar -xvf data.tar.xz
|
||||||
|
cp usr/lib/x86_64-linux-gnu/libvlccore.so* $vlc_dir
|
||||||
|
cd ../
|
||||||
|
rm -rf tmp
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
curl http://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins
|
||||||
|
ar p plugins data.tar.xz > data.tar.xz
|
||||||
|
tar -xvf data.tar.xz
|
||||||
|
find usr/lib/x86_64-linux-gnu/vlc/plugins/ -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN/../../' {} \;
|
||||||
|
cp -r usr/lib/x86_64-linux-gnu/vlc/{libvlc*,plugins} $vlc_dir
|
||||||
|
cd ../
|
||||||
|
rm -rf tmp
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
curl http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn
|
||||||
|
ar p idn data.tar.xz > data.tar.xz
|
||||||
|
tar -xvf data.tar.xz
|
||||||
|
cp lib/x86_64-linux-gnu/lib* $vlc_dir
|
||||||
|
cd ../
|
||||||
|
rm -rf tmp
|
||||||
|
)
|
||||||
|
|
||||||
|
find $vlc_dir -maxdepth 1 -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
40
scripts/desktop/prepare-vlc-mac.sh
Executable file
40
scripts/desktop/prepare-vlc-mac.sh
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
||||||
|
if [ "$ARCH" == "arm64" ]; then
|
||||||
|
ARCH=aarch64
|
||||||
|
vlc_arch=arm64
|
||||||
|
else
|
||||||
|
vlc_arch=intel64
|
||||||
|
fi
|
||||||
|
vlc_version=3.0.19
|
||||||
|
|
||||||
|
function readlink() {
|
||||||
|
echo "$(cd "$(dirname "$1")"; pwd -P)"
|
||||||
|
}
|
||||||
|
|
||||||
|
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
|
||||||
|
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/deps/vlc
|
||||||
|
#rm -rf $vlc_dir
|
||||||
|
mkdir -p $vlc_dir/vlc || exit 0
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
mkdir tmp 2>/dev/null || true
|
||||||
|
cd tmp
|
||||||
|
curl https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc
|
||||||
|
unzip -oqq vlc
|
||||||
|
install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen
|
||||||
|
cd VLC.app/Contents/MacOS/lib
|
||||||
|
for lib in $(ls *.dylib); do install_name_tool -add_rpath "@loader_path" $lib 2> /dev/null || true; done
|
||||||
|
cd ../plugins
|
||||||
|
for lib in $(ls *.dylib); do
|
||||||
|
install_name_tool -add_rpath "@loader_path/../../" $lib 2> /dev/null || true
|
||||||
|
done
|
||||||
|
cd ..
|
||||||
|
../../../vlc-cache-gen plugins
|
||||||
|
cp lib/* $vlc_dir/
|
||||||
|
cp -r -p plugins/ $vlc_dir/vlc/plugins
|
||||||
|
cd ../../../../
|
||||||
|
rm -rf tmp
|
Loading…
Reference in New Issue
Block a user