Skip to content

Commit adaa591

Browse files
committed
feat: Cross-Provider Failover Pooling Support
Implemented stream pooling support for cross-provider failover scenarios. Previously, when a channel failed over to a different provider, pooling detection would fail because it searched by the failover channel's ID rather than the original requested channel ID. ### Problem 1. **Provider Profile Pooling** (issue #651 ): Streams from failover sources weren't detected during pooling checks, causing duplicate streams when using pooled provider profiles 2. **Cross-Provider Failover Pooling**: When Channel 123 (Provider A) failed over to Channel 456 (Provider B), the system couldn't pool streams because it searched for Channel 456's ID instead of the originally requested Channel 123 ### Solution Track both the requested resource (what the user asked for) and the actual source (what's currently streaming) separately: - original_channel_id: The channel ID originally requested - original_playlist_uuid: The playlist UUID originally requested - id: The actual channel ID streaming (may be different due to failover) - playlist_uuid: The actual playlist UUID (may be different due to failover) - is_failover: Boolean flag indicating if stream is from a failover source
1 parent e454afb commit adaa591

File tree

2 files changed

+82
-30
lines changed

2 files changed

+82
-30
lines changed

app/Services/M3uProxyService.php

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
577577
// Get channel ID
578578
$id = $channel->id;
579579

580+
// Track the original requested channel and playlist for cross-provider failover pooling
581+
$originalChannelId = $channel->id;
582+
$originalPlaylistUuid = $playlist->uuid;
583+
580584
// IMPORTANT: Check for existing pooled stream BEFORE capacity check
581585
// If a pooled stream exists, we can reuse it without consuming additional capacity
582586
// NOTE: We need to select the provider profile FIRST to check for pooled streams with the same provider
@@ -597,13 +601,14 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
597601
}
598602
}
599603

600-
$existingStreamId = $this->findExistingPooledStream($id, $playlist->uuid, $profile->id, $selectedProfile?->id);
604+
// Search for pooled stream by ORIGINAL channel ID (handles cross-provider failovers)
605+
$existingStreamId = $this->findExistingPooledStream($originalChannelId, $originalPlaylistUuid, $profile->id, $selectedProfile?->id);
601606

