mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 02:33:03 +02:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a | |||
| af6d219936 | |||
| 82a07e2e09 | |||
| 12a9b99fff | |||
| 3adf761158 | |||
| 670a4c61ff | |||
| 220f50d3bb | |||
| e0bf9d2a7c | |||
| f61cf46a52 | |||
| d188128d27 | |||
| f698c4120d | |||
| 338a852d49 | |||
| a64ee2242c | |||
| e9ff5e6f0b | |||
| f3911d8b68 | |||
| 9ce0be6450 | |||
| 6ab3eff61c | |||
| 0281da1c5a | |||
| 0b4770188c | |||
| 9376bb05fa | |||
| ecca3b6793 | |||
| f41a971cd8 | |||
| 44ba66d619 | |||
| bf685a607f | |||
| 5713cf0508 | |||
| bdd50d70ca | |||
| 8188399ce6 | |||
| f72b7dbbbb | |||
| 2409afcc5c | |||
| 15c0d02c13 | |||
| a54a5081e6 | |||
| db9dfcf049 | |||
| 47f9948748 | |||
| 05e866df55 | |||
| fc431f0cb8 | |||
| 228ab359ed | |||
| 103a8587f7 | |||
| 7db0083928 | |||
| e6f6ab499a | |||
| 721b7dbba0 | |||
| a95ddab814 | |||
| 2941546ae4 | |||
| bd9b9179c1 | |||
| ce7d54c151 | |||
| 3c778c07c2 | |||
| 95207341db | |||
| 70cf24924d | |||
| a8ebba691e | |||
| ec19ea44ad | |||
| ca8dc0f0f5 | |||
| 1dc50a697c | |||
| 1167c314ee | |||
| 55781e2b34 | |||
| 7439e44e44 | |||
| cf2639df3d | |||
| 834de928c2 | |||
| 72efb21439 | |||
| aa8790ebdb | |||
| 6d491052ee | |||
| 87ff4691ce | |||
| 34d76e79ed | |||
| 31b43da96f | |||
| 0540e673e2 | |||
| 4e88a63809 | |||
| f7581f8a65 | |||
| e87a1c079c | |||
| 3f9477c246 | |||
| 05ed1e188e | |||
| f3d06e49f8 | |||
| f9a4b68967 | |||
| 3631cfe365 | |||
| 8766ae176e | |||
| 36b53d490f |
+12
@@ -70,3 +70,15 @@
|
|||||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
[submodule "app/src/unstable/assets/sources/spotify"]
|
||||||
path = app/src/unstable/assets/sources/spotify
|
path = app/src/unstable/assets/sources/spotify
|
||||||
url = ../plugins/spotify.git
|
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
|
||||||
|
|||||||
+11
-2
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
@@ -144,9 +144,19 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
assets {
|
||||||
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.google.dagger:dagger:2.48'
|
||||||
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
@@ -184,7 +194,6 @@ dependencies {
|
|||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jmdns:jmdns:3.5.1'
|
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<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="com.android.alarm.permission.SET_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_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_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
@@ -152,27 +153,27 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -186,44 +187,44 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -406,6 +406,39 @@ class DashSource {
|
|||||||
this.requestModifier = obj.requestModifier;
|
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 {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
@@ -762,3 +795,99 @@ class URLSearchParams {
|
|||||||
return searchString;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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,7 +18,10 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||||
val requestModifier = getRequestModifier();
|
val requestModifier = getRequestModifier();
|
||||||
return if (requestModifier != null) {
|
val requestExecutor = getRequestExecutor();
|
||||||
|
return if (requestExecutor != null) {
|
||||||
|
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
|
||||||
|
} else if (requestModifier != null) {
|
||||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||||
} else {
|
} else {
|
||||||
DefaultHttpDataSource.Factory();
|
DefaultHttpDataSource.Factory();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,9 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@@ -25,4 +28,18 @@ fun String?.yesNoToBoolean(): Boolean {
|
|||||||
|
|
||||||
fun Boolean?.toYesNo(): String {
|
fun Boolean?.toYesNo(): String {
|
||||||
return if (this == true) "YES" else "NO"
|
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,8 +2,11 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
@@ -23,6 +26,7 @@ import com.futo.platformplayer.states.StateBackup
|
|||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -34,6 +38,7 @@ import com.futo.platformplayer.views.fields.FormField
|
|||||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -44,6 +49,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
|
|
||||||
@@ -57,7 +63,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -73,7 +79,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
||||||
@FormFieldButton(R.drawable.ic_quiz)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +89,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
||||||
@FormFieldButton(R.drawable.ic_data_alert)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +121,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
@@ -129,7 +135,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
@@ -138,7 +144,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
@@ -148,6 +154,24 @@ 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)
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
var language = LanguageSettings();
|
var language = LanguageSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -326,7 +350,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
@@ -353,7 +377,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
var defaultPlaybackSpeed: Int = 3;
|
var defaultPlaybackSpeed: Int = 3;
|
||||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||||
@@ -369,35 +393,31 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredMeteredQuality: Int = 0;
|
var preferredMeteredQuality: Int = 0;
|
||||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||||
|
|
||||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
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)
|
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||||
var autoRotate: Int = 2;
|
var autoRotate: Int = 2;
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
|
||||||
|
|
||||||
@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)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
@@ -450,6 +470,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||||
var fullscreenPortrait: Boolean = false;
|
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)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -525,6 +555,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var keepScreenOn: Boolean = true;
|
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?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
@@ -775,10 +809,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun export() {
|
fun export() {
|
||||||
val activity = SettingsActivity.getActivity() ?: return;
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
||||||
StateBackup.saveExternalBackup(activity);
|
StateBackup.saveExternalBackup(activity);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.work.WorkManager
|
|||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.futo.platformplayer.activities.DeveloperActivity
|
import com.futo.platformplayer.activities.DeveloperActivity
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -491,6 +492,13 @@ 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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,14 +15,18 @@ 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.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
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.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.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
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.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.streams.sources.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
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.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
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.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.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
@@ -34,12 +38,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
|||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
@@ -91,9 +95,17 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
container.context,
|
||||||
}, false),
|
R.drawable.ic_notifications,
|
||||||
|
"Notifications",
|
||||||
|
"",
|
||||||
|
tag = "notifications",
|
||||||
|
call = {
|
||||||
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
),
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
"You can select which groups this subscription is part of.",
|
"You can select which groups this subscription is part of.",
|
||||||
@@ -128,22 +140,62 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
container.context,
|
||||||
}, false) else null,
|
R.drawable.ic_live_tv,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
"Livestreams",
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
"Check for livestreams",
|
||||||
}, false) else null,
|
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_VIDEOS))
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
SlideUpMenuItem(
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
container.context,
|
||||||
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
R.drawable.ic_play,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
"Videos",
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
"Check for videos",
|
||||||
}, false) else null,
|
tag = "fetchVideos",
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
call = {
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
}, false) else null/*,,
|
},
|
||||||
|
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/*,,
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Actions",
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
"Various things you can do with this subscription",
|
"Various things you can do with this subscription",
|
||||||
@@ -242,11 +294,23 @@ class UISlideOverlays {
|
|||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
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
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
audioButtons.add(SlideUpMenuItem(
|
||||||
}, false))
|
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
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
@@ -258,11 +322,22 @@ class UISlideOverlays {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
masterPlaylist.getVideoSources().forEach {
|
masterPlaylist.getVideoSources().forEach {
|
||||||
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideoVariant = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
videoButtons.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
container.context,
|
||||||
}, false))
|
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
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
@@ -321,8 +396,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
|
|
||||||
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
||||||
var selectedVideo: IVideoUrlSource? = null;
|
var selectedVideo: IVideoSource? = null;
|
||||||
var selectedAudio: IAudioUrlSource? = null;
|
var selectedAudio: IAudioSource? = null;
|
||||||
var selectedSubtitle: ISubtitleSource? = null;
|
var selectedSubtitle: ISubtitleSource? = null;
|
||||||
|
|
||||||
val videoSources = descriptor.videoSources;
|
val videoSources = descriptor.videoSources;
|
||||||
@@ -341,45 +416,93 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
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), "none", {
|
listOf(listOf(SlideUpMenuItem(
|
||||||
selectedVideo = null;
|
container.context,
|
||||||
menu?.selectOption(videoSources, "none");
|
R.drawable.ic_movie,
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context.getString(R.string.none),
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.audio_only),
|
||||||
}, false)) +
|
tag = "none",
|
||||||
|
call = {
|
||||||
|
selectedVideo = null;
|
||||||
|
menu?.selectOption(videoSources, "none");
|
||||||
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IVideoUrlSource -> {
|
is IVideoUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideo = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(videoSources, it);
|
SlideUpMenuItem(
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context,
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
R.drawable.ic_movie,
|
||||||
}, false)
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestSource -> {
|
is IHLSManifestSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).flatten().toList()
|
}.filterNotNull()).flatten().toList()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
) as IVideoUrlSource?;
|
) as IVideoSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSources != null) {
|
if (audioSources != null) {
|
||||||
@@ -388,43 +511,90 @@ class UISlideOverlays {
|
|||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IAudioUrlSource -> {
|
is IAudioUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedAudio = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(audioSources, it);
|
SlideUpMenuItem(
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context,
|
||||||
}, false);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestAudioSource -> {
|
is IHLSManifestAudioSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS Audio",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}.filterNotNull()));
|
||||||
|
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
SlideUpMenuItem(
|
||||||
if (selectedSubtitle == it) {
|
container.context,
|
||||||
selectedSubtitle = null;
|
R.drawable.ic_edit,
|
||||||
menu?.selectOption(subtitleSources, null);
|
it.name,
|
||||||
} else {
|
"",
|
||||||
selectedSubtitle = it;
|
tag = it,
|
||||||
menu?.selectOption(subtitleSources, it);
|
call = {
|
||||||
}
|
if (selectedSubtitle == it) {
|
||||||
}, false);
|
selectedSubtitle = null;
|
||||||
|
menu?.selectOption(subtitleSources, null);
|
||||||
|
} else {
|
||||||
|
selectedSubtitle = it;
|
||||||
|
menu?.selectOption(subtitleSources, it);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -442,6 +612,18 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
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();
|
menu.hide();
|
||||||
val subtitleToDownload = selectedSubtitle;
|
val subtitleToDownload = selectedSubtitle;
|
||||||
if(selectedAudio != null || !requiresAudio) {
|
if(selectedAudio != null || !requiresAudio) {
|
||||||
@@ -498,8 +680,9 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
@@ -536,23 +719,47 @@ class UISlideOverlays {
|
|||||||
);
|
);
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
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, it.third, {
|
SlideUpMenuItem(
|
||||||
targetPxSize = it.third;
|
container.context,
|
||||||
menu?.selectOption("Video", it.third);
|
R.drawable.ic_movie,
|
||||||
}, false)
|
it.first,
|
||||||
|
it.second,
|
||||||
|
tag = it.third,
|
||||||
|
call = {
|
||||||
|
targetPxSize = it.third;
|
||||||
|
menu?.selectOption("Video", it.third);
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
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), "", 1, {
|
SlideUpMenuItem(
|
||||||
targetBitrate = 1;
|
container.context,
|
||||||
menu?.selectOption("Bitrate", 1);
|
R.drawable.ic_movie,
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.low_bitrate),
|
||||||
}, false),
|
"",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
tag = 1,
|
||||||
targetBitrate = 9999999;
|
call = {
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
targetBitrate = 1;
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.selectOption("Bitrate", 1);
|
||||||
}, false)
|
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
|
||||||
|
)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
|
||||||
@@ -675,8 +882,12 @@ class UISlideOverlays {
|
|||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
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), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -688,42 +899,90 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
SlideUpMenuItem(
|
||||||
showDownloadVideoOverlay(video, container, true);
|
container.context,
|
||||||
}, false),
|
R.drawable.ic_download,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
container.context.getString(R.string.download),
|
||||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
container.context.getString(R.string.download_the_video),
|
||||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
tag = "download",
|
||||||
action = Intent.ACTION_SEND;
|
call = {
|
||||||
putExtra(Intent.EXTRA_TEXT, url);
|
showDownloadVideoOverlay(video, container, true);
|
||||||
type = "text/plain";
|
},
|
||||||
}, null));
|
invokeParent = false
|
||||||
}, false),
|
),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
SlideUpMenuItem(
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
container.context,
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
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");
|
||||||
|
}))
|
||||||
+ actions)
|
+ actions)
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
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), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
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",
|
container.context.getString(R.string.add_to_queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
"${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); }),
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
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), "add_to_new_playlist", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
};
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
call = {
|
||||||
|
showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
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), "",
|
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 = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -745,8 +1004,12 @@ class UISlideOverlays {
|
|||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
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), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -758,25 +1021,52 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
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), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
container.context.getString(R.string.queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
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),
|
tag = "queue",
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
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
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
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), "add_to_new_playlist", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
});
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
call = {
|
||||||
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
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), "",
|
playlistItems.add(SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
playlist.name,
|
||||||
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -801,20 +1091,36 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
val views = arrayOf(
|
val views = arrayOf(
|
||||||
hidden
|
hidden
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
.map { btn -> SlideUpMenuItem(
|
||||||
btn.handler?.invoke(btn);
|
container.context,
|
||||||
}, invokeParents) as View }.toTypedArray(),
|
btn.iconResource,
|
||||||
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), "", {
|
btn.text.text.toString(),
|
||||||
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
|
tag = "",
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
call = {
|
||||||
.filter { it != null }
|
btn.handler?.invoke(btn);
|
||||||
.map { it!! }
|
},
|
||||||
.toList();
|
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();
|
||||||
|
|
||||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||||
}
|
}
|
||||||
}, false))
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
).flatten().toTypedArray();
|
).flatten().toTypedArray();
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
@@ -826,14 +1132,21 @@ class UISlideOverlays {
|
|||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
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, "", it.second, {
|
options.map { SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_move_up,
|
||||||
|
it.first,
|
||||||
|
"",
|
||||||
|
tag = it.second,
|
||||||
|
call = {
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second))
|
if(!selection.contains(it.second))
|
||||||
selection.add(it.second);
|
selection.add(it.second);
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
selection.remove(it.second);
|
selection.remove(it.second);
|
||||||
}, false)
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
});
|
});
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.os.OperationCanceledException
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
@@ -146,6 +147,8 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setNavigationBarColorAndIcons() {
|
fun Activity.setNavigationBarColorAndIcons() {
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -20,30 +22,61 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
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.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
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.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.*
|
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.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
@@ -51,15 +84,22 @@ import com.futo.platformplayer.views.ToastView
|
|||||||
import com.futo.polycentric.core.ApiMethods
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
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.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
@@ -79,6 +119,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private lateinit var _fragContainerVideoDetail: FragmentContainerView;
|
private lateinit var _fragContainerVideoDetail: FragmentContainerView;
|
||||||
private lateinit var _fragContainerOverlay: FrameLayout;
|
private lateinit var _fragContainerOverlay: FrameLayout;
|
||||||
|
|
||||||
|
//Views
|
||||||
|
private lateinit var _buttonIncognito: ImageView;
|
||||||
|
|
||||||
//Frags TopBar
|
//Frags TopBar
|
||||||
lateinit var _fragTopBarGeneral: GeneralTopBarFragment;
|
lateinit var _fragTopBarGeneral: GeneralTopBarFragment;
|
||||||
lateinit var _fragTopBarSearch: SearchTopBarFragment;
|
lateinit var _fragTopBarSearch: SearchTopBarFragment;
|
||||||
@@ -129,9 +172,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
val onNavigated = Event1<MainFragment>();
|
val onNavigated = Event1<MainFragment>();
|
||||||
|
|
||||||
private lateinit var _orientationManager: OrientationManager;
|
|
||||||
var orientation: OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
|
|
||||||
private set;
|
|
||||||
private var _isVisible = true;
|
private var _isVisible = true;
|
||||||
private var _wasStopped = false;
|
private var _wasStopped = false;
|
||||||
|
|
||||||
@@ -156,6 +196,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
StrictMode.setVmPolicy(
|
||||||
|
VmPolicy.Builder()
|
||||||
|
.detectLeakedClosableObjects()
|
||||||
|
.penaltyLog()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||||
@@ -204,6 +253,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
}
|
}
|
||||||
@@ -290,6 +340,52 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
updateSegmentPaddings();
|
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 {
|
StatePlayer.instance.also {
|
||||||
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
||||||
if (!shouldSwapCurrentItem) {
|
if (!shouldSwapCurrentItem) {
|
||||||
@@ -364,26 +460,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
.commitNow();
|
.commitNow();
|
||||||
|
|
||||||
defaultTab.action(_fragBotBarMenu);
|
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;
|
StateSubscriptions.instance;
|
||||||
|
|
||||||
fragCurrent.onShown(null, false);
|
fragCurrent.onShown(null, false);
|
||||||
@@ -480,17 +556,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.v(TAG, "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;
|
_isVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +603,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
UIDialogs.showImportOptionsDialog(this);
|
UIDialogs.showImportOptionsDialog(this);
|
||||||
}
|
}
|
||||||
|
"ACTION" -> {
|
||||||
|
val action = intent.getStringExtra("ACTION");
|
||||||
|
StateDeveloper.instance.testState = "TestPlayback";
|
||||||
|
StateDeveloper.instance.testPlayback();
|
||||||
|
}
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when(intent.getStringExtra("TAB")){
|
when(intent.getStringExtra("TAB")){
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
@@ -886,18 +956,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
override fun onRestart() {
|
override fun onRestart() {
|
||||||
super.onRestart();
|
super.onRestart();
|
||||||
Logger.i(TAG, "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) {
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
@@ -912,9 +970,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Logger.v(TAG, "onDestroy")
|
Logger.v(TAG, "onDestroy")
|
||||||
|
|
||||||
_orientationManager.disable();
|
|
||||||
|
|
||||||
StateApp.instance.mainAppDestroyed(this);
|
StateApp.instance.mainAppDestroyed(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1180,6 +1235,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
return sourcesIntent;
|
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 {
|
fun getImportOptionsIntent(context: Context): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
@@ -184,12 +185,19 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
resultLauncher.launch(intent);
|
resultLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
settingsActivityClosed.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private var _lastActivity: SettingsActivity? = null;
|
private var _lastActivity: SettingsActivity? = null;
|
||||||
|
|
||||||
|
val settingsActivityClosed = Event0()
|
||||||
|
|
||||||
fun getActivity(): SettingsActivity? {
|
fun getActivity(): SettingsActivity? {
|
||||||
val act = _lastActivity;
|
val act = _lastActivity;
|
||||||
if(act != null && !act._isFinished)
|
if(act != null && !act._isFinished)
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ import okhttp3.WebSocket
|
|||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import java.time.Duration
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
protected val _builderTemplate: OkHttpClient.Builder;
|
protected var _builderTemplate: OkHttpClient.Builder;
|
||||||
|
|
||||||
private var client: OkHttpClient;
|
private var client: OkHttpClient;
|
||||||
|
|
||||||
@@ -32,6 +33,15 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
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>(
|
private val trustAllCerts = arrayOf<TrustManager>(
|
||||||
object: X509TrustManager {
|
object: X509TrustManager {
|
||||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
||||||
@@ -62,6 +72,15 @@ open class ManagedHttpClient {
|
|||||||
}.build();
|
}.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 {
|
open fun clone(): ManagedHttpClient {
|
||||||
val clonedClient = ManagedHttpClient(_builderTemplate);
|
val clonedClient = ManagedHttpClient(_builderTemplate);
|
||||||
clonedClient.user_agent = user_agent;
|
clonedClient.user_agent = user_agent;
|
||||||
|
|||||||
@@ -210,6 +210,20 @@ 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) {
|
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
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.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
for(getMethod in getMethods)
|
for(getMethod in getMethods)
|
||||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
if(!getMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(getMethod.second.contentType);
|
this.withContentType(getMethod.second.contentType);
|
||||||
}.withContentType(getMethod.second.contentType);
|
}.withContentType(getMethod.second.contentType);
|
||||||
for(postMethod in postMethods)
|
for(postMethod in postMethods)
|
||||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!postMethod.second.contentType.isEmpty())
|
if(!postMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(postMethod.second.contentType);
|
this.withContentType(postMethod.second.contentType);
|
||||||
}.withContentType(postMethod.second.contentType);
|
}.withContentType(postMethod.second.contentType);
|
||||||
|
|
||||||
for(getField in getFields) {
|
for(getField in getFields) {
|
||||||
getField.first.isAccessible = true;
|
getField.first.isAccessible = true;
|
||||||
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
||||||
val value = getField.first.get(obj) as String?;
|
val value = getField.first.get(obj) as String?;
|
||||||
if(value != null) {
|
if(value != null) {
|
||||||
val headers = HttpHeaders(
|
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
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
|
||||||
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
httpContext.setResponseHeaders(this.headers);
|
httpContext.setResponseHeaders(this.headers);
|
||||||
handler(httpContext);
|
handler(httpContext);
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ class PlatformClientPool {
|
|||||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||||
private var _poolCounter = 0;
|
private var _poolCounter = 0;
|
||||||
private val _poolName: String?;
|
private val _poolName: String?;
|
||||||
|
private val _privatePool: Boolean;
|
||||||
|
|
||||||
var isDead: Boolean = false
|
var isDead: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||||
|
|
||||||
constructor(parentClient: IPlatformClient, name: String? = null) {
|
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
||||||
_poolName = name;
|
_poolName = name;
|
||||||
|
_privatePool = privatePool;
|
||||||
if(parentClient !is JSClient)
|
if(parentClient !is JSClient)
|
||||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||||
@@ -51,7 +53,7 @@ class PlatformClientPool {
|
|||||||
reserved = _pool.keys.find { !it.isBusy };
|
reserved = _pool.keys.find { !it.isBusy };
|
||||||
if(reserved == null && _pool.size < capacity) {
|
if(reserved == null && _pool.size < capacity) {
|
||||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||||
reserved = _parent.getCopy();
|
reserved = _parent.getCopy(_privatePool);
|
||||||
|
|
||||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
StateApp.instance.handleCaptchaException(client, ex);
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ class PlatformMultiClientPool {
|
|||||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||||
|
|
||||||
private var _isFake = false;
|
private var _isFake = false;
|
||||||
|
private var _privatePool = false;
|
||||||
|
|
||||||
constructor(name: String, maxCap: Int = -1) {
|
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
||||||
_name = name;
|
_name = name;
|
||||||
_maxCap = if(maxCap > 0)
|
_maxCap = if(maxCap > 0)
|
||||||
maxCap
|
maxCap
|
||||||
else 99;
|
else 99;
|
||||||
|
_privatePool = isPrivatePool;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||||
@@ -19,7 +21,7 @@ class PlatformMultiClientPool {
|
|||||||
return parentClient;
|
return parentClient;
|
||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
||||||
this.onDead.subscribe { _, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ChapterType
|
fun fromInt(value: Int): ChapterType
|
||||||
{
|
{
|
||||||
val result = ChapterType.values().firstOrNull { it.value == value };
|
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum class ContentType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ContentType
|
fun fromInt(value: Int): ContentType
|
||||||
{
|
{
|
||||||
val result = ContentType.values().firstOrNull { it.value == value };
|
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : LiveEventType{
|
fun fromInt(value : Int) : LiveEventType{
|
||||||
return LiveEventType.values().first { it.value == value };
|
return LiveEventType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
{
|
{
|
||||||
val result = TextType.values().firstOrNull { it.value == value };
|
val result = TextType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : RatingType{
|
fun fromInt(value : Int) : RatingType{
|
||||||
return RatingType.values().first { it.value == value };
|
return RatingType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ class DevJSClient : JSClient {
|
|||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(): JSClient {
|
override fun getCopy(privateCopy: Boolean): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -164,13 +164,16 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
if(!withoutCredentials)
|
||||||
|
_auth = descriptor.getAuth();
|
||||||
|
else
|
||||||
|
_auth = null;
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
@@ -190,8 +193,8 @@ open class JSClient : IPlatformClient {
|
|||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
||||||
return JSClient(_context, descriptor, saveState(), _script);
|
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
class JSClientConstants {
|
||||||
|
companion object {
|
||||||
|
val PLUGIN_SPEC_VERSION = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -4,7 +4,9 @@ import android.net.Uri
|
|||||||
import com.futo.platformplayer.SignatureProvider
|
import com.futo.platformplayer.SignatureProvider
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.matchesDomain
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ class SourcePluginConfig(
|
|||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null,
|
||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
|
var maxDownloadParallelism: Int = 0
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -77,7 +80,8 @@ class SourcePluginConfig(
|
|||||||
private var _allowUrlsLowerVal: List<String>? = null;
|
private var _allowUrlsLowerVal: List<String>? = null;
|
||||||
private val _allowUrlsLower: List<String> get() {
|
private val _allowUrlsLower: List<String> get() {
|
||||||
if(_allowUrlsLowerVal == null)
|
if(_allowUrlsLowerVal == null)
|
||||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() };
|
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
||||||
|
.filter { it.length > 0 };
|
||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,7 +174,7 @@ class SourcePluginConfig(
|
|||||||
return true;
|
return true;
|
||||||
val uri = Uri.parse(url);
|
val uri = Uri.parse(url);
|
||||||
val host = uri.host?.lowercase() ?: "";
|
val host = uri.host?.lowercase() ?: "";
|
||||||
return _allowUrlsLower.any { it == host };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -54,4 +54,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
_hasGetDetails = _content.has("getDetails");
|
_hasGetDetails = _content.has("getDetails");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUnderlyingObject(): V8ValueObject? {
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
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,4 +35,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
|||||||
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
||||||
audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2;
|
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
@@ -0,0 +1,64 @@
|
|||||||
|
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
@@ -0,0 +1,114 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-8
@@ -10,10 +10,12 @@ 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.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
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.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.api.media.platforms.js.models.JSRequestModifier
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
@@ -21,9 +23,17 @@ abstract class JSSource {
|
|||||||
protected val _plugin: JSClient;
|
protected val _plugin: JSClient;
|
||||||
protected val _config: IV8PluginConfig;
|
protected val _config: IV8PluginConfig;
|
||||||
protected val _obj: V8ValueObject;
|
protected val _obj: V8ValueObject;
|
||||||
|
|
||||||
val hasRequestModifier: Boolean;
|
val hasRequestModifier: Boolean;
|
||||||
private val _requestModifier: JSRequest?;
|
private val _requestModifier: JSRequest?;
|
||||||
|
|
||||||
|
val hasRequestExecutor: Boolean;
|
||||||
|
private val _requestExecutor: JSRequest?;
|
||||||
|
|
||||||
|
val requiresCustomDatasource: Boolean get() {
|
||||||
|
return hasRequestModifier || hasRequestExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
val type : String;
|
val type : String;
|
||||||
|
|
||||||
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
||||||
@@ -36,6 +46,11 @@ abstract class JSSource {
|
|||||||
JSRequest(plugin, it, null, null, true);
|
JSRequest(plugin, it, null, null, true);
|
||||||
}
|
}
|
||||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
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? {
|
fun getRequestModifier(): IRequestModifier? {
|
||||||
@@ -44,20 +59,38 @@ abstract class JSSource {
|
|||||||
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!hasRequestModifier || _obj.isClosed) {
|
if (!hasRequestModifier || _obj.isClosed)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
||||||
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result !is V8ValueObject) {
|
if (result !is V8ValueObject)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return JSRequestModifier(_plugin, result)
|
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 {
|
companion object {
|
||||||
const val TYPE_AUDIOURL = "AudioUrlSource";
|
const val TYPE_AUDIOURL = "AudioUrlSource";
|
||||||
@@ -65,33 +98,45 @@ abstract class JSSource {
|
|||||||
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
||||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||||
const val TYPE_DASH = "DashSource";
|
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_HLS = "HLSSource";
|
||||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||||
|
|
||||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
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");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||||
TYPE_HLS -> fromV8HLS(plugin, obj);
|
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||||
TYPE_DASH -> fromV8Dash(plugin, obj);
|
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||||
|
else -> {
|
||||||
|
Logger.w("JSSource", "Unknown video type ${type}");
|
||||||
|
null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
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 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 fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
|
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");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||||
|
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
|
||||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> {
|
||||||
|
Logger.w("JSSource", "Unknown audio type ${type}");
|
||||||
|
null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -23,9 +23,11 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
|
|||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -21,6 +21,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
|||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -33,4 +33,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
|||||||
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
|
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
|
||||||
indexEnd = _obj.getOrDefault(config, "indexEnd", 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,13 +6,17 @@ import android.net.Uri
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.BuildConfig
|
import android.util.Xml
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.ManagedHttpServer
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
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.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
@@ -25,16 +29,23 @@ 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.streams.sources.LocalVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
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.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.logging.Logger
|
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.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.toUrlAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -42,17 +53,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.jmdns.JmDNS
|
|
||||||
import javax.jmdns.ServiceEvent
|
|
||||||
import javax.jmdns.ServiceListener
|
|
||||||
import javax.jmdns.ServiceTypeListener
|
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private var _jmDNS: JmDNS? = null;
|
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer(9999);
|
||||||
@@ -69,105 +78,51 @@ class StateCasting {
|
|||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
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;
|
val isCasting: Boolean get() = activeDevice != null;
|
||||||
|
|
||||||
private val _chromecastServiceListener = object : ServiceListener {
|
private fun handleServiceUpdated(services: List<DnsService>) {
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
for (s in services) {
|
||||||
Logger.i(TAG, "ChromeCast service added: " + event.info);
|
//TODO: Addresses IPv4 only?
|
||||||
addOrUpdateDevice(event);
|
val addresses = s.addresses.toTypedArray()
|
||||||
}
|
val port = s.port.toInt()
|
||||||
|
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
if (s.name.endsWith("._googlecast._tcp.local")) {
|
||||||
Logger.i(TAG, "ChromeCast service removed: " + event.info);
|
if (name == null) {
|
||||||
synchronized(devices) {
|
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateChromeCastDevice(name, addresses, port)
|
||||||
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
||||||
|
|
||||||
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) {
|
addOrUpdateAirPlayDevice(name, addresses, port)
|
||||||
Logger.i(TAG, "AirPlay service resolved: " + event.info);
|
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.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) {
|
fun handleUrl(context: Context, url: String) {
|
||||||
@@ -236,29 +191,30 @@ class StateCasting {
|
|||||||
rememberedDevices.clear();
|
rememberedDevices.clear();
|
||||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
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();
|
_castServer.start();
|
||||||
enableDeveloper(true);
|
enableDeveloper(true);
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService started.");
|
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
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
if (!_started)
|
if (!_started)
|
||||||
@@ -268,25 +224,7 @@ class StateCasting {
|
|||||||
|
|
||||||
Logger.i(TAG, "CastingService stopping.")
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
val jmDNS = _jmDNS;
|
stopDiscovering()
|
||||||
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();
|
_scopeIO.cancel();
|
||||||
_scopeMain.cancel();
|
_scopeMain.cancel();
|
||||||
|
|
||||||
@@ -436,15 +374,26 @@ class StateCasting {
|
|||||||
} else {
|
} else {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (ad is FCastCastingDevice) {
|
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||||
Logger.i(TAG, "Casting as DASH direct");
|
if (isRawDash) {
|
||||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as raw DASH");
|
||||||
} else if (ad is AirPlayCastingDevice) {
|
|
||||||
Logger.i(TAG, "Casting as HLS indirect");
|
try {
|
||||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
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);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as DASH indirect");
|
if (ad is FCastCastingDevice) {
|
||||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
@@ -452,14 +401,22 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||||
Logger.i(TAG, "Casting as singular video");
|
Logger.i(TAG, "Casting as singular video");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if (audioSource is IAudioUrlSource) {
|
} 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");
|
Logger.i(TAG, "Casting as singular audio");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if(videoSource is IHLSManifestSource) {
|
} else if(videoSource is IHLSManifestSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
Logger.i(TAG, "Casting as proxied HLS");
|
||||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
@@ -467,7 +424,7 @@ class StateCasting {
|
|||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||||
}
|
}
|
||||||
} else if(audioSource is IHLSManifestAudioSource) {
|
} else if(audioSource is IHLSManifestAudioSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
@@ -480,6 +437,26 @@ class StateCasting {
|
|||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
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 {
|
} else {
|
||||||
var str = listOf(
|
var str = listOf(
|
||||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
@@ -520,7 +497,7 @@ class StateCasting {
|
|||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
@@ -539,7 +516,7 @@ class StateCasting {
|
|||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
@@ -558,7 +535,7 @@ class StateCasting {
|
|||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -654,7 +631,7 @@ class StateCasting {
|
|||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -667,8 +644,11 @@ class StateCasting {
|
|||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
val subtitleUrl = url + subtitlePath;
|
val subtitleUrl = url + subtitlePath;
|
||||||
|
|
||||||
|
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
"application/dash+xml")
|
"application/dash+xml")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -699,13 +679,17 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
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 ad = activeDevice ?: return listOf();
|
||||||
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val subtitlePath = "/subtitle-${id}";
|
|
||||||
|
|
||||||
val videoUrl = videoSource?.getVideoUrl();
|
val videoPath = "/video-${id}"
|
||||||
val audioUrl = audioSource?.getAudioUrl();
|
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 subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
return@withContext subtitleSource.getSubtitlesURI();
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
@@ -734,26 +718,42 @@ 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);
|
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||||
|
|
||||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
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);
|
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
val hlsUrl = url + hlsPath
|
val hlsUrl = url + hlsPath
|
||||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
|
||||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||||
|
|
||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
@@ -780,7 +780,7 @@ class StateCasting {
|
|||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFuntionHandler
|
return@HttpFunctionHandler
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -797,7 +797,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@@ -827,7 +827,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
newPlaylistUrl = url + newPlaylistPath
|
newPlaylistUrl = url + newPlaylistPath
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
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> {
|
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 ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
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> {
|
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 ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -1090,8 +1090,11 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
"application/dash+xml")
|
"application/dash+xml")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -1117,6 +1120,166 @@ class StateCasting {
|
|||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
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 {
|
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
return when (deviceInfo.type) {
|
return when (deviceInfo.type) {
|
||||||
CastProtocolType.CHROMECAST -> {
|
CastProtocolType.CHROMECAST -> {
|
||||||
@@ -1211,7 +1374,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val newDevice = deviceFactory();
|
val newDevice = deviceFactory();
|
||||||
devices[name] = newDevice;
|
this.devices[name] = newDevice;
|
||||||
|
|
||||||
invokeEvents = {
|
invokeEvents = {
|
||||||
onDeviceAdded.emit(newDevice);
|
onDeviceAdded.emit(newDevice);
|
||||||
@@ -1225,7 +1388,7 @@ class StateCasting {
|
|||||||
fun enableDeveloper(enableDev: Boolean){
|
fun enableDeveloper(enableDev: Boolean){
|
||||||
_castServer.removeAllHandlers("dev");
|
_castServer.removeAllHandlers("dev");
|
||||||
if(enableDev) {
|
if(enableDev) {
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
|
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
|
||||||
if (context.query.containsKey("dashUrl")) {
|
if (context.query.containsKey("dashUrl")) {
|
||||||
val dashUrl = context.query["dashUrl"];
|
val dashUrl = context.query["dashUrl"];
|
||||||
val html = "<div>\n" +
|
val html = "<div>\n" +
|
||||||
@@ -1265,6 +1428,9 @@ class StateCasting {
|
|||||||
companion object {
|
companion object {
|
||||||
val instance: StateCasting = StateCasting();
|
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";
|
private val TAG = "StateCasting";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ import com.futo.platformplayer.states.StateDeveloper
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.google.gson.ExclusionStrategy
|
import com.google.gson.ExclusionStrategy
|
||||||
import com.google.gson.FieldAttributes
|
import com.google.gson.FieldAttributes
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -573,7 +571,7 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val resp = _client.get(body.url!!, body.headers);
|
val resp = _client.get(body.url!!, body.headers);
|
||||||
|
|
||||||
context.respondCode(200,
|
context.respondCode(200,
|
||||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
|
||||||
context.query.getOrDefault("CT", "text/plain"));
|
context.query.getOrDefault("CT", "text/plain"));
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
|
StateCasting.instance.startDiscovering()
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
_devices.clear();
|
_devices.clear();
|
||||||
@@ -169,6 +171,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.stop();
|
(_imageLoader.drawable as Animatable?)?.stop();
|
||||||
|
|
||||||
|
StateCasting.instance.stopDiscovering()
|
||||||
StateCasting.instance.onDeviceAdded.remove(this);
|
StateCasting.instance.onDeviceAdded.remove(this);
|
||||||
StateCasting.instance.onDeviceChanged.remove(this);
|
StateCasting.instance.onDeviceChanged.remove(this);
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
StateCasting.instance.onDeviceRemoved.remove(this);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ 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.AudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
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.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.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
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.IVideoUrlSource
|
||||||
@@ -25,6 +27,14 @@ 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.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
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.constructs.Event1
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
@@ -34,6 +44,7 @@ import com.futo.platformplayer.parsers.HLS
|
|||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
import hasAnySource
|
import hasAnySource
|
||||||
@@ -46,9 +57,12 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.Thread.sleep
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -56,6 +70,7 @@ import java.util.concurrent.ForkJoinPool
|
|||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.time.times
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class VideoDownload {
|
class VideoDownload {
|
||||||
@@ -71,12 +86,50 @@ class VideoDownload {
|
|||||||
|
|
||||||
var targetPixelCount: Long? = null;
|
var targetPixelCount: Long? = null;
|
||||||
var targetBitrate: Long? = null;
|
var targetBitrate: Long? = null;
|
||||||
|
var targetVideoName: String? = null;
|
||||||
|
var targetAudioName: String? = null;
|
||||||
|
|
||||||
var videoSource: VideoUrlSource?;
|
var videoSource: VideoUrlSource?;
|
||||||
var audioSource: AudioUrlSource?;
|
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?;
|
var subtitleSource: SubtitleRawSource?;
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
var prepareTime: OffsetDateTime? = null;
|
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 progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
var isCancelled = false;
|
||||||
|
|
||||||
@@ -118,14 +171,32 @@ class VideoDownload {
|
|||||||
this.subtitleSource = null;
|
this.subtitleSource = null;
|
||||||
this.targetPixelCount = targetPixelCount;
|
this.targetPixelCount = targetPixelCount;
|
||||||
this.targetBitrate = targetBitrate;
|
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: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||||
this.videoSource = VideoUrlSource.fromUrlSource(videoSource);
|
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||||
this.audioSource = AudioUrlSource.fromUrlSource(audioSource);
|
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.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
this.prepareTime = OffsetDateTime.now();
|
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 {
|
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
||||||
@@ -156,9 +227,21 @@ class VideoDownload {
|
|||||||
|
|
||||||
suspend fun prepare(client: ManagedHttpClient) {
|
suspend fun prepare(client: ManagedHttpClient) {
|
||||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
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)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null)
|
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null && targetVideoName == null && targetAudioName == null)
|
||||||
throw IllegalStateException("No sources or query values set");
|
throw IllegalStateException("No sources or query values set");
|
||||||
|
|
||||||
//Fetch full video object and determine source
|
//Fetch full video object and determine source
|
||||||
@@ -192,23 +275,35 @@ class VideoDownload {
|
|||||||
videoSources.add(source)
|
videoSources.add(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var vsource: IVideoSource? = null;
|
||||||
|
|
||||||
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
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())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
if(vsource != null) {
|
|
||||||
if (vsource is IVideoUrlSource)
|
if(vsource == null) {
|
||||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
videoSource = null;
|
||||||
else
|
if(original.video.videoSources.size == 0)
|
||||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
requireVideoSource = 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) {
|
if(audioSource == null && targetBitrate != null) {
|
||||||
val audioSources = arrayListOf<IAudioSource>()
|
var audioSources = mutableListOf<IAudioSource>()
|
||||||
val video = original.video
|
val video = original.video
|
||||||
if (video is VideoUnMuxedSourceDescriptor) {
|
if (video is VideoUnMuxedSourceDescriptor) {
|
||||||
for (source in video.audioSources) {
|
for (source in video.audioSources) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestAudioSource) {
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
@@ -226,25 +321,43 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
var asource: IAudioSource? = null;
|
||||||
?: if(videoSource != null ) null
|
if(targetAudioName != null) {
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
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)");
|
||||||
if(asource == null)
|
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;
|
audioSource = null;
|
||||||
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
|
requireVideoSource = false;
|
||||||
|
}
|
||||||
else if(asource is IAudioUrlSource)
|
else if(asource is IAudioUrlSource)
|
||||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||||
|
else if(asource is JSSource && requiresLiveAudioSource)
|
||||||
|
audioSourceLive = asource;
|
||||||
else
|
else
|
||||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null)
|
if(!isVideoDownloadReady)
|
||||||
throw DownloadException("No valid sources found for video/audio");
|
throw DownloadException("No valid sources found for video");
|
||||||
|
if(!isAudioDownloadReady)
|
||||||
|
throw DownloadException("No valid sources found for audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||||
Logger.i(TAG, "VideoDownload Download [${name}]");
|
Logger.i(TAG, "VideoDownload Download [${name}]");
|
||||||
if(videoDetails == null || (videoSource == null && audioSource == null))
|
if(videoDetails == null || (videoSourceToUse == null && audioSourceToUse == null))
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
||||||
|
|
||||||
@@ -253,12 +366,19 @@ class VideoDownload {
|
|||||||
|
|
||||||
if(isCancelled) throw CancellationException("Download got cancelled");
|
if(isCancelled) throw CancellationException("Download got cancelled");
|
||||||
|
|
||||||
if(videoSource != null) {
|
val actualVideoSource = if(requiresLiveVideoSource && videoSourceLive is IVideoSource)
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${videoSource!!.width}x${videoSource!!.height}].${videoContainerToExtension(videoSource!!.container)}".sanitizeFileName();
|
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();
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -273,10 +393,11 @@ class VideoDownload {
|
|||||||
var lastAudioLength: Long = 0;
|
var lastAudioLength: Long = 0;
|
||||||
var lastAudioRead: Long = 0;
|
var lastAudioRead: Long = 0;
|
||||||
|
|
||||||
if(videoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading video");
|
Logger.i(TAG, "Started downloading video");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastVideoLength = length;
|
lastVideoLength = length;
|
||||||
@@ -289,23 +410,34 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFileSize = when (videoSource!!.container) {
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
videoFileSize = when (videoSource!!.container) {
|
||||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
"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);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading audio");
|
Logger.i(TAG, "Started downloading audio");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastAudioLength = length;
|
lastAudioLength = length;
|
||||||
@@ -318,17 +450,27 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioFileSize = when (audioSource!!.container) {
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
audioFileSize = when (audioSource!!.container) {
|
||||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
"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);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (subtitleSource != null) {
|
if (subtitleSource != null) {
|
||||||
@@ -398,15 +540,20 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
||||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||||
segmentFiles.add(segmentFile)
|
val outputStream = segmentFile.outputStream()
|
||||||
|
try {
|
||||||
|
segmentFiles.add(segmentFile)
|
||||||
|
|
||||||
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
|
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadedTotalLength += segmentLength
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadedTotalLength += segmentLength
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Combining segments into $targetFile");
|
Logger.i(TAG, "Combining segments into $targetFile");
|
||||||
@@ -473,6 +620,86 @@ 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 {
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
@@ -484,17 +711,25 @@ class VideoDownload {
|
|||||||
|
|
||||||
try{
|
try{
|
||||||
val head = client.tryHead(videoUrl);
|
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"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
{
|
{
|
||||||
val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
|
val maxParallel = if(relatedPlugin != null && relatedPlugin.config.maxDownloadParallelism > 0)
|
||||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})");
|
relatedPlugin.config.maxDownloadParallelism else 99;
|
||||||
|
val concurrency = Math.min(maxParallel, Settings.instance.downloads.getByteRangeThreadCount());
|
||||||
|
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
||||||
sourceLength = head["content-length"]!!.toLong();
|
sourceLength = head["content-length"]!!.toLong();
|
||||||
onProgress(sourceLength, 0, 0);
|
onProgress(sourceLength, 0, 0);
|
||||||
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Download $name Sequential");
|
Logger.i(TAG, "Download $name Sequential");
|
||||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
try {
|
||||||
|
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
@@ -518,17 +753,19 @@ class VideoDownload {
|
|||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
val progressRate: Int = 4096 * 25;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 25;
|
val speedRate: Int = 4096 * 5;
|
||||||
var readSinceLastSpeedTest: Long = 0;
|
var readSinceLastSpeedTest: Long = 0;
|
||||||
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = client.get(url);
|
val result = client.get(url);
|
||||||
if (!result.isOk)
|
if (!result.isOk) {
|
||||||
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
|
}
|
||||||
if (result.body == null)
|
if (result.body == null)
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
||||||
|
|
||||||
@@ -536,41 +773,114 @@ class VideoDownload {
|
|||||||
val sourceStream = result.body.byteStream();
|
val sourceStream = result.body.byteStream();
|
||||||
|
|
||||||
var totalRead: Long = 0;
|
var totalRead: Long = 0;
|
||||||
var read: Int;
|
try {
|
||||||
|
var read: Int;
|
||||||
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
do {
|
||||||
|
read = sourceStream.read(buffer);
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
do {
|
fileStream.write(buffer, 0, read);
|
||||||
read = sourceStream.read(buffer);
|
|
||||||
if (read < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
fileStream.write(buffer, 0, read);
|
totalRead += read;
|
||||||
|
|
||||||
totalRead += 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;
|
||||||
|
}
|
||||||
|
|
||||||
readSinceLastSpeedTest += read;
|
if (isCancelled)
|
||||||
if (totalRead / progressRate > lastProgressCount) {
|
throw CancellationException("Cancelled");
|
||||||
onProgress(sourceLength, totalRead, lastSpeed);
|
} while (read > 0);
|
||||||
lastProgressCount++;
|
} finally {
|
||||||
}
|
sourceStream.close()
|
||||||
if (readSinceLastSpeedTest > speedRate) {
|
result.body.close()
|
||||||
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);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
return sourceLength;
|
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) {
|
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;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
@@ -643,23 +953,47 @@ class VideoDownload {
|
|||||||
return tasks.map { it.get() };
|
return tasks.map { it.get() };
|
||||||
}
|
}
|
||||||
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
||||||
val toRead = rangeEnd - rangeStart;
|
var retryCount = 0
|
||||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
var lastException: Throwable? = null
|
||||||
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();
|
|
||||||
|
|
||||||
if(read < toRead)
|
while (retryCount <= 3) {
|
||||||
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
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();
|
||||||
|
|
||||||
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
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!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate() {
|
fun validate() {
|
||||||
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
||||||
if(videoSource != null) {
|
if(videoSourceToUse != null) {
|
||||||
if(videoFilePath == null)
|
if(videoFilePath == null)
|
||||||
throw IllegalStateException("Missing video file name after download");
|
throw IllegalStateException("Missing video file name after download");
|
||||||
val expectedFile = File(videoFilePath!!);
|
val expectedFile = File(videoFilePath!!);
|
||||||
@@ -670,7 +1004,7 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(audioSourceToUse != null) {
|
||||||
if(audioFilePath == null)
|
if(audioFilePath == null)
|
||||||
throw IllegalStateException("Missing audio file name after download");
|
throw IllegalStateException("Missing audio file name after download");
|
||||||
val expectedFile = File(audioFilePath!!);
|
val expectedFile = File(audioFilePath!!);
|
||||||
@@ -692,15 +1026,15 @@ class VideoDownload {
|
|||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSource!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSource!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSource != null && videoSource is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
localVideoSource.streamMetaData = (videoSource as IStreamMetaDataSource).streamMetaData;
|
localVideoSource.streamMetaData = (videoSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(localAudioSource != null && audioSource != null && audioSource is IStreamMetaDataSource)
|
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
|
||||||
localAudioSource.streamMetaData = (audioSource as IStreamMetaDataSource).streamMetaData;
|
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(existing != null) {
|
if(existing != null) {
|
||||||
existing.videoSerialized = videoDetails!!;
|
existing.videoSerialized = videoDetails!!;
|
||||||
@@ -757,6 +1091,9 @@ class VideoDownload {
|
|||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
const val GROUP_WATCHLATER= "WatchLater";
|
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? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4";
|
||||||
@@ -803,4 +1140,27 @@ class VideoDownload {
|
|||||||
return "subtitle";
|
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,6 +6,8 @@ import com.caoccao.javet.exceptions.JavetException
|
|||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
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.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
@@ -133,9 +135,10 @@ class V8Plugin {
|
|||||||
synchronized(_runtimeLock) {
|
synchronized(_runtimeLock) {
|
||||||
if (_runtime != null)
|
if (_runtime != null)
|
||||||
return;
|
return;
|
||||||
|
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||||
val host = V8Host.getV8Instance();
|
val host = V8Host.getV8Instance();
|
||||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||||
|
|
||||||
_runtime = host.createV8Runtime(options);
|
_runtime = host.createV8Runtime(options);
|
||||||
if (!host.isIsolateCreated)
|
if (!host.isIsolateCreated)
|
||||||
throw IllegalStateException("Isolate not created");
|
throw IllegalStateException("Isolate not created");
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.packages
|
|||||||
|
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
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.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -49,9 +51,20 @@ class PackageBridge : V8Package {
|
|||||||
fun buildFlavor(): String {
|
fun buildFlavor(): String {
|
||||||
return BuildConfig.FLAVOR;
|
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
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
|
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
UIDialogs.toast(str);
|
UIDialogs.toast(str);
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ class PackageDOMParser : V8Package {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun parentElement(): DOMNode? {
|
||||||
|
return parentNode();
|
||||||
|
}
|
||||||
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
|
|||||||
@V8Property
|
@V8Property
|
||||||
fun textContent(): String = _element.text();
|
fun textContent(): String = _element.text();
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun tagName(): String = _element.tagName().uppercase();
|
||||||
|
@V8Property
|
||||||
fun text(): String = _element.text().ifEmpty { data() };
|
fun text(): String = _element.text().ifEmpty { data() };
|
||||||
@V8Property
|
@V8Property
|
||||||
fun data(): String = _element.data();
|
fun data(): String = _element.data();
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import com.caoccao.javet.enums.V8ConversionMode
|
|||||||
import com.caoccao.javet.enums.V8ProxyMode
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
import com.caoccao.javet.values.V8Value
|
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.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.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
@@ -16,6 +20,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
import com.futo.platformplayer.logging.Logger
|
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 java.net.SocketTimeoutException
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
@@ -64,33 +71,44 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.request(method, url, headers)
|
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.request(method, url, headers);
|
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.requestWithBody(method, url, body, headers)
|
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.requestWithBody(method, url, body, headers);
|
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.GET(url, headers)
|
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.GET(url, headers);
|
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
|
||||||
_packageClientAuth.POST(url, body, headers)
|
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);
|
||||||
else
|
else
|
||||||
_packageClient.POST(url, body, headers);
|
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -111,8 +129,19 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IBridgeHttpResponse {
|
||||||
|
val url: String;
|
||||||
|
val code: Int;
|
||||||
|
val headers: Map<String, List<String>>?;
|
||||||
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
class BridgeHttpStringResponse(
|
||||||
|
override val url: String,
|
||||||
|
override val code: Int, val
|
||||||
|
body: String?,
|
||||||
|
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
|
||||||
|
|
||||||
val isOk = code >= 200 && code < 300;
|
val isOk = code >= 200 && code < 300;
|
||||||
|
|
||||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
@@ -125,6 +154,37 @@ class PackageHttp: V8Package {
|
|||||||
return obj;
|
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.
|
//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)
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
@@ -147,6 +207,12 @@ class PackageHttp: V8Package {
|
|||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
||||||
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
= 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
|
//Client-specific
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -169,12 +235,14 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<BridgeHttpResponse> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _reqs.parallelStream().map {
|
||||||
|
if(it.second.method == "DUMMY")
|
||||||
|
return@map null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.toList();
|
.toList();
|
||||||
@@ -230,65 +298,116 @@ class PackageHttp: V8Package {
|
|||||||
if(_client is JSHttpClient)
|
if(_client is JSHttpClient)
|
||||||
_client.doAllowNewCookies = allow;
|
_client.doAllowNewCookies = allow;
|
||||||
}
|
}
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(timeoutMs: Int) {
|
||||||
|
if(_client is JSHttpClient) {
|
||||||
|
_client.setTimeout(timeoutMs.toLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
return@logExceptions catchHttp {
|
return@logExceptions catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, null);
|
//logRequest(method, url, headers, null);
|
||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
return@catchHttp when(returnType) {
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
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
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, body);
|
//logRequest(method, url, headers, body);
|
||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
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
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("GET", url, headers, null);
|
//logRequest("GET", url, headers, null);
|
||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
//val responseBody = resp.body?.string();
|
||||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
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
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("POST", url, headers, body);
|
//logRequest("POST", url, headers, body);
|
||||||
val resp = client.post(url, body, headers);
|
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);
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -388,13 +507,13 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,20 +633,25 @@ class PackageHttp: V8Package {
|
|||||||
val url: String,
|
val url: String,
|
||||||
val headers: MutableMap<String, String>,
|
val headers: MutableMap<String, String>,
|
||||||
val body: String? = null,
|
val body: String? = null,
|
||||||
val contentType: String? = null
|
val contentType: String? = null,
|
||||||
|
val respType: ReturnType = ReturnType.STRING
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ReturnType(val value: Int) {
|
||||||
|
STRING(0),
|
||||||
|
BYTES(1);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageHttp";
|
private const val TAG = "PackageHttp";
|
||||||
|
|||||||
+20
-1
@@ -16,6 +16,7 @@ import androidx.core.animation.doOnEnd
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
@@ -222,6 +223,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
buttons.removeAt(faqIndex)
|
buttons.removeAt(faqIndex)
|
||||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
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) {
|
for (data in buttons) {
|
||||||
val button = MenuButton(context, data, _fragment, true);
|
val button = MenuButton(context, data, _fragment, true);
|
||||||
@@ -305,6 +313,16 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
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);
|
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
|
//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
|
||||||
|
|
||||||
@@ -370,7 +388,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//98 is reversed for buy button
|
//96 is reserved for privacy button
|
||||||
|
//98 is reserved for buy button
|
||||||
//99 is reserved for more button
|
//99 is reserved for more button
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -221,8 +221,8 @@ class CommentsFragment : MainFragment() {
|
|||||||
|
|
||||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
//val navUrl = "https://harbor.social/" + 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);
|
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -118,8 +118,13 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
|
|
||||||
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
||||||
_overlayContainer.let {
|
_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), "hide",
|
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(
|
||||||
{ StateMeta.instance.addHiddenVideo(content.url);
|
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);
|
||||||
if (fragment is HomeFragment) {
|
if (fragment is HomeFragment) {
|
||||||
val removeIndex = recyclerData.results.indexOf(content);
|
val removeIndex = recyclerData.results.indexOf(content);
|
||||||
if (removeIndex >= 0) {
|
if (removeIndex >= 0) {
|
||||||
@@ -128,8 +133,12 @@ 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), "playFeed",
|
SlideUpMenuItem(context,
|
||||||
{
|
R.drawable.ic_playlist,
|
||||||
|
context.getString(R.string.play_feed_as_queue),
|
||||||
|
context.getString(R.string.play_entire_feed),
|
||||||
|
tag = "playFeed",
|
||||||
|
call = {
|
||||||
val newQueue = listOf(content) + recyclerData.results
|
val newQueue = listOf(content) + recyclerData.results
|
||||||
.filterIsInstance<IPlatformVideo>()
|
.filterIsInstance<IPlatformVideo>()
|
||||||
.filter { it != content };
|
.filter { it != content };
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
|
||||||
|
|
||||||
abstract class MainFragment : MainActivityFragment() {
|
abstract class MainFragment : MainActivityFragment() {
|
||||||
open val isMainView: Boolean = false;
|
open val isMainView: Boolean = false;
|
||||||
@@ -46,10 +45,6 @@ abstract class MainFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onOrientationChanged(orientation: OrientationManager.Orientation) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onBackPressed(): Boolean {
|
open fun onBackPressed(): Boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-13
@@ -109,19 +109,31 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
||||||
|
|
||||||
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
|
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), 1, {
|
SlideUpMenuItem(
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
context,
|
||||||
.setType("text/plain")
|
R.drawable.ic_list,
|
||||||
.setText(reconstruction)
|
context.getString(R.string.share_as_text),
|
||||||
.intent);
|
context.getString(R.string.share_as_a_list_of_video_urls),
|
||||||
}),
|
tag = 1,
|
||||||
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, {
|
call = {
|
||||||
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
.setType("text/plain")
|
||||||
.setType("application/json")
|
.setText(reconstruction)
|
||||||
.setStream(shareUri)
|
.intent);
|
||||||
.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);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+112
-119
@@ -3,30 +3,34 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
|||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.SimpleOrientationListener
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
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.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
import com.futo.platformplayer.listeners.AutoRotateChangeListener
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||||
|
|
||||||
|
|
||||||
class VideoDetailFragment : MainFragment {
|
class VideoDetailFragment : MainFragment {
|
||||||
override val isMainView : Boolean = false;
|
override val isMainView : Boolean = false;
|
||||||
override val hasBottomBar: Boolean = true;
|
override val hasBottomBar: Boolean = true;
|
||||||
@@ -37,23 +41,32 @@ class VideoDetailFragment : MainFragment {
|
|||||||
|
|
||||||
private var _viewDetail : VideoDetailView? = null;
|
private var _viewDetail : VideoDetailView? = null;
|
||||||
private var _view : SingleViewTouchableMotionLayout? = 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;
|
var isFullscreen : Boolean = false;
|
||||||
|
val onFullscreenChanged = Event1<Boolean>();
|
||||||
var isTransitioning : Boolean = false
|
var isTransitioning : Boolean = false
|
||||||
private set;
|
private set;
|
||||||
var isInPictureInPicture : Boolean = false
|
var isInPictureInPicture : Boolean = false
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
var state: State = State.CLOSED;
|
private var _state: State = State.CLOSED
|
||||||
|
|
||||||
|
var state: State
|
||||||
|
get() = _state
|
||||||
|
set(value) {
|
||||||
|
_state = value
|
||||||
|
onStateChanged(value)
|
||||||
|
}
|
||||||
|
|
||||||
val currentUrl get() = _viewDetail?.currentUrl;
|
val currentUrl get() = _viewDetail?.currentUrl;
|
||||||
|
|
||||||
val onMinimize = Event0();
|
val onMinimize = Event0();
|
||||||
val onTransitioning = Event1<Boolean>();
|
val onTransitioning = Event1<Boolean>();
|
||||||
val onMaximized = Event0();
|
val onMaximized = Event0();
|
||||||
|
|
||||||
var lastOrientation : OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
|
|
||||||
private set;
|
|
||||||
|
|
||||||
private var _isInitialMaximize = true;
|
private var _isInitialMaximize = true;
|
||||||
|
|
||||||
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
||||||
@@ -73,6 +86,29 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail?.prevVideo(true);
|
_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) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
Logger.i(TAG, "onShownWithView parameter=$parameter")
|
Logger.i(TAG, "onShownWithView parameter=$parameter")
|
||||||
@@ -98,49 +134,6 @@ 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 {
|
override fun onBackPressed(): Boolean {
|
||||||
Logger.i(TAG, "onBackPressed")
|
Logger.i(TAG, "onBackPressed")
|
||||||
|
|
||||||
@@ -154,6 +147,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
closeVideoDetails();
|
closeVideoDetails();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHide() {
|
override fun onHide() {
|
||||||
super.onHide();
|
super.onHide();
|
||||||
}
|
}
|
||||||
@@ -163,7 +157,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail?.preventPictureInPicture = true;
|
_viewDetail?.preventPictureInPicture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun minimizeVideoDetail(){
|
fun minimizeVideoDetail() {
|
||||||
_viewDetail?.setFullscreen(false);
|
_viewDetail?.setFullscreen(false);
|
||||||
if(_view != null)
|
if(_view != null)
|
||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
@@ -265,7 +259,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
context
|
|
||||||
_view?.let {
|
_view?.let {
|
||||||
if (it.progress >= 0.5 && it.progress < 1.0)
|
if (it.progress >= 0.5 && it.progress < 1.0)
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
@@ -273,9 +266,40 @@ class VideoDetailFragment : MainFragment {
|
|||||||
minimizeVideoDetail();
|
minimizeVideoDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||||
maximizeVideoDetail();
|
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!!;
|
return _view!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +357,6 @@ 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();
|
StateCasting.instance.onResume();
|
||||||
}
|
}
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -379,6 +398,12 @@ class VideoDetailFragment : MainFragment {
|
|||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
Logger.v(TAG, "onDestroyMainView");
|
Logger.v(TAG, "onDestroyMainView");
|
||||||
|
_autoRotateChangeListener?.unregister()
|
||||||
|
_orientationListener.stopListening()
|
||||||
|
|
||||||
|
SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
_viewDetail = null;
|
_viewDetail = null;
|
||||||
it.onDestroy();
|
it.onDestroy();
|
||||||
@@ -386,13 +411,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_view = null;
|
_view = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
|
||||||
onOrientationChanged(lastOrientation);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
@@ -408,64 +426,39 @@ class VideoDetailFragment : MainFragment {
|
|||||||
onMaximized.clear();
|
onMaximized.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFullscreenChanged(fullscreen : Boolean) {
|
|
||||||
activity?.let {
|
private fun hideSystemUI() {
|
||||||
if (fullscreen) {
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||||
if (Settings.instance.playback.fullscreenPortrait) {
|
activity?.window?.insetsController?.let { controller ->
|
||||||
changeOrientation(lastOrientation);
|
controller.hide(WindowInsets.Type.statusBars())
|
||||||
} else {
|
controller.hide(WindowInsets.Type.systemBars())
|
||||||
var orient = lastOrientation;
|
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
|
|
||||||
orient = OrientationManager.Orientation.LANDSCAPE;
|
|
||||||
changeOrientation(orient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
|
||||||
}
|
}
|
||||||
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)
|
private fun showSystemUI() {
|
||||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||||
controller.hide(WindowInsetsCompat.Type.statusBars());
|
activity?.window?.insetsController?.let { controller ->
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
controller.show(WindowInsets.Type.statusBars())
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
controller.show(WindowInsets.Type.systemBars())
|
||||||
}
|
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onFullscreenChanged(fullscreen : Boolean) {
|
||||||
|
isFullscreen = fullscreen;
|
||||||
|
onFullscreenChanged.emit(isFullscreen);
|
||||||
|
|
||||||
|
if (isFullscreen) {
|
||||||
|
hideSystemUI()
|
||||||
|
} else {
|
||||||
|
showSystemUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrientation();
|
||||||
|
_view?.allowMotion = !fullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "VideoDetailFragment";
|
private val TAG = "VideoDetailFragment";
|
||||||
|
|
||||||
|
|||||||
+130
-45
@@ -23,7 +23,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.webkit.WebView
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -61,6 +60,7 @@ 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.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
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.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.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
@@ -72,6 +72,7 @@ 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.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
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.JSVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
@@ -99,6 +100,7 @@ import com.futo.platformplayer.selectBestImage
|
|||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
@@ -111,6 +113,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
|||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
import com.futo.platformplayer.toHumanNowDiffString
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.toHumanTime
|
import com.futo.platformplayer.toHumanTime
|
||||||
@@ -158,6 +161,7 @@ import java.time.OffsetDateTime
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
class VideoDetailView : ConstraintLayout {
|
class VideoDetailView : ConstraintLayout {
|
||||||
private val TAG = "VideoDetailView"
|
private val TAG = "VideoDetailView"
|
||||||
|
|
||||||
@@ -656,8 +660,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
//val navUrl = "https://harbor.social/" + 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);
|
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
//_container_content_browser.goto(navUrl);
|
//_container_content_browser.goto(navUrl);
|
||||||
//switchContentView(_container_content_browser);
|
//switchContentView(_container_content_browser);
|
||||||
@@ -690,6 +694,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
_lastSubtitleSource = null;
|
_lastSubtitleSource = null;
|
||||||
video = null;
|
video = null;
|
||||||
|
_player.clear();
|
||||||
cleanupPlaybackTracker();
|
cleanupPlaybackTracker();
|
||||||
Logger.i(TAG, "Keep screen on unset onClose")
|
Logger.i(TAG, "Keep screen on unset onClose")
|
||||||
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
@@ -1166,6 +1171,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
//@OptIn(ExperimentalCoroutinesApi::class)
|
//@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
|
_didTriggerDatasourceErrroCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
|
|
||||||
if(newVideo && this.video?.url == videoDetail.url)
|
if(newVideo && this.video?.url == videoDetail.url)
|
||||||
return;
|
return;
|
||||||
@@ -1232,18 +1239,25 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
if(!StateApp.instance.privateMode) {
|
||||||
var tracker = video.getPlaybackTracker()
|
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
var tracker = video.getPlaybackTracker()
|
||||||
|
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||||
|
|
||||||
if (tracker == null) {
|
if (tracker == null) {
|
||||||
stopwatch.reset()
|
stopwatch.reset()
|
||||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||||
Logger.i(TAG, "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
Logger.i(
|
||||||
|
TAG,
|
||||||
|
"StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (me.video == video)
|
||||||
|
me._playbackTracker = tracker;
|
||||||
}
|
}
|
||||||
|
else if(me.video == video)
|
||||||
if(me.video == video)
|
me._playbackTracker = null;
|
||||||
me._playbackTracker = tracker;
|
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Playback tracker failed", ex);
|
Logger.e(TAG, "Playback tracker failed", ex);
|
||||||
@@ -1447,6 +1461,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
StatePlayer.instance.setCurrentlyPlaying(video);
|
StatePlayer.instance.setCurrentlyPlaying(video);
|
||||||
|
|
||||||
|
_liveChat?.stop();
|
||||||
|
_liveChat = null;
|
||||||
if(video.isLive && video.live != null) {
|
if(video.isLive && video.live != null) {
|
||||||
loadLiveChat(video);
|
loadLiveChat(video);
|
||||||
}
|
}
|
||||||
@@ -1643,6 +1659,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _didTriggerDatasourceErrroCount = 0;
|
||||||
private var _didTriggerDatasourceError = false;
|
private var _didTriggerDatasourceError = false;
|
||||||
private fun onDataSourceError(exception: Throwable) {
|
private fun onDataSourceError(exception: Throwable) {
|
||||||
Logger.e(TAG, "onDataSourceError", exception);
|
Logger.e(TAG, "onDataSourceError", exception);
|
||||||
@@ -1652,26 +1669,49 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
return;
|
return;
|
||||||
val config = currentVideo.sourceConfig;
|
val config = currentVideo.sourceConfig;
|
||||||
|
|
||||||
if(!_didTriggerDatasourceError) {
|
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||||
_didTriggerDatasourceError = true;
|
_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,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||||
context.getString(R.string.media_error),
|
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),
|
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,
|
null,
|
||||||
0,
|
0,
|
||||||
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||||
UIDialogs.Action(context.getString(R.string.yes), {
|
UIDialogs.Action(context.getString(R.string.yes), {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StatePlatform.instance.reloadClient(context, config.id);
|
StatePlatform.instance.reloadClient(context, config.id);
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to reload video.", e)
|
Logger.e(TAG, "Failed to reload video.", e)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}
|
||||||
);
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1768,15 +1808,21 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bestVideoSources = videoSources?.map { it.height * it.width }
|
val doDedup = Settings.instance.playback.simplifySources;
|
||||||
|
|
||||||
|
val bestVideoSources = if(doDedup) (videoSources?.map { it.height * it.width }
|
||||||
?.distinct()
|
?.distinct()
|
||||||
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
|
?.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 }
|
?.filter { it != null }
|
||||||
?.toList() ?: listOf();
|
?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
|
||||||
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
|
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
|
||||||
val bestAudioSources = audioSources
|
val bestAudioSources = if(doDedup) audioSources
|
||||||
?.filter { it.container == bestAudioContainer }
|
?.filter { it.container == bestAudioContainer }
|
||||||
?.toList() ?: listOf();
|
?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
|
||||||
|
?.distinct()
|
||||||
|
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||||
@@ -1805,40 +1851,56 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
|
||||||
*localVideoSources
|
*localVideoSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it,
|
SlideUpMenuItem(this.context,
|
||||||
{ handleSelectVideoTrack(it) });
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
tag = it,
|
||||||
|
call = { handleSelectVideoTrack(it) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(localAudioSource?.isNotEmpty() == true)
|
if(localAudioSource?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
|
||||||
*localAudioSource
|
*localAudioSource
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
SlideUpMenuItem(this.context,
|
||||||
{ handleSelectAudioTrack(it) });
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
it.bitrate.toHumanBitrate(),
|
||||||
|
tag = it,
|
||||||
|
call = { handleSelectAudioTrack(it) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(localSubtitleSources?.isNotEmpty() == true)
|
if(localSubtitleSources?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_subtitles), "subtitles",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_subtitles), "subtitles",
|
||||||
*localSubtitleSources
|
*localSubtitleSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it,
|
||||||
{ handleSelectSubtitleTrack(it) })
|
call = { handleSelectSubtitleTrack(it) })
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamVideoFormats?.isEmpty() == false)
|
if(liveStreamVideoFormats?.isEmpty() == false)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
||||||
*liveStreamVideoFormats
|
*liveStreamVideoFormats
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
|
SlideUpMenuItem(this.context,
|
||||||
{ _player.selectVideoTrack(it.height) });
|
R.drawable.ic_movie,
|
||||||
|
it.label ?: it.containerMimeType ?: it.bitrate.toString(),
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
tag = it,
|
||||||
|
call = { _player.selectVideoTrack(it.height) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||||
*liveStreamAudioFormats
|
*liveStreamAudioFormats
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", it,
|
SlideUpMenuItem(this.context,
|
||||||
{ _player.selectAudioTrack(it.bitrate) });
|
R.drawable.ic_music,
|
||||||
|
"${it.label ?: it.containerMimeType} ${it.bitrate}",
|
||||||
|
"",
|
||||||
|
tag = it,
|
||||||
|
call = { _player.selectAudioTrack(it.bitrate) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
|
|
||||||
@@ -1846,24 +1908,38 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
|
||||||
*bestVideoSources
|
*bestVideoSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it,
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
{ handleSelectVideoTrack(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) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(bestAudioSources.isNotEmpty())
|
if(bestAudioSources.isNotEmpty())
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
|
||||||
*bestAudioSources
|
*bestAudioSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
{ handleSelectAudioTrack(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) });
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(video?.subtitles?.isNotEmpty() == true)
|
if(video?.subtitles?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
|
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
|
||||||
*video.subtitles
|
*video.subtitles
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it,
|
||||||
{ handleSelectSubtitleTrack(it) })
|
call = { handleSelectSubtitleTrack(it) })
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null);
|
else null);
|
||||||
}
|
}
|
||||||
@@ -2304,6 +2380,15 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTracker(positionMilliseconds, isPlaying, false);
|
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) {
|
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ 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.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
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.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.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
@@ -44,8 +47,8 @@ class VideoHelper {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
|
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 IAudioUrlWidevineSource
|
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IAudioUrlWidevineSource
|
||||||
|
|
||||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
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? {
|
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||||
@@ -127,7 +130,7 @@ class VideoHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
|
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
||||||
val urlToUse = videoSource.getVideoUrl();
|
val urlToUse = videoSource.getVideoUrl();
|
||||||
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
|
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
|
||||||
videoSource.duration * 1000,
|
videoSource.duration * 1000,
|
||||||
@@ -145,10 +148,10 @@ class VideoHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
|
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
|
||||||
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
return Pair(DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
||||||
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
|
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
|
||||||
return@Resolver dataSpec;
|
return@Resolver dataSpec;
|
||||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build())
|
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()), manifestConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
|
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
|
||||||
@@ -186,5 +189,25 @@ class VideoHelper {
|
|||||||
return@Resolver dataSpec;
|
return@Resolver dataSpec;
|
||||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(audioSource.getAudioUrl())).build())
|
})).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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
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,7 +28,12 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
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.net.SocketException
|
||||||
|
import java.time.Duration
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
@@ -44,7 +49,12 @@ class DownloadService : Service() {
|
|||||||
private var _notificationManager: NotificationManager? = null;
|
private var _notificationManager: NotificationManager? = null;
|
||||||
private var _notificationChannel: NotificationChannel? = null;
|
private var _notificationChannel: NotificationChannel? = null;
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
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 var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
@@ -183,14 +193,19 @@ class DownloadService : Service() {
|
|||||||
Logger.w(TAG, "Video Download [${download.name}] expired, re-preparing");
|
Logger.w(TAG, "Video Download [${download.name}] expired, re-preparing");
|
||||||
download.videoDetails = null;
|
download.videoDetails = null;
|
||||||
|
|
||||||
|
if(download.targetVideoName == null && download.videoSource != null)
|
||||||
|
download.targetVideoName = download.videoSource!!.name;
|
||||||
if(download.targetPixelCount == null && download.videoSource != null)
|
if(download.targetPixelCount == null && download.videoSource != null)
|
||||||
download.targetPixelCount = (download.videoSource!!.width * download.videoSource!!.height).toLong();
|
download.targetPixelCount = (download.videoSource!!.width * download.videoSource!!.height).toLong();
|
||||||
download.videoSource = null;
|
download.videoSource = null;
|
||||||
|
|
||||||
|
if(download.targetAudioName == null && download.audioSource != null)
|
||||||
|
download.targetAudioName = download.audioSource!!.name;
|
||||||
if(download.targetBitrate == null && download.audioSource != null)
|
if(download.targetBitrate == null && download.audioSource != null)
|
||||||
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
||||||
download.audioSource = null;
|
download.audioSource = null;
|
||||||
}
|
}
|
||||||
if(download.videoDetails == null || (download.videoSource == null && download.audioSource == null))
|
if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady))
|
||||||
download.changeState(VideoDownload.State.PREPARING);
|
download.changeState(VideoDownload.State.PREPARING);
|
||||||
notifyDownload(download);
|
notifyDownload(download);
|
||||||
|
|
||||||
@@ -207,7 +222,7 @@ class DownloadService : Service() {
|
|||||||
download.progress = progress;
|
download.progress = progress;
|
||||||
|
|
||||||
val currentTime = System.currentTimeMillis();
|
val currentTime = System.currentTimeMillis();
|
||||||
if (currentTime - lastNotifyTime > 500) {
|
if (currentTime - lastNotifyTime > 800) {
|
||||||
notifyDownload(download);
|
notifyDownload(download);
|
||||||
lastNotifyTime = currentTime;
|
lastNotifyTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import android.media.AudioManager
|
|||||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
@@ -96,7 +98,7 @@ class MediaPlaybackService : Service() {
|
|||||||
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
||||||
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
||||||
.build());
|
.build());
|
||||||
_mediaSession?.setCallback(object: MediaSessionCompat.Callback() {
|
_mediaSession?.setCallback(object : MediaSessionCompat.Callback() {
|
||||||
override fun onSeekTo(pos: Long) {
|
override fun onSeekTo(pos: Long) {
|
||||||
super.onSeekTo(pos)
|
super.onSeekTo(pos)
|
||||||
Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)");
|
Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)");
|
||||||
@@ -118,7 +120,9 @@ class MediaPlaybackService : Service() {
|
|||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
Logger.i(TAG, "Media session callback onStop()");
|
Logger.i(TAG, "Media session callback onStop()");
|
||||||
MediaControlReceiver.onCloseReceived.emit();
|
//MediaControlReceiver.onCloseReceived.emit();
|
||||||
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
|
updateMediaSession( null);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
@@ -56,6 +57,18 @@ class StateApp {
|
|||||||
|
|
||||||
val sessionId = UUID.randomUUID().toString();
|
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? {
|
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.futo.platformplayer.SettingsDev
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
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.developer.DeveloperEndpoints
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -23,6 +31,12 @@ class StateDeveloper {
|
|||||||
|
|
||||||
var devProxy: DevProxySettings? = null;
|
var devProxy: DevProxySettings? = null;
|
||||||
|
|
||||||
|
var testState: String? = null;
|
||||||
|
val isPlaybackTesting: Boolean get() {
|
||||||
|
return SettingsDev.instance.developerMode && testState == "TestPlayback";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
fun initializeDev(id: String) {
|
fun initializeDev(id: String) {
|
||||||
currentDevID = id;
|
currentDevID = id;
|
||||||
synchronized(_devLogs) {
|
synchronized(_devLogs) {
|
||||||
@@ -135,6 +149,37 @@ 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 {
|
companion object {
|
||||||
const val DEV_ID = "DEV";
|
const val DEV_ID = "DEV";
|
||||||
|
|
||||||
@@ -152,6 +197,7 @@ class StateDeveloper {
|
|||||||
it._server?.stop();
|
it._server?.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
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.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.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
||||||
@@ -334,7 +336,7 @@ class StateDownloads {
|
|||||||
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||||
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
||||||
}
|
}
|
||||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ class StateHistory {
|
|||||||
return historyIndex[url];
|
return historyIndex[url];
|
||||||
}
|
}
|
||||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||||
|
if(StateApp.instance.privateMode)
|
||||||
|
return null;
|
||||||
val existing = historyIndex[video.url];
|
val existing = historyIndex[video.url];
|
||||||
var result: DBHistory.Index? = null;
|
var result: DBHistory.Index? = null;
|
||||||
if(existing != null) {
|
if(existing != null) {
|
||||||
@@ -113,6 +115,19 @@ class StateHistory {
|
|||||||
return result;
|
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) {
|
fun removeHistory(url: String) {
|
||||||
val hist = getHistoryIndexByUrl(url);
|
val hist = getHistoryIndexByUrl(url);
|
||||||
if(hist != null)
|
if(hist != null)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class StatePlatform {
|
|||||||
private val _channelClientPool = PlatformMultiClientPool("Channels", 15); //Used primarily for subscription/background channel fetches
|
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 _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers
|
||||||
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
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();
|
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
||||||
@@ -109,13 +110,24 @@ class StatePlatform {
|
|||||||
//Batched Requests
|
//Batched Requests
|
||||||
private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope,
|
private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope,
|
||||||
{ url ->
|
{ url ->
|
||||||
|
|
||||||
Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]");
|
Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]");
|
||||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
if(!StateApp.instance.privateMode) {
|
||||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||||
} ?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
_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)");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
if(!Settings.instance.browsing.videoCache)
|
if(!Settings.instance.browsing.videoCache || StateApp.instance.privateMode)
|
||||||
return@BatchedTaskHandler null;
|
return@BatchedTaskHandler null;
|
||||||
else {
|
else {
|
||||||
val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null;
|
val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null;
|
||||||
@@ -131,7 +143,7 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ para, result ->
|
{ para, result ->
|
||||||
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive))
|
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive) || StateApp.instance.privateMode)
|
||||||
return@BatchedTaskHandler
|
return@BatchedTaskHandler
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Caching [${para}]");
|
Logger.i(TAG, "Caching [${para}]");
|
||||||
@@ -871,7 +883,10 @@ class StatePlatform {
|
|||||||
if(!client.capabilities.hasGetComments)
|
if(!client.capabilities.hasGetComments)
|
||||||
return EmptyPager();
|
return EmptyPager();
|
||||||
|
|
||||||
return client.fromPool(_mainClientPool).getComments(url);
|
if(!StateApp.instance.privateMode)
|
||||||
|
return client.fromPool(_mainClientPool).getComments(url);
|
||||||
|
else
|
||||||
|
return client.fromPool(_privateClientPool).getComments(url);
|
||||||
}
|
}
|
||||||
fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||||
Logger.i(TAG, "Platform - getSubComments");
|
Logger.i(TAG, "Platform - getSubComments");
|
||||||
@@ -882,7 +897,11 @@ class StatePlatform {
|
|||||||
fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? {
|
fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? {
|
||||||
Logger.i(TAG, "Platform - getLiveChat");
|
Logger.i(TAG, "Platform - getLiveChat");
|
||||||
var client = getContentClient(url);
|
var client = getContentClient(url);
|
||||||
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
|
||||||
|
if(!StateApp.instance.privateMode)
|
||||||
|
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
||||||
|
else
|
||||||
|
return client.fromPool(_privateClientPool).getLiveEvents(url);
|
||||||
}
|
}
|
||||||
fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? {
|
fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? {
|
||||||
Logger.i(TAG, "Platform - getLiveChat");
|
Logger.i(TAG, "Platform - getLiveChat");
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ class StatePlayer {
|
|||||||
private var _thumbnailExoPlayer : PlayerManager? = null;
|
private var _thumbnailExoPlayer : PlayerManager? = null;
|
||||||
|
|
||||||
//Video Status
|
//Video Status
|
||||||
var rotationLock : Boolean = false;
|
var rotationLock: Boolean = false
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
onRotationLockChanged.emit(value)
|
||||||
|
}
|
||||||
|
val onRotationLockChanged = Event1<Boolean>()
|
||||||
var loopVideo : Boolean = false;
|
var loopVideo : Boolean = false;
|
||||||
|
|
||||||
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
|
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ enum class FeedStyle(val value: Int) {
|
|||||||
|
|
||||||
fun fromInt(value: Int): FeedStyle
|
fun fromInt(value: Int): FeedStyle
|
||||||
{
|
{
|
||||||
val result = FeedStyle.values().firstOrNull { it.value == value };
|
val result = FeedStyle.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+23
-3
@@ -6,14 +6,17 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
|
||||||
class SlideUpMenuItem : RelativeLayout {
|
class SlideUpMenuItem : ConstraintLayout {
|
||||||
|
|
||||||
private lateinit var _root: RelativeLayout;
|
private lateinit var _root: ConstraintLayout;
|
||||||
private lateinit var _image: ImageView;
|
private lateinit var _image: ImageView;
|
||||||
private lateinit var _text: TextView;
|
private lateinit var _text: TextView;
|
||||||
private lateinit var _subtext: TextView;
|
private lateinit var _subtext: TextView;
|
||||||
|
private lateinit var _description: TextView;
|
||||||
|
|
||||||
var selectedOption: Boolean = false;
|
var selectedOption: Boolean = false;
|
||||||
|
|
||||||
@@ -25,11 +28,27 @@ class SlideUpMenuItem : RelativeLayout {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any?, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
imageRes: Int = 0,
|
||||||
|
mainText: String,
|
||||||
|
subText: String = "",
|
||||||
|
description: String? = "",
|
||||||
|
tag: Any?,
|
||||||
|
call: (() -> Unit)? = null,
|
||||||
|
invokeParent: Boolean = true
|
||||||
|
): super(context){
|
||||||
init();
|
init();
|
||||||
_image.setImageResource(imageRes);
|
_image.setImageResource(imageRes);
|
||||||
_text.text = mainText;
|
_text.text = mainText;
|
||||||
_subtext.text = subText;
|
_subtext.text = subText;
|
||||||
|
|
||||||
|
if(description.isNullOrEmpty())
|
||||||
|
_description.isVisible = false;
|
||||||
|
else {
|
||||||
|
_description.text = description;
|
||||||
|
_description.isVisible = true;
|
||||||
|
}
|
||||||
this.itemTag = tag;
|
this.itemTag = tag;
|
||||||
|
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
@@ -48,6 +67,7 @@ class SlideUpMenuItem : RelativeLayout {
|
|||||||
_image = findViewById(R.id.slide_up_menu_item_image);
|
_image = findViewById(R.id.slide_up_menu_item_image);
|
||||||
_text = findViewById(R.id.slide_up_menu_item_text);
|
_text = findViewById(R.id.slide_up_menu_item_text);
|
||||||
_subtext = findViewById(R.id.slide_up_menu_item_subtext);
|
_subtext = findViewById(R.id.slide_up_menu_item_subtext);
|
||||||
|
_description = findViewById(R.id.slide_up_menu_item_description);
|
||||||
|
|
||||||
setOptionSelected(false);
|
setOptionSelected(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.widget.TextView
|
|||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.setMargins
|
import androidx.core.view.setMargins
|
||||||
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.PlaybackParameters
|
import androidx.media3.common.PlaybackParameters
|
||||||
import androidx.media3.common.VideoSize
|
import androidx.media3.common.VideoSize
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
@@ -123,7 +124,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
private var _currentChapterLoopId: Int = 0;
|
private var _currentChapterLoopId: Int = 0;
|
||||||
private var _currentChapter: IChapter? = null;
|
private var _currentChapter: IChapter? = null;
|
||||||
private var _promptedForPermissions: Boolean = false;
|
private var _promptedForPermissions: Boolean = false;
|
||||||
|
@UnstableApi
|
||||||
|
private var _desiredResizeModePortrait: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
|
|
||||||
//Events
|
//Events
|
||||||
val onMinimize = Event1<FutoVideoPlayer>();
|
val onMinimize = Event1<FutoVideoPlayer>();
|
||||||
@@ -567,6 +569,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun setFullScreen(fullScreen: Boolean) {
|
fun setFullScreen(fullScreen: Boolean) {
|
||||||
|
updateRotateLock()
|
||||||
|
|
||||||
if (isFullScreen == fullScreen) {
|
if (isFullScreen == fullScreen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -591,7 +595,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
gestureControl.hideControls();
|
gestureControl.hideControls();
|
||||||
//videoControlsBar.visibility = View.VISIBLE;
|
//videoControlsBar.visibility = View.VISIBLE;
|
||||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
_videoView.resizeMode = _desiredResizeModePortrait;
|
||||||
|
|
||||||
videoControls.show();
|
videoControls.show();
|
||||||
_videoControls_fullscreen.hideImmediately();
|
_videoControls_fullscreen.hideImmediately();
|
||||||
@@ -728,9 +732,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
Log.d(TAG, "WEIRD HEIGHT DETECTED: ${_lastSourceFit}, Width: ${w}, Height: ${h}, VWidth: ${viewWidth}");
|
Log.d(TAG, "WEIRD HEIGHT DETECTED: ${_lastSourceFit}, Width: ${w}, Height: ${h}, VWidth: ${viewWidth}");
|
||||||
}
|
}
|
||||||
if(_lastSourceFit != determinedHeight)
|
if(_lastSourceFit != determinedHeight)
|
||||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
else
|
else
|
||||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||||
|
_videoView.resizeMode = _desiredResizeModePortrait
|
||||||
}
|
}
|
||||||
|
|
||||||
val marginBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics).toInt();
|
val marginBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics).toInt();
|
||||||
@@ -759,7 +764,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateRotateLock() {
|
fun updateRotateLock() {
|
||||||
if(!Settings.instance.playback.isAutoRotate()) {
|
if(Settings.instance.playback.autoRotate == 0) {
|
||||||
_control_rotate_lock.visibility = View.GONE;
|
_control_rotate_lock.visibility = View.GONE;
|
||||||
_control_rotate_lock_fullscreen.visibility = View.GONE;
|
_control_rotate_lock_fullscreen.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ package com.futo.platformplayer.views.video
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Xml
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.OptIn
|
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
|
||||||
|
import androidx.media3.common.C.Encoding
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
@@ -14,8 +19,11 @@ import androidx.media3.common.text.CueGroup
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.DefaultDataSource
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
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.drm.DefaultDrmSessionManagerProvider
|
||||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
@@ -26,6 +34,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
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.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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||||
@@ -36,22 +45,31 @@ 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.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
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.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.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
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.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.JSHLSManifestAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
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.JSVideoUrlRangeSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
import com.google.gson.Gson
|
||||||
import getHttpDataSourceFactory
|
import getHttpDataSourceFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@@ -68,6 +86,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
private set;
|
private set;
|
||||||
|
|
||||||
private var _lastVideoMediaSource: MediaSource? = null;
|
private var _lastVideoMediaSource: MediaSource? = null;
|
||||||
|
private var _lastGeneratedDash: String? = null;
|
||||||
private var _lastAudioMediaSource: MediaSource? = null;
|
private var _lastAudioMediaSource: MediaSource? = null;
|
||||||
private var _lastSubtitleMediaSource: MediaSource? = null;
|
private var _lastSubtitleMediaSource: MediaSource? = null;
|
||||||
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
|
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
|
||||||
@@ -312,18 +331,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
swapSources(videoSource, audioSource,false, play, keepSubtitles);
|
swapSources(videoSource, audioSource,false, play, keepSubtitles);
|
||||||
}
|
}
|
||||||
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
||||||
swapSourceInternal(videoSource);
|
var videoSourceUsed = videoSource;
|
||||||
swapSourceInternal(audioSource);
|
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);
|
||||||
if(!keepSubtitles)
|
if(!keepSubtitles)
|
||||||
_lastSubtitleMediaSource = null;
|
_lastSubtitleMediaSource = null;
|
||||||
return loadSelectedSources(play, resume);
|
if(didSetVideo && didSetAudio)
|
||||||
|
return loadSelectedSources(play, resume);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||||
swapSourceInternal(videoSource);
|
var videoSourceUsed = videoSource;
|
||||||
return loadSelectedSources(play, resume);
|
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;
|
||||||
}
|
}
|
||||||
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||||
swapSourceInternal(audioSource);
|
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
|
||||||
|
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
|
||||||
|
else
|
||||||
|
swapSourceInternal(audioSource, play, resume);
|
||||||
return loadSelectedSources(play, resume);
|
return loadSelectedSources(play, resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,29 +412,34 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun swapSourceInternal(videoSource: IVideoSource?) {
|
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
|
||||||
when(videoSource) {
|
_lastGeneratedDash = null;
|
||||||
is LocalVideoSource -> swapVideoSourceLocal(videoSource);
|
val didSet = when(videoSource) {
|
||||||
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
|
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||||
is IDashManifestSource -> swapVideoSourceDash(videoSource);
|
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||||
is IHLSManifestSource -> swapVideoSourceHLS(videoSource);
|
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
|
||||||
is IVideoUrlSource -> swapVideoSourceUrl(videoSource);
|
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume);
|
||||||
null -> _lastVideoMediaSource = null;
|
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||||
|
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||||
|
null -> { _lastVideoMediaSource = null; true;}
|
||||||
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
||||||
}
|
}
|
||||||
lastVideoSource = videoSource;
|
lastVideoSource = videoSource;
|
||||||
|
return didSet;
|
||||||
}
|
}
|
||||||
private fun swapSourceInternal(audioSource: IAudioSource?) {
|
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
|
||||||
when(audioSource) {
|
val didSet = when(audioSource) {
|
||||||
is LocalAudioSource -> swapAudioSourceLocal(audioSource);
|
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
|
||||||
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
|
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
|
||||||
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
|
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
|
||||||
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
|
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume);
|
||||||
is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
|
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
||||||
null -> _lastAudioMediaSource = null;
|
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||||
|
null -> { _lastAudioMediaSource = null; true; }
|
||||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||||
}
|
}
|
||||||
lastAudioSource = audioSource;
|
lastAudioSource = audioSource;
|
||||||
|
return didSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Video loads
|
//Video loads
|
||||||
@@ -415,7 +458,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
if(videoSource.hasItag) {
|
if(videoSource.hasItag) {
|
||||||
//Temporary workaround for Youtube
|
//Temporary workaround for Youtube
|
||||||
try {
|
try {
|
||||||
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
val results = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
||||||
|
_lastGeneratedDash = results.second;
|
||||||
|
_lastVideoMediaSource = results.first;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//If it fails to create the dash workaround, fallback to standard progressive
|
//If it fails to create the dash workaround, fallback to standard progressive
|
||||||
@@ -431,7 +476,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [Url]");
|
Logger.i(TAG, "Loading VideoSource [Url]");
|
||||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||||
videoSource.getHttpDataSourceFactory()
|
videoSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
@@ -441,7 +486,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||||
videoSource.getHttpDataSourceFactory()
|
videoSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
@@ -449,9 +494,60 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@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) {
|
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [HLS]");
|
Logger.i(TAG, "Loading VideoSource [HLS]");
|
||||||
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||||
videoSource.getHttpDataSourceFactory()
|
videoSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
@@ -493,7 +589,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
||||||
Logger.i(TAG, "Loading AudioSource [Url]");
|
Logger.i(TAG, "Loading AudioSource [Url]");
|
||||||
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
val dataSource = if(audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||||
audioSource.getHttpDataSourceFactory()
|
audioSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
@@ -503,7 +599,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
||||||
Logger.i(TAG, "Loading AudioSource [HLS]");
|
Logger.i(TAG, "Loading AudioSource [HLS]");
|
||||||
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
val dataSource = if(audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||||
audioSource.getHttpDataSourceFactory()
|
audioSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
@@ -511,10 +607,42 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
.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)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||||
val dataSource = if (audioSource is JSSource && audioSource.hasRequestModifier)
|
val dataSource = if (audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||||
audioSource.getHttpDataSourceFactory()
|
audioSource.getHttpDataSourceFactory()
|
||||||
else
|
else
|
||||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||||
@@ -564,28 +692,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
val sourceAudio = _lastAudioMediaSource;
|
val sourceAudio = _lastAudioMediaSource;
|
||||||
val sourceSubs = _lastSubtitleMediaSource;
|
val sourceSubs = _lastSubtitleMediaSource;
|
||||||
|
|
||||||
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
|
|
||||||
|
|
||||||
beforeSourceChanged();
|
beforeSourceChanged();
|
||||||
|
|
||||||
_mediaSource = if(sources.size == 1) {
|
val source = mergeMediaSources(sourceVideo, sourceAudio, sourceSubs);
|
||||||
Logger.i(TAG, "Using single source mode")
|
if(source == null)
|
||||||
(sourceVideo ?: sourceAudio);
|
|
||||||
}
|
|
||||||
else if(sources.size > 1) {
|
|
||||||
Logger.i(TAG, "Using multi source mode ${sources.size}")
|
|
||||||
MergingMediaSource(true, *sources);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Logger.i(TAG, "Using no sources loaded");
|
|
||||||
stop();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
_mediaSource = source;
|
||||||
|
|
||||||
reloadMediaSource(play, resume);
|
reloadMediaSource(play, resume);
|
||||||
return true;
|
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) {
|
||||||
|
Logger.i(TAG, "Using single source mode")
|
||||||
|
return (sourceVideo ?: sourceAudio);
|
||||||
|
}
|
||||||
|
else if(sources.size > 1) {
|
||||||
|
Logger.i(TAG, "Using multi source mode ${sources.size}")
|
||||||
|
return MergingMediaSource(true, *sources);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.i(TAG, "Using no sources loaded");
|
||||||
|
stop();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
|
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
|
||||||
val player = exoPlayer ?: return
|
val player = exoPlayer ?: return
|
||||||
@@ -609,6 +746,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
exoPlayer?.player?.stop();
|
exoPlayer?.player?.stop();
|
||||||
exoPlayer?.player?.clearMediaItems();
|
exoPlayer?.player?.clearMediaItems();
|
||||||
|
_lastVideoMediaSource = null;
|
||||||
|
_lastAudioMediaSource = null;
|
||||||
|
_lastSubtitleMediaSource = null;
|
||||||
|
_mediaSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(){
|
fun stop(){
|
||||||
@@ -635,6 +776,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
|
|
||||||
when (error.errorCode) {
|
when (error.errorCode) {
|
||||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
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);
|
onDatasourceError.emit(error);
|
||||||
}
|
}
|
||||||
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||||
@@ -676,8 +828,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
companion object {
|
companion object {
|
||||||
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||||
|
|
||||||
val PREFERED_VIDEO_CONTAINERS = arrayOf("video/mp4", "video/webm", "video/3gpp");
|
val PREFERED_VIDEO_CONTAINERS_MP4Pref = arrayOf("video/mp4", "video/webm", "video/3gpp");
|
||||||
val PREFERED_AUDIO_CONTAINERS = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus");
|
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 SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip");
|
val SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip");
|
||||||
}
|
}
|
||||||
|
|||||||
+178
-101
@@ -13,6 +13,8 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.modifier.IRequest;
|
import com.futo.platformplayer.api.media.models.modifier.IRequest;
|
||||||
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
|
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 com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
@@ -25,11 +27,17 @@ import androidx.media3.datasource.HttpDataSource;
|
|||||||
import androidx.media3.datasource.HttpUtil;
|
import androidx.media3.datasource.HttpUtil;
|
||||||
import androidx.media3.datasource.TransferListener;
|
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.base.Predicate;
|
||||||
import com.google.common.collect.ForwardingMap;
|
import com.google.common.collect.ForwardingMap;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
@@ -45,6 +53,8 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Based on the default ExoPlayer DefaultHttpDataSource
|
* Based on the default ExoPlayer DefaultHttpDataSource
|
||||||
*/
|
*/
|
||||||
@@ -63,6 +73,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
private boolean allowCrossProtocolRedirects;
|
private boolean allowCrossProtocolRedirects;
|
||||||
private boolean keepPostFor302Redirects;
|
private boolean keepPostFor302Redirects;
|
||||||
@Nullable private IRequestModifier requestModifier = null;
|
@Nullable private IRequestModifier requestModifier = null;
|
||||||
|
@Nullable public JSRequestExecutor requestExecutor = null;
|
||||||
|
@Nullable public JSRequestExecutor requestExecutor2 = null;
|
||||||
|
|
||||||
|
|
||||||
/** Creates an instance. */
|
/** Creates an instance. */
|
||||||
public Factory() {
|
public Factory() {
|
||||||
@@ -89,6 +102,30 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
this.requestModifier = requestModifier;
|
this.requestModifier = requestModifier;
|
||||||
return this;
|
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.
|
* Sets the user agent that will be used.
|
||||||
@@ -195,7 +232,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
defaultRequestProperties,
|
defaultRequestProperties,
|
||||||
contentTypePredicate,
|
contentTypePredicate,
|
||||||
keepPostFor302Redirects,
|
keepPostFor302Redirects,
|
||||||
requestModifier);
|
requestModifier,
|
||||||
|
requestExecutor,
|
||||||
|
requestExecutor2);
|
||||||
if (transferListener != null) {
|
if (transferListener != null) {
|
||||||
dataSource.addTransferListener(transferListener);
|
dataSource.addTransferListener(transferListener);
|
||||||
}
|
}
|
||||||
@@ -231,6 +270,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
private long bytesToRead;
|
private long bytesToRead;
|
||||||
private long bytesRead;
|
private long bytesRead;
|
||||||
@Nullable private IRequestModifier requestModifier;
|
@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(
|
private JSHttpDataSource(
|
||||||
@Nullable String userAgent,
|
@Nullable String userAgent,
|
||||||
@@ -240,7 +283,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
@Nullable RequestProperties defaultRequestProperties,
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
@Nullable Predicate<String> contentTypePredicate,
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
boolean keepPostFor302Redirects,
|
boolean keepPostFor302Redirects,
|
||||||
@Nullable IRequestModifier requestModifier) {
|
@Nullable IRequestModifier requestModifier,
|
||||||
|
@Nullable JSRequestExecutor requestExecutor,
|
||||||
|
@Nullable JSRequestExecutor requestExecutor2) {
|
||||||
super(/* isNetwork= */ true);
|
super(/* isNetwork= */ true);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.connectTimeoutMillis = connectTimeoutMillis;
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
@@ -251,12 +296,14 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
this.requestProperties = new RequestProperties();
|
this.requestProperties = new RequestProperties();
|
||||||
this.keepPostFor302Redirects = keepPostFor302Redirects;
|
this.keepPostFor302Redirects = keepPostFor302Redirects;
|
||||||
this.requestModifier = requestModifier;
|
this.requestModifier = requestModifier;
|
||||||
|
this.requestExecutor = requestExecutor;
|
||||||
|
this.requestExecutor2 = requestExecutor2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
return connection == null ? null : Uri.parse(connection.getURL().toString());
|
return connection == null ? fallbackUri : Uri.parse(connection.getURL().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -306,119 +353,147 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
bytesToRead = 0;
|
bytesToRead = 0;
|
||||||
transferInitializing(dataSpec);
|
transferInitializing(dataSpec);
|
||||||
|
|
||||||
String responseMessage;
|
//Use executor 2 if it matches the urlPrefix
|
||||||
HttpURLConnection connection;
|
JSRequestExecutor executor = (requestExecutor2 != null && requestExecutor2.getUrlPrefix() != null && dataSpec.uri.toString().startsWith(requestExecutor2.getUrlPrefix())) ?
|
||||||
try {
|
requestExecutor2 : requestExecutor;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a valid response code.
|
if(executor != null) {
|
||||||
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 {
|
try {
|
||||||
errorResponseBody =
|
Logger.Companion.i(TAG, "Executor for " + dataSpec.uri.toString(), null);
|
||||||
errorStream != null ? Util.toByteArray(errorStream) : Util.EMPTY_BYTE_ARRAY;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
String responseMessage;
|
||||||
|
HttpURLConnection connection;
|
||||||
|
try {
|
||||||
|
this.connection = makeConnection(dataSpec);
|
||||||
|
connection = this.connection;
|
||||||
|
responseCode = connection.getResponseCode();
|
||||||
|
responseMessage = connection.getResponseMessage();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
errorResponseBody = Util.EMPTY_BYTE_ARRAY;
|
closeConnectionQuietly();
|
||||||
|
throw HttpDataSourceException.createForIOException(
|
||||||
|
e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||||
}
|
}
|
||||||
closeConnectionQuietly();
|
|
||||||
@Nullable
|
|
||||||
IOException cause = responseCode == 416
|
|
||||||
? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
throw new InvalidResponseCodeException(
|
// Check for a valid response code.
|
||||||
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for a valid content type.
|
@Nullable InputStream errorStream = connection.getErrorStream();
|
||||||
String contentType = connection.getContentType();
|
byte[] errorResponseBody;
|
||||||
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
|
try {
|
||||||
closeConnectionQuietly();
|
errorResponseBody =
|
||||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
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;
|
||||||
|
|
||||||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
throw new InvalidResponseCodeException(
|
||||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody);
|
||||||
// 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.
|
// Check for a valid content type.
|
||||||
boolean isCompressed = isCompressed(connection);
|
String contentType = connection.getContentType();
|
||||||
if (!isCompressed) {
|
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
|
||||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
closeConnectionQuietly();
|
||||||
bytesToRead = dataSpec.length;
|
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 {
|
} else {
|
||||||
long contentLength = HttpUtil.getContentLength(
|
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||||
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.
|
|
||||||
bytesToRead = dataSpec.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Determine the length of the data to be read, after skipping.
|
||||||
inputStream = connection.getInputStream();
|
boolean isCompressed = isCompressed(connection);
|
||||||
if (isCompressed) {
|
if (!isCompressed) {
|
||||||
inputStream = new GZIPInputStream(inputStream);
|
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.
|
||||||
|
bytesToRead = dataSpec.length;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
closeConnectionQuietly();
|
|
||||||
throw new HttpDataSourceException(
|
|
||||||
e,
|
|
||||||
dataSpec,
|
|
||||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
|
||||||
HttpDataSourceException.TYPE_OPEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
opened = true;
|
try {
|
||||||
transferStarted(dataSpec);
|
inputStream = connection.getInputStream();
|
||||||
|
if (isCompressed) {
|
||||||
try {
|
inputStream = new GZIPInputStream(inputStream);
|
||||||
skipFully(bytesToSkip, dataSpec);
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
closeConnectionQuietly();
|
closeConnectionQuietly();
|
||||||
|
throw new HttpDataSourceException(
|
||||||
if (e instanceof HttpDataSourceException) {
|
e,
|
||||||
throw (HttpDataSourceException) e;
|
dataSpec,
|
||||||
|
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||||
|
HttpDataSourceException.TYPE_OPEN);
|
||||||
}
|
}
|
||||||
throw new HttpDataSourceException(
|
|
||||||
e,
|
|
||||||
dataSpec,
|
|
||||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
|
||||||
HttpDataSourceException.TYPE_OPEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytesToRead;
|
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
|
@Override
|
||||||
@@ -582,6 +657,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
requestHeaders = result.getHeaders();
|
requestHeaders = result.getHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl + "\nHEADERS: [" + V8RemoteObject.Companion.getGsonStandard().toJson(requestHeaders)+ "]", null);
|
||||||
|
|
||||||
HttpURLConnection connection = openConnection(new URL(requestUrl));
|
HttpURLConnection connection = openConnection(new URL(requestUrl));
|
||||||
connection.setConnectTimeout(connectTimeoutMillis);
|
connection.setConnectTimeout(connectTimeoutMillis);
|
||||||
connection.setReadTimeout(readTimeoutMillis);
|
connection.setReadTimeout(readTimeoutMillis);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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,6 +70,21 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:elevation="15dp">
|
android:elevation="15dp">
|
||||||
</FrameLayout>
|
</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
|
<com.futo.platformplayer.views.ToastView
|
||||||
android:id="@+id/toast_view"
|
android:id="@+id/toast_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -79,4 +94,5 @@
|
|||||||
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
|
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
|
||||||
app:layout_constraintRight_toRightOf="@id/fragment_main"
|
app:layout_constraintRight_toRightOf="@id/fragment_main"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
|
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
|
||||||
|
|
||||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text=""
|
android:text=""
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
|
android:maxHeight="120dp"
|
||||||
android:layout_marginStart="30dp"
|
android:layout_marginStart="30dp"
|
||||||
android:layout_marginEnd="30dp"
|
android:layout_marginEnd="30dp"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical">
|
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
|
<ImageButton
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
@@ -13,13 +24,19 @@
|
|||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_construction" />
|
app:srcCompat="@drawable/ic_construction" />
|
||||||
|
-->
|
||||||
|
|
||||||
<!--<ImageButton
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingRight="12dp"
|
android:textSize="22dp"
|
||||||
android:scaleType="fitCenter"
|
android:layout_marginTop="-2dp"
|
||||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:text="Grayjay"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginStart="8dp"/>
|
||||||
|
<!--
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -42,6 +59,8 @@
|
|||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:layout_marginTop="-8dp"/>
|
android:layout_marginTop="-8dp"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -15,13 +15,6 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/foreground" />
|
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
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -34,30 +27,6 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:layout_marginStart="8dp"/>
|
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
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|||||||
@@ -1,44 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
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">
|
android:gravity="center_vertical">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
<ImageView
|
android:layout_width="match_parent"
|
||||||
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:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:id="@+id/slide_up_menu_item_root"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginLeft="45dp"
|
android:background="@drawable/background_slide_up_option"
|
||||||
tools:text="Cat videos"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:textSize="11dp"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_marginTop="2dp"
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
android:fontFamily="@font/inter_light"
|
<ImageView
|
||||||
android:textColor="@color/white" />
|
android:id="@+id/slide_up_menu_item_image"
|
||||||
<TextView
|
android:layout_width="20dp"
|
||||||
android:id="@+id/slide_up_menu_item_subtext"
|
android:layout_height="20dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_marginLeft="15dp"
|
||||||
android:layout_height="wrap_content"
|
android:scaleType="fitCenter"
|
||||||
android:gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_alignParentRight="true"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginRight="15dp"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
tools:text="3 videos"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:textSize="11dp"
|
app:srcCompat="@drawable/ic_share" />
|
||||||
android:layout_marginTop="2dp"
|
<LinearLayout
|
||||||
android:fontFamily="@font/inter_light"
|
android:layout_width="0dp"
|
||||||
android:textColor="#ACACAC" />
|
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>
|
||||||
|
|
||||||
</RelativeLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<string name="add_to">Add to</string>
|
<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="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_queue">Add to queue</string>
|
||||||
|
<string name="add_to_history">Add to history</string>
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
<string name="channel">Channel</string>
|
<string name="channel">Channel</string>
|
||||||
<string name="home">Home</string>
|
<string name="home">Home</string>
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
<string name="sources">Sources</string>
|
<string name="sources">Sources</string>
|
||||||
<string name="buy">Buy</string>
|
<string name="buy">Buy</string>
|
||||||
<string name="faq">FAQ</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="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
|
||||||
<string name="defaults">Defaults</string>
|
<string name="defaults">Defaults</string>
|
||||||
<string name="home_screen">Home Screen</string>
|
<string name="home_screen">Home Screen</string>
|
||||||
@@ -66,6 +68,8 @@
|
|||||||
<string name="enabled">Enabled</string>
|
<string name="enabled">Enabled</string>
|
||||||
<string name="keep_screen_on">Keep screen on</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="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="discover">Discover</string>
|
||||||
<string name="find_new_video_sources_to_add">Find new video sources to add</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>
|
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||||
@@ -281,6 +285,8 @@
|
|||||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||||
<string name="auto_update">Auto Update</string>
|
<string name="auto_update">Auto Update</string>
|
||||||
<string name="auto_rotate">Auto-Rotate</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="auto_rotate_dead_zone">Auto-Rotate Dead Zone</string>
|
||||||
<string name="automatic_backup">Automatic Backup</string>
|
<string name="automatic_backup">Automatic Backup</string>
|
||||||
<string name="background_behavior">Background Behavior</string>
|
<string name="background_behavior">Background Behavior</string>
|
||||||
@@ -367,6 +373,12 @@
|
|||||||
<string name="system_volume_descr">Gesture controls adjust system volume</string>
|
<string name="system_volume_descr">Gesture controls adjust system volume</string>
|
||||||
<string name="live_chat_webview">Live Chat Webview</string>
|
<string name="live_chat_webview">Live Chat Webview</string>
|
||||||
<string name="full_screen_portrait">Fullscreen portrait</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="allow_full_screen_portrait">Allow fullscreen portrait</string>
|
||||||
<string name="background_switch_audio">Switch to Audio in Background</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>
|
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||||
@@ -389,6 +401,7 @@
|
|||||||
<string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string>
|
<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_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="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="player">Player</string>
|
||||||
<string name="plugins">Plugins</string>
|
<string name="plugins">Plugins</string>
|
||||||
<string name="preferred_casting_quality">Preferred Casting Quality</string>
|
<string name="preferred_casting_quality">Preferred Casting Quality</string>
|
||||||
@@ -475,6 +488,8 @@
|
|||||||
<string name="removes_all_subscriptions">Removes all subscriptions</string>
|
<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="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="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="subscriptions_cache_5000">Subscriptions Cache 5000</string>
|
||||||
<string name="history_cache_100">History Cache 100</string>
|
<string name="history_cache_100">History Cache 100</string>
|
||||||
<string name="start_server_on_boot">Start Server on boot</string>
|
<string name="start_server_on_boot">Start Server on boot</string>
|
||||||
@@ -762,6 +777,8 @@
|
|||||||
<string name="zoom">Zoom</string>
|
<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="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="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">
|
<string-array name="home_screen_array">
|
||||||
<item>Recommendations</item>
|
<item>Recommendations</item>
|
||||||
<item>Subscriptions</item>
|
<item>Subscriptions</item>
|
||||||
|
|||||||
@@ -33,6 +33,11 @@
|
|||||||
<data android:host="bilibili.com" />
|
<data android:host="bilibili.com" />
|
||||||
<data android:host="bilibili.tv" />
|
<data android:host="bilibili.tv" />
|
||||||
<data android:host="spotify.com" />
|
<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="/" />
|
<data android:pathPrefix="/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
@@ -57,6 +62,11 @@
|
|||||||
<data android:host="bilibili.com" />
|
<data android:host="bilibili.com" />
|
||||||
<data android:host="bilibili.tv" />
|
<data android:host="bilibili.tv" />
|
||||||
<data android:host="spotify.com" />
|
<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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/bilibili updated: b518be4dd5...850acb49a8
Submodule
+1
Submodule app/src/stable/assets/sources/bitchute added at 872bfa2d75
+1
Submodule app/src/stable/assets/sources/dailymotion added at 069aa3d31a
Submodule app/src/stable/assets/sources/rumble updated: bedbc4a989...cbfe372bcc
Submodule app/src/stable/assets/sources/spotify updated: 4e826dcb6a...4d554e9388
Submodule app/src/stable/assets/sources/youtube updated: d2a903d5dd...a4766ec223
@@ -10,7 +10,9 @@
|
|||||||
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
||||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
||||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.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"
|
||||||
},
|
},
|
||||||
"SOURCES_EMBEDDED_DEFAULT": [
|
"SOURCES_EMBEDDED_DEFAULT": [
|
||||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
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