# ProPresenter 7 `.proplaylist` File Format Specification **Version:** 1.0 **Target Audience:** AI agents, automated parsers, developers **Proto Source:** greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT License) --- ## 1. Overview ### File Format - **Extension:** `.proplaylist` - **Container Format:** ZIP64 archive (PKZIP 4.5+) - **Compression:** Store only (no deflate compression) - **Binary Format:** Protocol Buffers (Google protobuf v3) - **Top-level Message:** `rv.data.Playlist` (defined in `playlist.proto`) - **Proto Definitions:** greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT) ### Container Structure - **Archive Type:** ZIP64 with store compression (compression method 0) - **ZIP64 EOCD Quirk:** 98-byte discrepancy between ZIP64 EOCD locator offset and actual EOCD position - **Entry Layout:** - `data` file at root (protobuf binary) - `.pro` song files at root (filename only, no directory structure) - Media files at original absolute paths (minus leading `/`) ### Known Limitations - **Binary Fidelity:** Round-trip decode→encode fails on all reference files. Proto definitions are incomplete; unknown fields are lost during serialization. - **Workaround:** Preserve original binary data if exact binary reproduction is required. ### File Validity - **Empty files (0 bytes):** Invalid. Throw exception. - **Playlists without items:** Valid. Empty playlists are allowed. - **Deduplication:** Same `.pro` file stored once; media files deduplicated by path. --- ## 2. Playlist Structure ### Hierarchy Diagram ``` PlaylistDocument (ZIP64 archive) ├── data (protobuf binary) │ └── Playlist (rv.data.Playlist) ← Root container named "PLAYLIST" │ ├── name (string, field 2) = "PLAYLIST" │ ├── uuid (rv.data.UUID, field 1) │ ├── type (rv.data.Playlist.Type, field 3) = TYPE_PLAYLIST (1) │ └── playlists (rv.data.Playlist.PlaylistArray, field 12) │ └── playlists[] (rv.data.Playlist) ← Actual named playlist │ ├── name (string, field 2) ← User-defined name │ ├── uuid (rv.data.UUID, field 1) │ ├── type (rv.data.Playlist.Type, field 3) = TYPE_PLAYLIST (1) │ └── items (rv.data.Playlist.PlaylistItems, field 13) │ └── items[] (rv.data.PlaylistItem) │ ├── uuid (rv.data.UUID, field 1) │ ├── name (string, field 2) │ └── ItemType (oneof) │ ├── header (field 3) ← Section divider │ ├── presentation (field 4) ← Song reference │ ├── cue (field 5) ← Inline cue │ ├── planning_center (field 6) ← PCO integration │ └── placeholder (field 8) ← Empty slot ├── *.pro files (song files, deduplicated) └── media files (images/videos at original absolute paths) ``` ### Navigation Paths **To access playlist items:** ``` PlaylistDocument (ZIP) → data (protobuf) → Playlist (root "PLAYLIST") → playlists.playlists[0] (actual playlist) → items.items[] → ItemType (oneof) ``` **To access presentation references:** ``` PlaylistItem → presentation → document_path (URL) → arrangement (UUID) → arrangement_name (string) → user_music_key (MusicKeyScale) ``` **To access header dividers:** ``` PlaylistItem → header → color (Color) → actions[] (Action) ``` --- ## 3. Fields Reference ### Playlist (rv.data.Playlist) | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `uuid` | `rv.data.UUID` | 1 | Unique identifier for the playlist | | `name` | `string` | 2 | Playlist name (root is always "PLAYLIST") | | `type` | `rv.data.Playlist.Type` | 3 | Playlist type (always TYPE_PLAYLIST = 1) | | `expanded` | `bool` | 4 | UI expansion state | | `targeted_layer_uuid` | `rv.data.UUID` | 5 | Target layer UUID | | `smart_directory_path` | `rv.data.URL` | 6 | Smart playlist directory path | | `hot_key` | `rv.data.HotKey` | 7 | Keyboard shortcut | | `cues[]` | `rv.data.Cue` | 8 | Array of cues (not used in observed files) | | `children[]` | `rv.data.Playlist` | 9 | Array of child playlists (deprecated) | | `timecode_enabled` | `bool` | 10 | Timecode synchronization enabled | | `timing` | `rv.data.Playlist.TimingType` | 11 | Timing type (NONE, TIMECODE, TIME_OF_DAY) | | `playlists` | `rv.data.Playlist.PlaylistArray` | 12 | Child playlists (oneof ChildrenType) | | `items` | `rv.data.Playlist.PlaylistItems` | 13 | Playlist items (oneof ChildrenType) | | `smart_directory` | `rv.data.Playlist.FolderDirectory` | 14 | Smart folder config (oneof LinkData) | | `pco_plan` | `rv.data.PlanningCenterPlan` | 15 | Planning Center plan (oneof LinkData) | | `startup_info` | `rv.data.Playlist.StartupInfo` | 16 | Startup trigger configuration | ### Playlist.PlaylistArray | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `playlists[]` | `rv.data.Playlist` | 1 | Array of child playlists | ### Playlist.PlaylistItems | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `items[]` | `rv.data.PlaylistItem` | 1 | Array of playlist items | ### PlaylistItem (rv.data.PlaylistItem) | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `uuid` | `rv.data.UUID` | 1 | Unique identifier for the item | | `name` | `string` | 2 | Item display name | | `tags[]` | `rv.data.UUID` | 7 | Array of tag UUIDs | | `is_hidden` | `bool` | 9 | Whether item is hidden in UI | | `header` | `rv.data.PlaylistItem.Header` | 3 | Section divider (oneof ItemType) | | `presentation` | `rv.data.PlaylistItem.Presentation` | 4 | Song reference (oneof ItemType) | | `cue` | `rv.data.Cue` | 5 | Inline cue (oneof ItemType) | | `planning_center` | `rv.data.PlaylistItem.PlanningCenter` | 6 | PCO integration (oneof ItemType) | | `placeholder` | `rv.data.PlaylistItem.Placeholder` | 8 | Empty slot (oneof ItemType) | ### PlaylistItem.Header | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `color` | `rv.data.Color` | 1 | RGBA color (float values 0.0-1.0) | | `actions[]` | `rv.data.Action` | 2 | Array of actions (rarely used) | ### PlaylistItem.Presentation | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `document_path` | `rv.data.URL` | 1 | Path to .pro file (URL format) | | `arrangement` | `rv.data.UUID` | 2 | Arrangement UUID | | `content_destination` | `rv.data.Action.ContentDestination` | 3 | Content destination layer | | `user_music_key` | `rv.data.MusicKeyScale` | 4 | User-selected music key | | `arrangement_name` | `string` | 5 | Arrangement name (UNDOCUMENTED) | ### PlaylistItem.PlanningCenter | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `item` | `rv.data.PlanningCenterPlan.PlanItem` | 1 | PCO plan item reference | | `linked_data` | `rv.data.PlaylistItem` | 2 | Linked playlist item | ### PlaylistItem.Placeholder | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `linked_data` | `rv.data.PlaylistItem` | 1 | Linked playlist item | --- ## 4. ZIP64 Container Format ### Archive Structure - **Format:** ZIP64 (PKZIP 4.5+) - **Compression:** Store only (compression method 0, no deflate) - **Entries:** 1. `data` file at root (protobuf binary) 2. `.pro` song files at root (filename only) 3. Media files at original absolute paths (minus leading `/`) ### ZIP64 EOCD Quirk - **Issue:** 98-byte discrepancy between ZIP64 EOCD locator offset and actual EOCD position - **Observed Pattern:** ZIP64 EOCD locator points to offset that is 98 bytes before actual EOCD record - **Workaround:** Search backward from end of file for ZIP64 EOCD signature (`0x06064b50`) ### Entry Layout Example ``` data ← Protobuf binary Test.pro ← Song file (filename only) Oceans.pro ← Song file (filename only) Users/me/Pictures/slide.jpg ← Media file (absolute path minus leading /) Users/me/Videos/intro.mp4 ← Media file (absolute path minus leading /) ``` ### Deduplication Rules - **Song Files:** Same `.pro` file stored once (by filename) - **Media Files:** Deduplicated by absolute path - **Example:** If 3 playlist items reference `Oceans.pro`, only 1 copy is stored in ZIP --- ## 5. Playlist Items ### Definition Playlist items represent individual entries in a playlist. Each item has a type (header, presentation, cue, planning_center, placeholder) defined by the `ItemType` oneof field. ### Item Types #### Header (Field 3) - **Purpose:** Section divider with color - **Usage:** Visual separator in playlist UI - **Fields:** `color` (RGBA), `actions[]` (rarely used) - **Example:** "Worship Set", "Announcements", "Offering" #### Presentation (Field 4) - **Purpose:** Reference to a `.pro` song file - **Usage:** Most common item type - **Fields:** - `document_path` (URL) — Path to `.pro` file - `arrangement` (UUID) — Arrangement UUID - `arrangement_name` (string) — Arrangement name (e.g., "normal", "bene", "test2") - `user_music_key` (MusicKeyScale) — User-selected music key - `content_destination` (ContentDestination) — Target layer - **Example:** Reference to "Oceans.pro" with arrangement "normal" #### Cue (Field 5) - **Purpose:** Inline cue (not observed in reference files) - **Usage:** Embedded cue without external `.pro` file - **Fields:** Full `rv.data.Cue` message #### PlanningCenter (Field 6) - **Purpose:** Planning Center Online integration - **Usage:** Link to PCO plan item - **Fields:** `item` (PlanItem), `linked_data` (PlaylistItem) - **Note:** Not in scope for this specification #### Placeholder (Field 8) - **Purpose:** Empty slot in playlist - **Usage:** Reserve space for future item - **Fields:** `linked_data` (PlaylistItem) ### Access Pattern ```php foreach ($playlist->getItems() as $item) { $uuid = $item->getUuid(); $name = $item->getName(); if ($item->hasPresentation()) { $presentation = $item->getPresentation(); $path = $presentation->getDocumentPath()->getAbsoluteString(); $arrangementName = $presentation->getArrangementName(); $arrangementUuid = $presentation->getArrangement()->getString(); } elseif ($item->hasHeader()) { $header = $item->getHeader(); $color = $header->getColor(); } elseif ($item->hasPlaceholder()) { // Empty slot } } ``` --- ## 6. URL Format ### URL Structure ProPresenter uses `rv.data.URL` messages with root type and relative path components. ### Root Types - **ROOT_USER_HOME (2):** User home directory (`~/`) - **ROOT_SHOW (10):** ProPresenter library directory ### Path Construction - **Format:** `root_type` + `relative_path` - **Example (ROOT_USER_HOME):** - Root: `ROOT_USER_HOME (2)` - Relative: `Music/ProPresenter/Oceans.pro` - Absolute: `file:///Users/username/Music/ProPresenter/Oceans.pro` - **Example (ROOT_SHOW):** - Root: `ROOT_SHOW (10)` - Relative: `Oceans.pro` - Absolute: `file:///Users/username/Library/Application Support/RenewedVision/ProPresenter/Oceans.pro` ### Media File Paths - **Storage:** Original absolute path minus leading `/` - **Example:** - Original: `file:///Users/me/Pictures/slide.jpg` - ZIP entry: `Users/me/Pictures/slide.jpg` --- ## 7. Protobuf Structure ### Root Container - **Message:** `rv.data.Playlist` - **Name:** Always "PLAYLIST" - **Type:** Always `TYPE_PLAYLIST (1)` - **Children:** `playlists` field (PlaylistArray) ### Actual Playlist - **Location:** `playlists.playlists[0]` - **Name:** User-defined (e.g., "Gottesdienst", "Sunday Service") - **Type:** Always `TYPE_PLAYLIST (1)` - **Children:** `items` field (PlaylistItems) ### Nested Structure ``` Playlist (root "PLAYLIST") → playlists (PlaylistArray, field 12) → playlists[] (Playlist) → items (PlaylistItems, field 13) → items[] (PlaylistItem) ``` ### Example (TestPlaylist.proplaylist) ``` Playlist { name: "PLAYLIST" type: TYPE_PLAYLIST (1) playlists: { playlists: [ { name: "TestPlaylist" type: TYPE_PLAYLIST (1) items: { items: [ { name: "Worship", header: { color: {...} } }, { name: "Oceans", presentation: { document_path: {...}, arrangement_name: "normal" } }, { name: "Amazing Grace", presentation: { document_path: {...}, arrangement_name: "bene" } }, ] } } ] } } ``` --- ## 8. Known Constants ### Application Info - **Platform:** macOS 14.8.3 - **Application:** ProPresenter v20 - **Observed in:** All reference files ### Playlist Type - **Root Playlist:** Always `TYPE_PLAYLIST (1)` - **Child Playlists:** Always `TYPE_PLAYLIST (1)` - **Other Types:** `TYPE_GROUP (2)`, `TYPE_SMART (3)`, `TYPE_ROOT (4)` not observed in reference files ### Root Name - **Value:** Always "PLAYLIST" - **Purpose:** Container for actual named playlists ### Arrangement Name (Field 5) - **Status:** UNDOCUMENTED in community proto - **Observed Values:** "normal", "bene", "test2", "Gottesdienst", etc. - **Purpose:** Human-readable arrangement name (complements arrangement UUID) - **Frequency:** Present in every `PlaylistItem.Presentation` in reference files --- ## 9. Edge Cases ### Empty Playlists - **Items:** 0 items - **Validity:** Valid - **Behavior:** `items.items[]` is empty array ### Playlists Without Presentations - **Items:** Only headers and placeholders - **Validity:** Valid - **Example:** Template playlists with section dividers ### Missing Arrangement Name - **Field:** `arrangement_name` (field 5) - **Behavior:** Empty string or not set - **Validity:** Valid (fallback to arrangement UUID) ### Duplicate Song References - **Scenario:** Same `.pro` file referenced multiple times - **ZIP Storage:** Single copy of `.pro` file - **Playlist Items:** Multiple `PlaylistItem.Presentation` entries with same `document_path` ### Media Files - **Storage:** Original absolute paths (minus leading `/`) - **Deduplication:** By absolute path - **Example:** `Users/me/Pictures/slide.jpg` stored once even if referenced in multiple songs --- ## 10. Reverse-Engineering Evidence ### Reference Files - **TestPlaylist.proplaylist:** 4 ZIP entries, 3 items (1 header, 2 presentations) - **Gottesdienst.proplaylist:** 14MB, 25+ items, multiple media files - **Gottesdienst 2.proplaylist:** 10MB, similar structure - **Gottesdienst 3.proplaylist:** 16MB, largest reference file ### Key Discoveries 1. **ZIP64 EOCD Quirk:** 98-byte offset discrepancy in all files 2. **Store Compression:** No deflate compression (method 0) 3. **Arrangement Name:** Field 5 on `PlaylistItem.Presentation` is undocumented but present in all files 4. **Root Container:** Always named "PLAYLIST" with `TYPE_PLAYLIST (1)` 5. **Deduplication:** Same `.pro` file stored once, media files deduplicated by path ### Observed Patterns - **Color Values:** RGBA floats (e.g., `[0.95, 0.27, 0.27, 1.0]` for red) - **UUID Format:** Standard UUID strings (e.g., `A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6`) - **Arrangement Names:** User-defined strings (e.g., "normal", "bene", "test2", "Gottesdienst") - **Media Paths:** Absolute file URLs (e.g., `file:///Users/me/Pictures/slide.jpg`) --- ## Appendix: Proto Field Numbers Quick Reference | Message | Field | Number | |---------|-------|--------| | Playlist | uuid | 1 | | Playlist | name | 2 | | Playlist | type | 3 | | Playlist | expanded | 4 | | Playlist | targeted_layer_uuid | 5 | | Playlist | smart_directory_path | 6 | | Playlist | hot_key | 7 | | Playlist | cues | 8 | | Playlist | children | 9 | | Playlist | timecode_enabled | 10 | | Playlist | timing | 11 | | Playlist | playlists | 12 | | Playlist | items | 13 | | Playlist | smart_directory | 14 | | Playlist | pco_plan | 15 | | Playlist | startup_info | 16 | | PlaylistArray | playlists | 1 | | PlaylistItems | items | 1 | | PlaylistItem | uuid | 1 | | PlaylistItem | name | 2 | | PlaylistItem | header | 3 | | PlaylistItem | presentation | 4 | | PlaylistItem | cue | 5 | | PlaylistItem | planning_center | 6 | | PlaylistItem | tags | 7 | | PlaylistItem | placeholder | 8 | | PlaylistItem | is_hidden | 9 | | PlaylistItem.Header | color | 1 | | PlaylistItem.Header | actions | 2 | | PlaylistItem.Presentation | document_path | 1 | | PlaylistItem.Presentation | arrangement | 2 | | PlaylistItem.Presentation | content_destination | 3 | | PlaylistItem.Presentation | user_music_key | 4 | | PlaylistItem.Presentation | arrangement_name | 5 | | PlaylistItem.PlanningCenter | item | 1 | | PlaylistItem.PlanningCenter | linked_data | 2 | | PlaylistItem.Placeholder | linked_data | 1 | --- **End of Specification**