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:
parent
017e40e298
commit
6ac278cc65
27 changed files with 1652 additions and 144 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">"การกระทำแตะสองครั้ง และการบีบเพื่อปรับขนาด เปิดใช้งาน
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue