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:
parent
10afb65456
commit
72bc635dfd
6 changed files with 180 additions and 35 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -108,7 +108,7 @@ class TcpListenerService : Service() {
|
|||
_discoveryService = null
|
||||
|
||||
_serverSocket?.close()
|
||||
_serverSocket = null;
|
||||
_serverSocket = null
|
||||
|
||||
_listenThread?.join()
|
||||
_listenThread = null
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue