Skip to content

Commit

Permalink
fix(YouTube - Change live ring click action): Channel does not open w…
Browse files Browse the repository at this point in the history
…hen the live ring of Shorts live stream is clicked
  • Loading branch information
inotia00 authored and anddea committed Jan 30, 2025
1 parent 5862325 commit dd6baa4
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,71 +16,80 @@ 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);
}

public static void hideEngagementPanel() {
engagementPanelOpen.compareAndSet(true, false);
/**
* 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 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);
return true;
}
}
} 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@ 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
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;"
Expand All @@ -35,21 +42,19 @@ 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)
var freeRegister = getInstruction<OneRegisterInstruction>(freeIndex).registerA

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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -894,15 +896,15 @@ val shortsComponentPatch = bytecodePatch(
"""

if (is_19_25_or_greater) {
shortsPlaybackIntentFingerprint.methodOrThrow().addInstructionsWithLabels(
shortsPlaybackStartIntentFingerprint.methodOrThrow().addInstructionsWithLabels(
0,
"""
move-object/from16 v0, p1
${extensionInstructions(0, 1)}
"""
)
} else {
shortsPlaybackIntentLegacyFingerprint.methodOrThrow().apply {
shortsPlaybackStartIntentLegacyFingerprint.methodOrThrow().apply {
val index = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
}
Expand Down
Loading

0 comments on commit dd6baa4

Please sign in to comment.