feat(YouTube - Voice Over Translation): Volume patch fixes, audio proxy, immediate pause on video stop (#1382)

Co-authored-by: Aaron Veil <70171475+anddea@users.noreply.github.com>
This commit is contained in:
Jav1x 2026-02-19 16:22:53 +03:00 committed by GitHub
parent 017e40e298
commit 6ac278cc65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1652 additions and 144 deletions

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2025 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/anddea.
* Original author(s):
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/anddea.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.overlaybutton

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s):
* - Jav1x (https://github.com/Jav1x)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.overlaybutton
@ -19,7 +46,9 @@ import app.morphe.extension.youtube.patches.utils.PatchStatus
import app.morphe.extension.youtube.patches.voiceovertranslation.VoiceOverTranslationPatch
import app.morphe.extension.youtube.settings.Settings
import app.morphe.extension.youtube.shared.PlayerControlButton
import app.morphe.extension.youtube.shared.RootView
import app.morphe.extension.youtube.shared.RootView.isAdProgressTextVisible
import app.morphe.extension.youtube.utils.VideoUtils
@Suppress("DEPRECATION", "unused")
object VoiceOverTranslationButton {
@ -31,11 +60,13 @@ object VoiceOverTranslationButton {
@JvmStatic
fun initializeButton(controlsView: View) {
try {
VoiceOverTranslationPatch.setOnTranslationStateChangeCallback { refreshActivatedState() }
instance = PlayerControlButton(
controlsViewGroup = controlsView,
imageViewButtonId = "revanced_vot_button",
buttonVisibility = { isButtonEnabled() },
onClickListener = { view: View -> onClick(view) },
onLongClickListener = { view: View -> onLongClick(view) },
)
} catch (ex: Exception) {
Logger.printException({ "VoiceOverTranslationButton initializeButton failure" }, ex)
@ -79,6 +110,16 @@ object VoiceOverTranslationButton {
instance?.imageView()?.isActivated = VoiceOverTranslationPatch.isTranslationActive()
}
private fun onLongClick(view: View): Boolean {
val context = RootView.getContext() ?: return false
VideoUtils.showVotBottomSheetDialog(context)
return true
}
private fun refreshActivatedState() {
instance?.setActivated()
}
private fun PlayerControlButton.setActivated() {
imageView()?.isActivated = VoiceOverTranslationPatch.isTranslationActive()
}

View file

@ -1,29 +1,59 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s) (based on contributions):
* - Jav1x (https://github.com/Jav1x)
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.voiceovertranslation;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.PlaybackParams;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@ -50,6 +80,10 @@ public class VoiceOverTranslationPatch {
private static final int STATUS_PART_CONTENT = 5;
private static final int STATUS_AUDIO_REQUESTED = 6;
private static final long PAUSE_DETECTION_TIMEOUT_MS = 1500;
private static final long PROXY_PREPARE_TIMEOUT_MS = 15000;
private static final String PROXY_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private static final AtomicReference<MediaPlayer> mediaPlayer = new AtomicReference<>(null);
@ -57,28 +91,48 @@ public class VoiceOverTranslationPatch {
private static final AtomicReference<String> currentTranslatedVideoId = new AtomicReference<>("");
private static volatile boolean isPaused = false;
private static float lastAppliedPlaybackSpeed = 1.0f;
private static Object audioFocusRequest;
private static volatile long lastVideoTimeMs = -1;
private static final long SEEK_DRIFT_THRESHOLD_MS = 20000;
private static final long USER_SEEK_JUMP_MS = 3000;
private static final Runnable pauseCheckRunnable = () -> {
if (!isPaused) {
pauseAudio();
}
};
private static Runnable proxyPrepareTimeoutRunnable = () -> {};
private static Runnable onTranslationStateChangeCallback;
public static void setOnTranslationStateChangeCallback(Runnable r) {
onTranslationStateChangeCallback = r;
}
private static void notifyTranslationStateChanged() {
if (onTranslationStateChangeCallback != null) {
mainHandler.post(onTranslationStateChangeCallback);
}
}
private static volatile String tempProxyFile = null;
private static volatile String pendingVideoId = "";
private static volatile String pendingVideoTitle = "";
private static volatile long pendingVideoLength = 0L;
private static volatile boolean pendingIsLive = false;
/** True when user started translation and original audio should be ducked before translated audio starts. */
public static volatile boolean translationStarting = false;
public static void initialize() {
VideoState.addOnPlayingListener(() -> mainHandler.post(() -> {
if (VideoState.getCurrent() != VideoState.PLAYING) return;
resumeAudio(-1);
}));
VideoState.addOnNotPlayingListener(() -> mainHandler.post(() -> {
mainHandler.removeCallbacks(pauseCheckRunnable);
pauseAudio();
}));
VideoInformation.addOnPlaybackSpeedChangeListener(() -> mainHandler.post(() -> {
if (VideoState.getCurrent() != VideoState.PLAYING) return;
MediaPlayer p = mediaPlayer.get();
@ -93,6 +147,9 @@ public class VoiceOverTranslationPatch {
) {
if (!Settings.VOT_ENABLED.get()) return;
String newId = videoId != null ? videoId : "";
if (!newId.equals(pendingVideoId)) {
translationStarting = false;
}
if (!newId.equals(currentTranslatedVideoId.get())) {
stopAudioPlayback();
}
@ -107,8 +164,11 @@ public class VoiceOverTranslationPatch {
if (!Settings.VOT_ENABLED.get()) return;
if (isTranslationActive()) {
translationStarting = false;
stopAudioPlayback();
notifyTranslationStateChanged();
showToastShort(str("revanced_vot_stopped"));
refreshOriginalAudioVolume();
return;
}
@ -128,10 +188,14 @@ public class VoiceOverTranslationPatch {
}
if (pendingVideoId == null || pendingVideoId.isEmpty()) return;
double durationSeconds = pendingVideoLength / 1000.0;
final String videoId = pendingVideoId;
final String videoTitle = pendingVideoTitle;
final double durationSeconds = pendingVideoLength / 1000.0;
showToastShort(str("revanced_vot_started"));
translationStarting = true;
refreshOriginalAudioVolume();
Utils.runOnBackgroundThread(() -> requestTranslation(
pendingVideoId, pendingVideoTitle,
videoId, videoTitle,
sourceLang, targetLang,
durationSeconds
));
@ -143,6 +207,58 @@ public class VoiceOverTranslationPatch {
return currentTranslatedVideoId.get() != null && !currentTranslatedVideoId.get().isEmpty();
}
/**
* Re-applies the current player volume so VOT original-audio multiplier takes effect immediately
* without reloading the video.
*/
public static void refreshOriginalAudioVolumeIfActive() {
if (!Settings.VOT_ENABLED.get()) return;
if (!isTranslationActive() && !translationStarting) return;
refreshOriginalAudioVolume();
}
/**
* Forces the player to re-apply volume so AudioTrack.setVolume hook runs immediately.
*/
public static void refreshOriginalAudioVolume() {
if (VotOriginalVolumePatch.applyCurrentMultiplierNow()) return;
// Fallback path if no AudioTrack has been captured yet.
float currentVolume = VideoInformation.getPlayerVolume();
if (Float.isNaN(currentVolume)) currentVolume = 1.0f;
if (currentVolume < 0f) currentVolume = 0f;
if (currentVolume > 1f) currentVolume = 1f;
float nudgedVolume = currentVolume >= 0.99f
? Math.max(0f, currentVolume - 0.01f)
: Math.min(1f, currentVolume + 0.01f);
VideoInformation.setPlayerVolume(nudgedVolume);
VideoInformation.setPlayerVolume(currentVolume);
}
/**
* Stops current translation and restarts it (e.g. when audio proxy setting changes).
* No-op if translation is not active.
*/
public static void restartTranslationIfActive() {
if (!Settings.VOT_ENABLED.get()) return;
if (!isTranslationActive()) return;
String videoId = currentTranslatedVideoId.get();
if (videoId == null || videoId.isEmpty()) return;
if (pendingIsLive) return;
if (pendingVideoLength > 4 * 60 * 60 * 1000L) return;
String sourceLang = Settings.VOT_SOURCE_LANGUAGE.get();
String targetLang = Settings.VOT_TARGET_LANGUAGE.get();
if (!sourceLang.isEmpty() && !"auto".equalsIgnoreCase(sourceLang) && sourceLang.equals(targetLang)) return;
stopAudioPlayback();
double durationSeconds = pendingVideoLength / 1000.0;
Utils.runOnBackgroundThread(() -> requestTranslation(
videoId, pendingVideoTitle,
sourceLang, targetLang,
durationSeconds
));
}
public static void setVideoTime(long videoTimeMillis) {
if (!Settings.VOT_ENABLED.get()) return;
if (isPaused) {
@ -172,6 +288,14 @@ public class VoiceOverTranslationPatch {
});
}
static String formatRemainingTime(int seconds) {
if (seconds < 60) {
return str("revanced_vot_time_sec", Math.max(1, seconds));
}
int minutes = (seconds + 30) / 60;
return str("revanced_vot_time_min", minutes);
}
private static void requestTranslation(
String videoId, String videoTitle,
String sourceLang, String targetLang,
@ -182,19 +306,45 @@ public class VoiceOverTranslationPatch {
String youtubeUrl = "https://youtu.be/" + videoId;
VotApiClient.TranslationResult result = VotApiClient.requestTranslation(
youtubeUrl, durationSeconds, sourceLang, targetLang, videoTitle);
if (result == null) return;
if (result == null) {
if (Settings.VOT_USE_LIVE_VOICES.get()) {
Settings.VOT_USE_LIVE_VOICES.save(false);
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_live_voices_unavailable")));
isTranslating.set(false);
requestTranslation(videoId, videoTitle, sourceLang, targetLang, durationSeconds);
return;
}
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_playback_error"));
});
return;
}
switch (result.status()) {
case STATUS_FINISHED:
case STATUS_PART_CONTENT:
if (result.audioUrl() != null && !result.audioUrl().isEmpty()) {
final String url = result.audioUrl();
Utils.runOnMainThread(() -> startAudioPlayback(videoId, url));
String directUrl = result.audioUrl();
String url = directUrl;
String fallback = null;
if (Settings.VOT_AUDIO_PROXY_ENABLED.get()) {
url = VotApiClient.toProxyAudioUrl(directUrl);
fallback = directUrl;
}
final String urlFinal = url;
final String fallbackFinal = fallback;
Utils.runOnMainThread(() -> startAudioPlayback(videoId, urlFinal, fallbackFinal));
} else {
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_playback_error"));
});
}
break;
case STATUS_WAITING:
case STATUS_LONG_WAITING:
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_stream_waiting")));
int waitTime = result.remainingTime() > 0 ? result.remainingTime() : 5;
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_stream_waiting", formatRemainingTime(waitTime))));
pollTranslation(videoId, videoTitle, youtubeUrl, durationSeconds, sourceLang, targetLang, waitTime);
break;
case STATUS_AUDIO_REQUESTED:
@ -202,10 +352,25 @@ public class VoiceOverTranslationPatch {
break;
case STATUS_FAILED:
default:
if (Settings.VOT_USE_LIVE_VOICES.get()) {
Settings.VOT_USE_LIVE_VOICES.save(false);
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_live_voices_unavailable")));
isTranslating.set(false);
requestTranslation(videoId, videoTitle, sourceLang, targetLang, durationSeconds);
return;
}
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_playback_error"));
});
break;
}
} catch (Exception e) {
Logger.printException(() -> "requestTranslation failed", e);
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_playback_error"));
});
} finally {
isTranslating.set(false);
}
@ -233,10 +398,32 @@ public class VoiceOverTranslationPatch {
if (result == null) continue;
if (result.status() == STATUS_FINISHED || result.status() == STATUS_PART_CONTENT) {
if (result.audioUrl() != null && !result.audioUrl().isEmpty()) {
Utils.runOnMainThread(() -> startAudioPlayback(videoId, result.audioUrl()));
String directUrl = result.audioUrl();
String audioUrl = directUrl;
String fallback = null;
if (Settings.VOT_AUDIO_PROXY_ENABLED.get()) {
audioUrl = VotApiClient.toProxyAudioUrl(directUrl);
fallback = directUrl;
}
final String audioUrlFinal = audioUrl;
final String fallbackFinal = fallback;
Utils.runOnMainThread(() -> startAudioPlayback(videoId, audioUrlFinal, fallbackFinal));
return;
}
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_playback_error")));
return;
} else if (result.status() == STATUS_FAILED) {
if (Settings.VOT_USE_LIVE_VOICES.get()) {
Settings.VOT_USE_LIVE_VOICES.save(false);
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_live_voices_unavailable")));
isTranslating.set(false);
requestTranslation(videoId, videoTitle, sourceLang, targetLang, duration);
return;
}
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_playback_error"));
});
return;
}
waitSeconds = result.remainingTime() > 0 ? result.remainingTime() : 5;
@ -244,6 +431,10 @@ public class VoiceOverTranslationPatch {
Logger.printException(() -> "pollTranslation failure", e);
}
}
Utils.runOnMainThread(() -> {
translationStarting = false;
showToastShort(str("revanced_vot_stream_not_ready"));
});
}
private static void handleAudioRequested(
@ -258,19 +449,182 @@ public class VoiceOverTranslationPatch {
VotApiClient.TranslationResult result = VotApiClient.requestTranslation(
url, duration, sourceLang, targetLang, videoTitle);
if (result != null && (result.status() == STATUS_WAITING || result.status() == STATUS_LONG_WAITING)) {
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_stream_waiting")));
int waitTime = result.remainingTime() > 0 ? result.remainingTime() : 10;
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_stream_waiting", formatRemainingTime(waitTime))));
pollTranslation(videoId, videoTitle, url, duration, sourceLang, targetLang, waitTime);
} else if (result != null && (result.status() == STATUS_FINISHED || result.status() == STATUS_PART_CONTENT) && result.audioUrl() != null) {
Utils.runOnMainThread(() -> startAudioPlayback(videoId, result.audioUrl()));
String directUrl = result.audioUrl();
String audioUrl = directUrl;
String fallback = null;
if (Settings.VOT_AUDIO_PROXY_ENABLED.get()) {
audioUrl = VotApiClient.toProxyAudioUrl(directUrl);
fallback = directUrl;
}
final String audioUrlFinal = audioUrl;
final String fallbackFinal = fallback;
Utils.runOnMainThread(() -> startAudioPlayback(videoId, audioUrlFinal, fallbackFinal));
}
} catch (Exception e) {
Logger.printException(() -> "handleAudioRequested failed", e);
Utils.runOnMainThread(() -> showToastShort(str("revanced_vot_playback_error")));
}
}
private static void startAudioPlayback(String videoId, String audioUrl) {
private static void startAudioPlayback(String videoId, String audioUrl, String fallbackUrl) {
stopAudioPlayback();
mainHandler.removeCallbacks(proxyPrepareTimeoutRunnable);
if (audioUrl.contains("/audio-proxy/")) {
Context ctx = RootView.getContext();
if (ctx == null) {
if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
startAudioPlayback(videoId, fallbackUrl, null);
} else {
showToastShort(str("revanced_vot_playback_error"));
}
return;
}
final Context ctxFinal = ctx;
Utils.runOnBackgroundThread(() -> {
String localPath = fetchProxyAudioToTemp(audioUrl, ctxFinal);
Utils.runOnMainThread(() -> {
if (localPath != null) {
startAudioPlaybackFromFile(videoId, localPath);
} else if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
startAudioPlayback(videoId, fallbackUrl, null);
} else {
showToastShort(str("revanced_vot_playback_error"));
}
});
});
return;
}
startAudioPlaybackDirect(videoId, audioUrl, fallbackUrl);
}
private static String fetchProxyAudioToTemp(String proxyUrl, Context ctx) {
String urlToFetch = proxyUrl;
int maxRedirects = 5;
for (int redirect = 0; redirect < maxRedirects; redirect++) {
HttpURLConnection conn = null;
FileOutputStream fos = null;
try {
URL url = new URL(urlToFetch);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=0-");
conn.setRequestProperty("User-Agent", PROXY_USER_AGENT);
conn.setRequestProperty("Accept", "*/*");
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
conn.setInstanceFollowRedirects(false);
conn.connect();
int code = conn.getResponseCode();
if (code == 301 || code == 302 || code == 307 || code == 308) {
String location = conn.getHeaderField("Location");
conn.disconnect();
if (location != null && !location.isEmpty()) {
urlToFetch = location.startsWith("http") ? location : url.getProtocol() + "://" + url.getHost() + location;
continue;
}
return null;
}
if (code != 200 && code != 206) return null;
File cacheDir = ctx.getCacheDir();
File tempFile = File.createTempFile("vot_proxy_", ".mp3", cacheDir);
long totalBytes = 0;
try (InputStream is = conn.getInputStream()) {
fos = new FileOutputStream(tempFile);
byte[] buf = new byte[8192];
int n;
while ((n = is.read(buf)) > 0) {
fos.write(buf, 0, n);
totalBytes += n;
}
}
try {
fos.close();
} catch (IOException ignored) {}
final long bytes = totalBytes;
if (bytes < 1000) {
boolean deleted = tempFile.delete();
if (!deleted) {
Logger.printDebug(() -> "VOT temp proxy file could not be deleted: " + tempFile.getAbsolutePath());
}
return null;
}
return tempFile.getAbsolutePath();
} catch (Exception e) {
Logger.printException(() -> "VOT proxy fetch failed", e);
return null;
} finally {
if (fos != null) {
try { fos.close(); } catch (IOException ignored) { }
}
if (conn != null) conn.disconnect();
}
}
return null;
}
private static void startAudioPlaybackFromFile(String videoId, String filePath) {
stopAudioPlayback();
tempProxyFile = filePath;
try {
MediaPlayer mp = new MediaPlayer();
mp.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build());
mp.setDataSource(filePath);
mp.setOnPreparedListener(player -> Utils.runOnMainThread(() -> {
translationStarting = false;
float vol = Settings.VOT_TRANSLATION_VOLUME.get() / 100.0f;
player.setVolume(vol, vol);
long videoTime = VideoInformation.getVideoTime();
if (videoTime > 0) player.seekTo((int) videoTime);
if (VideoState.getCurrent() == VideoState.PLAYING) {
applyPlaybackSpeedToPlayer(player);
player.start();
} else {
isPaused = true;
}
}));
mp.setOnErrorListener((p, what, extra) -> {
Logger.printDebug(() -> "VOT MediaPlayer error: what=" + what + " extra=" + extra);
Utils.runOnMainThread(() -> {
stopAudioPlayback();
showToastShort(str("revanced_vot_playback_error"));
});
return true;
});
mp.setOnCompletionListener(p -> deleteTempProxyFile());
mediaPlayer.set(mp);
currentTranslatedVideoId.set(videoId != null ? videoId : "");
notifyTranslationStateChanged();
mp.prepareAsync();
} catch (IOException e) {
Logger.printException(() -> "startAudioPlaybackFromFile failed", e);
deleteTempProxyFile();
showToastShort(str("revanced_vot_playback_error"));
}
}
private static void deleteTempProxyFile() {
String path = tempProxyFile;
tempProxyFile = null;
if (path != null) {
try {
File file = new File(path);
boolean deleted = file.delete();
if (!deleted) {
Logger.printDebug(() -> "VOT temp proxy file could not be deleted: " + file.getAbsolutePath());
}
} catch (Exception ignored) { }
}
}
private static void startAudioPlaybackDirect(String videoId, String audioUrl, String fallbackUrl) {
try {
MediaPlayer mp = new MediaPlayer();
mp.setAudioAttributes(
@ -279,35 +633,67 @@ public class VoiceOverTranslationPatch {
.setUsage(AudioAttributes.USAGE_MEDIA)
.build());
mp.setDataSource(audioUrl);
mp.setOnPreparedListener(player -> {
Utils.runOnMainThread(VoiceOverTranslationPatch::requestAudioFocusForTranslation);
final String fallback = fallbackUrl;
mp.setOnPreparedListener(player -> Utils.runOnMainThread(() -> {
translationStarting = false;
mainHandler.removeCallbacks(proxyPrepareTimeoutRunnable);
float vol = Settings.VOT_TRANSLATION_VOLUME.get() / 100.0f;
player.setVolume(vol, vol);
long videoTime = VideoInformation.getVideoTime();
if (videoTime > 0) player.seekTo((int) videoTime);
// Only start playing if the video is actually playing.
// Otherwise, mark it as paused so it starts automatically when the user hits play.
if (VideoState.getCurrent() == VideoState.PLAYING) {
// applyPlaybackSpeedToPlayer uses setPlaybackParams, which may
// auto-start the player on some Android versions.
applyPlaybackSpeedToPlayer(player);
player.start();
} else {
isPaused = true;
}
}));
mp.setOnErrorListener((p, what, extra) -> {
Logger.printDebug(() -> "VOT MediaPlayer error: what=" + what + " extra=" + extra + " url=" + audioUrl);
Utils.runOnMainThread(() -> {
stopAudioPlayback();
if (fallback != null && !fallback.isEmpty()) {
startAudioPlayback(videoId, fallback, null);
} else {
showToastShort(str("revanced_vot_playback_error"));
}
});
return true;
});
mp.setOnErrorListener((p, what, extra) -> true);
mediaPlayer.set(mp);
currentTranslatedVideoId.set(videoId != null ? videoId : "");
notifyTranslationStateChanged();
if (fallback != null && !fallback.isEmpty()) {
proxyPrepareTimeoutRunnable = () -> {
MediaPlayer p = mediaPlayer.get();
if (p != null && p == mp && !p.isPlaying()) {
Logger.printDebug(() -> "VOT proxy prepare timeout, retrying direct");
Utils.runOnMainThread(() -> {
stopAudioPlayback();
startAudioPlayback(videoId, fallback, null);
});
}
};
mainHandler.postDelayed(proxyPrepareTimeoutRunnable, PROXY_PREPARE_TIMEOUT_MS);
}
mp.prepareAsync();
} catch (IOException e) {
Logger.printException(() -> "startAudioPlayback failed for videoId: " + videoId, e);
Utils.runOnMainThread(() -> {
if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
startAudioPlayback(videoId, fallbackUrl, null);
} else {
showToastShort(str("revanced_vot_playback_error"));
}
});
}
}
public static void stopAudioPlayback() {
mainHandler.removeCallbacks(pauseCheckRunnable);
mainHandler.removeCallbacks(proxyPrepareTimeoutRunnable);
deleteTempProxyFile();
MediaPlayer mp = mediaPlayer.getAndSet(null);
if (mp != null) {
try {
@ -316,55 +702,10 @@ public class VoiceOverTranslationPatch {
} catch (Exception ignored) { }
}
currentTranslatedVideoId.set("");
notifyTranslationStateChanged();
isPaused = false;
lastAppliedPlaybackSpeed = 1.0f;
lastVideoTimeMs = -1;
abandonAudioFocus();
}
private static void requestAudioFocusForTranslation() {
try {
Context ctx = RootView.getContext();
if (ctx == null) return;
AudioManager am = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
if (am == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioFocusRequest req = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
.build();
int result = am.requestAudioFocus(req);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusRequest = req;
} else {
Logger.printDebug(() -> "requestAudioFocus failed: " + result);
}
} else {
int result = am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Logger.printDebug(() -> "requestAudioFocus failed: " + result);
}
}
} catch (Exception e) {
Logger.printException(() -> "requestAudioFocusForTranslation failed", e);
}
}
private static void abandonAudioFocus() {
try {
Context ctx = RootView.getContext();
if (ctx == null) return;
AudioManager am = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
if (am == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && audioFocusRequest != null) {
am.abandonAudioFocusRequest((AudioFocusRequest) audioFocusRequest);
audioFocusRequest = null;
} else {
am.abandonAudioFocus(null);
}
} catch (Exception ignored) { }
}
public static void pauseAudio() {
@ -392,6 +733,19 @@ public class VoiceOverTranslationPatch {
} catch (Exception ignored) { }
}
/**
* Applies the current VOT_TRANSLATION_VOLUME setting to the MediaPlayer if translation is playing.
* Call this when the user changes the volume in the bottom sheet.
*/
public static void applyVolumeToCurrentPlayer() {
MediaPlayer mp = mediaPlayer.get();
if (mp == null) return;
float vol = Settings.VOT_TRANSLATION_VOLUME.get() / 100.0f;
try {
mp.setVolume(vol, vol);
} catch (Exception ignored) { }
}
private static void applyPlaybackSpeedToPlayer(MediaPlayer mp) {
if (mp == null) return;
float speed = VideoInformation.getPlaybackSpeedFromPlayer();

View file

@ -1,14 +1,42 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s) (based on contributions):
* - Jav1x (https://github.com/Jav1x)
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.voiceovertranslation;
@ -20,6 +48,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
@ -59,6 +89,58 @@ public class VotApiClient {
String translationId, String message) {
}
/**
* Converts a direct audio URL (S3/Yandex) to a proxied URL.
* Format: https://{proxyHost}/video-translation/audio-proxy/{path}?{query}
* Takes path and query from the original URL. The proxy fetches using its configured
* base URL + path with the given query (AWS signature params).
*
* @param originalUrl the original audio URL
* @return proxied URL, or originalUrl on error
*/
@NonNull
public static String toProxyAudioUrl(@NonNull String originalUrl) {
if (originalUrl.isEmpty()) {
return originalUrl;
}
String proxyHost = Settings.VOT_PROXY_URL.get();
if (proxyHost.isEmpty()) {
proxyHost = DEFAULT_WORKER_HOST;
}
proxyHost = proxyHost.replaceFirst("^https?://", "").replaceAll("/+$", "");
try {
URI uri = new URI(originalUrl);
String path = uri.getRawPath();
String query = uri.getRawQuery();
if (path == null || path.isEmpty()) {
return originalUrl;
}
String result = getString(path, proxyHost, query);
Logger.printDebug(() -> "toProxyAudioUrl: " + originalUrl + " -> " + result);
return result;
} catch (URISyntaxException e) {
Logger.printDebug(() -> "toProxyAudioUrl: invalid URL " + originalUrl);
return originalUrl;
}
}
@NonNull
private static String getString(String path, String proxyHost, String query) {
String pathTrimmed = path.replaceFirst("^/+", "");
int lastSlash = pathTrimmed.lastIndexOf('/');
if (lastSlash >= 0) {
pathTrimmed = pathTrimmed.substring(lastSlash + 1);
}
StringBuilder proxyUrl = new StringBuilder();
proxyUrl.append("https://").append(proxyHost);
proxyUrl.append("/video-translation/audio-proxy/");
proxyUrl.append(pathTrimmed);
if (query != null && !query.isEmpty()) {
proxyUrl.append("?").append(query);
}
return proxyUrl.toString();
}
public static TranslationResult requestTranslation(
String videoUrl, double duration,
String sourceLang, String targetLang,
@ -76,7 +158,8 @@ public class VotApiClient {
byte[] body = VotProtobuf.encodeTranslationRequest(
videoUrl, true, duration,
apiSourceLang, targetLang, videoTitle
apiSourceLang, targetLang, videoTitle,
Settings.VOT_USE_LIVE_VOICES.get()
);
String path = "/video-translation/translate";

View file

@ -0,0 +1,108 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* Original author(s) (based on contributions):
* - Jav1x (https://github.com/Jav1x)
* - anddea (https://github.com/anddea)
*
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.voiceovertranslation;
import android.media.AudioTrack;
import java.lang.ref.WeakReference;
import app.morphe.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class VotOriginalVolumePatch {
private static volatile WeakReference<AudioTrack> lastAudioTrackRef = new WeakReference<>(null);
private static volatile float lastBaseVolume = 1.0f;
private static float applyMultiplier(float volume) {
if (!VoiceOverTranslationPatch.isTranslationActive() && !VoiceOverTranslationPatch.translationStarting) {
return volume;
}
int percent = Settings.VOT_ORIGINAL_AUDIO_VOLUME.get();
float mult = percent / 100.0f;
float result = volume * mult;
if (Float.isNaN(result) || result < 0f) return 0f;
return Math.min(result, 1f);
}
/**
* Applies the VOT original volume multiplier to the given volume.
* Called from bytecode patch before AudioTrack.setVolume(F).
* Only when translation is actively playing, dims original audio so translation is audible.
* Clamps result to 0..1 and handles NaN.
*
* @param audioTrack current player audio track receiving setVolume
* @param volume original volume (0..1) from ExoPlayer
* @return volume * (VOT_ORIGINAL_AUDIO_VOLUME/100) when translation playing, else unchanged
*/
public static float applyVolumeMultiplier(AudioTrack audioTrack, float volume) {
if (audioTrack != null) {
lastAudioTrackRef = new WeakReference<>(audioTrack);
}
if (!Float.isNaN(volume)) {
if (volume < 0f) {
lastBaseVolume = 0f;
} else lastBaseVolume = Math.min(volume, 1f);
}
return applyMultiplier(volume);
}
/**
* Applies current VOT original-audio setting immediately to the last known AudioTrack.
*
* @return true if update was applied
*/
public static boolean applyCurrentMultiplierNow() {
AudioTrack audioTrack = lastAudioTrackRef.get();
if (audioTrack == null) return false;
float base = lastBaseVolume;
if (Float.isNaN(base)) base = 1.0f;
if (base < 0f) base = 0f;
if (base > 1f) base = 1f;
float adjusted = applyMultiplier(base);
try {
audioTrack.setVolume(adjusted);
return true;
} catch (Exception ignored) {
return false;
}
}
}

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s):
* - Jav1x (https://github.com/Jav1x)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.voiceovertranslation;
@ -59,11 +86,13 @@ public class VotProtobuf {
* responseLanguage = 14 (string)
* unknown2 = 15 (int32)
* unknown3 = 16 (int32)
* useLivelyVoice = 18 (bool) live voices from Yandex (more natural TTS)
* videoTitle = 19 (string)
*/
public static byte[] encodeTranslationRequest(
String url, boolean firstRequest, double duration,
String language, String responseLanguage, String videoTitle
String language, String responseLanguage, String videoTitle,
boolean useLiveVoices
) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
@ -77,6 +106,7 @@ public class VotProtobuf {
writeString(out, 14, responseLanguage);
writeInt32(out, 15, 1); // unknown2
writeInt32(out, 16, 2); // unknown3
writeBool(out, 18, useLiveVoices); // useLivelyVoice live voices
if (videoTitle != null && !videoTitle.isEmpty()) {
writeString(out, 19, videoTitle);
}

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s):
* - Jav1x (https://github.com/Jav1x)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.patches.voiceovertranslation;
@ -107,6 +134,11 @@ public final class VotStreamReplacer {
break;
}
if (result.status() == STATUS_FAILED) {
if (Settings.VOT_USE_LIVE_VOICES.get()) {
Settings.VOT_USE_LIVE_VOICES.save(false);
Utils.runOnMainThread(() -> Utils.showToastShort(str("revanced_vot_live_voices_unavailable")));
continue; // retry with standard TTS
}
if (hadWaiting) {
Utils.runOnMainThread(() -> Utils.showToastShort(str("revanced_vot_stream_not_ready")));
}
@ -115,7 +147,9 @@ public final class VotStreamReplacer {
if (result.status() == STATUS_WAITING || result.status() == STATUS_LONG_WAITING) {
hadWaiting = true;
if (retry == 0) {
Utils.runOnMainThread(() -> Utils.showToastShort(str("revanced_vot_stream_waiting")));
int waitSecs = result.remainingTime() > 0 ? result.remainingTime() : 5;
String timeStr = VoiceOverTranslationPatch.formatRemainingTime(waitSecs);
Utils.runOnMainThread(() -> Utils.showToastShort(str("revanced_vot_stream_waiting", timeStr)));
}
waitSeconds = result.remainingTime() > 0 ? result.remainingTime() : 5;
waitSeconds = Math.min(waitSeconds, (int) ((deadline - System.currentTimeMillis()) / 1000));
@ -146,9 +180,13 @@ public final class VotStreamReplacer {
return stream;
}
int replaced = 0;
String audioUrl = result.audioUrl();
if (Settings.VOT_AUDIO_PROXY_ENABLED.get()) {
audioUrl = VotApiClient.toProxyAudioUrl(audioUrl);
}
for (Object format : formatList) {
if (StreamingDataOuterClassUtils.isAudioOnlyFormat(format)) {
StreamingDataOuterClassUtils.setUrl(format, result.audioUrl());
StreamingDataOuterClassUtils.setUrl(format, audioUrl);
replaced++;
}
}

View file

@ -711,7 +711,10 @@ public class Settings extends BaseSettings {
public static final StringSetting VOT_SOURCE_LANGUAGE = new StringSetting("vot_source_language", "auto", parent(VOT_ENABLED));
public static final StringSetting VOT_TARGET_LANGUAGE = new StringSetting("vot_target_language", "ru", parent(VOT_ENABLED));
public static final IntegerSetting VOT_TRANSLATION_VOLUME = new IntegerSetting("vot_translation_volume", 100, parent(VOT_ENABLED));
public static final IntegerSetting VOT_ORIGINAL_AUDIO_VOLUME = new IntegerSetting("vot_original_audio_volume", 20, parent(VOT_ENABLED));
public static final BooleanSetting VOT_USE_LIVE_VOICES = new BooleanSetting("vot_use_live_voices", TRUE, parent(VOT_ENABLED));
public static final StringSetting VOT_PROXY_URL = new StringSetting("vot_proxy_url", "vot-worker.toil.cc", parent(VOT_ENABLED));
public static final BooleanSetting VOT_AUDIO_PROXY_ENABLED = new BooleanSetting("vot_audio_proxy_enabled", FALSE, parent(VOT_ENABLED));
// PreferenceScreen: SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);

View file

@ -1,3 +1,45 @@
/*
* Copyright (C) anddea
*
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* Original author(s) (alphabetical order):
* - anddea (https://github.com/anddea)
* - inotia00 (https://github.com/inotia00)
* - Jav1x (https://github.com/Jav1x)
*
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7(b) Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.shared;
import static app.morphe.extension.shared.utils.Utils.getFormattedTimeStamp;
@ -377,6 +419,28 @@ public final class VideoInformation {
return playerParameter; // Return the original value since we are observing and not modifying.
}
/**
* Listener invoked when a new player response is received (video metadata loaded).
* Called off the main thread. Used by VOT to start translation after video reload.
*/
@Nullable
private static volatile OnPlayerResponseReceivedListener onPlayerResponseReceivedListener;
/**
* Sets a one-shot listener for the next player response. Cleared after invocation or when set to null.
*/
public static void setOnPlayerResponseReceivedListener(@Nullable OnPlayerResponseReceivedListener listener) {
onPlayerResponseReceivedListener = listener;
}
/**
* Listener for when player response is received. Called off the main thread.
*/
@FunctionalInterface
public interface OnPlayerResponseReceivedListener {
void onPlayerResponseReceived(@NonNull String videoId);
}
/**
* Injection point. Called off the main thread.
*
@ -386,6 +450,15 @@ public final class VideoInformation {
if (!playerResponseVideoId.equals(videoId)) {
playerResponseVideoId = videoId;
}
OnPlayerResponseReceivedListener listener = onPlayerResponseReceivedListener;
if (listener != null) {
onPlayerResponseReceivedListener = null;
try {
listener.onPlayerResponseReceived(videoId);
} catch (Exception e) {
Logger.printException(() -> "onPlayerResponseReceived failed", e);
}
}
}
/**
@ -665,7 +738,8 @@ public final class VideoInformation {
if (hasSetVolumeOnly(clazz)) return obj;
String[] getterNames = {
"getAudioComponent", "getPlayer", "getExoPlayer",
"getWrappedPlayer", "getInnerPlayer", "getPlayback"
"getWrappedPlayer", "getInnerPlayer", "getPlayback",
"getImpl", "getDelegate", "getExoPlayerImpl", "getPlaybackImpl"
};
for (String name : getterNames) {
try {

View file

@ -19,6 +19,7 @@ enum class VideoState {
private val nameToVideoState = entries.associateBy { it.name }
private val onPlayingListeners = CopyOnWriteArrayList<Runnable>()
private val onNotPlayingListeners = CopyOnWriteArrayList<Runnable>()
/** Add a listener that is run when state changes to PLAYING. Used e.g. by VOT to resume translation. */
@JvmStatic
@ -26,6 +27,12 @@ enum class VideoState {
onPlayingListeners.add(listener)
}
/** Add a listener that is run when state changes to non-PLAYING (PAUSED, ENDED, etc). Used e.g. by VOT to pause translation immediately. */
@JvmStatic
fun addOnNotPlayingListener(listener: Runnable) {
onNotPlayingListeners.add(listener)
}
@JvmStatic
fun setFromString(enumName: String) {
val state = nameToVideoState[enumName]
@ -51,6 +58,14 @@ enum class VideoState {
Logger.printException { "OnPlaying listener error: ${e.message}" }
}
}
} else if (type != null) {
for (listener in onNotPlayingListeners) {
try {
listener.run()
} catch (e: Exception) {
Logger.printException { "OnNotPlaying listener error: ${e.message}" }
}
}
}
}
}

View file

@ -388,6 +388,9 @@ public class ExtendedUtils extends PackageUtils {
ImageView iconView = new ImageView(mContext);
if (iconId != 0) {
iconView.setImageResource(iconId);
iconView.setVisibility(View.VISIBLE);
} else {
iconView.setVisibility(View.GONE);
}
iconView.setColorFilter(cf);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dipToPixels(24), dipToPixels(24));

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2025 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/anddea.
* Original author(s):
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/anddea.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.utils;

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2025 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/anddea.
* Original author(s):
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/anddea.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.utils;

View file

@ -1,3 +1,45 @@
/*
* Copyright (C) anddea
*
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* Original author(s) (alphabetical order):
* - anddea (https://github.com/anddea)
* - inotia00 (https://github.com/inotia00)
* - Jav1x (https://github.com/Jav1x)
*
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7(b) Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.utils;
import static app.morphe.extension.shared.utils.BaseThemeUtils.getDialogBackgroundColor;
@ -44,6 +86,7 @@ import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -64,6 +107,7 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import app.morphe.extension.shared.settings.BooleanSetting;
import app.morphe.extension.shared.settings.EnumSetting;
import app.morphe.extension.shared.ui.SheetBottomDialog;
import app.morphe.extension.shared.utils.BaseThemeUtils;
@ -75,6 +119,7 @@ import app.morphe.extension.youtube.patches.shorts.ShortsRepeatStatePatch.Shorts
import app.morphe.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
import app.morphe.extension.youtube.patches.video.CustomPlaybackSpeedPatch.PlaybackSpeedMenuType;
import app.morphe.extension.youtube.patches.video.PlaybackSpeedPatch;
import app.morphe.extension.youtube.patches.voiceovertranslation.VoiceOverTranslationPatch;
import app.morphe.extension.youtube.patches.video.VideoQualityPatch;
import app.morphe.extension.youtube.patches.video.VideoQualityPatch.VideoQualityMenuInterface;
import app.morphe.extension.youtube.settings.Settings;
@ -950,6 +995,310 @@ public class VideoUtils extends IntentUtils {
}
}
/**
* Shows a bottom sheet dialog for Voice Over Translation with a volume slider.
* Similar to the playback speed menu - slides up from the bottom.
*/
public static void showVotBottomSheetDialog(@NonNull Context context) {
try {
SheetBottomDialog.DraggableLinearLayout mainLayout =
SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor());
final int dip4 = dipToPixels(4);
final int dip8 = dipToPixels(8);
final int dip12 = dipToPixels(12);
final int dip20 = dipToPixels(20);
// Title: "Громкость перевода"
TextView titleText = new TextView(context);
titleText.setText(str("revanced_vot_translation_volume_title"));
titleText.setTextColor(ThemeUtils.getAppForegroundColor());
titleText.setTextSize(16);
titleText.setTypeface(Typeface.DEFAULT_BOLD);
titleText.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
titleParams.setMargins(0, dip20, 0, dip12);
titleText.setLayoutParams(titleParams);
mainLayout.addView(titleText);
// Slider row with -/+ buttons
LinearLayout sliderLayout = new LinearLayout(context);
sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
Button minusButton = createStyledButton(context, false, dip8, dip8);
Button plusButton = createStyledButton(context, true, dip8, dip8);
SeekBar volumeSlider = new SeekBar(context);
volumeSlider.setFocusable(true);
volumeSlider.setFocusableInTouchMode(true);
volumeSlider.setMax(100);
volumeSlider.setProgress(Settings.VOT_TRANSLATION_VOLUME.get());
volumeSlider.getProgressDrawable().setColorFilter(
ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
volumeSlider.getThumb().setColorFilter(
ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
volumeSlider.setLayoutParams(sliderParams);
TextView volumeValueText = new TextView(context);
volumeValueText.setText(str("revanced_vot_percent_value", Settings.VOT_TRANSLATION_VOLUME.get()));
volumeValueText.setTextColor(ThemeUtils.getAppForegroundColor());
volumeValueText.setTextSize(14);
volumeValueText.setMinWidth(dipToPixels(40));
sliderLayout.addView(minusButton);
sliderLayout.addView(volumeSlider);
sliderLayout.addView(plusButton);
sliderLayout.addView(volumeValueText);
mainLayout.addView(sliderLayout);
java.util.function.Consumer<Integer> applyVolume = vol -> {
vol = Utils.clamp(vol, 0, 100);
Settings.VOT_TRANSLATION_VOLUME.save(vol);
volumeSlider.setProgress(vol);
volumeValueText.setText(str("revanced_vot_percent_value", vol));
VoiceOverTranslationPatch.applyVolumeToCurrentPlayer();
};
volumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) applyVolume.accept(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
minusButton.setOnClickListener(v -> applyVolume.accept(Settings.VOT_TRANSLATION_VOLUME.get() - 5));
plusButton.setOnClickListener(v -> applyVolume.accept(Settings.VOT_TRANSLATION_VOLUME.get() + 5));
// Original audio volume
TextView origTitleText = new TextView(context);
origTitleText.setText(str("revanced_vot_original_audio_volume_title"));
origTitleText.setTextColor(ThemeUtils.getAppForegroundColor());
origTitleText.setTextSize(16);
origTitleText.setTypeface(Typeface.DEFAULT_BOLD);
origTitleText.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams origTitleParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
origTitleParams.setMargins(0, dip20, 0, dip12);
origTitleText.setLayoutParams(origTitleParams);
mainLayout.addView(origTitleText);
LinearLayout origSliderLayout = new LinearLayout(context);
origSliderLayout.setOrientation(LinearLayout.HORIZONTAL);
origSliderLayout.setGravity(Gravity.CENTER_VERTICAL);
Button origMinusButton = createStyledButton(context, false, dip8, dip8);
Button origPlusButton = createStyledButton(context, true, dip8, dip8);
SeekBar origVolumeSlider = new SeekBar(context);
origVolumeSlider.setFocusable(true);
origVolumeSlider.setFocusableInTouchMode(true);
origVolumeSlider.setMax(100);
origVolumeSlider.setProgress(Settings.VOT_ORIGINAL_AUDIO_VOLUME.get());
origVolumeSlider.getProgressDrawable().setColorFilter(
ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
origVolumeSlider.getThumb().setColorFilter(
ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
LinearLayout.LayoutParams origSliderParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
origVolumeSlider.setLayoutParams(origSliderParams);
TextView origVolumeValueText = new TextView(context);
origVolumeValueText.setText(str("revanced_vot_percent_value", Settings.VOT_ORIGINAL_AUDIO_VOLUME.get()));
origVolumeValueText.setTextColor(ThemeUtils.getAppForegroundColor());
origVolumeValueText.setTextSize(14);
origVolumeValueText.setMinWidth(dipToPixels(40));
origSliderLayout.addView(origMinusButton);
origSliderLayout.addView(origVolumeSlider);
origSliderLayout.addView(origPlusButton);
origSliderLayout.addView(origVolumeValueText);
mainLayout.addView(origSliderLayout);
java.util.function.Consumer<Integer> applyOrigVolume = vol -> {
vol = Utils.clamp(vol, 0, 100);
Settings.VOT_ORIGINAL_AUDIO_VOLUME.save(vol);
origVolumeSlider.setProgress(vol);
origVolumeValueText.setText(str("revanced_vot_percent_value", vol));
if (VoiceOverTranslationPatch.isTranslationActive()) {
VoiceOverTranslationPatch.refreshOriginalAudioVolumeIfActive();
}
};
origVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) applyOrigVolume.accept(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
origMinusButton.setOnClickListener(v -> applyOrigVolume.accept(Settings.VOT_ORIGINAL_AUDIO_VOLUME.get() - 5));
origPlusButton.setOnClickListener(v -> applyOrigVolume.accept(Settings.VOT_ORIGINAL_AUDIO_VOLUME.get() + 5));
// Voice style: segmented buttons (Standard | Live)
LinearLayout voiceStyleRow = createVotVoiceStyleButtons(context, VoiceOverTranslationPatch::restartTranslationIfActive);
mainLayout.addView(voiceStyleRow);
// Audio proxy toggle restart translation when changed (proxy vs direct URL)
LinearLayout proxyItem = createVotSwitchItem(context, str("revanced_vot_audio_proxy_title"),
Settings.VOT_AUDIO_PROXY_ENABLED, VoiceOverTranslationPatch::restartTranslationIfActive);
mainLayout.addView(proxyItem);
ExtendedUtils.showBottomSheetDialog(context, mainLayout);
} catch (Exception ex) {
Logger.printException(() -> "showVotBottomSheetDialog failure", ex);
}
}
/**
* Creates a segmented control for voice style: two buttons (Standard voices | Live voices).
* @param onChanged optional callback when the selection changes (e.g. to restart translation)
*/
private static LinearLayout createVotVoiceStyleButtons(Context context, Runnable onChanged) {
final int dip8 = dipToPixels(8);
final int dip12 = dipToPixels(12);
LinearLayout container = new LinearLayout(context);
container.setOrientation(LinearLayout.VERTICAL);
container.setPadding(dipToPixels(16), dip12, dipToPixels(16), dip12);
TextView labelText = new TextView(context);
labelText.setText(str("revanced_vot_voice_style_title"));
labelText.setTextColor(ThemeUtils.getAppForegroundColor());
labelText.setTextSize(14);
labelText.setTypeface(Typeface.DEFAULT_BOLD);
labelText.setGravity(Gravity.START);
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
labelParams.setMargins(0, 0, 0, dip8);
labelText.setLayoutParams(labelParams);
container.addView(labelText);
LinearLayout buttonsRow = new LinearLayout(context);
buttonsRow.setOrientation(LinearLayout.HORIZONTAL);
buttonsRow.setGravity(Gravity.CENTER);
Button standardButton = createVotSegmentedButton(context, str("revanced_vot_voice_style_standard"));
Button liveButton = createVotSegmentedButton(context, str("revanced_vot_voice_style_live"));
Runnable updateSelection = () -> {
boolean useLive = Settings.VOT_USE_LIVE_VOICES.get();
int selectedColor = getAdjustedBackgroundColor(true);
int unselectedColor = getAdjustedBackgroundColor(false);
ShapeDrawable standardBg = (ShapeDrawable) standardButton.getBackground();
ShapeDrawable liveBg = (ShapeDrawable) liveButton.getBackground();
standardBg.getPaint().setColor(useLive ? unselectedColor : selectedColor);
liveBg.getPaint().setColor(useLive ? selectedColor : unselectedColor);
standardBg.invalidateSelf();
liveBg.invalidateSelf();
standardButton.invalidate();
liveButton.invalidate();
};
standardButton.setOnClickListener(v -> {
Settings.VOT_USE_LIVE_VOICES.save(false);
updateSelection.run();
if (onChanged != null) onChanged.run();
});
liveButton.setOnClickListener(v -> {
Settings.VOT_USE_LIVE_VOICES.save(true);
updateSelection.run();
if (onChanged != null) onChanged.run();
});
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(0, dipToPixels(40), 1f);
btnParams.setMargins(0, 0, dip8 / 2, 0);
standardButton.setLayoutParams(btnParams);
LinearLayout.LayoutParams btnParams2 = new LinearLayout.LayoutParams(0, dipToPixels(40), 1f);
btnParams2.setMargins(dip8 / 2, 0, 0, 0);
liveButton.setLayoutParams(btnParams2);
buttonsRow.addView(standardButton);
buttonsRow.addView(liveButton);
container.addView(buttonsRow);
updateSelection.run();
return container;
}
private static Button createVotSegmentedButton(Context context, String text) {
Button button = new Button(context, null, 0);
button.setText(text);
button.setTextColor(ThemeUtils.getAppForegroundColor());
button.setTextSize(13);
button.setAllCaps(false);
button.setGravity(Gravity.CENTER);
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(12), null, null));
background.getPaint().setColor(getAdjustedBackgroundColor(false));
button.setBackground(background);
button.setPadding(dipToPixels(12), dipToPixels(8), dipToPixels(12), dipToPixels(8));
return button;
}
/**
* Creates a row with title and switch for a boolean setting (e.g. audio proxy).
*/
private static LinearLayout createVotSwitchItem(Context context, String title,
BooleanSetting setting) {
return createVotSwitchItem(context, title, setting, null);
}
private static LinearLayout createVotSwitchItem(Context context, String title,
BooleanSetting setting, Runnable onChanged) {
LinearLayout itemLayout = new LinearLayout(context);
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
itemLayout.setPadding(dipToPixels(16), dipToPixels(12), dipToPixels(16), dipToPixels(12));
itemLayout.setGravity(Gravity.CENTER_VERTICAL);
itemLayout.setClickable(true);
itemLayout.setFocusable(true);
android.graphics.drawable.StateListDrawable background = new android.graphics.drawable.StateListDrawable();
background.addState(new int[]{android.R.attr.state_pressed},
new android.graphics.drawable.ColorDrawable(ThemeUtils.getAdjustedBackgroundColor(true)));
background.addState(new int[]{}, new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
itemLayout.setBackground(background);
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(16);
titleView.setTextColor(ThemeUtils.getAppForegroundColor());
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
itemLayout.addView(titleView, titleParams);
Switch switchView = new Switch(context);
switchView.setChecked(setting.get());
switchView.getThumbDrawable().setColorFilter(ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_ATOP);
switchView.getTrackDrawable().setColorFilter(ThemeUtils.getAppForegroundColor(), PorterDuff.Mode.SRC_ATOP);
switchView.setOnCheckedChangeListener((v, isChecked) -> {
setting.save(isChecked);
if (onChanged != null) onChanged.run();
});
itemLayout.addView(switchView);
itemLayout.setOnClickListener(v -> switchView.setChecked(!setting.get()));
return itemLayout;
}
/**
* Creates a styled button with a plus or minus symbol.
*

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2025 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/anddea.
* Original author(s):
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/anddea.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.extension.youtube.utils;
@ -176,10 +203,12 @@ public class YandexVotUtils {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
assert response.body() != null;
String bodyString = response.body().string();
Logger.printException(() -> "VOT: Failed to create session: " + response.code() + " " + response.message() + ", Body: " + bodyString);
throw new IOException("Failed to create session: " + response.code());
}
assert response.body() != null;
byte[] responseBytes = response.body().bytes();
ManualYandexSessionResponse sessionResponse = ManualYandexSessionResponse.parseFrom(responseBytes);
if (TextUtils.isEmpty(sessionResponse.secretKey)) {
@ -482,10 +511,12 @@ public class YandexVotUtils {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
assert response.body() != null;
String bodyString = response.body().string();
Logger.printException(() -> "VOT: Translation request failed: " + response.code() + " " + response.message() + ", Body: " + bodyString);
throw new IOException("API Error: " + response.code());
}
assert response.body() != null;
byte[] responseBytes = response.body().bytes();
Logger.printDebug(() -> "VOT: Translation response body: " + Arrays.toString(responseBytes));
return ManualVideoTranslationResponse.parseFrom(responseBytes);
@ -517,10 +548,12 @@ public class YandexVotUtils {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
assert response.body() != null;
String bodyString = response.body().string();
Logger.printException(() -> "VOT: Failed to get subtitle tracks: " + response.code() + " " + response.message() + ", Body: " + bodyString);
return null;
}
assert response.body() != null;
ManualSubtitlesResponse subResponse = ManualSubtitlesResponse.parseFrom(response.body().bytes());
String availableSubsLog = subResponse.subtitles != null && !subResponse.subtitles.isEmpty()
? subResponse.subtitles.stream()
@ -569,6 +602,7 @@ public class YandexVotUtils {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
assert response.body() != null;
String bodyString = response.body().string();
Logger.printException(() -> "VOT: Failed fail-audio-js request: " + response.code() + " " + response.message() + ", Body: " + bodyString);
} else {
@ -605,6 +639,7 @@ public class YandexVotUtils {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
assert response.body() != null;
String bodyString = response.body().string();
Logger.printException(() -> "VOT: Failed /audio request: " + response.code() + " " + response.message() + ", Body: " + bodyString);
} else {
@ -771,12 +806,14 @@ public class YandexVotUtils {
public void onResponse(@NonNull Call call, @NonNull Response response) {
try (ResponseBody body = response.body()) {
if (!response.isSuccessful()) {
assert body != null;
String bodyPreview = body.source().peek().readString(1024, StandardCharsets.UTF_8);
Logger.printException(() -> "VOT: Failed to download subtitle: " + response.code() + " " + response.message() + ", Preview: " + bodyPreview);
postToMainThread(() -> callback.onFinalFailure(str("revanced_yandex_error_download_subs_failed", response.code())));
return;
}
assert body != null;
String subtitleText = body.string();
Logger.printInfo(() -> "VOT: Fetched subtitle content (" + yandexTargetLang + ", " + subtitleText.length() + " chars)");

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2025 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/anddea.
* Original author(s):
* - anddea (https://github.com/anddea)
*
* IMPORTANT: This file is the proprietary work of https://github.com/anddea.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.patches.youtube.player.overlaybuttons

View file

@ -1,14 +1,41 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of https://github.com/anddea/revanced-patches/.
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* The original author: https://github.com/Jav1x.
* Original author(s):
* - Jav1x (https://github.com/Jav1x)
*
* IMPORTANT: This file is the proprietary work of https://github.com/Jav1x.
* Any modifications, derivatives, or substantial rewrites of this file
* must retain this copyright notice and the original author attribution
* in the source code and version control history.
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.patches.youtube.video.voiceovertranslation
@ -74,6 +101,7 @@ val voiceOverTranslationPatch = resourcePatch(
dependsOn(
overlayButtonsPatch,
voiceOverTranslationBytecodePatch,
votOriginalVolumeBytecodePatch,
settingsPatch,
)

View file

@ -0,0 +1,149 @@
/*
* Copyright (C) 2026 anddea
*
* This file is part of the revanced-patches project:
* https://github.com/anddea/revanced-patches
*
* Original author(s) (based on contributions):
* - Jav1x (https://github.com/Jav1x)
* - anddea (https://github.com/anddea)
*
* Licensed under the GNU General Public License v3.0.
*
* ------------------------------------------------------------------------
* GPLv3 Section 7 Attribution Notice
* ------------------------------------------------------------------------
*
* This file contains substantial original work by the author(s) listed above.
*
* In accordance with Section 7 of the GNU General Public License v3.0,
* the following additional terms apply to this file:
*
* 1. Attribution (Section 7(b)): This specific copyright notice and the
* list of original authors above must be preserved in any copy or
* derivative work. You may add your own copyright notice below it,
* but you may not remove the original one.
*
* 2. Origin (Section 7(c)): Modified versions must be clearly marked as
* such (e.g., by adding a "Modified by" line or a new copyright notice).
* They must not be misrepresented as the original work.
*
* ------------------------------------------------------------------------
* Version Control Acknowledgement (Non-binding Request)
* ------------------------------------------------------------------------
*
* While not a legal requirement of the GPLv3, the original author(s)
* respectfully request that ports or substantial modifications retain
* historical authorship credit in version control systems (e.g., Git),
* listing original author(s) appropriately and modifiers as committers
* or co-authors.
*/
package app.morphe.patches.youtube.video.voiceovertranslation
import app.morphe.patcher.Fingerprint
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.patch.PatchException
import app.morphe.patcher.patch.bytecodePatch
import app.morphe.patcher.methodCall
import app.morphe.patches.youtube.utils.extension.Constants.EXTENSION_PATH
import app.morphe.util.fingerprint.methodOrThrow
import app.morphe.util.getReference
import app.morphe.util.indexOfFirstInstructionOrThrow
import app.morphe.util.parametersEqual
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"$EXTENSION_PATH/patches/voiceovertranslation/VotOriginalVolumePatch;"
private const val AUDIO_TRACK_CLASS = "Landroid/media/AudioTrack;"
private fun MethodReference.isAudioTrackSetVolume(): Boolean =
definingClass == AUDIO_TRACK_CLASS &&
name == "setVolume" &&
parametersEqual(parameterTypes, listOf("F")) &&
returnType == "I"
/**
* For invoke-virtual {obj, float}: float is last register.
* FiveRegisterInstruction (35c): registerC=obj, registerD=float.
* TwoRegisterInstruction (22c): registerA=obj, registerB=float.
* RegisterRangeInstruction (3rc): float in last register.
*/
private fun getVolumeRegister(instruction: Instruction): Int? {
return when (instruction) {
is FiveRegisterInstruction -> {
if (instruction.registerCount >= 2) instruction.registerD
else null
}
is TwoRegisterInstruction -> instruction.registerB
is RegisterRangeInstruction -> {
if (instruction.registerCount >= 2)
instruction.startRegister + instruction.registerCount - 1
else null
}
else -> null
}
}
/**
* For invoke-virtual {obj, float}: obj is first register.
* FiveRegisterInstruction (35c): registerC=obj.
* TwoRegisterInstruction (22c): registerA=obj.
* RegisterRangeInstruction (3rc): obj in startRegister.
*/
private fun getAudioTrackRegister(instruction: Instruction): Int? {
return when (instruction) {
is FiveRegisterInstruction -> if (instruction.registerCount >= 1) instruction.registerC else null
is TwoRegisterInstruction -> instruction.registerA
is RegisterRangeInstruction -> if (instruction.registerCount >= 1) instruction.startRegister else null
else -> null
}
}
private object AudioTrackSetVolumeMethodFingerprint : Fingerprint(
returnType = "V",
parameters = listOf(),
filters = listOf(
methodCall(
definingClass = "Landroid/media/AudioTrack;",
name = "setVolume",
parameters = listOf("F"),
returnType = "I"
)
)
)
private val audioTrackSetVolumeMethodFingerprint =
Pair("audioTrackSetVolumeMethodFingerprint", AudioTrackSetVolumeMethodFingerprint)
val votOriginalVolumeBytecodePatch = bytecodePatch(
description = "votOriginalVolumeBytecodePatch"
) {
dependsOn(voiceOverTranslationBytecodePatch)
execute {
val mutableMethod = audioTrackSetVolumeMethodFingerprint.methodOrThrow()
val index = mutableMethod.indexOfFirstInstructionOrThrow {
(opcode == Opcode.INVOKE_VIRTUAL || opcode == Opcode.INVOKE_VIRTUAL_RANGE) &&
(getReference<MethodReference>()?.isAudioTrackSetVolume() == true)
}
val instruction = mutableMethod.implementation!!.instructions.elementAt(index)
val audioTrackReg = getAudioTrackRegister(instruction)
?: throw PatchException("VotOriginalVolume: cannot get AudioTrack register")
val volReg = getVolumeRegister(instruction)
?: throw PatchException("VotOriginalVolume: cannot get volume register")
mutableMethod.addInstructions(
index,
"""
invoke-static { v$audioTrackReg, v$volReg }, $EXTENSION_CLASS_DESCRIPTOR->applyVolumeMultiplier(Landroid/media/AudioTrack;F)F
move-result v$volReg
""".trimIndent()
)
}
}

View file

@ -2735,10 +2735,18 @@ Disclaimer: Cookies issued without signing in may be invalid or may take time to
<string name="revanced_transcript_cookies_summary">Cookies to use in Transcript API requests.</string>
<string name="revanced_transcript_cookies_title">Transcript Cookies</string>
<string name="revanced_translations_title">Translations</string>
<string name="revanced_vot_audio_proxy_summary_off">Audio stream uses direct URL</string>
<string name="revanced_vot_audio_proxy_summary_on">Audio stream is proxied through the worker</string>
<string name="revanced_vot_audio_proxy_title">Proxy audio stream</string>
<string name="revanced_vot_enabled_summary_off">Yandex voice-over translation is disabled</string>
<string name="revanced_vot_enabled_summary_on">Yandex voice-over translation is enabled</string>
<string name="revanced_vot_enabled_title">Enable Voice Over Translation</string>
<string name="revanced_vot_lang_auto">Auto-detect</string>
<string name="revanced_vot_live_voices_unavailable">Live voices unavailable, using standard</string>
<string name="revanced_vot_original_audio_volume_summary">Volume of the original video track when translation is playing (0-100). Default: 50</string>
<string name="revanced_vot_original_audio_volume_title">Original audio volume</string>
<string name="revanced_vot_percent_value">%d%%</string>
<string name="revanced_vot_playback_error">Failed to play translation audio</string>
<string name="revanced_vot_proxy_url_summary">VOT worker proxy host. Default: vot-worker.toil.cc</string>
<string name="revanced_vot_proxy_url_title">Proxy server</string>
<string name="revanced_vot_source_language_summary">Select the language of the video audio</string>
@ -2748,14 +2756,22 @@ Disclaimer: Cookies issued without signing in may be invalid or may take time to
<string name="revanced_vot_stream_not_ready">Translation not ready. Use the button to enable when ready.</string>
<string name="revanced_vot_stream_ready">Translation ready, playing</string>
<string name="revanced_vot_stream_requesting">Requesting translation for stream…</string>
<string name="revanced_vot_stream_waiting">Waiting for translation…</string>
<string name="revanced_vot_stream_waiting">Waiting… ~%s</string>
<string name="revanced_vot_target_language_summary">Select the language to translate to</string>
<string name="revanced_vot_target_language_title">Target language</string>
<string name="revanced_vot_time_min">%d min</string>
<string name="revanced_vot_time_sec">%d sec</string>
<string name="revanced_vot_translation_volume_summary">Volume of the translated audio track (0-100). Default: 100</string>
<string name="revanced_vot_translation_volume_title">Translation volume</string>
<string name="revanced_vot_unavailable_live">Translation is not available for live streams</string>
<string name="revanced_vot_unavailable_same_language">Video audio is already in the target language</string>
<string name="revanced_vot_unavailable_too_long">Video is too long for translation (max 4 hours)</string>
<string name="revanced_vot_voice_style_live">Live voices</string>
<string name="revanced_vot_voice_style_live_summary_off">Standard TTS voices</string>
<string name="revanced_vot_voice_style_live_summary_on">Natural-sounding voices from Yandex</string>
<string name="revanced_vot_voice_style_live_title">Live voices</string>
<string name="revanced_vot_voice_style_standard">Standard voices</string>
<string name="revanced_vot_voice_style_title">Voice-over style</string>
<string name="revanced_watch_history_about_status_blocked">• Watch history is blocked.</string>
<string name="revanced_watch_history_about_status_original">"• Follows the watch history settings of Google account.
• Watch history may not work due to DNS or VPN."</string>

View file

@ -886,7 +886,10 @@
<app.morphe.extension.shared.settings.preference.CustomDialogListPreference android:entries="@array/revanced_vot_source_language_entries" android:title="@string/revanced_vot_source_language_title" android:key="vot_source_language" android:summary="@string/revanced_vot_source_language_summary" android:dependency="vot_enabled" android:entryValues="@array/revanced_vot_source_language_entry_values" android:defaultValue="auto" />
<app.morphe.extension.shared.settings.preference.CustomDialogListPreference android:entries="@array/revanced_vot_target_language_entries" android:title="@string/revanced_vot_target_language_title" android:key="vot_target_language" android:summary="@string/revanced_vot_target_language_summary" android:dependency="vot_enabled" android:entryValues="@array/revanced_vot_target_language_entry_values" android:defaultValue="ru" />
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:title="@string/revanced_vot_translation_volume_title" android:key="vot_translation_volume" android:summary="@string/revanced_vot_translation_volume_summary" android:dependency="vot_enabled" />
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:title="@string/revanced_vot_original_audio_volume_title" android:key="vot_original_audio_volume" android:summary="@string/revanced_vot_original_audio_volume_summary" android:dependency="vot_enabled" />
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:title="@string/revanced_vot_proxy_url_title" android:key="vot_proxy_url" android:summary="@string/revanced_vot_proxy_url_summary" android:dependency="vot_enabled" />
<SwitchPreference android:title="@string/revanced_vot_voice_style_live_title" android:key="vot_use_live_voices" android:summaryOn="@string/revanced_vot_voice_style_live_summary_on" android:summaryOff="@string/revanced_vot_voice_style_live_summary_off" android:dependency="vot_enabled" />
<SwitchPreference android:title="@string/revanced_vot_audio_proxy_title" android:key="vot_audio_proxy_enabled" android:summaryOn="@string/revanced_vot_audio_proxy_summary_on" android:summaryOff="@string/revanced_vot_audio_proxy_summary_off" android:dependency="vot_enabled" />
</PreferenceScreen>PREFERENCE_SCREEN: VOICE_OVER_TRANSLATION -->

View file

@ -87,6 +87,7 @@
<string name="revanced_change_form_factor_entry_3">טאבלט</string>
<string name="revanced_change_form_factor_entry_4">רכב</string>
<string name="revanced_change_form_factor_entry_5">טאבלט (מינימום 600 dp)</string>
<string name="revanced_change_form_factor_entry_6">רכב</string>
<string name="revanced_change_form_factor_title">גורם צורת הפריסה</string>
<string name="revanced_change_form_factor_user_dialog_message">"השינויים כוללים:
@ -96,6 +97,7 @@
פריסת רכב
• סרטוני Shorts נפתחים בנגן הרגיל
• פיד מאורגן לפי נושאים וערוצים"</string>
<string name="revanced_change_live_ring_click_action_summary_off">שידור חי נפתח בעת לחיצה על טבעת הלייב.</string>
<string name="revanced_change_player_flyout_menu_toggle_summary_off">נעשה שימוש במתגים.</string>
<string name="revanced_change_player_flyout_menu_toggle_summary_on">נעשה שימוש בלחצני טקסט.</string>
<string name="revanced_change_player_flyout_menu_toggle_title">שנה את סוג החלפת המצב</string>

View file

@ -2772,7 +2772,7 @@ JavaScript 클라이언트 중에서 권장되는 클라이언트는 TV Simply
<string name="revanced_vot_proxy_url_summary">VOT worker 프록시 호스트입니다. (기본값: vot-worker.toil.cc)</string>
<string name="revanced_vot_proxy_url_title">프록시 서버</string>
<string name="revanced_vot_source_language_summary">동영상 오디오의 언어를 선택하세요</string>
<string name="revanced_vot_source_language_title">언어 원본</string>
<string name="revanced_vot_source_language_title">원본 언어</string>
<string name="revanced_vot_started">번역이 시작되었습니다</string>
<string name="revanced_vot_stopped">번역이 중지되었습니다</string>
<string name="revanced_vot_stream_not_ready">번역이 준비되지 않았습니다. 준비되었을 때 버튼을 탭하여 활성화하세요.</string>

View file

@ -2723,6 +2723,26 @@ Aviso: Cookies emitidos sem login podem ser inválidos ou levar algum tempo para
<string name="revanced_transcript_cookies_summary">Cookies para uso em solicitações da API de transcrição.</string>
<string name="revanced_transcript_cookies_title">Cookies de transcrição</string>
<string name="revanced_translations_title">Traduções</string>
<string name="revanced_vot_enabled_summary_on">A tradução por voz do Yandex está ativada.</string>
<string name="revanced_vot_enabled_title">Ativar tradução por voz</string>
<string name="revanced_vot_lang_auto">Detecção automática</string>
<string name="revanced_vot_proxy_url_summary">Host proxy do servidor VOT. Padrão: vot-worker.toil.cc</string>
<string name="revanced_vot_proxy_url_title">Servidor proxy</string>
<string name="revanced_vot_source_language_summary">Selecione o idioma do áudio do vídeo</string>
<string name="revanced_vot_source_language_title">Língua de origem</string>
<string name="revanced_vot_started">A tradução começou</string>
<string name="revanced_vot_stopped">A tradução foi interrompida</string>
<string name="revanced_vot_stream_not_ready">Tradução ainda não está pronta. Use o botão para habilitá-la quando estiver pronta</string>
<string name="revanced_vot_stream_ready">Tradução pronta, tocando</string>
<string name="revanced_vot_stream_requesting">Solicitar tradução para a transmissão…</string>
<string name="revanced_vot_stream_waiting">Aguardando tradução…</string>
<string name="revanced_vot_target_language_summary">Selecione o idioma para o qual deseja traduzir</string>
<string name="revanced_vot_target_language_title">Língua de destino</string>
<string name="revanced_vot_translation_volume_summary">Volume da faixa de áudio traduzida (0-100). Padrão: 100</string>
<string name="revanced_vot_translation_volume_title">Volume de tradução</string>
<string name="revanced_vot_unavailable_live">A tradução não está disponível para transmissões ao vivo</string>
<string name="revanced_vot_unavailable_same_language">O áudio do vídeo já está no idioma de destino</string>
<string name="revanced_vot_unavailable_too_long">O vídeo é muito longo para ser traduzido (máximo de 4 horas)</string>
<string name="revanced_watch_history_about_status_blocked">• O histórico de exibição não funciona.</string>
<string name="revanced_watch_history_about_status_original">"• Segue as configurações do histórico de exibição da conta do Google.
• O histórico de exibição pode não funcionar devido ao DNS ou à VPN."</string>

View file

@ -2708,10 +2708,17 @@ uBlock Origin обнаружил обход этой проблемы, вклю
<string name="revanced_transcript_cookies_summary">Куки для запросов расшифровок API.</string>
<string name="revanced_transcript_cookies_title">Расшифровки куки</string>
<string name="revanced_translations_title">Переводы</string>
<string name="revanced_vot_audio_proxy_summary_off">Аудио поток загружается напрямую</string>
<string name="revanced_vot_audio_proxy_summary_on">Аудио поток идёт через прокси worker</string>
<string name="revanced_vot_audio_proxy_title">Проксировать аудио</string>
<string name="revanced_vot_enabled_summary_off">Закадровый перевод Яндекс выключен</string>
<string name="revanced_vot_enabled_summary_on">Закадровый перевод Яндекс включён</string>
<string name="revanced_vot_enabled_title">Включить закадровый перевод</string>
<string name="revanced_vot_lang_auto">Автоопределение</string>
<string name="revanced_vot_live_voices_unavailable">Живые голоса недоступны, используем обычные</string>
<string name="revanced_vot_original_audio_volume_summary">Громкость оригинальной дорожки при воспроизведении перевода (0-100). По умолчанию: 50</string>
<string name="revanced_vot_original_audio_volume_title">Громкость оригинала</string>
<string name="revanced_vot_playback_error">Не удалось воспроизвести аудио перевода</string>
<string name="revanced_vot_proxy_url_summary">Хост прокси-сервера VOT worker. По умолчанию: vot-worker.toil.cc</string>
<string name="revanced_vot_proxy_url_title">Прокси-сервер</string>
<string name="revanced_vot_source_language_summary">Выберите язык аудио видео</string>
@ -2721,14 +2728,22 @@ uBlock Origin обнаружил обход этой проблемы, вклю
<string name="revanced_vot_stream_not_ready">Перевод ещё не готов. Включите кнопкой, когда будет готов.</string>
<string name="revanced_vot_stream_ready">Перевод готов, воспроизведение</string>
<string name="revanced_vot_stream_requesting">Запрос перевода для потока…</string>
<string name="revanced_vot_stream_waiting">Ожидание перевода…</string>
<string name="revanced_vot_stream_waiting">Ожидание… ~%s</string>
<string name="revanced_vot_target_language_summary">Выберите язык перевода</string>
<string name="revanced_vot_target_language_title">Язык перевода</string>
<string name="revanced_vot_time_min">%d мин</string>
<string name="revanced_vot_time_sec">%d сек</string>
<string name="revanced_vot_translation_volume_summary">Громкость переведённой аудиодорожки (0-100). По умолчанию: 100</string>
<string name="revanced_vot_translation_volume_title">Громкость перевода</string>
<string name="revanced_vot_unavailable_live">Перевод недоступен для стримов</string>
<string name="revanced_vot_unavailable_same_language">Аудио видео уже на выбранном языке</string>
<string name="revanced_vot_unavailable_too_long">Видео слишком длинное для перевода (макс. 4 часа)</string>
<string name="revanced_vot_voice_style_live">Живые голоса</string>
<string name="revanced_vot_voice_style_live_summary_off">Стандартная TTS озвучка</string>
<string name="revanced_vot_voice_style_live_summary_on">Естественная озвучка от Яндекса</string>
<string name="revanced_vot_voice_style_live_title">Живые голоса</string>
<string name="revanced_vot_voice_style_standard">Обычные голоса</string>
<string name="revanced_vot_voice_style_title">Вид озвучки</string>
<string name="revanced_watch_history_about_status_blocked">• История просмотра не работает.</string>
<string name="revanced_watch_history_about_status_original">"• Статус истории просмотров обычный.
• История просмотров может не работать с DNS или VPN."</string>

View file

@ -458,6 +458,8 @@
<string name="revanced_hide_web_search_results_summary_on">ผลลัพธ์การค้นหาบนเว็บถูกซ่อน</string>
<string name="revanced_hide_web_search_results_title">ซ่อนผลลัพธ์การค้นหาบนเว็บ</string>
<string name="revanced_language_DEFAULT">ภาษาของแอป</string>
<string name="revanced_language_TH">"Thai
<small>ไทย</small>"</string>
<string name="revanced_language_title">ภาษา ReVanced</string>
<string name="revanced_miniplayer_double_tap_action_summary_off">การแตะสองครั้งและการบีบเพื่อปรับขนาดปิดใช้งานแล้ว</string>
<string name="revanced_miniplayer_double_tap_action_summary_on">"การกระทำแตะสองครั้ง และการบีบเพื่อปรับขนาด เปิดใช้งาน

View file

@ -1701,6 +1701,8 @@ AI 生成的回應可能包含不準確的資訊。
長按設定預設播放速度1.0x"</string>
<string name="revanced_overlay_button_speed_dialog_title">播放速度按鈕</string>
<string name="revanced_overlay_button_speed_dialog_type_title">快速對話類型</string>
<string name="revanced_overlay_button_vot_summary">顯示疊加按鈕,用於切換目前影片的語音翻譯</string>
<string name="revanced_overlay_button_vot_title">語音翻譯</string>
<string name="revanced_overlay_button_whitelist_summary">點選可開啟白名單對話框。
點選並按住可開啟白名單設定對話框。</string>
<string name="revanced_overlay_button_whitelist_title">顯示白名單按鈕</string>
@ -1823,6 +1825,7 @@ AI 生成的回應可能包含不準確的資訊。
<string name="revanced_preference_screen_video_filter_summary">依關鍵字或觀看次數隱藏影片。</string>
<string name="revanced_preference_screen_video_filter_title">影片篩選器</string>
<string name="revanced_preference_screen_video_title">影片</string>
<string name="revanced_preference_screen_vot_title">語音翻譯</string>
<string name="revanced_preference_screen_watch_history_summary">變更與觀看歷史記錄相關的設定。</string>
<string name="revanced_preference_screen_watch_history_title">觀看歷史記錄</string>
<string name="revanced_queue_manager_add_to_queue">加入佇列清單</string>
@ -2742,6 +2745,27 @@ YouTube 伺服器上的 n-sig 模式變化頻繁,如果正規表示式無法
<string name="revanced_transcript_cookies_summary">在 Transcript API 請求使用 Cookie。</string>
<string name="revanced_transcript_cookies_title">字幕 Cookie</string>
<string name="revanced_translations_title">翻譯</string>
<string name="revanced_vot_enabled_summary_off">Yandex語音翻譯已停用</string>
<string name="revanced_vot_enabled_summary_on">Yandex語音翻譯功能已啟用</string>
<string name="revanced_vot_enabled_title">啟用語音翻譯</string>
<string name="revanced_vot_lang_auto">自動偵測</string>
<string name="revanced_vot_proxy_url_summary">vot 工作進程代理主機。預設值vot-worker.toil.cc</string>
<string name="revanced_vot_proxy_url_title">代理伺服器</string>
<string name="revanced_vot_source_language_summary">選擇影片音訊的語言</string>
<string name="revanced_vot_source_language_title">原始語言</string>
<string name="revanced_vot_started">翻譯開始</string>
<string name="revanced_vot_stopped">翻譯已停止</string>
<string name="revanced_vot_stream_not_ready">翻譯尚未完成。完成後,請點選按鈕啟用翻譯。</string>
<string name="revanced_vot_stream_ready">翻譯完成,開始播放</string>
<string name="revanced_vot_stream_requesting">請求翻譯直播串流…</string>
<string name="revanced_vot_stream_waiting">等待翻譯…</string>
<string name="revanced_vot_target_language_summary">選擇要翻譯成的語言</string>
<string name="revanced_vot_target_language_title">目標語言</string>
<string name="revanced_vot_translation_volume_summary">翻譯後音軌的音量0-100。預設值100</string>
<string name="revanced_vot_translation_volume_title">翻譯音量</string>
<string name="revanced_vot_unavailable_live">直播內容暫不提供翻譯服務</string>
<string name="revanced_vot_unavailable_same_language">音訊已為目標語言</string>
<string name="revanced_vot_unavailable_too_long">影片太長,無法翻譯(最多 4 小時)</string>
<string name="revanced_watch_history_about_status_blocked">• 觀看歷史記錄不起作用。</string>
<string name="revanced_watch_history_about_status_original">"• 遵循Google 帳戶的觀看記錄設定。
• 由於 DNS 或 VPN 的原因,觀看記錄可能無法運作。"</string>

View file

@ -11,6 +11,10 @@ from utils.xml_processor import XMLProcessor
logger = logging.getLogger("xml_tools")
BLACKLIST = {
"revanced_vot_percent_value",
}
def compare_and_update(source_path: Path, dest_path: Path, missing_path: Path) -> None:
"""Compare source and destination files and update missing strings.
@ -26,8 +30,10 @@ def compare_and_update(source_path: Path, dest_path: Path, missing_path: Path) -
_, _, source_strings = XMLProcessor.parse_file(source_path)
_, _, dest_strings = XMLProcessor.parse_file(dest_path)
# Find missing strings
missing_strings = {name: data for name, data in source_strings.items() if name not in dest_strings}
# Find missing strings (excluding those in the BLACKLIST)
missing_strings = {
name: data for name, data in source_strings.items() if name not in dest_strings and name not in BLACKLIST
}
if missing_strings:
# Create new root with missing strings