The single net8.0 build compiled against Jellyfin 10.9.11 still loads on
10.11 and 12, so channels and browsing keep working, but 10.11 changed
APIs in ways that only fail at runtime. A binary audit of the DLL against
the real server assemblies found 9 broken references on both 10.11 and
12 (none on 10.9/10.10):
- TaskTriggerInfo.Type became an enum. GetDefaultTriggers throws, so
Jellyfin silently gives every SRF task a 24h fallback trigger. The
recording scheduler therefore ran about once a day and scheduled
recordings never started.
- ILibraryManager.GetItemList changed its return type: the expiration
check crashed on every run.
- Jellyfin.Data.Entities.User moved (IUserManager.Users, GetUserData,
SaveUserData, PlaybackProgressEventArgs.Users): resume cleanup returned
500 and the playback-stop guard threw on every stop.
Jellyfin 12 also rejects the legacy X-Emby-Token header with an empty
401, which the recordings UI surfaced as "Unexpected end of JSON input".
The pages now send Authorization: MediaBrowser Token="...", verified on
10.9, 10.11 and 12.
The plugin now multi-targets net8.0/net9.0/net10.0 against Jellyfin
10.9.11/10.11.0/12.0.0 (the oldest package of each line, so each DLL
loads on every patch release of it), with JELLYFIN_10_11_OR_GREATER and
JELLYFIN_12_OR_GREATER for the differences. Trigger construction moves
into Utilities/TaskTriggers so the version switch lives in one place.
.gitea/scripts/build-plugins.sh builds one zip per generation. jprm has
no targetAbi flag, so it rewrites build.yaml per flavour and restores
it. Jellyfin installs the highest version whose targetAbi it satisfies,
so the flavour code goes into the last version segment:
release v1.2.0 -> 1.2.0.9 / 1.2.0.11 / 1.2.0.12
nightly -> 1.0.<date>.<run>09 / 11 / 12
The PR, nightly and release workflows use the script and publish one
manifest entry per zip. Verified end to end against a three-entry
manifest: 10.11 installs the .11 build, 12.1 installs the .12 build.
The existing .NET 10 builder image builds all three unchanged.
Supporting changes: the test project multi-targets too, because jprm
publishes the whole solution for one framework at a time, and it drops
its Microsoft.Extensions.Logging 8.0.1 pin, which is a NU1605 downgrade
under the newer Jellyfin packages. CA1873 (new in the .NET 10 analyzers)
is silenced beside CA1848. The release manifest step checks out
origin/master like the nightly does since 7e1973a.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
318 lines
14 KiB
Markdown
318 lines
14 KiB
Markdown
# Jellyfin SRF Play Plugin
|
|
|
|
A Jellyfin plugin for accessing SRF Play (Swiss Radio and Television) video-on-demand content and live sports streaming.
|
|
|
|
## Status
|
|
|
|
**Beta/Alpha** - This plugin has been tested on two Jellyfin instances and is working. Some clients may experience issues with hardware decoding, which appears to be client-specific behavior.
|
|
|
|
## Quick Install
|
|
|
|
Add this repository URL in Jellyfin (Dashboard → Plugins → Repositories):
|
|
|
|
```
|
|
https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/manifest.json
|
|
```
|
|
|
|
Then install "SRF Play" from the plugin catalog.
|
|
|
|
## Features
|
|
|
|
- Access to SRF Play VOD content (video-on-demand, no DRM-protected content)
|
|
- **Live Sports Streaming** - Watch scheduled sports events (skiing, Formula 1, football, tennis, etc.)
|
|
- Support for all Swiss broadcasting units (SRF, RTS, RSI, RTR, SWI)
|
|
- Automatic content expiration handling
|
|
- Continue Watching cleanup — ended livestreams and abandoned playback don't pile up in the resume row
|
|
- Latest and trending content discovery
|
|
- Quality selection (Auto lets CDN decide, SD prefers 480p/360p, HD prefers 1080p/720p)
|
|
- HLS streaming support with Akamai token authentication
|
|
- Proxy support for routing traffic through alternate gateways
|
|
- Smart caching with reduced TTL for upcoming livestreams
|
|
|
|
## Screenshots
|
|
|
|
### Channel Menu
|
|

