AI-powered web application for analyzing volleyball plays using video frame extraction and GPT-4 Vision.
- Web App (
web-application/): volleyball-analytics web application (Express + frontend UI). - Function App (
function-app/): volleyballanalytics Azure Function(s) for motion-based video trimming.
- Video Upload: Upload volleyball game footage for AI analysis
- Frame Extraction: Uses ffmpeg to extract frames at configurable rates
- GPT-4 Vision Analysis: AI analyzes actual video frames to see player movements
- Configurable Cost Control: Adjust frame rate and max frames to control API costs
- Text-Based Analysis: Describe plays to get instant coaching feedback
- Motion-Based Trimming: Automatically remove non-play time from single-camera static recordings using frame-difference detection (no AI required)
- Detailed Coaching Insights:
- Play type identification
- Player positioning analysis
- Technical execution feedback
- Tactical suggestions
- Recommended drills
- Node.js 18+
- ffmpeg installed and in PATH
- Windows:
choco install ffmpegor download from https://ffmpeg.org - macOS:
brew install ffmpeg - Linux:
apt install ffmpeg
- Windows:
-
Install dependencies (web app)
cd web-application npm install -
Configure environment
cp .env.example .env # Edit .env and add your OpenAI API key -
Build the web project
npm run build
-
Start the server
npm start
-
Open http://localhost:3000 in your browser
The Azure Functions app lives in function-app/.
cd function-app
npm install
npm run buildThe build output (host.json, function.json shims, and compiled handlers) is generated under function-app/dist for deployment.
To persist uploaded and processed videos in Azure Blob Storage, set the following environment variables in .env:
AZURE_STORAGE_CONNECTION_STRINGAZURE_STORAGE_CONTAINER(defaults tovolleyball-videos)AZURE_STORAGE_INPUT_FOLDER(defaults toinputs)AZURE_STORAGE_OUTPUT_FOLDER(defaults toprocessed)
When unset, the app falls back to local disk storage under uploads/.
| Option | Default | Range | Description |
|---|---|---|---|
| Frames per Second | 1 | 0.1-5 | How many frames to extract per second |
| Max Frames | 15 | 1-50 | Maximum frames to send to AI |
| Video Length | 1 fps | 2 fps |
|---|---|---|
| 10 seconds | ~$0.03 | ~$0.05 |
| 15 seconds | ~$0.04 | ~$0.07 |
| 30 seconds | ~$0.07 | ~$0.12 |
Costs are approximate and may vary
Run in development mode with hot reload:
npm run devUpload a video file for analysis.
- Body:
multipart/form-datawithvideofile and optionaldescription - Response: Analysis results with coaching suggestions
Analyze a video from a direct public URL (e.g. Google Drive, Dropbox, or any cloud storage link).
- Body:
{ "url": "https://...", "description": "..." } - Response: Analysis results with coaching suggestions
Analyze a play based on text description.
- Body:
{ "description": "Your play description" } - Response: Analysis results with coaching suggestions
- Backend: Node.js, Express, TypeScript
- AI: OpenAI GPT-4o Vision API
- Video Processing: ffmpeg via fluent-ffmpeg
- Frontend: Vanilla JavaScript, CSS3
- File Upload: Multer
volleyball-analytics/
├── web-application/ # Web application project
│ ├── src/
│ │ ├── index.ts # Express server
│ │ ├── routes/
│ │ │ └── videoRoutes.ts # API routes
│ │ └── services/
│ │ ├── videoAnalyzer.ts # AI analysis service
│ │ ├── frameExtractor.ts # ffmpeg frame extraction
│ │ ├── motionDetector.ts # Motion-based play segment detection
│ │ └── videoTrimmer.ts # ffmpeg segment concatenation
│ ├── public/
│ │ ├── index.html # Frontend UI
│ │ ├── styles.css # Styles
│ │ └── app.js # Frontend logic
│ ├── uploads/ # Uploaded videos & extracted frames
│ └── dist/ # Compiled JavaScript for the web app
└── function-app/ # Azure Function App project (trimVideo function)
├── src/functions/ # Azure Functions (trimVideo)
├── src/services/ # Shared motion/trim pipeline logic for Functions
├── scripts/copyFunctions.js # Builds deployable host.json/function.json shims
└── dist/ # Compiled Function App assets
Upload a video file for frame-by-frame AI analysis.
Body (multipart/form-data):
video- Video file (required)description- Play context (optional)framesPerSecond- Frame extraction rate (default: 1)maxFrames- Maximum frames to analyze (default: 15)
Analyze a video provided as a URL.
Body (application/json):
url- Direct URL to a publicly accessible video file (MP4, WebM, MOV, AVI) hosted on any cloud storage (e.g. Google Drive, Dropbox) — requireddescription- Play context (optional)framesPerSecond- Frame extraction rate (default: 1)maxFrames- Maximum frames to analyze (default: 20)
Note: The URL must point directly to a downloadable video file. The file size limit is 100 MB.
curl -X POST http://localhost:3000/api/videos/analyze-url \
-H "Content-Type: application/json" \
-d '{"url":"https://storage.example.com/game-clip.mp4","description":"spike attempt from position 4"}'Body (application/json):
{ "description": "Your play description" }POST /api/videos/trim removes non-play periods from a static-camera recording using motion detection.
POST /api/videos/import-from-url queues the URL-based import pipeline for a publicly accessible video file.
No AI or third-party services are required — only ffmpeg.
The web application must know the Function App URL through VIDEO_URL_IMPORT_FUNCTION_URL; deployed environments should set this explicitly rather than relying on the local 127.0.0.1:7071 development fallback.
- Frames are sampled from the video at a configurable rate (default 2 fps) and scaled down to 160×90 grayscale pixels.
- The mean absolute pixel difference between consecutive frames is computed as the motion score.
- Scores are smoothed with a rolling-average window to reduce noise.
- Frames above a configurable threshold are marked as "active".
- Active runs are grouped into segments, short segments are dropped, and configurable pre/post-roll padding is added.
- Overlapping padded segments are merged; the final list is fed to ffmpeg
trim + concatto produce a clean MP4.
POST /api/videos/trim — multipart/form-data
| Field | Type | Default | Description |
|---|---|---|---|
video |
File | optional | Video file (MP4, WebM, MOV, AVI) |
videoUrl |
string | — | Direct public HTTP(S) link to a video file (alternative to uploading) |
sampleFps |
number | 2 |
Frames to sample per second for motion analysis |
threshold |
number | 0.02 |
Motion score threshold (0–1); lower = more sensitive |
minSegmentLength |
number | 3 |
Minimum play-segment length in seconds |
preRoll |
number | 1 |
Seconds of context to keep before each segment |
postRoll |
number | 1 |
Seconds of context to keep after each segment |
smoothingWindow |
number | 3 |
Rolling-average window size for score smoothing |
Success response (200):
{
"success": true,
"totalSegments": 4,
"segments": [
{ "start": 12.5, "end": 38.0 },
{ "start": 55.0, "end": 92.5 }
],
"downloadUrl": "/uploads/trimmed-1234567890-123456789.mp4"
}Error response (422) when no motion is detected:
{
"error": "No motion segments detected. Try lowering the threshold.",
"segments": []
}curl -X POST http://localhost:3000/api/videos/trim \
-F "video=@game.mp4" \
-F "threshold=0.015" \
-F "preRoll=2" \
-F "postRoll=2"Or provide a direct public link instead of uploading:
curl -X POST http://localhost:3000/api/videos/trim \
-F "videoUrl=https://storage.example.com/game.mp4"Download the result:
curl -O http://localhost:3000/uploads/trimmed-<id>.mp4ISC