1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Added support for DPAD left/right to skip and back button to hide controller. Added error message and loading overlay. Added demo mode (tap top-left 5 times).

This commit is contained in:
Koen 2023-08-16 10:48:50 +02:00
parent 10afb65456
commit 72bc635dfd
6 changed files with 180 additions and 35 deletions

View file

@ -36,7 +36,7 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene
private var _bytesRead = 0
private var _packetLength = 0
private var _state = SessionState.WaitingForLength
private var _outputStream: DataOutputStream? = DataOutputStream(_socket.outputStream);
private var _outputStream: DataOutputStream? = DataOutputStream(_socket.outputStream)
fun sendPlaybackUpdate(value: PlaybackUpdateMessage) {
send(Opcode.PlaybackUpdate, value)
@ -48,40 +48,40 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene
private inline fun <reified T> send(opcode: Opcode, message: T) {
try {
val data: ByteArray;
var jsonString: String? = null;
val data: ByteArray
var jsonString: String? = null
if (message != null) {
jsonString = Json.encodeToString(message);
data = jsonString.encodeToByteArray();
jsonString = Json.encodeToString(message)
data = jsonString.encodeToByteArray()
} else {
data = ByteArray(0);
data = ByteArray(0)
}
val size = 1 + data.size;
val outputStream = _outputStream;
val size = 1 + data.size
val outputStream = _outputStream
if (outputStream == null) {
Log.w(TAG, "Failed to send $size bytes, output stream is null.");
return;
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
return
}
val serializedSizeLE = ByteArray(4);
serializedSizeLE[0] = (size and 0xff).toByte();
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
outputStream.write(serializedSizeLE);
val serializedSizeLE = ByteArray(4)
serializedSizeLE[0] = (size and 0xff).toByte()
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
outputStream.write(serializedSizeLE)
val opcodeBytes = ByteArray(1);
opcodeBytes[0] = opcode.value;
outputStream.write(opcodeBytes);
val opcodeBytes = ByteArray(1)
opcodeBytes[0] = opcode.value
outputStream.write(opcodeBytes)
if (data.isNotEmpty()) {
outputStream.write(data);
outputStream.write(data)
}
Log.d(TAG, "Sent $size bytes: '$jsonString'.");
Log.d(TAG, "Sent $size bytes: '$jsonString'.")
} catch (e: Throwable) {
Log.i(TAG, "Failed to send message.", e);
Log.i(TAG, "Failed to send message.", e)
}
}

View file

@ -47,8 +47,11 @@ class MainActivity : AppCompatActivity() {
private lateinit var _imageSpinner: ImageView
private lateinit var _layoutConnectionInfo: ConstraintLayout
private lateinit var _videoBackground: StyledPlayerView
private lateinit var _viewDemo: View
private lateinit var _player: ExoPlayer
private var _updating: Boolean = false
private var _demoClickCount = 0
private var _lastDemoToast: Toast? = null
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
@ -64,6 +67,7 @@ class MainActivity : AppCompatActivity() {
_imageSpinner = findViewById(R.id.image_spinner)
_layoutConnectionInfo = findViewById(R.id.layout_connection_info)
_videoBackground = findViewById(R.id.video_background)
_viewDemo = findViewById(R.id.view_demo)
startVideo()
startAnimations()
@ -80,6 +84,18 @@ class MainActivity : AppCompatActivity() {
update()
}
_viewDemo.setOnClickListener {
_demoClickCount++
if (_demoClickCount in 2..4) {
val remainingClicks = 5 - _demoClickCount
_lastDemoToast?.cancel()
_lastDemoToast = Toast.makeText(this, "Click $remainingClicks more times to start demo", Toast.LENGTH_SHORT).apply { show() }
} else if (_demoClickCount == 5) {
TcpListenerService.instance?.onCastPlay(PlayMessage("video/mp4", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"))
_demoClickCount = 0
}
}
if (BuildConfig.IS_PLAYSTORE_VERSION) {
_text.visibility = View.INVISIBLE
_buttonUpdate.visibility = View.INVISIBLE
@ -249,15 +265,15 @@ class MainActivity : AppCompatActivity() {
}
private suspend fun checkForUpdates() {
Log.i(TAG, "Checking for updates...");
Log.i(TAG, "Checking for updates...")
withContext(Dispatchers.IO) {
try {
val latestVersion = downloadVersionCode()
if (latestVersion != null) {
val currentVersion = BuildConfig.VERSION_CODE;
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.");
val currentVersion = BuildConfig.VERSION_CODE
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.")
if (latestVersion > currentVersion) {
withContext(Dispatchers.Main) {
@ -267,8 +283,8 @@ class MainActivity : AppCompatActivity() {
setText(resources.getText(R.string.there_is_an_update_available_do_you_wish_to_update))
_buttonUpdate.visibility = View.VISIBLE
} catch (e: Throwable) {
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show();
Log.w(TAG, "Error occurred in update dialog.");
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show()
Log.w(TAG, "Error occurred in update dialog.")
}
}
} else {
@ -277,21 +293,21 @@ class MainActivity : AppCompatActivity() {
_buttonUpdate.visibility = View.INVISIBLE
//setText(getString(R.string.no_updates_available))
setText(null)
//Toast.makeText(this@MainActivity, "Already on latest version", Toast.LENGTH_LONG).show();
//Toast.makeText(this@MainActivity, "Already on latest version", Toast.LENGTH_LONG).show()
}
}
} else {
Log.w(TAG, "Failed to retrieve version from version URL.");
Log.w(TAG, "Failed to retrieve version from version URL.")
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show();
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show()
}
}
} catch (e: Throwable) {
Log.w(TAG, "Failed to check for updates.", e);
Log.w(TAG, "Failed to check for updates.", e)
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show();
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show()
}
}
}
@ -452,7 +468,7 @@ class MainActivity : AppCompatActivity() {
addr.hostAddress?.let { ips.add(it) }
}
}
return ips;
return ips
}
companion object {

View file

@ -1,18 +1,25 @@
package com.futo.fcast.receiver
import android.content.Context
import android.graphics.drawable.Animatable
import android.net.*
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.Window
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.text.ExoplayerCuesDecoder
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSource
@ -20,14 +27,18 @@ import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import kotlin.math.abs
import kotlin.math.max
class PlayerActivity : AppCompatActivity() {
private lateinit var _playerControlView: StyledPlayerView
private lateinit var _imageSpinner: ImageView
private lateinit var _textMessage: TextView
private lateinit var _layoutOverlay: ConstraintLayout
private lateinit var _exoPlayer: ExoPlayer
private var _shouldPlaybackRestartOnConnectivity: Boolean = false
private lateinit var _connectivityManager: ConnectivityManager
private lateinit var _scope: CoroutineScope
private var _wasPlaying = false;
private var _wasPlaying = false
val currentPosition get() = _exoPlayer.currentPosition
val isPlaying get() = _exoPlayer.isPlaying
@ -59,16 +70,25 @@ class PlayerActivity : AppCompatActivity() {
private val _playerEventListener = object: Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
Log.i(TAG, "onPlaybackStateChanged playbackState=$playbackState")
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
Log.i(TAG, "_shouldPlaybackRestartOnConnectivity=false")
_shouldPlaybackRestartOnConnectivity = false
}
if (playbackState == ExoPlayer.STATE_READY) {
setStatus(false, null)
} else if (playbackState == ExoPlayer.STATE_BUFFERING) {
setStatus(true, null)
}
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
Log.e(TAG, "onPlayerError: $error")
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
@ -83,6 +103,8 @@ class PlayerActivity : AppCompatActivity() {
_shouldPlaybackRestartOnConnectivity = true
}
}
setStatus(false, getFullExceptionMessage(error))
}
override fun onVolumeChanged(volume: Float) {
@ -107,8 +129,13 @@ class PlayerActivity : AppCompatActivity() {
setFullScreen()
_playerControlView = findViewById(R.id.player_control_view)
_imageSpinner = findViewById(R.id.image_spinner)
_textMessage = findViewById(R.id.text_message)
_layoutOverlay = findViewById(R.id.layout_overlay)
_scope = CoroutineScope(Dispatchers.Main)
setStatus(true, null)
val trackSelector = DefaultTrackSelector(this)
trackSelector.parameters = trackSelector.parameters
.buildUpon()
@ -148,6 +175,35 @@ class PlayerActivity : AppCompatActivity() {
if (hasFocus) setFullScreen()
}
private fun getFullExceptionMessage(ex: Throwable): String {
val messages = mutableListOf<String>()
var current: Throwable? = ex
while (current != null) {
messages.add(current.message ?: "Unknown error")
current = current.cause
}
return messages.joinToString(separator = "")
}
private fun setStatus(isLoading: Boolean, message: String?) {
if (isLoading) {
(_imageSpinner.drawable as Animatable?)?.start()
_imageSpinner.visibility = View.VISIBLE
} else {
(_imageSpinner.drawable as Animatable?)?.stop()
_imageSpinner.visibility = View.GONE
}
if (message != null) {
_textMessage.visibility = View.VISIBLE
_textMessage.text = message
} else {
_textMessage.visibility = View.GONE
}
_layoutOverlay.visibility = if (isLoading || message != null) View.VISIBLE else View.GONE
}
private fun setFullScreen() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.hide(WindowInsets.Type.statusBars())
@ -189,6 +245,28 @@ class PlayerActivity : AppCompatActivity() {
TcpListenerService.activityCount--
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
val newPosition = _exoPlayer.currentPosition - 10000
_exoPlayer.seekTo(max(0, newPosition))
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
val newPosition = _exoPlayer.currentPosition + 10000
_exoPlayer.seekTo(newPosition)
return true
}
KeyEvent.KEYCODE_BACK -> {
if (_playerControlView.isControllerFullyVisible) {
_playerControlView.hideController()
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
fun play(playMessage: PlayMessage) {
val mediaItemBuilder = MediaItem.Builder()
if (playMessage.container.isNotEmpty()) {
@ -225,6 +303,7 @@ class PlayerActivity : AppCompatActivity() {
_exoPlayer.seekTo(playMessage.time * 1000)
}
setStatus(true, null)
_wasPlaying = false
_exoPlayer.playWhenReady = true
_exoPlayer.prepare()
@ -250,5 +329,8 @@ class PlayerActivity : AppCompatActivity() {
companion object {
var instance: PlayerActivity? = null
private const val TAG = "PlayerActivity"
private const val SEEK_BACKWARD_MILLIS = 10_000
private const val SEEK_FORWARD_MILLIS = 10_000
}
}

View file

@ -108,7 +108,7 @@ class TcpListenerService : Service() {
_discoveryService = null
_serverSocket?.close()
_serverSocket = null;
_serverSocket = null
_listenThread?.join()
_listenThread = null

View file

@ -14,6 +14,13 @@
android:layout_height="match_parent"
app:resize_mode="zoom" />
<View
android:id="@+id/view_demo"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"

View file

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black">
<com.google.android.exoplayer2.ui.StyledPlayerView
@ -11,4 +12,43 @@
android:layout_height="match_parent"
android:keepScreenOn="true"
app:use_controller="true" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#66000000"
android:clickable="false">
<ImageView
android:id="@+id/image_spinner"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginStart="8dp"
android:alpha="0.5"
app:srcCompat="@drawable/ic_loader_animated"
android:clickable="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/text_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
tools:text="This is a test message"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:textSize="16sp"
android:clickable="false"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>