|
|
|
|
The main channel interface showing the content folders.
|
|
### Playback
|
|

|
|
|
|
Video playback with HLS streaming support and quality selection.
|
|
|
|
## Testing
|
|
|
|
The plugin includes comprehensive testing:
|
|
|
|
- **Unit tests** (xUnit framework) for core services
|
|
- **API spec validation tests** for all business units (SRF, RTS, RSI, RTR, SWI)
|
|
- **Integration testing** - Tested on multiple Jellyfin instances with VOD and live sports streaming
|
|
|
|
**Run tests:**
|
|
```bash
|
|
# All tests
|
|
dotnet test
|
|
|
|
# Unit tests only
|
|
dotnet test --filter "Category!=Integration&Category!=APISpec"
|
|
|
|
# API spec tests only
|
|
dotnet test --filter "Category=APISpec"
|
|
|
|
# With coverage
|
|
dotnet test --collect:"XPlat Code Coverage"
|
|
```
|
|
|
|
See [Test Documentation](Jellyfin.Plugin.SRFPlay.Tests/README.md) for more details.
|
|
|
|
## API Information
|
|
|
|
**Base URL:** `https://il.srgssr.ch/integrationlayer/2.0/`
|
|
|
|
### Key Endpoints
|
|
|
|
**Integration Layer API v2.0:**
|
|
- `GET /mediaComposition/byUrn/{urn}` - Get video metadata and playable URLs
|
|
- `GET /video/{businessUnit}/latest` - Get latest videos
|
|
- `GET /video/{businessUnit}/trending` - Get trending videos
|
|
|
|
**Play v3 API:**
|
|
- `GET /play/v3/api/{bu}/production/livestreams?eventType=SPORT` - Get scheduled sports livestreams
|
|
- `GET /play/v3/api/{bu}/production/shows/{id}` - Get show details
|
|
- `GET /play/v3/api/{bu}/production/videos-by-show-id?showId={id}` - Get episodes for a show
|
|
|
|
**Akamai Token Service:**
|
|
- `GET /akahd/token?acl=/{path}/*` - Authenticate stream URLs for playback
|
|
|
|
### URN Format
|
|
|
|
- `urn:{bu}:video:{id}` - Video URN (VOD content)
|
|
- `urn:{bu}:scheduled_livestream:video:{id}` - Scheduled sports livestream URN (supported)
|
|
- `urn:{bu}:video:livestream_{channel}` - Live TV channel URN (not supported due to Widevine DRM)
|
|
|
|
Examples:
|
|
- `urn:srf:video:c4927fcf-4ab4-4bcf-be4e-00e4ee9e6d6b` (VOD)
|
|
- `urn:srf:scheduled_livestream:video:7b31c9a6-a96e-4c0e-bfc2-f0d6237f2233` (Sports event)
|
|
- `urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651` (SRF 1 Live - DRM protected)
|
|
|
|
## Building the Plugin
|
|
|
|
### Prerequisites
|
|
|
|
- .NET 10.0 SDK (builds all three targets below)
|
|
- Jellyfin 10.9.11 or later
|
|
|
|
The plugin is built once per Jellyfin generation, because 10.11 changed APIs in ways that
|
|
compile but fail at runtime. Installing from the repository picks the right one automatically.
|
|
|
|
| Jellyfin | Target framework | targetAbi |
|
|
|---|---|---|
|
|
| 10.9, 10.10 | net8.0 | 10.9.0.0 |
|
|
| 10.11 | net9.0 | 10.11.0.0 |
|
|
| 12 | net10.0 | 12.0.0.0 |
|
|
|
|
`.gitea/scripts/build-plugins.sh` builds all three zips the way CI does.
|
|
|
|
### Build Steps
|
|
|
|
```bash
|
|
cd Jellyfin.Plugin.SRFPlay
|
|
dotnet build
|
|
```
|
|
|
|
### Output
|
|
|
|
The compiled plugins will be in `bin/Debug/net8.0/`, `bin/Debug/net9.0/` and `bin/Debug/net10.0/`
|
|
|
|
## Manual Installation
|
|
|
|
1. Build the plugin (see above)
|
|
2. Copy the compiled DLL to your Jellyfin plugins directory
|
|
3. Restart Jellyfin
|
|
4. Configure the plugin in Jellyfin Dashboard → Plugins → SRF Play
|
|
5. The SRF Play channel will appear in Jellyfin with three main folders:
|
|
- **Latest Videos** - Recently published content
|
|
- **Trending Videos** - Popular content
|
|
- **Live Sports & Events** - Scheduled sports livestreams (skiing, F1, football, etc.)
|
|
|
|
## Configuration
|
|
|
|
- **Business Unit**: Select the Swiss broadcasting unit (default: SRF)
|
|
- **Quality Preference**: Choose video quality — Auto (first available, CDN decides), SD, or HD
|
|
- **Content Refresh Interval**: How often to check for new content (1-168 hours)
|
|
- **Expiration Check Interval**: How often to check for expired content (1-168 hours)
|
|
- **Cache Duration**: How long to cache metadata (5-1440 minutes)
|
|
- **Enable Latest Content**: Automatically discover latest videos
|
|
- **Enable Trending Content**: Automatically discover trending videos
|
|
- **Proxy Settings**: Configure proxy server for routing SRF API traffic (optional)
|
|
- **Use Proxy**: Enable/disable proxy usage
|
|
- **Proxy Address**: Proxy server URL (e.g., http://proxy.example.com:8080)
|
|
- **Proxy Username**: Optional authentication username
|
|
- **Proxy Password**: Optional authentication password
|
|
- **Continue Watching Cleanup**: Stops stale SRF Play entries accumulating in the resume row
|
|
- **Clean up stale resume points**: Enables the daily "Clean Up SRF Play Continue Watching" task (4 AM)
|
|
- **Never keep a resume point for livestreams**: Clears the position the moment a livestream stops
|
|
- **Maximum Resume Point Age**: Clear entries untouched for this many days (default 30, 0 disables)
|
|
- **Minimum Resume Position**: Clear entries at or below this position (default 60s, 0 disables)
|
|
- **Finished Threshold**: Clear entries at or beyond this share of the runtime (default 92%, 0 disables)
|
|
|
|
Clearing a resume point only resets the playback position. Nothing is deleted, the item stays
|
|
unwatched, and only SRF Play items are ever touched. The plugin config page also has
|
|
**Clean Up Stale Entries Now** and **Clear All SRF Play Entries** buttons for running it on demand.
|
|
|
|
For detailed proxy setup instructions, see [PROXY_SETUP_GUIDE.md](PROXY_SETUP_GUIDE.md).
|
|
|
|
## Troubleshooting
|
|
|
|
If you encounter issues with the plugin:
|
|
- Check [DEBUG_GUIDE.md](DEBUG_GUIDE.md) for detailed logging information
|
|
- Enable debug logging in Jellyfin to see detailed request/response information
|
|
- Common issues include DRM-protected content and geo-restrictions
|
|
|
|
## Technical Architecture
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
Jellyfin.Plugin.SRFPlay/
|
|
├── Api/
|
|
│ ├── Models/ # API response models
|
|
│ │ ├── MediaComposition.cs # Root composition (HasChapters helper)
|
|
│ │ ├── Chapter.cs # Video/episode chapter with resources
|
|
│ │ ├── Resource.cs # Stream URL entry (IsPlayable helper)
|
|
│ │ ├── Show.cs
|
|
│ │ ├── Episode.cs
|
|
│ │ └── PlayV3/ # Play v3 API models
|
|
│ │ ├── PlayV3Show.cs
|
|
│ │ ├── PlayV3Topic.cs
|
|
│ │ ├── PlayV3Video.cs
|
|
│ │ ├── PlayV3TvProgram.cs
|
|
│ │ ├── PlayV3Response.cs
|
|
│ │ ├── PlayV3DirectResponse.cs
|
|
│ │ └── PlayV3DataContainer.cs
|
|
│ ├── SRFApiClient.cs # HTTP client for SRF APIs
|
|
│ ├── ISRFApiClientFactory.cs # Factory interface
|
|
│ └── SRFApiClientFactory.cs # Factory implementation
|
|
├── Channels/
|
|
│ └── SRFPlayChannel.cs # Channel implementation
|
|
├── Configuration/
|
|
│ ├── PluginConfiguration.cs
|
|
│ └── configPage.html
|
|
├── Constants/
|
|
│ └── ApiEndpoints.cs # API base URLs and endpoint constants
|
|
├── Controllers/
|
|
│ └── StreamProxyController.cs # HLS proxy endpoints (master/variant/segment)
|
|
├── Services/
|
|
│ ├── Interfaces/ # Service contracts
|
|
│ │ ├── IStreamProxyService.cs
|
|
│ │ ├── IStreamUrlResolver.cs
|
|
│ │ ├── IMediaCompositionFetcher.cs
|
|
│ │ ├── IMediaSourceFactory.cs
|
|
│ │ ├── IMetadataCache.cs
|
|
│ │ ├── IContentRefreshService.cs
|
|
│ │ ├── IContentExpirationService.cs
|
|
│ │ └── ICategoryService.cs
|
|
│ ├── StreamProxyService.cs # HLS proxy: auth, manifest rewriting, segments
|
|
│ ├── StreamUrlResolver.cs # Stream selection & Akamai authentication
|
|
│ ├── MediaCompositionFetcher.cs # Cached API fetcher
|
|
│ ├── MediaSourceFactory.cs # Jellyfin MediaSourceInfo builder
|
|
│ ├── MetadataCache.cs # Thread-safe ConcurrentDictionary cache
|
|
│ ├── ContentExpirationService.cs
|
|
│ ├── ContentRefreshService.cs
|
|
│ └── CategoryService.cs
|
|
├── Providers/
|
|
│ ├── SRFSeriesProvider.cs # Series metadata
|
|
│ ├── SRFEpisodeProvider.cs # Episode metadata
|
|
│ ├── SRFImageProvider.cs # Image fetching
|
|
│ └── SRFMediaProvider.cs # Playback URLs
|
|
├── Utilities/
|
|
│ ├── Extensions.cs # BusinessUnit.ToLowerString() extension
|
|
│ ├── MimeTypeHelper.cs # Content-type detection
|
|
│ ├── PlaceholderImageGenerator.cs
|
|
│ └── UrnHelper.cs # URN parsing utilities
|
|
├── ScheduledTasks/
|
|
│ ├── ContentRefreshTask.cs # Periodic content refresh
|
|
│ └── ExpirationCheckTask.cs # Periodic expiration check
|
|
├── ServiceRegistrator.cs # DI registration
|
|
└── Plugin.cs # Main plugin entry point
|
|
```
|
|
|
|
### Key Components
|
|
|
|
1. **API Client** (`SRFApiClient`): HTTP requests to SRF Integration Layer and Play v3 API, with proxy support
|
|
2. **Channel** (`SRFPlayChannel`): SRF Play channel with Latest, Trending, and Live Sports folders
|
|
3. **Stream Proxy** (`StreamProxyService` + `StreamProxyController`): HLS proxy that handles Akamai token auth, manifest URL rewriting, deferred authentication, and token refresh for both VOD and livestreams
|
|
4. **Stream Resolver** (`StreamUrlResolver`): Selects optimal HLS stream by quality preference, filters DRM content
|
|
5. **Metadata Cache** (`MetadataCache`): Thread-safe `ConcurrentDictionary` cache with dynamic TTL for livestreams
|
|
6. **Media Composition Fetcher** (`MediaCompositionFetcher`): Cached wrapper around API client for media composition requests
|
|
7. **Content Providers** (`SRFSeriesProvider`, `SRFEpisodeProvider`, `SRFImageProvider`, `SRFMediaProvider`): Jellyfin integration for series, episodes, images, and media sources
|
|
8. **Scheduled Tasks**: Automatic content refresh and expiration management
|
|
9. **Utilities**: Business unit extensions, MIME type helpers, URN parsing, placeholder image generation
|
|
|
|
## Important Notes
|
|
|
|
### Content Limitations
|
|
|
|
- **No DRM content**: Only non-DRM protected content is accessible (no Widevine/FairPlay)
|
|
- **No live TV channels**: Main channels (SRF 1, SRF zwei, SRF info) use DRM and are not supported
|
|
- **Sports livestreams supported**: Scheduled sports events (skiing, F1, football, etc.) work without DRM
|
|
- **Content expiration**: SRF content has validity periods, plugin tracks and removes expired content
|
|
- **Upcoming events**: Sports events appear before they start but require channel refresh to play once live
|
|
- **No subtitles**: Subtitle support not currently implemented
|
|
|
|
### Extensibility
|
|
|
|
The plugin is designed to support all Swiss broadcasting units:
|
|
- **SRF** (Schweizer Radio und Fernsehen - German)
|
|
- **RTS** (Radio Télévision Suisse - French)
|
|
- **RSI** (Radiotelevisione svizzera - Italian)
|
|
- **RTR** (Radiotelevisiun Svizra Rumantscha - Romansh)
|
|
- **SWI** (Swiss World International)
|
|
|
|
Currently focused on SRF but easily extensible.
|
|
|
|
## Development
|
|
|
|
The plugin includes:
|
|
- Complete API integration with SRF Play (Integration Layer v2.0 and Play v3)
|
|
- **Live sports streaming** with scheduled event detection
|
|
- Channel with Latest, Trending, and Live Sports folders
|
|
- Metadata providers for series and episodes
|
|
- Image fetching and caching
|
|
- HLS stream playback with Akamai token authentication
|
|
- Automatic content expiration handling
|
|
- Scheduled tasks for content refresh
|
|
- Smart caching with dynamic TTL for upcoming livestreams
|
|
|
|
### Known Issues
|
|
|
|
- Some clients may experience issues with hardware decoding (appears to be client-specific)
|
|
- Some edge cases may need additional handling
|
|
- Performance optimization may be needed for very large content catalogs
|
|
|
|
### Contributing
|
|
|
|
Contributions welcome! This project is hosted on a self-hosted [Gitea](https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay) instance.
|
|
|
|
**You don't need a separate account** — you can sign in with your existing GitHub account. On the [sign-in page](https://gitea.tourolle.paris/user/login), choose **"Sign in with GitHub"** to register and log in via GitHub OAuth. Once signed in, you can:
|
|
|
|
- **Raise issues** — report bugs or request features on the [issue tracker](https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/issues)
|
|
- **Contribute code** — fork the repository, push a branch, and open a pull request
|
|
|
|
## License
|
|
|
|
See LICENSE file for details.
|
|
|
|
## Acknowledgments
|
|
|
|
This plugin was developed partly using [Claude Code](https://docs.anthropic.com/en/docs/claude-code) by Anthropic.
|
|
|
|
Inspired by the excellent [Kodi SRG SSR addon](https://github.com/goggle/script.module.srgssr) by [@goggle](https://github.com/goggle), which served as a fantastic reference for understanding the SRG SSR API structure, authentication mechanisms, and handling of scheduled livestreams.
|
|
|
|
## References
|
|
|
|
- [SRF Play](https://www.srf.ch/play)
|
|
- [Jellyfin Plugin Documentation](https://jellyfin.org/docs/general/server/plugins/)
|
|
- [SRG SSR Integration Layer API](https://il.srgssr.ch/)
|
|
- [Kodi SRG SSR Addon](https://github.com/goggle/script.module.srgssr) - Reference implementation
|