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:
Stanislav Dmitrenko 2023-09-22 01:03:47 +08:00 committed by GitHub
parent 6de0ed4766
commit 2d7655281f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 787 additions and 187 deletions

View File

@ -139,8 +139,6 @@ dependencies {
//implementation("androidx.compose.material:material-icons-extended:$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")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

View File

@ -73,7 +73,7 @@ class MainActivity: FragmentActivity() {
override fun onStop() {
super.onStop()
VideoPlayer.stopAll()
VideoPlayerHolder.stopAll()
AppLock.appWasHidden()
}

View File

@ -97,6 +97,7 @@ kotlin {
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.7")
implementation("uk.co.caprica:vlcj:4.7.0")
}
}
val desktopTest by getting

View File

@ -27,7 +27,7 @@ actual class RecorderNative: RecorderInterface {
}
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
VideoPlayer.stopAll()
VideoPlayerHolder.stopAll()
AudioPlayer.stop()
val rec: MediaRecorder
recorder = initRecorder().also { rec = it }
@ -140,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface {
return null
}
VideoPlayer.stopAll()
VideoPlayerHolder.stopAll()
RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != fileSource.filePath) {

View File

@ -1,10 +1,13 @@
package chat.simplex.common.platform
import android.media.MediaMetadataRetriever
import android.media.session.PlaybackState
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
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.res.MR
import com.google.android.exoplayer2.*
@ -17,49 +20,15 @@ import kotlinx.coroutines.*
import java.io.File
import java.net.URI
actual class VideoPlayer private constructor(
private val uri: URI,
private val gallery: Boolean,
actual class VideoPlayer actual constructor(
override val uri: URI,
override val gallery: Boolean,
private val defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): 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
override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
@ -114,7 +83,7 @@ actual class VideoPlayer private constructor(
RecorderInterface.stopRecording?.invoke()
}
AudioPlayer.stop()
stopAll()
VideoPlayerHolder.stopAll()
if (listener.value == null) {
runCatching {
val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
@ -224,14 +193,14 @@ actual class VideoPlayer private constructor(
override fun release(remove: Boolean) {
player.release()
if (remove) {
players.remove(uri to gallery)
VideoPlayerHolder.players.remove(uri to gallery)
}
}
private fun setPreviewAndDuration() {
// It freezes main thread, doing it in IO thread
CoroutineScope(Dispatchers.IO).launch {
val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
withContext(Dispatchers.Main) {
preview.value = previewAndDuration.preview ?: defaultPreview
duration.value = (previewAndDuration.duration ?: 0)

View File

@ -51,7 +51,7 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
}
@Composable
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {

View File

@ -7,13 +7,12 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
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 dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@ -2113,6 +2112,23 @@ data class CryptoFile(
val isAbsolutePath: Boolean
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 {
fun plain(f: String): CryptoFile = CryptoFile(f, null)
}

View File

@ -7,6 +7,8 @@ import java.net.URI
interface VideoPlayerInterface {
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
val uri: URI
val gallery: Boolean
val soundEnabled: MutableState<Boolean>
val brokenVideo: MutableState<Boolean>
val videoPlaying: MutableState<Boolean>
@ -20,18 +22,45 @@ interface VideoPlayerInterface {
fun release(remove: Boolean)
}
expect class VideoPlayer: VideoPlayerInterface {
companion object {
fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean
fun release(uri: URI, gallery: Boolean, remove: Boolean)
fun stopAll()
fun releaseAll()
expect class VideoPlayer(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayerInterface
object VideoPlayerHolder {
val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
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()
}
}

View File

@ -737,7 +737,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
DisposableEffectOnGone(
whenGone = {
VideoPlayer.releaseAll()
VideoPlayerHolder.releaseAll()
}
)
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {

View File

@ -50,7 +50,7 @@ fun CIVideoView(
})
} else {
Box {
ImageView(preview, showMenu, onClick = {
VideoPreviewImageView(preview, onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
@ -75,7 +75,10 @@ fun CIVideoView(
else -> {}
}
}
})
},
onLongClick = {
showMenu.value = true
})
if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
}
@ -90,7 +93,7 @@ fun CIVideoView(
@Composable
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 progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
@ -111,6 +114,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
stop()
}
}
val onLongClick = { showMenu.value = true }
Box {
val windowWidth = LocalWindowWidth()
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,
width,
onClick = onClick,
onLongClick = { showMenu.value = true },
onLongClick = onLongClick,
stop
)
if (showPreview.value) {
ImageView(preview, showMenu, onClick)
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
VideoPreviewImageView(preview, onClick, onLongClick)
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
}
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
}
@ -201,7 +205,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
}
@Composable
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
Image(
@ -210,10 +214,10 @@ private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onC
modifier = Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onLongClick = onLongClick,
onClick = onClick
)
.onRightClick { showMenu.value = true },
.onRightClick(onLongClick),
contentScale = ContentScale.FillWidth,
)
}

View File

@ -46,9 +46,11 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val scope = rememberCoroutineScope()
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
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(
Modifier
.fillMaxSize()
@ -127,7 +129,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
FullScreenImageView(modifier, data, imageBitmap)
} else if (media is ProviderMedia.Video) {
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) {
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
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
@Composable
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) {
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) {
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)
@ -154,13 +161,16 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
player.enableSound(true)
snapshotFlow { isCurrentPage.value }
.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) {
FullScreenVideoView(player, modifier)
FullScreenVideoView(player, modifier, close)
}
}
@Composable
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier)
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit)

View File

@ -66,6 +66,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
if (chatModel.chatId.value != null) {
ModalManager.end.closeModalsExceptFirst()
}
AudioPlayer.stop()
VideoPlayerHolder.stopAll()
}
}
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp

