22 KiB
Video Downloader — 모바일 연동 + 모바일 앱 계획서 (v1)
목표: Windows 비디오 다운로더(내장 브라우저)에 모바일 앱을 만들고 PC↔모바일을 연동한다. 핵심 질문 — "모바일에서 이 앱과 무엇을 하게 할 것인가?"에 대한 조사 기반 답변.
작성일: 2026-08-15 · 관련 문서:
BROWSER_FEATURES_PLAN.md(v2, 데스크톱 브라우저 기능)조사 방법론: 4개 전문 조사팀 병렬 심층 조사 — ① 모바일 브라우저 UX(Chrome/Safari/Firefox/Samsung/Brave 모바일) ② PC↔모바일 연동 패턴(20여 개 제품: Chrome Send Tab, Handoff, Phone Link, LocalSend, KDE Connect, Jellyfin 등) ③ 모바일 영상 생태계(iOS/Android 제약, 다운로더 앱 생존 모델, 미디어 서버) ④ 기술 스택 검증(실제 코드베이스 grep 포함). 총 73회 웹 검색 + 공식 문서 열람.
0. 핵심 결론 (Executive Summary)
- 제품 형태 = "컴패니언 앱" 우선. PC가 일꾼(추출·다운로드), 폰은 리모컨+뷰어. 이는 Jellyfin/VLC 리모컨/Chromecast 모델로 검증된 구조이며, 양쪽 앱스토어 정책 리스크도 최소화함.
- 폰에서: ① 링크 보내 PC가 받게 하기 ② 다운로드 큐 원격 제어(진행률 실시간) ③ PC 라이브러리 스트리밍 시청 ④ 완료된 영상 폰으로 저장(오프라인)
- 아키텍처 = 클라우드 없는 LAN 직결. 데스크톱 앱에 Kestrel 서버를 내장(REST + SignalR), QR 코드로 페어링(주소+토큰 동시 전달). LocalSend/KDE Connect가 증명한 계정 없는 지속 가능한 모델. 원격 접근은 자체 릴레이 대신 Tailscale/VPN 문서화.
- 기술 스택 = .NET MAUI 10 (net10.0-android/ios). 기존
VideoDownloader.Core(net8.0) 엔진을 ~90% 그대로 재사용 — 모바일 비호환 지점은 단 4곳(§4 검증 표). Flutter/네이티브는 Core 재사용이라는 이 프로젝트의 존재 이유를 포기해야 하므로 기각. Avalonia는 데스크톱 실험용 유지. - 플랫폼 전략 = Android 먼저(APK/GitHub 배포), iOS는 컴패니언 프레이밍.
- Android: 자유도 최대 — 나중에 독립 다운로더 모드 추가 가능(YoutubeExplode 순수 C#).
- iOS: WebKit 강제 사실상 지속 + Apple이 Brave의 오프라인 영상 저장을 금지한 전례 → 기기 내 추출 코드 배제, "내 PC 라이브러리 원격 시청" 프레이밍만.
- 제로 설치 웹 컴패니언 겸비: 데스크톱 앱이 폰 브라우저용 페이지를 직접 서빙 → 앱 설치 없이 QR 찍으면 즉시 큐 모니터링 가능(1차 검증 수단).
1. 조사 결과 종합
1.1 모바일 브라우저 UX 관례 (앱 UI의 기준선)
| 관례 | 내용 | 출처 |
|---|---|---|
| 하단 주소창 기본 | Chrome 2025 정식 탑재(롱프레스로 전환), Safari/iOS 26도 상하단 선택. 단 Chrome의 실수(탭 버튼 숨김)는 반면교사 — 하단 바에 탭 버튼 유지 | Chrome/Safari |
| 전체 화면 탭 그리드 | 카드 썸네일 + 스와이프 닫기 + 탭 검색, 탭 수 배지 | 전 브라우저 |
| 제스처 | 화면 엣지 스와이프=뒤로/앞으로, 주소창 가로 스와이프=탭 전환, 당겨서 새로고침(비활성화 가능), 툴바 아래로 스와이프=몰입 모드 | Chrome/Safari/Samsung |
| 다운로드 관리자 | ⋮ 메뉴 속 풀페이지 화면(데스크톱 셸프와 다름). 우리 앱은 다운로드가 본업이므로 하단 바 전용 슬롯 + 감지 배지로 승격 | Chrome Android |
| 영상 감지 UI | Samsung Video Assistant(영상 위 오버레이 → 팝업 플레이어), Brave Playlist(탭 아이콘 → 재생목록 추가) — 데스크톱 감지 배너의 모바일 등가물 | Samsung/Brave |
| 리더/다크 | URL 옆 아이콘(Safari 배치), 시스템 테마 추종 + 강제 다크 토글(Firefox Night Mode) | Safari/Firefox |
1.2 PC↔모바일 연동 — 제품별 매트릭스 (20개 제품 조사)
| 제품 | 전송물 | 페어링 | 구조 |
|---|---|---|---|
| Chrome Send Tab | 탭/링크 | 계정(클라우드) | 클라우드 동기화+푸시 |
| Safari Handoff | 앱 상태 통째로 | iCloud+BLE 근접 | P2P Wi-Fi(AWDL) |
| Phone Link | 알림/SMS/사진 | BT+MS 계정 | 하이브리드 |
| Firefox Send Tab | 탭 | QR 페어링(FxA) | E2E 암호화 릴레이 |
| LocalSend | 파일/텍스트 | 같은 Wi-Fi+PIN | 순수 LAN, 계정 없음 |
| KDE Connect | 파일/클립보드/알림/입력 | LAN+TLS 인증서 확인 | 순수 LAN |
| Jellyfin | 라이브러리/제어 | 서버 URL+API 키 | 자체 호스팅 REST+WebSocket |
| VLC 리모컨 | 재생 제어 | 없음(HTTP 인터페이스) | 순수 LAN |
| Chromecast | 미디어 URL 위임 | mDNS | 폰=리모컨만, 미디어는 직접 |
| Intel Unison | 파일/SMS/알림 | 계정 | 2025-06 서비스 종료 ⚠️ |
도출된 원칙:
- 계정 기반 클라우드 동기화(Chrome/Edge)는 인프라+유지 비용 — 인디 규모에서 Unison처럼 죽는다. 계정 없는 LAN 모델이 지속 가능한 니치.
- QR 페어링 = 발견+키 교환을 동시에 해결하는 최고의 UX 트릭(WhatsApp Web/Firefox).
http://<ip>:<port>/#t=<토큰>. - LocalSend 보안 모델 차용: 자가서명 인증서 지문 양쪽 화면 표시(목측 검증), 선택적 PIN, 전송 전 수락 프롬프트("폰이 다운로드 3건 요청 — 수락?").
- 크로미캐스트 교훈: 폰은 URL/제어 패킷만 보내고 미디어 바이트를 프록시하지 않는다. 폰 앱은 큐 대상 stateless 설계(앱을 꺼도 PC 다운로드는 계속).
- 미러링하지 말 것: 알림 미러링(특권+유지비), BLE 근접(데스크톱 전파 신뢰성 낮음), 상시 클립보드 동기화(프라이버시 표면).
1.3 모바일 영상 생태계 — 플랫폼 제약표
| 항목 | iOS | Android |
|---|---|---|
| 다운로더 정책 | 5.2.2 위반 시 리젝. Brave Playlist 오프라인 저장 금지 전례. 생존 모델 = "파일 관리자+브라우저"(Documents by Readdle) 프레이밍 | Play도 실질 금지(TubeMate 퇴출). 탈출구 = APK/F-Droid/GitHub 배포(NewPipe·Seal·YTDLnis 전부) |
| 백그라운드 다운로드 | NSURLSession 백그라운드(앱 종료해도 계속, 강제종료 시 예외) |
FGS dataSync(~6h 제한·폐지 예정) → WorkManager가 정답(YTDLnis 방식) |
| 백그라운드 재생/PiP | Background Modes + AVAudioSession .playback |
mediaPlayback FGS + 상시 알림 |
| 파일 저장 | 샌드박스만. 사진 보관함/Files 내보내기 | MediaStore.Downloads(자기 파일은 권한 불필요) |
| 스트리밍 프로토콜 | AVPlayer = HLS만, DASH 불가 | ExoPlayer/Media3 = HLS+DASH 네이티브 |
| yt-dlp 실행 | 불가능(서브프로세스 금지) | 가능(Termux제 Python 번들) — 단 GPL, Play 배포 불가 |
- 미디어 서버 참고: Jellyfin(REST+WebSocket, 다운로드/포지션 동기화), Streamyfin(클라이언트 측 HLS→로컬 파일 변환으로 "다운로드" 구현 — 재생 가능=다운로드 가능 구조), Plex(코덱 호환 시 원본 직접, 아니면 트랜스코드).
- 오프라인 라이브러리 UX 기대치(Netflix/Spotify): 전용 다운로드 탭, 화질/크기 선택기+저장공간 대시보드, 백그라운드 큐, 재시작 생존.
- 수요 배경: YouTube가 2026년 비프리미엄 백그라운드 재생 루트를 봉쇄 중 → "내 라이브러리 무제한 백그라운드/PiP"는 진짜 차별점.
1.4 기술 스택 검증 (실제 코드 grep 기반)
스택 비교 평결:
| 스택 | Core 재사용 | 모바일 생태계 | 평결 |
|---|---|---|---|
| .NET MAUI 10 | ~100% (컴패니언+독립 모두) | SignalR 공식 지원, ZXing.Net.Maui, MediaElement | ✅ 채택 |
| Avalonia 12 모바일 | 동일 | 얇음(QR 스캐너/플레이어 없음, 네이티브 직접 붙여야) | 데스크톱 실험 유지 |
| Flutter | 0% (Dart 재작성) | 최고 | 기각 — Core 재사용 포기 |
| Kotlin/Swift 네이티브 | 0% | 최고 | 기각 — 코드 2벌 |
버전 주의: MAUI 8/9는 이미 지원 종료 → 신규 MAUI 앱은 .NET 10 대상 필수. 다행히 net10.0-android는 기존 net8.0 Core 어셈블리를 그대로 참조 가능.
VideoDownloader.Core 모바일 공유 분석 (파일:라인 검증 완료):
| 상태 | 위치 | 내용 |
|---|---|---|
| ✅ 그대로 사용 | Hls/HlsDownloader(병합 호출 제외), M3u8Parser, Dash/DashParser·Models, Models/*, Social/CookieRecord·FormatOption, Util/FileNameUtil |
HttpClient 기반 — 모바일 완전 호환 |
| ⚠️ 추상화 필요 | Social/YtDlpRunner.cs:131-181, DenoRunner.cs |
Process.Start → IProcessRunner/IToolLocator 인터페이스로 추출 |
| ⚠️ 추상화 필요 | Hls/HlsDownloader.cs:237, Dash/DashDownloader.cs:71 |
ffmpeg 서브프로세스 → IMediaMuxer |
| ⚠️ 추상화 필요 | Config/AppConfig.cs |
SpecialFolder.MyVideos → 플랫폼 경로 주입 |
→ 추상화 4곳만 하면 Core ~90% 공유. 이 리팩터는 BACKLOG의 Avalonia 이관 로드맵 2단계(공유 리팩터)와 동일한 작업 — 한 번의 리팩터로 모바일+크로스플랫폼 모두 준비됨 (시너지).
Android 독립 모드용 YouTube: Python 번들 대신 YoutubeExplode(순수 C#, Tyrrrz의 YoutubeDownloader 15.8k★가 사용) — MAUI 안에서 동작.
2. 유즈케이스 정의 (컴패니언 앱)
| # | 유즈케이스 | 흐름 | 근원 패턴 |
|---|---|---|---|
| U1 | 링크 → PC로 받기 | 폰 공유 시트(YouTube/웹페이지) → "PC에서 받기" → PC 대기열 즉시 추가 + 수락 프롬프트 | Chrome Send Tab + LocalSend 수락 프롬프트 |
| U2 | 큐 원격 제어 | 큐 목록 실시간(진행률/속도), 일시정지/재개/취소/재시도 | Jellyfin 리모컨 |
| U3 | 라이브러리 스트리밍 | PC 저장 폴더 그리드(썸네일+메타) → 탭하여 시청(Range HTTP 스트리밍, 이어보기 위치 양방향 동기화) | Jellyfin/Streamyfin |
| U4 | 폰으로 저장(오프라인) | 라이브러리 항목 다운로드 → 앱 오프라인 라이브러리(Neflix식 다운로드 탭) | Plex Downloads |
| U5 | 완료 알림 | PC에서 다운로드 완료 → 폰 푸시(연결 중일 때) | — |
| U6 | 웹 컴패니언(제로 설치) | 폰 카메라로 QR → 폰 브라우저에서 바로 큐/라이브러리 열람 | PairDrop/Jellyfin 웹 |
| U7 | (후기) Android 독립 다운로더 | PC 없이 폰에서 직접 HLS/DASH/YouTube 받기 | YTDLnis/YoutubeExplode |
3. 아키텍처 설계
┌────────────────── Windows 앱 (WPF) ──────────────────┐
│ WebView2 브라우저 + 감지 DownloadManager (Core) │
│ ▲ │
│ ┌───────────────┴────────────────┐ │
│ VideoDownloader.Server (신규 클래스 라이브러리) │
│ · Kestrel (동적 포트, ListenAnyIP) │
│ · Bearer 페어링 토큰 (256bit, QR로 배포) │
│ · mDNS 광고 (_videodl._tcp, Makaretu.Dns) │
│ · 정적 웹 컴패니언 페이지 서빙 (제로 설치 클라이언트) │
└──────┬───────────────┬──────────────┬────────────────┘
│ LAN HTTP(S) │ SignalR │ Range HTTP
┌──────▼─────┐ ┌──────▼─────┐ ┌─────▼──────┐
│ MAUI 앱 │ │ 폰 브라우저 │ │ 미디어 │
│ (Android→ │ │ (웹 컴패니언)│ │ 재생 │
│ iOS) │ └────────────┘ └────────────┘
└────────────┘
API 설계안 (LocalSend 프로토콜 + Jellyfin 참고):
POST /api/pair— 토큰 검증 + 기기명 등록POST /api/queue— URL 추가(U1; 데스크톱 수락 프롬프트 연동)GET /api/queue·POST /api/queue/{id}/pause|resume|cancel|retry— 큐 제어(U2)GET /api/library(+/{id}/thumb) — 메타데이터 카탈로그(U3; ffprobe/FFMpegCore 썸네일, 사이드카 캐시)GET /stream/{id}— MP4 Range 요청(206) 스트리밍(U3;Results.File(enableRangeProcessing:true))GET /file/{id}— 폰 저장용 다운로드(U4)/hubs/queue— SignalR 진행률/로그 푸시(U2/U5; v1은 2초 폴링으로도 가능)POST /api/position/{id}— 이어보기 위치 동기화(U3)
보안 모델: 페어링 토큰(Bearer, 상수시간 비교) + 재페어링 시 재생성. LAN HTTP는 iOS ATS NSAllowsLocalNetworking 예외로 허용. 원격(외부망)은 자체 릴레이 대신 Tailscale 문서화(Syncthing 커뮤니티 표준 패턴).
데스크톱 UI 연결점: 설정에 "모바일 연동" 섹션 → QR 다이얼로그(QRCoder) + 페어링된 기기 목록 + 원격 액세스 토글. 브라우저 계획(v2)의 명령 팔레트에 "기기로 보내기" 액션 추가 가능.
4. 모바일 앱 기능 목록 (Tier)
Tier M1 — 최소 컴패니언 (Android APK)
- QR 스캔 페어링(ZXing.Net.Maui) + 수동 IP:포트 폴백
- 큐 목록 + 실시간 진행률(SignalR or 폴링) + 일시정지/재개/취소/재시도
- 공유 타겟: 다른 앱에서 URL 공유 → "PC에서 받기" (U1)
- 링크 붙여넣기 직접 추가
- 웹 컴패니언 페이지(데스크톱이 서빙) — 앱 없이 검증용
Tier M2 — 라이브러리 + 시청
- 라이브러리 그리드(썸네일·제목·해상도·크기·추가일)
- MediaElement 스트리밍 재생(Range HTTP) — 백그라운드 오디오
- 이어보기 위치 동기화
- 폰으로 다운로드(오프라인 저장) + 다운로드 탭(Neflix식) + Android 진행 알림
- Android 재생중 미디어 알림(FFS mediaPlayback)
Tier M3 — iOS 확장
- net10.0-ios 타겟 + Info.plist 키 3종(ATS/로컬네트워크/Bonjour)
- TestFlight 배포, "내 PC 라이브러리 리모컨" 프레이밍 (기기 내 추출 코드 엄격 배제)
Tier M4 — Android 독립 다운로더 (선택, PC 없는 모드)
- Core HLS/DASH/직접 다운로더 + 모바일 muxer 어댑터
- YoutubeExplode로 YouTube 직접 수신
- WorkManager 백그라운드 큐 + 재시작 생존
- 내장 브라우저 탭(하단 주소창·탭 그리드·제스처 관례 준수, §1.1) + 감지 배지
- 저장: MediaStore.Downloads
시기상조/미차용 (명시적 제외)
- 클라우드 동기화/계정, 알림 미러링, 클립보드 동기, BLE 근접 전송, 자체 릴레이 서버, iOS 기기 내 yt-dlp(기술적·정책적 불가)
5. 로드맵 (의존성 순)
Phase M0 — 데스크톱 준비 (필수 선행, 1~2주)
- Core 추상화 리팩터:
IProcessRunner·IToolLocator·IMediaMuxer·경로 제공자 인터페이스 추출 (데스크톱 구현은 무변화, 기존 4개 프로젝트 무영향. BACKLOG Avalonia 로드맵 2단계와 동일 작업) VideoDownloader.Server신규 라이브러리: Kestrel 내장 + 토큰 인증 +/api/pair·/api/queue+ SignalR 허브- WPF 설정에 QR 페어링 다이얼로그(QRCoder) + mDNS 광고(Makaretu.Dns)
- 검증 관문: 폰 브라우저로 QR 스캔 → 웹 컴패니언에서 큐 조회/추가 동작 (앱 개발 전 서버 완성 입증)
- ⚠️ git 아님 →
git init후 착수 (브라우저 계획 Phase A와 동일 전제)
Phase M1 — Android 컴패니언 앱 (2~3주)
- MAUI 10(net10.0-android) 신규 프로젝트
VideoDownloader.Mobile+ Core 참조 - Tier M1 전체. APK/GitHub Releases 배포 (Play 정책 회피 — YTDLnis 선례)
Phase M2 — 라이브러리/시청 (2~3주)
- 데스크톱: ffprobe 메타데이터 + 썸네일 캐시,
/api/library·/stream/{id}(Range) - 앱: Tier M2 전체
Phase M3 — iOS (1~2주 + 심사)
- Tier M3. TestFlight 우선.
Phase M4 — Android 독립 모드 (선택, M2 이후 언제든)
- Tier M4. 브라우저 계획(BROWSER_FEATURES_PLAN v2)의 탭 아키텍처 경험을 모바일 WebView UI에 재활용.
브라우저 계획과의 순서 권고: 데스크톱 브라우저 기능(탭 등)과 모바일은 독립 트랙. 다만 Core 추상화(M0-1)만 먼저 해두면 양쪽이 자유로워짐. 서버(M0-2)는 브라우저 작업과 무관하게 병행 가능.
6. 리스크 & 대응
| 리스크 | 대응 |
|---|---|
| MAUI 품질(iOS 성능/바인딩 이슈 보고) | 컴패니언 규모(리스트·플레이어·스캐너)는 수용 가능한 범위. Android 먼저 출시해 리스크 분산 |
| iOS 5.2.2 리젝 (다운로더 오인) | 기기 내 추출 코드 배제 + "내 PC 리모컨" 프레이밍 + Brave 전례 문서화. TestFlight로 시작 |
| Windows 방화벽 첫 프롬프트 | 최초 비로컬 바인딩 시점에 안내 UI("허용" 가이드) |
| 멀티캐스트 차단 네트워크(기업/캠퍼스) | QR 페어링이 주 경로(발견 불필요) + 로컬 IP 프로브 폴백(LocalSend 방식) |
| iOS 로컬 네트워크 권한 거부 시 무음 실패 | 권한 거부 감지 → 설정 안내 화면 |
| Play 정책(독립 모드) | APK/GitHub 배포로 회피. Play 출시 시 사이트 특화 추출 코드 제거 빌드 |
| AVPlayer DASH 불가 | 컴패니언은 MP4/HLS 스트리밍만(앱 원본은 대부분 MP4). DASH 원본은 데스크톱에서 필요 시 ffmpeg 실시간 트랜스코드(후순위, Jellyfin 계층 구조 참고) |
| yt-dlp 인자 인젝션(YTDLnis RCE 전례, SonarSource) | U1 공유 URL 화이트리스트 검증 + 인자 배열 방식(이미 데스크톱 OpenInExplorer에서 동일 교훈 적용 완료) |
7. 성공 기준 (완료 정의)
- PC 앱 켠 상태에서 폰으로 QR 스캔 → 설치 없는 웹 컴패니언에서 큐 조회/URL 추가 성공 (M0)
- Android 앱: 공유 시트로 YouTube 링크 → PC 대기열 자동 추가 + PC 수락 → 다운로드 시작 (M1)
- Android 앱: 큐 진행률 실시간 갱신 + 일시정지/재개/취소 동작 (M1)
- 폰에서 PC 라이브러리 그리드 탐색 → 스트리밍 재생(탐색/시크 포함) → 앱 종료 후 재개 시 이어보기 (M2)
- 라이브러리 항목 폰 저장 → 오프라인 재생 (M2)
- iOS TestFlight 빌드 시동 + 페어링 + 큐 조회 (M3)
dotnet build전체 0 오류 — 기존 4개 프로젝트(WPF/Avalonia/Cli/Core) 회귀 없음 (M0~전체)- 외부망에서 연결 시도 → 명확한 "같은 네트워크 필요/Tailscale 안내" 메시지 (UX)
출처 (핵심)
- 연동 패턴: LocalSend 프로토콜 v2.2 · KDE Connect · Firefox FxA 페어링 아키텍처 · Mozilla Sync E2E · Syncthing P2P 분석 · Intel Unison 종료
- 모바일 브라우저 UX: Chrome 하단 주소창 · Samsung Video Assistant · Brave Playlist · iOS 오프라인 금지 공지
- 플랫폼 제약: Apple 5.2.2 가이드라인 · Android 14 FGS 타입 · iOS 백그라운드 다운로드 · TN3179 로컬 네트워크 프라이버시 · AVPlayer HLS-only
- 생태계: youtubedl-android · YTDLnis · YTDLnis 인자 인젝션 RCE · Streamyfin · Jellyfin 트랜스코딩 · Documents by Readdle
- 기술 스택: MAUI 지원 정책 · Rick Strahl: 데스크톱 내장 ASP.NET 서버 · SignalR 지원 플랫폼 · ZXing.Net.Maui · MAUI MediaElement · Makaretu.Dns.Multicast · QRCoder · YoutubeExplode · Kestrel Range 처리