setup media player service with session

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
This commit is contained in:
parneet-guraya 2024-08-25 21:55:05 +05:30
parent a2eba07608
commit 47479d5745
No known key found for this signature in database
GPG key ID: 63B807C4B2A9064B
13 changed files with 316 additions and 201 deletions

View file

@ -333,6 +333,7 @@ dependencies {
implementation 'org.conscrypt:conscrypt-android:2.5.3'
implementation "androidx.media3:media3-ui:$androidxMediaVersion"
implementation "androidx.media3:media3-session:$androidxMediaVersion"
implementation "androidx.media3:media3-exoplayer:$androidxMediaVersion"
implementation "androidx.media3:media3-datasource-okhttp:$androidxMediaVersion"

View file

@ -127,7 +127,14 @@
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute"
tools:replace="android:allowBackup">
<service
android:name="com.nextcloud.client.media.BackgroundPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_config" />

View file

@ -17,6 +17,8 @@ import com.nextcloud.client.integrations.IntegrationsModule;
import com.nextcloud.client.jobs.JobsModule;
import com.nextcloud.client.jobs.download.FileDownloadHelper;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.media.BackgroundPlayerService;
import com.nextcloud.client.media.CustomExoPlayer;
import com.nextcloud.client.network.NetworkModule;
import com.nextcloud.client.onboarding.OnboardingModule;
import com.nextcloud.client.preferences.PreferencesModule;
@ -46,7 +48,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
ThemeModule.class,
DatabaseModule.class,
DispatcherModule.class,
VariantModule.class
VariantModule.class,
})
@Singleton
public interface AppComponent {
@ -54,6 +56,8 @@ public interface AppComponent {
void inject(MainApp app);
void inject(MediaControlView mediaControlView);
void inject(CustomExoPlayer customExoPlayer);
void inject(BackgroundPlayerService backgroundPlayerService);
void inject(ThemeableSwitchPreference switchPreference);

View file

@ -17,6 +17,9 @@ import com.nextcloud.client.jobs.transfer.FileTransferService;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.logger.ui.LogsActivity;
import com.nextcloud.client.logger.ui.LogsViewModel;
import com.nextcloud.client.media.BackgroundPlayerService;
import com.nextcloud.client.media.CustomExoPlayer;
import com.nextcloud.client.media.NextcloudExoPlayer;
import com.nextcloud.client.media.PlayerService;
import com.nextcloud.client.migrations.Migrations;
import com.nextcloud.client.onboarding.FirstRunActivity;
@ -484,4 +487,9 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
@ContributesAndroidInjector
abstract BackgroundPlayerService backgroundPlayerService();
}

View file

@ -0,0 +1,82 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.media
import android.content.Intent
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.MainApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import javax.inject.Inject
class BackgroundPlayerService : MediaSessionService(), Injectable {
@Inject
lateinit var clientFactory: ClientFactory
@Inject
lateinit var userAccountManager: UserAccountManager
lateinit var exoPlayer: ExoPlayer
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
MainApp.getAppComponent().inject(this)
initNextcloudExoPlayer()
}
private fun initNextcloudExoPlayer() {
runBlocking {
var nextcloudClient: NextcloudClient
withContext(Dispatchers.IO) {
nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user)
}
nextcloudClient?.let {
exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
println(exoPlayer)
mediaSession =
MediaSession.Builder(applicationContext, exoPlayer).setCallback(object : MediaSession.Callback {
override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
stopSelf()
}
}).build()
}
println("created client $nextcloudClient")
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaSession?.player
if (player!!.playWhenReady) {
// Make sure the service is not in foreground.
player.pause()
}
stopSelf()
}
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? {
return mediaSession
}
}

View file

@ -0,0 +1,22 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.media
import android.accounts.AccountManager
import android.content.Context
import androidx.media3.exoplayer.ExoPlayer
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.network.ClientFactory
import com.owncloud.android.MainApp
import javax.inject.Inject
class CustomExoPlayer {
}

View file

@ -15,8 +15,11 @@ import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.MainApp
import javax.inject.Inject
object NextcloudExoPlayer {
private const val FIVE_SECONDS_IN_MILLIS = 5000L
@ -45,4 +48,5 @@ object NextcloudExoPlayer {
.setSeekForwardIncrementMs(FIVE_SECONDS_IN_MILLIS)
.build()
}
}

View file

@ -23,10 +23,10 @@ import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.LinearLayout
import android.widget.MediaController.MediaPlayerControl
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.core.content.ContextCompat
import androidx.media3.common.Player
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.databinding.MediaControlBinding
@ -50,7 +50,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
View.OnClickListener,
OnSeekBarChangeListener {
private var playerControl: MediaPlayerControl? = null
private var playerControl: Player? = null
private var binding: MediaControlBinding
private var isDragging = false
@ -62,7 +62,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
@Suppress("MagicNumber")
fun setMediaPlayer(player: MediaPlayerControl?) {
fun setMediaPlayer(player: Player?) {
playerControl = player
handler.sendEmptyMessage(SHOW_PROGRESS)
@ -104,14 +104,16 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
*/
private fun disableUnsupportedButtons() {
try {
if (playerControl?.canPause() == false) {
binding.playBtn.setEnabled(false)
//TODO: should we check for nullability && see if we need try catch block
if (playerControl!!.isCommandAvailable(Player.COMMAND_PLAY_PAUSE).not()) {
binding.playBtn.isEnabled = false
}
if (playerControl?.canSeekBackward() == false) {
binding.rewindBtn.setEnabled(false)
if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK).not()) {
binding.rewindBtn.isEnabled = false
}
if (playerControl?.canSeekForward() == false) {
binding.forwardBtn.setEnabled(false)
if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD).not()) {
binding.forwardBtn.isEnabled = false
}
} catch (ex: IncompatibleClassChangeError) {
// We were given an old version of the interface, that doesn't have
@ -149,7 +151,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
@Suppress("MagicNumber")
private fun formatTime(timeMs: Int): String {
private fun formatTime(timeMs: Long): String {
val totalSeconds = timeMs / 1000
val seconds = totalSeconds % 60
val minutes = totalSeconds / 60 % 60
@ -164,8 +166,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
@Suppress("MagicNumber")
private fun setProgress(): Int {
var position = 0
private fun setProgress(): Long {
var position = 0L
if (playerControl == null || isDragging) {
position = 0
}
@ -178,7 +180,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
val pos = 1000L * position / duration
binding.progressBar.progress = pos.toInt()
}
val percent = playerControl.bufferPercentage
val percent = playerControl.bufferedPercentage
binding.progressBar.setSecondaryProgress(percent * 10)
val endTime = if (duration > 0) formatTime(duration) else "--:--"
binding.totalTimeText.text = endTime
@ -202,22 +204,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
return true
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
if (uniqueDown && playerControl?.isPlaying == false) {
playerControl?.start()
if (uniqueDown && playerControl?.playWhenReady == false) {
playerControl?.play()
updatePausePlay()
}
return true
}
KeyEvent.KEYCODE_MEDIA_STOP,
KeyEvent.KEYCODE_MEDIA_PAUSE
-> {
if (uniqueDown && playerControl?.isPlaying == true) {
if (uniqueDown && playerControl?.playWhenReady == true) {
playerControl?.pause()
updatePausePlay()
}
return true
}
else -> return super.dispatchKeyEvent(event)
}
}
@ -225,18 +230,21 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
fun updatePausePlay() {
binding.playBtn.icon = ContextCompat.getDrawable(
context,
// isPlaying reflects if the playback is actually moving forward, If the media is buffering and it will play when ready
// it would still return that it is not playing. So, in case of buffering it will show the pause icon which would show that
// media is loading, when user has not paused but moved the progress to a different position this works as a buffering signal.
if (playerControl?.isPlaying == true) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
)
binding.forwardBtn.visibility = if (playerControl?.canSeekForward() == true) {
binding.forwardBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) {
VISIBLE
} else {
INVISIBLE
}
binding.rewindBtn.visibility = if (playerControl?.canSeekBackward() == true) {
binding.rewindBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK)) {
VISIBLE
} else {
INVISIBLE
@ -245,10 +253,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
private fun doPauseResume() {
playerControl?.run {
if (isPlaying) {
if (playWhenReady) {
pause()
} else {
start()
play()
}
}
updatePausePlay()
@ -267,16 +275,17 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
@Suppress("MagicNumber")
override fun onClick(v: View) {
var pos: Int
var pos: Long
playerControl?.let { playerControl ->
val playing = playerControl.isPlaying
val playing = playerControl.playWhenReady
val id = v.id
when (id) {
R.id.playBtn -> {
doPauseResume()
}
R.id.rewindBtn -> {
pos = playerControl.currentPosition
pos -= 5000
@ -286,6 +295,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
setProgress()
}
R.id.forwardBtn -> {
pos = playerControl.currentPosition
pos += 15000
@ -315,8 +325,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
playerControl?.let { playerControl ->
val duration = playerControl.duration.toLong()
val newPosition = duration * progress / 1000L
playerControl.seekTo(newPosition.toInt())
binding.currentTimeText.text = formatTime(newPosition.toInt())
playerControl.seekTo(newPosition)
binding.currentTimeText.text = formatTime(newPosition)
}
}

View file

@ -1832,6 +1832,7 @@ public class FileDisplayActivity extends FileActivity
((PreviewMediaFragment) fileFragment).updateFile(renamedFile);
if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
long position = ((PreviewMediaFragment) fileFragment).getPosition();
System.out.println("Start Fragment Media Preview");
startMediaPreview(renamedFile, position, true, true, true, false);
} else {
getFileOperationsHelper().openFile(renamedFile);

View file

@ -13,15 +13,11 @@
package com.owncloud.android.ui.preview
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
@ -45,20 +41,24 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import com.google.common.util.concurrent.MoreExecutors
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.download.FileDownloadHelper
import com.nextcloud.client.media.BackgroundPlayerService
import com.nextcloud.client.media.ExoplayerListener
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
import com.nextcloud.client.media.PlayerService
import com.nextcloud.client.media.PlayerServiceConnection
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.network.ClientFactory.CreationException
@ -71,7 +71,6 @@ import com.nextcloud.utils.extensions.statusBarHeight
import com.owncloud.android.R
import com.owncloud.android.databinding.ActivityPreviewMediaBinding
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.files.StreamMediaFileOperation
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
@ -88,7 +87,6 @@ import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
import com.owncloud.android.ui.dialog.SendShareDialog
import com.owncloud.android.ui.fragment.FileFragment
import com.owncloud.android.ui.fragment.OCFileListFragment
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.ErrorMessageAdapter
import com.owncloud.android.utils.MimeTypeUtil
@ -119,7 +117,7 @@ class PreviewMediaActivity :
private var autoplay = true
private val prepared = false
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
private var videoUri: Uri? = null
private var streamUri: Uri? = null
@Inject
lateinit var clientFactory: ClientFactory
@ -132,13 +130,15 @@ class PreviewMediaActivity :
private lateinit var binding: ActivityPreviewMediaBinding
private var emptyListView: ViewGroup? = null
private var exoPlayer: ExoPlayer? = null
private var videoPlayer: ExoPlayer? = null
private var audioMediaController: MediaController? = null
private var nextcloudClient: NextcloudClient? = null
private lateinit var windowInsetsController: WindowInsetsControllerCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
setTheme(R.style.Theme_ownCloud_Toolbar)
}
@ -156,6 +156,13 @@ class PreviewMediaActivity :
emptyListView = binding.emptyView.emptyListView
showProgressLayout()
addMarginForEmptyView()
if (file == null) {
return
}
if (MimeTypeUtil.isAudio(file)) {
setGenericThumbnail()
initializeAudioPlayer()
}
}
private fun addMarginForEmptyView() {
@ -173,25 +180,6 @@ class PreviewMediaActivity :
emptyListView?.layoutParams = layoutParams
}
private fun registerMediaControlReceiver() {
val filter = IntentFilter(MEDIA_CONTROL_READY_RECEIVER)
LocalBroadcastManager.getInstance(this).registerReceiver(mediaControlReceiver, filter)
}
private val mediaControlReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
intent.getBooleanExtra(PlayerService.IS_MEDIA_CONTROL_LAYOUT_READY, false).run {
if (this) {
hideProgressLayout()
mediaPlayerServiceConnection?.bind()
setupAudioPlayerServiceConnection()
} else {
showProgressLayout()
}
}
}
}
private fun initArguments(savedInstanceState: Bundle?) {
intent?.let {
initWithIntent(it)
@ -203,20 +191,6 @@ class PreviewMediaActivity :
} else {
initWithBundle(savedInstanceState)
}
if (MimeTypeUtil.isAudio(file)) {
preparePreviewForAudioFile()
}
}
private fun preparePreviewForAudioFile() {
registerMediaControlReceiver()
if (file.isDown) {
return
}
requestForDownload(file)
}
private fun initWithIntent(intent: Intent) {
@ -245,8 +219,6 @@ class PreviewMediaActivity :
if (isFileVideo) {
binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
} else {
extractAndSetCoverArt(file)
}
}
@ -265,13 +237,13 @@ class PreviewMediaActivity :
private fun showProgressLayout() {
binding.progress.visibility = View.VISIBLE
binding.mediaController.visibility = View.GONE
binding.audioControllerView.visibility = View.GONE
binding.emptyView.emptyListView.visibility = View.GONE
}
private fun hideProgressLayout() {
binding.progress.visibility = View.GONE
binding.mediaController.visibility = View.VISIBLE
binding.audioControllerView.visibility = View.VISIBLE
binding.emptyView.emptyListView.visibility = View.VISIBLE
}
@ -287,48 +259,6 @@ class PreviewMediaActivity :
}
}
/**
* tries to read the cover art from the audio file and sets it as cover art.
*
* @param file audio file with potential cover art
*/
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
private fun extractAndSetCoverArt(file: OCFile) {
if (!MimeTypeUtil.isAudio(file)) {
return
}
val bitmap = if (file.storagePath == null) {
getAudioThumbnail(file)
} else {
getThumbnail(file.storagePath) ?: getAudioThumbnail(file)
}
if (bitmap != null) {
binding.imagePreview.setImageBitmap(bitmap)
} else {
setGenericThumbnail()
}
}
@Suppress("TooGenericExceptionCaught")
private fun getThumbnail(storagePath: String?): Bitmap? {
return try {
MediaMetadataRetriever().run {
setDataSource(storagePath)
BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture?.size ?: 0)
}
} catch (t: Throwable) {
BitmapUtils.drawableToBitmap(genericThumbnail())
}
}
private fun getAudioThumbnail(file: OCFile): Bitmap? {
return ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
)
}
private fun setGenericThumbnail() {
binding.imagePreview.setImageDrawable(genericThumbnail())
}
@ -356,8 +286,8 @@ class PreviewMediaActivity :
private fun saveMediaInstanceState(bundle: Bundle) {
bundle.run {
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
exoPlayer?.let {
if (MimeTypeUtil.isVideo(file) && audioMediaController != null) {
audioMediaController?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.isPlaying
}
@ -375,42 +305,13 @@ class PreviewMediaActivity :
Log_OC.v(TAG, "onStart")
if (file == null) {
return
}
mediaPlayerServiceConnection?.bind()
if (MimeTypeUtil.isAudio(file)) {
setupAudioPlayerServiceConnection()
} else if (MimeTypeUtil.isVideo(file)) {
if (mediaPlayerServiceConnection?.isConnected == true) {
stopAudio()
}
if (exoPlayer != null) {
playVideo()
} else {
initNextcloudExoPlayer()
}
if (MimeTypeUtil.isVideo(file)) {
//TODO: should we somehow release any previous audio sessions?
initializeVideoPlayer()
}
}
private fun setupAudioPlayerServiceConnection() {
binding.mediaController.run {
setMediaPlayer(mediaPlayerServiceConnection)
visibility = View.VISIBLE
}
user?.let {
mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
}
binding.emptyView.emptyListView.visibility = View.GONE
binding.progress.visibility = View.GONE
}
private fun initNextcloudExoPlayer() {
private fun initializeVideoPlayer() {
val handler = Handler(Looper.getMainLooper())
Executors.newSingleThreadExecutor().execute {
try {
@ -418,9 +319,10 @@ class PreviewMediaActivity :
nextcloudClient?.let { client ->
handler.post {
exoPlayer = createNextcloudExoplayer(this, client)
videoPlayer = createNextcloudExoplayer(this, client)
exoPlayer?.let { player ->
videoPlayer?.let { player ->
player.addListener(
ExoplayerListener(
this,
@ -439,6 +341,72 @@ class PreviewMediaActivity :
}
}
private fun releaseVideoPlayer() {
videoPlayer?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.playWhenReady
it.release()
}
videoPlayer = null
}
private fun initializeAudioPlayer() {
val sessionToken = SessionToken(this, ComponentName(this, BackgroundPlayerService::class.java))
val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
controllerFuture.addListener(
{
try {
audioMediaController = controllerFuture.get()
playAudio()
binding.audioControllerView.setMediaPlayer(audioMediaController)
} catch (e: Exception) {
println("exception raised while getting the media controller ${e.message}")
}
},
MoreExecutors.directExecutor()
)
}
private fun playAudio() {
if (file.isDown) {
prepareAudioPlayer(file.storageUri)
} else {
try {
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
} catch (e: Exception) {
Log_OC.e(TAG, "Loading stream url not possible: $e")
}
}
}
private fun prepareAudioPlayer(uri: Uri) {
hideProgressLayout()
audioMediaController?.let { audioPlayer ->
audioPlayer.addListener(object : Player.Listener {
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
val artworkBitmap = mediaMetadata.artworkData?.let { bytes: ByteArray ->
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
if (artworkBitmap != null) {
binding.imagePreview.setImageBitmap(artworkBitmap)
}
}
})
audioPlayer.setMediaItem(MediaItem.fromUri(uri))
audioPlayer.playWhenReady = autoplay
audioPlayer.seekTo(savedPlaybackPosition)
audioPlayer.prepare()
}
}
private fun releaseAudioPlayer() {
audioMediaController?.let { audioPlayer ->
audioPlayer.stop()
audioPlayer.release()
}
audioMediaController = null
}
private fun initWindowInsetsController() {
windowInsetsController = WindowCompat.getInsetsController(
window,
@ -495,7 +463,7 @@ class PreviewMediaActivity :
}
}
)
it.player = exoPlayer
it.player = videoPlayer
}
}
@ -567,7 +535,7 @@ class PreviewMediaActivity :
}
R.id.action_remove_file -> {
exoPlayer?.stop()
videoPlayer?.pause()
val dialog = RemoveFilesDialogFragment.newInstance(file)
dialog.show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
}
@ -614,6 +582,7 @@ class PreviewMediaActivity :
val removedFile = operation.file
val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
if (!fileAvailable && removedFile == file) {
releaseAudioPlayer()
finish()
}
} else if (operation is SynchronizeFileOperation) {
@ -670,7 +639,7 @@ class PreviewMediaActivity :
setupVideoView()
if (file.isDown) {
playVideoUri(file.storageUri)
prepareVideoPlayer(file.storageUri)
} else {
try {
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
@ -680,22 +649,17 @@ class PreviewMediaActivity :
}
}
private fun playVideoUri(uri: Uri) {
private fun prepareVideoPlayer(uri: Uri) {
binding.progress.visibility = View.GONE
exoPlayer?.run {
setMediaItem(MediaItem.fromUri(uri))
val videoMediaItem = MediaItem.fromUri(uri)
videoPlayer?.run {
setMediaItem(videoMediaItem)
playWhenReady = autoplay
prepare()
if (savedPlaybackPosition >= 0) {
seekTo(savedPlaybackPosition)
prepare()
}
}
autoplay = false
}
private class LoadStreamUrl(
previewMediaActivity: PreviewMediaActivity,
private val user: User?,
@ -728,8 +692,12 @@ class PreviewMediaActivity :
val weakReference = previewMediaActivityWeakReference.get()
weakReference?.apply {
if (uri != null) {
videoUri = uri
playVideoUri(uri)
streamUri = uri
if (MimeTypeUtil.isVideo(file)) {
prepareVideoPlayer(uri)
} else if (MimeTypeUtil.isAudio(file)) {
prepareAudioPlayer(uri)
}
} else {
emptyListView?.visibility = View.VISIBLE
setVideoErrorMessage(
@ -754,29 +722,15 @@ class PreviewMediaActivity :
}
override fun onDestroy() {
Log_OC.v(TAG, "onDestroy")
LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaControlReceiver)
super.onDestroy()
exoPlayer?.run {
stop()
release()
}
Log_OC.v(TAG, "onDestroy")
}
override fun onStop() {
Log_OC.v(TAG, "onStop")
file?.let {
if (MimeTypeUtil.isVideo(it) && exoPlayer != null && exoPlayer?.isPlaying == true) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
}
}
exoPlayer?.pause()
stopAudio()
mediaPlayerServiceConnection?.unbind()
releaseVideoPlayer()
super.onStop()
}
@ -829,23 +783,15 @@ class PreviewMediaActivity :
}
private fun stopPreview(stopAudio: Boolean) {
//TODO: stop removes the media item attached but not release the player
// do we want to keep this behaviour or release the player too just like in onStop?
if (MimeTypeUtil.isAudio(file) && stopAudio) {
mediaPlayerServiceConnection?.pause()
audioMediaController?.pause()
} else if (MimeTypeUtil.isVideo(file)) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
exoPlayer?.stop()
releaseVideoPlayer()
}
}
val position: Long
get() {
if (prepared) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
}
Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
return savedPlaybackPosition
}
companion object {
private val TAG = PreviewMediaActivity::class.java.simpleName

View file

@ -330,7 +330,7 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
}
private fun prepareForAudio() {
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
binding.mediaController.setMediaPlayer(null)
binding.mediaController.visibility = View.VISIBLE
mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
binding.emptyView.emptyListView.visibility = View.GONE

View file

@ -42,7 +42,7 @@
app:show_buffering="always" />
<com.owncloud.android.media.MediaControlView
android:id="@+id/media_controller"
android:id="@+id/audio_controller_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
@ -65,7 +65,9 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:visibility="gone"
>
<include
android:id="@+id/empty_view"

View file

@ -2548,6 +2548,14 @@
<sha256 value="3670ba201f837bdce5ffaf4adc766a0d21cfd08db74efed5657513544c054eba" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.media3" name="media3-session" version="1.4.0">
<artifact name="media3-session-1.4.0.aar">
<sha256 value="a5daaaea8fc9a87ebb4411f1d97bcf887069132068b3af15374205cfd458bb7c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="media3-session-1.4.0.module">
<sha256 value="6bccbca5b01eaa3fd0502300f3530ba1f1cdc952927a0a0f3fb1b1ae39860ed6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.media3" name="media3-ui" version="1.2.0">
<artifact name="media3-ui-1.2.0.aar">
<sha256 value="fee39edbf615f9432f53af1cc9b20dd5706bfbc5dbd7fe581253a59eedd91482" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -9718,6 +9726,11 @@
<sha256 value="16e05e9f49621b87c53e69350140f3c46d42d966c67a933bdf4b063a2b1c8fc5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jacoco" name="org.jacoco.agent" version="0.8.12">
<artifact name="org.jacoco.agent-0.8.12.pom">
<sha256 value="0f9da994abd9827f957fc1ba7c5bad3fe918f62601c1d743f216b0615efe480e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jacoco" name="org.jacoco.ant" version="0.8.11">
<artifact name="org.jacoco.ant-0.8.11.jar">
<sha256 value="81d7eb8890d9be30a939612c295603541063529cdd03a53265aba74474b70b7c" origin="Generated by Gradle"/>
@ -11041,6 +11054,11 @@
<sha256 value="92eee24bc3c843e4881d46c1dd6505471ee3142facfb466b428cfea5a56c6b60" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm" version="9.7">
<artifact name="asm-9.7.pom">
<sha256 value="de00115f1d84f3a0b2ee3a4b6f6192d066f86d185d67b9d1522f2c80feac5f00" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-analysis" version="9.2">
<artifact name="asm-analysis-9.2.jar">
<sha256 value="878fbe521731c072d14d2d65b983b1beae6ad06fda0007b6a8bae81f73f433c4" origin="Generated by Gradle"/>
@ -11091,6 +11109,11 @@
<sha256 value="a98ae4895334baf8ff86bd66516210dbd9a03f1a6e15e47dda82afcf6b53d77c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-commons" version="9.7">
<artifact name="asm-commons-9.7.pom">
<sha256 value="5acee3ee7252ed90b8074c755d022787499a95fafff98ac4a685107c4da409b4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.2">
<artifact name="asm-tree-9.2.jar">
<sha256 value="aabf9bd23091a4ebfc109c1f3ee7cf3e4b89f6ba2d3f51c5243f16b3cffae011" origin="Generated by Gradle"/>
@ -11115,6 +11138,11 @@
<sha256 value="1bcb481d7fc16b955bb60ca07c8cfa2424bcee78bdc405bba31c7d6f5dc2d113" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.7">
<artifact name="asm-tree-9.7.pom">
<sha256 value="a34ea1e3e4128c01038db43c6976e88c779cf5af84b0505da266dfe6965668ec" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-util" version="9.2">
<artifact name="asm-util-9.2.jar">
<sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/>