Initial commit: phonestream project
This commit is contained in:
@@ -0,0 +1,355 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Connected Dots</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #0a0a0f;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls panel */
|
||||||
|
#controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 10;
|
||||||
|
min-width: 180px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls h3 {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-row:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.ctrl-row label {
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-row input[type="range"] {
|
||||||
|
width: 90px;
|
||||||
|
accent-color: #7c6cf0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-row .val {
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 28px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggle-controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
z-index: 11;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggle-controls:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls.hidden { display: none; }
|
||||||
|
#controls.hidden + #toggle-controls { display: flex; align-items: center; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<h3>⚙ Settings</h3>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Dots</label>
|
||||||
|
<input type="range" id="dotCount" min="20" max="200" value="80">
|
||||||
|
<span class="val" id="dotCountVal">80</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Max speed</label>
|
||||||
|
<input type="range" id="speed" min="0.5" max="6" step="0.1" value="2.5">
|
||||||
|
<span class="val" id="speedVal">2.5</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Jump chance</label>
|
||||||
|
<input type="range" id="jumpChance" min="0" max="100" value="30">
|
||||||
|
<span class="val" id="jumpChanceVal">30%</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Connect dist</label>
|
||||||
|
<input type="range" id="connDist" min="50" max="300" value="140">
|
||||||
|
<span class="val" id="connDistVal">140</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Trail</label>
|
||||||
|
<input type="range" id="trail" min="0" max="95" value="80">
|
||||||
|
<span class="val" id="trailVal">80%</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label>Glow</label>
|
||||||
|
<input type="range" id="glow" min="0" max="30" value="12">
|
||||||
|
<span class="val" id="glowVal">12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="toggle-controls" onclick="document.getElementById('controls').classList.toggle('hidden')">⚙</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let W, H;
|
||||||
|
let dots = [];
|
||||||
|
let mouse = { x: -9999, y: -9999 };
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const settings = {
|
||||||
|
dotCount: 80,
|
||||||
|
maxSpeed: 2.5,
|
||||||
|
jumpChance: 0.3,
|
||||||
|
connDist: 140,
|
||||||
|
trail: 0.80,
|
||||||
|
glow: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
W = canvas.width = window.innerWidth;
|
||||||
|
H = canvas.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dot {
|
||||||
|
constructor() {
|
||||||
|
this.x = Math.random() * W;
|
||||||
|
this.y = Math.random() * H;
|
||||||
|
this.vx = (Math.random() - 0.5) * settings.maxSpeed;
|
||||||
|
this.vy = (Math.random() - 0.5) * settings.maxSpeed;
|
||||||
|
this.radius = Math.random() * 2 + 1.5;
|
||||||
|
this.hue = Math.random() * 60 + 240; // blue-purple range
|
||||||
|
this.hueSpeed = (Math.random() - 0.5) * 0.5;
|
||||||
|
this.jumpCooldown = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump() {
|
||||||
|
// Teleport to a random position far away
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const dist = Math.min(W, H) * (0.3 + Math.random() * 0.5);
|
||||||
|
this.x = W/2 + Math.cos(angle) * dist;
|
||||||
|
this.y = H/2 + Math.sin(angle) * dist;
|
||||||
|
// Clamp
|
||||||
|
this.x = Math.max(0, Math.min(W, this.x));
|
||||||
|
this.y = Math.max(0, Math.min(H, this.y));
|
||||||
|
// Random new velocity
|
||||||
|
this.vx = (Math.random() - 0.5) * settings.maxSpeed * 2;
|
||||||
|
this.vy = (Math.random() - 0.5) * settings.maxSpeed * 2;
|
||||||
|
this.jumpCooldown = 60 + Math.random() * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const spd = settings.maxSpeed;
|
||||||
|
|
||||||
|
// Maybe jump (teleport)
|
||||||
|
if (this.jumpCooldown > 0) {
|
||||||
|
this.jumpCooldown--;
|
||||||
|
} else if (Math.random() < settings.jumpChance * 0.002) {
|
||||||
|
this.jump();
|
||||||
|
} else {
|
||||||
|
// Normal wandering movement
|
||||||
|
this.vx += (Math.random() - 0.5) * 0.15 * spd;
|
||||||
|
this.vy += (Math.random() - 0.5) * 0.15 * spd;
|
||||||
|
|
||||||
|
// Clamp speed
|
||||||
|
const maxSpd = spd;
|
||||||
|
const s = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
|
||||||
|
if (s > maxSpd) {
|
||||||
|
this.vx = (this.vx / s) * maxSpd;
|
||||||
|
this.vy = (this.vy / s) * maxSpd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.x += this.vx;
|
||||||
|
this.y += this.vy;
|
||||||
|
this.hue = (this.hue + this.hueSpeed + 360) % 360;
|
||||||
|
|
||||||
|
// Soft bounce off edges
|
||||||
|
if (this.x < 0) { this.x = 0; this.vx = Math.abs(this.vx) * 0.5; }
|
||||||
|
if (this.x > W) { this.x = W; this.vx = -Math.abs(this.vx) * 0.5; }
|
||||||
|
if (this.y < 0) { this.y = 0; this.vy = Math.abs(this.vy) * 0.5; }
|
||||||
|
if (this.y > H) { this.y = H; this.vy = -Math.abs(this.vy) * 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const glow = settings.glow;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.x, this.y, this.radius + glow, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `hsla(${this.hue}, 80%, 65%, 0.08)`;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `hsla(${this.hue}, 90%, 75%, 1)`;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDots() {
|
||||||
|
dots = [];
|
||||||
|
for (let i = 0; i < settings.dotCount; i++) {
|
||||||
|
dots.push(new Dot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLines() {
|
||||||
|
const dist = settings.connDist;
|
||||||
|
const dist2 = dist * dist;
|
||||||
|
|
||||||
|
for (let i = 0; i < dots.length; i++) {
|
||||||
|
for (let j = i + 1; j < dots.length; j++) {
|
||||||
|
const dx = dots[i].x - dots[j].x;
|
||||||
|
const dy = dots[i].y - dots[j].y;
|
||||||
|
const d2 = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if (d2 < dist2) {
|
||||||
|
const alpha = (1 - Math.sqrt(d2) / dist) * 0.6;
|
||||||
|
const hue = (dots[i].hue + dots[j].hue) / 2;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(dots[i].x, dots[i].y);
|
||||||
|
ctx.lineTo(dots[j].x, dots[j].y);
|
||||||
|
ctx.strokeStyle = `hsla(${hue}, 70%, 60%, ${alpha})`;
|
||||||
|
ctx.lineWidth = 0.8;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse interaction - dots are gently pushed away
|
||||||
|
function mouseInteract() {
|
||||||
|
if (mouse.x < 0) return;
|
||||||
|
const mouseDist = 150;
|
||||||
|
for (const dot of dots) {
|
||||||
|
const dx = dot.x - mouse.x;
|
||||||
|
const dy = dot.y - mouse.y;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < mouseDist && d > 0) {
|
||||||
|
const force = (1 - d / mouseDist) * 0.8;
|
||||||
|
dot.vx += (dx / d) * force;
|
||||||
|
dot.vy += (dy / d) * force;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
// Trail effect
|
||||||
|
ctx.fillStyle = `rgba(10, 10, 15, ${settings.trail / 100})`;
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
mouseInteract();
|
||||||
|
drawLines();
|
||||||
|
|
||||||
|
for (const dot of dots) {
|
||||||
|
dot.update();
|
||||||
|
dot.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
resize();
|
||||||
|
initDots();
|
||||||
|
animate();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
resize();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
mouse.x = e.clientX;
|
||||||
|
mouse.y = e.clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mouseleave', () => {
|
||||||
|
mouse.x = -9999;
|
||||||
|
mouse.y = -9999;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch support
|
||||||
|
window.addEventListener('touchmove', (e) => {
|
||||||
|
mouse.x = e.touches[0].clientX;
|
||||||
|
mouse.y = e.touches[0].clientY;
|
||||||
|
});
|
||||||
|
window.addEventListener('touchend', () => {
|
||||||
|
mouse.x = -9999;
|
||||||
|
mouse.y = -9999;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Controls binding ───
|
||||||
|
function bindSlider(id, settingKey, suffix = '') {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const valEl = document.getElementById(id + 'Val');
|
||||||
|
|
||||||
|
el.addEventListener('input', () => {
|
||||||
|
const val = parseFloat(el.value);
|
||||||
|
settings[settingKey] = val;
|
||||||
|
valEl.textContent = el.value + suffix;
|
||||||
|
|
||||||
|
// Reinit dots if count changed
|
||||||
|
if (settingKey === 'dotCount') {
|
||||||
|
initDots();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bindSlider('dotCount', 'dotCount');
|
||||||
|
bindSlider('speed', 'maxSpeed');
|
||||||
|
bindSlider('jumpChance', 'jumpChance', '%');
|
||||||
|
bindSlider('connDist', 'connDist');
|
||||||
|
bindSlider('trail', 'trail', '%');
|
||||||
|
bindSlider('glow', 'glow');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PhoneStream Exposure — управление экспозицией камеры через ADB
|
||||||
|
# Использование: ./exposure.sh [lock|unlock|comp|iso|ev|status] [значение]
|
||||||
|
|
||||||
|
PHONE_ID="c3c2c293"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
err() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
|
||||||
|
# Проверка что стрим запущен
|
||||||
|
check_stream() {
|
||||||
|
if ! pgrep -f "scrcpy.*v4l2-sink" >/dev/null 2>&1; then
|
||||||
|
err "Стрим не запущен. Сначала: ./start.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Блокировка экспозиции (AE Lock) ───
|
||||||
|
# Используем termux-camera или напрямую через service call
|
||||||
|
# Для Android 12+ используем cmd media.camera
|
||||||
|
|
||||||
|
ae_lock() {
|
||||||
|
check_stream
|
||||||
|
# Блокируем автоэкспозицию через cmd
|
||||||
|
adb shell "cmd media.camera set-camera-control ae-lock true" 2>/dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Экспозизаза заблокирована (AE Lock ON)"
|
||||||
|
else
|
||||||
|
# Альтернативный метод через service call
|
||||||
|
warn "cmd метод не сработал, пробуем альтернативу..."
|
||||||
|
ae_lock_alt
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ae_lock_alt() {
|
||||||
|
# Через camera service напрямую
|
||||||
|
# Получаем текущие параметры экспозиции
|
||||||
|
local exposure_info=$(adb shell "dumpsys media.camera" 2>/dev/null | grep -A 2 "android.sensor.exposureTime")
|
||||||
|
echo "Текущая экспозиция: $exposure_info"
|
||||||
|
warn "Для полной блокировки используй приложение камеры на телефоне:"
|
||||||
|
echo " 1. Открой Камеру на телефоне"
|
||||||
|
echo " 2. Нажми и удерживай на объект для фиксации AE/AF"
|
||||||
|
echo " 3. Увидишь 'AE/AF Lock'"
|
||||||
|
echo " 4. Теперь запусти стрим — экспозиция сохранится"
|
||||||
|
}
|
||||||
|
|
||||||
|
ae_unlock() {
|
||||||
|
check_stream
|
||||||
|
adb shell "cmd media.camera set-camera-control ae-lock false" 2>/dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Экспозизаза разблокирована (AE Lock OFF)"
|
||||||
|
else
|
||||||
|
log "Автоэкспозизация восстановлена"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Компенсация экспозиции (EV) ───
|
||||||
|
# Шаг = 1/6 EV, диапазон обычно от -12 до +12
|
||||||
|
set_ev() {
|
||||||
|
check_stream
|
||||||
|
local ev=${1:-0}
|
||||||
|
|
||||||
|
# Проверяем диапазон
|
||||||
|
if [ "$ev" -lt -12 ] || [ "$ev" -gt 12 ]; then
|
||||||
|
err "EV должен быть от -12 до +12"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
adb shell "cmd media.camera set-camera-control ae-compensation $ev" 2>/dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Компенсация экспозиции: ${ev} EV"
|
||||||
|
else
|
||||||
|
warn "Не удалось установить EV через cmd"
|
||||||
|
echo "Попробуй вручную в приложении камеры на телефоне"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── ISO вручную ───
|
||||||
|
# Требует Android 12+ и поддержки камерой
|
||||||
|
set_iso() {
|
||||||
|
check_stream
|
||||||
|
local iso=${1:-auto}
|
||||||
|
|
||||||
|
if [ "$iso" = "auto" ]; then
|
||||||
|
adb shell "cmd media.camera set-camera-control ae-mode 1" 2>/dev/null
|
||||||
|
log "ISO: автоматический"
|
||||||
|
else
|
||||||
|
# Переводим ISO в exposure time (ns) и sensitivity
|
||||||
|
# ISO 100 = sensitivity 100
|
||||||
|
adb shell "cmd media.camera set-camera-control manual-sensitivity $iso" 2>/dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "ISO установлен: $iso"
|
||||||
|
else
|
||||||
|
warn "Не удалось установить ISO напрямую"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Статус экспозиции ───
|
||||||
|
exposure_status() {
|
||||||
|
check_stream
|
||||||
|
echo -e "${CYAN}═══ Экспозиция камеры ═══${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
adb shell "dumpsys media.camera" 2>/dev/null | grep -E \
|
||||||
|
"android.control.aeLock|android.control.aeMode|android.control.aeExposureCompensation|android.control.aeState|android.sensor.exposureTime|android.sensor.sensitivity" | \
|
||||||
|
while read line; do
|
||||||
|
echo " $line"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Меню ───
|
||||||
|
menu() {
|
||||||
|
echo -e "${CYAN}═══ PhoneStream Exposure ═══${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " 1) 🔒 Заблокировать экспозицию (AE Lock)"
|
||||||
|
echo " 2) 🔓 Разблокировать экспозицию"
|
||||||
|
echo " 3) ☀️ Установить компенсацию EV (-12..+12)"
|
||||||
|
echo " 4) 📊 Статус экспозиции"
|
||||||
|
echo " 5) 🔧 Установить ISO"
|
||||||
|
echo ""
|
||||||
|
echo " Рекомендация: открой камеру на телефоне, нажми и держи"
|
||||||
|
echo " на объект для фиксации AE/AF, потом запускай стрим"
|
||||||
|
echo ""
|
||||||
|
read -p " → Выбор: " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) ae_lock ;;
|
||||||
|
2) ae_unlock ;;
|
||||||
|
3)
|
||||||
|
read -p " EV значение (-12..+12, 0=норма): " ev
|
||||||
|
set_ev "$ev"
|
||||||
|
;;
|
||||||
|
4) exposure_status ;;
|
||||||
|
5)
|
||||||
|
read -p " ISO (100-3200, auto=автомат): " iso
|
||||||
|
set_iso "$iso"
|
||||||
|
;;
|
||||||
|
*) err "Неверный выбор" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Точка входа ───
|
||||||
|
case "${1:-menu}" in
|
||||||
|
lock) ae_lock ;;
|
||||||
|
unlock) ae_unlock ;;
|
||||||
|
comp|ev) set_ev "$2" ;;
|
||||||
|
iso) set_iso "$2" ;;
|
||||||
|
status) exposure_status ;;
|
||||||
|
menu) menu ;;
|
||||||
|
*)
|
||||||
|
echo "Использование: $0 {lock|unlock|ev|iso|status|menu} [значение]"
|
||||||
|
echo ""
|
||||||
|
echo " Команды:"
|
||||||
|
echo " lock — Заблокировать экспозицию"
|
||||||
|
echo " unlock — Разблокировать (авто)"
|
||||||
|
echo " ev <значение> — Компенсация (-12..+12, шаг 1/6 EV)"
|
||||||
|
echo " iso <значение>— ISO (100-3200, auto)"
|
||||||
|
echo " status — Текущие параметры"
|
||||||
|
echo " menu — Интерактивное меню"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PhoneStream Tray — иконка в системном трее.
|
||||||
|
Запуск: python3 phonestream-tray.py
|
||||||
|
Зависимости: gir1.2-appindicator3-0.1 (sudo apt install gir1.2-appindicator3-0.1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('AppIndicator3', '0.1')
|
||||||
|
|
||||||
|
from gi.repository import Gtk, GLib
|
||||||
|
from gi.repository import AppIndicator3 as AppIndicator
|
||||||
|
|
||||||
|
SCRIPT = "/home/nikolas/bin/phonestream/phonestream.sh"
|
||||||
|
PHONE_ID = "c3c2c293"
|
||||||
|
|
||||||
|
PROFILES = [
|
||||||
|
(0, "▶ Максимум (5MP 4:3)", "start", "0"),
|
||||||
|
(1, "▶ Широкий (4MP 16:9)", "start", "1"),
|
||||||
|
(2, "▶ 2.5K (5MP 4:3)", "start", "2"),
|
||||||
|
(3, "▶ Full HD", "start", "3"),
|
||||||
|
(4, "▶ Full HD 60fps", "start", "4"),
|
||||||
|
(5, "▶ HD 120fps (slow-mo)", "start", "5"),
|
||||||
|
(6, "▶ HD 240fps (slow-mo)", "start", "6"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def is_stream_active():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["pgrep", "-f", "scrcpy.*v4l2-sink"],
|
||||||
|
capture_output=True, text=True, timeout=2)
|
||||||
|
return r.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_phone_connected():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["adb", "devices"],
|
||||||
|
capture_output=True, text=True, timeout=3)
|
||||||
|
return PHONE_ID in r.stdout
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(args):
|
||||||
|
def _run():
|
||||||
|
try:
|
||||||
|
subprocess.run([SCRIPT] + args, capture_output=True, text=True, timeout=15)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def make_menu():
|
||||||
|
phone_ok = is_phone_connected()
|
||||||
|
stream_active = is_stream_active()
|
||||||
|
|
||||||
|
menu = Gtk.Menu()
|
||||||
|
|
||||||
|
# Статус
|
||||||
|
phone_item = Gtk.MenuItem(label=f"📱 Телефон: {'Подключён' if phone_ok else 'Нет'}")
|
||||||
|
phone_item.set_sensitive(False)
|
||||||
|
menu.append(phone_item)
|
||||||
|
|
||||||
|
stream_item = Gtk.MenuItem(label=f"📷 Стрим: {'Активен ●' if stream_active else 'Стоп ○'}")
|
||||||
|
stream_item.set_sensitive(False)
|
||||||
|
menu.append(stream_item)
|
||||||
|
|
||||||
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
if not stream_active:
|
||||||
|
# Запуск с профилем
|
||||||
|
for idx, label, action, profile_idx in PROFILES:
|
||||||
|
item = Gtk.MenuItem(label=label)
|
||||||
|
item.connect("activate", lambda i, p=profile_idx: run_script(["start", p]))
|
||||||
|
menu.append(item)
|
||||||
|
else:
|
||||||
|
# Остановка
|
||||||
|
stop_item = Gtk.MenuItem(label="⏹ Остановить")
|
||||||
|
stop_item.connect("activate", lambda i: run_script(["stop"]))
|
||||||
|
menu.append(stop_item)
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
for idx, label, action, profile_idx in PROFILES:
|
||||||
|
restart_label = label.replace("▶", "🔄")
|
||||||
|
item = Gtk.MenuItem(label=restart_label)
|
||||||
|
item.connect("activate", lambda i, p=profile_idx: run_script(["restart", p]))
|
||||||
|
menu.append(item)
|
||||||
|
|
||||||
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
status_item = Gtk.MenuItem(label="📊 Статус")
|
||||||
|
status_item.connect("activate", lambda i: run_script(["status"]))
|
||||||
|
menu.append(status_item)
|
||||||
|
|
||||||
|
quit_item = Gtk.MenuItem(label="✕ Выход из трея")
|
||||||
|
quit_item.connect("activate", lambda i: Gtk.main_quit())
|
||||||
|
menu.append(quit_item)
|
||||||
|
|
||||||
|
menu.show_all()
|
||||||
|
return menu
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
indicator = AppIndicator.Indicator.new(
|
||||||
|
"phonestream",
|
||||||
|
"camera-web",
|
||||||
|
AppIndicator.IndicatorCategory.HARDWARE
|
||||||
|
)
|
||||||
|
indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE)
|
||||||
|
indicator.set_title("PhoneStream")
|
||||||
|
indicator.set_menu(make_menu())
|
||||||
|
|
||||||
|
# Обновление меню каждые 3 сек
|
||||||
|
def refresh(ind):
|
||||||
|
ind.set_menu(make_menu())
|
||||||
|
return True
|
||||||
|
|
||||||
|
GLib.timeout_add_seconds(3, refresh, indicator)
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
Gtk.main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+256
@@ -0,0 +1,256 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PhoneStream — Redmi Note 9 Pro → виртуальная веб-камера через USB
|
||||||
|
# Использование: ./phonestream.sh [start|stop|status|menu]
|
||||||
|
|
||||||
|
V4L2_DEV="/dev/video20"
|
||||||
|
MODULE="v4l2loopback"
|
||||||
|
PHONE_ID="c3c2c293"
|
||||||
|
|
||||||
|
# Профили: описание|разрешение|fps|битрейт
|
||||||
|
PROFILES=(
|
||||||
|
"Максимум (5MP 4:3)|2608x1960|30|25M"
|
||||||
|
"Широкий (4MP 16:9)|2688x1512|30|20M"
|
||||||
|
"2.5K (5MP 4:3)|2592x1944|30|20M"
|
||||||
|
"Full HD (1080p)|1920x1080|30|10M"
|
||||||
|
"Full HD 60fps|1920x1080|60|15M"
|
||||||
|
"HD 120fps (slow-mo)|1280x720|120|10M"
|
||||||
|
"HD 240fps (slow-mo)|1280x720|240|10M"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_PROFILE=0 # Максимум по умолчанию
|
||||||
|
|
||||||
|
# ─── Цвета ───
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
err() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
|
||||||
|
# ─── Проверка зависимостей ───
|
||||||
|
check_deps() {
|
||||||
|
local missing=()
|
||||||
|
for cmd in scrcpy adb v4l2-ctl; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||||
|
done
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
err "Не найдены: ${missing[*]}"
|
||||||
|
echo " sudo apt install scrcpy v4l2loopback-dkms v4l2loopback-utils ffmpeg adb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Загрузка v4l2loopback ───
|
||||||
|
load_module() {
|
||||||
|
if lsmod | grep -q "$MODULE"; then
|
||||||
|
log "v4l2loopback уже загружен"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Загружаю v4l2loopback..."
|
||||||
|
sudo modprobe "$MODULE" devices=1 video_nr=20 card_label="PhoneCam" exclusive_caps=1
|
||||||
|
sleep 1
|
||||||
|
if [ -e "$V4L2_DEV" ]; then
|
||||||
|
log "Виртуальная камера $V4L2_DEV готова"
|
||||||
|
else
|
||||||
|
err "Не удалось создать $V4L2_DEV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Выгрузка модуля ───
|
||||||
|
unload_module() {
|
||||||
|
if ! lsmod | grep -q "$MODULE"; then
|
||||||
|
log "v4l2loopback не загружен"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# Сначала убить все scrcpy
|
||||||
|
pkill -f "scrcpy.*v4l2-sink" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
sudo rmmod "$MODULE" 2>/dev/null
|
||||||
|
log "v4l2loopback выгружен"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Проверка телефона ───
|
||||||
|
check_phone() {
|
||||||
|
if ! adb devices 2>/dev/null | grep -q "$PHONE_ID"; then
|
||||||
|
err "Телефон не подключён по USB"
|
||||||
|
echo " 1. Подключи телефон по USB"
|
||||||
|
echo " 2. Включи отладку по USB (Настройки → Для разработчиков)"
|
||||||
|
echo " 3. Разреши отладку на телефоне"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Телефон подключён: $PHONE_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Запуск стрима ───
|
||||||
|
start_stream() {
|
||||||
|
local idx=${1:-$DEFAULT_PROFILE}
|
||||||
|
local profile="${PROFILES[$idx]}"
|
||||||
|
local res=$(echo "$profile" | cut -d'|' -f2)
|
||||||
|
local fps=$(echo "$profile" | cut -d'|' -f3)
|
||||||
|
local bitrate=$(echo "$profile" | cut -d'|' -f4)
|
||||||
|
local name=$(echo "$profile" | cut -d'|' -f1)
|
||||||
|
|
||||||
|
# Проверяем не запущен ли уже
|
||||||
|
if pgrep -f "scrcpy.*v4l2-sink" >/dev/null 2>&1; then
|
||||||
|
warn "Стрим уже запущен. Останови сначала: $0 stop"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_deps
|
||||||
|
load_module
|
||||||
|
check_phone
|
||||||
|
|
||||||
|
# Закрываем камеру на телефоне
|
||||||
|
adb shell am force-stop com.android.camera 2>/dev/null
|
||||||
|
adb shell am force-stop com.android.camera2 2>/dev/null
|
||||||
|
adb shell am force-stop com.google.android.GoogleCamera 2>/dev/null
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
log "Запуск: $name (${res}@${fps}fps, ${bitrate}bps)"
|
||||||
|
|
||||||
|
local high_speed_flag=""
|
||||||
|
if [ "$fps" -gt 60 ]; then
|
||||||
|
high_speed_flag="--camera-high-speed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
scrcpy \
|
||||||
|
--no-audio \
|
||||||
|
--no-playback \
|
||||||
|
--video-source=camera \
|
||||||
|
--camera-id=0 \
|
||||||
|
--camera-size="$res" \
|
||||||
|
--camera-fps="$fps" \
|
||||||
|
--video-bit-rate="$bitrate" \
|
||||||
|
--v4l2-sink="$V4L2_DEV" \
|
||||||
|
$high_speed_flag \
|
||||||
|
2>&1 &
|
||||||
|
|
||||||
|
local pid=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log "Стрим запущен (PID: $pid)"
|
||||||
|
log "Камера доступна: $V4L2_DEV"
|
||||||
|
echo ""
|
||||||
|
echo " Для остановки: $0 stop"
|
||||||
|
else
|
||||||
|
err "Не удалось запустить стрим"
|
||||||
|
wait "$pid" 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Остановка ───
|
||||||
|
stop_stream() {
|
||||||
|
pkill -f "scrcpy.*v4l2-sink" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
# Принудительно если не закрылся
|
||||||
|
pkill -9 -f "scrcpy.*v4l2-sink" 2>/dev/null
|
||||||
|
log "Стрим остановлен"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Статус ───
|
||||||
|
status() {
|
||||||
|
echo -e "${CYAN}═══ PhoneStream Status ═══${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# v4l2loopback
|
||||||
|
if lsmod | grep -q "$MODULE"; then
|
||||||
|
echo -e " Модуль v4l2loopback: ${GREEN}загружен${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Модуль v4l2loopback: ${RED}не загружен${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# /dev/video20
|
||||||
|
if [ -e "$V4L2_DEV" ]; then
|
||||||
|
echo -e " Виртуальная камера: ${GREEN}$V4L2_DEV${NC}"
|
||||||
|
local info=$(v4l2-ctl -d "$V4L2_DEV" --all 2>/dev/null | grep "Width/Height" | head -1)
|
||||||
|
if [ -n "$info" ]; then
|
||||||
|
echo -e " Разрешение: ${GREEN}$info${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " Виртуальная камера: ${RED}отсутствует${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Телефон
|
||||||
|
if adb devices 2>/dev/null | grep -q "$PHONE_ID"; then
|
||||||
|
echo -e " Телефон: ${GREEN}подключён${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Телефон: ${RED}не подключён${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Процесс
|
||||||
|
local pid=$(pgrep -f "scrcpy.*v4l2-sink" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$pid" ]; then
|
||||||
|
echo -e " Стрим: ${GREEN}активен (PID: $pid)${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Стрим: ${RED}не активен${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Меню выбора профиля ───
|
||||||
|
menu() {
|
||||||
|
echo -e "${CYAN}═══ PhoneStream — Выбери профиль ═══${NC}"
|
||||||
|
echo ""
|
||||||
|
for i in "${!PROFILES[@]}"; do
|
||||||
|
local name=$(echo "${PROFILES[$i]}" | cut -d'|' -f1)
|
||||||
|
local res=$(echo "${PROFILES[$i]}" | cut -d'|' -f2)
|
||||||
|
local fps=$(echo "${PROFILES[$i]}" | cut -d'|' -f3)
|
||||||
|
if [ "$i" -eq "$DEFAULT_PROFILE" ]; then
|
||||||
|
echo -e " ${GREEN}$i)${NC} $name ${YELLOW}[по умолчанию]${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}$i)${NC} $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -p " → Выбор [0]: " choice
|
||||||
|
choice=${choice:-$DEFAULT_PROFILE}
|
||||||
|
|
||||||
|
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -lt "${#PROFILES[@]}" ]; then
|
||||||
|
start_stream "$choice"
|
||||||
|
else
|
||||||
|
err "Неверный выбор"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Точка входа ───
|
||||||
|
case "${1:-start}" in
|
||||||
|
start)
|
||||||
|
start_stream "$2"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_stream
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
stop_stream
|
||||||
|
sleep 1
|
||||||
|
start_stream "$2"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
menu)
|
||||||
|
menu
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Использование: $0 {start|stop|restart|status|menu} [номер_профиля]"
|
||||||
|
echo ""
|
||||||
|
echo " Профили:"
|
||||||
|
for i in "${!PROFILES[@]}"; do
|
||||||
|
local name=$(echo "${PROFILES[$i]}" | cut -d'|' -f1)
|
||||||
|
echo " $i — $name"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo " Примеры:"
|
||||||
|
echo " $0 # Запуск с максимальным профилем"
|
||||||
|
echo " $0 start 3 # Запуск Full HD"
|
||||||
|
echo " $0 stop # Остановка"
|
||||||
|
echo " $0 menu # Интерактивный выбор"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user