mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3775e589f6 |
-12
@@ -70,15 +70,3 @@
|
||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
||||
path = app/src/unstable/assets/sources/spotify
|
||||
url = ../plugins/spotify.git
|
||||
[submodule "app/src/stable/assets/sources/bitchute"]
|
||||
path = app/src/stable/assets/sources/bitchute
|
||||
url = ../plugins/bitchute.git
|
||||
[submodule "app/src/unstable/assets/sources/bitchute"]
|
||||
path = app/src/unstable/assets/sources/bitchute
|
||||
url = ../plugins/bitchute.git
|
||||
[submodule "app/src/unstable/assets/sources/dailymotion"]
|
||||
path = app/src/unstable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||
path = app/src/stable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
|
||||
+2
-11
@@ -2,7 +2,7 @@ plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
@@ -144,19 +144,9 @@ android {
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
assets {
|
||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
@@ -194,6 +184,7 @@ dependencies {
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
|
||||
//Other
|
||||
implementation 'org.jmdns:jmdns:3.5.1'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||
@@ -51,7 +50,7 @@
|
||||
android:name=".activities.MainActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
@@ -153,27 +152,27 @@
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ExceptionActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.CaptchaActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.LoginActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.AddSourceActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||
<intent-filter>
|
||||
@@ -187,44 +186,44 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.AddSourceOptionsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricHomeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricBackupActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricCreateProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricWhyActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricImportProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ManageTabsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.QRCaptureActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.FCastGuideActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -406,39 +406,6 @@ class DashSource {
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashManifestRawSource {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "DashRawSource";
|
||||
this.name = obj.name ?? "";
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.container = obj.container ?? "";
|
||||
this.codec = obj.codec ?? "";
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
class DashManifestRawAudioSource {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "DashRawAudioSource";
|
||||
this.name = obj.name ?? "";
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.container = obj.container ?? "";
|
||||
this.codec = obj.codec ?? "";
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
this.manifest = obj.manifest ?? null;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RequestModifier {
|
||||
constructor(obj) {
|
||||
@@ -795,99 +762,3 @@ class URLSearchParams {
|
||||
return searchString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
|
||||
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
function btoa(input) {
|
||||
input = String(input);
|
||||
if (/[^\0-\xFF]/.test(input)) {
|
||||
// Note: no need to special-case astral symbols here, as surrogates are
|
||||
// matched, and the input is supposed to only contain ASCII anyway.
|
||||
error(
|
||||
'The string to be encoded contains characters outside of the ' +
|
||||
'Latin1 range.'
|
||||
);
|
||||
}
|
||||
var padding = input.length % 3;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
var a;
|
||||
var b;
|
||||
var c;
|
||||
var buffer;
|
||||
// Make sure any padding is handled outside of the loop.
|
||||
var length = input.length - padding;
|
||||
|
||||
while (++position < length) {
|
||||
// Read three bytes, i.e. 24 bits.
|
||||
a = input.charCodeAt(position) << 16;
|
||||
b = input.charCodeAt(++position) << 8;
|
||||
c = input.charCodeAt(++position);
|
||||
buffer = a + b + c;
|
||||
// Turn the 24 bits into four chunks of 6 bits each, and append the
|
||||
// matching character for each of them to the output.
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer & 0x3F)
|
||||
);
|
||||
}
|
||||
|
||||
if (padding == 2) {
|
||||
a = input.charCodeAt(position) << 8;
|
||||
b = input.charCodeAt(++position);
|
||||
buffer = a + b;
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 10) +
|
||||
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
|
||||
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
|
||||
'='
|
||||
);
|
||||
} else if (padding == 1) {
|
||||
buffer = input.charCodeAt(position);
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 2) +
|
||||
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
|
||||
'=='
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
function atob(input) {
|
||||
input = String(input)
|
||||
.replace(__REGEX_SPACE_CHARACTERS, '');
|
||||
var length = input.length;
|
||||
if (length % 4 == 0) {
|
||||
input = input.replace(/==?$/, '');
|
||||
length = input.length;
|
||||
}
|
||||
if (
|
||||
length % 4 == 1 ||
|
||||
// http://whatwg.org/C#alphanumeric-ascii-characters
|
||||
/[^+a-zA-Z0-9/]/.test(input)
|
||||
) {
|
||||
error(
|
||||
'Invalid character: the string to be decoded is not correctly encoded.'
|
||||
);
|
||||
}
|
||||
var bitCounter = 0;
|
||||
var bitStorage;
|
||||
var buffer;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
while (++position < length) {
|
||||
buffer = __btoa_TABLE.indexOf(input.charAt(position));
|
||||
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
|
||||
// Unless this is the first of a group of 4 characters…
|
||||
if (bitCounter++ % 4) {
|
||||
// …convert the first 8 bits to a single ASCII character.
|
||||
output += String.fromCharCode(
|
||||
0xFF & bitStorage >> (-2 * bitCounter & 6)
|
||||
);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AdvancedOrientationListener(private val activity: Activity, private val lifecycleScope: CoroutineScope) {
|
||||
private val sensorManager: SensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
private val magnetometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
|
||||
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var lastOrientationChangeTime = 0L
|
||||
private val debounceTime = 200L
|
||||
private val stabilityThresholdTime = 800L
|
||||
private var deviceAspectRatio: Float = 1.0f
|
||||
|
||||
private val gravity = FloatArray(3)
|
||||
private val geomagnetic = FloatArray(3)
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationAngles = FloatArray(3)
|
||||
|
||||
val onOrientationChanged = Event1<Int>()
|
||||
|
||||
private val sensorListener = object : SensorEventListener {
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
when (event.sensor.type) {
|
||||
Sensor.TYPE_ACCELEROMETER -> {
|
||||
System.arraycopy(event.values, 0, gravity, 0, gravity.size)
|
||||
}
|
||||
Sensor.TYPE_MAGNETIC_FIELD -> {
|
||||
System.arraycopy(event.values, 0, geomagnetic, 0, geomagnetic.size)
|
||||
}
|
||||
}
|
||||
|
||||
if (gravity.isNotEmpty() && geomagnetic.isNotEmpty()) {
|
||||
val success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic)
|
||||
if (success) {
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||
|
||||
val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
|
||||
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
|
||||
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
|
||||
|
||||
val newOrientation = when {
|
||||
roll in -155f .. -15f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
roll in 15f .. 155f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
isWithinThreshold(pitch, -90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
isWithinThreshold(pitch, 90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
}
|
||||
else -> lastOrientation
|
||||
}
|
||||
|
||||
//Logger.i("AdvancedOrientationListener", "newOrientation = ${newOrientation}, roll = ${roll}, pitch = ${pitch}, azimuth = ${azimuth}")
|
||||
|
||||
if (newOrientation != lastStableOrientation) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastOrientationChangeTime > debounceTime) {
|
||||
lastOrientationChangeTime = currentTime
|
||||
lastStableOrientation = newOrientation
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
}
|
||||
|
||||
private fun isWithinThreshold(value: Float, target: Float, threshold: Double): Boolean {
|
||||
return Math.abs(value - target) <= threshold
|
||||
}
|
||||
|
||||
init {
|
||||
sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
|
||||
val metrics = activity.resources.displayMetrics
|
||||
deviceAspectRatio = (metrics.heightPixels.toFloat() / metrics.widthPixels.toFloat())
|
||||
if (deviceAspectRatio == 0.0f)
|
||||
deviceAspectRatio = 1.0f
|
||||
|
||||
lastOrientation = activity.resources.configuration.orientation
|
||||
}
|
||||
|
||||
fun stopListening() {
|
||||
sensorManager.unregisterListener(sensorListener)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,7 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||
@UnstableApi
|
||||
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||
val requestModifier = getRequestModifier();
|
||||
val requestExecutor = getRequestExecutor();
|
||||
return if (requestExecutor != null) {
|
||||
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
|
||||
} else if (requestModifier != null) {
|
||||
return if (requestModifier != null) {
|
||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||
} else {
|
||||
DefaultHttpDataSource.Factory();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,6 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.net.Uri
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URLEncoder
|
||||
@@ -28,18 +25,4 @@ fun String?.yesNoToBoolean(): Boolean {
|
||||
|
||||
fun Boolean?.toYesNo(): String {
|
||||
return if (this == true) "YES" else "NO"
|
||||
}
|
||||
|
||||
fun InetAddress?.toUrlAddress(): String {
|
||||
return when (this) {
|
||||
is Inet6Address -> {
|
||||
"[${toString()}]"
|
||||
}
|
||||
is Inet4Address -> {
|
||||
toString()
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Invalid address type")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@ package com.futo.platformplayer
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.webkit.CookieManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
@@ -26,7 +23,6 @@ import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePayment
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -38,7 +34,6 @@ import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.stripe.android.customersheet.injection.CustomerSheetViewModelModule_Companion_ContextFactory.context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -49,7 +44,6 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
||||
@Serializable
|
||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||
|
||||
@@ -63,7 +57,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Transient
|
||||
val onTabsChanged = Event0();
|
||||
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -79,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||
@FormFieldButton(R.drawable.ic_quiz)
|
||||
fun openFAQ() {
|
||||
try {
|
||||
@@ -89,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
//Ignored
|
||||
}
|
||||
}
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||
@FormFieldButton(R.drawable.ic_data_alert)
|
||||
fun openIssues() {
|
||||
try {
|
||||
@@ -121,7 +115,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}*/
|
||||
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
try {
|
||||
@@ -135,7 +129,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||
@FormFieldButton(R.drawable.ic_move_up)
|
||||
fun import() {
|
||||
val act = SettingsActivity.getActivity() ?: return;
|
||||
@@ -144,7 +138,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
act.startActivity(intent);
|
||||
}
|
||||
|
||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
||||
@FormFieldButton(R.drawable.ic_link)
|
||||
fun manageLinks() {
|
||||
try {
|
||||
@@ -154,24 +148,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
||||
@FormFieldButton(R.drawable.battery_full_24px)
|
||||
fun ignoreBatteryOptimization() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
val intent = Intent()
|
||||
val packageName = it.packageName
|
||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
intent.setData(Uri.parse("package:$packageName"))
|
||||
it.startActivity(intent)
|
||||
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
|
||||
} else {
|
||||
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@FormField(R.string.language, "group", -1, 0)
|
||||
var language = LanguageSettings();
|
||||
@Serializable
|
||||
@@ -350,7 +326,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var playback = PlaybackSettings();
|
||||
@Serializable
|
||||
class PlaybackSettings {
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
@@ -377,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||
var defaultPlaybackSpeed: Int = 3;
|
||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||
@@ -393,31 +369,35 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> 1.0f;
|
||||
};
|
||||
|
||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredQuality: Int = 0;
|
||||
|
||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredMeteredQuality: Int = 0;
|
||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||
|
||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredPreviewQuality: Int = 5;
|
||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||
|
||||
|
||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||
var simplifySources: Boolean = true;
|
||||
|
||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||
var autoRotate: Int = 2;
|
||||
|
||||
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
|
||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||
|
||||
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||
var autoRotateDeadZone: Int = 0;
|
||||
|
||||
fun getAutoRotateDeadZoneDegrees(): Int {
|
||||
return autoRotateDeadZone * 5;
|
||||
}
|
||||
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||
@@ -470,16 +450,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||
var fullscreenPortrait: Boolean = false;
|
||||
|
||||
|
||||
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
|
||||
var preferWebmVideo: Boolean = false;
|
||||
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
|
||||
var preferWebmAudio: Boolean = false;
|
||||
|
||||
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
|
||||
@FormFieldWarning(R.string.changing_this_field_requires_restart)
|
||||
var allowVideoToGoUnderCutout: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
@@ -555,10 +525,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var keepScreenOn: Boolean = true;
|
||||
|
||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var alwaysProxyRequests: Boolean = false;
|
||||
|
||||
/*TODO: Should we have a different casting quality?
|
||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
@@ -809,10 +775,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun export() {
|
||||
val activity = SettingsActivity.getActivity() ?: return;
|
||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||
StateBackup.shareExternalBackup();
|
||||
}),
|
||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||
StateBackup.saveExternalBackup(activity);
|
||||
})
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.work.WorkManager
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.futo.platformplayer.activities.DeveloperActivity
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -492,13 +491,6 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.test_playback, FieldForm.BUTTON,
|
||||
R.string.test_playback, 1)
|
||||
fun testPlayback(context: Context) {
|
||||
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.hardware.SensorManager
|
||||
import android.view.OrientationEventListener
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SimpleOrientationListener(
|
||||
private val activity: Activity,
|
||||
private val lifecycleScope: CoroutineScope
|
||||
) {
|
||||
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private val stabilityThresholdTime = 500L
|
||||
|
||||
val onOrientationChanged = Event1<Int>()
|
||||
|
||||
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
val newOrientation = when {
|
||||
orientation in 45..134 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
orientation in 135..224 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
orientation in 225..314 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
orientation in 315..360 || orientation in 0..44 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
else -> lastOrientation
|
||||
}
|
||||
|
||||
if (newOrientation != lastStableOrientation) {
|
||||
lastStableOrientation = newOrientation
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
orientationListener.enable()
|
||||
lastOrientation = activity.resources.configuration.orientation
|
||||
}
|
||||
|
||||
fun stopListening() {
|
||||
orientationListener.disable()
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,14 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
@@ -38,12 +34,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
@@ -95,17 +91,9 @@ class UISlideOverlays {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
items.addAll(listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_notifications,
|
||||
"Notifications",
|
||||
"",
|
||||
tag = "notifications",
|
||||
call = {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
}, false),
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
@@ -140,62 +128,22 @@ class UISlideOverlays {
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_live_tv,
|
||||
"Livestreams",
|
||||
"Check for livestreams",
|
||||
tag = "fetchLive",
|
||||
call = {
|
||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Streams",
|
||||
"Check for streams",
|
||||
tag = "fetchStreams",
|
||||
call = {
|
||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||
}, false) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||
}, false) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Videos",
|
||||
"Check for videos",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Content",
|
||||
"Check for content",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_chat,
|
||||
"Posts",
|
||||
"Check for posts",
|
||||
tag = "fetchPosts",
|
||||
call = {
|
||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null/*,,
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
}, false) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||
}, false) else null/*,,
|
||||
|
||||
SlideUpMenuGroup(container.context, "Actions",
|
||||
"Various things you can do with this subscription",
|
||||
@@ -294,23 +242,11 @@ class UISlideOverlays {
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||
|
||||
masterPlaylist.getAudioSources().forEach { it ->
|
||||
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
audioButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudioVariant = it
|
||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||
selectedAudioVariant = it
|
||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}
|
||||
|
||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||
@@ -322,22 +258,11 @@ class UISlideOverlays {
|
||||
}*/
|
||||
|
||||
masterPlaylist.getVideoSources().forEach {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
videoButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}
|
||||
|
||||
val newItems = arrayListOf<View>()
|
||||
@@ -396,8 +321,8 @@ class UISlideOverlays {
|
||||
|
||||
|
||||
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
||||
var selectedVideo: IVideoSource? = null;
|
||||
var selectedAudio: IAudioSource? = null;
|
||||
var selectedVideo: IVideoUrlSource? = null;
|
||||
var selectedAudio: IAudioUrlSource? = null;
|
||||
var selectedSubtitle: ISubtitleSource? = null;
|
||||
|
||||
val videoSources = descriptor.videoSources;
|
||||
@@ -416,93 +341,45 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||
listOf(listOf(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.none),
|
||||
container.context.getString(R.string.audio_only),
|
||||
tag = "none",
|
||||
call = {
|
||||
selectedVideo = null;
|
||||
menu?.selectOption(videoSources, "none");
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)) +
|
||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
||||
selectedVideo = null;
|
||||
menu?.selectOption(videoSources, "none");
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false)) +
|
||||
videoSources
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
when (it) {
|
||||
is IVideoUrlSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
}
|
||||
|
||||
is JSDashManifestRawSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false)
|
||||
}
|
||||
|
||||
is IHLSManifestSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"HLS",
|
||||
tag = it,
|
||||
call = {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
}, false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||
null;//throw Exception("Unhandled source type")
|
||||
throw Exception("Unhandled source type")
|
||||
}
|
||||
}
|
||||
}.filterNotNull()).flatten().toList()
|
||||
}).flatten().toList()
|
||||
));
|
||||
|
||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||
//TODO: Add HLS support here
|
||||
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||
) as IVideoSource?;
|
||||
) as IVideoUrlSource?;
|
||||
}
|
||||
|
||||
if (audioSources != null) {
|
||||
@@ -511,90 +388,43 @@ class UISlideOverlays {
|
||||
.map {
|
||||
when (it) {
|
||||
is IAudioUrlSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
"${it.bitrate}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudio = it
|
||||
menu?.selectOption(audioSources, it);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
);
|
||||
}
|
||||
|
||||
is JSDashManifestRawAudioSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
"${it.bitrate}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudio = it
|
||||
menu?.selectOption(audioSources, it);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
);
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||
selectedAudio = it
|
||||
menu?.selectOption(audioSources, it);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false);
|
||||
}
|
||||
|
||||
is IHLSManifestAudioSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"HLS Audio",
|
||||
tag = it,
|
||||
call = {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
}, false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||
null;//throw Exception("Unhandled source type")
|
||||
throw Exception("Unhandled source type")
|
||||
}
|
||||
}
|
||||
}.filterNotNull()));
|
||||
}));
|
||||
|
||||
//TODO: Add HLS support here
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
|
||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||
}
|
||||
|
||||
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_edit,
|
||||
it.name,
|
||||
"",
|
||||
tag = it,
|
||||
call = {
|
||||
if (selectedSubtitle == it) {
|
||||
selectedSubtitle = null;
|
||||
menu?.selectOption(subtitleSources, null);
|
||||
} else {
|
||||
selectedSubtitle = it;
|
||||
menu?.selectOption(subtitleSources, it);
|
||||
}
|
||||
},
|
||||
invokeParent = false
|
||||
);
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||
if (selectedSubtitle == it) {
|
||||
selectedSubtitle = null;
|
||||
menu?.selectOption(subtitleSources, null);
|
||||
} else {
|
||||
selectedSubtitle = it;
|
||||
menu?.selectOption(subtitleSources, it);
|
||||
}
|
||||
}, false);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -612,18 +442,6 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
menu.onOK.subscribe {
|
||||
val sv = selectedVideo
|
||||
if (sv is IHLSManifestSource) {
|
||||
showHlsPicker(video, sv, sv.url, container)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
val sa = selectedAudio
|
||||
if (sa is IHLSManifestAudioSource) {
|
||||
showHlsPicker(video, sa, sa.url, container)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
menu.hide();
|
||||
val subtitleToDownload = selectedSubtitle;
|
||||
if(selectedAudio != null || !requiresAudio) {
|
||||
@@ -680,9 +498,8 @@ class UISlideOverlays {
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
||||
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
||||
handleUnknownDownload();
|
||||
loader.hide(true);
|
||||
}
|
||||
@@ -719,47 +536,23 @@ class UISlideOverlays {
|
||||
);
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.first,
|
||||
it.second,
|
||||
tag = it.third,
|
||||
call = {
|
||||
targetPxSize = it.third;
|
||||
menu?.selectOption("Video", it.third);
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
||||
targetPxSize = it.third;
|
||||
menu?.selectOption("Video", it.third);
|
||||
}, false)
|
||||
}));
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.low_bitrate),
|
||||
"",
|
||||
tag = 1,
|
||||
call = {
|
||||
targetBitrate = 1;
|
||||
menu?.selectOption("Bitrate", 1);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.high_bitrate),
|
||||
"",
|
||||
tag = 9999999,
|
||||
call = {
|
||||
targetBitrate = 9999999;
|
||||
menu?.selectOption("Bitrate", 9999999);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
||||
targetBitrate = 1;
|
||||
menu?.selectOption("Bitrate", 1);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
||||
targetBitrate = 9999999;
|
||||
menu?.selectOption("Bitrate", 9999999);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false)
|
||||
)));
|
||||
|
||||
|
||||
@@ -882,12 +675,8 @@ class UISlideOverlays {
|
||||
if (lastUpdated != null) {
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
lastUpdated.name,
|
||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||
{
|
||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}))
|
||||
@@ -899,90 +688,42 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
container.context.getString(R.string.download),
|
||||
container.context.getString(R.string.download_the_video),
|
||||
tag = "download",
|
||||
call = {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_share,
|
||||
container.context.getString(R.string.share),
|
||||
"Share the video",
|
||||
tag = "share",
|
||||
call = {
|
||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND;
|
||||
putExtra(Intent.EXTRA_TEXT, url);
|
||||
type = "text/plain";
|
||||
}, null));
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_visibility_off,
|
||||
container.context.getString(R.string.hide_creator_from_home),
|
||||
"",
|
||||
tag = "hide_creator",
|
||||
call = {
|
||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||
}))
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND;
|
||||
putExtra(Intent.EXTRA_TEXT, url);
|
||||
type = "text/plain";
|
||||
}, null));
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||
}))
|
||||
+ actions)
|
||||
));
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_queue_add,
|
||||
container.context.getString(R.string.add_to_queue),
|
||||
"${queue.size} " + container.context.getString(R.string.videos),
|
||||
tag = "queue",
|
||||
call = { StatePlayer.instance.addToQueue(video); }),
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_watchlist_add,
|
||||
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_history,
|
||||
container.context.getString(R.string.add_to_history),
|
||||
"Mark as watched",
|
||||
tag = "history",
|
||||
call = { StateHistory.instance.markAsWatched(video); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||
{ StatePlayer.instance.addToQueue(video); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
||||
));
|
||||
|
||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||
playlistItems.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
container.context.getString(R.string.new_playlist),
|
||||
container.context.getString(R.string.add_to_new_playlist),
|
||||
tag = "add_to_new_playlist",
|
||||
call = {
|
||||
showCreatePlaylistOverlay(container) {
|
||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
};
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||
showCreatePlaylistOverlay(container) {
|
||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
};
|
||||
}, false))
|
||||
|
||||
for (playlist in allPlaylists) {
|
||||
playlistItems.add(SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
"${container.context.getString(R.string.add_to)} " + playlist.name + "",
|
||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||
{
|
||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}));
|
||||
@@ -1004,12 +745,8 @@ class UISlideOverlays {
|
||||
if (lastUpdated != null) {
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
lastUpdated.name,
|
||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||
{
|
||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}))
|
||||
@@ -1021,52 +758,25 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_queue_add,
|
||||
container.context.getString(R.string.queue),
|
||||
"${queue.size} " + container.context.getString(R.string.videos),
|
||||
tag = "queue",
|
||||
call = { StatePlayer.instance.addToQueue(video); }),
|
||||
SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_watchlist_add,
|
||||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
container.context.getString(R.string.download),
|
||||
container.context.getString(R.string.download_the_video),
|
||||
tag = container.context.getString(R.string.download),
|
||||
call = { showDownloadVideoOverlay(video, container, true); },
|
||||
invokeParent = false
|
||||
))
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||
{ StatePlayer.instance.addToQueue(video); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||
);
|
||||
|
||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||
playlistItems.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
container.context.getString(R.string.new_playlist),
|
||||
container.context.getString(R.string.add_to_new_playlist),
|
||||
tag = "add_to_new_playlist",
|
||||
call = {
|
||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
});
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
});
|
||||
}, false))
|
||||
|
||||
for (playlist in allPlaylists) {
|
||||
playlistItems.add(SlideUpMenuItem(container.context,
|
||||
R.drawable.ic_playlist_add,
|
||||
playlist.name,
|
||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||
{
|
||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}));
|
||||
@@ -1091,36 +801,20 @@ class UISlideOverlays {
|
||||
|
||||
val views = arrayOf(
|
||||
hidden
|
||||
.map { btn -> SlideUpMenuItem(
|
||||
container.context,
|
||||
btn.iconResource,
|
||||
btn.text.text.toString(),
|
||||
"",
|
||||
tag = "",
|
||||
call = {
|
||||
btn.handler?.invoke(btn);
|
||||
},
|
||||
invokeParent = invokeParents
|
||||
) as View }.toTypedArray(),
|
||||
arrayOf(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_pin,
|
||||
container.context.getString(R.string.change_pins),
|
||||
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
||||
tag = "",
|
||||
call = {
|
||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||
val selected = it
|
||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||
.filter { it != null }
|
||||
.map { it!! }
|
||||
.toList();
|
||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||
btn.handler?.invoke(btn);
|
||||
}, invokeParents) as View }.toTypedArray(),
|
||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||
val selected = it
|
||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||
.filter { it != null }
|
||||
.map { it!! }
|
||||
.toList();
|
||||
|
||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||
}
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||
}
|
||||
}, false))
|
||||
).flatten().toTypedArray();
|
||||
|
||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||
@@ -1132,21 +826,14 @@ class UISlideOverlays {
|
||||
var overlay: SlideUpMenuOverlay? = null;
|
||||
|
||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||
options.map { SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_move_up,
|
||||
it.first,
|
||||
"",
|
||||
tag = it.second,
|
||||
call = {
|
||||
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||
if(!selection.contains(it.second))
|
||||
selection.add(it.second);
|
||||
} else
|
||||
}
|
||||
else
|
||||
selection.remove(it.second);
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
}, false)
|
||||
});
|
||||
overlay.onOK.subscribe {
|
||||
onOrdered.invoke(selection);
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.os.OperationCanceledException
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -147,8 +146,6 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setNavigationBarColorAndIcons() {
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
||||
|
||||
@@ -4,17 +4,15 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -22,61 +20,30 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.CommentsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupListFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.listeners.OrientationManager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StatePayment
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
@@ -84,22 +51,15 @@ import com.futo.platformplayer.views.ToastView
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
//TODO: Move to dimensions
|
||||
@@ -119,9 +79,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
private lateinit var _fragContainerVideoDetail: FragmentContainerView;
|
||||
private lateinit var _fragContainerOverlay: FrameLayout;
|
||||
|
||||
//Views
|
||||
private lateinit var _buttonIncognito: ImageView;
|
||||
|
||||
//Frags TopBar
|
||||
lateinit var _fragTopBarGeneral: GeneralTopBarFragment;
|
||||
lateinit var _fragTopBarSearch: SearchTopBarFragment;
|
||||
@@ -172,6 +129,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
val onNavigated = Event1<MainFragment>();
|
||||
|
||||
private lateinit var _orientationManager: OrientationManager;
|
||||
var orientation: OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
|
||||
private set;
|
||||
private var _isVisible = true;
|
||||
private var _wasStopped = false;
|
||||
|
||||
@@ -196,15 +156,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
constructor() : super() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(
|
||||
VmPolicy.Builder()
|
||||
.detectLeakedClosableObjects()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||
@@ -253,7 +204,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
setContentView(R.layout.activity_main);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
|
||||
runBlocking {
|
||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||
}
|
||||
@@ -340,52 +290,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
updateSegmentPaddings();
|
||||
};
|
||||
|
||||
|
||||
_buttonIncognito = findViewById(R.id.incognito_button);
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
StateApp.instance.privateModeChanged.subscribe {
|
||||
//Messing with visibility causes some issues with layout ordering?
|
||||
if(it) {
|
||||
_buttonIncognito.elevation = 99f;
|
||||
_buttonIncognito.alpha = 1f;
|
||||
}
|
||||
else {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
}
|
||||
_buttonIncognito.setOnClickListener {
|
||||
if(!StateApp.instance.privateMode)
|
||||
return@setOnClickListener;
|
||||
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
||||
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
||||
UIDialogs.Action("Cancel", {
|
||||
StateApp.instance.setPrivacyMode(true);
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Disable", {
|
||||
StateApp.instance.setPrivacyMode(false);
|
||||
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||
};
|
||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||
|
||||
if(it) {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
else {
|
||||
if(StateApp.instance.privateMode) {
|
||||
_buttonIncognito.elevation = 99f;
|
||||
_buttonIncognito.alpha = 1f;
|
||||
}
|
||||
else {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatePlayer.instance.also {
|
||||
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
||||
if (!shouldSwapCurrentItem) {
|
||||
@@ -460,6 +364,26 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
.commitNow();
|
||||
|
||||
defaultTab.action(_fragBotBarMenu);
|
||||
|
||||
_orientationManager = OrientationManager(this);
|
||||
_orientationManager.onOrientationChanged.subscribe {
|
||||
orientation = it;
|
||||
Logger.i(TAG, "Orientation changed (Found ${it})");
|
||||
fragCurrent.onOrientationChanged(it);
|
||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
||||
_fragVideoDetail.onOrientationChanged(it);
|
||||
else if(Settings.instance.other.bypassRotationPrevention)
|
||||
{
|
||||
requestedOrientation = when(orientation) {
|
||||
OrientationManager.Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
OrientationManager.Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
OrientationManager.Orientation.REVERSED_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
OrientationManager.Orientation.REVERSED_LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
}
|
||||
};
|
||||
_orientationManager.enable();
|
||||
|
||||
StateSubscriptions.instance;
|
||||
|
||||
fragCurrent.onShown(null, false);
|
||||
@@ -556,6 +480,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
override fun onResume() {
|
||||
super.onResume();
|
||||
Logger.v(TAG, "onResume")
|
||||
|
||||
val curOrientation = _orientationManager.orientation;
|
||||
|
||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.lastOrientation != curOrientation) {
|
||||
Logger.i(TAG, "Orientation mismatch (Found ${curOrientation})");
|
||||
orientation = curOrientation;
|
||||
fragCurrent.onOrientationChanged(curOrientation);
|
||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
||||
_fragVideoDetail.onOrientationChanged(curOrientation);
|
||||
}
|
||||
|
||||
_isVisible = true;
|
||||
}
|
||||
|
||||
@@ -603,11 +538,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
"IMPORT_OPTIONS" -> {
|
||||
UIDialogs.showImportOptionsDialog(this);
|
||||
}
|
||||
"ACTION" -> {
|
||||
val action = intent.getStringExtra("ACTION");
|
||||
StateDeveloper.instance.testState = "TestPlayback";
|
||||
StateDeveloper.instance.testPlayback();
|
||||
}
|
||||
"TAB" -> {
|
||||
when(intent.getStringExtra("TAB")){
|
||||
"Sources" -> {
|
||||
@@ -956,6 +886,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
override fun onRestart() {
|
||||
super.onRestart();
|
||||
Logger.i(TAG, "onRestart");
|
||||
|
||||
//Force Portrait on restart
|
||||
Logger.i(TAG, "Restarted with state ${_fragVideoDetail.state}");
|
||||
if(_fragVideoDetail.state != VideoDetailFragment.State.MAXIMIZED) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
WindowInsetsControllerCompat(window, rootView).let { controller ->
|
||||
controller.show(WindowInsetsCompat.Type.statusBars());
|
||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
_fragVideoDetail.onOrientationChanged(OrientationManager.Orientation.PORTRAIT);
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
@@ -970,6 +912,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy();
|
||||
Logger.v(TAG, "onDestroy")
|
||||
|
||||
_orientationManager.disable();
|
||||
|
||||
StateApp.instance.mainAppDestroyed(this);
|
||||
}
|
||||
|
||||
@@ -1235,13 +1180,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
fun getActionIntent(context: Context, action: String) : Intent {
|
||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||
sourcesIntent.action = "ACTION";
|
||||
sourcesIntent.putExtra("ACTION", action);
|
||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
|
||||
fun getImportOptionsIntent(context: Context): Intent {
|
||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
@@ -185,19 +184,12 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
resultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
settingsActivityClosed.emit()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
//TODO: Temporary for solving Settings issues
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var _lastActivity: SettingsActivity? = null;
|
||||
|
||||
val settingsActivityClosed = Event0()
|
||||
|
||||
fun getActivity(): SettingsActivity? {
|
||||
val act = _lastActivity;
|
||||
if(act != null && !act._isFinished)
|
||||
|
||||
@@ -17,14 +17,13 @@ import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
open class ManagedHttpClient {
|
||||
protected var _builderTemplate: OkHttpClient.Builder;
|
||||
protected val _builderTemplate: OkHttpClient.Builder;
|
||||
|
||||
private var client: OkHttpClient;
|
||||
|
||||
@@ -33,15 +32,6 @@ open class ManagedHttpClient {
|
||||
|
||||
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||
|
||||
fun setTimeout(timeout: Long) {
|
||||
rebuildClient {
|
||||
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
|
||||
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
|
||||
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
|
||||
.connectTimeout(Duration.ofMillis(timeout));
|
||||
}
|
||||
}
|
||||
|
||||
private val trustAllCerts = arrayOf<TrustManager>(
|
||||
object: X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
||||
@@ -72,15 +62,6 @@ open class ManagedHttpClient {
|
||||
}.build();
|
||||
}
|
||||
|
||||
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
|
||||
_builderTemplate = modify(_builderTemplate);
|
||||
client = _builderTemplate.addNetworkInterceptor { chain ->
|
||||
val request = beforeRequest(chain.request());
|
||||
val response = afterRequest(chain.proceed(request));
|
||||
return@addNetworkInterceptor response;
|
||||
}.build();
|
||||
}
|
||||
|
||||
open fun clone(): ManagedHttpClient {
|
||||
val clonedClient = ManagedHttpClient(_builderTemplate);
|
||||
clonedClient.user_agent = user_agent;
|
||||
|
||||
@@ -210,20 +210,6 @@ class HttpContext : AutoCloseable {
|
||||
}
|
||||
}
|
||||
}
|
||||
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
|
||||
if(headers.get("content-length").isNullOrEmpty()) {
|
||||
if (body != null) {
|
||||
headers.put("content-length", body.size.toString());
|
||||
} else {
|
||||
headers.put("content-length", "0")
|
||||
}
|
||||
}
|
||||
respond(status, headers) { responseStream ->
|
||||
if(body != null) {
|
||||
responseStream.write(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
|
||||
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
||||
|
||||
for(getMethod in getMethods)
|
||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||
if(!getMethod.second.contentType.isEmpty())
|
||||
this.withContentType(getMethod.second.contentType);
|
||||
}.withContentType(getMethod.second.contentType);
|
||||
for(postMethod in postMethods)
|
||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||
if(!postMethod.second.contentType.isEmpty())
|
||||
this.withContentType(postMethod.second.contentType);
|
||||
}.withContentType(postMethod.second.contentType);
|
||||
|
||||
for(getField in getFields) {
|
||||
getField.first.isAccessible = true;
|
||||
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
||||
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
||||
val value = getField.first.get(obj) as String?;
|
||||
if(value != null) {
|
||||
val headers = HttpHeaders(
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
|
||||
|
||||
import com.futo.platformplayer.api.http.server.HttpContext
|
||||
|
||||
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||
override fun handle(httpContext: HttpContext) {
|
||||
httpContext.setResponseHeaders(this.headers);
|
||||
handler(httpContext);
|
||||
|
||||
@@ -13,15 +13,13 @@ class PlatformClientPool {
|
||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||
private var _poolCounter = 0;
|
||||
private val _poolName: String?;
|
||||
private val _privatePool: Boolean;
|
||||
|
||||
var isDead: Boolean = false
|
||||
private set;
|
||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||
|
||||
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
||||
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||
_poolName = name;
|
||||
_privatePool = privatePool;
|
||||
if(parentClient !is JSClient)
|
||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||
@@ -53,7 +51,7 @@ class PlatformClientPool {
|
||||
reserved = _pool.keys.find { !it.isBusy };
|
||||
if(reserved == null && _pool.size < capacity) {
|
||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||
reserved = _parent.getCopy(_privatePool);
|
||||
reserved = _parent.getCopy();
|
||||
|
||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||
StateApp.instance.handleCaptchaException(client, ex);
|
||||
|
||||
@@ -6,14 +6,12 @@ class PlatformMultiClientPool {
|
||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
|
||||
private var _isFake = false;
|
||||
private var _privatePool = false;
|
||||
|
||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
||||
constructor(name: String, maxCap: Int = -1) {
|
||||
_name = name;
|
||||
_maxCap = if(maxCap > 0)
|
||||
maxCap
|
||||
else 99;
|
||||
_privatePool = isPrivatePool;
|
||||
}
|
||||
|
||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||
@@ -21,7 +19,7 @@ class PlatformMultiClientPool {
|
||||
return parentClient;
|
||||
val pool = synchronized(_clientPools) {
|
||||
if(!_clientPools.containsKey(parentClient))
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||
this.onDead.subscribe { _, pool ->
|
||||
synchronized(_clientPools) {
|
||||
if(_clientPools[parentClient] == pool)
|
||||
|
||||
@@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
||||
companion object {
|
||||
fun fromInt(value: Int): ChapterType
|
||||
{
|
||||
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||
val result = ChapterType.values().firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw UnknownPlatformException(value.toString());
|
||||
return result;
|
||||
|
||||
@@ -21,7 +21,7 @@ enum class ContentType(val value: Int) {
|
||||
companion object {
|
||||
fun fromInt(value: Int): ContentType
|
||||
{
|
||||
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||
val result = ContentType.values().firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw UnknownPlatformException(value.toString());
|
||||
return result;
|
||||
|
||||
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
||||
|
||||
companion object{
|
||||
fun fromInt(value : Int) : LiveEventType{
|
||||
return LiveEventType.entries.first { it.value == value };
|
||||
return LiveEventType.values().first { it.value == value };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
|
||||
companion object {
|
||||
fun fromInt(value: Int): TextType
|
||||
{
|
||||
val result = TextType.entries.firstOrNull { it.value == value };
|
||||
val result = TextType.values().firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||
return result;
|
||||
|
||||
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
||||
|
||||
companion object{
|
||||
fun fromInt(value : Int) : RatingType{
|
||||
return RatingType.entries.first { it.value == value };
|
||||
return RatingType.values().first { it.value == value };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,8 @@ class DevJSClient : JSClient {
|
||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||
}
|
||||
|
||||
override fun getCopy(privateCopy: Boolean): JSClient {
|
||||
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
||||
override fun getCopy(): JSClient {
|
||||
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
|
||||
@@ -164,16 +164,13 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||
this._context = context;
|
||||
this.config = descriptor.config;
|
||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
if(!withoutCredentials)
|
||||
_auth = descriptor.getAuth();
|
||||
else
|
||||
_auth = null;
|
||||
_auth = descriptor.getAuth();
|
||||
_captcha = descriptor.getCaptchaData();
|
||||
flags = descriptor.flags.toTypedArray();
|
||||
|
||||
@@ -193,8 +190,8 @@ open class JSClient : IPlatformClient {
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
|
||||
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
||||
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
||||
open fun getCopy(): JSClient {
|
||||
return JSClient(_context, descriptor, saveState(), _script);
|
||||
}
|
||||
|
||||
fun getUnderlyingPlugin(): V8Plugin {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
class JSClientConstants {
|
||||
companion object {
|
||||
val PLUGIN_SPEC_VERSION = 2;
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -4,9 +4,7 @@ import android.net.Uri
|
||||
import com.futo.platformplayer.SignatureProvider
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.serialization.Contextual
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
|
||||
@@ -50,7 +48,6 @@ class SourcePluginConfig(
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null,
|
||||
var allowAllHttpHeaderAccess: Boolean = false,
|
||||
var maxDownloadParallelism: Int = 0
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
@@ -80,8 +77,7 @@ class SourcePluginConfig(
|
||||
private var _allowUrlsLowerVal: List<String>? = null;
|
||||
private val _allowUrlsLower: List<String> get() {
|
||||
if(_allowUrlsLowerVal == null)
|
||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
||||
.filter { it.length > 0 };
|
||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() };
|
||||
return _allowUrlsLowerVal!!;
|
||||
};
|
||||
|
||||
@@ -174,7 +170,7 @@ class SourcePluginConfig(
|
||||
return true;
|
||||
val uri = Uri.parse(url);
|
||||
val host = uri.host?.lowercase() ?: "";
|
||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||
return _allowUrlsLower.any { it == host };
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -54,8 +54,4 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
||||
|
||||
_hasGetDetails = _content.has("getDetails");
|
||||
}
|
||||
|
||||
fun getUnderlyingObject(): V8ValueObject? {
|
||||
return _content;
|
||||
}
|
||||
}
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.primitive.V8ValueUndefined
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Base64
|
||||
|
||||
class JSRequestExecutor {
|
||||
private val _plugin: JSClient;
|
||||
private val _config: IV8PluginConfig;
|
||||
private var _executor: V8ValueObject;
|
||||
val urlPrefix: String?;
|
||||
|
||||
private val hasCleanup: Boolean;
|
||||
|
||||
constructor(plugin: JSClient, executor: V8ValueObject) {
|
||||
this._plugin = plugin;
|
||||
this._executor = executor;
|
||||
this._config = plugin.config;
|
||||
val config = plugin.config;
|
||||
|
||||
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
|
||||
|
||||
if(!executor.has("executeRequest"))
|
||||
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
|
||||
hasCleanup = executor.has("cleanup");
|
||||
}
|
||||
|
||||
//TODO: Executor properties?
|
||||
@Throws(ScriptException::class)
|
||||
open fun executeRequest(url: String, headers: Map<String, String>): ByteArray {
|
||||
if (_executor.isClosed)
|
||||
throw IllegalStateException("Executor object is closed");
|
||||
|
||||
val result = if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers);
|
||||
} as V8Value;
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers);
|
||||
} as V8Value;
|
||||
|
||||
try {
|
||||
if(result is V8ValueString) {
|
||||
val base64Result = Base64.getDecoder().decode(result.value);
|
||||
return base64Result;
|
||||
}
|
||||
if(result is V8ValueTypedArray) {
|
||||
val buffer = result.buffer;
|
||||
val byteBuffer = buffer.byteBuffer;
|
||||
val bytesResult = ByteArray(result.byteLength);
|
||||
byteBuffer.get(bytesResult, 0, result.byteLength);
|
||||
buffer.close();
|
||||
return bytesResult;
|
||||
}
|
||||
if(result is V8ValueObject && result.has("type")) {
|
||||
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
|
||||
when(type) {
|
||||
//TODO: Buffer type?
|
||||
}
|
||||
}
|
||||
if(result is V8ValueUndefined) {
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
|
||||
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
|
||||
}
|
||||
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
|
||||
}
|
||||
finally {
|
||||
result.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open fun cleanup() {
|
||||
if (!hasCleanup || _executor.isClosed)
|
||||
return;
|
||||
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
};
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
};
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: are these available..?
|
||||
@Serializable
|
||||
class ExecutorParameters {
|
||||
var rangeStart: Int = -1;
|
||||
var rangeEnd: Int = -1;
|
||||
|
||||
var segment: Int = -1;
|
||||
}
|
||||
-5
@@ -35,9 +35,4 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
||||
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
||||
audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2;
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RangeSource(url=[${getAudioUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
|
||||
return super.toString()
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
override val name : String;
|
||||
override val codec: String;
|
||||
override val bitrate: Int;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
|
||||
override val language: String;
|
||||
|
||||
val url: String;
|
||||
override var manifest: String?;
|
||||
|
||||
override val hasGenerate: Boolean;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
override fun generate(): String? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
if(_plugin is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
interface IJSDashManifestRawSource {
|
||||
val hasGenerate: Boolean;
|
||||
var manifest: String?;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
override val name : String;
|
||||
override val width: Int;
|
||||
override val height: Int;
|
||||
override val codec: String;
|
||||
override val bitrate: Int?;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
|
||||
var url: String?;
|
||||
override var manifest: String?;
|
||||
|
||||
override val hasGenerate: Boolean;
|
||||
val canMerge: Boolean;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
override open fun generate(): String? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
if(_plugin is DevJSClient) {
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class JSDashManifestMergingRawSource(
|
||||
val video: JSDashManifestRawSource,
|
||||
val audio: JSDashManifestRawAudioSource): JSDashManifestRawSource(video.getUnderlyingPlugin()!!, video.getUnderlyingObject()!!), IVideoSource {
|
||||
|
||||
override val name: String
|
||||
get() = video.name;
|
||||
override val bitrate: Int
|
||||
get() = (video.bitrate ?: 0) + audio.bitrate;
|
||||
override val codec: String
|
||||
get() = video.codec
|
||||
override val container: String
|
||||
get() = video.container
|
||||
override val duration: Long
|
||||
get() = video.duration;
|
||||
override val height: Int
|
||||
get() = video.height;
|
||||
override val width: Int
|
||||
get() = video.width;
|
||||
override val priority: Boolean
|
||||
get() = video.priority;
|
||||
|
||||
override fun generate(): String? {
|
||||
val videoDash = video.generate();
|
||||
val audioDash = audio.generate();
|
||||
if(videoDash != null && audioDash == null) return videoDash;
|
||||
if(audioDash != null && videoDash == null) return audioDash;
|
||||
if(videoDash == null) return null;
|
||||
|
||||
//TODO: Temporary simple solution..make more reliable version
|
||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||
if(audioAdaptationSet != null) {
|
||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
}
|
||||
else
|
||||
return videoDash;
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
|
||||
}
|
||||
}
|
||||
+8
-53
@@ -10,12 +10,10 @@ import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.orNull
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
|
||||
@@ -23,17 +21,9 @@ abstract class JSSource {
|
||||
protected val _plugin: JSClient;
|
||||
protected val _config: IV8PluginConfig;
|
||||
protected val _obj: V8ValueObject;
|
||||
|
||||
val hasRequestModifier: Boolean;
|
||||
private val _requestModifier: JSRequest?;
|
||||
|
||||
val hasRequestExecutor: Boolean;
|
||||
private val _requestExecutor: JSRequest?;
|
||||
|
||||
val requiresCustomDatasource: Boolean get() {
|
||||
return hasRequestModifier || hasRequestExecutor;
|
||||
}
|
||||
|
||||
val type : String;
|
||||
|
||||
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
||||
@@ -46,11 +36,6 @@ abstract class JSSource {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
}
|
||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
||||
|
||||
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
}
|
||||
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
|
||||
}
|
||||
|
||||
fun getRequestModifier(): IRequestModifier? {
|
||||
@@ -59,38 +44,20 @@ abstract class JSSource {
|
||||
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
||||
};
|
||||
|
||||
if (!hasRequestModifier || _obj.isClosed)
|
||||
if (!hasRequestModifier || _obj.isClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
||||
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
||||
};
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
if (result !is V8ValueObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSRequestModifier(_plugin, result)
|
||||
}
|
||||
open fun getRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasRequestExecutor || _obj.isClosed)
|
||||
return null;
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
|
||||
_obj.invoke("getRequestExecutor", arrayOf<Any>());
|
||||
};
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null;
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
fun getUnderlyingPlugin(): JSClient? {
|
||||
return _plugin;
|
||||
}
|
||||
fun getUnderlyingObject(): V8ValueObject? {
|
||||
return _obj;
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_AUDIOURL = "AudioUrlSource";
|
||||
@@ -98,45 +65,33 @@ abstract class JSSource {
|
||||
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||
const val TYPE_DASH = "DashSource";
|
||||
const val TYPE_DASH_RAW = "DashRawSource";
|
||||
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
|
||||
const val TYPE_HLS = "HLSSource";
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||
else -> {
|
||||
Logger.w("JSSource", "Unknown video type ${type}");
|
||||
null;
|
||||
};
|
||||
else -> throw NotImplementedError("Unknown type ${type}");
|
||||
}
|
||||
}
|
||||
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
||||
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
|
||||
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
|
||||
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
|
||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
|
||||
|
||||
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
|
||||
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
|
||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||
else -> {
|
||||
Logger.w("JSSource", "Unknown audio type ${type}");
|
||||
null;
|
||||
};
|
||||
else -> throw NotImplementedError("Unknown type ${type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -23,11 +23,9 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
|
||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||
.filterNotNull()
|
||||
.toTypedArray();
|
||||
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
||||
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
||||
.filterNotNull()
|
||||
.toTypedArray();
|
||||
}
|
||||
}
|
||||
-1
@@ -21,7 +21,6 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||
.filterNotNull()
|
||||
.toTypedArray();
|
||||
}
|
||||
|
||||
|
||||
-5
@@ -33,9 +33,4 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
||||
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
|
||||
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RangeSource(url=[${getVideoUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
|
||||
return super.toString()
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,13 @@ import android.net.Uri
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.util.Xml
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
@@ -29,23 +25,16 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSou
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.builders.DashBuilder
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.mdns.DnsService
|
||||
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.toUrlAddress
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -53,15 +42,17 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.UUID
|
||||
import javax.jmdns.JmDNS
|
||||
import javax.jmdns.ServiceEvent
|
||||
import javax.jmdns.ServiceListener
|
||||
import javax.jmdns.ServiceTypeListener
|
||||
|
||||
class StateCasting {
|
||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||
private var _jmDNS: JmDNS? = null;
|
||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||
|
||||
private val _castServer = ManagedHttpServer(9999);
|
||||
@@ -78,51 +69,105 @@ class StateCasting {
|
||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||
var activeDevice: CastingDevice? = null;
|
||||
private var _videoExecutor: JSRequestExecutor? = null
|
||||
private var _audioExecutor: JSRequestExecutor? = null
|
||||
private val _client = ManagedHttpClient();
|
||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
|
||||
"_googlecast._tcp.local",
|
||||
"_airplay._tcp.local",
|
||||
"_fastcast._tcp.local",
|
||||
"_fcast._tcp.local"
|
||||
)) { handleServiceUpdated(it) }
|
||||
|
||||
val isCasting: Boolean get() = activeDevice != null;
|
||||
|
||||
private fun handleServiceUpdated(services: List<DnsService>) {
|
||||
for (s in services) {
|
||||
//TODO: Addresses IPv4 only?
|
||||
val addresses = s.addresses.toTypedArray()
|
||||
val port = s.port.toInt()
|
||||
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
||||
if (s.name.endsWith("._googlecast._tcp.local")) {
|
||||
if (name == null) {
|
||||
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
||||
}
|
||||
private val _chromecastServiceListener = object : ServiceListener {
|
||||
override fun serviceAdded(event: ServiceEvent) {
|
||||
Logger.i(TAG, "ChromeCast service added: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
addOrUpdateChromeCastDevice(name, addresses, port)
|
||||
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
||||
if (name == null) {
|
||||
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
||||
override fun serviceRemoved(event: ServiceEvent) {
|
||||
Logger.i(TAG, "ChromeCast service removed: " + event.info);
|
||||
synchronized(devices) {
|
||||
val device = devices[event.info.name];
|
||||
if (device != null) {
|
||||
onDeviceRemoved.emit(device);
|
||||
}
|
||||
|
||||
addOrUpdateAirPlayDevice(name, addresses, port)
|
||||
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
||||
if (name == null) {
|
||||
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
||||
}
|
||||
|
||||
addOrUpdateFastCastDevice(name, addresses, port)
|
||||
} else if (s.name.endsWith("._fcast._tcp.local")) {
|
||||
if (name == null) {
|
||||
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
|
||||
}
|
||||
|
||||
addOrUpdateFastCastDevice(name, addresses, port)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serviceResolved(event: ServiceEvent) {
|
||||
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
||||
addOrUpdateChromeCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
||||
}
|
||||
}
|
||||
|
||||
private val _airPlayServiceListener = object : ServiceListener {
|
||||
override fun serviceAdded(event: ServiceEvent) {
|
||||
Logger.i(TAG, "AirPlay service added: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
override fun serviceRemoved(event: ServiceEvent) {
|
||||
Logger.i(TAG, "AirPlay service removed: " + event.info);
|
||||
synchronized(devices) {
|
||||
val device = devices[event.info.name];
|
||||
if (device != null) {
|
||||
onDeviceRemoved.emit(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serviceResolved(event: ServiceEvent) {
|
||||
Logger.i(TAG, "AirPlay service resolved: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
||||
addOrUpdateAirPlayDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
||||
}
|
||||
}
|
||||
|
||||
private val _fastCastServiceListener = object : ServiceListener {
|
||||
override fun serviceAdded(event: ServiceEvent) {
|
||||
Logger.i(TAG, "FastCast service added: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
override fun serviceRemoved(event: ServiceEvent) {
|
||||
Logger.i(TAG, "FastCast service removed: " + event.info);
|
||||
synchronized(devices) {
|
||||
val device = devices[event.info.name];
|
||||
if (device != null) {
|
||||
onDeviceRemoved.emit(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serviceResolved(event: ServiceEvent) {
|
||||
Logger.i(TAG, "FastCast service resolved: " + event.info);
|
||||
addOrUpdateDevice(event);
|
||||
}
|
||||
|
||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
||||
addOrUpdateFastCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
||||
}
|
||||
}
|
||||
|
||||
private val _serviceTypeListener = object : ServiceTypeListener {
|
||||
override fun serviceTypeAdded(event: ServiceEvent?) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Service type added (name: ${event.name}, type: ${event.type})");
|
||||
}
|
||||
|
||||
override fun subTypeForServiceTypeAdded(event: ServiceEvent?) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Sub type for service type added (name: ${event.name}, type: ${event.type})");
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUrl(context: Context, url: String) {
|
||||
@@ -191,30 +236,29 @@ class StateCasting {
|
||||
rememberedDevices.clear();
|
||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
||||
|
||||
_scopeIO.launch {
|
||||
try {
|
||||
val jmDNS = JmDNS.create(InetAddress.getLocalHost());
|
||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
||||
}
|
||||
|
||||
_jmDNS = jmDNS;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting service.", e);
|
||||
}
|
||||
}
|
||||
_castServer.start();
|
||||
enableDeveloper(true);
|
||||
|
||||
Logger.i(TAG, "CastingService started.");
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startDiscovering() {
|
||||
try {
|
||||
_serviceDiscoverer.start()
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopDiscovering() {
|
||||
try {
|
||||
_serviceDiscoverer.stop()
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
if (!_started)
|
||||
@@ -224,7 +268,25 @@ class StateCasting {
|
||||
|
||||
Logger.i(TAG, "CastingService stopping.")
|
||||
|
||||
stopDiscovering()
|
||||
val jmDNS = _jmDNS;
|
||||
if (jmDNS != null) {
|
||||
_scopeIO.launch {
|
||||
try {
|
||||
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
||||
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
||||
}
|
||||
|
||||
jmDNS.close();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to stop mDNS.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_scopeIO.cancel();
|
||||
_scopeMain.cancel();
|
||||
|
||||
@@ -374,26 +436,15 @@ class StateCasting {
|
||||
} else {
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||
if (isRawDash) {
|
||||
Logger.i(TAG, "Casting as raw DASH");
|
||||
|
||||
try {
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
}
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as HLS indirect");
|
||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as HLS indirect");
|
||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as DASH indirect");
|
||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
}
|
||||
Logger.i(TAG, "Casting as DASH indirect");
|
||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
@@ -401,22 +452,14 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||
Logger.i(TAG, "Casting as singular video");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
||||
Logger.i(TAG, "Casting as singular audio");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
@@ -424,7 +467,7 @@ class StateCasting {
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
@@ -437,26 +480,6 @@ class StateCasting {
|
||||
} else if (audioSource is LocalAudioSource) {
|
||||
Logger.i(TAG, "Casting as local audio");
|
||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||
} else if (videoSource is JSDashManifestRawSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
|
||||
}
|
||||
}
|
||||
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var str = listOf(
|
||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||
@@ -497,7 +520,7 @@ class StateCasting {
|
||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = url + videoPath;
|
||||
@@ -516,7 +539,7 @@ class StateCasting {
|
||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = url + audioPath;
|
||||
@@ -535,7 +558,7 @@ class StateCasting {
|
||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||
val ad = activeDevice ?: return listOf()
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
||||
val id = UUID.randomUUID()
|
||||
|
||||
val hlsPath = "/hls-${id}"
|
||||
@@ -631,7 +654,7 @@ class StateCasting {
|
||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
val dashPath = "/dash-${id}"
|
||||
@@ -644,11 +667,8 @@ class StateCasting {
|
||||
val audioUrl = url + audioPath;
|
||||
val subtitleUrl = url + subtitlePath;
|
||||
|
||||
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
|
||||
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, dashContent,
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
@@ -679,17 +699,13 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
val subtitlePath = "/subtitle-${id}";
|
||||
|
||||
val videoPath = "/video-${id}"
|
||||
val audioPath = "/audio-${id}"
|
||||
val subtitlePath = "/subtitle-${id}"
|
||||
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
|
||||
val videoUrl = videoSource?.getVideoUrl();
|
||||
val audioUrl = audioSource?.getAudioUrl();
|
||||
|
||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||
return@withContext subtitleSource.getSubtitlesURI();
|
||||
@@ -718,42 +734,26 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
|
||||
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||
|
||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
||||
Logger.v(TAG) { "Dash manifest: $content" };
|
||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||
|
||||
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||
}
|
||||
|
||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
|
||||
val id = UUID.randomUUID();
|
||||
val hlsPath = "/hls-${id}"
|
||||
val hlsUrl = url + hlsPath
|
||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||
|
||||
val headers = masterContext.headers.clone()
|
||||
@@ -780,7 +780,7 @@ class StateCasting {
|
||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||
return@HttpFunctionHandler
|
||||
return@HttpFuntionHandler
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
@@ -797,7 +797,7 @@ class StateCasting {
|
||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||
val newPlaylistUrl = url + newPlaylistPath;
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
val vpHeaders = vpContext.headers.clone()
|
||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
@@ -827,7 +827,7 @@ class StateCasting {
|
||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||
newPlaylistUrl = url + newPlaylistPath
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
val vpHeaders = vpContext.headers.clone()
|
||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
@@ -916,7 +916,7 @@ class StateCasting {
|
||||
|
||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
val hlsPath = "/hls-${id}"
|
||||
@@ -1044,9 +1044,9 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
val proxyStreams = ad !is FCastCastingDevice;
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
val dashPath = "/dash-${id}"
|
||||
@@ -1090,11 +1090,8 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, dashContent,
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
@@ -1120,166 +1117,6 @@ class StateCasting {
|
||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
}
|
||||
|
||||
private fun cleanExecutors() {
|
||||
if (_videoExecutor != null) {
|
||||
_videoExecutor?.cleanup()
|
||||
_videoExecutor = null
|
||||
}
|
||||
|
||||
if (_audioExecutor != null) {
|
||||
_audioExecutor?.cleanup()
|
||||
_audioExecutor = null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
cleanExecutors()
|
||||
_castServer.removeAllHandlers("castDashRaw")
|
||||
|
||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
val dashPath = "/dash-${id}"
|
||||
val videoPath = "/video-${id}"
|
||||
val audioPath = "/audio-${id}"
|
||||
val subtitlePath = "/subtitle-${id}"
|
||||
|
||||
val dashUrl = url + dashPath;
|
||||
Logger.i(TAG, "DASH url: $dashUrl");
|
||||
|
||||
val videoUrl = url + videoPath
|
||||
val audioUrl = url + audioPath
|
||||
|
||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||
return@withContext subtitleSource.getSubtitlesURI();
|
||||
} else null;
|
||||
|
||||
var subtitlesUrl: String? = null;
|
||||
if (subtitlesUri != null) {
|
||||
if(subtitlesUri.scheme == "file") {
|
||||
var content: String? = null;
|
||||
val inputStream = contentResolver.openInputStream(subtitlesUri);
|
||||
inputStream?.use { stream ->
|
||||
val reader = stream.bufferedReader();
|
||||
content = reader.use { it.readText() };
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
|
||||
subtitlesUrl = url + subtitlePath;
|
||||
} else {
|
||||
subtitlesUrl = subtitlesUri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
var dashContent = withContext(Dispatchers.IO) {
|
||||
//TODO: Include subtitlesURl in the future
|
||||
return@withContext if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||
} else if (audioSource != null) {
|
||||
audioSource.generate()
|
||||
} else if (videoSource != null) {
|
||||
videoSource.generate()
|
||||
} else {
|
||||
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||
null
|
||||
}
|
||||
} ?: throw Exception("Dash is null")
|
||||
|
||||
for (representation in representationRegex.findAll(dashContent)) {
|
||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||
return@replace it.value
|
||||
}
|
||||
|
||||
if (mediaType.startsWith("video/")) {
|
||||
return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||
} else if (mediaType.startsWith("audio/")) {
|
||||
return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||
} else {
|
||||
throw Exception("Expected audio or video")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoSource != null && !videoSource.hasRequestExecutor) {
|
||||
throw Exception("Video source without request executor not supported")
|
||||
}
|
||||
|
||||
if (audioSource != null && !audioSource.hasRequestExecutor) {
|
||||
throw Exception("Audio source without request executor not supported")
|
||||
}
|
||||
|
||||
if (audioSource != null && audioSource.hasRequestExecutor) {
|
||||
_audioExecutor = audioSource.getRequestExecutor()
|
||||
}
|
||||
|
||||
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||
_videoExecutor = videoSource.getRequestExecutor()
|
||||
}
|
||||
|
||||
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||
|
||||
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, dashContent,
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castDashRaw");
|
||||
|
||||
if (videoSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFunctionHandler("GET", videoPath) { httpContext ->
|
||||
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||
|
||||
val videoExecutor = _videoExecutor;
|
||||
if (videoExecutor != null) {
|
||||
val data = videoExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||
put("Content-Type", mediaType)
|
||||
}, data);
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castDashRaw");
|
||||
}
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||
|
||||
val audioExecutor = _audioExecutor;
|
||||
if (audioExecutor != null) {
|
||||
val data = audioExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||
put("Content-Type", mediaType)
|
||||
}, data);
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castDashRaw");
|
||||
}
|
||||
|
||||
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
|
||||
return listOf()
|
||||
}
|
||||
|
||||
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||
return when (deviceInfo.type) {
|
||||
CastProtocolType.CHROMECAST -> {
|
||||
@@ -1374,7 +1211,7 @@ class StateCasting {
|
||||
}
|
||||
} else {
|
||||
val newDevice = deviceFactory();
|
||||
this.devices[name] = newDevice;
|
||||
devices[name] = newDevice;
|
||||
|
||||
invokeEvents = {
|
||||
onDeviceAdded.emit(newDevice);
|
||||
@@ -1388,7 +1225,7 @@ class StateCasting {
|
||||
fun enableDeveloper(enableDev: Boolean){
|
||||
_castServer.removeAllHandlers("dev");
|
||||
if(enableDev) {
|
||||
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
|
||||
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
|
||||
if (context.query.containsKey("dashUrl")) {
|
||||
val dashUrl = context.query["dashUrl"];
|
||||
val html = "<div>\n" +
|
||||
@@ -1428,9 +1265,6 @@ class StateCasting {
|
||||
companion object {
|
||||
val instance: StateCasting = StateCasting();
|
||||
|
||||
private val representationRegex = Regex("<Representation .*?mimeType=\"(.*?)\".*?>(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL)
|
||||
private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL);
|
||||
|
||||
private val TAG = "StateCasting";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.google.gson.ExclusionStrategy
|
||||
import com.google.gson.FieldAttributes
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -571,7 +573,7 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
val resp = _client.get(body.url!!, body.headers);
|
||||
|
||||
context.respondCode(200,
|
||||
Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
|
||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
||||
context.query.getOrDefault("CT", "text/plain"));
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
|
||||
@@ -104,8 +104,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
super.show();
|
||||
Logger.i(TAG, "Dialog shown.");
|
||||
|
||||
StateCasting.instance.startDiscovering()
|
||||
|
||||
(_imageLoader.drawable as Animatable?)?.start();
|
||||
|
||||
_devices.clear();
|
||||
@@ -171,7 +169,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
(_imageLoader.drawable as Animatable?)?.stop();
|
||||
|
||||
StateCasting.instance.stopDiscovering()
|
||||
StateCasting.instance.onDeviceAdded.remove(this);
|
||||
StateCasting.instance.onDeviceChanged.remove(this);
|
||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
||||
|
||||
@@ -12,8 +12,6 @@ import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescri
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
@@ -27,14 +25,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.exceptions.DownloadException
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
@@ -44,7 +34,6 @@ import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import hasAnySource
|
||||
@@ -57,12 +46,9 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.Thread.sleep
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Executors
|
||||
@@ -70,7 +56,6 @@ import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.times
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class VideoDownload {
|
||||
@@ -86,50 +71,12 @@ class VideoDownload {
|
||||
|
||||
var targetPixelCount: Long? = null;
|
||||
var targetBitrate: Long? = null;
|
||||
var targetVideoName: String? = null;
|
||||
var targetAudioName: String? = null;
|
||||
|
||||
var videoSource: VideoUrlSource?;
|
||||
var audioSource: AudioUrlSource?;
|
||||
@Contextual
|
||||
@Transient
|
||||
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||
@Contextual
|
||||
@Transient
|
||||
val audioSourceToUse: IAudioSource? get () = if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
|
||||
|
||||
var requireVideoSource: Boolean = false;
|
||||
var requireAudioSource: Boolean = false;
|
||||
|
||||
@Contextual
|
||||
@Transient
|
||||
val isVideoDownloadReady: Boolean get() = !requireVideoSource ||
|
||||
((requiresLiveVideoSource && isLiveVideoSourceValid) || (!requiresLiveVideoSource && videoSource != null));
|
||||
@Contextual
|
||||
@Transient
|
||||
val isAudioDownloadReady: Boolean get() = !requireAudioSource ||
|
||||
((requiresLiveAudioSource && isLiveAudioSourceValid) || (!requiresLiveAudioSource && audioSource != null));
|
||||
|
||||
|
||||
var subtitleSource: SubtitleRawSource?;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
var prepareTime: OffsetDateTime? = null;
|
||||
|
||||
var requiresLiveVideoSource: Boolean = false;
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var videoSourceLive: JSSource? = null;
|
||||
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
|
||||
var requiresLiveAudioSource: Boolean = false;
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var audioSourceLive: JSSource? = null;
|
||||
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
|
||||
var hasVideoRequestExecutor: Boolean = false;
|
||||
var hasAudioRequestExecutor: Boolean = false;
|
||||
|
||||
var progress: Double = 0.0;
|
||||
var isCancelled = false;
|
||||
|
||||
@@ -171,32 +118,14 @@ class VideoDownload {
|
||||
this.subtitleSource = null;
|
||||
this.targetPixelCount = targetPixelCount;
|
||||
this.targetBitrate = targetBitrate;
|
||||
this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = false;
|
||||
this.requiresLiveAudioSource = false;
|
||||
this.targetVideoName = videoSource?.name;
|
||||
this.requireVideoSource = targetPixelCount != null
|
||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||
}
|
||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||
this.audioSource = if(audioSource is IAudioUrlSource) AudioUrlSource.fromUrlSource(audioSource) else null;
|
||||
this.videoSourceLive = if(videoSource is JSSource) videoSource else null;
|
||||
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
||||
this.videoSource = VideoUrlSource.fromUrlSource(videoSource);
|
||||
this.audioSource = AudioUrlSource.fromUrlSource(audioSource);
|
||||
this.subtitleSource = subtitleSource;
|
||||
this.prepareTime = OffsetDateTime.now();
|
||||
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||
this.targetVideoName = videoSource?.name;
|
||||
this.targetAudioName = audioSource?.name;
|
||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||
this.targetBitrate = if(audioSource != null) audioSource.bitrate.toLong() else null;
|
||||
this.requireVideoSource = videoSource != null;
|
||||
this.requireAudioSource = audioSource != null;
|
||||
}
|
||||
|
||||
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
||||
@@ -227,21 +156,9 @@ class VideoDownload {
|
||||
|
||||
suspend fun prepare(client: ManagedHttpClient) {
|
||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
||||
|
||||
//If live sources are required, ensure a live object is present
|
||||
if(requiresLiveVideoSource && !isLiveVideoSourceValid) {
|
||||
videoDetails = null;
|
||||
videoSource = null;
|
||||
videoSourceLive = null;
|
||||
}
|
||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||
videoDetails = null;
|
||||
audioSource = null;
|
||||
videoSourceLive = null;
|
||||
}
|
||||
if(video == null && videoDetails == null)
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null && targetVideoName == null && targetAudioName == null)
|
||||
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null)
|
||||
throw IllegalStateException("No sources or query values set");
|
||||
|
||||
//Fetch full video object and determine source
|
||||
@@ -275,35 +192,23 @@ class VideoDownload {
|
||||
videoSources.add(source)
|
||||
}
|
||||
}
|
||||
var vsource: IVideoSource? = null;
|
||||
|
||||
if(targetVideoName != null)
|
||||
vsource = videoSources.find { x -> x.isDownloadable() && x.name == targetVideoName };
|
||||
if(vsource == null && targetPixelCount == null)
|
||||
throw IllegalStateException("Could not find comparable downloadable video stream (No target pixel count)");
|
||||
if(vsource == null)
|
||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
|
||||
if(vsource == null) {
|
||||
videoSource = null;
|
||||
if(original.video.videoSources.size == 0)
|
||||
requireVideoSource = false;
|
||||
if(vsource != null) {
|
||||
if (vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||
}
|
||||
else if(vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
else if(vsource is JSSource && requiresLiveVideoSource)
|
||||
videoSourceLive = vsource;
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet) [" + vsource?.javaClass?.name + "]", false);
|
||||
}
|
||||
|
||||
if(audioSource == null && targetBitrate != null) {
|
||||
var audioSources = mutableListOf<IAudioSource>()
|
||||
val audioSources = arrayListOf<IAudioSource>()
|
||||
val video = original.video
|
||||
if (video is VideoUnMuxedSourceDescriptor) {
|
||||
for (source in video.audioSources) {
|
||||
if (source is IHLSManifestAudioSource) {
|
||||
if (source is IHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
@@ -321,43 +226,25 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
var asource: IAudioSource? = null;
|
||||
if(targetAudioName != null) {
|
||||
val filteredAudioSources = audioSources.filter { x -> x.isDownloadable() && x.name == targetAudioName }.toTypedArray();
|
||||
if(filteredAudioSources.size == 1)
|
||||
asource = filteredAudioSources.first();
|
||||
else if(filteredAudioSources.size > 1)
|
||||
audioSources = filteredAudioSources.toMutableList();
|
||||
}
|
||||
if(asource == null && targetBitrate == null)
|
||||
throw IllegalStateException("Could not find comparable downloadable video stream (No target bitrate)");
|
||||
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||
?: if(videoSource != null ) null
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
if(asource == null)
|
||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||
?: if(videoSource != null ) null
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
if(asource == null) {
|
||||
audioSource = null;
|
||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||
requireVideoSource = false;
|
||||
}
|
||||
else if(asource is IAudioUrlSource)
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||
else if(asource is JSSource && requiresLiveAudioSource)
|
||||
audioSourceLive = asource;
|
||||
else
|
||||
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
|
||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
||||
}
|
||||
|
||||
if(!isVideoDownloadReady)
|
||||
throw DownloadException("No valid sources found for video");
|
||||
if(!isAudioDownloadReady)
|
||||
throw DownloadException("No valid sources found for audio");
|
||||
if(videoSource == null && audioSource == null)
|
||||
throw DownloadException("No valid sources found for video/audio");
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||
Logger.i(TAG, "VideoDownload Download [${name}]");
|
||||
if(videoDetails == null || (videoSourceToUse == null && audioSourceToUse == null))
|
||||
if(videoDetails == null || (videoSource == null && audioSource == null))
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
||||
|
||||
@@ -366,19 +253,12 @@ class VideoDownload {
|
||||
|
||||
if(isCancelled) throw CancellationException("Download got cancelled");
|
||||
|
||||
val actualVideoSource = if(requiresLiveVideoSource && videoSourceLive is IVideoSource)
|
||||
videoSourceLive as IVideoSource?;
|
||||
else videoSource;
|
||||
val actualAudioSource = if(requiresLiveAudioSource && audioSourceLive is IAudioSource)
|
||||
audioSourceLive as IAudioSource?;
|
||||
else audioSource;
|
||||
|
||||
if(actualVideoSource != null) {
|
||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||
if(videoSource != null) {
|
||||
videoFileName = "${videoDetails!!.id.value!!} [${videoSource!!.width}x${videoSource!!.height}].${videoContainerToExtension(videoSource!!.container)}".sanitizeFileName();
|
||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||
if(audioSource != null) {
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
@@ -393,11 +273,10 @@ class VideoDownload {
|
||||
var lastAudioLength: Long = 0;
|
||||
var lastAudioRead: Long = 0;
|
||||
|
||||
if(actualVideoSource != null) {
|
||||
if(videoSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
Logger.i(TAG, "Started downloading video");
|
||||
|
||||
var lastEmit = 0L;
|
||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||
synchronized(progressLock) {
|
||||
lastVideoLength = length;
|
||||
@@ -410,34 +289,23 @@ class VideoDownload {
|
||||
val total = lastVideoRead + lastAudioRead;
|
||||
if(totalLength > 0) {
|
||||
val percentage = (total / totalLength.toDouble());
|
||||
onProgress?.invoke(percentage);
|
||||
progress = percentage;
|
||||
|
||||
val now = System.currentTimeMillis();
|
||||
if(now - lastEmit > 200) {
|
||||
lastEmit = System.currentTimeMillis();
|
||||
onProgress?.invoke(percentage);
|
||||
onProgressChanged.emit(percentage);
|
||||
}
|
||||
onProgressChanged.emit(percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(actualVideoSource is IVideoUrlSource)
|
||||
videoFileSize = when (videoSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
||||
videoFileSize = when (videoSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||
});
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
if(audioSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
Logger.i(TAG, "Started downloading audio");
|
||||
|
||||
var lastEmit = 0L;
|
||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||
synchronized(progressLock) {
|
||||
lastAudioLength = length;
|
||||
@@ -450,27 +318,17 @@ class VideoDownload {
|
||||
val total = lastVideoRead + lastAudioRead;
|
||||
if(totalLength > 0) {
|
||||
val percentage = (total / totalLength.toDouble());
|
||||
onProgress?.invoke(percentage);
|
||||
progress = percentage;
|
||||
|
||||
val now = System.currentTimeMillis();
|
||||
if(now - lastEmit > 200) {
|
||||
lastEmit = System.currentTimeMillis();
|
||||
onProgress?.invoke(percentage);
|
||||
onProgressChanged.emit(percentage);
|
||||
}
|
||||
onProgressChanged.emit(percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(actualAudioSource is IAudioUrlSource)
|
||||
audioFileSize = when (audioSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
||||
audioFileSize = when (audioSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||
});
|
||||
}
|
||||
if (subtitleSource != null) {
|
||||
@@ -540,20 +398,15 @@ class VideoDownload {
|
||||
|
||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||
val outputStream = segmentFile.outputStream()
|
||||
try {
|
||||
segmentFiles.add(segmentFile)
|
||||
segmentFiles.add(segmentFile)
|
||||
|
||||
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||
}
|
||||
|
||||
downloadedTotalLength += segmentLength
|
||||
} finally {
|
||||
outputStream.close()
|
||||
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||
}
|
||||
|
||||
downloadedTotalLength += segmentLength
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Combining segments into $targetFile");
|
||||
@@ -620,86 +473,6 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
|
||||
targetFile.createNewFile();
|
||||
|
||||
val sourceLength: Long?;
|
||||
val fileStream = FileOutputStream(targetFile);
|
||||
|
||||
try{
|
||||
var manifest = source.manifest;
|
||||
if(source.hasGenerate)
|
||||
manifest = source.generate();
|
||||
if(manifest == null)
|
||||
throw IllegalStateException("No manifest after generation");
|
||||
|
||||
//TODO: Temporary naive assume single-sourced dash
|
||||
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
|
||||
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
|
||||
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
||||
val foundTemplateUrl = foundTemplate.groupValues[1];
|
||||
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
|
||||
if(foundCues.count() <= 0)
|
||||
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||
|
||||
val executor = if(source is JSSource && source.hasRequestExecutor)
|
||||
source.getRequestExecutor();
|
||||
else
|
||||
null;
|
||||
val speedTracker = SpeedTracker(1000);
|
||||
|
||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||
|
||||
var written = 0;
|
||||
var indexCounter = 0;
|
||||
onProgress(foundCues.count().toLong(), 0, 0);
|
||||
for(cue in foundCues) {
|
||||
val t = cue.groupValues[1];
|
||||
val d = cue.groupValues[2];
|
||||
|
||||
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||
|
||||
val data = if(executor != null)
|
||||
executor.executeRequest(url, mapOf());
|
||||
else {
|
||||
val resp = client.get(url, mutableMapOf());
|
||||
if(!resp.isOk)
|
||||
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||
resp.body!!.bytes()
|
||||
}
|
||||
fileStream.write(data, 0, data.size);
|
||||
speedTracker.addWork(data.size.toLong());
|
||||
written += data.size;
|
||||
|
||||
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
|
||||
|
||||
indexCounter++;
|
||||
}
|
||||
sourceLength = written.toLong();
|
||||
|
||||
Logger.i(TAG, "$name downloadSource Finished");
|
||||
}
|
||||
catch(ioex: IOException) {
|
||||
if(targetFile.exists() ?: false)
|
||||
targetFile.delete();
|
||||
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||
throw Exception("Not enough space on device", ioex);
|
||||
else
|
||||
throw ioex;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
if(targetFile.exists() ?: false)
|
||||
targetFile.delete();
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
fileStream.close();
|
||||
}
|
||||
return sourceLength!!;
|
||||
}
|
||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
@@ -711,25 +484,17 @@ class VideoDownload {
|
||||
|
||||
try{
|
||||
val head = client.tryHead(videoUrl);
|
||||
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
||||
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||
{
|
||||
val maxParallel = if(relatedPlugin != null && relatedPlugin.config.maxDownloadParallelism > 0)
|
||||
relatedPlugin.config.maxDownloadParallelism else 99;
|
||||
val concurrency = Math.min(maxParallel, Settings.instance.downloads.getByteRangeThreadCount());
|
||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
||||
val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
|
||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})");
|
||||
sourceLength = head["content-length"]!!.toLong();
|
||||
onProgress(sourceLength, 0, 0);
|
||||
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "Download $name Sequential");
|
||||
try {
|
||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||
throw e
|
||||
}
|
||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "$name downloadSource Finished");
|
||||
@@ -753,19 +518,17 @@ class VideoDownload {
|
||||
return sourceLength!!;
|
||||
}
|
||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
val progressRate: Int = 4096 * 5;
|
||||
val progressRate: Int = 4096 * 25;
|
||||
var lastProgressCount: Int = 0;
|
||||
val speedRate: Int = 4096 * 5;
|
||||
val speedRate: Int = 4096 * 25;
|
||||
var readSinceLastSpeedTest: Long = 0;
|
||||
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
||||
|
||||
var lastSpeed: Long = 0;
|
||||
|
||||
val result = client.get(url);
|
||||
if (!result.isOk) {
|
||||
result.body?.close()
|
||||
if (!result.isOk)
|
||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||
}
|
||||
if (result.body == null)
|
||||
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
||||
|
||||
@@ -773,114 +536,41 @@ class VideoDownload {
|
||||
val sourceStream = result.body.byteStream();
|
||||
|
||||
var totalRead: Long = 0;
|
||||
try {
|
||||
var read: Int;
|
||||
val buffer = ByteArray(4096);
|
||||
var read: Int;
|
||||
|
||||
do {
|
||||
read = sourceStream.read(buffer);
|
||||
if (read < 0)
|
||||
break;
|
||||
val buffer = ByteArray(4096);
|
||||
|
||||
fileStream.write(buffer, 0, read);
|
||||
do {
|
||||
read = sourceStream.read(buffer);
|
||||
if (read < 0)
|
||||
break;
|
||||
|
||||
totalRead += read;
|
||||
fileStream.write(buffer, 0, read);
|
||||
|
||||
readSinceLastSpeedTest += read;
|
||||
if (totalRead.toDouble() / progressRate > lastProgressCount) {
|
||||
onProgress(sourceLength, totalRead, lastSpeed);
|
||||
lastProgressCount++;
|
||||
}
|
||||
if (readSinceLastSpeedTest > speedRate) {
|
||||
val lastSpeedTime = timeSinceLastSpeedTest;
|
||||
timeSinceLastSpeedTest = System.currentTimeMillis();
|
||||
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
||||
if (timeSince > 0)
|
||||
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
||||
readSinceLastSpeedTest = 0;
|
||||
}
|
||||
totalRead += read;
|
||||
|
||||
if (isCancelled)
|
||||
throw CancellationException("Cancelled");
|
||||
} while (read > 0);
|
||||
} finally {
|
||||
sourceStream.close()
|
||||
result.body.close()
|
||||
}
|
||||
readSinceLastSpeedTest += read;
|
||||
if (totalRead / progressRate > lastProgressCount) {
|
||||
onProgress(sourceLength, totalRead, lastSpeed);
|
||||
lastProgressCount++;
|
||||
}
|
||||
if (readSinceLastSpeedTest > speedRate) {
|
||||
val lastSpeedTime = timeSinceLastSpeedTest;
|
||||
timeSinceLastSpeedTest = System.currentTimeMillis();
|
||||
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
||||
if (timeSince > 0)
|
||||
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
||||
readSinceLastSpeedTest = 0;
|
||||
}
|
||||
|
||||
if (isCancelled)
|
||||
throw CancellationException("Cancelled");
|
||||
} while (read > 0);
|
||||
|
||||
lastSpeed = 0;
|
||||
onProgress(sourceLength, totalRead, 0);
|
||||
return sourceLength;
|
||||
}
|
||||
/*private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
val progressRate: Int = 4096 * 25
|
||||
var lastProgressCount: Int = 0
|
||||
val speedRate: Int = 4096 * 25
|
||||
var readSinceLastSpeedTest: Long = 0
|
||||
var timeSinceLastSpeedTest: Long = System.currentTimeMillis()
|
||||
|
||||
var lastSpeed: Long = 0
|
||||
|
||||
var totalRead: Long = 0
|
||||
var sourceLength: Long
|
||||
val buffer = ByteArray(4096)
|
||||
|
||||
var isPartialDownload = false
|
||||
var result: ManagedHttpClient.Response? = null
|
||||
do {
|
||||
result = client.get(url, if (isPartialDownload) hashMapOf("Range" to "bytes=$totalRead-") else hashMapOf())
|
||||
if (isPartialDownload) {
|
||||
if (result.code != 206)
|
||||
throw IllegalStateException("Failed to download source, byte range fallback failed. Web[${result.code}] Error")
|
||||
} else {
|
||||
if (!result.isOk)
|
||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error")
|
||||
}
|
||||
if (result.body == null)
|
||||
throw IllegalStateException("Failed to download source. Web[${result.code}] No response")
|
||||
|
||||
isPartialDownload = true
|
||||
sourceLength = result.body!!.contentLength()
|
||||
val sourceStream = result.body!!.byteStream()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
val read = sourceStream.read(buffer)
|
||||
if (read <= 0) {
|
||||
break
|
||||
}
|
||||
|
||||
fileStream.write(buffer, 0, read)
|
||||
|
||||
totalRead += read
|
||||
readSinceLastSpeedTest += read
|
||||
|
||||
if (totalRead / progressRate > lastProgressCount) {
|
||||
onProgress(sourceLength, totalRead, lastSpeed)
|
||||
lastProgressCount++
|
||||
}
|
||||
if (readSinceLastSpeedTest > speedRate) {
|
||||
val lastSpeedTime = timeSinceLastSpeedTest
|
||||
timeSinceLastSpeedTest = System.currentTimeMillis()
|
||||
val timeSince = timeSinceLastSpeedTest - lastSpeedTime
|
||||
if (timeSince > 0)
|
||||
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong()
|
||||
readSinceLastSpeedTest = 0
|
||||
}
|
||||
|
||||
if (isCancelled)
|
||||
throw CancellationException("Cancelled")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Sequential download was interrupted, trying to fallback to byte ranges", e)
|
||||
} finally {
|
||||
sourceStream.close()
|
||||
result.body?.close()
|
||||
}
|
||||
} while (totalRead < sourceLength)
|
||||
|
||||
onProgress(sourceLength, totalRead, 0)
|
||||
return sourceLength
|
||||
}*/
|
||||
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
||||
val progressRate: Int = 4096 * 5;
|
||||
var lastProgressCount: Int = 0;
|
||||
@@ -953,47 +643,23 @@ class VideoDownload {
|
||||
return tasks.map { it.get() };
|
||||
}
|
||||
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
||||
var retryCount = 0
|
||||
var lastException: Throwable? = null
|
||||
val toRead = rangeEnd - rangeStart;
|
||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
||||
if(!req.isOk)
|
||||
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
||||
if(req.body == null)
|
||||
throw IllegalStateException("Range request failed, No body");
|
||||
val read = req.body.contentLength();
|
||||
|
||||
while (retryCount <= 3) {
|
||||
try {
|
||||
val toRead = rangeEnd - rangeStart;
|
||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
||||
if (!req.isOk) {
|
||||
val bodyString = req.body?.string()
|
||||
req.body?.close()
|
||||
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
||||
}
|
||||
if (req.body == null)
|
||||
throw IllegalStateException("Range request failed, No body");
|
||||
val read = req.body.contentLength();
|
||||
if(read < toRead)
|
||||
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
||||
|
||||
if (read < toRead)
|
||||
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
||||
|
||||
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to download range (url=${url} bytes=${rangeStart}-${rangeEnd})", e)
|
||||
|
||||
retryCount++
|
||||
lastException = e
|
||||
|
||||
sleep(when (retryCount) {
|
||||
1 -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||
2 -> 2000 + ((Math.random() * 300.0).toLong() - 150)
|
||||
3 -> 4000 + ((Math.random() * 300.0).toLong() - 150)
|
||||
else -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
throw lastException!!
|
||||
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
fun validate() {
|
||||
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
||||
if(videoSourceToUse != null) {
|
||||
if(videoSource != null) {
|
||||
if(videoFilePath == null)
|
||||
throw IllegalStateException("Missing video file name after download");
|
||||
val expectedFile = File(videoFilePath!!);
|
||||
@@ -1004,7 +670,7 @@ class VideoDownload {
|
||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||
}
|
||||
}
|
||||
if(audioSourceToUse != null) {
|
||||
if(audioSource != null) {
|
||||
if(audioFilePath == null)
|
||||
throw IllegalStateException("Missing audio file name after download");
|
||||
val expectedFile = File(audioFilePath!!);
|
||||
@@ -1026,15 +692,15 @@ class VideoDownload {
|
||||
fun complete() {
|
||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSource!!, it, videoFileSize ?: 0) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSource!!, it, audioFileSize ?: 0) };
|
||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||
|
||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||
localVideoSource.streamMetaData = (videoSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||
if(localVideoSource != null && videoSource != null && videoSource is IStreamMetaDataSource)
|
||||
localVideoSource.streamMetaData = (videoSource as IStreamMetaDataSource).streamMetaData;
|
||||
|
||||
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
|
||||
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||
if(localAudioSource != null && audioSource != null && audioSource is IStreamMetaDataSource)
|
||||
localAudioSource.streamMetaData = (audioSource as IStreamMetaDataSource).streamMetaData;
|
||||
|
||||
if(existing != null) {
|
||||
existing.videoSerialized = videoDetails!!;
|
||||
@@ -1091,9 +757,6 @@ class VideoDownload {
|
||||
const val GROUP_PLAYLIST = "Playlist";
|
||||
const val GROUP_WATCHLATER= "WatchLater";
|
||||
|
||||
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||
|
||||
fun videoContainerToExtension(container: String): String? {
|
||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||
return "mp4";
|
||||
@@ -1140,27 +803,4 @@ class VideoDownload {
|
||||
return "subtitle";
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedTracker {
|
||||
private val segmentStart: Long;
|
||||
private val intervalMs: Long;
|
||||
private var workDone: Long;
|
||||
var lastSpeed: Long;
|
||||
constructor(intervalMs: Long) {
|
||||
segmentStart = System.currentTimeMillis();
|
||||
this.intervalMs = intervalMs;
|
||||
this.workDone = 0;
|
||||
this.lastSpeed = 0;
|
||||
}
|
||||
fun addWork(work: Long) {
|
||||
val now = System.currentTimeMillis();
|
||||
if((now - segmentStart) > intervalMs)
|
||||
{
|
||||
lastSpeed = workDone;
|
||||
workDone = 0;
|
||||
}
|
||||
workDone += work;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.interop.options.V8Flags
|
||||
import com.caoccao.javet.interop.options.V8RuntimeOptions
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
@@ -135,10 +133,9 @@ class V8Plugin {
|
||||
synchronized(_runtimeLock) {
|
||||
if (_runtime != null)
|
||||
return;
|
||||
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||
|
||||
val host = V8Host.getV8Instance();
|
||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||
|
||||
_runtime = host.createV8Runtime(options);
|
||||
if (!host.isIsolateCreated)
|
||||
throw IllegalStateException("Isolate not created");
|
||||
|
||||
@@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.packages
|
||||
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.annotations.V8Property
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -51,20 +49,9 @@ class PackageBridge : V8Package {
|
||||
fun buildFlavor(): String {
|
||||
return BuildConfig.FLAVOR;
|
||||
}
|
||||
@V8Property
|
||||
fun buildSpecVersion(): Int {
|
||||
return JSClientConstants.PLUGIN_SPEC_VERSION;
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun dispose(value: V8Value) {
|
||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||
value.close();
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun toast(str: String) {
|
||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.toast(str);
|
||||
|
||||
@@ -68,10 +68,6 @@ class PackageDOMParser : V8Package {
|
||||
return result;
|
||||
}
|
||||
@V8Property
|
||||
fun parentElement(): DOMNode? {
|
||||
return parentNode();
|
||||
}
|
||||
@V8Property
|
||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||
@V8Property
|
||||
fun innerHTML(): String = _element.html();
|
||||
@@ -80,8 +76,6 @@ class PackageDOMParser : V8Package {
|
||||
@V8Property
|
||||
fun textContent(): String = _element.text();
|
||||
@V8Property
|
||||
fun tagName(): String = _element.tagName().uppercase();
|
||||
@V8Property
|
||||
fun text(): String = _element.text().ifEmpty { data() };
|
||||
@V8Property
|
||||
fun data(): String = _element.data();
|
||||
|
||||
@@ -7,11 +7,7 @@ import com.caoccao.javet.enums.V8ConversionMode
|
||||
import com.caoccao.javet.enums.V8ProxyMode
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
|
||||
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
@@ -20,9 +16,6 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
@@ -71,44 +64,33 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
||||
return if(useAuth)
|
||||
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||
_packageClientAuth.request(method, url, headers)
|
||||
else
|
||||
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||
_packageClient.request(method, url, headers);
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
||||
return if(useAuth)
|
||||
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||
_packageClientAuth.requestWithBody(method, url, body, headers)
|
||||
else
|
||||
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||
_packageClient.requestWithBody(method, url, body, headers);
|
||||
}
|
||||
@V8Function
|
||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
||||
return if(useAuth)
|
||||
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||
_packageClientAuth.GET(url, headers)
|
||||
else
|
||||
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
_packageClient.GET(url, headers);
|
||||
}
|
||||
@V8Function
|
||||
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||
|
||||
val client = if(useAuth) _packageClientAuth else _packageClient;
|
||||
|
||||
if(body is V8ValueString)
|
||||
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is String)
|
||||
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is V8ValueTypedArray)
|
||||
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is ByteArray)
|
||||
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
||||
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
||||
return if(useAuth)
|
||||
_packageClientAuth.POST(url, body, headers)
|
||||
else
|
||||
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||
_packageClient.POST(url, body, headers);
|
||||
}
|
||||
|
||||
@V8Function
|
||||
@@ -129,19 +111,8 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
}
|
||||
|
||||
interface IBridgeHttpResponse {
|
||||
val url: String;
|
||||
val code: Int;
|
||||
val headers: Map<String, List<String>>?;
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class BridgeHttpStringResponse(
|
||||
override val url: String,
|
||||
override val code: Int, val
|
||||
body: String?,
|
||||
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
|
||||
|
||||
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
||||
val isOk = code >= 200 && code < 300;
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
@@ -154,37 +125,6 @@ class PackageHttp: V8Package {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@kotlinx.serialization.Serializable
|
||||
class BridgeHttpBytesResponse: IV8Convertable, IBridgeHttpResponse {
|
||||
override val url: String;
|
||||
override val code: Int;
|
||||
val body: ByteArray?;
|
||||
override val headers: Map<String, List<String>>?;
|
||||
|
||||
val isOk: Boolean;
|
||||
|
||||
constructor(url: String, code: Int, body: ByteArray? = null, headers: Map<String, List<String>>? = null) {
|
||||
this.url = url;
|
||||
this.code = code;
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
this.isOk = code >= 200 && code < 300;
|
||||
}
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
val obj = runtime.createV8ValueObject();
|
||||
obj.set("url", url);
|
||||
obj.set("code", code);
|
||||
if(body != null) {
|
||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
||||
buffer.fromBytes(body);
|
||||
obj.set("body", body);
|
||||
}
|
||||
obj.set("headers", headers);
|
||||
obj.set("isOk", isOk);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||
@@ -207,12 +147,6 @@ class PackageHttp: V8Package {
|
||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
||||
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
||||
|
||||
@V8Function
|
||||
fun DUMMY(): BatchBuilder {
|
||||
_reqs.add(Pair(_package.getDefaultClient(false), RequestDescriptor("DUMMY", "", mutableMapOf())));
|
||||
return BatchBuilder(_package, _reqs);
|
||||
}
|
||||
|
||||
//Client-specific
|
||||
|
||||
@V8Function
|
||||
@@ -235,14 +169,12 @@ class PackageHttp: V8Package {
|
||||
|
||||
//Finalizer
|
||||
@V8Function
|
||||
fun execute(): List<IBridgeHttpResponse?> {
|
||||
fun execute(): List<BridgeHttpResponse> {
|
||||
return _reqs.parallelStream().map {
|
||||
if(it.second.method == "DUMMY")
|
||||
return@map null;
|
||||
if(it.second.body != null)
|
||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
||||
else
|
||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
||||
}
|
||||
.asSequence()
|
||||
.toList();
|
||||
@@ -298,116 +230,65 @@ class PackageHttp: V8Package {
|
||||
if(_client is JSHttpClient)
|
||||
_client.doAllowNewCookies = allow;
|
||||
}
|
||||
@V8Function
|
||||
fun setTimeout(timeoutMs: Int) {
|
||||
if(_client is JSHttpClient) {
|
||||
_client.setTimeout(timeoutMs.toLong());
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
return@logExceptions catchHttp {
|
||||
val client = _client;
|
||||
//logRequest(method, url, headers, null);
|
||||
val resp = client.requestMethod(method, url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp when(returnType) {
|
||||
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||
}
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@V8Function
|
||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val client = _client;
|
||||
//logRequest(method, url, headers, body);
|
||||
val resp = client.requestMethod(method, url, body, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
|
||||
return@catchHttp when(returnType) {
|
||||
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||
}
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val client = _client;
|
||||
//logRequest("GET", url, headers, null);
|
||||
val resp = client.get(url, headers);
|
||||
//val responseBody = resp.body?.string();
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||
|
||||
|
||||
return@catchHttp when(returnType) {
|
||||
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||
}
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@V8Function
|
||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val client = _client;
|
||||
//logRequest("POST", url, headers, body);
|
||||
val resp = client.post(url, body, headers);
|
||||
//val responseBody = resp.body?.string();
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||
|
||||
|
||||
return@catchHttp when(returnType) {
|
||||
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@V8Function
|
||||
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val client = _client;
|
||||
//logRequest("POST", url, headers, body);
|
||||
val resp = client.post(url, body, headers);
|
||||
//val responseBody = resp.body?.string();
|
||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||
|
||||
|
||||
return@catchHttp when(returnType) {
|
||||
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||
}
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -507,13 +388,13 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
}
|
||||
|
||||
private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
|
||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
||||
try{
|
||||
return handle();
|
||||
}
|
||||
//Forward timeouts
|
||||
catch(ex: SocketTimeoutException) {
|
||||
return BridgeHttpStringResponse("", 408, null);
|
||||
return BridgeHttpResponse("", 408, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,25 +514,20 @@ class PackageHttp: V8Package {
|
||||
val url: String,
|
||||
val headers: MutableMap<String, String>,
|
||||
val body: String? = null,
|
||||
val contentType: String? = null,
|
||||
val respType: ReturnType = ReturnType.STRING
|
||||
val contentType: String? = null
|
||||
)
|
||||
|
||||
private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
||||
try{
|
||||
return handle();
|
||||
}
|
||||
//Forward timeouts
|
||||
catch(ex: SocketTimeoutException) {
|
||||
return BridgeHttpStringResponse("", 408, null);
|
||||
return BridgeHttpResponse("", 408, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class ReturnType(val value: Int) {
|
||||
STRING(0),
|
||||
BYTES(1);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PackageHttp";
|
||||
|
||||
+1
-20
@@ -16,7 +16,6 @@ import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.dp
|
||||
@@ -223,13 +222,6 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
buttons.removeAt(faqIndex)
|
||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
||||
}
|
||||
//Force privacy to be third
|
||||
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
||||
if (privacyIndex != -1) {
|
||||
val button = buttons[privacyIndex]
|
||||
buttons.removeAt(privacyIndex)
|
||||
buttons.add(if (buttons.size == 2) 2 else 1, button)
|
||||
}
|
||||
|
||||
for (data in buttons) {
|
||||
val button = MenuButton(context, data, _fragment, true);
|
||||
@@ -313,16 +305,6 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||
"All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0,
|
||||
UIDialogs.Action("Cancel", {
|
||||
StateApp.instance.setPrivacyMode(false);
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Enable", {
|
||||
StateApp.instance.setPrivacyMode(true);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}))
|
||||
|
||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||
|
||||
@@ -388,8 +370,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||
}
|
||||
})
|
||||
//96 is reserved for privacy button
|
||||
//98 is reserved for buy button
|
||||
//98 is reversed for buy button
|
||||
//99 is reserved for more button
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -221,8 +221,8 @@ class CommentsFragment : MainFragment() {
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||
}
|
||||
|
||||
+4
-13
@@ -118,13 +118,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
|
||||
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
||||
_overlayContainer.let {
|
||||
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(
|
||||
context,
|
||||
R.drawable.ic_visibility_off,
|
||||
context.getString(R.string.hide),
|
||||
context.getString(R.string.hide_from_home),
|
||||
tag = "hide",
|
||||
call = { StateMeta.instance.addHiddenVideo(content.url);
|
||||
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
|
||||
{ StateMeta.instance.addHiddenVideo(content.url);
|
||||
if (fragment is HomeFragment) {
|
||||
val removeIndex = recyclerData.results.indexOf(content);
|
||||
if (removeIndex >= 0) {
|
||||
@@ -133,12 +128,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
}
|
||||
}
|
||||
}),
|
||||
SlideUpMenuItem(context,
|
||||
R.drawable.ic_playlist,
|
||||
context.getString(R.string.play_feed_as_queue),
|
||||
context.getString(R.string.play_entire_feed),
|
||||
tag = "playFeed",
|
||||
call = {
|
||||
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
|
||||
{
|
||||
val newQueue = listOf(content) + recyclerData.results
|
||||
.filterIsInstance<IPlatformVideo>()
|
||||
.filter { it != content };
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
|
||||
import com.futo.platformplayer.listeners.OrientationManager
|
||||
|
||||
abstract class MainFragment : MainActivityFragment() {
|
||||
open val isMainView: Boolean = false;
|
||||
@@ -45,6 +46,10 @@ abstract class MainFragment : MainActivityFragment() {
|
||||
|
||||
}
|
||||
|
||||
open fun onOrientationChanged(orientation: OrientationManager.Orientation) {
|
||||
|
||||
}
|
||||
|
||||
open fun onBackPressed(): Boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
+13
-25
@@ -109,31 +109,19 @@ class PlaylistFragment : MainFragment() {
|
||||
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
||||
|
||||
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
|
||||
SlideUpMenuItem(
|
||||
context,
|
||||
R.drawable.ic_list,
|
||||
context.getString(R.string.share_as_text),
|
||||
context.getString(R.string.share_as_a_list_of_video_urls),
|
||||
tag = 1,
|
||||
call = {
|
||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setText(reconstruction)
|
||||
.intent);
|
||||
}),
|
||||
SlideUpMenuItem(
|
||||
context,
|
||||
R.drawable.ic_move_up,
|
||||
context.getString(R.string.share_as_import),
|
||||
context.getString(R.string.share_as_a_import_file_for_grayjay),
|
||||
tag = 2,
|
||||
call = {
|
||||
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||
.setType("application/json")
|
||||
.setStream(shareUri)
|
||||
.intent);
|
||||
})
|
||||
SlideUpMenuItem(context, R.drawable.ic_list, context.getString(R.string.share_as_text), context.getString(R.string.share_as_a_list_of_video_urls), 1, {
|
||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setText(reconstruction)
|
||||
.intent);
|
||||
}),
|
||||
SlideUpMenuItem(context, R.drawable.ic_move_up, context.getString(R.string.share_as_import), context.getString(R.string.share_as_a_import_file_for_grayjay), 2, {
|
||||
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||
.setType("application/json")
|
||||
.setStream(shareUri)
|
||||
.intent);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+120
-113
@@ -3,34 +3,30 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.SimpleOrientationListener
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.listeners.AutoRotateChangeListener
|
||||
import com.futo.platformplayer.listeners.OrientationManager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||
|
||||
|
||||
class VideoDetailFragment : MainFragment {
|
||||
override val isMainView : Boolean = false;
|
||||
override val hasBottomBar: Boolean = true;
|
||||
@@ -41,32 +37,23 @@ class VideoDetailFragment : MainFragment {
|
||||
|
||||
private var _viewDetail : VideoDetailView? = null;
|
||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||
private lateinit var _autoRotateChangeListener: AutoRotateChangeListener
|
||||
private lateinit var _orientationListener: SimpleOrientationListener
|
||||
private var _currentOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
|
||||
var isFullscreen : Boolean = false;
|
||||
val onFullscreenChanged = Event1<Boolean>();
|
||||
var isTransitioning : Boolean = false
|
||||
private set;
|
||||
var isInPictureInPicture : Boolean = false
|
||||
private set;
|
||||
|
||||
private var _state: State = State.CLOSED
|
||||
|
||||
var state: State
|
||||
get() = _state
|
||||
set(value) {
|
||||
_state = value
|
||||
onStateChanged(value)
|
||||
}
|
||||
|
||||
var state: State = State.CLOSED;
|
||||
val currentUrl get() = _viewDetail?.currentUrl;
|
||||
|
||||
val onMinimize = Event0();
|
||||
val onTransitioning = Event1<Boolean>();
|
||||
val onMaximized = Event0();
|
||||
|
||||
var lastOrientation : OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
|
||||
private set;
|
||||
|
||||
private var _isInitialMaximize = true;
|
||||
|
||||
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
||||
@@ -86,29 +73,6 @@ class VideoDetailFragment : MainFragment {
|
||||
_viewDetail?.prevVideo(true);
|
||||
}
|
||||
|
||||
private fun onStateChanged(state: VideoDetailFragment.State) {
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
private fun updateOrientation() {
|
||||
val isMaximized = state == State.MAXIMIZED
|
||||
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
|
||||
val currentOrientation = _currentOrientation
|
||||
val isFs = isFullscreen
|
||||
|
||||
if (isFs && isMaximized) {
|
||||
if (isFullScreenPortraitAllowed) {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||
} else {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
}
|
||||
} else {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
}
|
||||
|
||||
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
Logger.i(TAG, "onShownWithView parameter=$parameter")
|
||||
@@ -134,6 +98,49 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOrientationChanged(orientation: OrientationManager.Orientation) {
|
||||
super.onOrientationChanged(orientation);
|
||||
|
||||
if(!_isActive || state != State.MAXIMIZED)
|
||||
return;
|
||||
|
||||
var newOrientation = orientation;
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED) {
|
||||
newOrientation = OrientationManager.Orientation.PORTRAIT;
|
||||
} else if(StatePlayer.instance.rotationLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Settings.instance.other.bypassRotationPrevention && orientation == OrientationManager.Orientation.PORTRAIT)
|
||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||
|
||||
if(lastOrientation == newOrientation)
|
||||
return;
|
||||
|
||||
activity?.let {
|
||||
if (isFullscreen) {
|
||||
if (Settings.instance.playback.fullscreenPortrait) {
|
||||
changeOrientation(newOrientation);
|
||||
} else {
|
||||
if(newOrientation == OrientationManager.Orientation.REVERSED_LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
changeOrientation(OrientationManager.Orientation.REVERSED_LANDSCAPE);
|
||||
else if(newOrientation == OrientationManager.Orientation.LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
|
||||
changeOrientation(OrientationManager.Orientation.LANDSCAPE);
|
||||
else if(Settings.instance.playback.isAutoRotate() && (newOrientation == OrientationManager.Orientation.PORTRAIT || newOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
|
||||
_viewDetail?.setFullscreen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(Settings.instance.playback.isAutoRotate() && (lastOrientation == OrientationManager.Orientation.PORTRAIT || lastOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
|
||||
lastOrientation = newOrientation;
|
||||
_viewDetail?.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastOrientation = newOrientation;
|
||||
}
|
||||
override fun onBackPressed(): Boolean {
|
||||
Logger.i(TAG, "onBackPressed")
|
||||
|
||||
@@ -147,7 +154,6 @@ class VideoDetailFragment : MainFragment {
|
||||
closeVideoDetails();
|
||||
return true;
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
super.onHide();
|
||||
}
|
||||
@@ -157,7 +163,7 @@ class VideoDetailFragment : MainFragment {
|
||||
_viewDetail?.preventPictureInPicture = true;
|
||||
}
|
||||
|
||||
fun minimizeVideoDetail() {
|
||||
fun minimizeVideoDetail(){
|
||||
_viewDetail?.setFullscreen(false);
|
||||
if(_view != null)
|
||||
_view!!.transitionToStart();
|
||||
@@ -259,6 +265,7 @@ class VideoDetailFragment : MainFragment {
|
||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
||||
});
|
||||
|
||||
context
|
||||
_view?.let {
|
||||
if (it.progress >= 0.5 && it.progress < 1.0)
|
||||
maximizeVideoDetail();
|
||||
@@ -266,40 +273,9 @@ class VideoDetailFragment : MainFragment {
|
||||
minimizeVideoDetail();
|
||||
}
|
||||
|
||||
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||
|
||||
maximizeVideoDetail();
|
||||
|
||||
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
_orientationListener = SimpleOrientationListener(requireActivity(), lifecycleScope)
|
||||
_orientationListener.onOrientationChanged.subscribe {
|
||||
_currentOrientation = it
|
||||
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
|
||||
|
||||
if (Settings.instance.playback.isAutoRotate()) {
|
||||
if (state == State.MAXIMIZED && !isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
|
||||
_viewDetail?.setFullscreen(true)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
||||
_viewDetail?.setFullscreen(false)
|
||||
return@subscribe
|
||||
}
|
||||
}
|
||||
|
||||
updateOrientation()
|
||||
}
|
||||
return _view!!;
|
||||
}
|
||||
|
||||
@@ -357,6 +333,11 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
}
|
||||
|
||||
val realOrientation = if(activity is MainActivity) (activity as MainActivity).orientation else lastOrientation;
|
||||
Logger.i(TAG, "Real orientation on boot ${realOrientation}, lastOrientation: ${lastOrientation}");
|
||||
if(realOrientation != lastOrientation)
|
||||
onOrientationChanged(realOrientation);
|
||||
|
||||
StateCasting.instance.onResume();
|
||||
}
|
||||
override fun onPause() {
|
||||
@@ -398,12 +379,6 @@ class VideoDetailFragment : MainFragment {
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView();
|
||||
Logger.v(TAG, "onDestroyMainView");
|
||||
_autoRotateChangeListener?.unregister()
|
||||
_orientationListener.stopListening()
|
||||
|
||||
SettingsActivity.settingsActivityClosed.remove(this)
|
||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||
|
||||
_viewDetail?.let {
|
||||
_viewDetail = null;
|
||||
it.onDestroy();
|
||||
@@ -411,6 +386,13 @@ class VideoDetailFragment : MainFragment {
|
||||
_view = null;
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||
onOrientationChanged(lastOrientation);
|
||||
};
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
@@ -426,38 +408,63 @@ class VideoDetailFragment : MainFragment {
|
||||
onMaximized.clear();
|
||||
}
|
||||
|
||||
|
||||
private fun hideSystemUI() {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.statusBars())
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSystemUI() {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.show(WindowInsets.Type.statusBars())
|
||||
controller.show(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFullscreenChanged(fullscreen : Boolean) {
|
||||
isFullscreen = fullscreen;
|
||||
onFullscreenChanged.emit(isFullscreen);
|
||||
|
||||
if (isFullscreen) {
|
||||
hideSystemUI()
|
||||
} else {
|
||||
showSystemUI()
|
||||
activity?.let {
|
||||
if (fullscreen) {
|
||||
if (Settings.instance.playback.fullscreenPortrait) {
|
||||
changeOrientation(lastOrientation);
|
||||
} else {
|
||||
var orient = lastOrientation;
|
||||
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
|
||||
orient = OrientationManager.Orientation.LANDSCAPE;
|
||||
changeOrientation(orient);
|
||||
}
|
||||
}
|
||||
else
|
||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||
}
|
||||
|
||||
updateOrientation();
|
||||
isFullscreen = fullscreen;
|
||||
_view?.allowMotion = !fullscreen;
|
||||
}
|
||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
||||
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
||||
activity?.let {
|
||||
when (orientation) {
|
||||
OrientationManager.Orientation.LANDSCAPE -> {
|
||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
_view?.allowMotion = false;
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
||||
controller.hide(WindowInsetsCompat.Type.statusBars());
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
||||
}
|
||||
}
|
||||
OrientationManager.Orientation.REVERSED_LANDSCAPE -> {
|
||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
|
||||
_view?.allowMotion = false;
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
||||
controller.hide(WindowInsetsCompat.Type.statusBars());
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
_view?.allowMotion = true;
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(it.window, true)
|
||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
||||
controller.show(WindowInsetsCompat.Type.statusBars());
|
||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "VideoDetailFragment";
|
||||
|
||||
+47
-124
@@ -23,6 +23,7 @@ import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.WindowManager
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -58,6 +59,8 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
@@ -72,7 +75,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
@@ -100,7 +102,6 @@ import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -113,7 +114,6 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
@@ -660,8 +660,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_container_content_browser.goto(navUrl);
|
||||
//switchContentView(_container_content_browser);
|
||||
@@ -694,7 +694,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastAudioSource = null;
|
||||
_lastSubtitleSource = null;
|
||||
video = null;
|
||||
_player.clear();
|
||||
cleanupPlaybackTracker();
|
||||
Logger.i(TAG, "Keep screen on unset onClose")
|
||||
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
@@ -1171,8 +1170,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
//@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
_didTriggerDatasourceErrroCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
|
||||
if(newVideo && this.video?.url == videoDetail.url)
|
||||
return;
|
||||
@@ -1239,25 +1236,18 @@ class VideoDetailView : ConstraintLayout {
|
||||
}*/
|
||||
}
|
||||
try {
|
||||
if(!StateApp.instance.privateMode) {
|
||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||
var tracker = video.getPlaybackTracker()
|
||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||
var tracker = video.getPlaybackTracker()
|
||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
|
||||
if (tracker == null) {
|
||||
stopwatch.reset()
|
||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||
Logger.i(
|
||||
TAG,
|
||||
"StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
|
||||
)
|
||||
}
|
||||
|
||||
if (me.video == video)
|
||||
me._playbackTracker = tracker;
|
||||
if (tracker == null) {
|
||||
stopwatch.reset()
|
||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||
Logger.i(TAG, "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
}
|
||||
else if(me.video == video)
|
||||
me._playbackTracker = null;
|
||||
|
||||
if(me.video == video)
|
||||
me._playbackTracker = tracker;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Playback tracker failed", ex);
|
||||
@@ -1461,8 +1451,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
StatePlayer.instance.setCurrentlyPlaying(video);
|
||||
|
||||
_liveChat?.stop();
|
||||
_liveChat = null;
|
||||
if(video.isLive && video.live != null) {
|
||||
loadLiveChat(video);
|
||||
}
|
||||
@@ -1659,7 +1647,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private var _didTriggerDatasourceErrroCount = 0;
|
||||
private var _didTriggerDatasourceError = false;
|
||||
private fun onDataSourceError(exception: Throwable) {
|
||||
Logger.e(TAG, "onDataSourceError", exception);
|
||||
@@ -1669,49 +1656,26 @@ class VideoDetailView : ConstraintLayout {
|
||||
return;
|
||||
val config = currentVideo.sourceConfig;
|
||||
|
||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||
if(!_didTriggerDatasourceError) {
|
||||
_didTriggerDatasourceError = true;
|
||||
_didTriggerDatasourceErrroCount++;
|
||||
|
||||
UIDialogs.toast("Block detected, attempting bypass");
|
||||
//return;
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
|
||||
if(newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if(previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
||||
else null;
|
||||
val newAudioSource = if(previousAudioSource != null)
|
||||
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
||||
else null;
|
||||
withContext(Dispatchers.Main) {
|
||||
video = newDetails;
|
||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(_didTriggerDatasourceErrroCount > 3) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||
context.getString(R.string.media_error),
|
||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||
null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||
UIDialogs.Action(context.getString(R.string.yes), {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, config.id);
|
||||
reloadVideo();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to reload video.", e)
|
||||
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||
UIDialogs.Action(context.getString(R.string.yes), {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, config.id);
|
||||
reloadVideo();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to reload video.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1808,21 +1772,19 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
val doDedup = Settings.instance.playback.simplifySources;
|
||||
|
||||
val bestVideoSources = if(doDedup) (videoSources?.map { it.height * it.width }
|
||||
val bestVideoSources = (videoSources?.map { it.height * it.width }
|
||||
?.distinct()
|
||||
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
|
||||
?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))
|
||||
?.distinct()
|
||||
?.filter { it != null }
|
||||
?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
|
||||
?.toList() ?: listOf();
|
||||
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
|
||||
val bestAudioSources = if(doDedup) audioSources
|
||||
val bestAudioSources = audioSources
|
||||
?.filter { it.container == bestAudioContainer }
|
||||
?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
|
||||
?.distinct()
|
||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||
?.toList() ?: listOf();
|
||||
|
||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||
@@ -1851,56 +1813,40 @@ class VideoDetailView : ConstraintLayout {
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
|
||||
*localVideoSources
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
tag = it,
|
||||
call = { handleSelectVideoTrack(it) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it,
|
||||
{ handleSelectVideoTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(localAudioSource?.isNotEmpty() == true)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
|
||||
*localAudioSource
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
it.bitrate.toHumanBitrate(),
|
||||
tag = it,
|
||||
call = { handleSelectAudioTrack(it) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
||||
{ handleSelectAudioTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(localSubtitleSources?.isNotEmpty() == true)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_subtitles), "subtitles",
|
||||
*localSubtitleSources
|
||||
.map {
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it,
|
||||
call = { handleSelectSubtitleTrack(it) })
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
||||
{ handleSelectSubtitleTrack(it) })
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(liveStreamVideoFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
||||
*liveStreamVideoFormats
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it.label ?: it.containerMimeType ?: it.bitrate.toString(),
|
||||
"${it.width}x${it.height}",
|
||||
tag = it,
|
||||
call = { _player.selectVideoTrack(it.height) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
|
||||
{ _player.selectVideoTrack(it.height) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||
*liveStreamAudioFormats
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_music,
|
||||
"${it.label ?: it.containerMimeType} ${it.bitrate}",
|
||||
"",
|
||||
tag = it,
|
||||
call = { _player.selectAudioTrack(it.bitrate) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", it,
|
||||
{ _player.selectAudioTrack(it.bitrate) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
|
||||
@@ -1908,38 +1854,24 @@ class VideoDetailView : ConstraintLayout {
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
|
||||
*bestVideoSources
|
||||
.map {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it!!.name,
|
||||
if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "",
|
||||
(prefix + it.codec.trim()).trim(),
|
||||
tag = it,
|
||||
call = { handleSelectVideoTrack(it) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it,
|
||||
{ handleSelectVideoTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(bestAudioSources.isNotEmpty())
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
|
||||
*bestAudioSources
|
||||
.map {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
it.bitrate.toHumanBitrate(),
|
||||
(prefix + it.codec.trim()).trim(),
|
||||
tag = it,
|
||||
call = { handleSelectAudioTrack(it) });
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
||||
{ handleSelectAudioTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(video?.subtitles?.isNotEmpty() == true)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
|
||||
*video.subtitles
|
||||
.map {
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it,
|
||||
call = { handleSelectSubtitleTrack(it) })
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
||||
{ handleSelectSubtitleTrack(it) })
|
||||
}.toList().toTypedArray())
|
||||
else null);
|
||||
}
|
||||
@@ -2380,15 +2312,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
updateTracker(positionMilliseconds, isPlaying, false);
|
||||
|
||||
if(StateDeveloper.instance.isPlaybackTesting) {
|
||||
if((positionMilliseconds > 1000 * 65 || positionMilliseconds > (video!!.duration * 1000 - 1000))) {
|
||||
StateDeveloper.instance.testPlayback();
|
||||
}
|
||||
else if(video!!.duration > 70 && positionMilliseconds < 10000) {
|
||||
handleSeek(55000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
||||
|
||||
@@ -20,9 +20,6 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.Language
|
||||
@@ -47,8 +44,8 @@ class VideoHelper {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource;
|
||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IAudioUrlWidevineSource
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
|
||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource
|
||||
|
||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||
@@ -130,7 +127,7 @@ class VideoHelper {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
|
||||
val urlToUse = videoSource.getVideoUrl();
|
||||
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
|
||||
videoSource.duration * 1000,
|
||||
@@ -148,10 +145,10 @@ class VideoHelper {
|
||||
);
|
||||
|
||||
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
|
||||
return Pair(DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
||||
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
||||
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
|
||||
return@Resolver dataSpec;
|
||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()), manifestConfig);
|
||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build())
|
||||
}
|
||||
|
||||
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
|
||||
@@ -189,25 +186,5 @@ class VideoHelper {
|
||||
return@Resolver dataSpec;
|
||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(audioSource.getAudioUrl())).build())
|
||||
}
|
||||
|
||||
|
||||
fun estimateSourceSize(source: IVideoSource?): Int {
|
||||
if(source == null) return 0;
|
||||
if(source is IVideoSource) {
|
||||
if(source.bitrate ?: 0 <= 0 || source.duration.toInt() == 0)
|
||||
return 0;
|
||||
return (source.duration / 8).toInt() * source.bitrate!!;
|
||||
}
|
||||
else return 0;
|
||||
}
|
||||
fun estimateSourceSize(source: IAudioSource?): Int {
|
||||
if(source == null) return 0;
|
||||
if(source is IAudioSource) {
|
||||
if(source.bitrate <= 0 || source.duration?.toInt() ?: 0 == 0)
|
||||
return 0;
|
||||
return (source.duration!! / 8).toInt() * source.bitrate;
|
||||
}
|
||||
else return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.futo.platformplayer.listeners
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
|
||||
class AutoRotateObserver(handler: Handler, private val onChangeCallback: () -> Unit) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onChangeCallback()
|
||||
}
|
||||
}
|
||||
|
||||
class AutoRotateChangeListener(context: Context, handler: Handler, private val onAutoRotateChanged: (Boolean) -> Unit) {
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
private val autoRotateObserver = AutoRotateObserver(handler) {
|
||||
val isAutoRotateEnabled = isAutoRotateEnabled()
|
||||
onAutoRotateChanged(isAutoRotateEnabled)
|
||||
}
|
||||
|
||||
init {
|
||||
contentResolver.registerContentObserver(
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
|
||||
false,
|
||||
autoRotateObserver
|
||||
)
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
contentResolver.unregisterContentObserver(autoRotateObserver)
|
||||
}
|
||||
|
||||
private fun isAutoRotateEnabled(): Boolean {
|
||||
return Settings.System.getInt(
|
||||
contentResolver,
|
||||
Settings.System.ACCELEROMETER_ROTATION,
|
||||
0
|
||||
) == 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.futo.platformplayer.listeners
|
||||
|
||||
import android.content.Context
|
||||
import android.view.OrientationEventListener
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
||||
class OrientationManager : OrientationEventListener {
|
||||
|
||||
val onOrientationChanged = Event1<Orientation>();
|
||||
|
||||
var orientation : Orientation = Orientation.PORTRAIT;
|
||||
|
||||
constructor(context: Context) : super(context) { }
|
||||
|
||||
//TODO: Something weird is going on here
|
||||
//TODO: Old implementation felt pretty good for me, but now with 0 deadzone still feels bad, even though code should be identical?
|
||||
override fun onOrientationChanged(orientationAnglep: Int) {
|
||||
if (orientationAnglep == -1) return
|
||||
|
||||
val deadZone = Settings.instance.playback.getAutoRotateDeadZoneDegrees()
|
||||
val isInDeadZone = when (orientation) {
|
||||
Orientation.PORTRAIT -> orientationAnglep in 0 until (60 - deadZone) || orientationAnglep in (300 + deadZone) .. 360
|
||||
Orientation.REVERSED_LANDSCAPE -> orientationAnglep in (60 + deadZone) until (140 - deadZone)
|
||||
Orientation.REVERSED_PORTRAIT -> orientationAnglep in (140 + deadZone) until (220 - deadZone)
|
||||
Orientation.LANDSCAPE -> orientationAnglep in (220 + deadZone) until (300 - deadZone)
|
||||
}
|
||||
|
||||
if (isInDeadZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
val newOrientation = when (orientationAnglep) {
|
||||
in 60 until 140 -> Orientation.REVERSED_LANDSCAPE
|
||||
in 140 until 220 -> Orientation.REVERSED_PORTRAIT
|
||||
in 220 until 300 -> Orientation.LANDSCAPE
|
||||
else -> Orientation.PORTRAIT
|
||||
}
|
||||
|
||||
Logger.i("OrientationManager", "Orientation=$newOrientation orientationAnglep=$orientationAnglep");
|
||||
|
||||
if (newOrientation != orientation) {
|
||||
orientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Perhaps just use ActivityInfo orientations instead..
|
||||
enum class Orientation {
|
||||
PORTRAIT,
|
||||
LANDSCAPE,
|
||||
REVERSED_PORTRAIT,
|
||||
REVERSED_LANDSCAPE
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
data class BroadcastService(
|
||||
val deviceName: String,
|
||||
val serviceName: String,
|
||||
val port: UShort,
|
||||
val ttl: UInt,
|
||||
val weight: UShort,
|
||||
val priority: UShort,
|
||||
val texts: List<String>? = null
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
enum class QueryResponse(val value: Byte) {
|
||||
Query(0),
|
||||
Response(1)
|
||||
}
|
||||
|
||||
enum class DnsOpcode(val value: Byte) {
|
||||
StandardQuery(0),
|
||||
InverseQuery(1),
|
||||
ServerStatusRequest(2)
|
||||
}
|
||||
|
||||
enum class DnsResponseCode(val value: Byte) {
|
||||
NoError(0),
|
||||
FormatError(1),
|
||||
ServerFailure(2),
|
||||
NameError(3),
|
||||
NotImplemented(4),
|
||||
Refused(5)
|
||||
}
|
||||
|
||||
data class DnsPacketHeader(
|
||||
val identifier: UShort,
|
||||
val queryResponse: Int,
|
||||
val opcode: Int,
|
||||
val authoritativeAnswer: Boolean,
|
||||
val truncated: Boolean,
|
||||
val recursionDesired: Boolean,
|
||||
val recursionAvailable: Boolean,
|
||||
val answerAuthenticated: Boolean,
|
||||
val nonAuthenticatedData: Boolean,
|
||||
val responseCode: DnsResponseCode
|
||||
)
|
||||
|
||||
data class DnsPacket(
|
||||
val header: DnsPacketHeader,
|
||||
val questions: List<DnsQuestion>,
|
||||
val answers: List<DnsResourceRecord>,
|
||||
val authorities: List<DnsResourceRecord>,
|
||||
val additionals: List<DnsResourceRecord>
|
||||
) {
|
||||
companion object {
|
||||
fun parse(data: ByteArray): DnsPacket {
|
||||
val span = data.asUByteArray()
|
||||
val flags = (span[2].toInt() shl 8 or span[3].toInt()).toUShort()
|
||||
val questionCount = (span[4].toInt() shl 8 or span[5].toInt()).toUShort()
|
||||
val answerCount = (span[6].toInt() shl 8 or span[7].toInt()).toUShort()
|
||||
val authorityCount = (span[8].toInt() shl 8 or span[9].toInt()).toUShort()
|
||||
val additionalCount = (span[10].toInt() shl 8 or span[11].toInt()).toUShort()
|
||||
|
||||
var position = 12
|
||||
|
||||
val questions = List(questionCount.toInt()) {
|
||||
DnsQuestion.parse(data, position).also { position = it.second }
|
||||
}.map { it.first }
|
||||
|
||||
val answers = List(answerCount.toInt()) {
|
||||
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||
}.map { it.first }
|
||||
|
||||
val authorities = List(authorityCount.toInt()) {
|
||||
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||
}.map { it.first }
|
||||
|
||||
val additionals = List(additionalCount.toInt()) {
|
||||
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||
}.map { it.first }
|
||||
|
||||
return DnsPacket(
|
||||
header = DnsPacketHeader(
|
||||
identifier = (span[0].toInt() shl 8 or span[1].toInt()).toUShort(),
|
||||
queryResponse = ((flags.toUInt() shr 15) and 0b1u).toInt(),
|
||||
opcode = ((flags.toUInt() shr 11) and 0b1111u).toInt(),
|
||||
authoritativeAnswer = (flags.toInt() shr 10) and 0b1 != 0,
|
||||
truncated = (flags.toInt() shr 9) and 0b1 != 0,
|
||||
recursionDesired = (flags.toInt() shr 8) and 0b1 != 0,
|
||||
recursionAvailable = (flags.toInt() shr 7) and 0b1 != 0,
|
||||
answerAuthenticated = (flags.toInt() shr 5) and 0b1 != 0,
|
||||
nonAuthenticatedData = (flags.toInt() shr 4) and 0b1 != 0,
|
||||
responseCode = DnsResponseCode.entries[flags.toInt() and 0b1111]
|
||||
),
|
||||
questions = questions,
|
||||
answers = answers,
|
||||
authorities = authorities,
|
||||
additionals = additionals
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
|
||||
enum class QuestionType(val value: UShort) {
|
||||
A(1u),
|
||||
NS(2u),
|
||||
MD(3u),
|
||||
MF(4u),
|
||||
CNAME(5u),
|
||||
SOA(6u),
|
||||
MB(7u),
|
||||
MG(8u),
|
||||
MR(9u),
|
||||
NULL(10u),
|
||||
WKS(11u),
|
||||
PTR(12u),
|
||||
HINFO(13u),
|
||||
MINFO(14u),
|
||||
MX(15u),
|
||||
TXT(16u),
|
||||
RP(17u),
|
||||
AFSDB(18u),
|
||||
SIG(24u),
|
||||
KEY(25u),
|
||||
AAAA(28u),
|
||||
LOC(29u),
|
||||
SRV(33u),
|
||||
NAPTR(35u),
|
||||
KX(36u),
|
||||
CERT(37u),
|
||||
DNAME(39u),
|
||||
APL(42u),
|
||||
DS(43u),
|
||||
SSHFP(44u),
|
||||
IPSECKEY(45u),
|
||||
RRSIG(46u),
|
||||
NSEC(47u),
|
||||
DNSKEY(48u),
|
||||
DHCID(49u),
|
||||
NSEC3(50u),
|
||||
NSEC3PARAM(51u),
|
||||
TSLA(52u),
|
||||
SMIMEA(53u),
|
||||
HIP(55u),
|
||||
CDS(59u),
|
||||
CDNSKEY(60u),
|
||||
OPENPGPKEY(61u),
|
||||
CSYNC(62u),
|
||||
ZONEMD(63u),
|
||||
SVCB(64u),
|
||||
HTTPS(65u),
|
||||
EUI48(108u),
|
||||
EUI64(109u),
|
||||
TKEY(249u),
|
||||
TSIG(250u),
|
||||
URI(256u),
|
||||
CAA(257u),
|
||||
TA(32768u),
|
||||
DLV(32769u),
|
||||
AXFR(252u),
|
||||
IXFR(251u),
|
||||
OPT(41u),
|
||||
MAILB(253u),
|
||||
MALA(254u),
|
||||
All(252u)
|
||||
}
|
||||
|
||||
enum class QuestionClass(val value: UShort) {
|
||||
IN(1u),
|
||||
CS(2u),
|
||||
CH(3u),
|
||||
HS(4u),
|
||||
All(255u)
|
||||
}
|
||||
|
||||
data class DnsQuestion(
|
||||
override val name: String,
|
||||
override val type: Int,
|
||||
override val clazz: Int,
|
||||
val queryUnicast: Boolean
|
||||
) : DnsResourceRecordBase(name, type, clazz) {
|
||||
companion object {
|
||||
fun parse(data: ByteArray, startPosition: Int): Pair<DnsQuestion, Int> {
|
||||
val span = data.asUByteArray()
|
||||
var position = startPosition
|
||||
val qname = span.readDomainName(position).also { position = it.second }
|
||||
val qtype = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||
position += 2
|
||||
val qclass = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||
position += 2
|
||||
|
||||
return DnsQuestion(
|
||||
name = qname.first,
|
||||
type = qtype.toInt(),
|
||||
queryUnicast = ((qclass.toInt() shr 15) and 0b1) != 0,
|
||||
clazz = qclass.toInt() and 0b111111111111111
|
||||
) to position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class DnsResourceRecordBase(
|
||||
open val name: String,
|
||||
open val type: Int,
|
||||
open val clazz: Int
|
||||
)
|
||||
@@ -1,514 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.math.pow
|
||||
import java.net.InetAddress
|
||||
|
||||
data class PTRRecord(val domainName: String)
|
||||
|
||||
data class ARecord(val address: InetAddress)
|
||||
|
||||
data class AAAARecord(val address: InetAddress)
|
||||
|
||||
data class MXRecord(val preference: UShort, val exchange: String)
|
||||
|
||||
data class CNAMERecord(val cname: String)
|
||||
|
||||
data class TXTRecord(val texts: List<String>)
|
||||
|
||||
data class SOARecord(
|
||||
val primaryNameServer: String,
|
||||
val responsibleAuthorityMailbox: String,
|
||||
val serialNumber: Int,
|
||||
val refreshInterval: Int,
|
||||
val retryInterval: Int,
|
||||
val expiryLimit: Int,
|
||||
val minimumTTL: Int
|
||||
)
|
||||
|
||||
data class SRVRecord(val priority: UShort, val weight: UShort, val port: UShort, val target: String)
|
||||
|
||||
data class NSRecord(val nameServer: String)
|
||||
|
||||
data class CAARecord(val flags: Byte, val tag: String, val value: String)
|
||||
|
||||
data class HINFORecord(val cpu: String, val os: String)
|
||||
|
||||
data class RPRecord(val mailbox: String, val txtDomainName: String)
|
||||
|
||||
|
||||
data class AFSDBRecord(val subtype: UShort, val hostname: String)
|
||||
data class LOCRecord(
|
||||
val version: Byte,
|
||||
val size: Double,
|
||||
val horizontalPrecision: Double,
|
||||
val verticalPrecision: Double,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Double
|
||||
) {
|
||||
companion object {
|
||||
fun decodeSizeOrPrecision(coded: Byte): Double {
|
||||
val baseValue = (coded.toInt() shr 4) and 0x0F
|
||||
val exponent = coded.toInt() and 0x0F
|
||||
return baseValue * 10.0.pow(exponent.toDouble())
|
||||
}
|
||||
|
||||
fun decodeLatitudeOrLongitude(coded: Int): Double {
|
||||
val arcSeconds = coded / 1E3
|
||||
return arcSeconds / 3600.0
|
||||
}
|
||||
|
||||
fun decodeAltitude(coded: Int): Double {
|
||||
return (coded / 100.0) - 100000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NAPTRRecord(
|
||||
val order: UShort,
|
||||
val preference: UShort,
|
||||
val flags: String,
|
||||
val services: String,
|
||||
val regexp: String,
|
||||
val replacement: String
|
||||
)
|
||||
|
||||
data class RRSIGRecord(
|
||||
val typeCovered: UShort,
|
||||
val algorithm: Byte,
|
||||
val labels: Byte,
|
||||
val originalTTL: UInt,
|
||||
val signatureExpiration: UInt,
|
||||
val signatureInception: UInt,
|
||||
val keyTag: UShort,
|
||||
val signersName: String,
|
||||
val signature: ByteArray
|
||||
)
|
||||
|
||||
data class KXRecord(val preference: UShort, val exchanger: String)
|
||||
|
||||
data class CERTRecord(val type: UShort, val keyTag: UShort, val algorithm: Byte, val certificate: ByteArray)
|
||||
|
||||
|
||||
|
||||
data class DNAMERecord(val target: String)
|
||||
|
||||
data class DSRecord(val keyTag: UShort, val algorithm: Byte, val digestType: Byte, val digest: ByteArray)
|
||||
|
||||
data class SSHFPRecord(val algorithm: Byte, val fingerprintType: Byte, val fingerprint: ByteArray)
|
||||
|
||||
data class TLSARecord(val usage: Byte, val selector: Byte, val matchingType: Byte, val certificateAssociationData: ByteArray)
|
||||
|
||||
data class SMIMEARecord(val usage: Byte, val selector: Byte, val matchingType: Byte, val certificateAssociationData: ByteArray)
|
||||
|
||||
data class URIRecord(val priority: UShort, val weight: UShort, val target: String)
|
||||
|
||||
data class NSECRecord(val ownerName: String, val typeBitMaps: List<Pair<Byte, ByteArray>>)
|
||||
data class NSEC3Record(
|
||||
val hashAlgorithm: Byte,
|
||||
val flags: Byte,
|
||||
val iterations: UShort,
|
||||
val salt: ByteArray,
|
||||
val nextHashedOwnerName: ByteArray,
|
||||
val typeBitMaps: List<UShort>
|
||||
)
|
||||
|
||||
data class NSEC3PARAMRecord(val hashAlgorithm: Byte, val flags: Byte, val iterations: UShort, val salt: ByteArray)
|
||||
data class SPFRecord(val texts: List<String>)
|
||||
data class TKEYRecord(
|
||||
val algorithm: String,
|
||||
val inception: UInt,
|
||||
val expiration: UInt,
|
||||
val mode: UShort,
|
||||
val error: UShort,
|
||||
val keyData: ByteArray,
|
||||
val otherData: ByteArray
|
||||
)
|
||||
|
||||
data class TSIGRecord(
|
||||
val algorithmName: String,
|
||||
val timeSigned: UInt,
|
||||
val fudge: UShort,
|
||||
val mac: ByteArray,
|
||||
val originalID: UShort,
|
||||
val error: UShort,
|
||||
val otherData: ByteArray
|
||||
)
|
||||
|
||||
data class OPTRecordOption(val code: UShort, val data: ByteArray)
|
||||
data class OPTRecord(val options: List<OPTRecordOption>)
|
||||
|
||||
class DnsReader(private val data: ByteArray, private var position: Int = 0, private val length: Int = data.size) {
|
||||
|
||||
private val endPosition: Int = position + length
|
||||
|
||||
fun readDomainName(): String {
|
||||
return data.asUByteArray().readDomainName(position).also { position = it.second }.first
|
||||
}
|
||||
|
||||
fun readDouble(): Double {
|
||||
checkRemainingBytes(Double.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Double.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).double
|
||||
position += Double.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readInt16(): Short {
|
||||
checkRemainingBytes(Short.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).short
|
||||
position += Short.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readInt32(): Int {
|
||||
checkRemainingBytes(Int.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int
|
||||
position += Int.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readInt64(): Long {
|
||||
checkRemainingBytes(Long.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).long
|
||||
position += Long.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readSingle(): Float {
|
||||
checkRemainingBytes(Float.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Float.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).float
|
||||
position += Float.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readByte(): Byte {
|
||||
checkRemainingBytes(Byte.SIZE_BYTES)
|
||||
return data[position++]
|
||||
}
|
||||
|
||||
fun readBytes(length: Int): ByteArray {
|
||||
checkRemainingBytes(length)
|
||||
return ByteArray(length).also { data.copyInto(it, startIndex = position, endIndex = position + length) }
|
||||
.also { position += length }
|
||||
}
|
||||
|
||||
fun readUInt16(): UShort {
|
||||
checkRemainingBytes(Short.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).short.toUShort()
|
||||
position += Short.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readUInt32(): UInt {
|
||||
checkRemainingBytes(Int.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int.toUInt()
|
||||
position += Int.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readUInt64(): ULong {
|
||||
checkRemainingBytes(Long.SIZE_BYTES)
|
||||
val result = ByteBuffer.wrap(data, position, Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).long.toULong()
|
||||
position += Long.SIZE_BYTES
|
||||
return result
|
||||
}
|
||||
|
||||
fun readString(): String {
|
||||
val length = data[position++].toInt()
|
||||
checkRemainingBytes(length)
|
||||
return String(data, position, length, StandardCharsets.UTF_8).also { position += length }
|
||||
}
|
||||
|
||||
private fun checkRemainingBytes(requiredBytes: Int) {
|
||||
if (position + requiredBytes > endPosition) throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
fun readRPRecord(): RPRecord {
|
||||
return RPRecord(readDomainName(), readDomainName())
|
||||
}
|
||||
|
||||
fun readKXRecord(): KXRecord {
|
||||
val preference = readUInt16()
|
||||
val exchanger = readDomainName()
|
||||
return KXRecord(preference, exchanger)
|
||||
}
|
||||
|
||||
fun readCERTRecord(): CERTRecord {
|
||||
val type = readUInt16()
|
||||
val keyTag = readUInt16()
|
||||
val algorithm = readByte()
|
||||
val certificateLength = readUInt16().toInt() - 5
|
||||
val certificate = readBytes(certificateLength)
|
||||
return CERTRecord(type, keyTag, algorithm, certificate)
|
||||
}
|
||||
|
||||
fun readPTRRecord(): PTRRecord {
|
||||
return PTRRecord(readDomainName())
|
||||
}
|
||||
|
||||
fun readARecord(): ARecord {
|
||||
val address = readBytes(4)
|
||||
return ARecord(InetAddress.getByAddress(address))
|
||||
}
|
||||
|
||||
fun readAAAARecord(): AAAARecord {
|
||||
val address = readBytes(16)
|
||||
return AAAARecord(InetAddress.getByAddress(address))
|
||||
}
|
||||
|
||||
fun readMXRecord(): MXRecord {
|
||||
val preference = readUInt16()
|
||||
val exchange = readDomainName()
|
||||
return MXRecord(preference, exchange)
|
||||
}
|
||||
|
||||
fun readCNAMERecord(): CNAMERecord {
|
||||
return CNAMERecord(readDomainName())
|
||||
}
|
||||
|
||||
fun readTXTRecord(): TXTRecord {
|
||||
val texts = mutableListOf<String>()
|
||||
while (position < endPosition) {
|
||||
val textLength = data[position++].toInt()
|
||||
checkRemainingBytes(textLength)
|
||||
val text = String(data, position, textLength, StandardCharsets.UTF_8)
|
||||
texts.add(text)
|
||||
position += textLength
|
||||
}
|
||||
return TXTRecord(texts)
|
||||
}
|
||||
|
||||
fun readSOARecord(): SOARecord {
|
||||
val primaryNameServer = readDomainName()
|
||||
val responsibleAuthorityMailbox = readDomainName()
|
||||
val serialNumber = readInt32()
|
||||
val refreshInterval = readInt32()
|
||||
val retryInterval = readInt32()
|
||||
val expiryLimit = readInt32()
|
||||
val minimumTTL = readInt32()
|
||||
return SOARecord(primaryNameServer, responsibleAuthorityMailbox, serialNumber, refreshInterval, retryInterval, expiryLimit, minimumTTL)
|
||||
}
|
||||
|
||||
fun readSRVRecord(): SRVRecord {
|
||||
val priority = readUInt16()
|
||||
val weight = readUInt16()
|
||||
val port = readUInt16()
|
||||
val target = readDomainName()
|
||||
return SRVRecord(priority, weight, port, target)
|
||||
}
|
||||
|
||||
fun readNSRecord(): NSRecord {
|
||||
return NSRecord(readDomainName())
|
||||
}
|
||||
|
||||
fun readCAARecord(): CAARecord {
|
||||
val length = readUInt16().toInt()
|
||||
val flags = readByte()
|
||||
val tagLength = readByte().toInt()
|
||||
val tag = String(data, position, tagLength, StandardCharsets.US_ASCII).also { position += tagLength }
|
||||
val valueLength = length - 1 - 1 - tagLength
|
||||
val value = String(data, position, valueLength, StandardCharsets.US_ASCII).also { position += valueLength }
|
||||
return CAARecord(flags, tag, value)
|
||||
}
|
||||
|
||||
fun readHINFORecord(): HINFORecord {
|
||||
val cpuLength = readByte().toInt()
|
||||
val cpu = String(data, position, cpuLength, StandardCharsets.US_ASCII).also { position += cpuLength }
|
||||
val osLength = readByte().toInt()
|
||||
val os = String(data, position, osLength, StandardCharsets.US_ASCII).also { position += osLength }
|
||||
return HINFORecord(cpu, os)
|
||||
}
|
||||
|
||||
fun readAFSDBRecord(): AFSDBRecord {
|
||||
return AFSDBRecord(readUInt16(), readDomainName())
|
||||
}
|
||||
|
||||
fun readLOCRecord(): LOCRecord {
|
||||
val version = readByte()
|
||||
val size = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||
val horizontalPrecision = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||
val verticalPrecision = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||
val latitudeCoded = readInt32()
|
||||
val longitudeCoded = readInt32()
|
||||
val altitudeCoded = readInt32()
|
||||
val latitude = LOCRecord.decodeLatitudeOrLongitude(latitudeCoded)
|
||||
val longitude = LOCRecord.decodeLatitudeOrLongitude(longitudeCoded)
|
||||
val altitude = LOCRecord.decodeAltitude(altitudeCoded)
|
||||
return LOCRecord(version, size, horizontalPrecision, verticalPrecision, latitude, longitude, altitude)
|
||||
}
|
||||
|
||||
fun readNAPTRRecord(): NAPTRRecord {
|
||||
val order = readUInt16()
|
||||
val preference = readUInt16()
|
||||
val flags = readString()
|
||||
val services = readString()
|
||||
val regexp = readString()
|
||||
val replacement = readDomainName()
|
||||
return NAPTRRecord(order, preference, flags, services, regexp, replacement)
|
||||
}
|
||||
|
||||
fun readDNAMERecord(): DNAMERecord {
|
||||
return DNAMERecord(readDomainName())
|
||||
}
|
||||
|
||||
fun readDSRecord(): DSRecord {
|
||||
val keyTag = readUInt16()
|
||||
val algorithm = readByte()
|
||||
val digestType = readByte()
|
||||
val digestLength = readUInt16().toInt() - 4
|
||||
val digest = readBytes(digestLength)
|
||||
return DSRecord(keyTag, algorithm, digestType, digest)
|
||||
}
|
||||
|
||||
fun readSSHFPRecord(): SSHFPRecord {
|
||||
val algorithm = readByte()
|
||||
val fingerprintType = readByte()
|
||||
val fingerprintLength = readUInt16().toInt() - 2
|
||||
val fingerprint = readBytes(fingerprintLength)
|
||||
return SSHFPRecord(algorithm, fingerprintType, fingerprint)
|
||||
}
|
||||
|
||||
fun readTLSARecord(): TLSARecord {
|
||||
val usage = readByte()
|
||||
val selector = readByte()
|
||||
val matchingType = readByte()
|
||||
val dataLength = readUInt16().toInt() - 3
|
||||
val certificateAssociationData = readBytes(dataLength)
|
||||
return TLSARecord(usage, selector, matchingType, certificateAssociationData)
|
||||
}
|
||||
|
||||
fun readSMIMEARecord(): SMIMEARecord {
|
||||
val usage = readByte()
|
||||
val selector = readByte()
|
||||
val matchingType = readByte()
|
||||
val dataLength = readUInt16().toInt() - 3
|
||||
val certificateAssociationData = readBytes(dataLength)
|
||||
return SMIMEARecord(usage, selector, matchingType, certificateAssociationData)
|
||||
}
|
||||
|
||||
fun readURIRecord(): URIRecord {
|
||||
val priority = readUInt16()
|
||||
val weight = readUInt16()
|
||||
val length = readUInt16().toInt()
|
||||
val target = String(data, position, length, StandardCharsets.US_ASCII).also { position += length }
|
||||
return URIRecord(priority, weight, target)
|
||||
}
|
||||
|
||||
fun readRRSIGRecord(): RRSIGRecord {
|
||||
val typeCovered = readUInt16()
|
||||
val algorithm = readByte()
|
||||
val labels = readByte()
|
||||
val originalTTL = readUInt32()
|
||||
val signatureExpiration = readUInt32()
|
||||
val signatureInception = readUInt32()
|
||||
val keyTag = readUInt16()
|
||||
val signersName = readDomainName()
|
||||
val signatureLength = readUInt16().toInt()
|
||||
val signature = readBytes(signatureLength)
|
||||
return RRSIGRecord(
|
||||
typeCovered,
|
||||
algorithm,
|
||||
labels,
|
||||
originalTTL,
|
||||
signatureExpiration,
|
||||
signatureInception,
|
||||
keyTag,
|
||||
signersName,
|
||||
signature
|
||||
)
|
||||
}
|
||||
|
||||
fun readNSECRecord(): NSECRecord {
|
||||
val ownerName = readDomainName()
|
||||
val typeBitMaps = mutableListOf<Pair<Byte, ByteArray>>()
|
||||
while (position < endPosition) {
|
||||
val windowBlock = readByte()
|
||||
val bitmapLength = readByte().toInt()
|
||||
val bitmap = readBytes(bitmapLength)
|
||||
typeBitMaps.add(windowBlock to bitmap)
|
||||
}
|
||||
return NSECRecord(ownerName, typeBitMaps)
|
||||
}
|
||||
|
||||
fun readNSEC3Record(): NSEC3Record {
|
||||
val hashAlgorithm = readByte()
|
||||
val flags = readByte()
|
||||
val iterations = readUInt16()
|
||||
val saltLength = readByte().toInt()
|
||||
val salt = readBytes(saltLength)
|
||||
val hashLength = readByte().toInt()
|
||||
val nextHashedOwnerName = readBytes(hashLength)
|
||||
val bitMapLength = readUInt16().toInt()
|
||||
val typeBitMaps = mutableListOf<UShort>()
|
||||
val endPos = position + bitMapLength
|
||||
while (position < endPos) {
|
||||
typeBitMaps.add(readUInt16())
|
||||
}
|
||||
return NSEC3Record(hashAlgorithm, flags, iterations, salt, nextHashedOwnerName, typeBitMaps)
|
||||
}
|
||||
|
||||
fun readNSEC3PARAMRecord(): NSEC3PARAMRecord {
|
||||
val hashAlgorithm = readByte()
|
||||
val flags = readByte()
|
||||
val iterations = readUInt16()
|
||||
val saltLength = readByte().toInt()
|
||||
val salt = readBytes(saltLength)
|
||||
return NSEC3PARAMRecord(hashAlgorithm, flags, iterations, salt)
|
||||
}
|
||||
|
||||
|
||||
fun readSPFRecord(): SPFRecord {
|
||||
val length = readUInt16().toInt()
|
||||
val texts = mutableListOf<String>()
|
||||
val endPos = position + length
|
||||
while (position < endPos) {
|
||||
val textLength = readByte().toInt()
|
||||
val text = String(data, position, textLength, StandardCharsets.US_ASCII).also { position += textLength }
|
||||
texts.add(text)
|
||||
}
|
||||
return SPFRecord(texts)
|
||||
}
|
||||
|
||||
fun readTKEYRecord(): TKEYRecord {
|
||||
val algorithm = readDomainName()
|
||||
val inception = readUInt32()
|
||||
val expiration = readUInt32()
|
||||
val mode = readUInt16()
|
||||
val error = readUInt16()
|
||||
val keySize = readUInt16().toInt()
|
||||
val keyData = readBytes(keySize)
|
||||
val otherSize = readUInt16().toInt()
|
||||
val otherData = readBytes(otherSize)
|
||||
return TKEYRecord(algorithm, inception, expiration, mode, error, keyData, otherData)
|
||||
}
|
||||
|
||||
fun readTSIGRecord(): TSIGRecord {
|
||||
val algorithmName = readDomainName()
|
||||
val timeSigned = readUInt32()
|
||||
val fudge = readUInt16()
|
||||
val macSize = readUInt16().toInt()
|
||||
val mac = readBytes(macSize)
|
||||
val originalID = readUInt16()
|
||||
val error = readUInt16()
|
||||
val otherSize = readUInt16().toInt()
|
||||
val otherData = readBytes(otherSize)
|
||||
return TSIGRecord(algorithmName, timeSigned, fudge, mac, originalID, error, otherData)
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun readOPTRecord(): OPTRecord {
|
||||
val options = mutableListOf<OPTRecordOption>()
|
||||
while (position < endPosition) {
|
||||
val optionCode = readUInt16()
|
||||
val optionLength = readUInt16().toInt()
|
||||
val optionData = readBytes(optionLength)
|
||||
options.add(OPTRecordOption(optionCode, optionData))
|
||||
}
|
||||
return OPTRecord(options)
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||
|
||||
enum class ResourceRecordType(val value: UShort) {
|
||||
None(0u),
|
||||
A(1u),
|
||||
NS(2u),
|
||||
MD(3u),
|
||||
MF(4u),
|
||||
CNAME(5u),
|
||||
SOA(6u),
|
||||
MB(7u),
|
||||
MG(8u),
|
||||
MR(9u),
|
||||
NULL(10u),
|
||||
WKS(11u),
|
||||
PTR(12u),
|
||||
HINFO(13u),
|
||||
MINFO(14u),
|
||||
MX(15u),
|
||||
TXT(16u),
|
||||
RP(17u),
|
||||
AFSDB(18u),
|
||||
SIG(24u),
|
||||
KEY(25u),
|
||||
AAAA(28u),
|
||||
LOC(29u),
|
||||
SRV(33u),
|
||||
NAPTR(35u),
|
||||
KX(36u),
|
||||
CERT(37u),
|
||||
DNAME(39u),
|
||||
APL(42u),
|
||||
DS(43u),
|
||||
SSHFP(44u),
|
||||
IPSECKEY(45u),
|
||||
RRSIG(46u),
|
||||
NSEC(47u),
|
||||
DNSKEY(48u),
|
||||
DHCID(49u),
|
||||
NSEC3(50u),
|
||||
NSEC3PARAM(51u),
|
||||
TSLA(52u),
|
||||
SMIMEA(53u),
|
||||
HIP(55u),
|
||||
CDS(59u),
|
||||
CDNSKEY(60u),
|
||||
OPENPGPKEY(61u),
|
||||
CSYNC(62u),
|
||||
ZONEMD(63u),
|
||||
SVCB(64u),
|
||||
HTTPS(65u),
|
||||
EUI48(108u),
|
||||
EUI64(109u),
|
||||
TKEY(249u),
|
||||
TSIG(250u),
|
||||
URI(256u),
|
||||
CAA(257u),
|
||||
TA(32768u),
|
||||
DLV(32769u),
|
||||
AXFR(252u),
|
||||
IXFR(251u),
|
||||
OPT(41u)
|
||||
}
|
||||
|
||||
enum class ResourceRecordClass(val value: UShort) {
|
||||
IN(1u),
|
||||
CS(2u),
|
||||
CH(3u),
|
||||
HS(4u)
|
||||
}
|
||||
|
||||
data class DnsResourceRecord(
|
||||
override val name: String,
|
||||
override val type: Int,
|
||||
override val clazz: Int,
|
||||
val timeToLive: UInt,
|
||||
val cacheFlush: Boolean,
|
||||
val dataPosition: Int = -1,
|
||||
val dataLength: Int = -1,
|
||||
private val data: ByteArray? = null
|
||||
) : DnsResourceRecordBase(name, type, clazz) {
|
||||
|
||||
companion object {
|
||||
fun parse(data: ByteArray, startPosition: Int): Pair<DnsResourceRecord, Int> {
|
||||
val span = data.asUByteArray()
|
||||
var position = startPosition
|
||||
val name = span.readDomainName(position).also { position = it.second }
|
||||
val type = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||
position += 2
|
||||
val clazz = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||
position += 2
|
||||
val ttl = (span[position].toInt() shl 24 or (span[position + 1].toInt() shl 16) or
|
||||
(span[position + 2].toInt() shl 8) or span[position + 3].toInt()).toUInt()
|
||||
position += 4
|
||||
val rdlength = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||
val rdposition = position + 2
|
||||
position += 2 + rdlength.toInt()
|
||||
|
||||
return DnsResourceRecord(
|
||||
name = name.first,
|
||||
type = type.toInt(),
|
||||
clazz = clazz.toInt() and 0b1111111_11111111,
|
||||
timeToLive = ttl,
|
||||
cacheFlush = ((clazz.toInt() shr 15) and 0b1) != 0,
|
||||
dataPosition = rdposition,
|
||||
dataLength = rdlength.toInt(),
|
||||
data = data
|
||||
) to position
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataReader(): DnsReader {
|
||||
return DnsReader(data!!, dataPosition, dataLength)
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class DnsWriter {
|
||||
private val data = mutableListOf<Byte>()
|
||||
private val namePositions = mutableMapOf<String, Int>()
|
||||
|
||||
fun toByteArray(): ByteArray = data.toByteArray()
|
||||
|
||||
fun writePacket(
|
||||
header: DnsPacketHeader,
|
||||
questionCount: Int? = null, questionWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||
answerCount: Int? = null, answerWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||
authorityCount: Int? = null, authorityWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||
additionalsCount: Int? = null, additionalWriter: ((DnsWriter, Int) -> Unit)? = null
|
||||
) {
|
||||
if (questionCount != null && questionWriter == null || questionCount == null && questionWriter != null)
|
||||
throw Exception("When question count is given, question writer should also be given.")
|
||||
if (answerCount != null && answerWriter == null || answerCount == null && answerWriter != null)
|
||||
throw Exception("When answer count is given, answer writer should also be given.")
|
||||
if (authorityCount != null && authorityWriter == null || authorityCount == null && authorityWriter != null)
|
||||
throw Exception("When authority count is given, authority writer should also be given.")
|
||||
if (additionalsCount != null && additionalWriter == null || additionalsCount == null && additionalWriter != null)
|
||||
throw Exception("When additionals count is given, additional writer should also be given.")
|
||||
|
||||
writeHeader(header, questionCount ?: 0, answerCount ?: 0, authorityCount ?: 0, additionalsCount ?: 0)
|
||||
|
||||
repeat(questionCount ?: 0) { questionWriter?.invoke(this, it) }
|
||||
repeat(answerCount ?: 0) { answerWriter?.invoke(this, it) }
|
||||
repeat(authorityCount ?: 0) { authorityWriter?.invoke(this, it) }
|
||||
repeat(additionalsCount ?: 0) { additionalWriter?.invoke(this, it) }
|
||||
}
|
||||
|
||||
fun writeHeader(header: DnsPacketHeader, questionCount: Int, answerCount: Int, authorityCount: Int, additionalsCount: Int) {
|
||||
write(header.identifier)
|
||||
|
||||
var flags: UShort = 0u
|
||||
flags = flags or ((header.queryResponse.toUInt() and 0xFFFFu) shl 15).toUShort()
|
||||
flags = flags or ((header.opcode.toUInt() and 0xFFFFu) shl 11).toUShort()
|
||||
flags = flags or ((if (header.authoritativeAnswer) 1u else 0u) shl 10).toUShort()
|
||||
flags = flags or ((if (header.truncated) 1u else 0u) shl 9).toUShort()
|
||||
flags = flags or ((if (header.recursionDesired) 1u else 0u) shl 8).toUShort()
|
||||
flags = flags or ((if (header.recursionAvailable) 1u else 0u) shl 7).toUShort()
|
||||
flags = flags or ((if (header.answerAuthenticated) 1u else 0u) shl 5).toUShort()
|
||||
flags = flags or ((if (header.nonAuthenticatedData) 1u else 0u) shl 4).toUShort()
|
||||
flags = flags or header.responseCode.value.toUShort()
|
||||
write(flags)
|
||||
|
||||
write(questionCount.toUShort())
|
||||
write(answerCount.toUShort())
|
||||
write(authorityCount.toUShort())
|
||||
write(additionalsCount.toUShort())
|
||||
}
|
||||
|
||||
fun writeDomainName(name: String) {
|
||||
synchronized(namePositions) {
|
||||
val labels = name.split('.')
|
||||
for (label in labels) {
|
||||
val nameAtOffset = name.substring(name.indexOf(label))
|
||||
if (namePositions.containsKey(nameAtOffset)) {
|
||||
val position = namePositions[nameAtOffset]!!
|
||||
val pointer = (0b11000000_00000000 or position).toUShort()
|
||||
write(pointer)
|
||||
return
|
||||
}
|
||||
if (label.isNotEmpty()) {
|
||||
val labelBytes = label.toByteArray(StandardCharsets.UTF_8)
|
||||
val nameStartPos = data.size
|
||||
write(labelBytes.size.toByte())
|
||||
write(labelBytes)
|
||||
namePositions[nameAtOffset] = nameStartPos
|
||||
}
|
||||
}
|
||||
write(0.toByte()) // End of domain name
|
||||
}
|
||||
}
|
||||
|
||||
fun write(value: DnsResourceRecord, dataWriter: (DnsWriter) -> Unit) {
|
||||
writeDomainName(value.name)
|
||||
write(value.type.toUShort())
|
||||
val cls = ((if (value.cacheFlush) 1u else 0u) shl 15).toUShort() or value.clazz.toUShort()
|
||||
write(cls)
|
||||
write(value.timeToLive)
|
||||
|
||||
val lengthOffset = data.size
|
||||
write(0.toUShort())
|
||||
dataWriter(this)
|
||||
val rdLength = data.size - lengthOffset - 2
|
||||
val rdLengthBytes = ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(rdLength.toShort()).array()
|
||||
data[lengthOffset] = rdLengthBytes[0]
|
||||
data[lengthOffset + 1] = rdLengthBytes[1]
|
||||
}
|
||||
|
||||
fun write(value: DnsQuestion) {
|
||||
writeDomainName(value.name)
|
||||
write(value.type.toUShort())
|
||||
write(((if (value.queryUnicast) 1u else 0u shl 15).toUShort() or value.clazz.toUShort()))
|
||||
}
|
||||
|
||||
fun write(value: Double) {
|
||||
val bytes = ByteBuffer.allocate(Double.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putDouble(value).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: Short) {
|
||||
val bytes = ByteBuffer.allocate(Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putShort(value).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: Int) {
|
||||
val bytes = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putInt(value).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: Long) {
|
||||
val bytes = ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putLong(value).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: Float) {
|
||||
val bytes = ByteBuffer.allocate(Float.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putFloat(value).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: Byte) {
|
||||
data.add(value)
|
||||
}
|
||||
|
||||
fun write(value: ByteArray) {
|
||||
data.addAll(value.asIterable())
|
||||
}
|
||||
|
||||
fun write(value: ByteArray, offset: Int, length: Int) {
|
||||
data.addAll(value.slice(offset until offset + length))
|
||||
}
|
||||
|
||||
fun write(value: UShort) {
|
||||
val bytes = ByteBuffer.allocate(Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putShort(value.toShort()).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: UInt) {
|
||||
val bytes = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putInt(value.toInt()).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: ULong) {
|
||||
val bytes = ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putLong(value.toLong()).array()
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: String) {
|
||||
val bytes = value.toByteArray(StandardCharsets.UTF_8)
|
||||
write(bytes.size.toByte())
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: PTRRecord) {
|
||||
writeDomainName(value.domainName)
|
||||
}
|
||||
|
||||
fun write(value: ARecord) {
|
||||
val bytes = value.address.address
|
||||
if (bytes.size != 4) throw Exception("Unexpected amount of address bytes.")
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: AAAARecord) {
|
||||
val bytes = value.address.address
|
||||
if (bytes.size != 16) throw Exception("Unexpected amount of address bytes.")
|
||||
write(bytes)
|
||||
}
|
||||
|
||||
fun write(value: TXTRecord) {
|
||||
value.texts.forEach {
|
||||
val bytes = it.toByteArray(StandardCharsets.UTF_8)
|
||||
write(bytes.size.toByte())
|
||||
write(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fun write(value: SRVRecord) {
|
||||
write(value.priority)
|
||||
write(value.weight)
|
||||
write(value.port)
|
||||
writeDomainName(value.target)
|
||||
}
|
||||
|
||||
fun write(value: NSECRecord) {
|
||||
writeDomainName(value.ownerName)
|
||||
value.typeBitMaps.forEach { (windowBlock, bitmap) ->
|
||||
write(windowBlock)
|
||||
write(bitmap.size.toByte())
|
||||
write(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
fun write(value: OPTRecord) {
|
||||
value.options.forEach { option ->
|
||||
write(option.code)
|
||||
write(option.data.size.toUShort())
|
||||
write(option.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object Extensions {
|
||||
fun ByteArray.toByteDump(): String {
|
||||
val result = StringBuilder()
|
||||
for (i in indices) {
|
||||
result.append(String.format("%02X ", this[i]))
|
||||
|
||||
if ((i + 1) % 16 == 0 || i == size - 1) {
|
||||
val padding = 3 * (16 - (i % 16 + 1))
|
||||
if (i == size - 1 && (i + 1) % 16 != 0) result.append(" ".repeat(padding))
|
||||
|
||||
result.append("; ")
|
||||
val start = i - (i % 16)
|
||||
val end = minOf(i, size - 1)
|
||||
for (j in start..end) {
|
||||
val ch = if (this[j] in 32..127) this[j].toChar() else '.'
|
||||
result.append(ch)
|
||||
}
|
||||
if (i != size - 1) result.appendLine()
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun UByteArray.readDomainName(startPosition: Int): Pair<String, Int> {
|
||||
var position = startPosition
|
||||
return readDomainName(position, 0)
|
||||
}
|
||||
|
||||
private fun UByteArray.readDomainName(position: Int, depth: Int = 0): Pair<String, Int> {
|
||||
if (depth > 16) throw Exception("Exceeded maximum recursion depth in DNS packet. Possible circular reference.")
|
||||
|
||||
val domainParts = mutableListOf<String>()
|
||||
var newPosition = position
|
||||
|
||||
while (true) {
|
||||
if (newPosition < 0)
|
||||
println()
|
||||
|
||||
val length = this[newPosition].toUByte()
|
||||
if ((length and 0b11000000u).toUInt() == 0b11000000u) {
|
||||
val offset = (((length and 0b00111111u).toUInt()) shl 8) or this[newPosition + 1].toUInt()
|
||||
val (part, _) = this.readDomainName(offset.toInt(), depth + 1)
|
||||
domainParts.add(part)
|
||||
newPosition += 2
|
||||
break
|
||||
} else if (length.toUInt() == 0u) {
|
||||
newPosition++
|
||||
break
|
||||
} else {
|
||||
newPosition++
|
||||
val part = String(this.asByteArray(), newPosition, length.toInt(), Charsets.UTF_8)
|
||||
domainParts.add(part)
|
||||
newPosition += length.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
return domainParts.joinToString(".") to newPosition
|
||||
}
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class MDNSListener {
|
||||
companion object {
|
||||
private val TAG = "MDNSListener"
|
||||
const val MulticastPort = 5353
|
||||
val MulticastAddressIPv4: InetAddress = InetAddress.getByName("224.0.0.251")
|
||||
val MulticastAddressIPv6: InetAddress = InetAddress.getByName("FF02::FB")
|
||||
val MdnsEndpointIPv6: InetSocketAddress = InetSocketAddress(MulticastAddressIPv6, MulticastPort)
|
||||
val MdnsEndpointIPv4: InetSocketAddress = InetSocketAddress(MulticastAddressIPv4, MulticastPort)
|
||||
}
|
||||
|
||||
private val _lockObject = ReentrantLock()
|
||||
private var _receiver4: DatagramSocket? = null
|
||||
private var _receiver6: DatagramSocket? = null
|
||||
private val _senders = mutableListOf<DatagramSocket>()
|
||||
private val _nicMonitor = NICMonitor()
|
||||
private val _serviceRecordAggregator = ServiceRecordAggregator()
|
||||
private var _started = false
|
||||
private var _threadReceiver4: Thread? = null
|
||||
private var _threadReceiver6: Thread? = null
|
||||
private var _scope: CoroutineScope? = null
|
||||
|
||||
var onPacket: ((DnsPacket) -> Unit)? = null
|
||||
var onServicesUpdated: ((List<DnsService>) -> Unit)? = null
|
||||
|
||||
private val _recordLockObject = ReentrantLock()
|
||||
private val _recordsA = mutableListOf<Pair<DnsResourceRecord, ARecord>>()
|
||||
private val _recordsAAAA = mutableListOf<Pair<DnsResourceRecord, AAAARecord>>()
|
||||
private val _recordsPTR = mutableListOf<Pair<DnsResourceRecord, PTRRecord>>()
|
||||
private val _recordsTXT = mutableListOf<Pair<DnsResourceRecord, TXTRecord>>()
|
||||
private val _recordsSRV = mutableListOf<Pair<DnsResourceRecord, SRVRecord>>()
|
||||
private val _services = mutableListOf<BroadcastService>()
|
||||
|
||||
init {
|
||||
_nicMonitor.added = { onNicsAdded(it) }
|
||||
_nicMonitor.removed = { onNicsRemoved(it) }
|
||||
_serviceRecordAggregator.onServicesUpdated = { onServicesUpdated?.invoke(it) }
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (_started) throw Exception("Already running.")
|
||||
_started = true
|
||||
|
||||
_scope = CoroutineScope(Dispatchers.IO);
|
||||
|
||||
Logger.i(TAG, "Starting")
|
||||
_lockObject.withLock {
|
||||
val receiver4 = DatagramSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
|
||||
}
|
||||
_receiver4 = receiver4
|
||||
|
||||
val receiver6 = DatagramSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
|
||||
}
|
||||
_receiver6 = receiver6
|
||||
|
||||
_nicMonitor.start()
|
||||
_serviceRecordAggregator.start()
|
||||
onNicsAdded(_nicMonitor.current)
|
||||
|
||||
_threadReceiver4 = Thread {
|
||||
receiveLoop(receiver4)
|
||||
}.apply { start() }
|
||||
|
||||
_threadReceiver6 = Thread {
|
||||
receiveLoop(receiver6)
|
||||
}.apply { start() }
|
||||
}
|
||||
}
|
||||
|
||||
fun queryServices(names: Array<String>) {
|
||||
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||
|
||||
val writer = DnsWriter()
|
||||
writer.writePacket(
|
||||
DnsPacketHeader(
|
||||
identifier = 0u,
|
||||
queryResponse = QueryResponse.Query.value.toInt(),
|
||||
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||
truncated = false,
|
||||
nonAuthenticatedData = false,
|
||||
recursionDesired = false,
|
||||
answerAuthenticated = false,
|
||||
authoritativeAnswer = false,
|
||||
recursionAvailable = false,
|
||||
responseCode = DnsResponseCode.NoError
|
||||
),
|
||||
questionCount = names.size,
|
||||
questionWriter = { w, i ->
|
||||
w.write(
|
||||
DnsQuestion(
|
||||
name = names[i],
|
||||
type = QuestionType.PTR.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
send(writer.toByteArray())
|
||||
}
|
||||
|
||||
private fun send(data: ByteArray) {
|
||||
_lockObject.withLock {
|
||||
for (sender in _senders) {
|
||||
try {
|
||||
val endPoint = if (sender.localAddress is Inet4Address) MdnsEndpointIPv4 else MdnsEndpointIPv6
|
||||
sender.send(DatagramPacket(data, data.size, endPoint))
|
||||
} catch (e: Exception) {
|
||||
Logger.i(TAG, "Failed to send on ${sender.localSocketAddress}: ${e.message}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun queryAllQuestions(names: Array<String>) {
|
||||
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||
|
||||
val questions = names.flatMap { _serviceRecordAggregator.getAllQuestions(it) }
|
||||
questions.groupBy { it.name }.forEach { (_, questionsForHost) ->
|
||||
val writer = DnsWriter()
|
||||
writer.writePacket(
|
||||
DnsPacketHeader(
|
||||
identifier = 0u,
|
||||
queryResponse = QueryResponse.Query.value.toInt(),
|
||||
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||
truncated = false,
|
||||
nonAuthenticatedData = false,
|
||||
recursionDesired = false,
|
||||
answerAuthenticated = false,
|
||||
authoritativeAnswer = false,
|
||||
recursionAvailable = false,
|
||||
responseCode = DnsResponseCode.NoError
|
||||
),
|
||||
questionCount = questionsForHost.size,
|
||||
questionWriter = { w, i -> w.write(questionsForHost[i]) }
|
||||
)
|
||||
send(writer.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNicsAdded(nics: List<NetworkInterface>) {
|
||||
_lockObject.withLock {
|
||||
if (!_started) return
|
||||
|
||||
val addresses = nics.flatMap { nic ->
|
||||
nic.interfaceAddresses.map { it.address }
|
||||
.filter { it is Inet4Address || (it is Inet6Address && it.isLinkLocalAddress) }
|
||||
}
|
||||
|
||||
addresses.forEach { address ->
|
||||
Logger.i(TAG, "New address discovered $address")
|
||||
|
||||
try {
|
||||
when (address) {
|
||||
is Inet4Address -> {
|
||||
val sender = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(address, MulticastPort))
|
||||
joinGroup(InetSocketAddress(MulticastAddressIPv4, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||
}
|
||||
_senders.add(sender)
|
||||
}
|
||||
|
||||
is Inet6Address -> {
|
||||
val sender = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(address, MulticastPort))
|
||||
joinGroup(InetSocketAddress(MulticastAddressIPv6, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||
}
|
||||
_senders.add(sender)
|
||||
}
|
||||
|
||||
else -> throw UnsupportedOperationException("Address type ${address.javaClass.name} is not supported.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.i(TAG, "Exception occurred when processing added address $address: ${e.message}.")
|
||||
// Close the socket if there was an error
|
||||
(_senders.lastOrNull() as? MulticastSocket)?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nics.isNotEmpty()) {
|
||||
try {
|
||||
updateBroadcastRecords()
|
||||
broadcastRecords()
|
||||
} catch (e: Exception) {
|
||||
Logger.i(TAG, "Exception occurred when broadcasting records: ${e.message}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNicsRemoved(nics: List<NetworkInterface>) {
|
||||
_lockObject.withLock {
|
||||
if (!_started) return
|
||||
//TODO: Cleanup?
|
||||
}
|
||||
|
||||
if (nics.isNotEmpty()) {
|
||||
try {
|
||||
updateBroadcastRecords()
|
||||
broadcastRecords()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Exception occurred when broadcasting records", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun receiveLoop(client: DatagramSocket) {
|
||||
Logger.i(TAG, "Started receive loop")
|
||||
|
||||
val buffer = ByteArray(1024)
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
while (_started) {
|
||||
try {
|
||||
client.receive(packet)
|
||||
handleResult(packet)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "An exception occurred while handling UDP result:", e)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped receive loop")
|
||||
}
|
||||
|
||||
fun broadcastService(
|
||||
deviceName: String,
|
||||
serviceName: String,
|
||||
port: UShort,
|
||||
ttl: UInt = 120u,
|
||||
weight: UShort = 0u,
|
||||
priority: UShort = 0u,
|
||||
texts: List<String>? = null
|
||||
) {
|
||||
_recordLockObject.withLock {
|
||||
_services.add(
|
||||
BroadcastService(
|
||||
deviceName = deviceName,
|
||||
port = port,
|
||||
priority = priority,
|
||||
serviceName = serviceName,
|
||||
texts = texts,
|
||||
ttl = ttl,
|
||||
weight = weight
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
updateBroadcastRecords()
|
||||
broadcastRecords()
|
||||
}
|
||||
|
||||
private fun updateBroadcastRecords() {
|
||||
_recordLockObject.withLock {
|
||||
_recordsSRV.clear()
|
||||
_recordsPTR.clear()
|
||||
_recordsA.clear()
|
||||
_recordsAAAA.clear()
|
||||
_recordsTXT.clear()
|
||||
|
||||
_services.forEach { service ->
|
||||
val id = UUID.randomUUID().toString()
|
||||
val deviceDomainName = "${service.deviceName}.${service.serviceName}"
|
||||
val addressName = "$id.local"
|
||||
|
||||
_recordsSRV.add(
|
||||
DnsResourceRecord(
|
||||
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||
type = ResourceRecordType.SRV.value.toInt(),
|
||||
timeToLive = service.ttl,
|
||||
name = deviceDomainName,
|
||||
cacheFlush = false
|
||||
) to SRVRecord(
|
||||
target = addressName,
|
||||
port = service.port,
|
||||
priority = service.priority,
|
||||
weight = service.weight
|
||||
)
|
||||
)
|
||||
|
||||
_recordsPTR.add(
|
||||
DnsResourceRecord(
|
||||
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||
type = ResourceRecordType.PTR.value.toInt(),
|
||||
timeToLive = service.ttl,
|
||||
name = service.serviceName,
|
||||
cacheFlush = false
|
||||
) to PTRRecord(
|
||||
domainName = deviceDomainName
|
||||
)
|
||||
)
|
||||
|
||||
val addresses = _nicMonitor.current.flatMap { nic ->
|
||||
nic.interfaceAddresses.map { it.address }
|
||||
}
|
||||
|
||||
addresses.forEach { address ->
|
||||
when (address) {
|
||||
is Inet4Address -> _recordsA.add(
|
||||
DnsResourceRecord(
|
||||
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||
type = ResourceRecordType.A.value.toInt(),
|
||||
timeToLive = service.ttl,
|
||||
name = addressName,
|
||||
cacheFlush = false
|
||||
) to ARecord(
|
||||
address = address
|
||||
)
|
||||
)
|
||||
|
||||
is Inet6Address -> _recordsAAAA.add(
|
||||
DnsResourceRecord(
|
||||
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||
type = ResourceRecordType.AAAA.value.toInt(),
|
||||
timeToLive = service.ttl,
|
||||
name = addressName,
|
||||
cacheFlush = false
|
||||
) to AAAARecord(
|
||||
address = address
|
||||
)
|
||||
)
|
||||
|
||||
else -> Logger.i(TAG, "Invalid address type: $address.")
|
||||
}
|
||||
}
|
||||
|
||||
if (service.texts != null) {
|
||||
_recordsTXT.add(
|
||||
DnsResourceRecord(
|
||||
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||
type = ResourceRecordType.TXT.value.toInt(),
|
||||
timeToLive = service.ttl,
|
||||
name = deviceDomainName,
|
||||
cacheFlush = false
|
||||
) to TXTRecord(
|
||||
texts = service.texts
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastRecords(questions: List<DnsQuestion>? = null) {
|
||||
val writer = DnsWriter()
|
||||
_recordLockObject.withLock {
|
||||
val recordsA: List<Pair<DnsResourceRecord, ARecord>>
|
||||
val recordsAAAA: List<Pair<DnsResourceRecord, AAAARecord>>
|
||||
val recordsPTR: List<Pair<DnsResourceRecord, PTRRecord>>
|
||||
val recordsTXT: List<Pair<DnsResourceRecord, TXTRecord>>
|
||||
val recordsSRV: List<Pair<DnsResourceRecord, SRVRecord>>
|
||||
|
||||
if (questions != null) {
|
||||
recordsA = _recordsA.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||
recordsAAAA = _recordsAAAA.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||
recordsPTR = _recordsPTR.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||
recordsSRV = _recordsSRV.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||
recordsTXT = _recordsTXT.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||
} else {
|
||||
recordsA = _recordsA
|
||||
recordsAAAA = _recordsAAAA
|
||||
recordsPTR = _recordsPTR
|
||||
recordsSRV = _recordsSRV
|
||||
recordsTXT = _recordsTXT
|
||||
}
|
||||
|
||||
val answerCount = recordsA.size + recordsAAAA.size + recordsPTR.size + recordsSRV.size + recordsTXT.size
|
||||
if (answerCount < 1) return
|
||||
|
||||
val txtOffset = recordsA.size + recordsAAAA.size + recordsPTR.size + recordsSRV.size
|
||||
val srvOffset = recordsA.size + recordsAAAA.size + recordsPTR.size
|
||||
val ptrOffset = recordsA.size + recordsAAAA.size
|
||||
val aaaaOffset = recordsA.size
|
||||
|
||||
writer.writePacket(
|
||||
DnsPacketHeader(
|
||||
identifier = 0u,
|
||||
queryResponse = QueryResponse.Response.value.toInt(),
|
||||
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||
truncated = false,
|
||||
nonAuthenticatedData = false,
|
||||
recursionDesired = false,
|
||||
answerAuthenticated = false,
|
||||
authoritativeAnswer = true,
|
||||
recursionAvailable = false,
|
||||
responseCode = DnsResponseCode.NoError
|
||||
),
|
||||
answerCount = answerCount,
|
||||
answerWriter = { w, i ->
|
||||
when {
|
||||
i >= txtOffset -> {
|
||||
val record = recordsTXT[i - txtOffset]
|
||||
w.write(record.first) { it.write(record.second) }
|
||||
}
|
||||
|
||||
i >= srvOffset -> {
|
||||
val record = recordsSRV[i - srvOffset]
|
||||
w.write(record.first) { it.write(record.second) }
|
||||
}
|
||||
|
||||
i >= ptrOffset -> {
|
||||
val record = recordsPTR[i - ptrOffset]
|
||||
w.write(record.first) { it.write(record.second) }
|
||||
}
|
||||
|
||||
i >= aaaaOffset -> {
|
||||
val record = recordsAAAA[i - aaaaOffset]
|
||||
w.write(record.first) { it.write(record.second) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
val record = recordsA[i]
|
||||
w.write(record.first) { it.write(record.second) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
send(writer.toByteArray())
|
||||
}
|
||||
|
||||
private fun handleResult(result: DatagramPacket) {
|
||||
try {
|
||||
val packet = DnsPacket.parse(result.data)
|
||||
if (packet.questions.isNotEmpty()) {
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
broadcastRecords(packet.questions)
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Broadcasting records failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
_serviceRecordAggregator.add(packet)
|
||||
onPacket?.invoke(packet)
|
||||
} catch (e: Exception) {
|
||||
Logger.v(TAG, "Failed to handle packet: ${Base64.getEncoder().encodeToString(result.data.slice(IntRange(0, result.length - 1)).toByteArray())}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
_lockObject.withLock {
|
||||
_started = false
|
||||
|
||||
_scope?.cancel()
|
||||
_scope = null
|
||||
|
||||
_nicMonitor.stop()
|
||||
_serviceRecordAggregator.stop()
|
||||
|
||||
_receiver4?.close()
|
||||
_receiver4 = null
|
||||
|
||||
_receiver6?.close()
|
||||
_receiver6 = null
|
||||
|
||||
_senders.forEach { it.close() }
|
||||
_senders.clear()
|
||||
}
|
||||
|
||||
_threadReceiver4?.join()
|
||||
_threadReceiver4 = null
|
||||
|
||||
_threadReceiver6?.join()
|
||||
_threadReceiver6 = null
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.NetworkInterface
|
||||
|
||||
class NICMonitor {
|
||||
private val lockObject = Any()
|
||||
private val nics = mutableListOf<NetworkInterface>()
|
||||
private var cts: Job? = null
|
||||
|
||||
val current: List<NetworkInterface>
|
||||
get() = synchronized(nics) { nics.toList() }
|
||||
|
||||
var added: ((List<NetworkInterface>) -> Unit)? = null
|
||||
var removed: ((List<NetworkInterface>) -> Unit)? = null
|
||||
|
||||
fun start() {
|
||||
synchronized(lockObject) {
|
||||
if (cts != null) throw Exception("Already started.")
|
||||
|
||||
cts = CoroutineScope(Dispatchers.Default).launch {
|
||||
loopAsync()
|
||||
}
|
||||
}
|
||||
|
||||
nics.clear()
|
||||
nics.addAll(getCurrentInterfaces().toList())
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
synchronized(lockObject) {
|
||||
cts?.cancel()
|
||||
cts = null
|
||||
}
|
||||
|
||||
synchronized(nics) {
|
||||
nics.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loopAsync() {
|
||||
while (cts?.isActive == true) {
|
||||
try {
|
||||
val currentNics = getCurrentInterfaces().toList()
|
||||
removed?.invoke(nics.filter { k -> currentNics.none { n -> k.name == n.name } })
|
||||
added?.invoke(currentNics.filter { nic -> nics.none { k -> k.name == nic.name } })
|
||||
|
||||
synchronized(nics) {
|
||||
nics.clear()
|
||||
nics.addAll(currentNics)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentInterfaces(): List<NetworkInterface> {
|
||||
val nics = NetworkInterface.getNetworkInterfaces().toList()
|
||||
.filter { it.isUp && !it.isLoopback }
|
||||
|
||||
return if (nics.isNotEmpty()) nics else NetworkInterface.getNetworkInterfaces().toList()
|
||||
.filter { it.isUp }
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
class ServiceDiscoverer(names: Array<String>, private val _onServicesUpdated: (List<DnsService>) -> Unit) {
|
||||
private val _names: Array<String>
|
||||
private var _listener: MDNSListener? = null
|
||||
private var _started = false
|
||||
private var _thread: Thread? = null
|
||||
|
||||
init {
|
||||
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||
_names = names
|
||||
}
|
||||
|
||||
fun broadcastService(
|
||||
deviceName: String,
|
||||
serviceName: String,
|
||||
port: UShort,
|
||||
ttl: UInt = 120u,
|
||||
weight: UShort = 0u,
|
||||
priority: UShort = 0u,
|
||||
texts: List<String>? = null
|
||||
) {
|
||||
_listener?.let {
|
||||
it.broadcastService(deviceName, serviceName, port, ttl, weight, priority, texts)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
_started = false
|
||||
_listener?.stop()
|
||||
_listener = null
|
||||
_thread?.join()
|
||||
_thread = null
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (_started) throw Exception("Already running.")
|
||||
_started = true
|
||||
|
||||
val listener = MDNSListener()
|
||||
_listener = listener
|
||||
listener.onServicesUpdated = { _onServicesUpdated?.invoke(it) }
|
||||
listener.start()
|
||||
|
||||
_thread = Thread {
|
||||
try {
|
||||
sleep(2000)
|
||||
|
||||
while (_started) {
|
||||
listener.queryServices(_names)
|
||||
sleep(2000)
|
||||
listener.queryAllQuestions(_names)
|
||||
sleep(2000)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Exception in loop thread", e)
|
||||
stop()
|
||||
}
|
||||
}.apply { start() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "ServiceDiscoverer"
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package com.futo.platformplayer.mdns
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.InetAddress
|
||||
import java.util.Date
|
||||
|
||||
data class DnsService(
|
||||
var name: String,
|
||||
var target: String,
|
||||
var port: UShort,
|
||||
val addresses: MutableList<InetAddress> = mutableListOf(),
|
||||
val pointers: MutableList<String> = mutableListOf(),
|
||||
val texts: MutableList<String> = mutableListOf()
|
||||
)
|
||||
|
||||
data class CachedDnsAddressRecord(
|
||||
val expirationTime: Date,
|
||||
val address: InetAddress
|
||||
)
|
||||
|
||||
data class CachedDnsTxtRecord(
|
||||
val expirationTime: Date,
|
||||
val texts: List<String>
|
||||
)
|
||||
|
||||
data class CachedDnsPtrRecord(
|
||||
val expirationTime: Date,
|
||||
val target: String
|
||||
)
|
||||
|
||||
data class CachedDnsSrvRecord(
|
||||
val expirationTime: Date,
|
||||
val service: SRVRecord
|
||||
)
|
||||
|
||||
class ServiceRecordAggregator {
|
||||
private val _lockObject = Any()
|
||||
private val _cachedAddressRecords = mutableMapOf<String, MutableList<CachedDnsAddressRecord>>()
|
||||
private val _cachedTxtRecords = mutableMapOf<String, CachedDnsTxtRecord>()
|
||||
private val _cachedPtrRecords = mutableMapOf<String, MutableList<CachedDnsPtrRecord>>()
|
||||
private val _cachedSrvRecords = mutableMapOf<String, CachedDnsSrvRecord>()
|
||||
private val _currentServices = mutableListOf<DnsService>()
|
||||
private var _cts: Job? = null
|
||||
|
||||
var onServicesUpdated: ((List<DnsService>) -> Unit)? = null
|
||||
|
||||
fun start() {
|
||||
synchronized(_lockObject) {
|
||||
if (_cts != null) throw Exception("Already started.")
|
||||
|
||||
_cts = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (isActive) {
|
||||
val now = Date()
|
||||
synchronized(_currentServices) {
|
||||
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
|
||||
val newServices = getCurrentServices()
|
||||
_currentServices.clear()
|
||||
_currentServices.addAll(newServices)
|
||||
}
|
||||
|
||||
onServicesUpdated?.invoke(_currentServices)
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
synchronized(_lockObject) {
|
||||
_cts?.cancel()
|
||||
_cts = null
|
||||
}
|
||||
}
|
||||
|
||||
fun add(packet: DnsPacket) {
|
||||
val dnsResourceRecords = packet.answers + packet.additionals + packet.authorities
|
||||
val txtRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.TXT.value.toInt() }.map { it to it.getDataReader().readTXTRecord() }
|
||||
val aRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.A.value.toInt() }.map { it to it.getDataReader().readARecord() }
|
||||
val aaaaRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.AAAA.value.toInt() }.map { it to it.getDataReader().readAAAARecord() }
|
||||
val srvRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.SRV.value.toInt() }.map { it to it.getDataReader().readSRVRecord() }
|
||||
val ptrRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.PTR.value.toInt() }.map { it to it.getDataReader().readPTRRecord() }
|
||||
|
||||
/*val builder = StringBuilder()
|
||||
builder.appendLine("Received records:")
|
||||
srvRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: (Port: ${it.second.port}, Target: ${it.second.target}, Priority: ${it.second.priority}, Weight: ${it.second.weight})") }
|
||||
ptrRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.domainName}") }
|
||||
txtRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.texts.joinToString(", ")}") }
|
||||
aRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
|
||||
aaaaRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
|
||||
synchronized(lockObject) {
|
||||
// Save to file if necessary
|
||||
}*/
|
||||
|
||||
val currentServices: MutableList<DnsService>
|
||||
synchronized(this._currentServices) {
|
||||
ptrRecords.forEach { record ->
|
||||
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
||||
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
||||
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
||||
|
||||
aRecords.forEach { aRecord ->
|
||||
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
||||
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
||||
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
||||
}
|
||||
|
||||
aaaaRecords.forEach { aaaaRecord ->
|
||||
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
||||
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
||||
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
||||
}
|
||||
}
|
||||
|
||||
txtRecords.forEach { txtRecord ->
|
||||
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
||||
}
|
||||
|
||||
srvRecords.forEach { srvRecord ->
|
||||
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
||||
}
|
||||
|
||||
currentServices = getCurrentServices()
|
||||
this._currentServices.clear()
|
||||
this._currentServices.addAll(currentServices)
|
||||
}
|
||||
|
||||
onServicesUpdated?.invoke(currentServices)
|
||||
}
|
||||
|
||||
fun getAllQuestions(serviceName: String): List<DnsQuestion> {
|
||||
val questions = mutableListOf<DnsQuestion>()
|
||||
synchronized(_currentServices) {
|
||||
val servicePtrRecords = _cachedPtrRecords[serviceName] ?: return emptyList()
|
||||
|
||||
val ptrWithoutSrvRecord = servicePtrRecords.filterNot { _cachedSrvRecords.containsKey(it.target) }.map { it.target }
|
||||
questions.addAll(ptrWithoutSrvRecord.flatMap { s ->
|
||||
listOf(
|
||||
DnsQuestion(
|
||||
name = s,
|
||||
type = QuestionType.SRV.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val incompleteCurrentServices = _currentServices.filter { it.addresses.isEmpty() && it.name.endsWith(serviceName) }
|
||||
questions.addAll(incompleteCurrentServices.flatMap { s ->
|
||||
listOf(
|
||||
DnsQuestion(
|
||||
name = s.name,
|
||||
type = QuestionType.TXT.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
),
|
||||
DnsQuestion(
|
||||
name = s.target,
|
||||
type = QuestionType.A.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
),
|
||||
DnsQuestion(
|
||||
name = s.target,
|
||||
type = QuestionType.AAAA.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
return questions
|
||||
}
|
||||
|
||||
private fun getCurrentServices(): MutableList<DnsService> {
|
||||
val currentServices = _cachedSrvRecords.map { (key, value) ->
|
||||
DnsService(
|
||||
name = key,
|
||||
target = value.service.target,
|
||||
port = value.service.port
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
currentServices.forEach { service ->
|
||||
_cachedAddressRecords[service.target]?.let {
|
||||
service.addresses.addAll(it.map { record -> record.address })
|
||||
}
|
||||
}
|
||||
|
||||
currentServices.forEach { service ->
|
||||
service.pointers.addAll(_cachedPtrRecords.filter { it.value.any { ptr -> ptr.target == service.name } }.map { it.key })
|
||||
}
|
||||
|
||||
currentServices.forEach { service ->
|
||||
_cachedTxtRecords[service.name]?.let {
|
||||
service.texts.addAll(it.texts)
|
||||
}
|
||||
}
|
||||
|
||||
return currentServices
|
||||
}
|
||||
|
||||
private inline fun <T> MutableList<T>.replaceOrAdd(newElement: T, predicate: (T) -> Boolean) {
|
||||
val index = indexOfFirst(predicate)
|
||||
if (index >= 0) {
|
||||
this[index] = newElement
|
||||
} else {
|
||||
add(newElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.SocketException
|
||||
import java.time.Duration
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class DownloadService : Service() {
|
||||
@@ -49,12 +44,7 @@ class DownloadService : Service() {
|
||||
private var _notificationManager: NotificationManager? = null;
|
||||
private var _notificationChannel: NotificationChannel? = null;
|
||||
|
||||
private val _client = ManagedHttpClient(OkHttpClient.Builder()
|
||||
//.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(InetAddress.getByName("192.168.1.175"), 8081)))
|
||||
.readTimeout(Duration.ofMinutes(0))
|
||||
.writeTimeout(Duration.ofMinutes(0))
|
||||
.connectTimeout(Duration.ofSeconds(100))
|
||||
.callTimeout(Duration.ofMinutes(0)))
|
||||
private val _client = ManagedHttpClient();
|
||||
|
||||
private var _started = false;
|
||||
|
||||
@@ -193,19 +183,14 @@ class DownloadService : Service() {
|
||||
Logger.w(TAG, "Video Download [${download.name}] expired, re-preparing");
|
||||
download.videoDetails = null;
|
||||
|
||||
if(download.targetVideoName == null && download.videoSource != null)
|
||||
download.targetVideoName = download.videoSource!!.name;
|
||||
if(download.targetPixelCount == null && download.videoSource != null)
|
||||
download.targetPixelCount = (download.videoSource!!.width * download.videoSource!!.height).toLong();
|
||||
download.videoSource = null;
|
||||
|
||||
if(download.targetAudioName == null && download.audioSource != null)
|
||||
download.targetAudioName = download.audioSource!!.name;
|
||||
if(download.targetBitrate == null && download.audioSource != null)
|
||||
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
||||
download.audioSource = null;
|
||||
}
|
||||
if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady))
|
||||
if(download.videoDetails == null || (download.videoSource == null && download.audioSource == null))
|
||||
download.changeState(VideoDownload.State.PREPARING);
|
||||
notifyDownload(download);
|
||||
|
||||
@@ -222,7 +207,7 @@ class DownloadService : Service() {
|
||||
download.progress = progress;
|
||||
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastNotifyTime > 800) {
|
||||
if (currentTime - lastNotifyTime > 500) {
|
||||
notifyDownload(download);
|
||||
lastNotifyTime = currentTime;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,15 @@ class MediaPlaybackService : Service() {
|
||||
private var _audioFocusLossTime_ms: Long? = null
|
||||
private var _playbackState = PlaybackStateCompat.STATE_NONE;
|
||||
|
||||
private val _updateIntervalMs: Long = 5 * 60 * 1000
|
||||
private val _handler: Handler = Handler(Looper.getMainLooper())
|
||||
private val _updateRunnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
updateMediaSession(null)
|
||||
_handler.postDelayed(this, _updateIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Logger.v(TAG, "onStartCommand");
|
||||
|
||||
@@ -76,6 +85,8 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
_callOnStarted?.invoke(this);
|
||||
_instance = this;
|
||||
|
||||
_handler.postDelayed(_updateRunnable, _updateIntervalMs)
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to start MediaPlaybackService due to: " + ex.message, ex);
|
||||
@@ -98,7 +109,7 @@ class MediaPlaybackService : Service() {
|
||||
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
||||
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
||||
.build());
|
||||
_mediaSession?.setCallback(object : MediaSessionCompat.Callback() {
|
||||
_mediaSession?.setCallback(object: MediaSessionCompat.Callback() {
|
||||
override fun onSeekTo(pos: Long) {
|
||||
super.onSeekTo(pos)
|
||||
Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)");
|
||||
@@ -120,9 +131,7 @@ class MediaPlaybackService : Service() {
|
||||
override fun onStop() {
|
||||
super.onStop();
|
||||
Logger.i(TAG, "Media session callback onStop()");
|
||||
//MediaControlReceiver.onCloseReceived.emit();
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
updateMediaSession( null);
|
||||
MediaControlReceiver.onCloseReceived.emit();
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
@@ -147,6 +156,7 @@ class MediaPlaybackService : Service() {
|
||||
override fun onDestroy() {
|
||||
Logger.v(TAG, "onDestroy");
|
||||
_instance = null;
|
||||
_handler.removeCallbacks(_updateRunnable)
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
@@ -57,18 +56,6 @@ class StateApp {
|
||||
|
||||
val sessionId = UUID.randomUUID().toString();
|
||||
|
||||
var privateMode: Boolean = false
|
||||
get(){
|
||||
return field;
|
||||
}
|
||||
private set(value) {
|
||||
field = value;
|
||||
}
|
||||
val privateModeChanged = Event1<Boolean>();
|
||||
fun setPrivacyMode(value: Boolean) {
|
||||
privateMode = value;
|
||||
privateModeChanged.emit(privateMode);
|
||||
}
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.SettingsDev
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/***
|
||||
@@ -31,12 +23,6 @@ class StateDeveloper {
|
||||
|
||||
var devProxy: DevProxySettings? = null;
|
||||
|
||||
var testState: String? = null;
|
||||
val isPlaybackTesting: Boolean get() {
|
||||
return SettingsDev.instance.developerMode && testState == "TestPlayback";
|
||||
};
|
||||
|
||||
|
||||
fun initializeDev(id: String) {
|
||||
currentDevID = id;
|
||||
synchronized(_devLogs) {
|
||||
@@ -149,37 +135,6 @@ class StateDeveloper {
|
||||
}
|
||||
|
||||
|
||||
private var homePager: IPager<IPlatformContent>? = null;
|
||||
private var pagerIndex = 0;
|
||||
fun testPlayback(){
|
||||
val mainActivity = if(StateApp.instance.isMainActive) StateApp.instance.context as MainActivity else return;
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
if(homePager == null)
|
||||
homePager = StatePlatform.instance.getHome();
|
||||
var pager = homePager ?: return@launch;
|
||||
pagerIndex++;
|
||||
val video = if(pager.getResults().size <= pagerIndex) {
|
||||
if(!pager.hasMorePages()) {
|
||||
homePager = StatePlatform.instance.getHome();
|
||||
pager = homePager as IPager<IPlatformContent>;
|
||||
}
|
||||
pager.nextPage();
|
||||
pagerIndex = 0;
|
||||
val results = pager.getResults();
|
||||
if(results.size <= 0)
|
||||
null;
|
||||
else
|
||||
results[0];
|
||||
}
|
||||
else
|
||||
pager.getResults()[pagerIndex];
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||
mainActivity.navigate(mainActivity._fragVideoDetail, video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEV_ID = "DEV";
|
||||
|
||||
@@ -197,7 +152,6 @@ class StateDeveloper {
|
||||
it._server?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
||||
@@ -8,9 +8,7 @@ import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
||||
@@ -336,7 +334,7 @@ class StateDownloads {
|
||||
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
||||
}
|
||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
||||
}
|
||||
|
||||
|
||||
@@ -96,8 +96,6 @@ class StateHistory {
|
||||
return historyIndex[url];
|
||||
}
|
||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||
if(StateApp.instance.privateMode)
|
||||
return null;
|
||||
val existing = historyIndex[video.url];
|
||||
var result: DBHistory.Index? = null;
|
||||
if(existing != null) {
|
||||
@@ -115,19 +113,6 @@ class StateHistory {
|
||||
return result;
|
||||
}
|
||||
|
||||
fun markAsWatched(video: IPlatformVideo) {
|
||||
try {
|
||||
val history = getHistoryByVideo(video, true, OffsetDateTime.now());
|
||||
if (history != null) {
|
||||
updateHistoryPosition(video, history, true, Math.max(1, video.duration - 1));
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to mark as watched", ex);
|
||||
UIDialogs.toast("Failed to mark as watched\n" + ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHistory(url: String) {
|
||||
val hist = getHistoryIndexByUrl(url);
|
||||
if(hist != null)
|
||||
|
||||
@@ -93,7 +93,6 @@ class StatePlatform {
|
||||
private val _channelClientPool = PlatformMultiClientPool("Channels", 15); //Used primarily for subscription/background channel fetches
|
||||
private val _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers
|
||||
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
||||
private val _privateClientPool = PlatformMultiClientPool("Private", 2, true); //Used primarily for calls if in incognito mode
|
||||
|
||||
|
||||
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
||||
@@ -110,24 +109,13 @@ class StatePlatform {
|
||||
//Batched Requests
|
||||
private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope,
|
||||
{ url ->
|
||||
|
||||
Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]");
|
||||
if(!StateApp.instance.privateMode) {
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
||||
}
|
||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "Fetching details with private client");
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_privateClientPool.getClientPooled(it).getContentDetails(url)
|
||||
}
|
||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
}
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
||||
} ?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
},
|
||||
{
|
||||
if(!Settings.instance.browsing.videoCache || StateApp.instance.privateMode)
|
||||
if(!Settings.instance.browsing.videoCache)
|
||||
return@BatchedTaskHandler null;
|
||||
else {
|
||||
val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null;
|
||||
@@ -143,7 +131,7 @@ class StatePlatform {
|
||||
}
|
||||
},
|
||||
{ para, result ->
|
||||
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive) || StateApp.instance.privateMode)
|
||||
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive))
|
||||
return@BatchedTaskHandler
|
||||
else {
|
||||
Logger.i(TAG, "Caching [${para}]");
|
||||
@@ -883,10 +871,7 @@ class StatePlatform {
|
||||
if(!client.capabilities.hasGetComments)
|
||||
return EmptyPager();
|
||||
|
||||
if(!StateApp.instance.privateMode)
|
||||
return client.fromPool(_mainClientPool).getComments(url);
|
||||
else
|
||||
return client.fromPool(_privateClientPool).getComments(url);
|
||||
return client.fromPool(_mainClientPool).getComments(url);
|
||||
}
|
||||
fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||
Logger.i(TAG, "Platform - getSubComments");
|
||||
@@ -897,11 +882,7 @@ class StatePlatform {
|
||||
fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? {
|
||||
Logger.i(TAG, "Platform - getLiveChat");
|
||||
var client = getContentClient(url);
|
||||
|
||||
if(!StateApp.instance.privateMode)
|
||||
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
||||
else
|
||||
return client.fromPool(_privateClientPool).getLiveEvents(url);
|
||||
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
||||
}
|
||||
fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? {
|
||||
Logger.i(TAG, "Platform - getLiveChat");
|
||||
|
||||
@@ -38,13 +38,7 @@ class StatePlayer {
|
||||
private var _thumbnailExoPlayer : PlayerManager? = null;
|
||||
|
||||
//Video Status
|
||||
var rotationLock: Boolean = false
|
||||
get() = field
|
||||
set(value) {
|
||||
field = value
|
||||
onRotationLockChanged.emit(value)
|
||||
}
|
||||
val onRotationLockChanged = Event1<Boolean>()
|
||||
var rotationLock : Boolean = false;
|
||||
var loopVideo : Boolean = false;
|
||||
|
||||
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
|
||||
|
||||
@@ -16,7 +16,7 @@ enum class FeedStyle(val value: Int) {
|
||||
|
||||
fun fromInt(value: Int): FeedStyle
|
||||
{
|
||||
val result = FeedStyle.entries.firstOrNull { it.value == value };
|
||||
val result = FeedStyle.values().firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw UnknownPlatformException(value.toString());
|
||||
return result;
|
||||
|
||||
+3
-23
@@ -6,17 +6,14 @@ import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.futo.platformplayer.R
|
||||
|
||||
class SlideUpMenuItem : ConstraintLayout {
|
||||
class SlideUpMenuItem : RelativeLayout {
|
||||
|
||||
private lateinit var _root: ConstraintLayout;
|
||||
private lateinit var _root: RelativeLayout;
|
||||
private lateinit var _image: ImageView;
|
||||
private lateinit var _text: TextView;
|
||||
private lateinit var _subtext: TextView;
|
||||
private lateinit var _description: TextView;
|
||||
|
||||
var selectedOption: Boolean = false;
|
||||
|
||||
@@ -28,27 +25,11 @@ class SlideUpMenuItem : ConstraintLayout {
|
||||
init();
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
imageRes: Int = 0,
|
||||
mainText: String,
|
||||
subText: String = "",
|
||||
description: String? = "",
|
||||
tag: Any?,
|
||||
call: (() -> Unit)? = null,
|
||||
invokeParent: Boolean = true
|
||||
): super(context){
|
||||
constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any?, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){
|
||||
init();
|
||||
_image.setImageResource(imageRes);
|
||||
_text.text = mainText;
|
||||
_subtext.text = subText;
|
||||
|
||||
if(description.isNullOrEmpty())
|
||||
_description.isVisible = false;
|
||||
else {
|
||||
_description.text = description;
|
||||
_description.isVisible = true;
|
||||
}
|
||||
this.itemTag = tag;
|
||||
|
||||
if (call != null) {
|
||||
@@ -67,7 +48,6 @@ class SlideUpMenuItem : ConstraintLayout {
|
||||
_image = findViewById(R.id.slide_up_menu_item_image);
|
||||
_text = findViewById(R.id.slide_up_menu_item_text);
|
||||
_subtext = findViewById(R.id.slide_up_menu_item_subtext);
|
||||
_description = findViewById(R.id.slide_up_menu_item_description);
|
||||
|
||||
setOptionSelected(false);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.widget.TextView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -124,8 +123,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
private var _currentChapterLoopId: Int = 0;
|
||||
private var _currentChapter: IChapter? = null;
|
||||
private var _promptedForPermissions: Boolean = false;
|
||||
@UnstableApi
|
||||
private var _desiredResizeModePortrait: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
|
||||
|
||||
//Events
|
||||
val onMinimize = Event1<FutoVideoPlayer>();
|
||||
@@ -569,8 +567,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setFullScreen(fullScreen: Boolean) {
|
||||
updateRotateLock()
|
||||
|
||||
if (isFullScreen == fullScreen) {
|
||||
return;
|
||||
}
|
||||
@@ -595,7 +591,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.VISIBLE;
|
||||
_videoView.resizeMode = _desiredResizeModePortrait;
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hideImmediately();
|
||||
@@ -732,10 +728,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
Log.d(TAG, "WEIRD HEIGHT DETECTED: ${_lastSourceFit}, Width: ${w}, Height: ${h}, VWidth: ${viewWidth}");
|
||||
}
|
||||
if(_lastSourceFit != determinedHeight)
|
||||
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
else
|
||||
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
_videoView.resizeMode = _desiredResizeModePortrait
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
}
|
||||
|
||||
val marginBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics).toInt();
|
||||
@@ -764,7 +759,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
fun updateRotateLock() {
|
||||
if(Settings.instance.playback.autoRotate == 0) {
|
||||
if(!Settings.instance.playback.isAutoRotate()) {
|
||||
_control_rotate_lock.visibility = View.GONE;
|
||||
_control_rotate_lock_fullscreen.visibility = View.GONE;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,9 @@ package com.futo.platformplayer.views.video
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.util.Xml
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.Encoding
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
@@ -19,11 +14,8 @@ import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifest
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
@@ -34,7 +26,6 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
@@ -45,31 +36,22 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
import com.google.gson.Gson
|
||||
import getHttpDataSourceFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -86,7 +68,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
private set;
|
||||
|
||||
private var _lastVideoMediaSource: MediaSource? = null;
|
||||
private var _lastGeneratedDash: String? = null;
|
||||
private var _lastAudioMediaSource: MediaSource? = null;
|
||||
private var _lastSubtitleMediaSource: MediaSource? = null;
|
||||
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
|
||||
@@ -331,37 +312,18 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
swapSources(videoSource, audioSource,false, play, keepSubtitles);
|
||||
}
|
||||
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
||||
var videoSourceUsed = videoSource;
|
||||
var audioSourceUsed = audioSource;
|
||||
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
|
||||
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
|
||||
audioSourceUsed = null;
|
||||
}
|
||||
|
||||
val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume);
|
||||
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume);
|
||||
swapSourceInternal(videoSource);
|
||||
swapSourceInternal(audioSource);
|
||||
if(!keepSubtitles)
|
||||
_lastSubtitleMediaSource = null;
|
||||
if(didSetVideo && didSetAudio)
|
||||
return loadSelectedSources(play, resume);
|
||||
else
|
||||
return true;
|
||||
return loadSelectedSources(play, resume);
|
||||
}
|
||||
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
var videoSourceUsed = videoSource;
|
||||
if(videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
|
||||
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
|
||||
val didSet = swapSourceInternal(videoSourceUsed, play, resume);
|
||||
if(didSet)
|
||||
return loadSelectedSources(play, resume);
|
||||
else
|
||||
return true;
|
||||
swapSourceInternal(videoSource);
|
||||
return loadSelectedSources(play, resume);
|
||||
}
|
||||
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
|
||||
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
|
||||
else
|
||||
swapSourceInternal(audioSource, play, resume);
|
||||
swapSourceInternal(audioSource);
|
||||
return loadSelectedSources(play, resume);
|
||||
}
|
||||
|
||||
@@ -412,34 +374,29 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
|
||||
|
||||
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
_lastGeneratedDash = null;
|
||||
val didSet = when(videoSource) {
|
||||
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume);
|
||||
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||
null -> { _lastVideoMediaSource = null; true;}
|
||||
private fun swapSourceInternal(videoSource: IVideoSource?) {
|
||||
when(videoSource) {
|
||||
is LocalVideoSource -> swapVideoSourceLocal(videoSource);
|
||||
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
|
||||
is IDashManifestSource -> swapVideoSourceDash(videoSource);
|
||||
is IHLSManifestSource -> swapVideoSourceHLS(videoSource);
|
||||
is IVideoUrlSource -> swapVideoSourceUrl(videoSource);
|
||||
null -> _lastVideoMediaSource = null;
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
||||
}
|
||||
lastVideoSource = videoSource;
|
||||
return didSet;
|
||||
}
|
||||
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
val didSet = when(audioSource) {
|
||||
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
|
||||
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
|
||||
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
|
||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume);
|
||||
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
||||
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||
null -> { _lastAudioMediaSource = null; true; }
|
||||
private fun swapSourceInternal(audioSource: IAudioSource?) {
|
||||
when(audioSource) {
|
||||
is LocalAudioSource -> swapAudioSourceLocal(audioSource);
|
||||
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
|
||||
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
|
||||
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
|
||||
is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
|
||||
null -> _lastAudioMediaSource = null;
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||
}
|
||||
lastAudioSource = audioSource;
|
||||
return didSet;
|
||||
}
|
||||
|
||||
//Video loads
|
||||
@@ -458,9 +415,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
if(videoSource.hasItag) {
|
||||
//Temporary workaround for Youtube
|
||||
try {
|
||||
val results = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
||||
_lastGeneratedDash = results.second;
|
||||
_lastVideoMediaSource = results.first;
|
||||
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
||||
return;
|
||||
}
|
||||
//If it fails to create the dash workaround, fallback to standard progressive
|
||||
@@ -476,7 +431,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [Url]");
|
||||
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
@@ -486,7 +441,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
@@ -494,60 +449,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
|
||||
if(videoSource.hasGenerate) {
|
||||
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val generated = videoSource.generate();
|
||||
if (generated != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
|
||||
if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource)
|
||||
dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor());
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(
|
||||
DashManifestParser().parse(
|
||||
Uri.parse(videoSource.url),
|
||||
ByteArrayInputStream(
|
||||
generated?.toByteArray() ?: ByteArray(0)
|
||||
)
|
||||
)
|
||||
);
|
||||
if(lastVideoSource == videoSource || (videoSource is JSDashManifestMergingRawSource && videoSource.video == lastVideoSource));
|
||||
loadSelectedSources(play, resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "DashRaw generator failed", ex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
|
||||
if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource)
|
||||
dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor());
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url),
|
||||
ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0))));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [HLS]");
|
||||
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
@@ -589,7 +493,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [Url]");
|
||||
val dataSource = if(audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
@@ -599,7 +503,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [HLS]");
|
||||
val dataSource = if(audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
@@ -607,42 +511,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean {
|
||||
Logger.i(TAG, "Loading AudioSource [DashRaw]");
|
||||
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
if(audioSource.hasGenerate) {
|
||||
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
|
||||
val generated = audioSource.generate();
|
||||
if(generated != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
|
||||
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0))));
|
||||
loadSelectedSources(play, resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(
|
||||
DashManifestParser().parse(
|
||||
Uri.parse(audioSource.url),
|
||||
ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0))
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||
val dataSource = if (audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
val dataSource = if (audioSource is JSSource && audioSource.hasRequestModifier)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
@@ -692,36 +564,27 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val sourceAudio = _lastAudioMediaSource;
|
||||
val sourceSubs = _lastSubtitleMediaSource;
|
||||
|
||||
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
|
||||
|
||||
beforeSourceChanged();
|
||||
|
||||
val source = mergeMediaSources(sourceVideo, sourceAudio, sourceSubs);
|
||||
if(source == null)
|
||||
return false;
|
||||
_mediaSource = source;
|
||||
|
||||
reloadMediaSource(play, resume);
|
||||
return true;
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun mergeMediaSources(sourceVideo: MediaSource?, sourceAudio: MediaSource?, sourceSubs: MediaSource?): MediaSource? {
|
||||
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
|
||||
if(sources.size == 1) {
|
||||
_mediaSource = if(sources.size == 1) {
|
||||
Logger.i(TAG, "Using single source mode")
|
||||
return (sourceVideo ?: sourceAudio);
|
||||
(sourceVideo ?: sourceAudio);
|
||||
}
|
||||
else if(sources.size > 1) {
|
||||
Logger.i(TAG, "Using multi source mode ${sources.size}")
|
||||
return MergingMediaSource(true, *sources);
|
||||
MergingMediaSource(true, *sources);
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "Using no sources loaded");
|
||||
stop();
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reloadMediaSource(play, resume);
|
||||
return true;
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
|
||||
@@ -746,10 +609,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
fun clear() {
|
||||
exoPlayer?.player?.stop();
|
||||
exoPlayer?.player?.clearMediaItems();
|
||||
_lastVideoMediaSource = null;
|
||||
_lastAudioMediaSource = null;
|
||||
_lastSubtitleMediaSource = null;
|
||||
_mediaSource = null;
|
||||
}
|
||||
|
||||
fun stop(){
|
||||
@@ -776,17 +635,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
||||
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
|
||||
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
|
||||
|
||||
Logger.w(TAG, null) {
|
||||
"ERROR BAD HTTP ${cause.responseCode},\n" +
|
||||
"Video Source: ${lastVideoSource?.toString()}\n" +
|
||||
"Audio Source: ${lastAudioSource?.toString()}\n" +
|
||||
"Dash: ${_lastGeneratedDash}"
|
||||
};
|
||||
}
|
||||
onDatasourceError.emit(error);
|
||||
}
|
||||
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||
@@ -828,15 +676,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
companion object {
|
||||
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||
|
||||
val PREFERED_VIDEO_CONTAINERS_MP4Pref = arrayOf("video/mp4", "video/webm", "video/3gpp");
|
||||
val PREFERED_VIDEO_CONTAINERS_WEBMPref = arrayOf("video/webm", "video/mp4", "video/3gpp");
|
||||
val PREFERED_VIDEO_CONTAINERS: Array<String> get() { return if(Settings.instance.playback.preferWebmVideo)
|
||||
PREFERED_VIDEO_CONTAINERS_WEBMPref else PREFERED_VIDEO_CONTAINERS_MP4Pref }
|
||||
|
||||
val PREFERED_AUDIO_CONTAINERS_MP4Pref = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus");
|
||||
val PREFERED_AUDIO_CONTAINERS_WEBMPref = arrayOf("audio/webm", "audio/opus", "audio/mp3", "audio/mp4");
|
||||
val PREFERED_AUDIO_CONTAINERS: Array<String> get() { return if(Settings.instance.playback.preferWebmAudio)
|
||||
PREFERED_AUDIO_CONTAINERS_WEBMPref else PREFERED_AUDIO_CONTAINERS_MP4Pref }
|
||||
val PREFERED_VIDEO_CONTAINERS = arrayOf("video/mp4", "video/webm", "video/3gpp");
|
||||
val PREFERED_AUDIO_CONTAINERS = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus");
|
||||
|
||||
val SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip");
|
||||
}
|
||||
|
||||
+105
-182
@@ -13,8 +13,6 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.futo.platformplayer.api.media.models.modifier.IRequest;
|
||||
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest;
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor;
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
@@ -27,17 +25,11 @@ import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.datasource.HttpUtil;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject;
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException;
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException;
|
||||
import com.futo.platformplayer.logging.Logger;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ForwardingMap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
@@ -53,8 +45,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import kotlinx.serialization.json.Json;
|
||||
|
||||
/*
|
||||
* Based on the default ExoPlayer DefaultHttpDataSource
|
||||
*/
|
||||
@@ -73,9 +63,6 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
private boolean allowCrossProtocolRedirects;
|
||||
private boolean keepPostFor302Redirects;
|
||||
@Nullable private IRequestModifier requestModifier = null;
|
||||
@Nullable public JSRequestExecutor requestExecutor = null;
|
||||
@Nullable public JSRequestExecutor requestExecutor2 = null;
|
||||
|
||||
|
||||
/** Creates an instance. */
|
||||
public Factory() {
|
||||
@@ -102,30 +89,6 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
this.requestModifier = requestModifier;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Sets the request executor that will be used.
|
||||
*
|
||||
* <p>The default is {@code null}, which results in no request modification
|
||||
*
|
||||
* @param requestExecutor The request modifier that will be used, or {@code null} to use no request modifier
|
||||
* @return This factory.
|
||||
*/
|
||||
public Factory setRequestExecutor(@Nullable JSRequestExecutor requestExecutor) {
|
||||
this.requestExecutor = requestExecutor;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Sets the secondary request executor that will be used.
|
||||
*
|
||||
* <p>The default is {@code null}, which results in no request modification
|
||||
*
|
||||
* @param requestExecutor The request modifier that will be used, or {@code null} to use no request modifier
|
||||
* @return This factory.
|
||||
*/
|
||||
public Factory setRequestExecutor2(@Nullable JSRequestExecutor requestExecutor) {
|
||||
this.requestExecutor2 = requestExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user agent that will be used.
|
||||
@@ -232,9 +195,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
defaultRequestProperties,
|
||||
contentTypePredicate,
|
||||
keepPostFor302Redirects,
|
||||
requestModifier,
|
||||
requestExecutor,
|
||||
requestExecutor2);
|
||||
requestModifier);
|
||||
if (transferListener != null) {
|
||||
dataSource.addTransferListener(transferListener);
|
||||
}
|
||||
@@ -270,10 +231,6 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
private long bytesToRead;
|
||||
private long bytesRead;
|
||||
@Nullable private IRequestModifier requestModifier;
|
||||
@Nullable public JSRequestExecutor requestExecutor;
|
||||
@Nullable public JSRequestExecutor requestExecutor2; //Not ideal, but required for now to have 2 executors under 1 datasource
|
||||
|
||||
private Uri fallbackUri = null;
|
||||
|
||||
private JSHttpDataSource(
|
||||
@Nullable String userAgent,
|
||||
@@ -283,9 +240,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
@Nullable RequestProperties defaultRequestProperties,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
boolean keepPostFor302Redirects,
|
||||
@Nullable IRequestModifier requestModifier,
|
||||
@Nullable JSRequestExecutor requestExecutor,
|
||||
@Nullable JSRequestExecutor requestExecutor2) {
|
||||
@Nullable IRequestModifier requestModifier) {
|
||||
super(/* isNetwork= */ true);
|
||||
this.userAgent = userAgent;
|
||||
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||
@@ -296,14 +251,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
this.requestProperties = new RequestProperties();
|
||||
this.keepPostFor302Redirects = keepPostFor302Redirects;
|
||||
this.requestModifier = requestModifier;
|
||||
this.requestExecutor = requestExecutor;
|
||||
this.requestExecutor2 = requestExecutor2;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return connection == null ? fallbackUri : Uri.parse(connection.getURL().toString());
|
||||
return connection == null ? null : Uri.parse(connection.getURL().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -353,147 +306,119 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
bytesToRead = 0;
|
||||
transferInitializing(dataSpec);
|
||||
|
||||
//Use executor 2 if it matches the urlPrefix
|
||||
JSRequestExecutor executor = (requestExecutor2 != null && requestExecutor2.getUrlPrefix() != null && dataSpec.uri.toString().startsWith(requestExecutor2.getUrlPrefix())) ?
|
||||
requestExecutor2 : requestExecutor;
|
||||
|
||||
if(executor != null) {
|
||||
try {
|
||||
Logger.Companion.i(TAG, "Executor for " + dataSpec.uri.toString(), null);
|
||||
byte[] data = executor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders);
|
||||
Logger.Companion.i(TAG, "Executor result for " + dataSpec.uri.toString() + " : " + data.length, null);
|
||||
if (data == null)
|
||||
throw new HttpDataSourceException(
|
||||
"No response",
|
||||
dataSpec,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
HttpDataSourceException.TYPE_OPEN);
|
||||
inputStream = new ByteArrayInputStream(data);
|
||||
fallbackUri = dataSpec.uri;
|
||||
bytesToRead = data.length;
|
||||
|
||||
transferStarted(dataSpec);
|
||||
return data.length;
|
||||
}
|
||||
catch(PluginException ex) {
|
||||
throw HttpDataSourceException.createForIOException(new IOException("Executor failed: " + ex.getMessage(), ex), dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
String responseMessage;
|
||||
HttpURLConnection connection;
|
||||
try {
|
||||
this.connection = makeConnection(dataSpec);
|
||||
connection = this.connection;
|
||||
responseCode = connection.getResponseCode();
|
||||
responseMessage = connection.getResponseMessage();
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw HttpDataSourceException.createForIOException(
|
||||
e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
else {
|
||||
String responseMessage;
|
||||
HttpURLConnection connection;
|
||||
|
||||
// Check for a valid response code.
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
Map<String, List<String>> headers = connection.getHeaderFields();
|
||||
if (responseCode == 416) {
|
||||
long documentSize = HttpUtil.getDocumentSize(connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
|
||||
if (dataSpec.position == documentSize) {
|
||||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable InputStream errorStream = connection.getErrorStream();
|
||||
byte[] errorResponseBody;
|
||||
try {
|
||||
this.connection = makeConnection(dataSpec);
|
||||
connection = this.connection;
|
||||
responseCode = connection.getResponseCode();
|
||||
responseMessage = connection.getResponseMessage();
|
||||
errorResponseBody =
|
||||
errorStream != null ? Util.toByteArray(errorStream) : Util.EMPTY_BYTE_ARRAY;
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw HttpDataSourceException.createForIOException(
|
||||
e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
errorResponseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
closeConnectionQuietly();
|
||||
@Nullable
|
||||
IOException cause = responseCode == 416
|
||||
? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
|
||||
: null;
|
||||
|
||||
// Check for a valid response code.
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
Map<String, List<String>> headers = connection.getHeaderFields();
|
||||
if (responseCode == 416) {
|
||||
long documentSize = HttpUtil.getDocumentSize(connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
|
||||
if (dataSpec.position == documentSize) {
|
||||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
|
||||
}
|
||||
}
|
||||
throw new InvalidResponseCodeException(
|
||||
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody);
|
||||
}
|
||||
|
||||
@Nullable InputStream errorStream = connection.getErrorStream();
|
||||
byte[] errorResponseBody;
|
||||
try {
|
||||
errorResponseBody =
|
||||
errorStream != null ? Util.toByteArray(errorStream) : Util.EMPTY_BYTE_ARRAY;
|
||||
} catch (IOException e) {
|
||||
errorResponseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
closeConnectionQuietly();
|
||||
@Nullable
|
||||
IOException cause = responseCode == 416
|
||||
? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
|
||||
: null;
|
||||
// Check for a valid content type.
|
||||
String contentType = connection.getContentType();
|
||||
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
|
||||
closeConnectionQuietly();
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
}
|
||||
|
||||
throw new InvalidResponseCodeException(
|
||||
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody);
|
||||
}
|
||||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
long bytesToSkip;
|
||||
if (requestModifier != null && !requestModifier.getAllowByteSkip()) {
|
||||
bytesToSkip = 0;
|
||||
} else {
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
}
|
||||
|
||||
// Check for a valid content type.
|
||||
String contentType = connection.getContentType();
|
||||
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
|
||||
closeConnectionQuietly();
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
}
|
||||
|
||||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
long bytesToSkip;
|
||||
if (requestModifier != null && !requestModifier.getAllowByteSkip()) {
|
||||
bytesToSkip = 0;
|
||||
} else {
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
}
|
||||
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
boolean isCompressed = isCompressed(connection);
|
||||
if (!isCompressed) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesToRead = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = HttpUtil.getContentLength(
|
||||
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
|
||||
connection.getHeaderField(HttpHeaders.CONTENT_RANGE)
|
||||
);
|
||||
|
||||
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||
}
|
||||
} else {
|
||||
// Gzip is enabled. If the server opts to use gzip then the content length in the response
|
||||
// will be that of the compressed data, which isn't what we want. Always use the dataSpec
|
||||
// length in this case.
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
boolean isCompressed = isCompressed(connection);
|
||||
if (!isCompressed) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesToRead = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = HttpUtil.getContentLength(
|
||||
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
|
||||
connection.getHeaderField(HttpHeaders.CONTENT_RANGE)
|
||||
);
|
||||
|
||||
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
if (isCompressed) {
|
||||
inputStream = new GZIPInputStream(inputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw new HttpDataSourceException(
|
||||
e,
|
||||
dataSpec,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
|
||||
try {
|
||||
skipFully(bytesToSkip, dataSpec);
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
|
||||
if (e instanceof HttpDataSourceException) {
|
||||
throw (HttpDataSourceException) e;
|
||||
}
|
||||
throw new HttpDataSourceException(
|
||||
e,
|
||||
dataSpec,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
return bytesToRead;
|
||||
} else {
|
||||
// Gzip is enabled. If the server opts to use gzip then the content length in the response
|
||||
// will be that of the compressed data, which isn't what we want. Always use the dataSpec
|
||||
// length in this case.
|
||||
bytesToRead = dataSpec.length;
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
if (isCompressed) {
|
||||
inputStream = new GZIPInputStream(inputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw new HttpDataSourceException(
|
||||
e,
|
||||
dataSpec,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
|
||||
try {
|
||||
skipFully(bytesToSkip, dataSpec);
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
|
||||
if (e instanceof HttpDataSourceException) {
|
||||
throw (HttpDataSourceException) e;
|
||||
}
|
||||
throw new HttpDataSourceException(
|
||||
e,
|
||||
dataSpec,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -657,8 +582,6 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
requestHeaders = result.getHeaders();
|
||||
}
|
||||
|
||||
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl + "\nHEADERS: [" + V8RemoteObject.Companion.getGsonStandard().toJson(requestHeaders)+ "]", null);
|
||||
|
||||
HttpURLConnection connection = openConnection(new URL(requestUrl));
|
||||
connection.setConnectTimeout(connectTimeoutMillis);
|
||||
connection.setReadTimeout(readTimeoutMillis);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#CC111111" />
|
||||
<corners android:radius="100dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#635DAC"
|
||||
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
|
||||
</vector>
|
||||
@@ -70,21 +70,6 @@
|
||||
android:visibility="gone"
|
||||
android:elevation="15dp">
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/incognito_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_disabled_visible_purple"
|
||||
android:background="@drawable/background_button_round_black"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="visible"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:elevation="50dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/toast_view" />
|
||||
|
||||
<com.futo.platformplayer.views.ToastView
|
||||
android:id="@+id/toast_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -94,5 +79,4 @@
|
||||
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
|
||||
app:layout_constraintRight_toRightOf="@id/fragment_main"
|
||||
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
@@ -52,7 +52,6 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:maxHeight="120dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginTop="5dp"
|
||||
|
||||
@@ -6,17 +6,6 @@
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/app_icon"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--
|
||||
<ImageButton
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
@@ -24,19 +13,13 @@
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_construction" />
|
||||
-->
|
||||
|
||||
<TextView
|
||||
<!--<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="22dp"
|
||||
android:layout_marginTop="-2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="Grayjay"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>
|
||||
<!--
|
||||
android:paddingRight="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
@@ -59,8 +42,6 @@
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>
|
||||
-->
|
||||
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -27,6 +34,30 @@
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<!--
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/under"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="3dp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/construction"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -1,81 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/background_slide_up_option"
|
||||
android:id="@+id/slide_up_menu_item_root"
|
||||
android:gravity="center_vertical">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/slide_up_menu_item_root"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/background_slide_up_option"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<ImageView
|
||||
android:id="@+id/slide_up_menu_item_image"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_gravity="center_vertical"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:srcCompat="@drawable/ic_share" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/slide_up_menu_item_image"
|
||||
app:layout_constraintRight_toLeftOf="@id/slide_up_menu_item_subtext"
|
||||
android:gravity="start"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/slide_up_menu_item_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="20dp"
|
||||
tools:text="Cat videos"
|
||||
android:textSize="11dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white" />
|
||||
<TextView
|
||||
android:id="@+id/slide_up_menu_item_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="20dp"
|
||||
tools:text="test Description"
|
||||
android:textSize="11dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="#ACACAC" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/slide_up_menu_item_subtext"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="15dp"
|
||||
tools:text="3 videos"
|
||||
android:textSize="11dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:textColor="#ACACAC" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ImageView
|
||||
android:id="@+id/slide_up_menu_item_image"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_share" />
|
||||
<TextView
|
||||
android:id="@+id/slide_up_menu_item_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="45dp"
|
||||
tools:text="Cat videos"
|
||||
android:textSize="11dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white" />
|
||||
<TextView
|
||||
android:id="@+id/slide_up_menu_item_subtext"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="15dp"
|
||||
tools:text="3 videos"
|
||||
android:textSize="11dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="#ACACAC" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -7,7 +7,6 @@
|
||||
<string name="add_to">Add to</string>
|
||||
<string name="lorem_ipsum" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
|
||||
<string name="add_to_queue">Add to queue</string>
|
||||
<string name="add_to_history">Add to history</string>
|
||||
<string name="general">General</string>
|
||||
<string name="channel">Channel</string>
|
||||
<string name="home">Home</string>
|
||||
@@ -26,7 +25,6 @@
|
||||
<string name="sources">Sources</string>
|
||||
<string name="buy">Buy</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="privacy_mode">Privacy Mode</string>
|
||||
<string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
|
||||
<string name="defaults">Defaults</string>
|
||||
<string name="home_screen">Home Screen</string>
|
||||
@@ -68,8 +66,6 @@
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="keep_screen_on">Keep screen on</string>
|
||||
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
|
||||
<string name="always_proxy_requests">Always proxy requests</string>
|
||||
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
|
||||
<string name="discover">Discover</string>
|
||||
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
||||
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||
@@ -285,8 +281,6 @@
|
||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||
<string name="auto_update">Auto Update</string>
|
||||
<string name="auto_rotate">Auto-Rotate</string>
|
||||
<string name="simplify_sources">Simplify sources</string>
|
||||
<string name="simplify_sources_description">Deduplicate sources by resolution so that only more relevant sources are visible.</string>
|
||||
<string name="auto_rotate_dead_zone">Auto-Rotate Dead Zone</string>
|
||||
<string name="automatic_backup">Automatic Backup</string>
|
||||
<string name="background_behavior">Background Behavior</string>
|
||||
@@ -373,12 +367,6 @@
|
||||
<string name="system_volume_descr">Gesture controls adjust system volume</string>
|
||||
<string name="live_chat_webview">Live Chat Webview</string>
|
||||
<string name="full_screen_portrait">Fullscreen portrait</string>
|
||||
<string name="prefer_webm">Prefer Webm Video Codecs</string>
|
||||
<string name="prefer_webm_description">If player should prefer Webm codecs (vp9/opus) over mp4 codecs (h264/AAC), may result in worse compatibility.</string>
|
||||
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
|
||||
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
|
||||
<string name="allow_under_cutout">Allow video under cutout</string>
|
||||
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.</string>
|
||||
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
|
||||
<string name="background_switch_audio">Switch to Audio in Background</string>
|
||||
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||
@@ -401,7 +389,6 @@
|
||||
<string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string>
|
||||
<string name="bypass_rotation_prevention_description">Allows for rotation on non-video views.\nWARNING: Not designed for it</string>
|
||||
<string name="bypass_rotation_prevention_warning">This may cause unexpected behavior, and is mostly untested.</string>
|
||||
<string name="changing_this_field_requires_restart">Changing this field will require an app restart.</string>
|
||||
<string name="player">Player</string>
|
||||
<string name="plugins">Plugins</string>
|
||||
<string name="preferred_casting_quality">Preferred Casting Quality</string>
|
||||
@@ -488,8 +475,6 @@
|
||||
<string name="removes_all_subscriptions">Removes all subscriptions</string>
|
||||
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
|
||||
<string name="start_server">Start Server</string>
|
||||
<string name="test_playback">Test Playback</string>
|
||||
<string name="test_playback_desc">Keeps playing videos</string>
|
||||
<string name="subscriptions_cache_5000">Subscriptions Cache 5000</string>
|
||||
<string name="history_cache_100">History Cache 100</string>
|
||||
<string name="start_server_on_boot">Start Server on boot</string>
|
||||
@@ -777,8 +762,6 @@
|
||||
<string name="zoom">Zoom</string>
|
||||
<string name="check_to_see_if_an_update_is_available">Check to see if an update is available.</string>
|
||||
<string name="scroll_to_top">Scroll to top</string>
|
||||
<string name="disable_battery_optimization">Disable Battery Optimization</string>
|
||||
<string name="click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions">Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
||||
@@ -33,11 +33,6 @@
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
<data android:host="dailymotion.com" />
|
||||
<data android:host="www.dailymotion.com" />
|
||||
<data android:host="bitchute.com" />
|
||||
<data android:host="www.bitchute.com" />
|
||||
<data android:host="old.bitchute.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
@@ -62,11 +57,6 @@
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
<data android:host="dailymotion.com" />
|
||||
<data android:host="www.dailymotion.com" />
|
||||
<data android:host="bitchute.com" />
|
||||
<data android:host="www.bitchute.com" />
|
||||
<data android:host="old.bitchute.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: 850acb49a8...b518be4dd5
Submodule app/src/stable/assets/sources/bitchute deleted from 872bfa2d75
Submodule app/src/stable/assets/sources/dailymotion deleted from 069aa3d31a
Submodule app/src/stable/assets/sources/rumble updated: cbfe372bcc...bedbc4a989
Submodule app/src/stable/assets/sources/spotify updated: 4d554e9388...c700081466
Submodule app/src/stable/assets/sources/youtube updated: a4766ec223...d2a903d5dd
@@ -10,9 +10,7 @@
|
||||
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json",
|
||||
"9c87e8db-e75d-48f4-afe5-2d203d4b95c5": "sources/dailymotion/build/DailymotionConfig.json",
|
||||
"e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json"
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
|
||||
@@ -1,394 +0,0 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.mdns.DnsOpcode
|
||||
import com.futo.platformplayer.mdns.DnsPacket
|
||||
import com.futo.platformplayer.mdns.DnsPacketHeader
|
||||
import com.futo.platformplayer.mdns.DnsQuestion
|
||||
import com.futo.platformplayer.mdns.DnsReader
|
||||
import com.futo.platformplayer.mdns.DnsResponseCode
|
||||
import com.futo.platformplayer.mdns.DnsWriter
|
||||
import com.futo.platformplayer.mdns.QueryResponse
|
||||
import com.futo.platformplayer.mdns.QuestionClass
|
||||
import com.futo.platformplayer.mdns.QuestionType
|
||||
import com.futo.platformplayer.mdns.ResourceRecordClass
|
||||
import com.futo.platformplayer.mdns.ResourceRecordType
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetAddress
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
|
||||
|
||||
class MdnsTests {
|
||||
|
||||
@Test
|
||||
fun `BasicOperation`() {
|
||||
val expectedData = byteArrayOf(
|
||||
0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03
|
||||
)
|
||||
|
||||
val writer = DnsWriter()
|
||||
writer.write(1.toUShort())
|
||||
writer.write(2.toUInt())
|
||||
writer.write(3.toULong())
|
||||
writer.write(1.toShort())
|
||||
writer.write(2)
|
||||
writer.write(3L)
|
||||
|
||||
assertContentEquals(expectedData, writer.toByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DnsQuestionFormat`() {
|
||||
val expectedBytes = ubyteArrayOf(
|
||||
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x08u, 0x5fu, 0x61u, 0x69u, 0x72u, 0x70u, 0x6cu, 0x61u, 0x79u, 0x04u, 0x5fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6cu, 0x6fu, 0x63u, 0x61u, 0x6cu, 0x00u, 0x00u, 0x0cu, 0x00u, 0x01u
|
||||
).asByteArray()
|
||||
|
||||
val writer = DnsWriter()
|
||||
writer.writePacket(
|
||||
header = DnsPacketHeader(
|
||||
identifier = 0.toUShort(),
|
||||
queryResponse = QueryResponse.Query.value.toInt(),
|
||||
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||
authoritativeAnswer = false,
|
||||
truncated = false,
|
||||
recursionDesired = false,
|
||||
recursionAvailable = false,
|
||||
answerAuthenticated = false,
|
||||
nonAuthenticatedData = false,
|
||||
responseCode = DnsResponseCode.NoError
|
||||
),
|
||||
questionCount = 1,
|
||||
questionWriter = { w, _ ->
|
||||
w.write(DnsQuestion(
|
||||
name = "_airplay._tcp.local",
|
||||
type = QuestionType.PTR.value.toInt(),
|
||||
clazz = QuestionClass.IN.value.toInt(),
|
||||
queryUnicast = false
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
assertContentEquals(expectedBytes, writer.toByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BeyondTests`() {
|
||||
val data = byteArrayOf(
|
||||
0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03
|
||||
)
|
||||
|
||||
val reader = DnsReader(data)
|
||||
assertEquals(1, reader.readInt16())
|
||||
assertEquals(2, reader.readInt32())
|
||||
assertEquals(3L, reader.readInt64())
|
||||
assertEquals(1.toUShort(), reader.readUInt16())
|
||||
assertEquals(2.toUInt(), reader.readUInt32())
|
||||
assertEquals(3.toULong(), reader.readUInt64())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ParseDnsPrinter`() {
|
||||
val data = ubyteArrayOf(
|
||||
0x00u, 0x00u,
|
||||
0x84u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x06u, 0x04u, 0x5fu, 0x69u, 0x70u, 0x70u, 0x04u,
|
||||
0x5fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6cu, 0x6fu, 0x63u, 0x61u, 0x6cu, 0x00u, 0x00u, 0x0cu, 0x00u, 0x01u, 0x00u,
|
||||
0x00u, 0x11u, 0x94u, 0x00u, 0x1eu, 0x1bu, 0x42u, 0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u,
|
||||
0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u,
|
||||
0x73u, 0xc0u, 0x0cu, 0xc0u, 0x27u, 0x00u, 0x10u, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x02u, 0x53u, 0x09u,
|
||||
0x74u, 0x78u, 0x74u, 0x76u, 0x65u, 0x72u, 0x73u, 0x3du, 0x31u, 0x08u, 0x71u, 0x74u, 0x6fu, 0x74u, 0x61u, 0x6cu,
|
||||
0x3du, 0x31u, 0x42u, 0x70u, 0x64u, 0x6cu, 0x3du, 0x61u, 0x70u, 0x70u, 0x6cu, 0x69u, 0x63u, 0x61u, 0x74u, 0x69u,
|
||||
0x6fu, 0x6eu, 0x2fu, 0x6fu, 0x63u, 0x74u, 0x65u, 0x74u, 0x2du, 0x73u, 0x74u, 0x72u, 0x65u, 0x61u, 0x6du, 0x2cu,
|
||||
0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu, 0x75u, 0x72u, 0x66u, 0x2cu, 0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu,
|
||||
0x6au, 0x70u, 0x65u, 0x67u, 0x2cu, 0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu, 0x70u, 0x77u, 0x67u, 0x2du, 0x72u,
|
||||
0x61u, 0x73u, 0x74u, 0x65u, 0x72u, 0x0cu, 0x72u, 0x70u, 0x3du, 0x69u, 0x70u, 0x70u, 0x2fu, 0x70u, 0x72u, 0x69u,
|
||||
0x6eu, 0x74u, 0x05u, 0x6eu, 0x6fu, 0x74u, 0x65u, 0x3du, 0x1eu, 0x74u, 0x79u, 0x3du, 0x42u, 0x72u, 0x6fu, 0x74u,
|
||||
0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u,
|
||||
0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u, 0x73u, 0x25u, 0x70u, 0x72u, 0x6fu, 0x64u, 0x75u, 0x63u, 0x74u, 0x3du,
|
||||
0x28u, 0x42u, 0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u,
|
||||
0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u, 0x73u, 0x29u, 0x3cu, 0x61u, 0x64u,
|
||||
0x6du, 0x69u, 0x6eu, 0x75u, 0x72u, 0x6cu, 0x3du, 0x68u, 0x74u, 0x74u, 0x70u, 0x3au, 0x2fu, 0x2fu, 0x42u, 0x52u,
|
||||
0x57u, 0x31u, 0x30u, 0x35u, 0x42u, 0x41u, 0x44u, 0x34u, 0x41u, 0x31u, 0x35u, 0x37u, 0x30u, 0x2eu, 0x6cu, 0x6fu,
|
||||
0x63u, 0x61u, 0x6cu, 0x2eu, 0x2fu, 0x6eu, 0x65u, 0x74u, 0x2fu, 0x6eu, 0x65u, 0x74u, 0x2fu, 0x61u, 0x69u, 0x72u,
|
||||
0x70u, 0x72u, 0x69u, 0x6eu, 0x74u, 0x2eu, 0x68u, 0x74u, 0x6du, 0x6cu, 0x0bu, 0x70u, 0x72u, 0x69u, 0x6fu, 0x72u,
|
||||
0x69u, 0x74u, 0x79u, 0x3du, 0x32u, 0x35u, 0x0fu, 0x75u, 0x73u, 0x62u, 0x5fu, 0x4du, 0x46u, 0x47u, 0x3du, 0x42u,
|
||||
0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x1bu, 0x75u, 0x73u, 0x62u, 0x5fu, 0x4du, 0x44u, 0x4cu, 0x3du, 0x44u,
|
||||
0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u,
|
||||
0x65u, 0x73u, 0x19u, 0x75u, 0x73u, 0x62u, 0x5fu, 0x43u, 0x4du, 0x44u, 0x3du, 0x50u, 0x4au, 0x4cu, 0x2cu, 0x50u,
|
||||
0x43u, 0x4cu, 0x2cu, 0x50u, 0x43u, 0x4cu, 0x58u, 0x4cu, 0x2cu, 0x55u, 0x52u, 0x46u, 0x07u, 0x43u, 0x6fu, 0x6cu,
|
||||
0x6fu, 0x72u, 0x3du, 0x54u, 0x08u, 0x43u, 0x6fu, 0x70u, 0x69u, 0x65u, 0x73u, 0x3du, 0x54u, 0x08u, 0x44u, 0x75u,
|
||||
0x70u, 0x6cu, 0x65u, 0x78u, 0x3du, 0x54u, 0x05u, 0x46u, 0x61u, 0x78u, 0x3du, 0x46u, 0x06u, 0x53u, 0x63u, 0x61u,
|
||||
0x6eu, 0x3du, 0x54u, 0x0du, 0x50u, 0x61u, 0x70u, 0x65u, 0x72u, 0x43u, 0x75u, 0x73u, 0x74u, 0x6fu, 0x6du, 0x3du,
|
||||
0x54u, 0x08u, 0x42u, 0x69u, 0x6eu, 0x61u, 0x72u, 0x79u, 0x3du, 0x54u, 0x0du, 0x54u, 0x72u, 0x61u, 0x6eu, 0x73u,
|
||||
0x70u, 0x61u, 0x72u, 0x65u, 0x6eu, 0x74u, 0x3du, 0x54u, 0x06u, 0x54u, 0x42u, 0x43u, 0x50u, 0x3du, 0x46u, 0x3eu,
|
||||
0x55u, 0x52u, 0x46u, 0x3du, 0x53u, 0x52u, 0x47u, 0x42u, 0x32u, 0x34u, 0x2cu, 0x57u, 0x38u, 0x2cu, 0x43u, 0x50u,
|
||||
0x31u, 0x2cu, 0x49u, 0x53u, 0x34u, 0x2du, 0x31u, 0x2cu, 0x4du, 0x54u, 0x31u, 0x2du, 0x33u, 0x2du, 0x34u, 0x2du,
|
||||
0x35u, 0x2du, 0x38u, 0x2du, 0x31u, 0x31u, 0x2cu, 0x4fu, 0x42u, 0x31u, 0x30u, 0x2cu, 0x50u, 0x51u, 0x34u, 0x2cu,
|
||||
0x52u, 0x53u, 0x36u, 0x30u, 0x30u, 0x2cu, 0x56u, 0x31u, 0x2eu, 0x34u, 0x2cu, 0x44u, 0x4du, 0x31u, 0x25u, 0x6bu,
|
||||
0x69u, 0x6eu, 0x64u, 0x3du, 0x64u, 0x6fu, 0x63u, 0x75u, 0x6du, 0x65u, 0x6eu, 0x74u, 0x2cu, 0x65u, 0x6eu, 0x76u,
|
||||
0x65u, 0x6cu, 0x6fu, 0x70u, 0x65u, 0x2cu, 0x6cu, 0x61u, 0x62u, 0x65u, 0x6cu, 0x2cu, 0x70u, 0x6fu, 0x73u, 0x74u,
|
||||
0x63u, 0x61u, 0x72u, 0x64u, 0x11u, 0x50u, 0x61u, 0x70u, 0x65u, 0x72u, 0x4du, 0x61u, 0x78u, 0x3du, 0x6cu, 0x65u,
|
||||
0x67u, 0x61u, 0x6cu, 0x2du, 0x41u, 0x34u, 0x29u, 0x55u, 0x55u, 0x49u, 0x44u, 0x3du, 0x65u, 0x33u, 0x32u, 0x34u,
|
||||
0x38u, 0x30u, 0x30u, 0x30u, 0x2du, 0x38u, 0x30u, 0x63u, 0x65u, 0x2du, 0x31u, 0x31u, 0x64u, 0x62u, 0x2du, 0x38u,
|
||||
0x30u, 0x30u, 0x30u, 0x2du, 0x33u, 0x63u, 0x32u, 0x61u, 0x66u, 0x34u, 0x61u, 0x61u, 0x63u, 0x30u, 0x61u, 0x34u,
|
||||
0x0cu, 0x70u, 0x72u, 0x69u, 0x6eu, 0x74u, 0x5fu, 0x77u, 0x66u, 0x64u, 0x73u, 0x3du, 0x54u, 0x14u, 0x6du, 0x6fu,
|
||||
0x70u, 0x72u, 0x69u, 0x61u, 0x2du, 0x63u, 0x65u, 0x72u, 0x74u, 0x69u, 0x66u, 0x69u, 0x65u, 0x64u, 0x3du, 0x31u,
|
||||
0x2eu, 0x33u, 0x0fu, 0x42u, 0x52u, 0x57u, 0x31u, 0x30u, 0x35u, 0x42u, 0x41u, 0x44u, 0x34u, 0x41u, 0x31u, 0x35u,
|
||||
0x37u, 0x30u, 0xc0u, 0x16u, 0x00u, 0x01u, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x04u, 0xc0u, 0xa8u,
|
||||
0x01u, 0xc5u, 0xc2u, 0xa4u, 0x00u, 0x1cu, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x10u, 0xfeu, 0x80u,
|
||||
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x12u, 0x5bu, 0xadu, 0xffu, 0xfeu, 0x4au, 0x15u, 0x70u, 0xc0u, 0x27u,
|
||||
0x00u, 0x21u, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x08u, 0x00u, 0x00u, 0x00u, 0x00u, 0x02u, 0x77u,
|
||||
0xc2u, 0xa4u, 0xc0u, 0x27u, 0x00u, 0x2fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x09u, 0xc0u, 0x27u,
|
||||
0x00u, 0x05u, 0x00u, 0x00u, 0x80u, 0x00u, 0x40u, 0xc2u, 0xa4u, 0x00u, 0x2fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||
0x78u, 0x00u, 0x08u, 0xc2u, 0xa4u, 0x00u, 0x04u, 0x40u, 0x00u, 0x00u, 0x08u
|
||||
)
|
||||
|
||||
val packet = DnsPacket.parse(data.asByteArray())
|
||||
assertEquals(QueryResponse.Response.value.toInt(), packet.header.queryResponse)
|
||||
assertEquals(DnsOpcode.StandardQuery.value.toInt(), packet.header.opcode)
|
||||
assertTrue(packet.header.authoritativeAnswer)
|
||||
assertEquals(false, packet.header.truncated)
|
||||
assertEquals(false, packet.header.recursionDesired)
|
||||
assertEquals(false, packet.header.recursionAvailable)
|
||||
assertEquals(false, packet.header.answerAuthenticated)
|
||||
assertEquals(false, packet.header.nonAuthenticatedData)
|
||||
assertEquals(DnsResponseCode.NoError, packet.header.responseCode)
|
||||
assertEquals(0, packet.questions.size)
|
||||
assertEquals(1, packet.answers.size)
|
||||
assertEquals(0, packet.authorities.size)
|
||||
assertEquals(6, packet.additionals.size)
|
||||
|
||||
val firstAnswer = packet.answers[0]
|
||||
assertEquals("_ipp._tcp.local", firstAnswer.name)
|
||||
assertEquals(ResourceRecordType.PTR.value.toInt(), firstAnswer.type)
|
||||
assertEquals(ResourceRecordClass.IN.value.toInt(), firstAnswer.clazz)
|
||||
assertEquals(false, firstAnswer.cacheFlush)
|
||||
assertEquals(4500u, firstAnswer.timeToLive)
|
||||
assertEquals(30, firstAnswer.dataLength)
|
||||
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", firstAnswer.getDataReader().readPTRRecord().domainName)
|
||||
|
||||
val firstAdditional = packet.additionals[0]
|
||||
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", firstAdditional.name)
|
||||
assertEquals(ResourceRecordType.TXT.value.toInt(), firstAdditional.type)
|
||||
assertEquals(ResourceRecordClass.IN.value.toInt(), firstAdditional.clazz)
|
||||
assertEquals(true, firstAdditional.cacheFlush)
|
||||
assertEquals(4500u, firstAdditional.timeToLive)
|
||||
assertEquals(595, firstAdditional.dataLength)
|
||||
|
||||
val txtRecord = firstAdditional.getDataReader().readTXTRecord()
|
||||
assertContentEquals(arrayOf(
|
||||
"txtvers=1",
|
||||
"qtotal=1",
|
||||
"pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster",
|
||||
"rp=ipp/print",
|
||||
"note=",
|
||||
"ty=Brother DCP-L3550CDW series",
|
||||
"product=(Brother DCP-L3550CDW series)",
|
||||
"adminurl=http://BRW105BAD4A1570.local./net/net/airprint.html",
|
||||
"priority=25",
|
||||
"usb_MFG=Brother",
|
||||
"usb_MDL=DCP-L3550CDW series",
|
||||
"usb_CMD=PJL,PCL,PCLXL,URF",
|
||||
"Color=T",
|
||||
"Copies=T",
|
||||
"Duplex=T",
|
||||
"Fax=F",
|
||||
"Scan=T",
|
||||
"PaperCustom=T",
|
||||
"Binary=T",
|
||||
"Transparent=T",
|
||||
"TBCP=F",
|
||||
"URF=SRGB24,W8,CP1,IS4-1,MT1-3-4-5-8-11,OB10,PQ4,RS600,V1.4,DM1",
|
||||
"kind=document,envelope,label,postcard",
|
||||
"PaperMax=legal-A4",
|
||||
"UUID=e3248000-80ce-11db-8000-3c2af4aac0a4",
|
||||
"print_wfds=T",
|
||||
"mopria-certified=1.3"
|
||||
), txtRecord.texts.toTypedArray())
|
||||
|
||||
val aRecord = packet.additionals[1].getDataReader().readARecord()
|
||||
assertEquals(InetAddress.getByName("192.168.1.197"), aRecord.address)
|
||||
|
||||
val aaaaRecord = packet.additionals[2].getDataReader().readAAAARecord()
|
||||
assertEquals(InetAddress.getByName("fe80::125b:adff:fe4a:1570"), aaaaRecord.address)
|
||||
|
||||
val srvRecord = packet.additionals[3].getDataReader().readSRVRecord()
|
||||
assertEquals("BRW105BAD4A1570.local", srvRecord.target)
|
||||
assertEquals(0, srvRecord.weight.toInt())
|
||||
assertEquals(0, srvRecord.priority.toInt())
|
||||
assertEquals(631, srvRecord.port.toInt())
|
||||
|
||||
val nSECRecord = packet.additionals[4].getDataReader().readNSECRecord()
|
||||
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", nSECRecord.ownerName)
|
||||
assertEquals(1, nSECRecord.typeBitMaps.size)
|
||||
assertEquals(0, nSECRecord.typeBitMaps[0].first)
|
||||
assertContentEquals(byteArrayOf(0, 0, 128.toByte(), 0, 64), nSECRecord.typeBitMaps[0].second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ParseSamsungTV`() {
|
||||
val data = loadByteArray("samsung-airplay.hex")
|
||||
val packet = DnsPacket.parse(data)
|
||||
assertEquals(QueryResponse.Response.value.toInt(), packet.header.queryResponse)
|
||||
assertEquals(DnsOpcode.StandardQuery.value.toInt(), packet.header.opcode)
|
||||
assertTrue(packet.header.authoritativeAnswer)
|
||||
assertEquals(false, packet.header.truncated)
|
||||
assertEquals(false, packet.header.recursionDesired)
|
||||
assertEquals(false, packet.header.recursionAvailable)
|
||||
assertEquals(false, packet.header.answerAuthenticated)
|
||||
assertEquals(false, packet.header.nonAuthenticatedData)
|
||||
assertEquals(DnsResponseCode.NoError, packet.header.responseCode)
|
||||
assertEquals(0, packet.questions.size)
|
||||
assertEquals(6, packet.answers.size)
|
||||
assertEquals(0, packet.authorities.size)
|
||||
assertEquals(4, packet.additionals.size)
|
||||
|
||||
assertEquals("9.1.168.192.in-addr.arpa", packet.answers[0].name)
|
||||
assertEquals(ResourceRecordType.PTR.value.toInt(), packet.answers[0].type)
|
||||
assertEquals(ResourceRecordClass.IN.value.toInt(), packet.answers[0].clazz)
|
||||
assertTrue(packet.answers[0].cacheFlush)
|
||||
assertEquals(120u, packet.answers[0].timeToLive)
|
||||
assertEquals(15, packet.answers[0].dataLength)
|
||||
assertEquals("Samsung.local", packet.answers[0].getDataReader().readPTRRecord().domainName)
|
||||
|
||||
val txtRecord = packet.answers[1].getDataReader().readTXTRecord()
|
||||
assertContentEquals(arrayOf(
|
||||
"acl=0",
|
||||
"deviceid=D4:9D:C0:2F:52:16",
|
||||
"features=0x7F8AD0,0x38BCB46",
|
||||
"rsf=0x3",
|
||||
"fv=p20.0.1",
|
||||
"flags=0x244",
|
||||
"model=URU8000",
|
||||
"manufacturer=Samsung",
|
||||
"serialNumber=0EQC3HDM900064X",
|
||||
"protovers=1.1",
|
||||
"srcvers=377.17.24.6",
|
||||
"pi=ED:0C:A5:ED:10:08",
|
||||
"psi=00000000-0000-0000-0000-ED0CA5ED1008",
|
||||
"gid=00000000-0000-0000-0000-ED0CA5ED1008",
|
||||
"gcgl=0",
|
||||
"pk=d25488cbff1334756165cd7229a235475ef591f2595f38ed251d46b8a4d2345d"
|
||||
), txtRecord.texts.toTypedArray())
|
||||
|
||||
val srvRecord = packet.answers[4].getDataReader().readSRVRecord()
|
||||
assertEquals(33482, srvRecord.port.toInt())
|
||||
assertEquals(0, srvRecord.priority.toInt())
|
||||
assertEquals(0, srvRecord.weight.toInt())
|
||||
assertEquals("Samsung.local", srvRecord.target)
|
||||
|
||||
val aRecord = packet.answers[5].getDataReader().readARecord()
|
||||
assertEquals(InetAddress.getByName("192.168.1.9"), aRecord.address)
|
||||
|
||||
val nSECRecord = packet.additionals[0].getDataReader().readNSECRecord()
|
||||
assertEquals("9.1.168.192.in-addr.arpa", nSECRecord.ownerName)
|
||||
assertEquals(1, nSECRecord.typeBitMaps.size)
|
||||
assertEquals(0, nSECRecord.typeBitMaps[0].first)
|
||||
assertContentEquals(byteArrayOf(0, 8), nSECRecord.typeBitMaps[0].second)
|
||||
|
||||
val optRecord = packet.additionals[3].getDataReader().readOPTRecord()
|
||||
assertEquals(1, optRecord.options.size)
|
||||
assertEquals(65001, optRecord.options[0].code.toInt())
|
||||
assertEquals(5, optRecord.options[0].data.size)
|
||||
assertContentEquals(byteArrayOf(0, 0, 116, 206.toByte(), 97), optRecord.options[0].data)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UnicodeTest`() {
|
||||
val data = ubyteArrayOf(
|
||||
0x00u, 0x00u, 0x84u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x01u, 0x15u, 0x41u, 0x69u, 0x64u,
|
||||
0x61u, 0x6Eu, 0xE2u, 0x80u, 0x99u, 0x73u, 0x20u, 0x4Du, 0x61u, 0x63u, 0x42u, 0x6Fu, 0x6Fu, 0x6Bu, 0x20u, 0x50u,
|
||||
0x72u, 0x6Fu, 0x0Fu, 0x5Fu, 0x63u, 0x6Fu, 0x6Du, 0x70u, 0x61u, 0x6Eu, 0x69u, 0x6Fu, 0x6Eu, 0x2Du, 0x6Cu, 0x69u,
|
||||
0x6Eu, 0x6Bu, 0x04u, 0x5Fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6Cu, 0x6Fu, 0x63u, 0x61u, 0x6Cu, 0x00u, 0x00u, 0x10u,
|
||||
0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x5Bu, 0x16u, 0x72u, 0x70u, 0x42u, 0x41u, 0x3Du, 0x30u, 0x33u,
|
||||
0x3Au, 0x43u, 0x32u, 0x3Au, 0x33u, 0x33u, 0x3Au, 0x38u, 0x36u, 0x3Au, 0x33u, 0x43u, 0x3Au, 0x45u, 0x45u, 0x11u,
|
||||
0x72u, 0x70u, 0x41u, 0x44u, 0x3Du, 0x66u, 0x33u, 0x33u, 0x37u, 0x61u, 0x38u, 0x61u, 0x32u, 0x38u, 0x64u, 0x35u,
|
||||
0x31u, 0x0Cu, 0x72u, 0x70u, 0x46u, 0x6Cu, 0x3Du, 0x30u, 0x78u, 0x32u, 0x30u, 0x30u, 0x30u, 0x30u, 0x11u, 0x72u,
|
||||
0x70u, 0x48u, 0x4Eu, 0x3Du, 0x31u, 0x66u, 0x66u, 0x64u, 0x64u, 0x64u, 0x66u, 0x33u, 0x63u, 0x39u, 0x65u, 0x33u,
|
||||
0x07u, 0x72u, 0x70u, 0x4Du, 0x61u, 0x63u, 0x3Du, 0x30u, 0x0Au, 0x72u, 0x70u, 0x56u, 0x72u, 0x3Du, 0x33u, 0x36u,
|
||||
0x30u, 0x2Eu, 0x34u, 0xC0u, 0x0Cu, 0x00u, 0x2Fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x09u, 0xC0u,
|
||||
0x0Cu, 0x00u, 0x05u, 0x00u, 0x00u, 0x80u, 0x00u, 0x40u
|
||||
)
|
||||
|
||||
val packet = DnsPacket.parse(data.asByteArray())
|
||||
assertEquals("Aidan’s MacBook Pro._companion-link._tcp.local", packet.additionals[0].name)
|
||||
}
|
||||
|
||||
/*@Test
|
||||
fun `TestReadDomainName`() {
|
||||
val data = ubyteArrayOf(
|
||||
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x0Bu, 0x5Fu, 0x67u, 0x6Fu,
|
||||
0x6Fu, 0x67u, 0x6Cu, 0x65u, 0x63u, 0x61u, 0x73u, 0x74u, 0x04u, 0x5Fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6Cu, 0x6Fu,
|
||||
0x63u, 0x61u, 0x6Cu, 0xC0u, 0x0Cu, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x08u, 0x5Fu, 0x61u, 0x69u, 0x72u, 0x70u, 0x6Cu,
|
||||
0x61u, 0x79u, 0xC0u, 0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x09u, 0x5Fu, 0x66u, 0x61u, 0x73u, 0x74u, 0x63u, 0x61u,
|
||||
0x73u, 0x74u, 0xC0u, 0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x06u, 0x5Fu, 0x66u, 0x63u, 0x61u, 0x73u, 0x74u, 0xC0u,
|
||||
0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u
|
||||
)
|
||||
|
||||
val packet = DnsPacket.parse(data.asByteArray())
|
||||
println()
|
||||
}*/
|
||||
|
||||
private fun loadByteArray(name: String): ByteArray {
|
||||
javaClass.classLoader.getResourceAsStream(name).use { input ->
|
||||
requireNotNull(input) { "File not found: $name" }
|
||||
val result = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(4096)
|
||||
var length: Int
|
||||
|
||||
while ((input.read(buffer).also { length = it }) > 0) {
|
||||
result.write(buffer, 0, length)
|
||||
}
|
||||
return result.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReserializeDnsPrinter`() {
|
||||
val data = loadByteArray("samsung-airplay.hex")
|
||||
val packet = DnsPacket.parse(data)
|
||||
val writer = DnsWriter()
|
||||
writer.writePacket(
|
||||
header = packet.header,
|
||||
questionCount = packet.questions.size,
|
||||
questionWriter = { _, _ -> },
|
||||
answerCount = packet.answers.size,
|
||||
answerWriter = { w, i ->
|
||||
w.write(packet.answers[i]) { v ->
|
||||
val reader = packet.answers[i].getDataReader()
|
||||
when (i) {
|
||||
0, 2, 3 -> v.write(reader.readPTRRecord())
|
||||
1 -> v.write(reader.readTXTRecord())
|
||||
4 -> v.write(reader.readSRVRecord())
|
||||
5 -> v.write(reader.readARecord())
|
||||
}
|
||||
}
|
||||
},
|
||||
authorityCount = packet.authorities.size,
|
||||
authorityWriter = { _, _ -> },
|
||||
additionalsCount = packet.additionals.size,
|
||||
additionalWriter = { w, i ->
|
||||
w.write(packet.additionals[i]) { v ->
|
||||
val reader = packet.additionals[i].getDataReader()
|
||||
when (i) {
|
||||
0, 1, 2 -> v.write(reader.readNSECRecord())
|
||||
3 -> v.write(reader.readOPTRecord())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assertContentEquals(data, writer.toByteArray())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user