Added casting controls to connected dialog.

This commit is contained in:
Koen 2023-12-13 15:02:49 +01:00
parent 0432f06eb3
commit 2ac8e0e621
10 changed files with 306 additions and 58 deletions

View file

@ -11,12 +11,27 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.dialogs.*
import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
import com.futo.platformplayer.dialogs.CastingAddDialog
import com.futo.platformplayer.dialogs.CastingHelpDialog
import com.futo.platformplayer.dialogs.ChangelogDialog
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.dialogs.ConnectedCastingDialog
import com.futo.platformplayer.dialogs.ImportDialog
import com.futo.platformplayer.dialogs.ImportOptionsDialog
import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
@ -341,11 +356,17 @@ class UIDialogs {
val d = StateCasting.instance.activeDevice;
if (d != null) {
val dialog = ConnectedCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
} else {
val dialog = ConnectCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
val c = context
if (c is Activity) {

View file

@ -56,7 +56,8 @@ class AirPlayCastingDevice : CastingDevice {
Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition;
setTime(resumePosition);
setDuration(duration);
if (resumePosition > 0.0) {
val pos = resumePosition / duration;
Logger.i(TAG, "resumePosition: $resumePosition, duration: ${duration}, pos: $pos")
@ -170,8 +171,16 @@ class AirPlayCastingDevice : CastingDevice {
}
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
setTime(progress);
time = progress;
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
if (durationIndex == -1) {
continue;
}
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
setDuration(duration);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
}
@ -196,7 +205,7 @@ class AirPlayCastingDevice : CastingDevice {
}
override fun changeSpeed(speed: Double) {
this.speed = speed
setSpeed(speed)
post("rate?value=$speed")
}

View file

@ -1,7 +1,6 @@
package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@ -11,7 +10,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.InetAddress
import java.time.OffsetDateTime
enum class CastConnectionState {
DISCONNECTED,
@ -59,36 +57,58 @@ abstract class CastingDevice {
onPlayChanged.emit(value);
}
};
var timeReceivedAt: OffsetDateTime = OffsetDateTime.now()
private set;
private var lastTimeChangeTime_ms: Long = 0
var time: Double = 0.0
set(value) {
val changed = value != field;
field = value;
if (changed) {
timeReceivedAt = OffsetDateTime.now();
onTimeChanged.emit(value);
}
};
private set
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
time = value
lastTimeChangeTime_ms = changeTime_ms
onTimeChanged.emit(value)
}
}
private var lastDurationChangeTime_ms: Long = 0
var duration: Double = 0.0
private set
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
duration = value
lastDurationChangeTime_ms = changeTime_ms
onDurationChanged.emit(value)
}
}
private var lastVolumeChangeTime_ms: Long = 0
var volume: Double = 1.0
set(value) {
val changed = value != field;
field = value;
if (changed) {
onVolumeChanged.emit(value);
}
};
private set
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
volume = value
lastVolumeChangeTime_ms = changeTime_ms
onVolumeChanged.emit(value)
}
}
private var lastSpeedChangeTime_ms: Long = 0
var speed: Double = 1.0
set(value) {
val changed = value != field;
field = value;
if (changed) {
onSpeedChanged.emit(value);
}
};
private set
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
speed = value
lastSpeedChangeTime_ms = changeTime_ms
onSpeedChanged.emit(value)
}
}
val expectedCurrentTime: Double
get() {
val diff = timeReceivedAt.getNowDiffMiliseconds().toDouble() / 1000.0;
val diff = (System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0;
return time + diff;
};
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
@ -104,6 +124,7 @@ abstract class CastingDevice {
var onConnectionStateChanged = Event1<CastConnectionState>();
var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>();

View file

@ -74,7 +74,8 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition;
setTime(resumePosition);
setDuration(duration);
_streamType = streamType;
_contentType = contentType;
_contentId = contentId;
@ -136,7 +137,7 @@ class ChromecastCastingDevice : CastingDevice {
return;
}
this.volume = volume
setVolume(volume)
val setVolumeObject = JSONObject();
setVolumeObject.put("type", "SET_VOLUME");
@ -490,7 +491,7 @@ class ChromecastCastingDevice : CastingDevice {
if (!sessionIsRunning) {
_sessionId = null;
_mediaSessionId = null;
time = 0.0;
setTime(0.0);
_transportId = null;
Logger.w(TAG, "Session not found.");
@ -510,7 +511,7 @@ class ChromecastCastingDevice : CastingDevice {
val volumeLevel = volume.getString("level").toDouble();
val volumeMuted = volume.getBoolean("muted");
//val volumeStepInterval = volume.getString("stepInterval").toFloat();
this.volume = if (volumeMuted) 0.0 else volumeLevel;
setVolume(if (volumeMuted) 0.0 else volumeLevel);
Logger.i(TAG, "Status update received volume (level: $volumeLevel, muted: $volumeMuted)");
} else if (type == "MEDIA_STATUS") {
@ -521,10 +522,16 @@ class ChromecastCastingDevice : CastingDevice {
val playerState = status.getString("playerState");
val currentTime = status.getDouble("currentTime");
if (status.has("media")) {
val media = status.getJSONObject("media")
if (media.has("duration")) {
setDuration(media.getDouble("duration"))
}
}
isPlaying = playerState == "PLAYING";
if (isPlaying) {
time = currentTime;
setTime(currentTime);
}
val playbackRate = status.getInt("playbackRate");

View file

@ -92,7 +92,8 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition;
setTime(resumePosition);
setDuration(duration);
sendMessage(Opcode.PLAY, FCastPlayMessage(
container = contentType,
url = contentId,
@ -100,7 +101,7 @@ class FCastCastingDevice : CastingDevice {
speed = speed
));
this.speed = speed ?: 1.0
setSpeed(speed ?: 1.0);
}
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
@ -115,7 +116,8 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition;
setTime(resumePosition);
setDuration(duration);
sendMessage(Opcode.PLAY, FCastPlayMessage(
container = contentType,
content = content,
@ -123,7 +125,7 @@ class FCastCastingDevice : CastingDevice {
speed = speed
));
this.speed = speed ?: 1.0
setSpeed(speed ?: 1.0);
}
override fun changeVolume(volume: Double) {
@ -131,7 +133,7 @@ class FCastCastingDevice : CastingDevice {
return;
}
this.volume = volume
setVolume(volume);
sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
}
@ -140,7 +142,7 @@ class FCastCastingDevice : CastingDevice {
return;
}
this.speed = speed
setSpeed(speed);
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
}
@ -330,7 +332,8 @@ class FCastCastingDevice : CastingDevice {
}
val playbackUpdate = FCastCastingDevice.json.decodeFromString<FCastPlaybackUpdateMessage>(json);
time = playbackUpdate.time;
setTime(playbackUpdate.time, playbackUpdate.generationTime);
setDuration(playbackUpdate.duration, playbackUpdate.generationTime);
isPlaying = when (playbackUpdate.state) {
1 -> true
else -> false
@ -343,7 +346,7 @@ class FCastCastingDevice : CastingDevice {
}
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
volume = volumeUpdate.volume;
setVolume(volumeUpdate.volume, volumeUpdate.generationTime);
}
Opcode.PLAYBACK_ERROR -> {
if (json == null) {

View file

@ -66,6 +66,8 @@ class StateCasting {
val onActiveDeviceConnectionStateChanged = Event2<CastingDevice, CastConnectionState>();
val onActiveDevicePlayChanged = Event1<Boolean>();
val onActiveDeviceTimeChanged = Event1<Double>();
val onActiveDeviceDurationChanged = Event1<Double>();
val onActiveDeviceVolumeChanged = Event1<Double>();
var activeDevice: CastingDevice? = null;
private val _client = ManagedHttpClient();
var _resumeCastingDevice: CastingDeviceInfo? = null;
@ -297,9 +299,11 @@ class StateCasting {
val ad = activeDevice;
if (ad != null) {
Logger.i(TAG, "Stopping previous device because a new one is being connected.")
ad.onPlayChanged.clear();
ad.onTimeChanged.clear();
ad.onConnectionStateChanged.clear();
device.onConnectionStateChanged.clear();
device.onPlayChanged.clear();
device.onTimeChanged.clear();
device.onVolumeChanged.clear();
device.onDurationChanged.clear();
ad.stop();
}
@ -309,9 +313,11 @@ class StateCasting {
if (castConnectionState == CastConnectionState.DISCONNECTED) {
Logger.i(TAG, "Clearing events: $castConnectionState");
device.onConnectionStateChanged.clear();
device.onPlayChanged.clear();
device.onTimeChanged.clear();
device.onConnectionStateChanged.clear();
device.onVolumeChanged.clear();
device.onDurationChanged.clear();
activeDevice = null;
}
@ -331,6 +337,12 @@ class StateCasting {
device.onPlayChanged.subscribe {
invokeInMainScopeIfRequired { onActiveDevicePlayChanged.emit(it) };
}
device.onDurationChanged.subscribe {
invokeInMainScopeIfRequired { onActiveDeviceDurationChanged.emit(it) };
};
device.onVolumeChanged.subscribe {
invokeInMainScopeIfRequired { onActiveDeviceVolumeChanged.emit(it) };
};
device.onTimeChanged.subscribe {
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
};
@ -345,6 +357,8 @@ class StateCasting {
device.onConnectionStateChanged.clear();
device.onPlayChanged.clear();
device.onTimeChanged.clear();
device.onVolumeChanged.clear();
device.onDurationChanged.clear();
return;
}

View file

@ -7,12 +7,20 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.R
import com.futo.platformplayer.casting.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.ChromecastCastingDevice
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.google.android.material.slider.Slider
import com.google.android.material.slider.Slider.OnChangeListener
@ -27,8 +35,16 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
private lateinit var _textType: TextView;
private lateinit var _buttonDisconnect: LinearLayout;
private lateinit var _sliderVolume: Slider;
private lateinit var _sliderPosition: Slider;
private lateinit var _layoutVolumeAdjustable: LinearLayout;
private lateinit var _layoutVolumeFixed: LinearLayout;
private lateinit var _buttonPrevious: ImageButton;
private lateinit var _buttonPlay: ImageButton;
private lateinit var _buttonPause: ImageButton;
private lateinit var _buttonStop: ImageButton;
private lateinit var _buttonNext: ImageButton;
private var _device: CastingDevice? = null;
override fun onCreate(savedInstanceState: Bundle?) {
@ -42,17 +58,61 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_textType = findViewById(R.id.text_type);
_buttonDisconnect = findViewById(R.id.button_disconnect);
_sliderVolume = findViewById(R.id.slider_volume);
_sliderPosition = findViewById(R.id.slider_position);
_layoutVolumeAdjustable = findViewById(R.id.layout_volume_adjustable);
_layoutVolumeFixed = findViewById(R.id.layout_volume_fixed);
_buttonPrevious = findViewById(R.id.button_previous);
_buttonPrevious.setOnClickListener {
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.previousVideo()
}
_buttonPlay = findViewById(R.id.button_play);
_buttonPlay.setOnClickListener {
StateCasting.instance.activeDevice?.resumeVideo()
}
_buttonPause = findViewById(R.id.button_pause);
_buttonPause.setOnClickListener {
StateCasting.instance.activeDevice?.pauseVideo()
}
_buttonStop = findViewById(R.id.button_stop);
_buttonStop.setOnClickListener {
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
StateCasting.instance.activeDevice?.stopVideo()
}
_buttonNext = findViewById(R.id.button_next);
_buttonNext.setOnClickListener {
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.nextVideo()
}
_buttonClose.setOnClickListener { dismiss(); };
_buttonDisconnect.setOnClickListener {
StateCasting.instance.activeDevice?.stopCasting();
dismiss();
};
_sliderPosition.addOnChangeListener(OnChangeListener { _, value, fromUser ->
if (!fromUser) {
return@OnChangeListener
}
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
try {
activeDevice.seekVideo(value.toDouble());
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change volume.", e);
}
});
//TODO: Check if volume slider is properly hidden in all cases
_sliderVolume.addOnChangeListener(OnChangeListener { _, value, _ ->
_sliderVolume.addOnChangeListener(OnChangeListener { _, value, fromUser ->
if (!fromUser) {
return@OnChangeListener
}
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
if (activeDevice.canSetVolume) {
try {
@ -71,11 +131,21 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
super.show();
Logger.i(TAG, "Dialog shown.");
_device?.onVolumeChanged?.remove(this);
_device?.onVolumeChanged?.subscribe {
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
_sliderVolume.value = it.toFloat();
};
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
_sliderPosition.value = it.toFloat();
};
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
_sliderPosition.valueTo = it.toFloat();
};
_device = StateCasting.instance.activeDevice;
val d = _device;
val isConnected = d != null && d.connectionState == CastConnectionState.CONNECTED;
@ -89,7 +159,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
override fun dismiss() {
super.dismiss();
_device?.onVolumeChanged?.remove(this);
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
_device = null;
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
}
@ -110,6 +182,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_textName.text = d.name;
_sliderVolume.value = d.volume.toFloat();
_sliderPosition.valueFrom = 0.0f;
_sliderPosition.valueTo = d.duration.toFloat();
_sliderPosition.value = d.time.toFloat();
if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE;

View file

@ -67,6 +67,14 @@ class VideoDetailFragment : MainFragment {
constructor() : super() {
}
fun nextVideo() {
_viewDetail?.nextVideo(true, true, true);
}
fun previousVideo() {
_viewDetail?.prevVideo(true);
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
Logger.i(TAG, "onShownWithView parameter=$parameter")

View file

@ -51,7 +51,8 @@
android:layout_height="35dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:clickable="true">
android:clickable="true"
android:layout_marginTop="8dp">
<ImageView
android:id="@+id/image_device"
@ -125,11 +126,12 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginStart="20dp">
android:layout_marginStart="20dp"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/text_volume"
android:layout_width="wrap_content"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@string/volume"
android:textSize="14dp"
@ -150,6 +152,93 @@
android:layout_marginEnd="15dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_position_adjustable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/text_position"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@string/position"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_position"
android:layout_width="match_parent"
android:layout_height="20dp"
app:thumbColor="@color/colorPrimary"
app:trackColorActive="@color/colorPrimary"
app:trackColorInactive="@color/gray_67"
android:value="0.2"
android:valueFrom="0"
android:valueTo="1"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:divider="@drawable/divider_transparent_8dp"
android:showDividers="middle"
android:gravity="center"
android:layout_marginTop="8dp">
<ImageButton
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
android:clickable="true"
android:padding="10dp"
app:srcCompat="@drawable/ic_skip_previous" />
<ImageButton
android:id="@+id/button_play"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="20dp"
android:scaleType="fitCenter"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@+id/button_pause"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:scaleType="fitCenter"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white" />
<ImageButton
android:id="@+id/button_stop"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="fitCenter"
android:padding="5dp"
android:clickable="true"
app:srcCompat="@drawable/ic_stop_notif" />
<ImageButton
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:clickable="true"
android:scaleType="centerCrop"
android:padding="10dp"
app:srcCompat="@drawable/ic_skip_next" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_volume_fixed"
android:layout_width="match_parent"

View file

@ -715,6 +715,7 @@
<string name="login_to_view_your_comments">Login to view your comments</string>
<string name="polycentric_is_disabled">Polycentric is disabled</string>
<string name="play_pause">Play Pause</string>
<string name="position">Position</string>
<string-array name="home_screen_array">
<item>Recommendations</item>
<item>Subscriptions</item>