Commit b17b0a16 authored by Geoffrey Métais's avatar Geoffrey Métais

Refactor PlaybackService

Add a PlaylistManager and a PlayerController to split code and
responsibilities
parent a830fe58
......@@ -578,11 +578,14 @@ public class MediaPlayer extends VLCObject<MediaPlayer.Event> {
}
}
public int setRenderer(RendererItem item) {
return nativeSetRenderer(item);
}
public synchronized boolean hasMedia() {
return mMedia != null;
}
/**
* Get the Media used by this MediaPlayer. This Media should be released with {@link #release()}.
*/
......
......@@ -121,8 +121,7 @@ public class PreferencesActivity extends AppCompatActivity implements PlaybackSe
}
public void restartMediaPlayer(){
if (mService != null)
mService.restartMediaPlayer();
if (mService != null) mService.restartMediaPlayer();
}
public void exitAndRescan(){
......
......@@ -1605,8 +1605,7 @@ public class VideoPlayerActivity extends AppCompatActivity implements IVLCVout.C
case MediaPlayer.Event.EndReached:
/* Don't end the activity if the media has subitems since the next child will be
* loaded by the PlaybackService */
if (!mHasSubItems)
endReached();
if (!mHasSubItems) endReached();
break;
case MediaPlayer.Event.EncounteredError:
encounteredError();
......@@ -1750,17 +1749,17 @@ public class VideoPlayerActivity extends AppCompatActivity implements IVLCVout.C
seek(0);
return;
}
if (mService.expand(false) == 0) {
mHandler.removeMessages(LOADING_ANIMATION);
mHandler.sendEmptyMessageDelayed(LOADING_ANIMATION, LOADING_ANIMATION_DELAY);
Log.d(TAG, "Found a video playlist, expanding it");
mHandler.post(new Runnable() {
@Override
public void run() {
loadMedia();
}
});
}
// if (mService.expand(false) == 0) {
// mHandler.removeMessages(LOADING_ANIMATION);
// mHandler.sendEmptyMessageDelayed(LOADING_ANIMATION, LOADING_ANIMATION_DELAY);
// Log.d(TAG, "Found a video playlist, expanding it");
// mHandler.post(new Runnable() {
// @Override
// public void run() {
// loadMedia();
// }
// });
// }
//Ignore repeat
if (mService.getRepeatType() == Constants.REPEAT_ALL && mService.getMediaListSize() == 1)
exitOK();
......
......@@ -7,7 +7,9 @@ import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Log;
import org.videolan.libvlc.util.AndroidUtil;
import org.videolan.medialibrary.Tools;
......@@ -25,6 +27,8 @@ import java.util.List;
public class MediaUtils {
private static final String TAG = "VLC/MediaUtils";
private static SubtitlesDownloader sSubtitlesDownloader;
public static void getSubs(Activity activity, List<MediaWrapper> mediaList) {
......@@ -274,4 +278,21 @@ public class MediaUtils {
dialog.dismiss();
}
}
public static void retrieveMediaTitle(MediaWrapper mw) {
Cursor cursor = null;
try {
cursor = VLCApplication.getAppContext().getContentResolver().query(mw.getUri(), null, null, null, null);
if (cursor == null) return;
final int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex > -1 && cursor.getCount() > 0) {
cursor.moveToFirst();
if (!cursor.isNull(nameIndex)) mw.setTitle(cursor.getString(nameIndex));
}
} catch (SecurityException|IllegalArgumentException e) { // We may not have storage access permission yet
Log.w(TAG, "retrieveMediaTitle: fail to resolve file from "+mw.getUri(), e);
} finally {
if (cursor != null && !cursor.isClosed()) cursor.close();
}
}
}
......@@ -157,9 +157,7 @@ public class MediaWrapperList {
@Nullable
public MediaWrapper getMedia(int position) {
if (!isValid(position))
return null;
return mInternalList.get(position);
return isValid(position) ? mInternalList.get(position) : null;
}
public List<MediaWrapper> getAll() {
......
package org.videolan.vlc.media
import android.net.Uri
import android.support.annotation.MainThread
import android.support.v4.media.session.PlaybackStateCompat
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import org.videolan.libvlc.IVLCVout
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.RendererItem
import org.videolan.medialibrary.media.MediaWrapper
import org.videolan.vlc.RendererDelegate
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.gui.preferences.PreferencesActivity
import org.videolan.vlc.util.VLCInstance
import org.videolan.vlc.util.VLCOptions
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
class PlayerController : IVLCVout.Callback, MediaPlayer.EventListener {
private val playerContext by lazy(LazyThreadSafetyMode.NONE) { newSingleThreadContext("vlc-player") }
private val settings by lazy(LazyThreadSafetyMode.NONE) { VLCApplication.getSettings() }
private var mediaplayer = newMediaPlayer()
var switchToVideo = false
var seekable = false
var pausable = false
var previousMediaStats: Media.Stats? = null
private set
var playbackState = PlaybackStateCompat.STATE_STOPPED
private set
fun getVout() = mediaplayer.vlcVout
fun getMedia(): Media? = mediaplayer.media
fun play() {
if (mediaplayer.hasMedia()) mediaplayer.play()
}
fun pause(): Boolean {
if (!mediaplayer.hasMedia() && !pausable) return false
mediaplayer.pause()
return true
}
fun stop() {
if (mediaplayer.hasMedia()) mediaplayer.stop()
}
fun releaseMedia() = mediaplayer.media?.let {
it.setEventListener(null)
it.release()
}
private var mediaplayerEventListener: MediaPlayer.EventListener? = null
internal fun startPlayback(media: Media, listener: MediaPlayer.EventListener) {
/* Pausable and seekable are true by default */
seekable = true
pausable = true
mediaplayer.media = media
media.release()
mediaplayerEventListener = listener
mediaplayer.setEventListener(this)
mediaplayer.setEqualizer(VLCOptions.getEqualizerSetFromSettings(VLCApplication.getAppContext()))
mediaplayer.setVideoTitleDisplay(MediaPlayer.Position.Disable, 0)
if (mediaplayer.rate == 1.0f && settings.getBoolean(PreferencesActivity.KEY_PLAYBACK_SPEED_PERSIST, true))
setRate(settings.getFloat(PreferencesActivity.KEY_PLAYBACK_RATE, 1.0f), false)
mediaplayer.play()
}
@MainThread
fun restart() {
val mp = mediaplayer
mediaplayer = newMediaPlayer()
release(mp)
}
fun seek(position: Long, length: Double = getLength().toDouble()) {
if (length > 0.0) setPosition((position / length).toFloat())
else setTime(position)
}
fun setPosition(position: Float) {
if (seekable) mediaplayer.position = position
}
fun setTime(time: Long) {
if (seekable) mediaplayer.time = time
}
fun isPlaying() = mediaplayer.isPlaying
fun isVideoPlaying() = mediaplayer.vlcVout.areViewsAttached()
fun canSwitchToVideo() = mediaplayer.hasMedia() && mediaplayer.videoTracksCount > 0
fun getVideoTracksCount() = if (mediaplayer.hasMedia()) mediaplayer.videoTracksCount else 0
fun getVideoTracks() = mediaplayer.videoTracks
fun getVideoTrack() = mediaplayer.videoTrack
fun getCurrentVideoTrack() = mediaplayer.currentVideoTrack
fun getAudioTracksCount() = mediaplayer.audioTracksCount
fun getAudioTracks() = mediaplayer.audioTracks
fun getAudioTrack() = mediaplayer.audioTrack
fun setAudioTrack(index: Int) = mediaplayer.setAudioTrack(index)
fun getAudioDelay() = mediaplayer.audioDelay
fun getSpuDelay() = mediaplayer.spuDelay
fun getRate() = mediaplayer.rate
fun setSpuDelay(delay: Long) = mediaplayer.setSpuDelay(delay)
fun setVideoTrackEnabled(enabled: Boolean) = mediaplayer.setVideoTrackEnabled(enabled)
fun addSubtitleTrack(path: String, select: Boolean) = mediaplayer.addSlave(Media.Slave.Type.Subtitle, path, select)
fun addSubtitleTrack(uri: Uri, select: Boolean) = mediaplayer.addSlave(Media.Slave.Type.Subtitle, uri, select)
fun getSpuTracks() = mediaplayer.spuTracks
fun getSpuTrack() = mediaplayer.spuTrack
fun setSpuTrack(index: Int) = mediaplayer.setSpuTrack(index)
fun getSpuTracksCount() = mediaplayer.spuTracksCount
fun setAudioDelay(delay: Long) = mediaplayer.setAudioDelay(delay)
fun setEqualizer(equalizer: MediaPlayer.Equalizer) = mediaplayer.setEqualizer(equalizer)
@MainThread
fun setVideoScale(scale: Float) {
mediaplayer.scale = scale
}
fun setVideoAspectRatio(aspect: String?) {
mediaplayer.aspectRatio = aspect
}
fun setRenderer(renderer: RendererItem?) = mediaplayer.setRenderer(renderer)
fun release(player: MediaPlayer = mediaplayer) {
player.setEventListener(null)
player.setVideoTrackEnabled(false)
if (isVideoPlaying()) player.vlcVout.detachViews()
launch(playerContext) { player.release() }
}
fun setSlaves(media: MediaWrapper) = launch {
val list = MediaDatabase.getInstance().getSlaves(media.location)
for (slave in list) mediaplayer.addSlave(slave.type, Uri.parse(slave.uri), false)
}
private fun newMediaPlayer() : MediaPlayer {
return MediaPlayer(VLCInstance.get()).apply {
VLCOptions.getAout(VLCApplication.getSettings())?.let { setAudioOutput(it) }
setRenderer(RendererDelegate.selectedRenderer)
this.vlcVout.addCallback(this@PlayerController)
}
}
override fun onSurfacesCreated(vlcVout: IVLCVout?) {}
override fun onSurfacesDestroyed(vlcVout: IVLCVout?) {
switchToVideo = false
}
fun getTime() = if (mediaplayer.hasMedia()) mediaplayer.time else 0L
fun setRate(rate: Float, save: Boolean) {
mediaplayer.rate = rate
if (save && settings.getBoolean(PreferencesActivity.KEY_PLAYBACK_SPEED_PERSIST, true))
settings.edit().putFloat(PreferencesActivity.KEY_PLAYBACK_RATE, rate).apply()
}
/**
* Update current media meta and return true if player needs to be updated
*
* @param id of the Meta event received, -1 for none
* @return true if UI needs to be updated
*/
internal fun updateCurrentMeta(id: Int, mw: MediaWrapper?): Boolean {
if (id == Media.Meta.Publisher) return false
mw?.updateMeta(mediaplayer)
return id != Media.Meta.NowPlaying || mw?.nowPlaying !== null
}
fun getLength() = if (mediaplayer.hasMedia()) mediaplayer.length else 0L
fun setPreviousStats() {
val media = mediaplayer.media ?: return
previousMediaStats = media.stats
media.release()
}
fun updateViewpoint(yaw: Float, pitch: Float, roll: Float, fov: Float, absolute: Boolean) = mediaplayer.updateViewpoint(yaw, pitch, roll, fov, absolute)
fun navigate(where: Int) = mediaplayer.navigate(where)
fun getChapters(title: Int) = mediaplayer.getChapters(title)
fun getTitles() = mediaplayer.titles
fun getChapterIdx() = mediaplayer.chapter
fun setChapterIdx(chapter: Int) {
mediaplayer.chapter = chapter
}
fun getTitleIdx() = mediaplayer.title
fun setTitleIdx(title: Int) {
mediaplayer.title = title
}
fun getVolume() = mediaplayer.volume
fun setVolume(volume: Int) = mediaplayer.setVolume(volume)
override fun onEvent(event: MediaPlayer.Event?) {
if (event === null) return
when(event.type) {
MediaPlayer.Event.Playing -> playbackState = PlaybackStateCompat.STATE_PLAYING
MediaPlayer.Event.Paused -> playbackState = PlaybackStateCompat.STATE_PAUSED
MediaPlayer.Event.Stopped -> playbackState = PlaybackStateCompat.STATE_STOPPED
MediaPlayer.Event.PausableChanged -> pausable = event.pausable
MediaPlayer.Event.SeekableChanged -> seekable = event.seekable
}
mediaplayerEventListener?.onEvent(event)
}
}
\ No newline at end of file
package org.videolan.vlc.media
import android.content.Intent
import android.net.Uri
import android.support.annotation.MainThread
import android.support.v4.content.LocalBroadcastManager
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import kotlinx.coroutines.experimental.CoroutineStart
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.medialibrary.Medialibrary
import org.videolan.medialibrary.media.MediaWrapper
import org.videolan.vlc.*
import org.videolan.vlc.gui.preferences.PreferencesActivity
import org.videolan.vlc.gui.preferences.PreferencesFragment
import org.videolan.vlc.gui.video.VideoPlayerActivity
import org.videolan.vlc.util.*
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventListener, Media.EventListener {
private val TAG = "VLC/PlaylistManager"
private val PREVIOUS_LIMIT_DELAY = 5000L
private val AUDIO_REPEAT_MODE_KEY = "audio_repeat_mode"
private val medialibrary by lazy(LazyThreadSafetyMode.NONE) { Medialibrary.getInstance() }
val player by lazy(LazyThreadSafetyMode.NONE) { PlayerController() }
private val settings by lazy(LazyThreadSafetyMode.NONE) { VLCApplication.getSettings() }
private val ctx by lazy(LazyThreadSafetyMode.NONE) { VLCApplication.getAppContext() }
private val mediaList = MediaWrapperList()
var currentIndex = -1
private var nextIndex = -1
private var prevIndex = -1
private var previous = Stack<Int>()
var repeating = Constants.REPEAT_NONE
var shuffling = false
var videoBackground = false
private set
var isBenchmark = false
var isHardware = false
private var parsed = false
var savedTime = 0L
private var random = Random(System.currentTimeMillis())
private val expanding = AtomicBoolean(false)
fun hasMedia() = mediaList.size() != 0
fun hasCurrentMedia() = isValidPosition(currentIndex)
fun hasPlaylist() = mediaList.size() > 1
fun canShuffle() = mediaList.size() > 2
fun isValidPosition(position: Int) = position in 0 until mediaList.size()
/**
* Loads a selection of files (a non-user-supplied collection of media)
* into the primary or "currently playing" playlist.
*
* @param mediaPathList A list of locations to load
* @param position The position to start playing at
*/
@MainThread
fun loadLocations(mediaPathList: List<String>, position: Int) {
val mediaList = ArrayList<MediaWrapper>()
for (i in mediaPathList.indices) {
val location = mediaPathList[i]
var mediaWrapper = medialibrary.getMedia(location)
if (mediaWrapper == null) {
if (!location.validateLocation()) {
Log.w(TAG, "Invalid location " + location)
service.showToast(service.resources.getString(R.string.invalid_location, location), Toast.LENGTH_SHORT)
continue
}
Log.v(TAG, "Creating on-the-fly Media object for " + location)
mediaWrapper = MediaWrapper(Uri.parse(location))
}
mediaList.add(mediaWrapper)
}
load(mediaList, position)
}
fun load(list: List<MediaWrapper>, position: Int) {
mediaList.removeEventListener(this)
mediaList.clear()
previous.clear()
for (media in list) mediaList.add(media)
if (!hasMedia()) {
Log.w(TAG, "Warning: empty media list, nothing to play !")
return
}
currentIndex = if (isValidPosition(position)) position else 0
// Add handler after loading the list
mediaList.addEventListener(this)
playIndex(position)
onPlaylistLoaded()
}
@Volatile
private var loadingLastPlaylist = false
fun loadLastPlaylist(type: Int) {
if (loadingLastPlaylist) return
loadingLastPlaylist = true
launch(UI, CoroutineStart.UNDISPATCHED) {
val audio = type == Constants.PLAYLIST_TYPE_AUDIO
val currentMedia = settings.getString(if (audio) "current_song" else "current_media", "")
if ("" == currentMedia) return@launch
val locations = settings.getString(if (audio) "audio_list" else "media_list", "")!!.split(" ".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()
if (Util.isArrayEmpty(locations)) return@launch
val playList = async {
locations.map { Uri.decode(it) }
.mapTo(ArrayList<MediaWrapper>(locations.size)) { medialibrary.findMedia(MediaWrapper(Uri.parse(it))) }
}.await()
// load playlist
shuffling = settings.getBoolean(if (audio) "audio_shuffling" else "media_shuffling", false)
setRepeatType(settings.getInt(if (audio) "audio_repeating" else "media_repeating", Constants.REPEAT_NONE))
val position = settings.getInt(if (audio) "position_in_audio_list" else "position_in_media_list", 0)
savedTime = settings.getLong(if (audio) "position_in_song" else "position_in_media", -1)
if (!audio) {
if (position < playList.size && settings.getBoolean(PreferencesActivity.VIDEO_PAUSED, false)) {
playList[position].addFlags(MediaWrapper.MEDIA_PAUSED)
}
val rate = settings.getFloat(PreferencesActivity.VIDEO_SPEED, player.getRate())
if (rate != 1.0f) player.setRate(rate, false)
}
load(playList, position)
loadingLastPlaylist = false
}
}
private fun onPlaylistLoaded() {
service.onPlaylistLoaded()
saveMediaList()
savePosition(true)
saveCurrentMedia()
determinePrevAndNextIndices()
}
fun play() = player.play()
fun pause() {
if (player.pause()) savePosition()
}
@MainThread
fun next() {
val size = mediaList.size()
previous.push(currentIndex)
currentIndex = nextIndex
if (size == 0 || currentIndex < 0 || currentIndex >= size) {
Log.w(TAG, "Warning: invalid next index, aborted !")
//Close video player if started
LocalBroadcastManager.getInstance(ctx).sendBroadcast(Intent(Constants.EXIT_PLAYER))
player.stop()
return
}
videoBackground = !player.isVideoPlaying() && player.canSwitchToVideo()
playIndex(currentIndex)
}
fun stop(systemExit: Boolean = false) {
savePosition()
if (hasMedia()) saveMediaMeta()
player.releaseMedia()
mediaList.removeEventListener(this)
previous.clear()
currentIndex = -1
mediaList.clear()
if (systemExit) player.release() else player.restart()
medialibrary.resumeBackgroundOperations()
service.onPlaybackStopped()
if (!systemExit) service.hideNotification()
}
@MainThread
fun previous(force : Boolean) {
if (hasPrevious() && currentIndex > 0 &&
(force || !player.seekable || player.getTime() < PREVIOUS_LIMIT_DELAY)) {
val size = mediaList.size()
currentIndex = prevIndex
if (previous.size > 0) previous.pop()
if (size == 0 || prevIndex < 0 || currentIndex >= size) {
Log.w(TAG, "Warning: invalid previous index, aborted !")
player.stop()
return
}
playIndex(currentIndex)
} else player.setPosition(0F)
}
fun shuffle() {
if (shuffling) previous.clear()
shuffling = !shuffling
savePosition()
determinePrevAndNextIndices()
}
fun setRepeatType(repeatType: Int) {
repeating = repeatType
if (mediaList.isAudioList && settings.getBoolean("audio_save_repeat", false))
settings.edit().putInt(AUDIO_REPEAT_MODE_KEY, repeating).apply()
savePosition()
determinePrevAndNextIndices()
}
fun playIndex(index: Int, flags: Int = 0) {
if (mediaList.size() == 0) {
Log.w(TAG, "Warning: empty media list, nothing to play !")
return
}
currentIndex = if (isValidPosition(index)) {
index
} else {
Log.w(TAG, "Warning: index $index out of bounds")
0
}
val mw = mediaList.getMedia(index) ?: return
val isVideoPlaying = mw.type == MediaWrapper.TYPE_VIDEO && player.isVideoPlaying()
if (!videoBackground && isVideoPlaying) mw.addFlags(MediaWrapper.MEDIA_VIDEO)
if (videoBackground) mw.addFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
parsed = false
player.switchToVideo = false
if (TextUtils.equals(mw.uri.scheme, "content")) MediaUtils.retrieveMediaTitle(mw)
if (mw.hasFlag(MediaWrapper.MEDIA_FORCE_AUDIO) && player.getAudioTracksCount() == 0) {
next()
} else if (mw.type != MediaWrapper.TYPE_VIDEO || isVideoPlaying || mw.hasFlag(MediaWrapper.MEDIA_FORCE_AUDIO)
|| RendererDelegate.selectedRenderer !== null) {
val media = Media(VLCInstance.get(), FileUtils.getUri(mw.uri))
VLCOptions.setMediaOptions(media, ctx, flags or mw.flags)
/* keeping only video during benchmark */
if (isBenchmark) {