Skip to content

feat: Add Apple Silicon support with VideoToolbox hardware encoding (#15)#18

Open
ebbbang wants to merge 1 commit intoJMS1717:mainfrom
ebbbang:feature/apple-silicon-videotoolbox
Open

feat: Add Apple Silicon support with VideoToolbox hardware encoding (#15)#18
ebbbang wants to merge 1 commit intoJMS1717:mainfrom
ebbbang:feature/apple-silicon-videotoolbox

Conversation

@ebbbang
Copy link

@ebbbang ebbbang commented Dec 16, 2025

Closes #15

Summary

  • Adds hardware-accelerated video encoding support for Apple Silicon Macs (M1/M2/M3/M4)
  • Uses VideoToolbox via a hybrid setup: Docker for services, native macOS worker for GPU access
  • Supports H.264 and HEVC encoding (AV1 falls back to CPU as VideoToolbox doesn't support it)

Architecture

Since Docker Desktop on macOS runs in a Linux VM without GPU passthrough, this uses a hybrid approach:

  • Docker: Runs Redis, frontend, and backend API
  • Native worker: Runs on macOS with VideoToolbox access for hardware encoding
  • Redis: Exposed on port 6380 to avoid conflicts with native Redis installations

Changes

  • worker/app/hw_detect.py: Added _check_videotoolbox() and encoder mappings
  • worker/app/startup_tests.py: Added VideoToolbox test configuration
  • backend-api/app/models.py: Added VideoToolbox codec literals
  • backend-api/app/settings_manager.py: Added VideoToolbox visibility settings
  • docker-compose.macos.yml: New compose file for macOS hybrid setup
  • scripts/macos-setup.sh: Setup script for native worker dependencies
  • docs/GPU_SUPPORT.md: Added VideoToolbox documentation
  • README.md: Updated macOS section with hybrid setup instructions
  • .github/copilot-instructions.md: Added VideoToolbox references
  • .gitignore: Added venv/

Usage

# Start Docker services
docker-compose -f docker-compose.macos.yml up -d

# Run setup script (installs FFmpeg/Python if needed)
./scripts/macos-setup.sh

# Start native worker
source venv/bin/activate
export REDIS_URL=redis://localhost:6380/0
celery -A worker.celery_app worker --loglevel=info

Test plan

- Verify VideoToolbox detection on Apple Silicon Mac
- Test H.264 encoding with h264_videotoolbox
- Test HEVC encoding with hevc_videotoolbox
- Verify AV1 falls back to CPU encoding
- Confirm setup script works on fresh macOS install
- Test hybrid setup with Docker services + native worker

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
* Added Apple VideoToolbox hardware encoding support for macOS with H.264 and HEVC codecs
* Introduced hybrid Docker + native worker setup for macOS with GPU acceleration

* **Documentation**
* Updated macOS installation guide with two options: CPU-only and hardware-accelerated paths
* Expanded GPU support documentation with VideoToolbox capabilities and limitations

* **Chores**
* Added macOS environment setup script and Docker configuration for Apple Silicon

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

  Add hardware-accelerated video encoding for macOS using VideoToolbox.
  This enables H.264 and HEVC encoding on M1/M2/M3/M4 Macs via a hybrid
  setup where Docker runs services and a native worker uses the GPU.

  - Add _check_videotoolbox() detection in hw_detect.py
  - Add h264_videotoolbox and hevc_videotoolbox encoder mappings
  - Add VideoToolbox startup tests
  - Create docker-compose.macos.yml (exposes Redis on port 6380)
  - Create scripts/macos-setup.sh for easy setup
  - Update models.py and settings_manager.py with new codecs
  - Update documentation (README, GPU_SUPPORT.md, copilot-instructions)
@coderabbitai
Copy link

coderabbitai bot commented Dec 16, 2025

Walkthrough

This PR introduces Apple Silicon and macOS support with VideoToolbox hardware acceleration. Changes include backend model expansion for VideoToolbox codecs, new hardware detection logic, a macOS-specific Docker Compose file, a native worker setup script, and comprehensive documentation updates.

Changes

Cohort / File(s) Summary
Documentation & Configuration
.github/copilot-instructions.md, README.md, docs/GPU_SUPPORT.md
Added macOS and Apple Silicon setup guidance, hardware option descriptions, VideoToolbox encoder mapping, and hybrid setup workflow documentation.
Build & Environment Configuration
.gitignore, docker-compose.macos.yml
Added Python virtual environment directory to gitignore; introduced macOS-specific Docker Compose configuration with 8mblocal service, port mappings, and Redis setup for Apple Silicon.
Backend API Models & Settings
backend-api/app/models.py, backend-api/app/settings_manager.py
Expanded CompressRequest, DefaultPresets, and PresetProfile video codec Literals to include h264_videotoolbox and hevc_videotoolbox; added phase field to JobMetadata; added VideoToolbox codec visibility flags and environment variable mappings.
Worker Hardware Detection & Tests
worker/app/hw_detect.py, worker/app/startup_tests.py
Added _check_videotoolbox() detection function and VideoToolbox support path in detect_hw_accel(); extended map_codec_to_hw() to recognize and map VideoToolbox encoders; added corresponding VideoToolbox test cases.
Setup & Configuration Scripts
scripts/macos-setup.sh
Added new macOS setup script verifying Homebrew, Apple Silicon, Python 3.10+, and FFmpeg with VideoToolbox support; installs dependencies and provides guidance for native worker deployment with environment variable configuration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • worker/app/hw_detect.py: Verify VideoToolbox detection robustness (_check_videotoolbox() function) and correct encoding test; ensure codec mapping handles all VideoToolbox variants correctly; validate that AV1 properly falls back to CPU.
  • Hybrid setup workflow: Confirm that the Docker Compose macOS file and native worker setup script integrate seamlessly; verify environment variable exports match backend expectations.
  • Model backward compatibility: Confirm new codec options and phase field in JobMetadata maintain backward compatibility with existing clients and don't break API contracts.

Possibly related PRs

Poem

🐰 Hop along to the Mac, with Silicon bright,
VideoToolbox encoders now taking flight!
From Docker containers to native workers so keen,
Apple's acceleration has finally been seen!
🎬✨ The M1 hums a merry tune,
8mb.local shall bloom this June!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Apple Silicon support with VideoToolbox hardware encoding, which matches the primary objective of the PR.
Linked Issues check ✅ Passed The PR fully addresses issue #15 by implementing VideoToolbox support for Apple Silicon hardware encoding, native worker setup, hybrid Docker+native workflow, and H.264/HEVC acceleration with CPU fallback for AV1.
Out of Scope Changes check ✅ Passed All changes are scoped to Apple Silicon VideoToolbox support: hardware detection, codec models, documentation, setup scripts, and Docker macOS configuration. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
scripts/macos-setup.sh (1)

112-112: Consider adding concurrency flag for consistency.

The Celery worker command omits the concurrency flag. Based on coding guidelines, the supervisord pattern includes --concurrency=4. Consider adding it for consistency:

-     celery -A worker.celery_app worker --loglevel=info
+     celery -A worker.celery_app worker --loglevel=info --concurrency=4

This ensures consistent behavior with the Docker-based worker.

Based on coding guidelines, worker startup should follow the supervisord pattern with concurrency specified.

docs/GPU_SUPPORT.md (1)

128-146: Optional: Add language specifier to code fence.

The ASCII diagram would be more semantically correct with a language identifier:

-```
+```text
 ┌─────────────────────────────────────────────────────┐
 │              Docker (Linux containers)               │

This addresses the static analysis hint while clarifying the content type.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 013c276 and 40cf5ad.

📒 Files selected for processing (10)
  • .github/copilot-instructions.md (2 hunks)
  • .gitignore (1 hunks)
  • README.md (1 hunks)
  • backend-api/app/models.py (4 hunks)
  • backend-api/app/settings_manager.py (2 hunks)
  • docker-compose.macos.yml (1 hunks)
  • docs/GPU_SUPPORT.md (4 hunks)
  • scripts/macos-setup.sh (1 hunks)
  • worker/app/hw_detect.py (7 hunks)
  • worker/app/startup_tests.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/{backend-api,worker}/app/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/{backend-api,worker}/app/**/*.py: Job/task IDs: backend generates job_id and worker uses Celery task_id. Redis keys must follow the pattern: job:{task.id}, progress:{task_id} and cancel:{task_id}
File naming convention: uploads must be saved to /app/uploads with jobid_filename format; outputs to /app/outputs with _8mblocal_{taskid} suffix to avoid collisions
When editing worker or backend code, preserve Redis key names (job:{task.id}, progress:{task_id}, cancel:{task_id}) and published event formats (type, task_id, progress fields)

Files:

  • backend-api/app/settings_manager.py
  • worker/app/startup_tests.py
  • backend-api/app/models.py
  • worker/app/hw_detect.py
backend-api/app/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend API: use celery_app.send_task('worker.worker.compress_video', ...) pattern to enqueue Celery tasks with proper task kwargs; output naming must follow file naming conventions

Files:

  • backend-api/app/settings_manager.py
  • backend-api/app/models.py
worker/app/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

worker/app/**/*.py: Progress messages published to Redis must be JSON events with type field (log, progress, done, or error) and include task_id and progress fields for frontend consumption
Hardware encoder detection and testing: do not assume a listed hardware encoder will initialize successfully; prefer reading the startup test cache or respecting ENCODER_TEST_CACHE logic in worker/app/worker.py

Files:

  • worker/app/startup_tests.py
  • worker/app/hw_detect.py
worker/app/{hw_detect,worker}.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Encoder mapping: requested codec is mapped to hardware encoder in worker/app/hw_detect.py via map_codec_to_hw function; when startup test marks encoder unavailable, fall back to CPU encoders

Files:

  • worker/app/hw_detect.py
🧠 Learnings (9)
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to backend-api/app/**/*.py : Backend API: use `celery_app.send_task('worker.worker.compress_video', ...)` pattern to enqueue Celery tasks with proper task kwargs; output naming must follow file naming conventions

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to **/{backend-api,worker}/app/**/*.py : File naming convention: uploads must be saved to `/app/uploads` with `jobid_filename` format; outputs to `/app/outputs` with `_8mblocal_{taskid}` suffix to avoid collisions

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to **/{backend-api,worker}/app/**/*.py : Job/task IDs: backend generates `job_id` and worker uses Celery `task_id`. Redis keys must follow the pattern: `job:{task.id}`, `progress:{task_id}` and `cancel:{task_id}`

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to worker/app/**/*.py : Hardware encoder detection and testing: do not assume a listed hardware encoder will initialize successfully; prefer reading the startup test cache or respecting `ENCODER_TEST_CACHE` logic in `worker/app/worker.py`

Applied to files:

  • .github/copilot-instructions.md
  • docs/GPU_SUPPORT.md
  • worker/app/startup_tests.py
  • worker/app/hw_detect.py
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to worker/app/{hw_detect,worker}.py : Encoder mapping: requested codec is mapped to hardware encoder in `worker/app/hw_detect.py` via `map_codec_to_hw` function; when startup test marks encoder unavailable, fall back to CPU encoders

Applied to files:

  • .github/copilot-instructions.md
  • docs/GPU_SUPPORT.md
  • worker/app/startup_tests.py
  • worker/app/hw_detect.py
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to **/{backend-api,worker}/app/**/*.py : When editing worker or backend code, preserve Redis key names (`job:{task.id}`, `progress:{task_id}`, `cancel:{task_id}`) and published event formats (type, task_id, progress fields)

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to supervisord.conf : Worker startup command should follow supervisord pattern: `celery -A worker.celery_app worker --loglevel=info -n 8mblocal@%h --concurrency=4` with `REDIS_URL` environment variable set

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to worker/app/**/*.py : Progress messages published to Redis must be JSON events with `type` field (`log`, `progress`, `done`, or `error`) and include `task_id` and `progress` fields for frontend consumption

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:09:55.051Z
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Frontend development: run `npm run dev` from frontend directory using Vite; `npm run build` for production bundles; use SSE for live progress updates and call backend APIs

Applied to files:

  • .github/copilot-instructions.md
🪛 LanguageTool
.github/copilot-instructions.md

[uncategorized] ~29-~29: The operating system from Apple is written “macOS”.
Context: ...undles. - macOS with Apple Silicon: Use docker-compose.macos.yml for services, then run `./scripts/...

(MAC_OS)

🪛 markdownlint-cli2 (0.18.1)
docs/GPU_SUPPORT.md

128-128: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

README.md

547-547: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


552-552: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🪛 Ruff (0.14.8)
worker/app/hw_detect.py

349-349: Starting a process with a partial executable path

(S607)


359-375: Starting a process with a partial executable path

(S607)


379-379: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (18)
.gitignore (1)

9-9: LGTM!

The venv/ ignore pattern aligns with the macOS setup workflow that creates a Python virtual environment for the native worker.

.github/copilot-instructions.md (1)

15-15: LGTM! Documentation accurately reflects macOS VideoToolbox workflow.

The updates correctly document the hybrid macOS setup (Docker services + native worker) and VideoToolbox as a macOS-only hardware path, consistent with the new artifacts introduced in this PR.

Also applies to: 29-29, 34-36

scripts/macos-setup.sh (1)

1-114: LGTM! Script follows best practices for macOS setup.

The script correctly:

  • Verifies prerequisites (Homebrew, Apple Silicon, Python 3.10+, FFmpeg with VideoToolbox)
  • Installs missing dependencies when Homebrew is available
  • Sets up a Python virtual environment and installs requirements
  • Provides clear instructions for running the native worker with proper environment variables

The Redis URL (redis://localhost:6380/0) correctly aligns with the port mapping in docker-compose.macos.yml.

docker-compose.macos.yml (1)

1-26: LGTM! Port mapping correctly enables hybrid setup.

The configuration appropriately:

  • Exposes Redis on port 6380 externally (6380:6379) to avoid conflicts with native Redis
  • Sets internal REDIS_URL to redis://127.0.0.1:6379/0 for container services
  • Allows the native macOS worker to connect via redis://localhost:6380/0

This correctly implements the hybrid Docker + native worker architecture.

worker/app/startup_tests.py (1)

418-424: LGTM! VideoToolbox test configuration follows established patterns.

The VideoToolbox hardware type is correctly integrated:

  • Tests H.264 and HEVC encoders (VideoToolbox's supported codecs)
  • Uses appropriate hardware decoder flags (-hwaccel videotoolbox)
  • Consistent with existing NVIDIA/Intel/VAAPI test structure
  • Correctly omits AV1 (VideoToolbox doesn't support AV1 encoding)

Based on learnings, encoder testing follows the startup test pattern and will validate actual initialization.

backend-api/app/settings_manager.py (1)

401-403: LGTM! VideoToolbox codec visibility settings integrated correctly.

The additions:

  • Follow the established pattern for hardware codec visibility (NVENC/QSV/VAAPI/AMF)
  • Use consistent environment variable naming (CODEC_H264_VIDEOTOOLBOX, CODEC_HEVC_VIDEOTOOLBOX)
  • Default to true like other codecs
  • Align with the VideoToolbox codec literals added to models.py

Based on coding guidelines, codec visibility settings control which codecs appear in the UI.

Also applies to: 428-429

README.md (1)

546-575: LGTM! macOS section clearly documents both installation paths.

The documentation correctly:

  • Distinguishes CPU-only Docker (Option 1) from hybrid VideoToolbox setup (Option 2)
  • Provides step-by-step commands for the hybrid setup
  • Uses correct Redis URL (redis://localhost:6380/0) matching the port mapping in docker-compose.macos.yml
  • Sets appropriate environment variables (UPLOAD_DIR, OUTPUT_DIR, PYTHONPATH)
  • References GPU_SUPPORT.md for additional details
docs/GPU_SUPPORT.md (3)

10-11: LGTM! VideoToolbox correctly integrated into multi-vendor GPU support.

The documentation accurately:

  • Positions VideoToolbox as the fourth hardware option (macOS/Apple Silicon)
  • Notes the requirement for a native worker (Docker on macOS lacks GPU passthrough)
  • Shows correct encoder mappings (h264_videotoolbox, hevc_videotoolbox, N/A for AV1)
  • Explains AV1 CPU fallback due to VideoToolbox limitations

Based on learnings, encoder mapping follows the established pattern in worker/app/hw_detect.py.

Also applies to: 20-21, 29-35


124-181: LGTM! macOS hybrid setup clearly documented.

The section provides comprehensive guidance:

  • Architecture diagram illustrating Docker services + native worker
  • Step-by-step setup commands matching README.md and scripts/macos-setup.sh
  • Correct Redis URL (redis://localhost:6380/0) for native worker connection
  • Prerequisites and limitations clearly stated

268-268: LGTM! Future enhancements updated to reflect completion.

The checkbox correctly marks Apple VideoToolbox support as completed.

worker/app/hw_detect.py (5)

4-4: LGTM!

The platform import is correctly used to detect macOS/Darwin for VideoToolbox availability.


76-89: LGTM!

The VideoToolbox detection correctly integrates into the detection priority order and appropriately omits AV1 support (as noted in the comment). The structure is consistent with other hardware acceleration backends.


413-414: LGTM!

The VideoToolbox encoder handling correctly omits init_hw_device (unlike QSV/VAAPI), which is consistent with how VideoToolbox encoders operate. The pixel format (nv12) and profile settings are appropriate for VideoToolbox hardware encoding.

Note: The pattern of setting decode_method="videotoolbox" in detection but not adding hwaccel init flags here follows the same pattern as NVENC (which sets decode_method="cuda" but handles hwaccel in the worker). This assumes the worker will determine whether to use hardware decoding based on input codec support.

Also applies to: 453-459


510-515: LGTM!

The legacy fallback path for VideoToolbox encoders correctly mirrors the explicit encoder handling, ensuring consistent flag application regardless of which code path is taken.


341-382: Align VideoToolbox encoding test timeout with other hardware detection tests.

The 10-second timeout for the VideoToolbox encoding test (line 377) is inconsistent with the 5-second timeout used for Intel QSV (line 209). Both functions test identical minimal encoding scenarios (64×64 video, 0.1s duration, 1 frame), yet VideoToolbox uses 2× the timeout without code comments or documentation justifying the difference. Reduce the VideoToolbox encoding test timeout to 5 seconds to match the established pattern across hardware detection functions.

⛔ Skipped due to learnings
Learnt from: CR
Repo: JMS1717/8mb.local PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:09:55.051Z
Learning: Applies to worker/app/**/*.py : Hardware encoder detection and testing: do not assume a listed hardware encoder will initialize successfully; prefer reading the startup test cache or respecting `ENCODER_TEST_CACHE` logic in `worker/app/worker.py`
backend-api/app/models.py (3)

20-20: LGTM!

The VideoToolbox codec additions (h264_videotoolbox, hevc_videotoolbox) are consistently applied across all relevant model definitions and correctly omit av1_videotoolbox since VideoToolbox does not support AV1 encoding.

Also applies to: 68-68, 108-108


96-98: LGTM!

The VideoToolbox visibility settings follow the established pattern for other hardware encoder types and correctly include only H.264 and HEVC (omitting AV1, which VideoToolbox doesn't support).


143-143: Clarify: Is this field related to VideoToolbox support?

The phase field addition to JobMetadata is not mentioned in the PR objectives or description, which focus on Apple Silicon/VideoToolbox hardware encoding support. This field appears to be for tracking encoding phases (queued → encoding → finalizing → done).

While the field is properly typed and backward compatible, can you confirm whether this belongs in this PR or should be part of a separate feature? If it's related to better progress reporting for VideoToolbox encoding jobs, please clarify the connection.

@ebbbang
Copy link
Author

ebbbang commented Dec 17, 2025

@bradlington Can you give this a try?

CC: @JMS1717

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] ARM64 / Apple Silicon support

1 participant