Commit 8689ed8d authored by Geoffrey Métais's avatar Geoffrey Métais
Browse files

TV: Oreo home screen preview

parent b04f1d58
......@@ -29,7 +29,7 @@ class FileProvider : ContentProvider() {
override fun openFile(uri: Uri, mode: String?): ParcelFileDescriptor {
val file = File(uri.path)
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
}
throw FileNotFoundException(uri.path)
}
......
......@@ -20,6 +20,7 @@
*****************************************************************************/
package videolan.org.commontools
import android.content.ComponentName
import android.content.ContentUris
import android.content.Context
import android.content.SharedPreferences
......@@ -108,7 +109,12 @@ fun createUri(appId: String, id: String? = null) : Uri {
return builder.build()
}
fun buildProgram(program: ProgramDesc) : PreviewProgram {
fun buildProgram(cn: ComponentName, program: ProgramDesc) : PreviewProgram {
val previewProgramVideoUri = TvContractCompat.buildPreviewProgramUri(program.id)
.buildUpon()
.appendQueryParameter("input", TvContractCompat.buildInputId(cn))
.build()
val stringId = program.id.toString()
return PreviewProgram.Builder()
.setChannelId(program.channelId)
.setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
......@@ -120,12 +126,18 @@ fun buildProgram(program: ProgramDesc) : PreviewProgram {
.setDescription(program.description)
.setPosterArtUri(program.artUri)
.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_16_9)
.setIntentUri(createUri(program.appId, program.id))
.setInternalProviderId(program.id)
.setIntentUri(createUri(program.appId, stringId))
.setInternalProviderId(stringId)
.setPreviewVideoUri(previewProgramVideoUri)
.build()
}
fun buildWatchNextProgram(program: ProgramDesc) : WatchNextProgram {
fun buildWatchNextProgram(cn: ComponentName, program: ProgramDesc) : WatchNextProgram {
val previewProgramVideoUri = TvContractCompat.buildPreviewProgramUri(program.id)
.buildUpon()
.appendQueryParameter("input", TvContractCompat.buildInputId(cn))
.build()
val stringId = program.id.toString()
return WatchNextProgram.Builder()
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
......@@ -138,8 +150,9 @@ fun buildWatchNextProgram(program: ProgramDesc) : WatchNextProgram {
.setDescription(program.description)
.setPosterArtUri(program.artUri)
.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_16_9)
.setIntentUri(createUri(program.appId, program.id))
.setInternalProviderId(program.id)
.setIntentUri(createUri(program.appId, stringId))
.setInternalProviderId(stringId)
.setPreviewVideoUri(previewProgramVideoUri)
.build()
}
......@@ -167,9 +180,9 @@ fun ProgramsList.indexOfId(id: Long) : Int {
return -1
}
class ProgramDesc(
data class ProgramDesc(
val channelId: Long,
val id: String,
val id: Long,
val title: String,
val description: String?,
val artUri: Uri,
......@@ -177,4 +190,5 @@ class ProgramDesc(
val time: Int,
val width: Int,
val height: Int,
val appId: String)
\ No newline at end of file
val appId: String,
val previewVideoUri: Uri? = null)
\ No newline at end of file
......@@ -616,6 +616,15 @@
android:exported="true"
android:grantUriPermissions="true">
</provider>
<service android:name=".PreviewVideoInputService"
android:permission="android.permission.BIND_TV_INPUT">
<intent-filter>
<action android:name="android.media.tv.TvInputService" />
</intent-filter>
<meta-data
android:name="android.media.tv.input"
android:resource="@xml/previewinputservice" />
</service>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<tv-input/>
\ No newline at end of file
package org.videolan.vlc
import android.content.Context
import android.media.tv.TvInputManager
import android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
import android.media.tv.TvInputService
import android.net.Uri
import android.util.Log
import android.view.Surface
import kotlinx.coroutines.experimental.CoroutineStart
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.medialibrary.Medialibrary
import org.videolan.vlc.media.MediaPlayerEventListener
import org.videolan.vlc.media.PlayerController
import org.videolan.vlc.util.VLCIO
import org.videolan.vlc.util.VLCInstance
import org.videolan.vlc.util.random
import java.io.IOException
import kotlin.coroutines.experimental.suspendCoroutine
private const val TAG = "PreviewInputService"
class PreviewVideoInputService : TvInputService() {
override fun onCreateSession(inputId: String): TvInputService.Session? {
return PreviewSession(this)
}
private inner class PreviewSession(private val context: Context
) : TvInputService.Session(context), MediaPlayerEventListener {
val player by lazy(LazyThreadSafetyMode.NONE) { PlayerController() }
override fun onRelease() {
player.release()
}
override fun onTune(uri: Uri): Boolean {
notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING)
val id = uri.lastPathSegment.toLong()
launch(UI, CoroutineStart.UNDISPATCHED) {
val mw = getMedia(id)
if (mw == null) {
Log.w(TAG, "Could not find video $id")
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)
return@launch
}
try {
val media = Media(VLCInstance.get(), mw.uri)
val start = if (mw.length <= 0L) 0 else mw.length.random()/1000
media.addOption(":start-time=$start")
player.getVout()?.apply {
setVideoSurface(surface, null)
attachViews(null)
setWindowSize(width, height)
}
player.setVideoAspectRatio(null)
player.setVideoScale(0f)
player.startPlayback(media, this@PreviewSession)
notifyVideoAvailable()
} catch (e: IOException) {
Log.e(TAG, "Could not prepare media player", e)
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)
}
}
return true
}
private suspend fun getMedia(id: Long) = withContext(VLCIO) {
val ml = Medialibrary.getInstance()
if (ml.isInitiated) ml.getMedia(id)
else suspendCoroutine { continuation ->
ml.addOnMedialibraryReadyListener(object : Medialibrary.OnMedialibraryReadyListener {
override fun onMedialibraryReady() {
ml.removeOnMedialibraryReadyListener(this)
continuation.resume(ml.getMedia(id))
}
override fun onMedialibraryIdle() {}
})
this@PreviewVideoInputService.startMedialibrary(false, false, false)
}
}
private var width = 0
private var height = 0
private lateinit var surface: Surface
override fun onSetSurface(surface: Surface?): Boolean {
if (surface == null) return false
this.surface = surface
return true
}
override fun onOverlayViewSizeChanged(width: Int, height: Int) {
this.width = width
this.height = height
}
override fun onSetStreamVolume(volume: Float) {
player.setVolume((volume*100).toInt())
}
override fun onSetCaptionEnabled(enabled: Boolean) {}
override suspend fun onEvent(event: MediaPlayer.Event) {
when(event.type) {
MediaPlayer.Event.EndReached -> player.release()
}
}
}
}
\ No newline at end of file
......@@ -68,8 +68,8 @@ class PlayerController : IVLCVout.Callback, MediaPlayer.EventListener {
it.release()
}
private var mediaplayerEventListener: MediaPLayerEventListener? = null
internal fun startPlayback(media: Media, listener: MediaPLayerEventListener) {
private var mediaplayerEventListener: MediaPlayerEventListener? = null
internal fun startPlayback(media: Media, listener: MediaPlayerEventListener) {
mediaplayerEventListener = listener
resetPlaybackState(media.duration)
mediaplayer.setEventListener(null)
......@@ -329,6 +329,6 @@ class PlayerController : IVLCVout.Callback, MediaPlayer.EventListener {
class Progress(var time: Long = 0L, var length: Long = 0L)
internal interface MediaPLayerEventListener {
internal interface MediaPlayerEventListener {
suspend fun onEvent(event: MediaPlayer.Event)
}
\ No newline at end of file
......@@ -26,7 +26,6 @@ import org.videolan.vlc.VLCApplication
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.media.PlaylistManager.Companion.hasMedia
import org.videolan.vlc.util.*
import java.util.*
......@@ -657,7 +656,7 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
}
}
private val mediaplayerEventListener = object : MediaPLayerEventListener {
private val mediaplayerEventListener = object : MediaPlayerEventListener {
override suspend fun onEvent(event: MediaPlayer.Event) {
when (event.type) {
MediaPlayer.Event.Playing -> {
......
......@@ -8,7 +8,6 @@ import android.support.v4.app.FragmentActivity
import android.support.v7.preference.PreferenceManager
import kotlinx.coroutines.experimental.delay
import org.videolan.libvlc.Media
import org.videolan.vlc.VLCApplication
import java.io.File
import java.net.URI
import java.net.URISyntaxException
......@@ -53,4 +52,6 @@ fun Media?.canExpand() = this != null && (type == Media.Type.Directory || type =
fun Context.getAppSystemService(name: String) = applicationContext.getSystemService(name)
fun Context.getPreferences() = PreferenceManager.getDefaultSharedPreferences(this)
\ No newline at end of file
fun Context.getPreferences() = PreferenceManager.getDefaultSharedPreferences(this)
fun Long.random() = (Random().nextFloat() * this).toLong()
\ No newline at end of file
......@@ -20,6 +20,7 @@
*****************************************************************************/
package org.videolan.vlc.util
import android.content.ComponentName
import android.content.Context
import android.database.Cursor
import android.net.Uri
......@@ -34,16 +35,12 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.videolan.medialibrary.Medialibrary
import org.videolan.medialibrary.media.MediaWrapper
import org.videolan.vlc.*
import org.videolan.vlc.BuildConfig
import org.videolan.vlc.R
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.startMedialibrary
import videolan.org.commontools.*
private const val TAG = "VLC/TvChannels"
private const val MAX_RECOMMENDATIONS = 3
......@@ -75,6 +72,7 @@ suspend fun updatePrograms(context: Context, channelId: Long) {
val programs = withContext(VLCIO) { existingPrograms(context, channelId) }
val videoList = withContext(VLCIO) { VLCApplication.getMLInstance().recentVideos }
if (Util.isArrayEmpty(videoList)) return
val cn = ComponentName(context, PreviewVideoInputService::class.java)
for ((count, mw) in videoList.withIndex()) {
if (mw == null) continue
val index = programs.indexOfId(mw.id)
......@@ -82,11 +80,13 @@ suspend fun updatePrograms(context: Context, channelId: Long) {
programs.removeAt(index)
continue
}
val desc = ProgramDesc(channelId, mw.id.toString(), mw.title, mw.description,
val desc = ProgramDesc(channelId, mw.id, mw.title, mw.description,
mw.artUri(), mw.length.toInt(), mw.time.toInt(),
mw.width, mw.height, BuildConfig.APPLICATION_ID)
val program = buildProgram(desc)
launch(VLCIO) { context.contentResolver.insert(TvContractCompat.PreviewPrograms.CONTENT_URI, program.toContentValues()) }
val program = buildProgram(cn, desc)
launch(VLCIO) {
context.contentResolver.insert(TvContractCompat.PreviewPrograms.CONTENT_URI, program.toContentValues())
}
if (count - programs.size >= MAX_RECOMMENDATIONS) break
}
for (program in programs) {
......@@ -121,10 +121,11 @@ fun setResumeProgram(context: Context, mw: MediaWrapper) {
}
}
if (!isProgramPresent && mw.time != 0L) {
val desc = ProgramDesc(0L, mw.id.toString(), mw.title, mw.description,
val desc = ProgramDesc(0L, mw.id, mw.title, mw.description,
mw.artUri(), mw.length.toInt(), mw.time.toInt(),
mw.width, mw.height, BuildConfig.APPLICATION_ID)
val program = buildWatchNextProgram(desc)
val cn = ComponentName(context, PreviewVideoInputService::class.java)
val program = buildWatchNextProgram(cn, desc)
val watchNextProgramUri = context.contentResolver.insert(TvContractCompat.WatchNextPrograms.CONTENT_URI, program.toContentValues())
if (watchNextProgramUri == null || watchNextProgramUri == Uri.EMPTY) Log.e(TAG, "Insert watch next program failed")
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment