fix(YouTube - Change live ring click action): Channel does not open when the live ring of Shorts live stream is clicked

This commit is contained in:
inotia00 2025-01-29 19:25:07 +09:00
parent 21b8b48e47
commit 1151a9a5be
8 changed files with 197 additions and 88 deletions

View File

@ -1,9 +1,10 @@
package app.revanced.extension.youtube.patches.general;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import com.facebook.litho.ComponentHost;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.patches.general.requests.VideoDetailsRequest;
@ -15,37 +16,70 @@ public final class OpenChannelOfLiveAvatarPatch {
private static final boolean CHANGE_LIVE_RING_CLICK_ACTION =
Settings.CHANGE_LIVE_RING_CLICK_ACTION.get();
private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false);
private static volatile String videoId = "";
/**
* This key's value is the LithoView that opened the video (Live ring or Thumbnails).
*/
private static final String ELEMENTS_SENDER_VIEW =
"com.google.android.libraries.youtube.rendering.elements.sender_view";
/**
* If the video is open by clicking live ring, this key does not exists.
*/
private static final String VIDEO_THUMBNAIL_VIEW_KEY =
"VideoPresenterConstants.VIDEO_THUMBNAIL_VIEW_KEY";
public static void showEngagementPanel(@Nullable Object object) {
engagementPanelOpen.set(object != null);
/**
* Injection point.
*
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
* @param newlyLoadedVideoId id of the current video
*/
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
try {
if (!CHANGE_LIVE_RING_CLICK_ACTION) {
return;
}
// Video id is empty
if (newlyLoadedVideoId.isEmpty()) {
return;
}
// Video was opened by clicking the thumbnail
if (playbackStartDescriptorMap.containsKey(VIDEO_THUMBNAIL_VIEW_KEY)) {
return;
}
// If the video was opened in the watch history, there is no VIDEO_THUMBNAIL_VIEW_KEY
// In this case, check the view that opened the video (Live ring is litho)
if (!(playbackStartDescriptorMap.get(ELEMENTS_SENDER_VIEW) instanceof ComponentHost componentHost)) {
return;
}
// Child count of other litho Views such as Thumbnail and Watch history: 2
// Child count of live ring: 1
if (componentHost.getChildCount() != 1) {
return;
}
// Fetch channel id
videoId = newlyLoadedVideoId;
VideoDetailsRequest.fetchRequestIfNeeded(newlyLoadedVideoId);
} catch (Exception ex) {
Logger.printException(() -> "fetchVideoInformation failure", ex);
}
}
public static void hideEngagementPanel() {
engagementPanelOpen.compareAndSet(true, false);
}
public static boolean openChannelOfLiveAvatar() {
public static boolean openChannel() {
try {
if (!CHANGE_LIVE_RING_CLICK_ACTION) {
return false;
}
if (engagementPanelOpen.get()) {
return false;
}
// If it is not fetch, the video id is empty
if (videoId.isEmpty()) {
return false;
}
VideoDetailsRequest request = VideoDetailsRequest.getRequestForVideoId(videoId);
if (request != null) {
String channelId = request.getInfo();
// Open the channel
if (channelId != null) {
videoId = "";
VideoUtils.openChannel(channelId);
@ -53,33 +87,9 @@ public final class OpenChannelOfLiveAvatarPatch {
}
}
} catch (Exception ex) {
Logger.printException(() -> "openChannelOfLiveAvatar failure", ex);
Logger.printException(() -> "openChannel failure", ex);
}
return false;
}
public static void openChannelOfLiveAvatar(Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
try {
if (!CHANGE_LIVE_RING_CLICK_ACTION) {
return;
}
if (playbackStartDescriptorMap == null) {
return;
}
if (playbackStartDescriptorMap.containsKey(VIDEO_THUMBNAIL_VIEW_KEY)) {
return;
}
if (engagementPanelOpen.get()) {
return;
}
if (newlyLoadedVideoId.isEmpty()) {
return;
}
videoId = newlyLoadedVideoId;
VideoDetailsRequest.fetchRequestIfNeeded(newlyLoadedVideoId);
} catch (Exception ex) {
Logger.printException(() -> "openChannelOfLiveAvatar failure", ex);
}
}
}

View File

@ -0,0 +1,19 @@
package com.facebook.litho;
import android.content.Context;
import android.view.ViewGroup;
/**
* "CompileOnly" class
* <p>
* This class will not be included and "replaced" by the real package's class.
*/
public class ComponentHost extends ViewGroup {
public ComponentHost(Context context) {
super(context);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}

View File

@ -4,15 +4,19 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.engagement.engagementPanelHookPatch
import app.revanced.patches.youtube.utils.engagement.hookEngagementPanelState
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LIVE_RING_CLICK_ACTION
import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.patches.youtube.video.playbackstart.PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.video.playbackstart.playbackStartDescriptorPatch
import app.revanced.patches.youtube.video.playbackstart.playbackStartVideoIdReference
import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentFingerprint
import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentLegacyFingerprint
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
@ -20,7 +24,10 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"$GENERAL_PATH/OpenChannelOfLiveAvatarPatch;"
@ -35,13 +42,11 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
dependsOn(
settingsPatch,
playbackStartDescriptorPatch,
engagementPanelHookPatch,
versionCheckPatch,
)
execute {
hookEngagementPanelState(EXTENSION_CLASS_DESCRIPTOR)
clientSettingEndpointFingerprint.methodOrThrow().apply {
val eqzIndex = indexOfFirstInstructionReversedOrThrow(Opcode.IF_EQZ)
var freeIndex = indexOfFirstInstructionReversedOrThrow(eqzIndex, Opcode.NEW_INSTANCE)
@ -49,7 +54,7 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
addInstructionsWithLabels(
eqzIndex, """
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannelOfLiveAvatar()Z
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannel()Z
move-result v$freeRegister
if-eqz v$freeRegister, :ignore
return-void
@ -76,11 +81,82 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
playbackStartIndex + 1, """
invoke-virtual { v$playbackStartRegister }, $playbackStartVideoIdReference
move-result-object v$freeRegister
invoke-static { v$mapRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->openChannelOfLiveAvatar(Ljava/util/Map;Ljava/lang/String;)V
invoke-static { v$mapRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchChannelId(Ljava/util/Map;Ljava/lang/String;)V
"""
)
}
fun MutableMethod.openChannel() =
implementation!!.instructions
.withIndex()
.filter { (_, instruction) ->
val reference = (instruction as? ReferenceInstruction)?.reference
instruction.opcode == Opcode.NEW_INSTANCE &&
reference is TypeReference &&
reference.type == "Landroid/os/Bundle;"
}
.map { (index, _) -> index }
.reversed()
.forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructionsWithLabels(
index, """
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannel()Z
move-result v$register
if-eqz v$register, :ignore
return-void
:ignore
nop
"""
)
}
fun fetchChannelIdInstructions(
playbackStartRegister: Int,
mapRegister: Int,
videoIdRegister: Int,
) =
"""
invoke-virtual { v$playbackStartRegister }, $playbackStartVideoIdReference
move-result-object v$videoIdRegister
invoke-static { v$mapRegister, v$videoIdRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchChannelId(Ljava/util/Map;Ljava/lang/String;)V
"""
if (is_19_25_or_greater) {
shortsPlaybackStartIntentFingerprint.methodOrThrow().apply {
openChannel()
addInstructionsWithLabels(
0, """
move-object/from16 v0, p1
move-object/from16 v1, p2
${fetchChannelIdInstructions(0, 1, 2)}
"""
)
}
} else {
shortsPlaybackStartIntentLegacyFingerprint.methodOrThrow().apply {
openChannel()
val playbackStartIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
}
val mapIndex = indexOfFirstInstructionReversedOrThrow(playbackStartIndex, Opcode.IPUT)
val mapRegister = getInstruction<TwoRegisterInstruction>(mapIndex).registerA
val playbackStartRegister = getInstruction<OneRegisterInstruction>(playbackStartIndex + 1).registerA
val videoIdRegister = getInstruction<FiveRegisterInstruction>(playbackStartIndex).registerC
addInstructionsWithLabels(
playbackStartIndex + 2, """
move-object/from16 v$mapRegister, p2
${fetchChannelIdInstructions(playbackStartRegister, mapRegister, videoIdRegister)}
"""
)
}
}
// region add settings
addPreference(

View File

@ -187,41 +187,3 @@ internal val shortsFullscreenFeatureFingerprint = legacyFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
literals = listOf(FULLSCREEN_FEATURE_FLAG),
)
// Pre 19.25
internal val shortsPlaybackIntentLegacyFingerprint = legacyFingerprint(
name = "shortsPlaybackIntentLegacyFingerprint",
returnType = "V",
parameters = listOf(
"L",
"Ljava/util/Map;",
"J",
"Ljava/lang/String;",
"Z",
"Ljava/util/Map;"
),
strings = listOf(
// None of these strings are unique.
"com.google.android.apps.youtube.app.endpoint.flags",
"ReelWatchFragmentArgs",
"reels_fragment_descriptor"
)
)
internal val shortsPlaybackIntentFingerprint = legacyFingerprint(
name = "shortsPlaybackIntentFingerprint",
accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL,
returnType = "V",
parameters = listOf(
"Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;",
"Ljava/util/Map;",
"J",
"Ljava/lang/String;"
),
strings = listOf(
// None of these strings are unique.
"com.google.android.apps.youtube.app.endpoint.flags",
"ReelWatchFragmentArgs",
"reels_fragment_descriptor"
)
)

View File

@ -65,6 +65,8 @@ import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.patches.youtube.video.playbackstart.PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.video.playbackstart.playbackStartDescriptorPatch
import app.revanced.patches.youtube.video.playbackstart.playbackStartVideoIdReference
import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentFingerprint
import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentLegacyFingerprint
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
import app.revanced.patches.youtube.video.videoid.videoIdPatch
import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT
@ -894,7 +896,7 @@ val shortsComponentPatch = bytecodePatch(
"""
if (is_19_25_or_greater) {
shortsPlaybackIntentFingerprint.methodOrThrow().addInstructionsWithLabels(
shortsPlaybackStartIntentFingerprint.methodOrThrow().addInstructionsWithLabels(
0,
"""
move-object/from16 v0, p1
@ -902,7 +904,7 @@ val shortsComponentPatch = bytecodePatch(
"""
)
} else {
shortsPlaybackIntentLegacyFingerprint.methodOrThrow().apply {
shortsPlaybackStartIntentLegacyFingerprint.methodOrThrow().apply {
val index = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
}

View File

@ -1,6 +1,8 @@
package app.revanced.patches.youtube.video.playbackstart
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
const val PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR =
"Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;"
@ -16,4 +18,42 @@ internal val playbackStartFeatureFlagFingerprint = legacyFingerprint(
literals = listOf(45380134L)
)
internal val shortsPlaybackStartIntentFingerprint = legacyFingerprint(
name = "shortsPlaybackStartIntentFingerprint",
accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL,
returnType = "V",
parameters = listOf(
"Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;",
"Ljava/util/Map;",
"J",
"Ljava/lang/String;"
),
strings = listOf(
// None of these strings are unique.
"com.google.android.apps.youtube.app.endpoint.flags",
"ReelWatchFragmentArgs",
"reels_fragment_descriptor"
)
)
// Pre 19.25
internal val shortsPlaybackStartIntentLegacyFingerprint = legacyFingerprint(
name = "shortsPlaybackStartIntentLegacyFingerprint",
returnType = "V",
parameters = listOf(
"L",
"Ljava/util/Map;",
"J",
"Ljava/lang/String;",
"Z",
"Ljava/util/Map;"
),
strings = listOf(
// None of these strings are unique.
"com.google.android.apps.youtube.app.endpoint.flags",
"ReelWatchFragmentArgs",
"reels_fragment_descriptor"
)
)

View File

@ -2,7 +2,7 @@ package app.revanced.patches.youtube.video.playbackstart
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
@ -15,7 +15,7 @@ internal lateinit var playbackStartVideoIdReference: Reference
val playbackStartDescriptorPatch = bytecodePatch(
description = "playbackStartDescriptorPatch"
) {
dependsOn(sharedResourceIdPatch)
dependsOn(sharedExtensionPatch)
execute {
// Find the obfuscated method name for PlaybackStartDescriptor.videoId()

View File

@ -451,7 +451,7 @@ This does not bypass the age restriction. It just accepts it automatically."</st
<string name="revanced_change_live_ring_click_action_title">Change live ring click action</string>
<string name="revanced_change_live_ring_click_action_summary_on">"Channel opens when the live ring is clicked.
Limitation: Live ring of the Vertical live streams (Shorts live streams) cannot open a channel."</string>
Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open."</string>
<string name="revanced_change_live_ring_click_action_summary_off">Live stream opens when the live ring is clicked.</string>
<string name="revanced_spoof_app_version_title">Spoof app version</string>
<string name="revanced_spoof_app_version_summary_on">Version spoofed</string>