/* * Copyright (C) 2020 Stefan Schüller * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package net.schueller.peertube.service import android.annotation.SuppressLint import android.app.Notification import net.schueller.peertube.helper.MetaDataHelper.getMetaString import net.schueller.peertube.model.Video.Companion.getMediaDescription import android.os.IBinder import com.google.android.exoplayer2.ui.PlayerNotificationManager import android.content.IntentFilter import android.media.AudioManager import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import android.media.session.PlaybackState import android.content.Intent import android.widget.Toast import net.schueller.peertube.helper.APIUrlHelper import net.schueller.peertube.network.UnsafeOkHttpClient import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter import android.app.PendingIntent import android.app.Service import net.schueller.peertube.activity.VideoPlayActivity import net.schueller.peertube.activity.VideoListActivity import android.graphics.Bitmap import android.support.v4.media.session.MediaSessionCompat import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import android.support.v4.media.MediaDescriptionCompat import android.content.BroadcastReceiver import android.content.Context import android.net.Uri import android.os.Binder import android.os.Build import android.util.Log import android.webkit.URLUtil import androidx.core.app.NotificationCompat import com.google.android.exoplayer2.* import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.ui.PlayerNotificationManager.NotificationListener import net.schueller.peertube.R.drawable import net.schueller.peertube.R.string import net.schueller.peertube.model.Video import java.lang.Exception class VideoPlayerService : Service() { private val mBinder: IBinder = LocalBinder() @JvmField var player: ExoPlayer? = null private var currentVideo: Video? = null private var currentStreamUrl: String? = null private var currentStreamUrlIsHLS = false private var playerNotificationManager: PlayerNotificationManager? = null private val becomeNoisyIntentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver() override fun onCreate() { Log.v(TAG, "onCreate...") super.onCreate() player = ExoPlayer.Builder(applicationContext) .setTrackSelector(DefaultTrackSelector(applicationContext)) .build() // Stop player if audio device changes, e.g. headphones unplugged player!!.addListener(object : Player.Listener { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { if (playbackState.toLong() == PlaybackState.ACTION_PAUSE) { // this means that pause is available, hence the audio is playing Log.v(TAG, "ACTION_PLAY: $playbackState") registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter) } if (playbackState .toLong() == PlaybackState.ACTION_PLAY ) { // this means that play is available, hence the audio is paused or stopped Log.v(TAG, "ACTION_PAUSE: $playbackState") safeUnregisterReceiver() } } }) } inner class LocalBinder : Binder() { // Return this instance of VideoPlayerService so clients can call public methods val service: VideoPlayerService get() =// Return this instance of VideoPlayerService so clients can call public methods this@VideoPlayerService } override fun onDestroy() { Log.v(TAG, "onDestroy...") if (playerNotificationManager != null) { playerNotificationManager!!.setPlayer(null) } //Was seeing an error when exiting the program about not unregistering the receiver. safeUnregisterReceiver() if (player != null) { player!!.release() player = null } super.onDestroy() } private fun safeUnregisterReceiver() { try { unregisterReceiver(myNoisyAudioStreamReceiver) } catch (e: Exception) { Log.e("VideoPlayerService", "attempted to unregister a non-registered service") } } override fun onBind(intent: Intent): IBinder { return mBinder } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val context: Context = this Log.v(TAG, "onStartCommand...") return if (!URLUtil.isValidUrl(currentStreamUrl)) { Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show() START_NOT_STICKY } else { playVideo() START_STICKY } } fun setCurrentVideo(video: Video?) { Log.v(TAG, "setCurrentVideo...") currentVideo = video } fun setCurrentStreamUrl(streamUrl: String, isHLS: Boolean) { Log.v(TAG, "setCurrentStreamUrl...$streamUrl") currentStreamUrlIsHLS = isHLS currentStreamUrl = streamUrl } /** * Returns the current playback speed of the player. * * @return the current playback speed of the player. *///Playback speed control var playBackSpeed: Float get() = player!!.playbackParameters.speed set(speed) { Log.v(TAG, "setPlayBackSpeed...") player!!.playbackParameters = PlaybackParameters(speed) } private fun playVideo() { val context: Context = this // We need a valid URL Log.v(TAG, "playVideo...") // Produces DataSource instances through which media data is loaded. val okhttpClientBuilder: okhttp3.OkHttpClient.Builder = if (!APIUrlHelper.useInsecureConnection(this)) { okhttp3.OkHttpClient.Builder() } else { UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder() } // Create a data source factory. val dataSourceFactory: OkHttpDataSource.Factory = OkHttpDataSource.Factory( okhttpClientBuilder.build() ) // Create a progressive media source pointing to a stream uri. val mediaSource: MediaSource = if (currentStreamUrlIsHLS) { HlsMediaSource.Factory(dataSourceFactory) .createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl))) } else { ProgressiveMediaSource.Factory(dataSourceFactory) .createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl))) } // Set the media source to be played. player!!.setMediaSource(mediaSource) // Prepare the player. player!!.prepare() // Auto play player!!.playWhenReady = true //set playback speed to global default val sharedPref = getSharedPreferences( packageName + "_preferences", Context.MODE_PRIVATE ) val speed = sharedPref.getString(getString(string.pref_video_speed_key), "1.0")!!.toFloat() playBackSpeed = speed playerNotificationManager = PlayerNotificationManager.Builder( this, PLAYBACK_NOTIFICATION_ID, PLAYBACK_CHANNEL_ID, ).setMediaDescriptionAdapter( object : MediaDescriptionAdapter { override fun getCurrentContentTitle(player: Player): CharSequence { return currentVideo!!.name } @SuppressLint("UnspecifiedImmutableFlag") override fun createCurrentContentIntent(player: Player): PendingIntent? { val intent = Intent(context, VideoPlayActivity::class.java) intent.putExtra(VideoListActivity.EXTRA_VIDEOID, currentVideo!!.uuid) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) else PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } override fun getCurrentContentText(player: Player): CharSequence { return getMetaString( currentVideo!!.createdAt, currentVideo!!.views, baseContext ) } override fun getCurrentSubText(player: Player): CharSequence { return ""} override fun getCurrentLargeIcon( player: Player, callback: PlayerNotificationManager.BitmapCallback ): Bitmap? { return null } } ).setNotificationListener( object : NotificationListener { override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) { super.onNotificationPosted(notificationId, notification, ongoing) if (ongoing) // allow notification to be dismissed if player is stopped startForeground(notificationId, notification) else stopForeground(false) } override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { super.onNotificationCancelled(notificationId, dismissedByUser) stopSelf() stopForeground(true) } } ).setChannelNameResourceId(string.playback_channel_name) .setChannelDescriptionResourceId(string.playback_channel_description) .build() playerNotificationManager!!.setPriority(NotificationCompat.PRIORITY_DEFAULT) playerNotificationManager!!.setSmallIcon(drawable.ic_logo_bw) // don't show skip buttons in notification playerNotificationManager!!.setUseNextAction(false) playerNotificationManager!!.setUsePreviousAction(false) playerNotificationManager!!.setPlayer(player) // external Media control, Android Wear / Google Home etc. val mediaSession = MediaSessionCompat(context, MEDIA_SESSION_TAG) mediaSession.isActive = true playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken) val mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) { override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat { return getMediaDescription(currentVideo!!) } }) mediaSessionConnector.setPlayer(player) // Audio Focus val audioAttributes = AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_MOVIE) .build() player!!.setAudioAttributes(audioAttributes, true) } // pause playback on audio output change private inner class BecomingNoisyReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) { player!!.playWhenReady = false } } } companion object { private const val TAG = "VideoPlayerService" private const val MEDIA_SESSION_TAG = "peertube_player" private const val PLAYBACK_CHANNEL_ID = "playback_channel" private const val PLAYBACK_NOTIFICATION_ID = 1 } }