commit 7858eaeb2c6acf378123fd851e6588478ccd2fa9 Author: Nikolay Date: Fri Jul 3 16:08:02 2026 +0300 Initial commit: phonestream project diff --git a/dots.html b/dots.html new file mode 100644 index 0000000..3656c71 --- /dev/null +++ b/dots.html @@ -0,0 +1,355 @@ + + + + + +Connected Dots + + + + + + +
+

⚙ Settings

+
+ + + 80 +
+
+ + + 2.5 +
+
+ + + 30% +
+
+ + + 140 +
+
+ + + 80% +
+
+ + + 12 +
+
+ + + + + + diff --git a/exposure.sh b/exposure.sh new file mode 100755 index 0000000..2261ebc --- /dev/null +++ b/exposure.sh @@ -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 diff --git a/phonestream-tray.py b/phonestream-tray.py new file mode 100755 index 0000000..d3320c2 --- /dev/null +++ b/phonestream-tray.py @@ -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() diff --git a/phonestream.sh b/phonestream.sh new file mode 100755 index 0000000..a59391b --- /dev/null +++ b/phonestream.sh @@ -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 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c09a606 --- /dev/null +++ b/start.sh @@ -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}"