This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
SwingCam - A native Android app for recording golf swings with HTTP API control. Designed specifically for Pixel 9, leveraging its slow-motion camera capabilities. The app runs an embedded HTTP server (Ktor) allowing external triggers from golf simulators or other devices. Simple by design: just duration configuration, everything else handled by the phone's camera.
Project documentation is organized as follows:
- README.md - Main project overview, features, and getting started
- CLAUDE.md - This file - development guide for AI assistants
- TODO.md - Feature backlog with priorities
- docs/
- QUICKSTART.md - 5-minute setup guide
- USAGE.md - Detailed API documentation and examples
- ARCHITECTURE.md - Project structure and design decisions
- WEB_INTERFACE_PLAN.md - Implementation plan for web interface feature
- Language: Kotlin
- Min SDK: 26 (Android 8.0)
- Target SDK: 34 (Android 14)
- Build System: Gradle with Kotlin DSL
- Camera: CameraX Video API (androidx.camera)
- HTTP Server: Ktor (embedded Netty server on port 8080)
- Video Playback: ExoPlayer (androidx.media3) - planned
- JSON: Gson
- Architecture: Single Activity with ViewBinding
# Build debug APK
./gradlew assembleDebug
# Build release APK
./gradlew assembleRelease
# Install on connected device
./gradlew installDebug
# Clean build
./gradlew clean# Run on connected device/emulator
./gradlew installDebug
adb shell am start -n com.example.swingcam/.MainActivity
# View logs
adb logcat | grep "SwingCam\|CameraManager\|HttpServerService"No formal test suite yet. Manual testing:
- Build and install on Pixel 9
- Grant permissions (Camera, Microphone, Notifications)
- Start HTTP server in app
- Trigger recording via HTTP API
- Verify recording saves and appears in list
Quick reinstall during development:
./gradlew installDebug && adb shell am start -n com.example.swingcam/.MainActivityClear app data:
adb shell pm clear com.example.swingcamView app files:
adb shell run-as com.example.swingcam ls files/recordings-
MainActivity.kt - Main entry point
- Manages camera initialization
- Binds to HttpServerService
- Handles UI updates and recording list
- Orchestrates all components
-
HttpServerService.kt - Foreground service
- Runs Ktor embedded server on port 8080
- Implements REST API endpoints
- Communicates with MainActivity via callback interface
- Shows persistent notification with server status
-
CameraManager.kt - Camera abstraction
- Uses CameraX Video API (VideoCapture for recording)
- Uses CameraX ImageCapture for live preview snapshots
- Configures for highest quality (slow-motion)
- Auto-stops recording after configured duration
- Captures JPEG snapshots for web interface preview (~1fps)
- Callbacks for recording lifecycle events
-
RecordingRepository.kt - File management
- Manages recordings directory
- CRUD operations for recordings and metadata
- Loads/saves JSON metadata files
-
RecordingAdapter.kt - RecyclerView adapter
- Displays list of recordings
- Play/delete buttons per recording
-
Web Interface (assets/web/)
index.html- Remote web interface UIstyles.css- Golf-themed responsive stylingapp.js- Client-side JavaScript for video playback and polling- Accessible at http://[phone-ip]:8080
- Auto-plays most recent recording
- Real-time polling for new recordings (2.5s interval)
- Live camera preview (toggle on/off)
Config.kt
data class Config(
val duration: Int = 5, // Recording duration in seconds
val postShotDelay: Int = 500 // Delay after shot detection before stopping
)RecordingMetadata.kt
data class RecordingMetadata(
val filename: String,
val timestamp: String,
val duration: Int,
val fileSize: Long,
val filePath: String,
val shotMetadata: ShotMetadata? = null // Optional shot data from launch monitor
)ShotMetadata.kt
data class ShotMetadata(
val ballData: BallData? = null, // Ball flight metrics (12 fields)
val clubData: ClubData? = null, // Club performance metrics (9 fields)
val timestamp: Long = System.currentTimeMillis()
)
data class BallData(
val ballSpeed: Double? = null, // mph or km/h
val launchAngle: Double? = null, // degrees
val launchDirection: Double? = null, // degrees
val spinRate: Int? = null, // rpm
val carryDistance: Double? = null, // yards or meters
// ... 7 more optional fields (see ShotMetadata.kt)
)
data class ClubData(
val clubSpeed: Double? = null, // mph or km/h
val clubType: String? = null, // e.g., "Driver", "7-iron"
val smashFactor: Double? = null, // ratio
val attackAngle: Double? = null, // degrees
// ... 5 more optional fields (see ShotMetadata.kt)
)Recording Management:
POST /api/record- Start a recordingGET /api/status- Get camera statusGET /api/recordings- List all recordings (sorted newest first)DELETE /api/recordings/{filename}- Delete specific recordingDELETE /api/recordings- Delete all recordings
Launch Monitor Mode:
POST /api/lm/arm- Arm launch monitor (start continuous recording)POST /api/lm/shot-detected- Extract last N seconds from buffer (accepts optional ball data)POST /api/lm/cancel- Cancel launch monitor modeGET /api/lm/status- Get launch monitor status
Shot Metadata:
PATCH /api/recordings/{filename}/metadata- Update shot metadata (typically club data sent after ball data)
Web Interface:
GET /- Serve web interface (index.html)GET /styles.css- Serve CSS stylesheetGET /app.js- Serve JavaScript applicationGET /api/recordings/{filename}/stream- Stream video with HTTP range supportGET /api/camera/preview- Get live camera preview snapshot (JPEG)
- External client sends
POST /api/record - Ktor server receives request in HttpServerService
- Service calls callback to MainActivity
- MainActivity triggers CameraManager.startRecording()
- CameraManager records for configured duration
- On completion, metadata saved via RecordingRepository
- UI refreshes to show new recording
- Web interface polls and auto-updates with new recording
app/files/
├── config.json # App configuration
└── recordings/
├── swing_20241022_143530.mp4 # Video file
├── swing_20241022_143530.json # Metadata (includes optional shot data)
├── swing_20241022_144120.mp4
└── swing_20241022_144120.json
Metadata JSON Structure:
{
"filename": "swing_20241022_143530.mp4",
"timestamp": "2024-10-22 14:35:30",
"duration": 2,
"fileSize": 12450000,
"filePath": "/data/.../recordings/swing_20241022_143530.mp4",
"shotMetadata": {
"ballData": {
"ballSpeed": 165.3,
"launchAngle": 12.5,
"carryDistance": 275.5
},
"clubData": {
"clubSpeed": 112.5,
"clubType": "Driver",
"smashFactor": 1.47
},
"timestamp": 1729612530000
}
}- Uses
Quality.HIGHESTto leverage Pixel's best slow-motion mode - Back camera (rear-facing) for recording swings
- CameraX handles all encoding and frame rate optimization
- No manual FPS/resolution config - phone decides best settings
CameraManager.setupCamera()- Initialize camera (once)CameraManager.startRecording()- Begin capture- Auto-stop after
config.durationseconds viadelay() onRecordingFinishedcallback with output file- MainActivity saves metadata and refreshes UI
- Runs in foreground service to prevent Android from killing it
- Notification required for foreground service (Android 8+)
- Binds to all network interfaces (0.0.0.0)
- Port 8080 (not configurable currently)
- GSON content negotiation for JSON responses
Required permissions (AndroidManifest.xml):
CAMERA- Video recordingRECORD_AUDIO- Audio in videosINTERNET- HTTP serverFOREGROUND_SERVICE- Keep server runningPOST_NOTIFICATIONS- Show server notification (Android 13+)
Runtime permissions requested in MainActivity.checkPermissions()
- Uses app-private internal storage (
context.filesDir) - Recordings stored in
files/recordings/subdirectory - FileProvider configured for potential sharing (file_paths.xml)
- No external storage access needed
- Add route in
HttpServerService.routing { }block - Add method to
ServerCallbackinterface - Implement method in
MainActivity.createServerCallback() - Update API documentation in README.md
Example:
// In HttpServerService
get("/api/config") {
val callback = serverCallback
if (callback != null) {
call.respond(callback.getConfig())
}
}
// In ServerCallback interface
fun getConfig(): Map<String, Any>
// In MainActivity
override fun getConfig(): Map<String, Any> {
return mapOf("duration" to config.duration)
}Duration is loaded from config.json:
val config = Config.load(filesDir)To change default, edit Config.kt:
data class Config(
val duration: Int = 10 // Change default here
)Or save new config:
Config.save(filesDir, Config(duration = 10))ExoPlayer dependency already included. To implement:
- Create
VideoPlayerActivity.kt - Add layout with
PlayerView - Initialize ExoPlayer with recorded video URI
- Update
MainActivity.playRecording()to launch activity - Handle player lifecycle (pause/resume/release)
Example:
private fun playRecording(metadata: RecordingMetadata) {
val intent = Intent(this, VideoPlayerActivity::class.java).apply {
putExtra("VIDEO_PATH", metadata.filePath)
}
startActivity(intent)
}Check logs for these key messages:
- "Camera setup complete with slow-motion mode" - Camera initialized
- "Recording started: swing_*.mp4" - Recording began
- "Recording saved: /path/to/file" - Recording completed
- "Recording error: X" - Something failed
Common issues:
- Camera in use by another app
- Insufficient storage space
- Permissions not granted
- CameraX not compatible with device
Currently hardcoded to 8080. To change:
- Edit
HttpServerService.PORTconstant - Update documentation in README.md and USAGE.md
- Consider making it configurable in Config.kt
The web interface is automatically served when the HTTP server is running:
- Start the HTTP server in the app (auto-starts on launch)
- Note the IP address displayed in the app (e.g., 10.0.0.147)
- Open browser on same WiFi network:
http://[phone-ip]:8080
Features:
- Auto-plays most recent swing recording with loop
- Polls for new recordings every 2.5 seconds
- Click any recording in list to play it
- Toggle live camera preview on/off
- Responsive design (mobile, tablet, desktop)
- HTTP range request support for smooth video seeking
Debugging Web Interface:
- Check browser console for JavaScript errors
- Verify
/api/recordingsreturns data - Test video streaming:
curl -I http://[ip]:8080/api/recordings/[filename]/stream - Test preview:
curl http://[ip]:8080/api/camera/preview -o preview.jpg
- Kotlin style: Official Kotlin coding conventions
- Null safety: Use nullable types (
?) and safe calls (?.) - Coroutines: Use
lifecycleScope.launchfor async operations in MainActivity - ViewBinding: Enabled, use instead of findViewById
- Logging: Use Android Log with appropriate tags
- CameraManager: "CameraManager"
- HttpServerService: "HttpServerService"
- MainActivity: "MainActivity"
- Error handling: Try-catch with user-friendly Toast messages
- File paths: Use
Fileobjects, convert to String only when necessary
- MainActivity may be destroyed/recreated by system
- HttpServerService runs independently in foreground
- Service communicates via callback interface
- Bind/unbind service in MainActivity lifecycle
- Only one recording at a time (enforced by
isRecordingflag) - Camera must be released when activity destroyed
- CameraX requires ViewLifecycleOwner (MainActivity implements)
- Some devices may not support highest quality
- Server only accessible on local network
- No authentication/security (trust network)
- IP address changes if WiFi changes
- Port 8080 must be available
- Internal storage only (private to app)
- Videos can be large (varies by duration)
- No automatic cleanup of old recordings
- User must manually delete via app or API
- Native slow-motion support via camera hardware
- CameraX automatically uses best available quality
- Excellent low-light performance
- High frame rate recording optimized
- Runtime notification permission required
- Foreground service restrictions (camera type)
- Enhanced privacy controls
- Foreground service requires notification
- Notification channels required
- Version: 2.3.7
- Lightweight embedded server
- No external web server required
- Netty engine for performance
- Version: 1.3.1
- Modern camera API (replaces Camera2)
- Lifecycle-aware
- Consistent API across devices
- Hardware encoding support
- Version: 1.2.1
- Modern replacement for MediaPlayer
- Better format support
- Adaptive streaming capabilities
- Currently included but not used (future playback feature)
- Version: 2.10.1
- JSON serialization/deserialization
- Used for config.json and metadata files
- Simple, reliable
Manual testing checklist:
Initial Setup
- Fresh install grants all permissions
- Camera initializes successfully
- Server starts and shows IP address
Recording
- Manual button triggers recording
- HTTP API triggers recording
- Recording auto-stops after duration
- Video file saved to recordings/
- Metadata JSON created
- Recording appears in list
API
- GET /api/status returns correct data
- POST /api/record starts recording
- GET /api/recordings lists all recordings
- DELETE /api/recordings/{filename} removes recording
- DELETE /api/recordings clears all
- GET /api/recordings/{filename}/stream returns video with range support
- GET /api/camera/preview returns JPEG snapshot
- POST /api/lm/shot-detected with ball data saves metadata
- PATCH /api/recordings/{filename}/metadata updates club data
- GET /api/recordings includes shotMetadata in response
Web Interface
- Open http://[phone-ip]:8080 in browser
- Most recent recording auto-plays and loops
- Click recording in list to play it
- New recording appears within 3 seconds of creation
- Video seeking/scrubbing works smoothly
- Enable live preview shows camera feed (~1fps)
- Responsive design works on mobile and desktop
- Shot metadata displays in video player when available
- Club data auto-updates within 2.5s when added via PATCH
- Recordings list shows ball/club data summary
Edge Cases
- Recording while already recording (rejected)
- Low storage space (graceful error)
- Network change (IP address updates)
- App backgrounded during recording (continues)
- Service killed by system (restarts)
-
Remote Web View Interface (October 2025) - Fully implemented web interface for viewing/playing recordings remotely
- Access at http://[phone-ip]:8080
- Auto-play most recent recording with loop
- Real-time polling for new recordings
- HTTP range request support for video seeking
- Live camera preview (toggle on/off)
- Implementation details: WEB_INTERFACE_PLAN.md
-
Shot Metadata from Launch Monitor (October 2025) - Complete launch monitor integration with shot data capture
- Ball data fields: ball speed, launch angle, spin rate, carry distance, etc. (12 total)
- Club data fields: club speed, club path, face angle, smash factor, etc. (9 total)
- All fields optional to support different launch monitor capabilities
- Two-step workflow: ball data sent with shot-detected, club data sent separately via PATCH
- Metadata stored in recording JSON files alongside videos
- Displayed in Android app recordings list with key metrics
- Displayed in web interface with detailed grid layout
- Web interface auto-updates when club data arrives (no refresh needed)
- Complete API documentation in USAGE.md
See TODO.md for the complete feature backlog with priorities.
Some potential features to add:
-
Video Playback
- In-app ExoPlayer integration
- Slow-motion playback controls
- Frame-by-frame scrubbing
-
Configuration UI
- Settings screen for duration
- Preset durations (3s, 5s, 7s, 10s)
- Test recording feature
-
Cloud Upload
- Google Drive integration
- Automatic upload after recording
- Background upload service
-
Sharing
- Share to other apps
- Export to gallery
- Generate shareable links
-
Advanced Features
- Bluetooth remote trigger
- Voice command trigger
- Multiple camera angles (if device supports)
- Drawing/annotation on videos
Build errors:
- Sync Gradle files
- Invalidate caches and restart Android Studio
- Check JDK version (requires JDK 17)
Runtime crashes:
- Check logcat for stack traces
- Verify permissions granted
- Ensure device meets minimum SDK (26)
Camera not working:
- Check other apps aren't using camera
- Verify camera permission granted
- Try restarting device
Server not accessible:
- Verify devices on same WiFi network
- Check firewall settings
- Test with localhost (adb forward)
Recording not saving:
- Check available storage
- Verify write permissions
- Check logs for specific error