602607
if ($existingStreamId) {
603608
Log::info('Reusing existing pooled transcoded stream (bypassing capacity check)', [
604609
'stream_id' => $existingStreamId,
605-
'channel_id' => $id,
606-
'playlist_uuid' => $playlist->uuid,
610+
'original_channel_id' => $originalChannelId,
611+
'original_playlist_uuid' => $originalPlaylistUuid,
607612
'profile_id' => $profile->id,
608613
'provider_profile_id' => $selectedProfile?->id,
609614
]);
@@ -615,6 +620,7 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
615620
// Check if primary playlist has stream limits and if it's at capacity
616621
// Only check capacity if we're about to create a NEW stream (no existing pooled stream found)
617622
$primaryUrl = null;
623+
$actualChannel = $channel; // Track the actual channel being used (may differ from original if failover)
618624
if ($playlist->available_streams !== 0) {
619625
$activeStreams = self::getActiveStreamsCountByMetadata('playlist_uuid', $playlist->uuid);
620626

@@ -660,6 +666,7 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
660666
if ($failoverPlaylist->available_streams === 0) {
661667
// No limits on this failover playlist, use it
662668
$playlist = $failoverPlaylist;
669+
$actualChannel = $failoverChannel; // Track that we're using a failover channel
663670
$primaryUrl = PlaylistUrlService::getChannelUrl($failoverChannel, $playlist);
664671
break;
665672
} else {
@@ -669,6 +676,7 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
669676
if ($failoverActiveStreams < $failoverPlaylist->available_streams) {
670677
// Found available failover playlist
671678
$playlist = $failoverPlaylist;
679+
$actualChannel = $failoverChannel; // Track that we're using a failover channel
672680
$primaryUrl = PlaylistUrlService::getChannelUrl($failoverChannel, $playlist);
673681
break;
674682
}
@@ -750,18 +758,32 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
750758
// (before capacity check) to avoid blocking reuse of existing streams.
751759
// If we reach here, no existing stream was found, so create a new one.
752760

761+
// Determine if this is a failover stream
762+
$isFailover = ($actualChannel->id !== $originalChannelId);
763+
753764
$metadata = [
754-
'id' => $id,
765+
'id' => $actualChannel->id, // Actual channel being streamed
755766
'type' => 'channel',
756-
'playlist_uuid' => $playlist->uuid,
767+
'playlist_uuid' => $playlist->uuid, // Actual playlist being used
757768
'profile_id' => $profile->id,
769+
'original_channel_id' => $originalChannelId, // For cross-provider failover pooling
770+
'original_playlist_uuid' => $originalPlaylistUuid, // For cross-provider failover pooling
771+
'is_failover' => $isFailover,
758772
];
759773

760774
// Add provider profile ID if using profiles
761775
if ($selectedProfile) {
762776
$metadata['provider_profile_id'] = $selectedProfile->id;
763777
}
764778

779+
Log::debug('Creating transcoded stream with failover tracking', [
780+
'original_channel_id' => $originalChannelId,
781+
'actual_channel_id' => $actualChannel->id,
782+
'is_failover' => $isFailover,
783+
'original_playlist_uuid' => $originalPlaylistUuid,
784+
'actual_playlist_uuid' => $playlist->uuid,
785+
]);
786+
765787
$streamId = $this->createTranscodedStream($primaryUrl, $profile, $failovers, $userAgent, $headers, $metadata);
766788

767789
// Track connection for provider profile
@@ -773,11 +795,17 @@ public function getChannelUrl($playlist, $channel, ?Request $request = null, ?St
773795
return $this->buildTranscodeStreamUrl($streamId, $profile->format ?? 'ts', $username);
774796
} else {
775797
// Use direct streaming endpoint
798+
// Determine if this is a failover stream
799+
$isFailover = ($actualChannel->id !== $originalChannelId);
800+
776801
$metadata = [
777-
'id' => $id,
802+
'id' => $actualChannel->id, // Actual channel being streamed
778803
'type' => 'channel',
779-
'playlist_uuid' => $playlist->uuid,
804+
'playlist_uuid' => $playlist->uuid, // Actual playlist being used
780805
'strict_live_ts' => $playlist->strict_live_ts,
806+
'original_channel_id' => $originalChannelId, // For cross-provider failover pooling
807+
'original_playlist_uuid' => $originalPlaylistUuid, // For cross-provider failover pooling
808+
'is_failover' => $isFailover,
781809
];
782810

783811
// Add provider profile ID if using profiles
@@ -1472,23 +1500,27 @@ public function getPublicUrl(): string
14721500
* This allows multiple clients to connect to the same transcoded stream without
14731501
* consuming additional provider connections.
14741502
*
1475-
* @param int $channelId Channel ID
1476-
* @param string $playlistUuid Playlist UUID
1503+
* Supports cross-provider failover pooling by searching based on the ORIGINAL
1504+
* requested channel, not the actual source channel (which may be a failover).
1505+
*
1506+
* @param int $channelId Original requested channel ID
1507+
* @param string $playlistUuid Original requested playlist UUID
14771508
* @param int|null $profileId StreamProfile ID (transcoding profile)
14781509
* @param int|null $providerProfileId PlaylistProfile ID (provider profile)
14791510
* @return string|null Stream ID if found, null otherwise
14801511
*/
14811512
protected function findExistingPooledStream(int $channelId, string $playlistUuid, ?int $profileId = null, ?int $providerProfileId = null): ?string
14821513
{
14831514
try {
1484-
// Query m3u-proxy for streams by metadata
1515+
// Query m3u-proxy for streams by ORIGINAL channel ID metadata
1516+
// This enables pooling across different provider failovers
14851517
$endpoint = $this->apiBaseUrl.'/streams/by-metadata';
14861518
$response = Http::timeout(5)->acceptJson()
14871519
->withHeaders(array_filter([
14881520
'X-API-Token' => $this->apiToken,
14891521
]))
14901522
->get($endpoint, [
1491-
'field' => 'id',
1523+
'field' => 'original_channel_id', // Search by original, not actual channel
14921524
'value' => (string) $channelId,
14931525
'active_only' => true, // Only return active streams
14941526
]);
@@ -1505,22 +1537,25 @@ protected function findExistingPooledStream(int $channelId, string $playlistUuid
15051537
$metadata = $stream['metadata'] ?? [];
15061538

15071539
// Check if this stream matches our criteria:
1508-
// 1. Same channel ID
1509-
// 2. Same playlist UUID
1540+
// 1. Same ORIGINAL channel ID (enables cross-provider failover pooling)
1541+
// 2. Same ORIGINAL playlist UUID (enables cross-provider failover pooling)
15101542
// 3. Is a transcoded stream (has transcoding metadata)
15111543
// 4. Same StreamProfile ID (transcoding profile, if specified)
15121544
// 5. Same PlaylistProfile ID (provider profile, if specified)
15131545
if (
1514-
($metadata['id'] ?? null) == $channelId &&
1515-
($metadata['playlist_uuid'] ?? null) === $playlistUuid &&
1546+
($metadata['original_channel_id'] ?? null) == $channelId &&
1547+
($metadata['original_playlist_uuid'] ?? null) === $playlistUuid &&
15161548
($metadata['transcoding'] ?? null) === 'true' &&
15171549
($profileId === null || ($metadata['profile_id'] ?? null) == $profileId) &&
15181550
($providerProfileId === null || ($metadata['provider_profile_id'] ?? null) == $providerProfileId)
15191551
) {
1520-
Log::info('Found existing pooled transcoded stream', [
1552+
Log::info('Found existing pooled transcoded stream (cross-provider failover support)', [
15211553
'stream_id' => $stream['stream_id'],
1522-
'channel_id' => $channelId,
1523-
'playlist_uuid' => $playlistUuid,
1554+
'original_channel_id' => $channelId,
1555+
'original_playlist_uuid' => $playlistUuid,
1556+
'actual_channel_id' => $metadata['id'] ?? null,
1557+
'actual_playlist_uuid' => $metadata['playlist_uuid'] ?? null,
1558+
'is_failover' => $metadata['is_failover'] ?? false,
15241559
'profile_id' => $profileId,
15251560
'provider_profile_id' => $providerProfileId,
15261561
'client_count' => $stream['client_count'],

docs/stream-pooling.md

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,19 @@ The m3u-proxy handles the actual stream pooling:
100100
1. **Stream Creation**: Each transcoded stream includes metadata:
101101
```json
102102
{
103-
"id": "12345",
103+
"id": "456",
104104
"type": "channel",
105-
"playlist_uuid": "abc-def-123",
105+
"playlist_uuid": "provider-b-uuid",
106106
"transcoding": "true",
107107
"profile_id": "5",
108-
"provider_profile_id": "2"
108+
"provider_profile_id": "2",
109+
"original_channel_id": "123",
110+
"original_playlist_uuid": "provider-a-uuid",
111+
"is_failover": true
109112
}
110113
```
114+
115+
Note: `id` and `playlist_uuid` represent the ACTUAL source, while `original_*` fields track the REQUESTED channel for cross-provider pooling.
111116

112117
2. **Client Registration**: Multiple clients can register to the same stream
113118
3. **FFmpeg Process Sharing**: All clients receive data from the same FFmpeg transcoding process
@@ -163,12 +168,14 @@ Provider limits are respected automatically:
163168
### For Pooling to Work
164169

165170
1. **Transcoding must be enabled** (profile parameter provided)
166-
2. **Same channel** (same channel ID)
167-
3. **Same playlist** (same playlist UUID)
171+
2. **Same original channel** (same original channel ID - even if served from different failover sources)
172+
3. **Same original playlist** (same original playlist UUID - even if served from different failover playlists)
168173
4. **Same transcoding profile** (StreamProfile ID)
169174
5. **Same provider profile** (PlaylistProfile ID, if using pooled provider profiles)
170175
6. **Stream still active** (has at least one connected client)
171176

177+
**New in v1.x**: Pooling now works across cross-provider failovers! If Channel 123 from Provider A fails over to Channel 456 from Provider B, subsequent requests for Channel 123 will correctly pool into the existing stream.
178+
172179
### Direct Streams (Non-Transcoded)
173180

174181
Direct streams (without transcoding) **do NOT pool** because:
@@ -220,20 +227,30 @@ Look for these log messages:
220227

221228
### m3u-editor Logs
222229
```
223-
[INFO] Found existing pooled transcoded stream
230+
[INFO] Found existing pooled transcoded stream (cross-provider failover support)
224231
stream_id: abc123...
225-
channel_id: 12345
226-
playlist_uuid: xyz789...
232+
original_channel_id: 123
233+
original_playlist_uuid: provider-a-uuid
234+
actual_channel_id: 456
235+
actual_playlist_uuid: provider-b-uuid
236+
is_failover: true
227237
profile_id: 5
228238
provider_profile_id: 2
229239
client_count: 3
230240
231241
[INFO] Reusing existing pooled transcoded stream (bypassing capacity check)
232242
stream_id: abc123...
233-
channel_id: 12345
234-
playlist_uuid: xyz789...
243+
original_channel_id: 123
244+
original_playlist_uuid: provider-a-uuid
235245
profile_id: 5
236246
provider_profile_id: 2
247+
248+
[DEBUG] Creating transcoded stream with failover tracking
249+
original_channel_id: 123
250+
actual_channel_id: 456
251+
is_failover: true
252+
original_playlist_uuid: provider-a-uuid
253+
actual_playlist_uuid: provider-b-uuid
237254
```
238255

239256
### m3u-proxy Logs
@@ -272,11 +289,11 @@ curl -H "X-API-Token: your-token" \
272289
```
273290

274291
**Common Issues**:
275-
- Different playlists (playlist_uuid doesn't match)
292+
- Different original channels (original_channel_id doesn't match)
293+
- Different original playlists (original_playlist_uuid doesn't match)
276294
- Different transcoding profiles (profile_id doesn't match)
277295
- Different provider profiles (provider_profile_id doesn't match)
278296
- Stream expired (no active clients when new user tries to connect)
279-
- Failover sources not matching (fixed in v1.x - now properly matches provider_profile_id)
280297

281298
### Stream Quality Issues
282299

0 commit comments

Comments
 (0)