mirror of
git://f0xx.org/android_cast
synced 2026-07-29 09:17:51 +03:00
snap, PiP
This commit is contained in:
@@ -67,8 +67,10 @@
|
|||||||
android:name=".receiver.ReceiverPlaybackActivity"
|
android:name=".receiver.ReceiverPlaybackActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:theme="@style/Theme.AndroidCast.Fullscreen"
|
android:theme="@style/Theme.AndroidCast.Fullscreen"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|density" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".sender.ScreenCastService"
|
android:name=".sender.ScreenCastService"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.util.List;
|
|||||||
/** Requests and explains runtime permissions for LAN cast. */
|
/** Requests and explains runtime permissions for LAN cast. */
|
||||||
public final class PermissionHelper {
|
public final class PermissionHelper {
|
||||||
public static final int REQUEST_CODE = 1001;
|
public static final int REQUEST_CODE = 1001;
|
||||||
|
public static final int REQUEST_RECEIVER_PLAYBACK = 1002;
|
||||||
|
|
||||||
public static final class PermissionItem {
|
public static final class PermissionItem {
|
||||||
public final String permission;
|
public final String permission;
|
||||||
@@ -115,9 +116,47 @@ public final class PermissionHelper {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void openAppSettings(Activity activity) {
|
public static void openAppSettings(Activity activity) {
|
||||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||||
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Permissions needed before receiver cast / PiP (notifications for foreground service). */
|
||||||
|
public static boolean hasReceiverPlaybackReady(Activity activity) {
|
||||||
|
return missingForReceiverPlayback(activity).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void remindForReceiverPlayback(Activity activity) {
|
||||||
|
List<PermissionItem> missing = missingForReceiverPlayback(activity);
|
||||||
|
if (missing.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder body = new StringBuilder(activity.getString(R.string.pip_permissions_hint));
|
||||||
|
body.append("\n\n");
|
||||||
|
for (PermissionItem item : missing) {
|
||||||
|
body.append("• ").append(item.label).append('\n');
|
||||||
|
}
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.permissions_missing_title)
|
||||||
|
.setMessage(body.toString().trim())
|
||||||
|
.setPositiveButton(R.string.permissions_grant, (d, w) ->
|
||||||
|
ActivityCompat.requestPermissions(activity,
|
||||||
|
permissionsOnly(missing), REQUEST_RECEIVER_PLAYBACK))
|
||||||
|
.setNeutralButton(R.string.permissions_app_settings, (d, w) -> openAppSettings(activity))
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PermissionItem> missingForReceiverPlayback(Activity activity) {
|
||||||
|
List<PermissionItem> needed = new ArrayList<>();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
needed.add(new PermissionItem(Manifest.permission.POST_NOTIFICATIONS,
|
||||||
|
activity.getString(R.string.permission_notifications_cast), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AppOpsManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Rational;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import android.app.PictureInPictureParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picture-in-picture for receiver playback (API 26+). No runtime permission; user can disable PiP per app.
|
||||||
|
*/
|
||||||
|
public final class PictureInPictureHelper {
|
||||||
|
private PictureInPictureHelper() {}
|
||||||
|
|
||||||
|
public static boolean isSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the app is allowed to enter PiP (system / user app-ops). */
|
||||||
|
public static boolean isAllowedForApp(@NonNull Context context) {
|
||||||
|
if (!isSupported()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PackageManager pm = context.getPackageManager();
|
||||||
|
if (!pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
|
||||||
|
if (appOps == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int mode = appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
|
||||||
|
android.os.Process.myUid(), context.getPackageName());
|
||||||
|
return mode == AppOpsManager.MODE_ALLOWED;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PictureInPictureParams buildParams(int videoWidth, int videoHeight) {
|
||||||
|
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
|
||||||
|
.setAspectRatio(aspectRatio(videoWidth, videoHeight));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
builder.setAutoEnterEnabled(true);
|
||||||
|
builder.setSeamlessResizeEnabled(true);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyParams(@NonNull Activity activity, int videoWidth, int videoHeight) {
|
||||||
|
if (!isSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activity.setPictureInPictureParams(buildParams(videoWidth, videoHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean enter(@NonNull Activity activity, int videoWidth, int videoHeight) {
|
||||||
|
if (!isSupported() || !isAllowedForApp(activity)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return activity.enterPictureInPictureMode(buildParams(videoWidth, videoHeight));
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showEnableInSettingsDialog(@NonNull Activity activity) {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.pip_disabled_title)
|
||||||
|
.setMessage(R.string.pip_disabled_message)
|
||||||
|
.setPositiveButton(R.string.permissions_app_settings, (d, w) -> openPipSettings(activity))
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPipSettings(@NonNull Activity activity) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Intent intent = new Intent("android.settings.APP_PICTURE_IN_PICTURE_SETTINGS",
|
||||||
|
Uri.fromParts("package", activity.getPackageName(), null));
|
||||||
|
activity.startActivity(intent);
|
||||||
|
} else {
|
||||||
|
PermissionHelper.openAppSettings(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rational aspectRatio(int width, int height) {
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
return new Rational(width, height);
|
||||||
|
}
|
||||||
|
return new Rational(16, 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import android.os.Handler;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.Toast;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
@@ -26,6 +28,8 @@ import com.foxx.androidcast.AppPreferences;
|
|||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.DrawerHostActivity;
|
import com.foxx.androidcast.DrawerHostActivity;
|
||||||
import com.foxx.androidcast.CastThemeHelper;
|
import com.foxx.androidcast.CastThemeHelper;
|
||||||
|
import com.foxx.androidcast.PermissionHelper;
|
||||||
|
import com.foxx.androidcast.PictureInPictureHelper;
|
||||||
import com.foxx.androidcast.ICastReceiverService;
|
import com.foxx.androidcast.ICastReceiverService;
|
||||||
import com.foxx.androidcast.ICastStatusCallback;
|
import com.foxx.androidcast.ICastStatusCallback;
|
||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
@@ -53,7 +57,11 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
private TextView diagnosticsText;
|
private TextView diagnosticsText;
|
||||||
private ImageView robotView;
|
private ImageView robotView;
|
||||||
private TextView awaitingMessage;
|
private TextView awaitingMessage;
|
||||||
|
private ImageButton pipButton;
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
/** Sender connected or actively streaming — PiP allowed after permissions. */
|
||||||
|
private boolean castSessionActive;
|
||||||
|
private boolean inPictureInPicture;
|
||||||
private SurfaceTexture surfaceTexture;
|
private SurfaceTexture surfaceTexture;
|
||||||
private int videoWidth;
|
private int videoWidth;
|
||||||
private int videoHeight;
|
private int videoHeight;
|
||||||
@@ -70,7 +78,10 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticated(String senderName) {
|
public void onAuthenticated(String senderName) {
|
||||||
runOnUiThread(() -> showAwaitingOverlay(R.string.playback_awaiting_stream));
|
runOnUiThread(() -> {
|
||||||
|
onCastSessionStarted();
|
||||||
|
showAwaitingOverlay(R.string.playback_awaiting_stream);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -104,7 +115,10 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisconnected() {
|
public void onDisconnected() {
|
||||||
runOnUiThread(() -> onStreamLost());
|
runOnUiThread(() -> {
|
||||||
|
onCastSessionEnded();
|
||||||
|
onStreamLost();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,8 +178,13 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
diagnosticsText = findViewById(R.id.text_diagnostics);
|
diagnosticsText = findViewById(R.id.text_diagnostics);
|
||||||
robotView = findViewById(R.id.image_robot);
|
robotView = findViewById(R.id.image_robot);
|
||||||
awaitingMessage = findViewById(R.id.text_awaiting_message);
|
awaitingMessage = findViewById(R.id.text_awaiting_message);
|
||||||
|
pipButton = findViewById(R.id.button_pip);
|
||||||
|
if (pipButton != null) {
|
||||||
|
pipButton.setOnClickListener(v -> tryEnterPictureInPicture());
|
||||||
|
}
|
||||||
textureView.setSurfaceTextureListener(surfaceListener);
|
textureView.setSurfaceTextureListener(surfaceListener);
|
||||||
applyAspectRatio();
|
applyAspectRatio();
|
||||||
|
updatePipUi();
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false)) {
|
if (getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false)) {
|
||||||
startListeningService();
|
startListeningService();
|
||||||
@@ -214,11 +233,38 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
if (!inPictureInPicture) {
|
||||||
|
PermissionHelper.remindForReceiverPlayback(this);
|
||||||
|
}
|
||||||
if (awaitingOverlay != null && awaitingOverlay.getVisibility() == View.VISIBLE) {
|
if (awaitingOverlay != null && awaitingOverlay.getVisibility() == View.VISIBLE) {
|
||||||
startRobotAnimation();
|
startRobotAnimation();
|
||||||
}
|
}
|
||||||
mainHandler.post(diagnosticsRefresh);
|
mainHandler.post(diagnosticsRefresh);
|
||||||
updateDiagnosticsVisibility();
|
updateDiagnosticsVisibility();
|
||||||
|
updatePipUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUserLeaveHint() {
|
||||||
|
super.onUserLeaveHint();
|
||||||
|
if (castSessionActive && PermissionHelper.hasReceiverPlaybackReady(this)) {
|
||||||
|
tryEnterPictureInPicture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
|
||||||
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
|
||||||
|
inPictureInPicture = isInPictureInPictureMode;
|
||||||
|
applyPipChromeVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == PermissionHelper.REQUEST_RECEIVER_PLAYBACK) {
|
||||||
|
updatePipUi();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -357,6 +403,9 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
}
|
}
|
||||||
videoWidth = width;
|
videoWidth = width;
|
||||||
videoHeight = height;
|
videoHeight = height;
|
||||||
|
if (castSessionActive && PermissionHelper.hasReceiverPlaybackReady(this)) {
|
||||||
|
PictureInPictureHelper.applyParams(this, videoWidth, videoHeight);
|
||||||
|
}
|
||||||
applyAspectRatio();
|
applyAspectRatio();
|
||||||
applyTextureFitCenter();
|
applyTextureFitCenter();
|
||||||
if (surfaceTexture != null) {
|
if (surfaceTexture != null) {
|
||||||
@@ -453,8 +502,81 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
super.onWindowFocusChanged(hasFocus);
|
super.onWindowFocusChanged(hasFocus);
|
||||||
if (hasFocus) {
|
if (hasFocus && !inPictureInPicture) {
|
||||||
applyFullscreen();
|
applyFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onCastSessionStarted() {
|
||||||
|
castSessionActive = true;
|
||||||
|
PermissionHelper.remindForReceiverPlayback(this);
|
||||||
|
updatePipUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCastSessionEnded() {
|
||||||
|
castSessionActive = false;
|
||||||
|
updatePipUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePipUi() {
|
||||||
|
if (pipButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean show = castSessionActive && PictureInPictureHelper.isSupported()
|
||||||
|
&& PermissionHelper.hasReceiverPlaybackReady(this);
|
||||||
|
pipButton.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
if (show) {
|
||||||
|
pipButton.bringToFront();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryEnterPictureInPicture() {
|
||||||
|
if (!castSessionActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PermissionHelper.hasReceiverPlaybackReady(this)) {
|
||||||
|
PermissionHelper.remindForReceiverPlayback(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PictureInPictureHelper.isSupported()) {
|
||||||
|
Toast.makeText(this, R.string.pip_unavailable, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PictureInPictureHelper.isAllowedForApp(this)) {
|
||||||
|
PictureInPictureHelper.showEnableInSettingsDialog(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PictureInPictureHelper.enter(this, videoWidth, videoHeight)) {
|
||||||
|
PictureInPictureHelper.showEnableInSettingsDialog(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPipChromeVisibility() {
|
||||||
|
View handle = findViewById(R.id.settings_drawer_handle);
|
||||||
|
if (handle != null) {
|
||||||
|
handle.setVisibility(inPictureInPicture ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (pipButton != null) {
|
||||||
|
pipButton.setVisibility(inPictureInPicture || !castSessionActive ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (inPictureInPicture) {
|
||||||
|
if (diagnosticsText != null) {
|
||||||
|
diagnosticsText.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (awaitingOverlay != null && streamRendering) {
|
||||||
|
awaitingOverlay.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
stopRobotAnimation();
|
||||||
|
} else {
|
||||||
|
updateDiagnosticsVisibility();
|
||||||
|
if (!streamRendering && awaitingOverlay != null
|
||||||
|
&& awaitingOverlay.getVisibility() == View.VISIBLE) {
|
||||||
|
startRobotAnimation();
|
||||||
|
}
|
||||||
|
if (castSessionActive) {
|
||||||
|
applyFullscreen();
|
||||||
|
}
|
||||||
|
updatePipUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/src/main/res/drawable/ic_picture_in_picture.xml
Normal file
10
app/src/main/res/drawable/ic_picture_in_picture.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M19,7h-8v6h8V7zM21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z" />
|
||||||
|
</vector>
|
||||||
@@ -34,4 +34,16 @@
|
|||||||
layout="@layout/overlay_cast_diagnostics"
|
layout="@layout/overlay_cast_diagnostics"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_pip"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/pip_enter"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_picture_in_picture"
|
||||||
|
android:visibility="gone" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -195,4 +195,10 @@
|
|||||||
<string name="codec_speex">Speex</string>
|
<string name="codec_speex">Speex</string>
|
||||||
<string name="codec_opus">Opus</string>
|
<string name="codec_opus">Opus</string>
|
||||||
<string name="codec_audio_auto">AUTO (AAC)</string>
|
<string name="codec_audio_auto">AUTO (AAC)</string>
|
||||||
|
<string name="pip_enter">Картинка в картинке</string>
|
||||||
|
<string name="pip_unavailable">Режим «картинка в картинке» недоступен на этом устройстве</string>
|
||||||
|
<string name="pip_disabled_title">Картинка в картинке отключена</string>
|
||||||
|
<string name="pip_disabled_message">Разрешите картинку в картинке для Android Cast в настройках системы.</string>
|
||||||
|
<string name="pip_permissions_hint">Уведомления нужны, чтобы приёмник оставался активным при трансляции в фоне.</string>
|
||||||
|
<string name="permission_notifications_cast">Уведомления (статус трансляции)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -195,4 +195,10 @@
|
|||||||
<string name="codec_speex">Speex</string>
|
<string name="codec_speex">Speex</string>
|
||||||
<string name="codec_opus">Opus</string>
|
<string name="codec_opus">Opus</string>
|
||||||
<string name="codec_audio_auto">AUTO (AAC)</string>
|
<string name="codec_audio_auto">AUTO (AAC)</string>
|
||||||
|
<string name="pip_enter">Picture-in-picture</string>
|
||||||
|
<string name="pip_unavailable">Picture-in-picture is not available on this device</string>
|
||||||
|
<string name="pip_disabled_title">Picture-in-picture disabled</string>
|
||||||
|
<string name="pip_disabled_message">Allow picture-in-picture for Android Cast in system settings, then try again.</string>
|
||||||
|
<string name="pip_permissions_hint">Notifications keep the receiver visible while casting in the background.</string>
|
||||||
|
<string name="permission_notifications_cast">Notifications (cast status)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user