Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ The integration works transparently:

TX-Queue IPC Mode provides superior performance and reliability compared to traditional mutex-based streaming.

### Live VOD Switching for Discontinuity Handling

**Smart Playlist Switching**: Automatic detection and handling of stream discontinuities:

- **Discontinuity Detection**: Uses TSDuck HLS wrapper to detect stream discontinuities in real-time
- **Automatic VOD Switching**: Switches to VOD-style playlist when discontinuities persist (3+ cycles)
- **Seamless Recovery**: Returns to original playlist when stream quality stabilizes (5+ clean cycles)
- **Minimal Disruption**: Maintains playback continuity during playlist transitions
- **Configurable Thresholds**: Tunable sensitivity for different streaming conditions

This feature addresses ad insertion discontinuities and stream quality issues by dynamically switching between live HLS and VOD-style playlists based on detected discontinuities, ensuring smooth playback experience.

## TLS Client Integration

This version includes an integrated TLS client from the [tlsclient](https://github.com/zero3k/tlsclient) repository, providing:
Expand Down
21 changes: 20 additions & 1 deletion TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,26 @@ wine Tardsplaya.exe
5. Malformed API responses ✓
```

### **Phase 4: Windows 7 Specific**
### **Phase 4: Live VOD Switching Testing**
**NEW**: Test automatic discontinuity handling and playlist switching:

```
1. Start stream on channel with ad breaks ✓
2. Monitor debug logs for discontinuity detection ✓
3. Verify automatic switch to VOD playlist ✓
4. Confirm smooth playback during transition ✓
5. Wait for clean stream periods ✓
6. Verify switch back to original playlist ✓
7. Test multiple discontinuity/recovery cycles ✓
```

**Debug Log Indicators**:
- `[PRODUCER] Discontinuities detected in playlist`
- `[LIVE-VOD] Discontinuity threshold reached (3/3), switching to VOD playlist`
- `[LIVE-VOD] Clean playback restored (5/5), switching back to original playlist`
- `[LIVE-VOD] Switched to VOD playlist: https://usher.ttvnw.net/...`

### **Phase 5: Windows 7 Specific**
```
1. Run on clean Windows 7 VM ✓
2. Test without updated certificates ✓
Expand Down
44 changes: 44 additions & 0 deletions twitch_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,50 @@ std::wstring GetModernAccessToken(const std::wstring& channel) {
return L""; // Failed to get token from GraphQL API
}

// Generate VOD playlist URL for a channel (alternative to live HLS when discontinuities found)
std::wstring GenerateVodPlaylistUrl(const std::wstring& channel) {
AddLog(L"Generating VOD playlist URL for channel: " + channel);

// For VOD playback, we need to construct a VOD server URL
// Based on TwitchLink reference: https://usher.ttvnw.net/vod/{video_id}.m3u8
// However, for live streams experiencing discontinuities, we use a different approach
// We construct a VOD-style URL using the channel name with special parameters

std::wstring channel_lower = ToLower(channel);

// Try to get access token first
std::wstring token_info = GetModernAccessToken(channel);
if (token_info.empty() || token_info == L"OFFLINE") {
AddLog(L"Cannot generate VOD URL: no access token available for " + channel);
return L"";
}

// Parse token (format: "signature|token")
size_t sep_pos = token_info.find(L'|');
if (sep_pos == std::wstring::npos) {
AddLog(L"Invalid token format for VOD URL generation");
return L"";
}

std::wstring signature = token_info.substr(0, sep_pos);
std::wstring token = token_info.substr(sep_pos + 1);

// Generate VOD-style URL with live stream fallback
// This uses the same HLS endpoint but with different parameters that may help with discontinuities
std::wstring vod_url = L"https://usher.ttvnw.net/api/channel/hls/" + channel_lower + L".m3u8";
vod_url += L"?sig=" + signature;
vod_url += L"&token=" + token;
vod_url += L"&allow_source=true";
vod_url += L"&allow_audio_only=true";
vod_url += L"&fast_bread=false"; // Disable fast bread for more stable playback
vod_url += L"&p=" + std::to_wstring(rand() % 999999); // Random cache busting
vod_url += L"&type=any"; // Allow any stream type
vod_url += L"&platform=web";

AddLog(L"Generated VOD playlist URL: " + vod_url);
return vod_url;
}

// Parse M3U8 playlist using improved logic from TLS client example
std::map<std::wstring, std::wstring> ParseM3U8Playlist(const std::string& m3u8Content) {
std::map<std::wstring, std::wstring> result;
Expand Down
4 changes: 4 additions & 0 deletions twitch_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ bool FetchTwitchStreamQualities(
// Returns the access token in format "signature|token" or empty string on failure
std::wstring GetModernAccessToken(const std::wstring& channel);

// Generate VOD playlist URL for a channel (alternative to live HLS when discontinuities found)
// Returns empty string if unable to generate VOD URL
std::wstring GenerateVodPlaylistUrl(const std::wstring& channel);

// Parse M3U8 playlist using improved logic from TLS client example
// Returns a map of quality names to stream URLs
std::map<std::wstring, std::wstring> ParseM3U8Playlist(const std::string& m3u8Content);
70 changes: 68 additions & 2 deletions tx_queue_ipc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "stream_thread.h"
#include "stream_pipe.h"
#include "tsduck_hls_wrapper.h"
#include "twitch_api.h"
#include <sstream>
#include <iomanip>
#include <regex>
Expand Down Expand Up @@ -505,6 +506,13 @@ bool TxQueueStreamManager::StartStreaming(
chunk_count_ptr_ = chunk_count;
should_stop_ = false;

// Initialize discontinuity tracking
original_playlist_url_ = playlist_url;
using_vod_playlist_.store(false);
discontinuity_count_.store(0);
consecutive_clean_cycles_.store(0);
vod_playlist_url_.clear();

// Start producer thread (downloads segments and feeds to tx-queue)
producer_thread_ = std::thread(&TxQueueStreamManager::ProducerThreadFunction, this, playlist_url);

Expand Down Expand Up @@ -562,9 +570,12 @@ void TxQueueStreamManager::ProducerThreadFunction(const std::wstring& playlist_u
tsduck_hls::PlaylistParser playlist_parser;

while (!should_stop_.load() && (!cancel_token_ptr_ || !cancel_token_ptr_->load())) {
// Get the current playlist URL (may switch between original and VOD)
std::wstring current_playlist_url = GetCurrentPlaylistUrl();

// Download current playlist
std::string playlist_content;
if (!HttpGetText(playlist_url, playlist_content, cancel_token_ptr_)) {
if (!HttpGetText(current_playlist_url, playlist_content, cancel_token_ptr_)) {
consecutive_errors++;
LogMessage(L"[PRODUCER] Failed to download playlist, attempt " +
std::to_wstring(consecutive_errors) + L"/" + std::to_wstring(max_errors));
Expand All @@ -591,6 +602,9 @@ void TxQueueStreamManager::ProducerThreadFunction(const std::wstring& playlist_u
auto media_segments = playlist_parser.GetSegments();
bool playlist_has_discontinuities = playlist_parser.HasDiscontinuities();

// Handle discontinuity detection for live VOD switching
HandleDiscontinuityDetection(playlist_has_discontinuities);

if (playlist_has_discontinuities) {
LogMessage(L"[PRODUCER] Discontinuities detected in playlist - buffer flushing enabled");
}
Expand All @@ -602,7 +616,7 @@ void TxQueueStreamManager::ProducerThreadFunction(const std::wstring& playlist_u
// Convert segment URL to wide string and make absolute
std::wstring segment_url = media_segment.url;
if (segment_url.find(L"http") != 0) {
segment_url = JoinUrl(playlist_url, segment_url);
segment_url = JoinUrl(current_playlist_url, segment_url);
}

// Skip already downloaded segments
Expand Down Expand Up @@ -761,4 +775,56 @@ void TxQueueStreamManager::UpdateChunkCount(int count) {
if (chunk_count_ptr_) {
chunk_count_ptr_->store(count);
}
}

// Live VOD switching helper methods
void TxQueueStreamManager::HandleDiscontinuityDetection(bool has_discontinuities) {
if (has_discontinuities) {
consecutive_clean_cycles_.store(0);
int current_count = discontinuity_count_.fetch_add(1) + 1;

// Switch to VOD if we hit the threshold and aren't already using VOD
if (current_count >= DISCONTINUITY_THRESHOLD && !using_vod_playlist_.load()) {
LogMessage(L"[LIVE-VOD] Discontinuity threshold reached (" +
std::to_wstring(current_count) + L"/" + std::to_wstring(DISCONTINUITY_THRESHOLD) +
L"), switching to VOD playlist");
SwitchToVodPlaylist();
}
} else {
discontinuity_count_.store(0);
int clean_count = consecutive_clean_cycles_.fetch_add(1) + 1;

// Switch back to original if we have enough clean cycles and are using VOD
if (clean_count >= CLEAN_THRESHOLD && using_vod_playlist_.load()) {
LogMessage(L"[LIVE-VOD] Clean playback restored (" +
std::to_wstring(clean_count) + L"/" + std::to_wstring(CLEAN_THRESHOLD) +
L"), switching back to original playlist");
SwitchToOriginalPlaylist();
}
}
}

std::wstring TxQueueStreamManager::GetCurrentPlaylistUrl() const {
return using_vod_playlist_.load() ? vod_playlist_url_ : original_playlist_url_;
}

bool TxQueueStreamManager::SwitchToVodPlaylist() {
if (vod_playlist_url_.empty()) {
// Generate VOD playlist URL
vod_playlist_url_ = GenerateVodPlaylistUrl(channel_name_);
if (vod_playlist_url_.empty()) {
LogMessage(L"[LIVE-VOD] Failed to generate VOD playlist URL for " + channel_name_);
return false;
}
}

using_vod_playlist_.store(true);
LogMessage(L"[LIVE-VOD] Switched to VOD playlist: " + vod_playlist_url_);
return true;
}

bool TxQueueStreamManager::SwitchToOriginalPlaylist() {
using_vod_playlist_.store(false);
LogMessage(L"[LIVE-VOD] Switched back to original playlist: " + original_playlist_url_);
return true;
}
15 changes: 15 additions & 0 deletions tx_queue_ipc.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ class TxQueueStreamManager {
std::atomic<int>* chunk_count_ptr_;
std::atomic<bool>* cancel_token_ptr_;

// Discontinuity tracking for live VOD switching
std::atomic<bool> using_vod_playlist_{false};
std::atomic<int> discontinuity_count_{0};
std::atomic<int> consecutive_clean_cycles_{0};
std::wstring original_playlist_url_;
std::wstring vod_playlist_url_;
static constexpr int DISCONTINUITY_THRESHOLD = 3; // Switch to VOD after 3 cycles with discontinuities
static constexpr int CLEAN_THRESHOLD = 5; // Switch back after 5 clean cycles

// Thread functions
void ProducerThreadFunction(const std::wstring& playlist_url);
void ConsumerThreadFunction();
Expand All @@ -218,6 +227,12 @@ class TxQueueStreamManager {
bool DownloadSegment(const std::wstring& segment_url, std::vector<char>& segment_data);
void LogMessage(const std::wstring& message);
void UpdateChunkCount(int count);

// Live VOD switching helpers
void HandleDiscontinuityDetection(bool has_discontinuities);
std::wstring GetCurrentPlaylistUrl() const;
bool SwitchToVodPlaylist();
bool SwitchToOriginalPlaylist();
};

} // namespace tardsplaya