Compare commits

...

8 Commits

Author SHA1 Message Date
semantic-release-bot
e9df0ec5d1 chore(release): 4.8.3-dev.1 [skip ci]
## [4.8.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.2...v4.8.3-dev.1) (2024-05-25)

### Bug Fixes

* **YouTube - Spoof client:** Improve Android spoofing ([#3230](https://github.com/ReVanced/revanced-patches/issues/3230)) ([c20dbff](c20dbffc86))
2024-05-25 22:12:20 +00:00
LisoUseInAIKyrios
c20dbffc86 fix(YouTube - Spoof client): Improve Android spoofing (#3230) 2024-05-26 02:10:21 +04:00
semantic-release-bot
9c58b4b1f7 chore(release): 4.8.2 [skip ci]
## [4.8.2](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2) (2024-05-24)

### Bug Fixes

* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([82a0847](82a0847c6d))
2024-05-24 00:33:49 +00:00
oSumAtrIX
4eab75511f chore: Merge branch dev to main (#3223) 2024-05-24 02:31:24 +02:00
semantic-release-bot
04e556f4f1 chore(release): 4.8.2-dev.1 [skip ci]
## [4.8.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2-dev.1) (2024-05-23)

### Bug Fixes

* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([82a0847](82a0847c6d))
2024-05-23 20:44:17 +00:00
oSumAtrIX
82a0847c6d fix(YouTube - Client spoof): Spoof iOS client model to fix various side effects (#3220)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2024-05-23 22:42:11 +02:00
semantic-release-bot
0d1c455964 chore(release): 4.8.1 [skip ci]
## [4.8.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.0...v4.8.1) (2024-05-21)

### Bug Fixes

* Use UrlDecoder API available in older Android versions ([f92dabd](f92dabd8ea))
2024-05-21 01:41:36 +00:00
oSumAtrIX
395c1280e2 chore: Merge branch dev to main (#3201) 2024-05-21 03:39:34 +02:00
20 changed files with 106 additions and 170 deletions

View File

@@ -1,3 +1,31 @@
## [4.8.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.2...v4.8.3-dev.1) (2024-05-25)
### Bug Fixes
* **YouTube - Spoof client:** Improve Android spoofing ([#3230](https://github.com/ReVanced/revanced-patches/issues/3230)) ([b688923](https://github.com/ReVanced/revanced-patches/commit/b688923c7e83805f2377a19b20a969b8cb749a9c))
## [4.8.2](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2) (2024-05-24)
### Bug Fixes
* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([9b5f4ce](https://github.com/ReVanced/revanced-patches/commit/9b5f4ce2b251c67e24cfcac3edae70c8a8aae230))
## [4.8.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2-dev.1) (2024-05-23)
### Bug Fixes
* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([9b5f4ce](https://github.com/ReVanced/revanced-patches/commit/9b5f4ce2b251c67e24cfcac3edae70c8a8aae230))
## [4.8.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.0...v4.8.1) (2024-05-21)
### Bug Fixes
* Use UrlDecoder API available in older Android versions ([d42fbb1](https://github.com/ReVanced/revanced-patches/commit/d42fbb152126cf2177315c4706fb03bc89f5af1c))
## [4.8.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.0...v4.8.1-dev.1) (2024-05-21)

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 4.8.1-dev.1
version = 4.8.3-dev.1

View File

@@ -15,9 +15,12 @@ import app.revanced.patches.all.misc.resources.AddResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreen
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreen.Sorting
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.*
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.BuildInitPlaybackRequestFingerprint
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.BuildPlayerRequestURIFingerprint
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.CreatePlayerRequestBodyFingerprint
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.CreatePlayerRequestBodyWithModelFingerprint
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.SetPlayerRequestClientTypeFingerprint
import app.revanced.patches.youtube.misc.settings.SettingsPatch
import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch
import app.revanced.util.getReference
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
@@ -34,12 +37,9 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
name = "Spoof client",
description = "Spoofs the client to allow video playback.",
dependencies = [
SpoofClientResourcePatch::class,
PlayerResponseMethodHookPatch::class,
SettingsPatch::class,
AddResourcesPatch::class,
UserAgentClientSpoofPatch::class,
PlayerResponseMethodHookPatch::class,
],
compatiblePackages = [
CompatiblePackage(
@@ -69,18 +69,11 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
)
object SpoofClientPatch : BytecodePatch(
setOf(
// Client type spoof.
BuildInitPlaybackRequestFingerprint,
BuildPlayerRequestURIFingerprint,
SetPlayerRequestClientTypeFingerprint,
CreatePlayerRequestBodyFingerprint,
// Storyboard spoof.
StoryboardRendererSpecFingerprint,
PlayerResponseModelImplRecommendedLevelFingerprint,
StoryboardRendererDecoderRecommendedLevelFingerprint,
PlayerResponseModelImplGeneralFingerprint,
StoryboardRendererDecoderSpecFingerprint,
CreatePlayerRequestBodyWithModelFingerprint,
),
) {
private const val INTEGRATIONS_CLASS_DESCRIPTOR =
@@ -100,7 +93,6 @@ object SpoofClientPatch : BytecodePatch(
SwitchPreference("revanced_spoof_client_use_ios"),
),
),
)
// region Block /initplayback requests to fall back to /get_watch requests.
@@ -149,11 +141,11 @@ object SpoofClientPatch : BytecodePatch(
SetPlayerRequestClientTypeFingerprint.resultOrThrow().let { result ->
// Field in the player request object that holds the client info object.
val clientInfoField = result.mutableMethod
.getInstructions().first { instruction ->
.getInstructions().find { instruction ->
// requestMessage.clientInfo = clientInfoBuilder.build();
instruction.opcode == Opcode.IPUT_OBJECT &&
instruction.getReference<FieldReference>()?.type == CLIENT_INFO_CLASS_DESCRIPTOR
}.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoField")
}?.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoField")
// Client info object's client type field.
val clientInfoClientTypeField = result.mutableMethod
@@ -168,6 +160,19 @@ object SpoofClientPatch : BytecodePatch(
Triple(clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField)
}
val clientInfoClientModelField = CreatePlayerRequestBodyWithModelFingerprint.resultOrThrow().let {
val getClientModelIndex = CreatePlayerRequestBodyWithModelFingerprint.indexOfBuildModelInstruction(it.method)
val instructions = it.mutableMethod.getInstructions()
// The next IPUT_OBJECT instruction after getting the client model is setting the client model field.
instructions.subList(
getClientModelIndex,
instructions.size,
).find { instruction ->
instruction.opcode == Opcode.IPUT_OBJECT
}?.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoClientModelField")
}
// endregion
// region Spoof client type for /player requests.
@@ -189,7 +194,7 @@ object SpoofClientPatch : BytecodePatch(
)
}
// Change requestMessage.clientInfo.clientType and requestMessage.clientInfo.clientVersion to the spoofed values.
// Change client info to use the spoofed values.
// Do this in a helper method, to remove the need of picking out multiple free registers from the hooked code.
result.mutableClass.methods.add(
ImmutableMethod(
@@ -216,6 +221,12 @@ object SpoofClientPatch : BytecodePatch(
move-result v1
iput v1, v0, $clientInfoClientTypeField
# Set client model to the spoofed value.
iget-object v1, v0, $clientInfoClientModelField
invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->getClientModel(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $clientInfoClientModelField
# Set client version to the spoofed value.
iget-object v1, v0, $clientInfoClientVersionField
invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String;
@@ -232,104 +243,5 @@ object SpoofClientPatch : BytecodePatch(
// endregion
// region Fix storyboard if Android Testsuite is used.
PlayerResponseMethodHookPatch += PlayerResponseMethodHookPatch.Hook.ProtoBufferParameter(
"$INTEGRATIONS_CLASS_DESCRIPTOR->setPlayerResponseVideoId(" +
"Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;",
)
// Hook recommended seekbar thumbnails quality level for regular videos.
StoryboardRendererDecoderRecommendedLevelFingerprint.resultOrThrow().let {
val endIndex = it.scanResult.patternScanResult!!.endIndex
it.mutableMethod.apply {
val originalValueRegister =
getInstruction<OneRegisterInstruction>(endIndex).registerA
addInstructions(
endIndex + 1,
"""
invoke-static { v$originalValueRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->getRecommendedLevel(I)I
move-result v$originalValueRegister
""",
)
}
}
// Hook the recommended precise seeking thumbnails quality.
PlayerResponseModelImplRecommendedLevelFingerprint.resultOrThrow().let {
val endIndex = it.scanResult.patternScanResult!!.endIndex
it.mutableMethod.apply {
val originalValueRegister =
getInstruction<OneRegisterInstruction>(endIndex).registerA
addInstructions(
endIndex,
"""
invoke-static { v$originalValueRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->getRecommendedLevel(I)I
move-result v$originalValueRegister
""",
)
}
}
// TODO: Hook the seekbar recommended level for Shorts to fix Shorts low quality seekbar thumbnails.
/**
* Hook StoryBoard renderer url.
*/
PlayerResponseModelImplGeneralFingerprint.resultOrThrow().let {
val getStoryBoardIndex = it.scanResult.patternScanResult!!.endIndex
it.mutableMethod.apply {
val getStoryBoardRegister = getInstruction<OneRegisterInstruction>(getStoryBoardIndex).registerA
addInstructions(
getStoryBoardIndex,
"""
invoke-static { v$getStoryBoardRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->getStoryboardRendererSpec(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$getStoryBoardRegister
""",
)
}
}
// Hook the seekbar thumbnail decoder, required for Shorts.
StoryboardRendererDecoderSpecFingerprint.resultOrThrow().let {
val storyBoardUrlIndex = it.scanResult.patternScanResult!!.startIndex + 1
it.mutableMethod.apply {
val getStoryBoardRegister = getInstruction<OneRegisterInstruction>(storyBoardUrlIndex).registerA
addInstructions(
storyBoardUrlIndex + 1,
"""
invoke-static { v$getStoryBoardRegister }, ${INTEGRATIONS_CLASS_DESCRIPTOR}->getStoryboardRendererSpec(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$getStoryBoardRegister
""",
)
}
}
StoryboardRendererSpecFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val storyBoardUrlParams = "p0"
addInstructions(
0,
"""
if-nez $storyBoardUrlParams, :ignore
invoke-static { $storyBoardUrlParams }, $INTEGRATIONS_CLASS_DESCRIPTOR->getStoryboardRendererSpec(Ljava/lang/String;)Ljava/lang/String;
move-result-object $storyBoardUrlParams
:ignore
nop
""",
)
}
}
// endregion
}
}

View File

@@ -1,18 +0,0 @@
package app.revanced.patches.youtube.misc.fix.playback
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patches.shared.misc.mapping.ResourceMappingPatch
@Patch(dependencies = [ResourceMappingPatch::class])
internal object SpoofClientResourcePatch : ResourcePatch() {
internal var scrubbedPreviewThumbnailResourceId: Long = -1
override fun execute(context: ResourceContext) {
scrubbedPreviewThumbnailResourceId = ResourceMappingPatch[
"id",
"thumbnail",
]
}
}

View File

@@ -43,7 +43,7 @@ object SpoofSignaturePatch : BytecodePatch(
StoryboardRendererDecoderSpecFingerprint,
StoryboardRendererDecoderRecommendedLevelFingerprint,
StoryboardThumbnailParentFingerprint,
ScrubbedPreviewLayoutFingerprint,
SpoofSignaturePatchScrubbedPreviewLayoutFingerprint,
StatsQueryParameterFingerprint,
ParamsMapPutFingerprint,
),

View File

@@ -0,0 +1,31 @@
package app.revanced.patches.youtube.misc.fix.playback.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.youtube.misc.fix.playback.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfBuildModelInstruction
import app.revanced.util.containsWideLiteralInstructionValue
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
internal object CreatePlayerRequestBodyWithModelFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf(),
customFingerprint = { methodDef, _ ->
methodDef.containsWideLiteralInstructionValue(1073741824) &&
indexOfBuildModelInstruction(methodDef) >= 0
},
) {
fun indexOfBuildModelInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
val reference = getReference<FieldReference>()
reference?.definingClass == "Landroid/os/Build;" &&
reference.name == "MODEL" &&
reference.type == "Ljava/lang/String;"
}
}

View File

@@ -5,6 +5,7 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object ParamsMapPutFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -6,6 +6,7 @@ import app.revanced.util.containsWideLiteralInstructionValue
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object PlayerResponseModelImplGeneralFingerprint : MethodFingerprint(
returnType = "Ljava/lang/String;",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -6,6 +6,7 @@ import app.revanced.util.containsWideLiteralInstructionValue
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object PlayerResponseModelImplLiveStreamFingerprint : MethodFingerprint(
returnType = "Ljava/lang/String;",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -6,6 +6,7 @@ import app.revanced.util.containsWideLiteralInstructionValue
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object PlayerResponseModelImplRecommendedLevelFingerprint : MethodFingerprint(
returnType = "I",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -1,27 +0,0 @@
package app.revanced.patches.youtube.misc.fix.playback.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patches.youtube.misc.fix.playback.SpoofClientResourcePatch
import app.revanced.util.patch.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal object ScrubbedPreviewLayoutFingerprint : LiteralValueFingerprint(
accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
returnType = "V",
parameters = listOf("Landroid/content/Context;", "Landroid/util/AttributeSet;", "I", "I"),
opcodes = listOf(
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT,
Opcode.INVOKE_VIRTUAL,
Opcode.CONST,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.CHECK_CAST,
Opcode.IPUT_OBJECT, // preview imageview
),
// This resource is used in ~ 40 different locations, but this method has a distinct list of parameters to match to.
literalSupplier = { SpoofClientResourcePatch.scrubbedPreviewThumbnailResourceId },
)

View File

@@ -1,12 +1,13 @@
package app.revanced.patches.youtube.misc.fix.playback.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.util.patch.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal object SetPlayerRequestClientTypeFingerprint : MethodFingerprint(
strings = listOf("10.29"),
internal object SetPlayerRequestClientTypeFingerprint : LiteralValueFingerprint(
opcodes = listOf(
Opcode.IGET,
Opcode.IPUT, // Sets ClientInfo.clientId.
),
strings = listOf("10.29"),
literalSupplier = { 134217728 }
)

View File

@@ -6,6 +6,7 @@ import app.revanced.util.patch.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object SpoofSignaturePatchScrubbedPreviewLayoutFingerprint : LiteralValueFingerprint(
accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
returnType = "V",

View File

@@ -2,6 +2,7 @@ package app.revanced.patches.youtube.misc.fix.playback.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StatsQueryParameterFingerprint : MethodFingerprint(
strings = listOf("adunit"),
)

View File

@@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.Opcode
/**
* Resolves to the same method as [StoryboardRendererDecoderSpecFingerprint].
*/
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StoryboardRendererDecoderRecommendedLevelFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.Opcode
/**
* Resolves to the same method as [StoryboardRendererDecoderRecommendedLevelFingerprint].
*/
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StoryboardRendererDecoderSpecFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,

View File

@@ -4,6 +4,7 @@ import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StoryboardRendererSpecFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
returnType = "L",

View File

@@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.Opcode
/**
* Resolves using the class found in [StoryboardThumbnailParentFingerprint].
*/
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StoryboardThumbnailFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "Z",

View File

@@ -10,6 +10,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
* An additional change here might force the thumbnails to be created,
* or possibly a change somewhere else (maybe involving YouTube 18.23.35 class `hte`)
*/
@Deprecated("Fingerprint is obsolete and will be deleted soon")
internal object StoryboardThumbnailParentFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "Landroid/graphics/Bitmap;",

View File

@@ -1092,11 +1092,9 @@
<string name="revanced_spoof_client_summary_on">Client is spoofed</string>
<string name="revanced_spoof_client_summary_off">Client is not spoofed\n\nVideo playback may not work</string>
<string name="revanced_spoof_client_user_dialog_message">Turning off this setting may cause video playback issues.</string>
<!-- It is ideal, but not required, if the text here appears is alphabetically after the text used for 'revanced_client_spoof_title' -->
<string name="revanced_spoof_client_use_ios_title">Spoof client to iOS</string>
<string name="revanced_spoof_client_use_ios_summary">Spoof the client to iOS instead of an Android Testsuite</string>
<string name="revanced_spoof_client_use_ios_summary_on">Client is spoofed to an iOS client\n\nSide effects include:\n• 60 fps video may not be available\n• No HDR video\n• Some videos may not load\n• Higher video qualities may be missing</string>
<string name="revanced_spoof_client_use_ios_summary_off">Client is spoofed to an Android Testsuite client (iOS client is used for live streams)\n\nSide effects include:\n• Subtitles are missing\n• Player gestures may not work\n• Low quality Shorts seekbar thumbnails</string>
<string name="revanced_spoof_client_use_ios_summary_on">Client is currently spoofed to iOS\n\nSide effects include:\n• No HDR video\n• Speed menu is missing\n• Watch history may not work\n• Live streams cannot play as audio only\n• Live streams not available on older devices</string>
<string name="revanced_spoof_client_use_ios_summary_off">Client is currently spoofed to Android VR\n\nSide effects include:\n• No HDR video\n• Player swipe gestures do not work\n• Kids videos do not playback\n• Paused videos can randomly resume</string>
<string name="revanced_spoof_client_storyboard_timeout">Spoof client thumbnails not available (API timed out)</string>
<string name="revanced_spoof_client_storyboard_io_exception">Spoof client thumbnails temporarily not available: %s</string>
</patch>