Initial commit: phonestream project

This commit is contained in:
Nikolay
2026-07-03 16:08:02 +03:00
commit 7858eaeb2c
5 changed files with 920 additions and 0 deletions
+355
View File
@@ -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
View File
@@ -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
+132
View File
@@ -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
View File
@@ -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
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# PhoneStream — быстрый запуск
# Использование: ./start.sh [профиль]
# Профили: 0=Максимум 1=Широкий 2=2.5K 3=FullHD 4=FHD60 5=HD120 6=HD240
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$SCRIPT_DIR/phonestream.sh" start "${1:-0}"