View File

@ -29,6 +29,10 @@ fun initApp() {
//testCrypto()
}
fun discoverVlcLibs(path: String) {
uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1)
}
private fun applyAppLocale() {
val lang = ChatController.appPrefs.appLanguage.get()
if (lang == null || lang == Locale.getDefault().language) return

View File

@ -21,6 +21,8 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
actual val databaseExportDir: File = tmpDir
val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() }
actual fun desktopOpenDatabaseDir() {
if (Desktop.isDesktopSupported()) {
try {

View File

@ -1,9 +1,17 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import chat.simplex.common.model.*
import chat.simplex.common.views.usersettings.showInDevelopingAlert
import kotlinx.coroutines.CoroutineScope
import chat.simplex.common.views.helpers.AlertManager
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 {
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
@ -18,36 +26,187 @@ actual class RecorderNative: RecorderInterface {
}
actual object AudioPlayer: AudioPlayerInterface {
override fun play(fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
showInDevelopingAlert()
val player by lazy { AudioPlayerComponent().mediaPlayer() }
// 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() {
/*LALAL*/
if (currentlyPlaying.value == null) return
player.stop()
stopListener()
}
override fun stop(item: ChatItem) {
/*LALAL*/
}
override fun stop(item: ChatItem) = stop(item.file?.fileName)
// FileName or filePath are ok
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>) {
TODO("Not yet implemented")
pro.value = pause()
audioPlaying.value = false
}
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? {
/*LALAL*/
return null
var res: Int? = 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 {
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
override fun stop() { /*LALAL*/ }

View File

@ -3,51 +3,213 @@ package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
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 kotlin.math.max
actual class VideoPlayer: VideoPlayerInterface {
actual companion object {
actual fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
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*/ }
}
actual class VideoPlayer actual constructor(
override val uri: URI,
override val gallery: Boolean,
private val defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayerInterface {
override val soundEnabled: MutableState<Boolean> = mutableStateOf(false)
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
override val progress: 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() {
/*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) {
if (appPlatform.isDesktop) {
showInDevelopingAlert()
if (progress.value == duration.value) {
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 {
/*TODO*/
return false
if (isReleased) 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) {
/*TODO*/
override fun release(remove: Boolean) { withApi {
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")
}
}

View File

@ -6,9 +6,7 @@ import androidx.compose.ui.unit.Dp
import chat.simplex.common.platform.VideoPlayer
@Composable
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
/* LALAL */
}
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {}
@Composable
actual fun LocalWindowWidth(): Dp {

View File

@ -1,14 +1,23 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.foundation.*
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.awt.SwingPanel
import androidx.compose.ui.graphics.*
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.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlin.math.max
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
@ -20,6 +29,43 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
)
}
@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)
}
}
}

View File

@ -133,7 +133,6 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
}
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
// LALAL
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L)
}

View File

@ -1,6 +1,5 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
import java.util.*
plugins {
kotlin("multiplatform")
@ -22,6 +21,7 @@ kotlin {
dependencies {
implementation(project(":common"))
implementation(compose.desktop.currentOs)
implementation("net.java.dev.jna:jna:5.13.0")
}
}
val jvmTest by getting
@ -33,16 +33,23 @@ compose {
desktop {
application {
// For debugging via VisualVM
/*jvmArgs += listOf(
"-Dcom.sun.management.jmxremote.port=8080",
"-Dcom.sun.management.jmxremote.ssl=false",
"-Dcom.sun.management.jmxremote.authenticate=false"
)*/
val debugJava = false
if (debugJava) {
jvmArgs += listOf(
"-Dcom.sun.management.jmxremote.port=8080",
"-Dcom.sun.management.jmxremote.ssl=false",
"-Dcom.sun.management.jmxremote.authenticate=false"
)
}
mainClass = "chat.simplex.desktop.MainKt"
nativeDistributions {
// For debugging via VisualVM
//modules("jdk.zipfs", "jdk.management.agent")
modules("jdk.zipfs")
if (debugJava) {
modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent")
} else {
// 'jdk.unsupported' is for vlcj
modules("jdk.zipfs", "jdk.unsupported")
}
//includeAllModules = true
outputBaseDir.set(project.file("../release"))
targetFormats(
@ -145,57 +152,119 @@ tasks.named("compileJava") {
afterEvaluate {
tasks.create("cmakeBuildAndCopy") {
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 {
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
copyDetails.forEach { (destinationDir, details) ->
details.forEach { detail ->
val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path)
if (target.exists()) {
target.setLastModified(detail.lastModified)
}
}
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
}

View File

@ -5,6 +5,8 @@ import chat.simplex.common.showApp
import java.io.File
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import kotlin.io.path.setLastModifiedTime
fun main() {
initHaskell()
@ -20,6 +22,15 @@ private fun initHaskell() {
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
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()
initHS()
}
@ -34,7 +45,12 @@ private fun copyResources(from: String, to: Path) {
return FileVisitResult.CONTINUE
}
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
}
})

View File

@ -23,3 +23,4 @@ rm -rf apps/multiplatform/desktop/build/cmake
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 $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

View File

@ -95,3 +95,4 @@ rm -rf apps/multiplatform/desktop/build/cmake
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 $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

View 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' {} \;

View 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