From 8a2fa68f930b1f20d5bb8af8a2d3d431bf11d6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Thu, 27 Nov 2025 09:01:05 +0000 Subject: [PATCH 01/19] Default Display setting added --- CMakeLists.txt | 2 +- pip/preferences.h | 1 + pip/preferences.m | 42 ++++++++++++++++++++++++++++++++++++++++++ pip/window.m | 18 ++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 679c8ab..69ce50e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0) project(PiP) -add_compile_options(-fobjc-arc -Wno-deprecated-declarations -Wno-format) +add_compile_options(-fobjc-arc -Wno-deprecated-declarations -Wno-format -mmacosx-version-min=14.0) set(frameworks AVFoundation Cocoa VideoToolbox AudioToolbox CoreMedia QuartzCore OpenGL Metal MetalKit PIP SkyLight ScreenCaptureKit) list(TRANSFORM frameworks PREPEND "-framework ") diff --git a/pip/preferences.h b/pip/preferences.h index bbccd14..c0a2f57 100644 --- a/pip/preferences.h +++ b/pip/preferences.h @@ -18,6 +18,7 @@ typedef enum{ NSObject* getPref(NSString* key); NSObject* getPrefOption(NSString* key); void setPref(NSString* key, NSObject* val); +NSArray* getDisplayList(void); @interface Preferences : NSPanel diff --git a/pip/preferences.m b/pip/preferences.m index e74f015..e423454 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -14,11 +14,25 @@ Preferences* global_pref = nil; +NSArray* getDisplayList(void){ + NSMutableArray* displays = [[NSMutableArray alloc] init]; + [displays addObject:@{@"name": @"Ninguno", @"id": @-1}]; + for(NSScreen* screen in [NSScreen screens]){ + NSDictionary* dict = [screen deviceDescription]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + NSString* name = @"Display"; + if (@available(macOS 10.15, *)) name = [screen localizedName]; + [displays addObject:@{@"name": name, @"id": [NSNumber numberWithUnsignedInt:did]}]; + } + return displays; +} + typedef enum{ OptionTypeNumber, OptionTypeSelect, OptionTypeCheckBox, OptionTypeTextInput, + OptionTypeDisplaySelect, } OptionType; #define OPTION(name, text, type, options, value, desc) \ @@ -28,6 +42,7 @@ NSMutableArray* prefs = [NSMutableArray arrayWithArray:@[ OPTION(hidpi, "Use HiDPI mode", CheckBox, [NSNull null], @1, @"on supported displays"), OPTION(renderer, "Display Renderer", Select, (@[@"Metal", @"Opengl"]), [NSNumber numberWithInt:DisplayRendererTypeOpenGL], [NSNull null]), + OPTION(default_display, "Default Display", DisplaySelect, [NSNull null], @-1, [NSNull null]), #ifndef NO_AIRPLAY OPTION(airplay, "AirPlay Receiver", CheckBox, [NSNull null], @0, @"Use PiP as Airplay receiver"), OPTION(airplay_scale_factor, "AirPlay Scale factor", Select, (@[@"1.00", @"2.00", @"3.00", @"Default"]), @3, [NSNull null]), @@ -171,6 +186,12 @@ - (void)onSelect:(NSMenuItem*)sender{ setPref(sender.identifier, [NSNumber numberWithLong:index]); } +- (void)onDisplaySelect:(NSMenuItem*)sender{ + NSNumber* displayId = [sender representedObject]; +// NSLog(@"onDisplaySelect: %@ -> %@", sender.identifier, displayId); + setPref(sender.identifier, displayId); +} + - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{ NSInteger col = [[tableView tableColumns] indexOfObject:tableColumn]; // NSLog(@"row: %ld, col: %ld", row, col); @@ -222,6 +243,27 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null } case OptionTypeTextInput: break; + case OptionTypeDisplaySelect:{ + NSPopUpButton* button = [[NSPopUpButton alloc] init]; + button.translatesAutoresizingMaskIntoConstraints = false; + button.menu = [[NSMenu alloc] init]; + + NSArray* displays = getDisplayList(); + int savedDisplayId = [(NSNumber*)value intValue]; + int selectedIndex = 0; + for(int i = 0; i < displays.count; i++){ + NSDictionary* display = displays[i]; + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:display[@"name"] action:@selector(onDisplaySelect:) keyEquivalent:@""]; + item.target = self; + item.identifier = key; + item.representedObject = display[@"id"]; + [button.menu addItem:item]; + if([display[@"id"] intValue] == savedDisplayId) selectedIndex = i; + } + [button selectItem:[button.menu itemArray][selectedIndex]]; + view = button; + break; + } } } if(!view) goto end; diff --git a/pip/window.m b/pip/window.m index 9ce2db4..660f448 100644 --- a/pip/window.m +++ b/pip/window.m @@ -1060,6 +1060,24 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ [self setupNonHLSControls]; + if(!is_airplay_session){ + int defaultDisplayId = [(NSNumber*)getPref(@"default_display") intValue]; + if(defaultDisplayId > 0){ + NSArray* displays = getDisplayList(); + for(NSDictionary* display in displays){ + if([display[@"id"] intValue] == defaultDisplayId){ + WindowSel* sel = [WindowSel getDefault]; + sel.title = display[@"name"]; + sel.dspId = defaultDisplayId; + NSMenuItem* item = [[NSMenuItem alloc] init]; + [item setRepresentedObject:sel]; + [self changeWindow:item]; + break; + } + } // End of loop through displays + } + } // End of if not airplay session + return self; } From c79a965df4e0aeb11bfb445d0e0ad632bff571fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Tue, 3 Feb 2026 14:22:47 +0000 Subject: [PATCH 02/19] Add custom display names feature - Added UI panel to configure custom names for displays - Custom names appear in Default Display selector and right-click menu - Names are persisted in user preferences --- pip/preferences.h | 3 + pip/preferences.m | 320 +++++++++++++++++++++++++++++++++++++++++++++- pip/window.m | 3 +- 3 files changed, 319 insertions(+), 7 deletions(-) diff --git a/pip/preferences.h b/pip/preferences.h index c0a2f57..a3f3c70 100644 --- a/pip/preferences.h +++ b/pip/preferences.h @@ -19,6 +19,9 @@ NSObject* getPref(NSString* key); NSObject* getPrefOption(NSString* key); void setPref(NSString* key, NSObject* val); NSArray* getDisplayList(void); +NSString* getDisplayNameForId(CGDirectDisplayID displayId); +void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name); +void showDisplayNamesPanel(void); @interface Preferences : NSPanel diff --git a/pip/preferences.m b/pip/preferences.m index e423454..2ea3379 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -13,19 +13,75 @@ #endif Preferences* global_pref = nil; +static NSPanel* displayNamesPanel = nil; + +/** + * Gets the custom display name for a given display ID. + * @param displayId The CGDirectDisplayID of the display + * @return The custom name if set, otherwise nil + */ +NSString* getCustomDisplayNameForId(CGDirectDisplayID displayId){ + NSDictionary* customNames = (NSDictionary*)getPref(@"display_custom_names"); + if(!customNames) return nil; + NSString* key = [NSString stringWithFormat:@"%u", displayId]; + return customNames[key]; +} // End of getCustomDisplayNameForId() + +/** + * Gets the display name for a given display ID, using custom name if available. + * @param displayId The CGDirectDisplayID of the display + * @return The custom name if set, otherwise the system localized name + */ +NSString* getDisplayNameForId(CGDirectDisplayID displayId){ + NSString* customName = getCustomDisplayNameForId(displayId); + if(customName && customName.length > 0) return customName; + + // Find the screen and return its localized name + for(NSScreen* screen in [NSScreen screens]){ + NSDictionary* dict = [screen deviceDescription]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + if(did == displayId){ + if (@available(macOS 10.15, *)) return [screen localizedName]; + return [NSString stringWithFormat:@"Display %u", displayId]; + } + } // End of loop through screens + return [NSString stringWithFormat:@"Display %u", displayId]; +} // End of getDisplayNameForId() + +/** + * Sets a custom display name for a given display ID. + * @param displayId The CGDirectDisplayID of the display + * @param name The custom name to set (empty string to clear) + */ +void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ + NSDictionary* existingNames = (NSDictionary*)[[NSUserDefaults standardUserDefaults] objectForKey:@"display_custom_names"]; + NSMutableDictionary* customNames = existingNames ? [existingNames mutableCopy] : [[NSMutableDictionary alloc] init]; + NSString* key = [NSString stringWithFormat:@"%u", displayId]; + + if(name && name.length > 0){ + customNames[key] = name; + } else { + [customNames removeObjectForKey:key]; + } + + [[NSUserDefaults standardUserDefaults] setObject:customNames forKey:@"display_custom_names"]; +} // End of setCustomDisplayName() +/** + * Gets the list of available displays with their names (using custom names if set). + * @return An array of dictionaries with "name" and "id" keys + */ NSArray* getDisplayList(void){ NSMutableArray* displays = [[NSMutableArray alloc] init]; [displays addObject:@{@"name": @"Ninguno", @"id": @-1}]; for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; - NSString* name = @"Display"; - if (@available(macOS 10.15, *)) name = [screen localizedName]; + NSString* name = getDisplayNameForId(did); [displays addObject:@{@"name": name, @"id": [NSNumber numberWithUnsignedInt:did]}]; - } + } // End of loop through screens return displays; -} +} // End of getDisplayList() typedef enum{ OptionTypeNumber, @@ -33,6 +89,7 @@ OptionTypeCheckBox, OptionTypeTextInput, OptionTypeDisplaySelect, + OptionTypeButton, } OptionType; #define OPTION(name, text, type, options, value, desc) \ @@ -43,6 +100,7 @@ OPTION(hidpi, "Use HiDPI mode", CheckBox, [NSNull null], @1, @"on supported displays"), OPTION(renderer, "Display Renderer", Select, (@[@"Metal", @"Opengl"]), [NSNumber numberWithInt:DisplayRendererTypeOpenGL], [NSNull null]), OPTION(default_display, "Default Display", DisplaySelect, [NSNull null], @-1, [NSNull null]), + OPTION(display_names, "Display Names", Button, [NSNull null], [NSNull null], @"Configure..."), #ifndef NO_AIRPLAY OPTION(airplay, "AirPlay Receiver", CheckBox, [NSNull null], @0, @"Use PiP as Airplay receiver"), OPTION(airplay_scale_factor, "AirPlay Scale factor", Select, (@[@"1.00", @"2.00", @"3.00", @"Default"]), @3, [NSNull null]), @@ -192,6 +250,12 @@ - (void)onDisplaySelect:(NSMenuItem*)sender{ setPref(sender.identifier, displayId); } +- (void)onButtonClick:(NSButton*)sender{ + if([sender.identifier isEqual:@"display_names"]){ + showDisplayNamesPanel(); + } +} // End of onButtonClick() + - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{ NSInteger col = [[tableView tableColumns] indexOfObject:tableColumn]; // NSLog(@"row: %ld, col: %ld", row, col); @@ -259,11 +323,19 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null item.representedObject = display[@"id"]; [button.menu addItem:item]; if([display[@"id"] intValue] == savedDisplayId) selectedIndex = i; - } + } // End of loop through displays [button selectItem:[button.menu itemArray][selectedIndex]]; view = button; break; } + case OptionTypeButton:{ + NSButton* button = [NSButton buttonWithTitle:pref[@"desc"] target:self action:@selector(onButtonClick:)]; + button.translatesAutoresizingMaskIntoConstraints = false; + button.bezelStyle = NSBezelStyleRounded; + button.identifier = key; + view = button; + break; + } } } if(!view) goto end; @@ -296,3 +368,241 @@ - (void)windowWillClose:(NSNotification *)notification{ } @end + +#pragma mark - Display Names Panel + +@interface DisplayNamesPanel : NSPanel +@property (nonatomic, strong) NSTableView* tableView; +@property (nonatomic, strong) NSMutableArray* displayData; +@property (nonatomic, strong) NSMutableArray* textFields; +- (void)refreshPreferencesWindow; +@end + +@implementation DisplayNamesPanel + +/** + * Initializes the display names panel. + * @return The initialized panel + */ +-(id)init{ + self = [super + initWithContentRect:NSMakeRect(0, 0, 450, 200) + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable + backing:NSBackingStoreBuffered defer:YES + ]; + self.delegate = self; + self.level = NSFloatingWindowLevel; + self.collectionBehavior = NSWindowCollectionBehaviorManaged | NSWindowCollectionBehaviorParticipatesInCycle; + self.becomesKeyOnlyIfNeeded = NO; + [self setTitle:@"Display Names"]; + + [self loadDisplayData]; + _textFields = [[NSMutableArray alloc] init]; + + NSView* rootView = [[NSView alloc] init]; + rootView.translatesAutoresizingMaskIntoConstraints = false; + + // Create scroll view for table + NSScrollView* scrollView = [[NSScrollView alloc] init]; + scrollView.hasHorizontalScroller = false; + scrollView.hasVerticalScroller = true; + scrollView.translatesAutoresizingMaskIntoConstraints = false; + [rootView addSubview:scrollView]; + + // Create OK button + NSButton* okButton = [NSButton buttonWithTitle:@"OK" target:self action:@selector(onOKClick:)]; + okButton.translatesAutoresizingMaskIntoConstraints = false; + okButton.bezelStyle = NSBezelStyleRounded; + okButton.keyEquivalent = @"\r"; // Enter key + [rootView addSubview:okButton]; + + // Layout constraints + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeRight multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeTop multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:okButton attribute:NSLayoutAttributeTop multiplier:1 constant:-10]]; + + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:okButton attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeRight multiplier:1 constant:-15]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:okButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeBottom multiplier:1 constant:-10]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:okButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:80]]; + + _tableView = [[NSTableView alloc] init]; + _tableView.frame = rootView.bounds; + _tableView.delegate = self; + _tableView.dataSource = self; + _tableView.headerView = nil; + _tableView.intercellSpacing = NSMakeSize(0, 5); + _tableView.translatesAutoresizingMaskIntoConstraints = NO; + _tableView.rowHeight = 28; + + NSTableColumn* systemNameCol = [[NSTableColumn alloc] initWithIdentifier:@"systemName"]; + systemNameCol.title = @"System Name"; + systemNameCol.width = 150; + [_tableView addTableColumn:systemNameCol]; + + NSTableColumn* customNameCol = [[NSTableColumn alloc] initWithIdentifier:@"customName"]; + customNameCol.title = @"Custom Name"; + customNameCol.width = 250; + [_tableView addTableColumn:customNameCol]; + + scrollView.documentView = _tableView; + [self setContentView:rootView]; + + NSSize windowSize = [self frame].size; + NSSize screenSize = [[self screen] visibleFrame].size; + NSPoint point = NSMakePoint(screenSize.width/2 - windowSize.width/2, screenSize.height/2 - windowSize.height/2); + [self setFrameOrigin:point]; + + return self; +} // End of init() + +/** + * Called when OK button is clicked. Closes the panel. + * @param sender The button that was clicked + */ +- (void)onOKClick:(NSButton*)sender{ + [self close]; +} // End of onOKClick() + +/** + * Refreshes the preferences window to show updated display names. + */ +- (void)refreshPreferencesWindow{ + if(global_pref){ + // Find the table view in the preferences window and reload it + NSView* contentView = [global_pref contentView]; + for(NSView* subview in contentView.subviews){ + if([subview isKindOfClass:[NSScrollView class]]){ + NSScrollView* scrollView = (NSScrollView*)subview; + if([scrollView.documentView isKindOfClass:[NSTableView class]]){ + NSTableView* tableView = (NSTableView*)scrollView.documentView; + [tableView reloadData]; + break; + } + } + } // End of loop through subviews + } +} // End of refreshPreferencesWindow() + +/** + * Loads display data from connected screens. + */ +- (void)loadDisplayData{ + _displayData = [[NSMutableArray alloc] init]; + for(NSScreen* screen in [NSScreen screens]){ + NSDictionary* dict = [screen deviceDescription]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + NSString* systemName = @"Display"; + if (@available(macOS 10.15, *)) systemName = [screen localizedName]; + NSString* customName = getCustomDisplayNameForId(did); + if(!customName) customName = @""; + + [_displayData addObject:[@{ + @"id": [NSNumber numberWithUnsignedInt:did], + @"systemName": systemName, + @"customName": customName + } mutableCopy]]; + } // End of loop through screens +} // End of loadDisplayData() + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{ + return _displayData.count; +} + +- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{ + NSTableCellView* cell = [[NSTableCellView alloc] init]; + NSMutableDictionary* display = _displayData[row]; + + if([tableColumn.identifier isEqual:@"systemName"]){ + NSTextField* text = [[NSTextField alloc] init]; + text.stringValue = display[@"systemName"]; + text.editable = false; + text.drawsBackground = false; + text.bordered = false; + text.translatesAutoresizingMaskIntoConstraints = false; + text.textColor = [NSColor secondaryLabelColor]; + [cell addSubview:text]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1 constant:8]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1 constant:-8]]; + } else if([tableColumn.identifier isEqual:@"customName"]){ + NSTextField* textField = [[NSTextField alloc] init]; + textField.stringValue = display[@"customName"]; + textField.placeholderString = display[@"systemName"]; + textField.editable = YES; + textField.selectable = YES; + textField.bezeled = YES; + textField.bezelStyle = NSTextFieldSquareBezel; + textField.drawsBackground = YES; + textField.backgroundColor = [NSColor textBackgroundColor]; + textField.translatesAutoresizingMaskIntoConstraints = false; + textField.delegate = self; + textField.tag = row; + textField.cell.scrollable = YES; + + // Add to text fields array for tab navigation + while(_textFields.count <= (NSUInteger)row){ + [_textFields addObject:[NSNull null]]; + } + _textFields[row] = textField; + + // Set up tab order + if(row > 0 && _textFields.count > 1 && _textFields[row-1] != [NSNull null]){ + NSTextField* prevField = _textFields[row-1]; + [prevField setNextKeyView:textField]; + } + if(row == 0 && _textFields.count > 0){ + [self setInitialFirstResponder:textField]; + } + + [cell addSubview:textField]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1 constant:8]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1 constant:-8]]; + } + + return cell; +} // End of tableView:viewForTableColumn:row: + +- (void)controlTextDidEndEditing:(NSNotification *)notification{ + NSTextField* textField = notification.object; + NSInteger row = textField.tag; + if(row >= 0 && row < (NSInteger)_displayData.count){ + NSMutableDictionary* display = _displayData[row]; + NSString* newName = textField.stringValue; + display[@"customName"] = newName; + CGDirectDisplayID displayId = [display[@"id"] unsignedIntValue]; + setCustomDisplayName(displayId, newName); + } +} // End of controlTextDidEndEditing() + +- (nullable NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row{ + NSTableRowView* rowView = [[NSTableRowView alloc] init]; + rowView.emphasized = false; + return rowView; +} + +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex{ + return NO; +} + +- (void)windowDidBecomeKey:(NSNotification *)notification{ + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; +} + +- (void)windowWillClose:(NSNotification *)notification{ + [self refreshPreferencesWindow]; + displayNamesPanel = nil; +} + +@end + +/** + * Shows the display names configuration panel. + */ +void showDisplayNamesPanel(void){ + if(!displayNamesPanel){ + displayNamesPanel = [[DisplayNamesPanel alloc] init]; + } + [displayNamesPanel makeKeyAndOrderFront:nil]; +} // End of showDisplayNamesPanel() diff --git a/pip/window.m b/pip/window.m index 660f448..695e240 100644 --- a/pip/window.m +++ b/pip/window.m @@ -1752,8 +1752,7 @@ - (void)rightMouseDown:(NSEvent *)theEvent { // NSLog(@"%@", dict); CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; - NSString* windowTitle = [NSString stringWithFormat:@"Display %u", did]; - if (@available(macOS 10.15, *)) windowTitle = [NSString stringWithFormat:@"%@", [screen localizedName]]; + NSString* windowTitle = getDisplayNameForId(did); WindowSel* sel = [WindowSel getDefault]; sel.title = windowTitle; From 5dd4ecdbf1cc2c0c453d38680b04868a7559dc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Tue, 3 Feb 2026 14:37:30 +0000 Subject: [PATCH 03/19] Fix preferences UI and Display Names panel issues - Add visual spacing between Display Renderer, Default Display, and Display Names options - Fix Shift+Tab keyboard navigation in Display Names panel by completing circular tab order - Add visual cue for unknown displays (italic placeholder text) - Fix display names not persisting when clicking OK by committing edits before close --- pip/preferences.m | 91 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/pip/preferences.m b/pip/preferences.m index 2ea3379..54ae663 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -359,6 +359,22 @@ - (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex{ return NO; } +/** + * Returns the height for a specific row in the preferences table. + * Adds extra spacing after certain rows to create visual groups. + * @param tableView The table view + * @param row The row index + * @return The height for the row + */ +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row{ + CGFloat baseHeight = 26; + // Add 12pt padding below Display Renderer, Default Display, and Display Names rows + if(row == 1 || row == 2 || row == 3){ + return baseHeight + 12; + } + return baseHeight; +} // End of tableView:heightOfRow: + - (void)windowDidBecomeKey:(NSNotification *)notification{ [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } @@ -375,7 +391,9 @@ @interface DisplayNamesPanel : NSPanel ... -> last text field -> OK button -> first text field + */ +- (void)completeTabOrder{ + if(_textFields.count == 0) return; + + // Find first and last valid text fields + NSTextField* firstField = nil; + NSTextField* lastField = nil; + + for(NSUInteger i = 0; i < _textFields.count; i++){ + if(_textFields[i] != [NSNull null]){ + if(!firstField) firstField = _textFields[i]; + lastField = _textFields[i]; + } + } // End of loop through text fields + + if(firstField && lastField && _okButton){ + [lastField setNextKeyView:_okButton]; + [_okButton setNextKeyView:firstField]; + } +} // End of completeTabOrder() + - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{ return _displayData.count; } @@ -515,12 +567,24 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null if([tableColumn.identifier isEqual:@"systemName"]){ NSTextField* text = [[NSTextField alloc] init]; - text.stringValue = display[@"systemName"]; + NSString* systemName = display[@"systemName"]; + if(systemName && systemName.length > 0){ + text.stringValue = systemName; + text.textColor = [NSColor secondaryLabelColor]; + } else { + text.stringValue = @"(Unknown display)"; + // Use italic font for placeholder + NSFont* currentFont = [NSFont systemFontOfSize:[NSFont systemFontSize]]; + NSFontDescriptor* italicDescriptor = [currentFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitItalic]; + if(italicDescriptor){ + text.font = [NSFont fontWithDescriptor:italicDescriptor size:currentFont.pointSize]; + } + text.textColor = [NSColor tertiaryLabelColor]; // More muted than secondaryLabelColor + } text.editable = false; text.drawsBackground = false; text.bordered = false; text.translatesAutoresizingMaskIntoConstraints = false; - text.textColor = [NSColor secondaryLabelColor]; [cell addSubview:text]; [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1 constant:8]]; @@ -528,7 +592,8 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null } else if([tableColumn.identifier isEqual:@"customName"]){ NSTextField* textField = [[NSTextField alloc] init]; textField.stringValue = display[@"customName"]; - textField.placeholderString = display[@"systemName"]; + NSString* systemName = display[@"systemName"]; + textField.placeholderString = (systemName && systemName.length > 0) ? systemName : @"Enter display name"; textField.editable = YES; textField.selectable = YES; textField.bezeled = YES; From 0113e191b9255d5c4d6ce545432883d564d1721d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Tue, 3 Feb 2026 17:15:57 +0000 Subject: [PATCH 04/19] Add camera audio capture and monitoring feature - Capture audio from camera/microphone during video capture - Add ring buffer for real-time audio playback via AudioUnit - Add microphone permission handling (non-blocking) - Add "Audio Monitoring" menu toggle when camera is active - Audio enabled by default when camera has microphone --- pip/info.plist | 2 + pip/window.m | 374 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 373 insertions(+), 3 deletions(-) diff --git a/pip/info.plist b/pip/info.plist index 70a861b..faadf74 100644 --- a/pip/info.plist +++ b/pip/info.plist @@ -18,5 +18,7 @@ True NSCameraUsageDescription This app needs access to your camera to capture video + NSMicrophoneUsageDescription + This app needs access to your microphone to capture audio from your camera diff --git a/pip/window.m b/pip/window.m index 695e240..4e88250 100644 --- a/pip/window.m +++ b/pip/window.m @@ -799,6 +799,18 @@ @implementation Window{ NSString* camera_id; AVCaptureDeviceFormat* camera_format; AVCaptureDevicePosition camera_position; + + // Camera audio capture and playback + AVCaptureAudioDataOutput* camera_audio_output; + AudioUnit camera_audio_unit; + bool camera_audio_enabled; + bool camera_has_microphone; + + // Ring buffer for camera audio + uint8_t camera_audio_buffer[64 * 1024]; + UInt32 camera_audio_write_idx; + UInt32 camera_audio_read_idx; + AudioStreamBasicDescription camera_audio_format; #if __has_include() SCStream *window_stream API_AVAILABLE(macos(12.3)); SCStreamConfiguration *window_stream_config API_AVAILABLE(macos(12.3)); @@ -833,6 +845,15 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ camera_output = nil; camera_id = nil; camera_format = nil; + + // Initialize camera audio variables + camera_audio_output = nil; + camera_audio_unit = NULL; + camera_audio_enabled = true; // Enabled by default + camera_has_microphone = false; + camera_audio_write_idx = 0; + camera_audio_read_idx = 0; + memset(&camera_audio_format, 0, sizeof(camera_audio_format)); #if __has_include() if (@available(macOS 12.3, *)) { window_stream = nil; @@ -1989,6 +2010,15 @@ - (void)rightMouseDown:(NSEvent *)theEvent { [item setSubmenu:resolutionMenu]; }) } + + // Audio monitoring toggle + if(camera_has_microphone) { + ADD_MENU_ITEM(theMenu, @"Audio Monitoring", @selector(toggleCameraAudio:), NULL, { + if(camera_audio_enabled) { + [item setState:NSControlStateValueOn]; + } + }) + } } if(!pvc && ([self is_capturing] || is_airplay_session || is_hls_session)){ @@ -2066,16 +2096,254 @@ -(void)stopWindowStream{ #endif } +#pragma mark - Camera Audio Methods + +/** + * Checks microphone permission status and requests access if needed. + * @param completionHandler Called with YES if permission granted, NO otherwise + */ +-(void)checkMicrophonePermission:(void(^)(BOOL granted))completionHandler { + AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + + switch(authStatus) { + case AVAuthorizationStatusAuthorized: + completionHandler(YES); + break; + case AVAuthorizationStatusDenied: + case AVAuthorizationStatusRestricted: + NSLog(@"Microphone permission denied or restricted"); + completionHandler(NO); + break; + case AVAuthorizationStatusNotDetermined: + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(granted); + }); + }]; + break; + } +} // End of checkMicrophonePermission: + +/** + * AudioUnit render callback for camera audio playback. + * Reads PCM data from the ring buffer and outputs to speakers. + */ +static OSStatus CameraAudioRenderCallback(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) { + Window *window = (__bridge Window *)inRefCon; + + // Clear output buffers first + for(UInt32 i = 0; i < ioData->mNumberBuffers; ++i) { + memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize); + } + + // Check if we have data available + if(window->camera_audio_read_idx == window->camera_audio_write_idx) { + return noErr; // No data available + } + + UInt32 bufferLen = sizeof(window->camera_audio_buffer); + UInt32 dataAvailable; + if(window->camera_audio_write_idx > window->camera_audio_read_idx) { + dataAvailable = window->camera_audio_write_idx - window->camera_audio_read_idx; + } else { + dataAvailable = bufferLen - window->camera_audio_read_idx + window->camera_audio_write_idx; + } + + UInt32 bytesPerFrame = window->camera_audio_format.mBytesPerFrame; + if(bytesPerFrame == 0) bytesPerFrame = 4; // Default: 16-bit stereo + + UInt32 bytesRequested = bytesPerFrame * inNumberFrames; + UInt32 bytesToCopy = (bytesRequested < dataAvailable) ? bytesRequested : dataAvailable; + + // Copy data from ring buffer to output (may need to wrap) + uint8_t *outBuffer = (uint8_t *)ioData->mBuffers[0].mData; + UInt32 firstChunkSize = bufferLen - window->camera_audio_read_idx; + if(firstChunkSize > bytesToCopy) firstChunkSize = bytesToCopy; + + memcpy(outBuffer, window->camera_audio_buffer + window->camera_audio_read_idx, firstChunkSize); + + if(firstChunkSize < bytesToCopy) { + // Wrap around + memcpy(outBuffer + firstChunkSize, window->camera_audio_buffer, bytesToCopy - firstChunkSize); + window->camera_audio_read_idx = bytesToCopy - firstChunkSize; + } else { + window->camera_audio_read_idx += firstChunkSize; + if(window->camera_audio_read_idx >= bufferLen) { + window->camera_audio_read_idx = 0; + } + } + + return noErr; +} // End of CameraAudioRenderCallback() + +/** + * Sets up the AudioUnit for camera audio playback. + * @param format The audio stream format from the camera + */ +-(void)setupCameraAudioUnit:(AudioStreamBasicDescription)format { + if(camera_audio_unit) { + [self teardownCameraAudioUnit]; + } + + camera_audio_format = format; + camera_audio_write_idx = 0; + camera_audio_read_idx = 0; + + AudioComponentDescription outputUnitDescription = { + .componentType = kAudioUnitType_Output, + .componentSubType = kAudioUnitSubType_DefaultOutput, + .componentManufacturer = kAudioUnitManufacturer_Apple + }; + + AudioComponent outputComponent = AudioComponentFindNext(NULL, &outputUnitDescription); + if(!outputComponent) { + NSLog(@"Failed to find audio output component"); + return; + } + + OSStatus status = AudioComponentInstanceNew(outputComponent, &camera_audio_unit); + if(status != noErr) { + NSLog(@"AudioComponentInstanceNew failed: %d", (int)status); + return; + } + + status = AudioUnitInitialize(camera_audio_unit); + if(status != noErr) { + NSLog(@"AudioUnitInitialize failed: %d", (int)status); + AudioComponentInstanceDispose(camera_audio_unit); + camera_audio_unit = NULL; + return; + } + + status = AudioUnitSetProperty(camera_audio_unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + &format, + sizeof(format)); + if(status != noErr) { + NSLog(@"AudioUnitSetProperty StreamFormat failed: %d", (int)status); + } + + AURenderCallbackStruct callbackInfo = { + .inputProc = CameraAudioRenderCallback, + .inputProcRefCon = (__bridge void *)(self), + }; + + AudioUnitSetProperty(camera_audio_unit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Global, + 0, + &callbackInfo, + sizeof(callbackInfo)); + + AudioOutputUnitStart(camera_audio_unit); + NSLog(@"Camera audio unit started: sampleRate=%.0f, channels=%u, bitsPerChannel=%u", + format.mSampleRate, format.mChannelsPerFrame, format.mBitsPerChannel); +} // End of setupCameraAudioUnit: + +/** + * Tears down the camera audio unit. + */ +-(void)teardownCameraAudioUnit { + if(camera_audio_unit) { + AudioOutputUnitStop(camera_audio_unit); + AudioUnitUninitialize(camera_audio_unit); + AudioComponentInstanceDispose(camera_audio_unit); + camera_audio_unit = NULL; + } + memset(&camera_audio_format, 0, sizeof(camera_audio_format)); +} // End of teardownCameraAudioUnit + +/** + * Processes audio sample buffer from camera and writes to ring buffer for playback. + * @param sampleBuffer The audio sample buffer from AVCaptureAudioDataOutput + */ +-(void)processCameraAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer { + // Get audio format if not already set + CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); + if(!formatDesc) return; + + const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc); + if(!asbd) return; + + // Setup audio unit on first sample (when we know the format) + if(!camera_audio_unit) { + AudioStreamBasicDescription outputFormat = *asbd; + + // Ensure we have a valid PCM format for output + if(outputFormat.mFormatID != kAudioFormatLinearPCM) { + NSLog(@"Unsupported audio format: %u", (unsigned int)outputFormat.mFormatID); + return; + } + + [self setupCameraAudioUnit:outputFormat]; + } + + // Get audio data from sample buffer + CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); + if(!blockBuffer) return; + + size_t totalLength = 0; + char *dataPointer = NULL; + OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer); + if(status != noErr || !dataPointer || totalLength == 0) return; + + UInt32 bufferLen = sizeof(camera_audio_buffer); + + // Calculate space available in ring buffer + UInt32 spaceAvailable; + if(camera_audio_write_idx >= camera_audio_read_idx) { + spaceAvailable = bufferLen - camera_audio_write_idx + camera_audio_read_idx - 1; + } else { + spaceAvailable = camera_audio_read_idx - camera_audio_write_idx - 1; + } + + if(totalLength > spaceAvailable) { + // Buffer overflow - advance read pointer to make room (drop oldest data) + UInt32 overflow = (UInt32)totalLength - spaceAvailable; + camera_audio_read_idx = (camera_audio_read_idx + overflow) % bufferLen; + } + + // Copy data to ring buffer (may need to wrap) + UInt32 firstChunkSize = bufferLen - camera_audio_write_idx; + if(firstChunkSize > totalLength) firstChunkSize = (UInt32)totalLength; + + memcpy(camera_audio_buffer + camera_audio_write_idx, dataPointer, firstChunkSize); + + if(firstChunkSize < totalLength) { + // Wrap around + memcpy(camera_audio_buffer, dataPointer + firstChunkSize, totalLength - firstChunkSize); + camera_audio_write_idx = (UInt32)(totalLength - firstChunkSize); + } else { + camera_audio_write_idx += firstChunkSize; + if(camera_audio_write_idx >= bufferLen) { + camera_audio_write_idx = 0; + } + } +} // End of processCameraAudioSampleBuffer: + -(void)stopCameraCapture{ if(!camera_session) return; [camera_session stopRunning]; + + // Clean up audio + [self teardownCameraAudioUnit]; + camera_audio_output = nil; + camera_has_microphone = false; + camera_session = nil; camera_input = nil; camera_output = nil; camera_id = nil; camera_format = nil; camera_position = AVCaptureDevicePositionUnspecified; -} +} // End of stopCameraCapture -(NSArray *)getAvailableCameraResolutions:(NSString*)deviceId { NSMutableArray *resolutions = [[NSMutableArray alloc] init]; @@ -2243,6 +2511,72 @@ -(void)startCameraCapture:(NSString*)deviceId{ // We'll handle un-mirroring in the capture output delegate if needed } + // Set up audio capture if enabled and camera has microphone + camera_has_microphone = false; + camera_audio_output = nil; + + if(camera_audio_enabled) { + // Try to find an audio device associated with this camera + // Many cameras have built-in mics that appear as separate audio devices + AVCaptureDevice *audioDevice = nil; + NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; + for(AVCaptureDevice *audioD in audioDevices) { + // Match by name or manufacturer (webcam mics often share these) + if([audioD.localizedName containsString:device.localizedName] || + [audioD.manufacturer isEqualToString:device.manufacturer]) { + audioDevice = audioD; + break; + } + } // End of loop through audio devices + + // If no matching audio device found, check if we have a default microphone + if(!audioDevice && audioDevices.count > 0) { + audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + } + + if(audioDevice) { + camera_has_microphone = true; + + [self checkMicrophonePermission:^(BOOL granted) { + if(!granted) { + NSLog(@"Microphone permission not granted, camera audio disabled"); + self->camera_has_microphone = false; + return; + } + + // Add audio input + NSError *audioError = nil; + AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&audioError]; + if(audioError || !audioInput) { + NSLog(@"Failed to create audio input: %@", audioError); + self->camera_has_microphone = false; + return; + } + + if(![self->camera_session canAddInput:audioInput]) { + NSLog(@"Cannot add audio input to camera session"); + self->camera_has_microphone = false; + return; + } + [self->camera_session addInput:audioInput]; + + // Add audio output + AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; + dispatch_queue_t audioQueue = dispatch_queue_create("com.pip.camera.audio", DISPATCH_QUEUE_SERIAL); + [audioOutput setSampleBufferDelegate:self queue:audioQueue]; + + if(![self->camera_session canAddOutput:audioOutput]) { + NSLog(@"Cannot add audio output to camera session"); + return; + } + [self->camera_session addOutput:audioOutput]; + self->camera_audio_output = audioOutput; + + NSLog(@"Camera audio capture enabled for device: %@", audioDevice.localizedName); + }]; + } + } // End of audio capture setup + camera_session = session; camera_input = input; camera_output = output; @@ -2263,6 +2597,14 @@ -(void)startCameraCapture:(NSString*)deviceId{ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if(!is_playing || isWinClosing || !camera_session) return; + // Check if this is audio or video output + if(output == camera_audio_output && camera_audio_enabled) { + // Audio processing + [self processCameraAudioSampleBuffer:sampleBuffer]; + return; + } + + // Video processing CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); if(!imageBuffer) return; @@ -2275,7 +2617,7 @@ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleB } }); } -} +} // End of captureOutput:didOutputSampleBuffer:fromConnection: - (void)changeWindow:(id)sender{ WindowSel* sel = [sender representedObject]; @@ -2998,7 +3340,33 @@ - (void)selectCameraResolution:(id)sender { NSLog(@"Failed to lock device for configuration: %@", error); } } -} +} // End of selectCameraResolution: + +/** + * Toggles camera audio monitoring on/off. + * @param sender The menu item that triggered this action + */ +-(void)toggleCameraAudio:(id)sender { + camera_audio_enabled = !camera_audio_enabled; + + if(camera_audio_enabled && camera_id && camera_has_microphone) { + // Restart camera capture to enable audio + NSString *currentCameraId = [camera_id copy]; + [self startCameraCapture:currentCameraId]; + } else if(!camera_audio_enabled) { + // Stop audio playback + [self teardownCameraAudioUnit]; + // Remove audio output from session if present + if(camera_audio_output && camera_session) { + [camera_session beginConfiguration]; + [camera_session removeOutput:camera_audio_output]; + [camera_session commitConfiguration]; + camera_audio_output = nil; + } + } + + NSLog(@"Camera audio monitoring %@", camera_audio_enabled ? @"enabled" : @"disabled"); +} // End of toggleCameraAudio: - (void)updateHLSInputViewLayout { if (!hlsInputView) return; From d965801c1dfae311417a61c0d8ce30e163064dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Thu, 5 Feb 2026 18:19:45 +0000 Subject: [PATCH 05/19] Fix display names persistence across reboots - Use stable EDID-based identifiers (vendor-model-serial) instead of CGDirectDisplayID which changes between reboots - Remove NSScreen fallback that could deadlock WindowServer - Fix intValue to unsignedIntValue for proper display ID handling - Maintain backwards compatibility with legacy preference format --- pip/preferences.m | 56 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/pip/preferences.m b/pip/preferences.m index 54ae663..603fde4 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -8,6 +8,7 @@ #import "preferences.h" #import +#import #if __has_include() #import #endif @@ -15,6 +16,24 @@ Preferences* global_pref = nil; static NSPanel* displayNamesPanel = nil; +/** + * Gets a stable identifier for a display using EDID data (vendor, model, serial). + * This identifier remains stable across reboots unlike CGDirectDisplayID. + * Note: We only use CoreGraphics APIs here to avoid WindowServer deadlocks + * that can occur when calling NSScreen APIs during display reconfiguration. + * @param displayId The CGDirectDisplayID of the display + * @return A stable identifier string + */ +NSString* getStableDisplayIdentifier(CGDirectDisplayID displayId){ + uint32_t vendorId = CGDisplayVendorNumber(displayId); + uint32_t modelId = CGDisplayModelNumber(displayId); + uint32_t serialNum = CGDisplaySerialNumber(displayId); + + // Use EDID data (vendor-model-serial) as stable identifier + // This is safe to call anytime and doesn't touch WindowServer/NSScreen + return [NSString stringWithFormat:@"%u-%u-%u", vendorId, modelId, serialNum]; +} // End of getStableDisplayIdentifier() + /** * Gets the custom display name for a given display ID. * @param displayId The CGDirectDisplayID of the display @@ -23,8 +42,17 @@ NSString* getCustomDisplayNameForId(CGDirectDisplayID displayId){ NSDictionary* customNames = (NSDictionary*)getPref(@"display_custom_names"); if(!customNames) return nil; - NSString* key = [NSString stringWithFormat:@"%u", displayId]; - return customNames[key]; + + // First try stable identifier + NSString* stableKey = getStableDisplayIdentifier(displayId); + if(stableKey){ + NSString* name = customNames[stableKey]; + if(name) return name; + } + + // Fallback to old format for backwards compatibility + NSString* legacyKey = [NSString stringWithFormat:@"%u", displayId]; + return customNames[legacyKey]; } // End of getCustomDisplayNameForId() /** @@ -39,7 +67,7 @@ // Find the screen and return its localized name for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; - CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; if(did == displayId){ if (@available(macOS 10.15, *)) return [screen localizedName]; return [NSString stringWithFormat:@"Display %u", displayId]; @@ -50,18 +78,30 @@ /** * Sets a custom display name for a given display ID. + * Uses stable identifier (EDID-based) to persist names across reboots. * @param displayId The CGDirectDisplayID of the display * @param name The custom name to set (empty string to clear) */ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ NSDictionary* existingNames = (NSDictionary*)[[NSUserDefaults standardUserDefaults] objectForKey:@"display_custom_names"]; NSMutableDictionary* customNames = existingNames ? [existingNames mutableCopy] : [[NSMutableDictionary alloc] init]; - NSString* key = [NSString stringWithFormat:@"%u", displayId]; + + // Use stable identifier instead of display ID + NSString* stableKey = getStableDisplayIdentifier(displayId); + if(!stableKey){ + stableKey = [NSString stringWithFormat:@"%u", displayId]; + } + + // Remove any legacy entry with just the display ID + NSString* legacyKey = [NSString stringWithFormat:@"%u", displayId]; + if(![stableKey isEqualToString:legacyKey]){ + [customNames removeObjectForKey:legacyKey]; + } if(name && name.length > 0){ - customNames[key] = name; + customNames[stableKey] = name; } else { - [customNames removeObjectForKey:key]; + [customNames removeObjectForKey:stableKey]; } [[NSUserDefaults standardUserDefaults] setObject:customNames forKey:@"display_custom_names"]; @@ -76,7 +116,7 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ [displays addObject:@{@"name": @"Ninguno", @"id": @-1}]; for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; - CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; NSString* name = getDisplayNameForId(did); [displays addObject:@{@"name": name, @"id": [NSNumber numberWithUnsignedInt:did]}]; } // End of loop through screens @@ -519,7 +559,7 @@ - (void)loadDisplayData{ _displayData = [[NSMutableArray alloc] init]; for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; - CGDirectDisplayID did = [dict[@"NSScreenNumber"] intValue]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; NSString* systemName = @"Display"; if (@available(macOS 10.15, *)) systemName = [screen localizedName]; NSString* customName = getCustomDisplayNameForId(did); From 192e3036260890b7d642f23fe7dc693a570cc12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Fri, 6 Feb 2026 08:05:06 +0000 Subject: [PATCH 06/19] Fix camera audio capture using AVCaptureAudioPreviewOutput Replace manual AudioUnit-based audio handling with Apple's built-in AVCaptureAudioPreviewOutput for camera audio monitoring. This fixes garbled audio issues caused by format conversion problems. Changes: - Remove complex ring buffer and AudioUnit render callback - Use AVCaptureAudioPreviewOutput for automatic format handling - Simplify audio toggle to volume control (0.0/1.0) - Remove unnecessary audio processing in capture delegate --- pip/window.m | 381 ++++++++++----------------------------------------- 1 file changed, 73 insertions(+), 308 deletions(-) diff --git a/pip/window.m b/pip/window.m index 4e88250..e31c230 100644 --- a/pip/window.m +++ b/pip/window.m @@ -800,17 +800,10 @@ @implementation Window{ AVCaptureDeviceFormat* camera_format; AVCaptureDevicePosition camera_position; - // Camera audio capture and playback - AVCaptureAudioDataOutput* camera_audio_output; - AudioUnit camera_audio_unit; + // Camera audio capture and playback using AVCaptureAudioPreviewOutput + AVCaptureAudioPreviewOutput* camera_audio_preview; bool camera_audio_enabled; bool camera_has_microphone; - - // Ring buffer for camera audio - uint8_t camera_audio_buffer[64 * 1024]; - UInt32 camera_audio_write_idx; - UInt32 camera_audio_read_idx; - AudioStreamBasicDescription camera_audio_format; #if __has_include() SCStream *window_stream API_AVAILABLE(macos(12.3)); SCStreamConfiguration *window_stream_config API_AVAILABLE(macos(12.3)); @@ -847,13 +840,9 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ camera_format = nil; // Initialize camera audio variables - camera_audio_output = nil; - camera_audio_unit = NULL; + camera_audio_preview = nil; camera_audio_enabled = true; // Enabled by default camera_has_microphone = false; - camera_audio_write_idx = 0; - camera_audio_read_idx = 0; - memset(&camera_audio_format, 0, sizeof(camera_audio_format)); #if __has_include() if (@available(macOS 12.3, *)) { window_stream = nil; @@ -2099,242 +2088,14 @@ -(void)stopWindowStream{ #pragma mark - Camera Audio Methods /** - * Checks microphone permission status and requests access if needed. - * @param completionHandler Called with YES if permission granted, NO otherwise - */ --(void)checkMicrophonePermission:(void(^)(BOOL granted))completionHandler { - AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; - - switch(authStatus) { - case AVAuthorizationStatusAuthorized: - completionHandler(YES); - break; - case AVAuthorizationStatusDenied: - case AVAuthorizationStatusRestricted: - NSLog(@"Microphone permission denied or restricted"); - completionHandler(NO); - break; - case AVAuthorizationStatusNotDetermined: - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(granted); - }); - }]; - break; - } -} // End of checkMicrophonePermission: - -/** - * AudioUnit render callback for camera audio playback. - * Reads PCM data from the ring buffer and outputs to speakers. - */ -static OSStatus CameraAudioRenderCallback(void *inRefCon, - AudioUnitRenderActionFlags *ioActionFlags, - const AudioTimeStamp *inTimeStamp, - UInt32 inBusNumber, - UInt32 inNumberFrames, - AudioBufferList *ioData) { - Window *window = (__bridge Window *)inRefCon; - - // Clear output buffers first - for(UInt32 i = 0; i < ioData->mNumberBuffers; ++i) { - memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize); - } - - // Check if we have data available - if(window->camera_audio_read_idx == window->camera_audio_write_idx) { - return noErr; // No data available - } - - UInt32 bufferLen = sizeof(window->camera_audio_buffer); - UInt32 dataAvailable; - if(window->camera_audio_write_idx > window->camera_audio_read_idx) { - dataAvailable = window->camera_audio_write_idx - window->camera_audio_read_idx; - } else { - dataAvailable = bufferLen - window->camera_audio_read_idx + window->camera_audio_write_idx; - } - - UInt32 bytesPerFrame = window->camera_audio_format.mBytesPerFrame; - if(bytesPerFrame == 0) bytesPerFrame = 4; // Default: 16-bit stereo - - UInt32 bytesRequested = bytesPerFrame * inNumberFrames; - UInt32 bytesToCopy = (bytesRequested < dataAvailable) ? bytesRequested : dataAvailable; - - // Copy data from ring buffer to output (may need to wrap) - uint8_t *outBuffer = (uint8_t *)ioData->mBuffers[0].mData; - UInt32 firstChunkSize = bufferLen - window->camera_audio_read_idx; - if(firstChunkSize > bytesToCopy) firstChunkSize = bytesToCopy; - - memcpy(outBuffer, window->camera_audio_buffer + window->camera_audio_read_idx, firstChunkSize); - - if(firstChunkSize < bytesToCopy) { - // Wrap around - memcpy(outBuffer + firstChunkSize, window->camera_audio_buffer, bytesToCopy - firstChunkSize); - window->camera_audio_read_idx = bytesToCopy - firstChunkSize; - } else { - window->camera_audio_read_idx += firstChunkSize; - if(window->camera_audio_read_idx >= bufferLen) { - window->camera_audio_read_idx = 0; - } - } - - return noErr; -} // End of CameraAudioRenderCallback() - -/** - * Sets up the AudioUnit for camera audio playback. - * @param format The audio stream format from the camera - */ --(void)setupCameraAudioUnit:(AudioStreamBasicDescription)format { - if(camera_audio_unit) { - [self teardownCameraAudioUnit]; - } - - camera_audio_format = format; - camera_audio_write_idx = 0; - camera_audio_read_idx = 0; - - AudioComponentDescription outputUnitDescription = { - .componentType = kAudioUnitType_Output, - .componentSubType = kAudioUnitSubType_DefaultOutput, - .componentManufacturer = kAudioUnitManufacturer_Apple - }; - - AudioComponent outputComponent = AudioComponentFindNext(NULL, &outputUnitDescription); - if(!outputComponent) { - NSLog(@"Failed to find audio output component"); - return; - } - - OSStatus status = AudioComponentInstanceNew(outputComponent, &camera_audio_unit); - if(status != noErr) { - NSLog(@"AudioComponentInstanceNew failed: %d", (int)status); - return; - } - - status = AudioUnitInitialize(camera_audio_unit); - if(status != noErr) { - NSLog(@"AudioUnitInitialize failed: %d", (int)status); - AudioComponentInstanceDispose(camera_audio_unit); - camera_audio_unit = NULL; - return; - } - - status = AudioUnitSetProperty(camera_audio_unit, - kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, - 0, - &format, - sizeof(format)); - if(status != noErr) { - NSLog(@"AudioUnitSetProperty StreamFormat failed: %d", (int)status); - } - - AURenderCallbackStruct callbackInfo = { - .inputProc = CameraAudioRenderCallback, - .inputProcRefCon = (__bridge void *)(self), - }; - - AudioUnitSetProperty(camera_audio_unit, - kAudioUnitProperty_SetRenderCallback, - kAudioUnitScope_Global, - 0, - &callbackInfo, - sizeof(callbackInfo)); - - AudioOutputUnitStart(camera_audio_unit); - NSLog(@"Camera audio unit started: sampleRate=%.0f, channels=%u, bitsPerChannel=%u", - format.mSampleRate, format.mChannelsPerFrame, format.mBitsPerChannel); -} // End of setupCameraAudioUnit: - -/** - * Tears down the camera audio unit. + * Stops camera capture and cleans up resources. */ --(void)teardownCameraAudioUnit { - if(camera_audio_unit) { - AudioOutputUnitStop(camera_audio_unit); - AudioUnitUninitialize(camera_audio_unit); - AudioComponentInstanceDispose(camera_audio_unit); - camera_audio_unit = NULL; - } - memset(&camera_audio_format, 0, sizeof(camera_audio_format)); -} // End of teardownCameraAudioUnit - -/** - * Processes audio sample buffer from camera and writes to ring buffer for playback. - * @param sampleBuffer The audio sample buffer from AVCaptureAudioDataOutput - */ --(void)processCameraAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer { - // Get audio format if not already set - CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); - if(!formatDesc) return; - - const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc); - if(!asbd) return; - - // Setup audio unit on first sample (when we know the format) - if(!camera_audio_unit) { - AudioStreamBasicDescription outputFormat = *asbd; - - // Ensure we have a valid PCM format for output - if(outputFormat.mFormatID != kAudioFormatLinearPCM) { - NSLog(@"Unsupported audio format: %u", (unsigned int)outputFormat.mFormatID); - return; - } - - [self setupCameraAudioUnit:outputFormat]; - } - - // Get audio data from sample buffer - CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); - if(!blockBuffer) return; - - size_t totalLength = 0; - char *dataPointer = NULL; - OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer); - if(status != noErr || !dataPointer || totalLength == 0) return; - - UInt32 bufferLen = sizeof(camera_audio_buffer); - - // Calculate space available in ring buffer - UInt32 spaceAvailable; - if(camera_audio_write_idx >= camera_audio_read_idx) { - spaceAvailable = bufferLen - camera_audio_write_idx + camera_audio_read_idx - 1; - } else { - spaceAvailable = camera_audio_read_idx - camera_audio_write_idx - 1; - } - - if(totalLength > spaceAvailable) { - // Buffer overflow - advance read pointer to make room (drop oldest data) - UInt32 overflow = (UInt32)totalLength - spaceAvailable; - camera_audio_read_idx = (camera_audio_read_idx + overflow) % bufferLen; - } - - // Copy data to ring buffer (may need to wrap) - UInt32 firstChunkSize = bufferLen - camera_audio_write_idx; - if(firstChunkSize > totalLength) firstChunkSize = (UInt32)totalLength; - - memcpy(camera_audio_buffer + camera_audio_write_idx, dataPointer, firstChunkSize); - - if(firstChunkSize < totalLength) { - // Wrap around - memcpy(camera_audio_buffer, dataPointer + firstChunkSize, totalLength - firstChunkSize); - camera_audio_write_idx = (UInt32)(totalLength - firstChunkSize); - } else { - camera_audio_write_idx += firstChunkSize; - if(camera_audio_write_idx >= bufferLen) { - camera_audio_write_idx = 0; - } - } -} // End of processCameraAudioSampleBuffer: - -(void)stopCameraCapture{ if(!camera_session) return; [camera_session stopRunning]; - // Clean up audio - [self teardownCameraAudioUnit]; - camera_audio_output = nil; + // Clean up audio preview + camera_audio_preview = nil; camera_has_microphone = false; camera_session = nil; @@ -2511,71 +2272,77 @@ -(void)startCameraCapture:(NSString*)deviceId{ // We'll handle un-mirroring in the capture output delegate if needed } - // Set up audio capture if enabled and camera has microphone + // Set up audio preview if enabled - uses AVCaptureAudioPreviewOutput for automatic format handling camera_has_microphone = false; - camera_audio_output = nil; + camera_audio_preview = nil; if(camera_audio_enabled) { - // Try to find an audio device associated with this camera - // Many cameras have built-in mics that appear as separate audio devices - AVCaptureDevice *audioDevice = nil; - NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; - for(AVCaptureDevice *audioD in audioDevices) { - // Match by name or manufacturer (webcam mics often share these) - if([audioD.localizedName containsString:device.localizedName] || - [audioD.manufacturer isEqualToString:device.manufacturer]) { - audioDevice = audioD; - break; + // Check microphone permission + AVAuthorizationStatus audioAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + + __block BOOL micPermissionGranted = (audioAuthStatus == AVAuthorizationStatusAuthorized); + + if(audioAuthStatus == AVAuthorizationStatusNotDetermined) { + // Request permission synchronously using a semaphore + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { + micPermissionGranted = granted; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + } + + if(micPermissionGranted) { + // Try to find an audio device associated with this camera + AVCaptureDevice *audioDevice = nil; + NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; + + for(AVCaptureDevice *audioD in audioDevices) { + if([audioD.localizedName containsString:device.localizedName] || + [audioD.manufacturer isEqualToString:device.manufacturer]) { + audioDevice = audioD; + NSLog(@"Found matching audio device: %@ for camera: %@", audioD.localizedName, device.localizedName); + break; + } } - } // End of loop through audio devices - - // If no matching audio device found, check if we have a default microphone - if(!audioDevice && audioDevices.count > 0) { - audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; - } - if(audioDevice) { - camera_has_microphone = true; - - [self checkMicrophonePermission:^(BOOL granted) { - if(!granted) { - NSLog(@"Microphone permission not granted, camera audio disabled"); - self->camera_has_microphone = false; - return; - } + // If no matching audio device found, use the default microphone + if(!audioDevice && audioDevices.count > 0) { + audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + NSLog(@"Using default audio device: %@", audioDevice.localizedName); + } - // Add audio input + if(audioDevice) { NSError *audioError = nil; AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&audioError]; + if(audioError || !audioInput) { NSLog(@"Failed to create audio input: %@", audioError); - self->camera_has_microphone = false; - return; - } - - if(![self->camera_session canAddInput:audioInput]) { + } else if(![session canAddInput:audioInput]) { NSLog(@"Cannot add audio input to camera session"); - self->camera_has_microphone = false; - return; - } - [self->camera_session addInput:audioInput]; + } else { + [session addInput:audioInput]; - // Add audio output - AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; - dispatch_queue_t audioQueue = dispatch_queue_create("com.pip.camera.audio", DISPATCH_QUEUE_SERIAL); - [audioOutput setSampleBufferDelegate:self queue:audioQueue]; + // Use AVCaptureAudioPreviewOutput for automatic audio playback + // This handles all format conversion automatically + AVCaptureAudioPreviewOutput *audioPreview = [[AVCaptureAudioPreviewOutput alloc] init]; + audioPreview.volume = 1.0; // Full volume + audioPreview.outputDeviceUniqueID = nil; // Use default output device - if(![self->camera_session canAddOutput:audioOutput]) { - NSLog(@"Cannot add audio output to camera session"); - return; + if(![session canAddOutput:audioPreview]) { + NSLog(@"Cannot add audio preview output to camera session"); + } else { + [session addOutput:audioPreview]; + camera_audio_preview = audioPreview; + camera_has_microphone = true; + NSLog(@"Camera audio preview configured for device: %@", audioDevice.localizedName); + } } - [self->camera_session addOutput:audioOutput]; - self->camera_audio_output = audioOutput; - - NSLog(@"Camera audio capture enabled for device: %@", audioDevice.localizedName); - }]; + } + } else { + NSLog(@"Microphone permission not granted, camera audio disabled"); } - } // End of audio capture setup + } // End of audio configuration camera_session = session; camera_input = input; @@ -2588,6 +2355,7 @@ -(void)startCameraCapture:(NSString*)deviceId{ camera_format = device.activeFormat; } + // Start session after all inputs/outputs are configured [session startRunning]; is_playing = true; @@ -2597,14 +2365,11 @@ -(void)startCameraCapture:(NSString*)deviceId{ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if(!is_playing || isWinClosing || !camera_session) return; - // Check if this is audio or video output - if(output == camera_audio_output && camera_audio_enabled) { - // Audio processing - [self processCameraAudioSampleBuffer:sampleBuffer]; + // Only handle video output - audio is handled automatically by AVCaptureAudioPreviewOutput + if(![output isKindOfClass:[AVCaptureVideoDataOutput class]]) { return; } - // Video processing CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); if(!imageBuffer) return; @@ -3354,14 +3119,14 @@ -(void)toggleCameraAudio:(id)sender { NSString *currentCameraId = [camera_id copy]; [self startCameraCapture:currentCameraId]; } else if(!camera_audio_enabled) { - // Stop audio playback - [self teardownCameraAudioUnit]; - // Remove audio output from session if present - if(camera_audio_output && camera_session) { - [camera_session beginConfiguration]; - [camera_session removeOutput:camera_audio_output]; - [camera_session commitConfiguration]; - camera_audio_output = nil; + // Mute audio by setting volume to 0, or remove output + if(camera_audio_preview) { + camera_audio_preview.volume = 0.0; + } + } else { + // Unmute audio + if(camera_audio_preview) { + camera_audio_preview.volume = 1.0; } } From 73e288fd62ae3b89bfb300ce7276fd7868966c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Fri, 6 Feb 2026 08:05:32 +0000 Subject: [PATCH 07/19] Add Shift+Tab navigation in preferences text fields --- pip/preferences.m | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pip/preferences.m b/pip/preferences.m index 603fde4..6251f0b 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -681,6 +681,38 @@ - (void)controlTextDidEndEditing:(NSNotification *)notification{ } } // End of controlTextDidEndEditing() +/** + * Handles special key commands in text fields, including Shift+Tab for backward navigation. + * @param control The control sending the command + * @param textView The field editor + * @param commandSelector The command selector + * @return YES if the command was handled, NO otherwise + */ +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector{ + if(commandSelector == @selector(insertBacktab:)){ + // Shift+Tab pressed - move to previous field + NSTextField* currentField = (NSTextField*)control; + NSInteger currentRow = currentField.tag; + + // Find previous text field + for(NSInteger i = currentRow - 1; i >= 0; i--){ + if(i < (NSInteger)_textFields.count && _textFields[i] != [NSNull null]){ + NSTextField* prevField = _textFields[i]; + [self makeFirstResponder:prevField]; + return YES; + } + } // End of loop searching for previous field + + // If at first field, wrap to OK button + if(_okButton){ + [self makeFirstResponder:_okButton]; + return YES; + } + return YES; + } + return NO; +} // End of control:textView:doCommandBySelector: + - (nullable NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row{ NSTableRowView* rowView = [[NSTableRowView alloc] init]; rowView.emphasized = false; From 5b3bea2a593f50e19528b01235f97c3d2f9b1541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Fri, 6 Feb 2026 08:09:41 +0000 Subject: [PATCH 08/19] Close app when window is closed with red button --- pip/window.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pip/window.m b/pip/window.m index e31c230..5867fa2 100644 --- a/pip/window.m +++ b/pip/window.m @@ -3389,7 +3389,8 @@ - (void)dismissHLSInputView { } - (void)windowWillClose:(NSNotification *)notification{ -// NSLog(@"windowWillClose"); + // Terminate the app when the window is closed + [[NSApplication sharedApplication] terminate:nil]; } - (void)windowDidBecomeKey:(NSNotification *)notification{ From b4ac695caf8783c6c70225f9111e02cf6fcb31bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garci=CC=81a?= Date: Fri, 6 Feb 2026 19:07:14 +0000 Subject: [PATCH 09/19] Add multi-window support (Phase 1 + Phase 2) - Multiple PiP windows with independent sources via Cmd+N - Shared MTLDevice across all windows to reduce VRAM usage - Window management: grid layout (Cmd+G), cascade, close all (Opt+Cmd+W) - Clone window (Shift+Cmd+N) with camera exclusivity warning - Window manager panel (Opt+Cmd+M) showing source name, type, status - Blank window overlay hint for source selection - Disconnect/error handling for displays, cameras, and captured windows - New preferences: max windows limit, new window behavior (blank/clone) - Performance warning when 6+ windows open - Fix NSPanel not counted by applicationShouldTerminateAfterLastWindowClosed - Manual last-window termination check in windowWillClose - Prevent panel auto-hide during permission dialogs (hidesOnDeactivate=NO) --- MULTI-WINDOW-PLAN.md | 322 +++++++++++++++++++++++++++ pip/imageRenderer.h | 3 + pip/main.m | 513 ++++++++++++++++++++++++++++++++++++++++++- pip/metalRenderer.m | 2 +- pip/preferences.m | 24 +- pip/window.h | 3 + pip/window.m | 231 ++++++++++++++++++- 7 files changed, 1085 insertions(+), 13 deletions(-) create mode 100644 MULTI-WINDOW-PLAN.md diff --git a/MULTI-WINDOW-PLAN.md b/MULTI-WINDOW-PLAN.md new file mode 100644 index 0000000..4253db6 --- /dev/null +++ b/MULTI-WINDOW-PLAN.md @@ -0,0 +1,322 @@ +# Multi-Window PiP: Implementation Plan + +## Overview + +This document outlines the plan to enhance the PiP application's multi-window experience, allowing users to have multiple floating windows with different input sources (monitors, windows, cameras, HLS streams) simultaneously. + +**Current state:** The app already supports creating multiple windows via Cmd+N. Each `Window` (NSPanel subclass) independently manages its own capture session, renderer, and source selection. However, there is no window management UI, no guidance for new windows, no layout tools, and GPU resources are duplicated per window. + +**Goal:** Make multi-window a first-class, polished experience rather than just a side effect of "you can press Cmd+N again." + +--- + +## Technical Feasibility Analysis + +### Concurrent Capture Sessions + +| Source Type | API | Concurrent Limit | Notes | +|---|---|---|---| +| Display | CGDisplayStream | No hard limit | Bound by GPU bandwidth; practical limit ~4-6 | +| Window | SCStream (ScreenCaptureKit) | No hard limit | Queue depth should stay ≤ 8 per stream to avoid memory pressure | +| Camera | AVCaptureSession | **One session per device** | Same camera cannot be opened by two sessions; must fan out frames | +| HLS | AVPlayer | No hard limit | Network bandwidth is the bottleneck | +| AirPlay | Custom RAOP server | 10 sessions (current soft limit) | Already supports multi-session | + +**Key constraint:** A camera device is exclusive to one `AVCaptureSession`. If two windows want the same camera, a source-sharing layer is needed (deferred to Phase 3). + +### GPU Resources + +- `MTLDevice` is thread-safe and should be created once and shared across all windows. +- Each window can safely create its own `MTLCommandQueue` from the shared device. +- Pipeline states and shader libraries should be cached and reused. +- This reduces VRAM usage and avoids redundant device initialization. + +### Realistic Performance Bounds + +- **Target use case:** 2-4 simultaneous windows (typical user). +- **Soft limit:** 6 windows (show performance warning). +- **Hard limit:** 8 windows (configurable in preferences). +- This is a lightweight PiP tool, not OBS. Optimize for the common case. + +--- + +## UX Design Decisions + +### Window Creation Flow + +**Decision:** New windows open blank with an overlay hint. No modal picker. + +- Cmd+N creates a new window. +- If preference is "Clone current": new window copies the frontmost window's source. +- If preference is "Blank with hint" (default): new window shows a centered overlay message: _"Clic derecho para seleccionar fuente"_ (Right-click to select source). +- The overlay is a simple `NSTextField` on a semi-transparent `NSVisualEffectView`, centered on the window. +- The overlay disappears when a source is selected. + +### Window Identification + +- Each window's `title` is already set to the source name (e.g., "Display 1", "Safari - Google", "FaceTime HD Camera"). +- AppKit will use these titles in the Window menu automatically. +- When hovering over a borderless window, the title bar appears briefly (existing behavior), showing the source name. +- No color-coded borders for Phase 1 (adds visual noise). + +### Source Selection + +- **Keep right-click context menu per window.** It's simple, already works, and aligns with user mental model. +- No changes to the source selection mechanism in Phase 1. + +### Closing Behavior + +- Closing a window closes only that window (existing behavior). +- "Close All Windows" quits the app (matches Preview.app behavior). +- No confirmation dialog (not a destructive app). +- Menu bar persistence is a future consideration. + +### Layout Management + +- **Arrange in Grid:** Distributes all open PiP windows in a grid on the current screen. +- **Cascade:** Offsets windows diagonally from top-left. +- Both use `screen.visibleFrame` to respect menu bar and dock. +- Grid preserves each window's aspect ratio (scale to fit within cell, center in cell). + +### Preferences + +Two new preferences for Phase 1: +1. **New window behavior:** "Blank with hint" (default) / "Clone current window" +2. **Maximum simultaneous windows:** Numeric, default 8 + +--- + +## Implementation Phases + +### Phase 1: Foundation + +Minimum viable multi-window enhancement. Low risk, high impact. + +#### 1.1 Automatic Window List in Window Menu + +**Files:** `main.m` + +- Call `[NSApp setWindowsMenu:windowMenu]` after creating the Window menu. +- AppKit will automatically list all open `Window` instances by their `title`. +- Ensure each window has a meaningful title (already the case: source name is set in `-changeWindow:`). +- Add custom items above the auto-managed list: "Close All Windows", "Arrange in Grid", "Cascade". + +#### 1.2 Close All Windows + +**Files:** `main.m` + +- Add menu item "Cerrar todas las ventanas" (Close All Windows) with shortcut Opt+Cmd+W. +- Action: iterate `[NSApp windows]`, close each `Window` instance. +- App terminates when last window closes (existing behavior via `applicationShouldTerminateAfterLastWindowClosed:`). + +#### 1.3 Overlay Hint for Blank Windows + +**Files:** `window.m` + +- When a new window is created (non-AirPlay) and no source is selected: + - Add a `NSVisualEffectView` overlay (blending mode: behind window, material: dark) centered on the window. + - Inside it, a centered `NSTextField` with text: _"Clic derecho para seleccionar fuente"_. + - Style: white text, medium system font, non-editable, non-selectable. +- When `-changeWindow:` is called and a source is selected, remove (hide) the overlay. +- The overlay should resize with the window (auto-resizing mask or constraints). + +#### 1.4 Arrange in Grid + +**Files:** `main.m` or a new utility function in `window.m` + +Algorithm: +1. Collect all open `Window` instances from `[NSApp windows]`. +2. Get `visibleFrame` of the screen where the frontmost window resides. +3. Calculate grid: `cols = ceil(sqrt(N))`, `rows = ceil(N / cols)`. +4. Cell size: `cellW = visibleFrame.width / cols`, `cellH = visibleFrame.height / rows`. +5. For each window: + - Calculate target size: scale to fit within cell while preserving aspect ratio. + - Clamp to window's `minSize`. + - Center within the cell. + - Animate with `setFrame:display:animate:`. + +Edge cases: +- Single window: center it on screen, don't resize. +- Respect each window's minimum size. + +#### 1.5 Cascade + +**Files:** `main.m` or utility function + +Algorithm: +1. Start at top-left of `visibleFrame`. +2. Each window offset by (25, -25) from the previous. +3. Don't resize windows. +4. Wrap back to top-left if cascade goes off-screen. + +#### 1.6 Shared MTLDevice + +**Files:** `main.m` (or app delegate), `window.m`, `metalRenderer.m` + +- Create `MTLCreateSystemDefaultDevice()` once in the app delegate and store it. +- Pass the shared device to each `Window` at creation time. +- `Window` passes it to `MetalRenderer` init. +- `MetalRenderer` uses the shared device but creates its own `MTLCommandQueue`. +- The device is retained by the app delegate for the app's lifetime. + +#### 1.7 New Preferences + +**Files:** `preferences.m`, `preferences.h` + +Add two new rows to the preferences table: +1. **"Comportamiento de nueva ventana"** (New window behavior): Popup with options "En blanco con pista" / "Clonar ventana actual". +2. **"Máximo de ventanas simultáneas"** (Max simultaneous windows): Popup with options 2, 4, 6, 8, 10. + +#### 1.8 Max Windows Enforcement + +**Files:** `main.m` (in the Cmd+N handler) + +- Before creating a new window, check count of open `Window` instances. +- If at limit: + - First time: show `NSAlert` with message _"Se alcanzó el máximo de ventanas. Puedes cambiar el límite en Preferencias."_ and "OK" + "Preferencias..." buttons. + - Subsequent times: just `NSBeep()`. +- Track "has shown alert" with a simple static boolean. + +#### 1.9 Keyboard Shortcut to Cycle Windows + +**Files:** `main.m` + +- Add Cmd+` (backtick) to cycle through open PiP windows (this is standard macOS behavior and may already work via AppKit if the Window menu is properly configured). +- Verify it works; if not, add a manual implementation that calls `[NSApp windows]` and `makeKeyAndOrderFront:` on the next window. + +--- + +### Phase 2: Management + +Better visibility and control over multiple windows. + +#### 2.1 Window Manager Panel + +- New `NSPanel` with an `NSTableView`. +- Columns: source icon + name, type (Display/Window/Camera/HLS), status (Active/Paused/Disconnected). +- Click a row to focus that window. +- Context menu on row: Close, Reassign Source, Clone. +- Opened via Window menu item "Administrador de ventanas" or keyboard shortcut. + +#### 2.2 Clone Current Window + +- Menu item "Clonar ventana" (Clone Window) with shortcut Shift+Cmd+N. +- Creates a new window and copies the frontmost window's source (`WindowSel`). +- Calls `-changeWindow:` on the new window with the same selection. +- For cameras: warn that the same camera can't be opened twice; offer to pick a different source. + +#### 2.3 Disconnect / Error Handling + +Uniform handling across all windows: +- **Display disconnected:** Stop stream, show overlay _"Pantalla desconectada"_ with option to pick a new source. +- **Camera unplugged:** Stop session, show overlay _"Cámara no disponible"_. +- **Captured window closed:** Stop stream, show overlay _"Ventana cerrada"_. +- All overlays include a "Select new source" button. + +#### 2.4 Performance Warnings + +- When opening the 6th+ window, show a brief non-modal notification: _"Muchas ventanas abiertas. El rendimiento puede verse afectado."_ +- Optional: show FPS indicator per window (toggle in preferences). + +--- + +### Phase 3: Power Features + +Architectural changes for advanced users. + +#### 3.1 CaptureSourceController Abstraction + +- Create per-source-type controllers: `DisplaySourceController`, `WindowSourceController`, `CameraSourceController`, `HLSSourceController`. +- Each has `-start`, `-stop`, and a delegate callback delivering native frame data. +- Window becomes a consumer that receives frames from a controller. +- This decouples capture logic from window management. + +#### 3.2 Source Sharing + +- When multiple windows select the same source, they attach as consumers to one `CaptureSourceController`. +- Especially important for cameras (AVCaptureSession exclusivity). +- One capture session fans out frames to multiple renderers. +- Source-specific controls (resolution, audio) live on the controller; any window can present them but changes affect all attached windows. + +#### 3.3 Adaptive Throttling + +- Monitor total capture load (aggregate FPS, frame times). +- When load exceeds threshold, automatically reduce FPS or resolution on lower-priority windows. +- Priority can be based on: frontmost > visible > minimized. +- ScreenCaptureKit `queueDepth` kept ≤ 8 per stream. + +#### 3.4 Per-Window Quality Settings + +- Move renderer type, FPS cap, resolution, and crop settings to per-window preferences. +- Access via right-click menu > "Window Settings" submenu. +- Global preferences become defaults for new windows. + +#### 3.5 Session Persistence / Restore + +- On quit, save the list of open windows with their source identifiers, positions, and sizes to `NSUserDefaults`. +- On launch, attempt to restore the session: + - For displays: match by EDID identifier (already used for custom names). + - For windows: match by app name + window title (best effort). + - For cameras: match by `uniqueID`. + - For HLS: match by URL. +- If a source is unavailable, open the window blank with a "Source unavailable" overlay. + +--- + +## Summary + +| Phase | Effort | Impact | Risk | +|---|---|---|---| +| Phase 1 | Low-Medium | High (usable multi-window) | Low | +| Phase 2 | Medium | Medium (better management) | Low | +| Phase 3 | High | Medium (power users) | Medium (architectural changes) | + +Phase 1 can be implemented without major refactoring — it builds on the existing architecture. Phases 2 and 3 introduce new abstractions but can be done incrementally. + +The key insight is that the app already supports multiple independent windows. The work is primarily about **UX polish** (guiding users, managing windows, arranging layouts) and **resource optimization** (shared GPU device, performance limits), not about fundamental architectural changes. + +--- + +## Phase 1 Implementation Status + +**All Phase 1 items implemented.** Files modified: + +| File | Changes | +|---|---| +| `main.m` | Shared MTLDevice (dispatch_once), allPipWindows/visiblePipWindows helpers, closeAllWindows (closes prefs first), arrangeInGrid, arrangeInCascade, max windows enforcement with alert, clone window behavior via preference, Window menu auto-population via setWindowsMenu, applicationShouldTerminateAfterLastWindowClosed→YES | +| `window.m` | PassthroughView class (hitTest→nil), sourceHintOverlay ivar + setup in init (PassthroughView with centered label), hide/show in changeWindow, cleanup in close, cloneSourceToWindow: method | +| `window.h` | Added cloneSourceToWindow: declaration | +| `metalRenderer.m` | Uses getSharedMTLDevice() instead of MTLCreateSystemDefaultDevice() | +| `imageRenderer.h` | Added Metal import and getSharedMTLDevice() declaration | +| `preferences.m` | Two new prefs (new_window_behavior, max_windows), panel height 230→290, auto-quit when prefs close and no PiP windows remain (dispatch_async for safety) | + +### Issues found during Codex review and fixed: +1. Overlay blocked right-click events → used PassthroughView with hitTest:→nil +2. pipWindows excluded minimized windows → split into allPipWindows/visiblePipWindows +3. Grid/cascade used keyWindow's screen (could be Preferences) → use first PiP window's screen +4. Overlay was destroyed instead of hidden → changed to hide/show so it reappears +5. getSharedMTLDevice wasn't thread-safe → added dispatch_once +6. closeAllWindows didn't close preferences panel → added explicit close +7. Prefs close could leave app running with no windows → added auto-quit check +8. new_window_behavior pref was unused → implemented clone via cloneSourceToWindow: + +### Note on 1.9 (Cmd+` cycling): +Standard macOS Cmd+` window cycling should work automatically now that the Window menu is properly configured with `setWindowsMenu:`. AppKit handles this natively for all windows registered in the windows menu. + +--- + +## Phase 2 Implementation Status + +**All Phase 2 items implemented.** Files modified: + +| File | Changes | +|---|---| +| `main.m` | "Clonar ventana" menu item (Shift+Cmd+N) with cloneCurrentWindow method, performance warning on 6th+ window (shown once), WindowManagerPanel class (NSPanel with 3-column NSTableView: source name, type, status), "Administrador de ventanas" menu item (Opt+Cmd+M), auto-refresh timer with weakSelf pattern | +| `window.m` | Promoted hintLabel to ivar for dynamic text, showDisconnectOverlay: method (stops captures, shows overlay with disconnect message), handleDisplayDisconnected: for display removal, cameraSessionError: for camera unplug/error, CGDisplayReconfigurationCallback registration/unregistration, AVCaptureSession/Device notification observers in startCameraCapture/stopCameraCapture, SCStream didStopWithError: now shows disconnect overlay, CGDisplayStream callback handles kCGDisplayStreamFrameStatusStopped, sourceType/sourceStatus public methods, cloneSourceToWindow: returns BOOL, forward declaration category for C callback | +| `window.h` | Added sourceType, sourceStatus declarations, changed cloneSourceToWindow: return type to BOOL | + +### Issues found during Codex review and fixed: +1. WindowManagerPanel refresh timer created retain cycle → used weakSelf pattern in block +2. CGDisplayReconfigurationCallback uses __bridge (no retain) → safe because close always unregisters before dealloc (NSWindow lifecycle guarantees close before dealloc) +3. Camera notification handler reads camera_session on arbitrary thread → benign pointer check, all state mutation dispatched to main queue +4. showDisconnectOverlay: verified as idempotent — safe to call from multiple error paths simultaneously diff --git a/pip/imageRenderer.h b/pip/imageRenderer.h index d789eff..c0138c0 100644 --- a/pip/imageRenderer.h +++ b/pip/imageRenderer.h @@ -10,6 +10,9 @@ #define imageRenderer_h #import +#import + +id getSharedMTLDevice(void); @protocol ImageRendererDelegate - (void)onResize:(CGSize)size andAspectRatio:(CGSize) ar; diff --git a/pip/main.m b/pip/main.m index ead0ab2..accaf3b 100644 --- a/pip/main.m +++ b/pip/main.m @@ -9,12 +9,11 @@ #import "window.h" #import "preferences.h" #import +#import #ifndef NO_AIRPLAY #import "airplaySender.h" #endif -extern int windowCount; - #define ADD_SEP() [menu addItem:[NSMenuItem separatorItem]] #define INIT_MENU(title) {menu = [[NSMenu alloc] initWithTitle:title]; NSMenuItem* item = [[NSMenuItem alloc] init];[item setSubmenu:menu];[menubar addItem:item];} #define ADD_ITEM(title, sel, key) [menu addItem:[[NSMenuItem alloc] initWithTitle:title action:@selector(sel) keyEquivalent:key]] @@ -31,10 +30,40 @@ #define ADD_SCALE_ITEM(scale) [self addScaleMenuItemWithTitle:@"Scale " STRINGIFY(scale) keyEquivalent:@ STRINGIFY(scale) mask:NO andScale:100 * scale toMenu:menu]; #define ADD_SCALE_ITEM_INVERSE(scale) [self addScaleMenuItemWithTitle:@"Scale 1/" STRINGIFY(scale) keyEquivalent:@ STRINGIFY(scale) mask:YES andScale:100 / scale toMenu:menu]; +/** + * Returns the shared Metal device for the application. + * Creates it on first access using dispatch_once for thread safety. + * All windows should use this device instead of creating their own. + * @return The shared MTLDevice instance + */ +id getSharedMTLDevice(void){ + static id sharedMTLDevice = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedMTLDevice = MTLCreateSystemDefaultDevice(); + }); + return sharedMTLDevice; +} // End of getSharedMTLDevice() + +@class MyApplicationDelegate; + +@interface WindowManagerPanel : NSPanel +@property (nonatomic, strong) NSTableView* tableView; +@property (nonatomic, strong) NSMutableArray* windowList; +@property (nonatomic, weak) MyApplicationDelegate* appDelegate; +@property (nonatomic, strong) NSTimer* refreshTimer; +- (id)initWithAppDelegate:(MyApplicationDelegate*)delegate; +- (void)refreshWindowList; +@end + +static WindowManagerPanel* windowManagerPanel = nil; + @interface MyApplicationDelegate : NSObject { NSApplication* app; NSMenuItem* windowMenuItem; boolean_t clickThroughState; + bool maxWindowsAlertShown; + bool performanceWarningShown; } @end @@ -58,6 +87,7 @@ -(id)initWithApp:(NSApplication*) application{ INIT_MENU(@"File"); ADD_ITEM(@"New", newWindow, @"n"); + ADD_ITEM_MASK(@"Clonar ventana", cloneCurrentWindow, @"n", NSEventModifierFlagCommand | NSEventModifierFlagShift); ADD_ITEM(@"Stream HLS", loadHLSStream:, @"l"); ADD_ITEM(@"Click Through", clickThrough:, @"c"); ADD_ITEM(@"Close", performClose:, @"w"); @@ -89,8 +119,16 @@ -(id)initWithApp:(NSApplication*) application{ ADD_ITEM(@"Join all spaces", togglePin, @"j"); ADD_ITEM(@"Bring All to Front", arrangeInFront:, @""); ADD_ITEM(@"Toggle Native PiP", toggleNativePip, @"p"); + ADD_SEP(); + ADD_ITEM(@"Organizar en cuadr\u00edcula", arrangeInGrid, @"g"); + ADD_ITEM(@"Cascada", arrangeInCascade, @""); + ADD_SEP(); + ADD_ITEM_MASK(@"Cerrar todas las ventanas", closeAllWindows, @"w", NSEventModifierFlagCommand | NSEventModifierFlagOption); + ADD_SEP(); + ADD_ITEM_MASK(@"Administrador de ventanas", showWindowManager, @"m", NSEventModifierFlagCommand | NSEventModifierFlagOption); [app setMainMenu:menubar]; + [app setWindowsMenu:menu]; [app setDelegate:self]; return self; @@ -120,12 +158,124 @@ - (void) getActiveWindow: (void (^)(Window* window))cb{ if(currentWindow) cb((Window*)currentWindow); } +/** + * Creates a new PiP window, respecting the max windows preference. + * If "clone current" behavior is selected, copies the source from the frontmost window. + * @return The new Window, or nil if max windows limit was reached + */ - (NSWindow*) newWindow{ + // Check max windows limit + // max_windows preference index: 0=2, 1=4, 2=6, 3=8, 4=10 + NSInteger maxIdx = [(NSNumber*)getPref(@"max_windows") intValue]; + NSInteger maxWindows = (maxIdx + 1) * 2; + NSArray* currentWindows = [self allPipWindows]; + if((NSInteger)currentWindows.count >= maxWindows){ + if(!maxWindowsAlertShown){ + maxWindowsAlertShown = true; + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"L\u00edmite de ventanas alcanzado"]; + [alert setInformativeText:[NSString stringWithFormat:@"Se alcanz\u00f3 el m\u00e1ximo de %ld ventanas. Puedes cambiar el l\u00edmite en Preferencias.", (long)maxWindows]]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Preferencias..."]; + NSModalResponse response = [alert runModal]; + if(response == NSAlertSecondButtonReturn){ + [self showPreferencePanel:self]; + } + } else { + NSBeep(); + } + return nil; + } + + // Show performance warning once when opening the 6th+ window + if(!performanceWarningShown && (NSInteger)currentWindows.count >= 5){ + performanceWarningShown = true; + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Advertencia de rendimiento"]; + [alert setInformativeText:@"Muchas ventanas abiertas. El rendimiento puede verse afectado."]; + [alert addButtonWithTitle:@"OK"]; + [alert setAlertStyle:NSAlertStyleInformational]; + [alert runModal]; + } + NSWindow* window = [[Window alloc] initWithAirplay: false andTitle:nil]; [window makeKeyAndOrderFront:self]; [window setIgnoresMouseEvents:clickThroughState]; + + // If "clone current" behavior is selected, copy the source from the previous key window + NSInteger newWindowBehavior = [(NSNumber*)getPref(@"new_window_behavior") intValue]; + if(newWindowBehavior == 1 && currentWindows.count > 0){ + // Find the frontmost PiP window (the one that was key before this new one) + Window* sourceWindow = nil; + for(Window* w in currentWindows){ + if([w isVisible]){ + sourceWindow = w; + break; + } + } // End of loop to find source window for cloning + if(sourceWindow){ + [sourceWindow cloneSourceToWindow:(Window*)window]; + } + } + return window; -} +} // End of newWindow + +/** + * Clones the frontmost PiP window's source into a new window. + * Shows an alert if the source is a camera (can't be shared). + */ +- (void) cloneCurrentWindow{ + // Check max windows limit + NSInteger maxIdx = [(NSNumber*)getPref(@"max_windows") intValue]; + NSInteger maxWindows = (maxIdx + 1) * 2; + NSArray* currentWindows = [self allPipWindows]; + if((NSInteger)currentWindows.count >= maxWindows){ + if(!maxWindowsAlertShown){ + maxWindowsAlertShown = true; + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"L\u00edmite de ventanas alcanzado"]; + [alert setInformativeText:[NSString stringWithFormat:@"Se alcanz\u00f3 el m\u00e1ximo de %ld ventanas. Puedes cambiar el l\u00edmite en Preferencias.", (long)maxWindows]]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Preferencias..."]; + NSModalResponse response = [alert runModal]; + if(response == NSAlertSecondButtonReturn){ + [self showPreferencePanel:self]; + } + } else { + NSBeep(); + } + return; + } + + // Find the frontmost PiP window to clone from + __block Window* sourceWindow = nil; + [self getActiveWindow:^(Window* window){ + sourceWindow = window; + }]; + + if(!sourceWindow){ + [self newWindow]; + return; + } + + // Create new window and attempt clone + NSWindow* newWin = [[Window alloc] initWithAirplay:false andTitle:nil]; + [newWin makeKeyAndOrderFront:self]; + [newWin setIgnoresMouseEvents:clickThroughState]; + + BOOL cloned = [sourceWindow cloneSourceToWindow:(Window*)newWin]; + if(!cloned){ + // If clone failed (camera or no source), show alert for camera + if([[sourceWindow sourceType] isEqualToString:@"C\u00e1mara"]){ + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"No se puede clonar"]; + [alert setInformativeText:@"Las fuentes de c\u00e1mara no se pueden compartir entre ventanas. Selecciona otra fuente con clic derecho."]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + } + } +} // End of cloneCurrentWindow - (void) hideAll{ [app hide:self]; @@ -140,6 +290,157 @@ -(void) clickThrough:(id)sender{ } } +/** + * Returns all PiP Window instances, including minimized/hidden ones. + * Used for counting (max windows) and closing all. + * @return Array of Window objects + */ +- (NSArray*) allPipWindows{ + NSMutableArray* windows = [[NSMutableArray alloc] init]; + for(NSWindow* window in [app windows]){ + if([window isKindOfClass:[Window class]]) [windows addObject:(Window*)window]; + } + return windows; +} // End of allPipWindows + +/** + * Returns visible PiP Window instances only. + * Used for layout operations (grid, cascade). + * @return Array of visible Window objects + */ +- (NSArray*) visiblePipWindows{ + NSMutableArray* windows = [[NSMutableArray alloc] init]; + for(NSWindow* window in [app windows]){ + if([window isKindOfClass:[Window class]] && [window isVisible]) [windows addObject:(Window*)window]; + } + return windows; +} // End of visiblePipWindows + +/** + * Closes all open PiP windows. The app will quit since applicationShouldTerminateAfterLastWindowClosed returns YES. + */ +- (void) closeAllWindows{ + // Close preferences panel first so it doesn't keep the app alive + if(global_pref){ + [global_pref close]; + } + for(Window* window in [self allPipWindows]){ + [window performClose:self]; + } +} // End of closeAllWindows + +/** + * Arranges all open PiP windows in a grid layout on the current screen. + * Uses the screen's visible frame to avoid menu bar and dock. + * Preserves each window's aspect ratio. + */ +- (void) arrangeInGrid{ + NSArray* windows = [self visiblePipWindows]; + NSInteger count = windows.count; + if(count == 0) return; + + // Get visible frame from the first PiP window's screen (not keyWindow, which might be Preferences) + NSScreen* screen = [windows[0] screen]; + if(!screen) screen = [NSScreen mainScreen]; + NSRect visibleFrame = [screen visibleFrame]; + + if(count == 1){ + // Single window: center without resizing + Window* win = windows[0]; + NSRect winFrame = [win frame]; + NSPoint center = NSMakePoint( + visibleFrame.origin.x + (visibleFrame.size.width - winFrame.size.width) / 2, + visibleFrame.origin.y + (visibleFrame.size.height - winFrame.size.height) / 2 + ); + [win setFrameOrigin:center]; + return; + } + + // Calculate grid dimensions + NSInteger cols = (NSInteger)ceil(sqrt((double)count)); + NSInteger rows = (NSInteger)ceil((double)count / cols); + + CGFloat cellW = visibleFrame.size.width / cols; + CGFloat cellH = visibleFrame.size.height / rows; + + for(NSInteger i = 0; i < count; i++){ + Window* win = windows[i]; + NSInteger row = i / cols; + NSInteger col = i % cols; + + // Flip row so first window is top-left (macOS has origin at bottom-left) + NSInteger flippedRow = rows - 1 - row; + + // Scale window to fit within cell while preserving aspect ratio + NSSize winSize = [win frame].size; + CGFloat aspect = winSize.width / winSize.height; + CGFloat targetW = cellW - 4; // 2px margin on each side + CGFloat targetH = cellH - 4; + + if(targetW / targetH > aspect){ + targetW = targetH * aspect; + } else { + targetH = targetW / aspect; + } + + // Enforce minimum size + NSSize minSize = [win minSize]; + if(targetW < minSize.width) targetW = minSize.width; + if(targetH < minSize.height) targetH = minSize.height; + + // Center within cell + CGFloat x = visibleFrame.origin.x + col * cellW + (cellW - targetW) / 2; + CGFloat y = visibleFrame.origin.y + flippedRow * cellH + (cellH - targetH) / 2; + + [win setFrame:NSMakeRect(x, y, targetW, targetH) display:YES animate:YES]; + } // End of loop through windows for grid layout +} // End of arrangeInGrid + +/** + * Arranges all open PiP windows in a cascade layout, offset diagonally. + * Wraps back to start if cascade goes off-screen. + */ +- (void) arrangeInCascade{ + NSArray* windows = [self visiblePipWindows]; + NSInteger count = windows.count; + if(count == 0) return; + + // Use the first PiP window's screen (not keyWindow, which might be Preferences) + NSScreen* screen = [windows[0] screen]; + if(!screen) screen = [NSScreen mainScreen]; + NSRect visibleFrame = [screen visibleFrame]; + + CGFloat offsetX = 25; + CGFloat offsetY = 25; + CGFloat startX = visibleFrame.origin.x + 10; + CGFloat startY = visibleFrame.origin.y + visibleFrame.size.height; + CGFloat curX = startX; + CGFloat curY = startY; + + for(NSInteger i = 0; i < count; i++){ + Window* win = windows[i]; + NSSize winSize = [win frame].size; + + // Position window (top-left corner, adjusting for macOS bottom-left origin) + CGFloat x = curX; + CGFloat y = curY - winSize.height; + + // Wrap if window goes off-screen + if(x + winSize.width > visibleFrame.origin.x + visibleFrame.size.width || + y < visibleFrame.origin.y){ + curX = startX; + curY = startY; + x = curX; + y = curY - winSize.height; + } + + [win setFrameOrigin:NSMakePoint(x, y)]; + + curX += offsetX; + curY -= offsetY; + } // End of loop through windows for cascade layout +} // End of arrangeInCascade + -(void)applicationDidFinishLaunching:(NSNotification *)notification{ [app setActivationPolicy:NSApplicationActivationPolicyRegular]; [app activateIgnoringOtherApps:YES]; @@ -176,10 +477,214 @@ - (void)showPreferencePanel:(id)sender{ [global_pref makeKeyAndOrderFront:self]; } +/** + * Shows the window manager panel. Creates it if it doesn't exist. + */ +- (void)showWindowManager{ + if(!windowManagerPanel){ + windowManagerPanel = [[WindowManagerPanel alloc] initWithAppDelegate:self]; + } + [windowManagerPanel makeKeyAndOrderFront:self]; + [windowManagerPanel refreshWindowList]; +} + -(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender{ - return false; + // Must return NO because Window is an NSPanel subclass. + // NSPanel windows are NOT counted by AppKit for this check, + // so returning YES causes immediate termination at launch. + // Termination is handled manually in Window's windowWillClose: instead. + return NO; +} + +@end + +#pragma mark - Window Manager Panel + +@implementation WindowManagerPanel + +/** + * Initializes the window manager panel. + * @param delegate The app delegate for accessing PiP windows + * @return The initialized panel + */ +- (id)initWithAppDelegate:(MyApplicationDelegate*)delegate{ + self = [super + initWithContentRect:NSMakeRect(0, 0, 420, 250) + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered defer:YES + ]; + self.delegate = self; + self.level = NSFloatingWindowLevel; + self.collectionBehavior = NSWindowCollectionBehaviorManaged | NSWindowCollectionBehaviorParticipatesInCycle; + [self setTitle:@"Administrador de ventanas"]; + self.minSize = NSMakeSize(300, 150); + + _appDelegate = delegate; + _windowList = [[NSMutableArray alloc] init]; + + NSView* rootView = [[NSView alloc] init]; + rootView.translatesAutoresizingMaskIntoConstraints = false; + + NSScrollView* scrollView = [[NSScrollView alloc] init]; + scrollView.hasHorizontalScroller = false; + scrollView.hasVerticalScroller = true; + scrollView.translatesAutoresizingMaskIntoConstraints = false; + [rootView addSubview:scrollView]; + + // Fill root view with scroll view + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeRight multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeTop multiplier:1 constant:0]]; + [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]]; + + _tableView = [[NSTableView alloc] init]; + _tableView.delegate = self; + _tableView.dataSource = self; + _tableView.headerView = nil; + _tableView.intercellSpacing = NSMakeSize(0, 2); + _tableView.translatesAutoresizingMaskIntoConstraints = NO; + _tableView.rowHeight = 28; + _tableView.doubleAction = @selector(onDoubleClick:); + _tableView.target = self; + + NSTableColumn* nameCol = [[NSTableColumn alloc] initWithIdentifier:@"name"]; + nameCol.title = @"Fuente"; + nameCol.width = 200; + [_tableView addTableColumn:nameCol]; + + NSTableColumn* typeCol = [[NSTableColumn alloc] initWithIdentifier:@"type"]; + typeCol.title = @"Tipo"; + typeCol.width = 80; + [_tableView addTableColumn:typeCol]; + + NSTableColumn* statusCol = [[NSTableColumn alloc] initWithIdentifier:@"status"]; + statusCol.title = @"Estado"; + statusCol.width = 80; + [_tableView addTableColumn:statusCol]; + + scrollView.documentView = _tableView; + [self setContentView:rootView]; + + // Center on screen + NSSize windowSize = [self frame].size; + NSSize screenSize = [[self screen] visibleFrame].size; + NSPoint origin = [[self screen] visibleFrame].origin; + NSPoint point = NSMakePoint(origin.x + screenSize.width/2 - windowSize.width/2, origin.y + screenSize.height/2 - windowSize.height/2); + [self setFrameOrigin:point]; + + return self; +} // End of initWithAppDelegate: + +/** + * Refreshes the list of open PiP windows and reloads the table. + */ +- (void)refreshWindowList{ + [_windowList removeAllObjects]; + for(NSWindow* window in [[NSApplication sharedApplication] windows]){ + if([window isKindOfClass:[Window class]]){ + [_windowList addObject:(Window*)window]; + } + } // End of loop through windows + [_tableView reloadData]; +} // End of refreshWindowList + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{ + return _windowList.count; +} + +- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{ + if(row >= (NSInteger)_windowList.count) return nil; + Window* win = _windowList[row]; + + NSTableCellView* cell = [[NSTableCellView alloc] init]; + NSTextField* text = [[NSTextField alloc] init]; + text.editable = NO; + text.selectable = NO; + text.bezeled = NO; + text.drawsBackground = NO; + text.translatesAutoresizingMaskIntoConstraints = false; + + if([tableColumn.identifier isEqual:@"name"]){ + text.stringValue = [win title] ?: @"Sin t\u00edtulo"; + text.lineBreakMode = NSLineBreakByTruncatingTail; + } else if([tableColumn.identifier isEqual:@"type"]){ + text.stringValue = [win sourceType]; + text.textColor = [NSColor secondaryLabelColor]; + } else if([tableColumn.identifier isEqual:@"status"]){ + NSString* status = [win sourceStatus]; + text.stringValue = status; + if([status isEqualToString:@"Activo"]){ + text.textColor = [NSColor systemGreenColor]; + } else if([status isEqualToString:@"Sin fuente"]){ + text.textColor = [NSColor tertiaryLabelColor]; + } else { + text.textColor = [NSColor systemOrangeColor]; + } + } + + [cell addSubview:text]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1 constant:8]]; + [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1 constant:-8]]; + + return cell; } +- (nullable NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row{ + NSTableRowView* rowView = [[NSTableRowView alloc] init]; + rowView.emphasized = false; + return rowView; +} + +/** + * Handles double-click on a row: focuses the corresponding PiP window. + * @param sender The table view + */ +- (void)onDoubleClick:(id)sender{ + NSInteger row = [_tableView clickedRow]; + if(row < 0 || row >= (NSInteger)_windowList.count) return; + Window* win = _windowList[row]; + [win makeKeyAndOrderFront:nil]; +} // End of onDoubleClick: + +- (void)tableViewSelectionDidChange:(NSNotification *)notification{ + NSInteger row = [_tableView selectedRow]; + if(row < 0 || row >= (NSInteger)_windowList.count) return; + Window* win = _windowList[row]; + [win makeKeyAndOrderFront:nil]; +} + +- (void)windowDidBecomeKey:(NSNotification *)notification{ + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [self refreshWindowList]; + + // Start auto-refresh timer (weakSelf to avoid retain cycle with repeating timer) + if(!_refreshTimer){ + __weak WindowManagerPanel* weakSelf = self; + _refreshTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer* timer){ + WindowManagerPanel* strongSelf = weakSelf; + if(strongSelf) [strongSelf refreshWindowList]; + else [timer invalidate]; + }]; + } +} + +- (void)windowDidResignKey:(NSNotification *)notification{ + // Stop auto-refresh when panel loses focus + if(_refreshTimer){ + [_refreshTimer invalidate]; + _refreshTimer = nil; + } +} + +- (void)windowWillClose:(NSNotification *)notification{ + if(_refreshTimer){ + [_refreshTimer invalidate]; + _refreshTimer = nil; + } + windowManagerPanel = nil; +} // End of windowWillClose: + @end int main(int argc, const char * argv[]) { diff --git a/pip/metalRenderer.m b/pip/metalRenderer.m index f52edaf..dd0929d 100644 --- a/pip/metalRenderer.m +++ b/pip/metalRenderer.m @@ -31,7 +31,7 @@ @implementation MetalRenderer{ - (instancetype)init:(BOOL)hidpi{ self = [super init]; self.image = nil; - self.device = MTLCreateSystemDefaultDevice(); + self.device = getSharedMTLDevice(); self.view = [[MTKView alloc] initWithFrame:CGRectZero device:self.device]; self.view.clearColor = MTLClearColorMake(0, 0, 0, 0); self.view.delegate = self; diff --git a/pip/preferences.m b/pip/preferences.m index 6251f0b..82d0a9e 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -153,6 +153,8 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ OPTION(wfilter_floating, "Exclude windows", CheckBox, [NSNull null], @1, @"that are floating"), OPTION(wfilter_desktop_elemnts, "Exclude windows", CheckBox, [NSNull null], @1, @"that are desktop elements"), OPTION(mouse_capture, "Show mouse cursor", CheckBox, [NSNull null], @0, @"when pipping screen"), + OPTION(new_window_behavior, "Nueva ventana", Select, (@[@"En blanco con pista", @"Clonar ventana actual"]), @0, [NSNull null]), + OPTION(max_windows, "M\u00e1ximo de ventanas", Select, (@[@"2", @"4", @"6", @"8", @"10"]), @3, [NSNull null]), ]]; // Add ScreenCaptureKit option only on macOS 12.3+ @@ -199,7 +201,7 @@ @implementation Preferences{ -(id)init{ self = [super - initWithContentRect:NSMakeRect(0, 0, 450, 230) + initWithContentRect:NSMakeRect(0, 0, 450, 290) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskNonactivatingPanel backing:NSBackingStoreBuffered defer:YES ]; @@ -421,7 +423,25 @@ - (void)windowDidBecomeKey:(NSNotification *)notification{ - (void)windowWillClose:(NSNotification *)notification{ global_pref = nil; -} + + // If no PiP windows remain, quit the app + BOOL hasPipWindows = NO; + Class windowClass = NSClassFromString(@"Window"); + if(windowClass){ + for(NSWindow* window in [[NSApplication sharedApplication] windows]){ + if([window isKindOfClass:windowClass]){ + hasPipWindows = YES; + break; + } + } // End of loop checking for remaining PiP windows + } + if(!hasPipWindows){ + // Defer to next runloop tick to avoid re-entrancy with window close + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSApplication sharedApplication] terminate:nil]; + }); + } +} // End of windowWillClose: @end diff --git a/pip/window.h b/pip/window.h index ab06ae2..346ebeb 100644 --- a/pip/window.h +++ b/pip/window.h @@ -71,5 +71,8 @@ - (void) setVolume:(float)volume; - (void) setAudioInputFormat:(UInt32)format withsampleRate:(UInt32)sampleRate andChannels:(UInt32)channelCount andSPF:(UInt32)spf; - (void) loadHLSURL:(NSURL*)url; +- (BOOL) cloneSourceToWindow:(Window*)target; +- (NSString*) sourceType; +- (NSString*) sourceStatus; @end #endif /* Window_h */ diff --git a/pip/window.m b/pip/window.m index 5867fa2..27d9413 100644 --- a/pip/window.m +++ b/pip/window.m @@ -420,6 +420,17 @@ - (void)setImage:(NSImage *)image { } @end +/** + * A view that passes through all mouse events to views behind it. + * Used for the source hint overlay so right-click menus still work. + */ +@interface PassthroughView : NSView +@end + +@implementation PassthroughView +- (NSView *)hitTest:(NSPoint)point { return nil; } +@end + @interface NSImage (ImageAdditions) +(NSImage *)swatchWithColor:(NSColor *)color size:(NSSize)size; @end @@ -735,6 +746,28 @@ - (void)magnifyWithEvent:(NSEvent *)event{ @end +// Forward declaration for methods called from C callbacks +@interface Window (DisconnectHandling) +- (void)handleDisplayDisconnected:(CGDirectDisplayID)displayId; +@end + +/** + * C callback for display reconfiguration events. + * Called when a display is added, removed, or reconfigured. + * The userInfo parameter is a pointer to the Window instance. + * @param display The display that was reconfigured + * @param flags The type of reconfiguration + * @param userInfo Pointer to the Window instance + */ +static void displayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void* userInfo){ + if(flags & kCGDisplayRemoveFlag){ + Window* window = (__bridge Window*)userInfo; + dispatch_async(dispatch_get_main_queue(), ^{ + [window handleDisplayDisconnected:display]; + }); + } +} // End of displayReconfigurationCallback() + @implementation Window{ NSTimer* timer; NSView* butCont; @@ -783,6 +816,9 @@ @implementation Window{ NSTimer* mouse_timer; bool mouse_timer_rerun; + NSView* sourceHintOverlay; + NSTextField* hintLabel; + NSString* airplay_title; bool was_floating; bool is_playing; @@ -877,6 +913,7 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ self.movable = YES; self.delegate = self; self.releasedWhenClosed = NO; + self.hidesOnDeactivate = NO; self.level = NSFloatingWindowLevel; self.movableByWindowBackground = YES; self.titlebarAppearsTransparent = true; @@ -1048,6 +1085,32 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ [[hlsButCont.widthAnchor constraintEqualToConstant:hlsButContRect.size.width] setActive:true]; [[hlsButCont.centerXAnchor constraintEqualToAnchor:rootView.centerXAnchor constant:-hlsButContRect.origin.x] setActive:true]; + // Create source hint overlay for blank windows + if(!is_airplay_session){ + sourceHintOverlay = [[PassthroughView alloc] initWithFrame:kStartRect]; + sourceHintOverlay.wantsLayer = YES; + sourceHintOverlay.layer.backgroundColor = [[NSColor colorWithWhite:0.0 alpha:0.6] CGColor]; + sourceHintOverlay.layer.cornerRadius = 10; + sourceHintOverlay.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + + hintLabel = [[NSTextField alloc] init]; + hintLabel.stringValue = @"Clic derecho para seleccionar fuente"; + hintLabel.editable = NO; + hintLabel.selectable = NO; + hintLabel.bezeled = NO; + hintLabel.drawsBackground = NO; + hintLabel.textColor = [NSColor whiteColor]; + hintLabel.font = [NSFont systemFontOfSize:14 weight:NSFontWeightMedium]; + hintLabel.alignment = NSTextAlignmentCenter; + hintLabel.translatesAutoresizingMaskIntoConstraints = NO; + + [sourceHintOverlay addSubview:hintLabel]; + [sourceHintOverlay addConstraint:[NSLayoutConstraint constraintWithItem:hintLabel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:sourceHintOverlay attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; + [sourceHintOverlay addConstraint:[NSLayoutConstraint constraintWithItem:hintLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:sourceHintOverlay attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; + + [rootView addSubview:sourceHintOverlay positioned:NSWindowAbove relativeTo:nil]; + } // End of source hint overlay setup + NSTrackingAreaOptions nstopts = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingAssumeInside; nstopts |= NSTrackingMouseMoved; NSTrackingArea *nstArea = [[NSTrackingArea alloc] initWithRect:[[self contentView] frame] options:nstopts owner:self userInfo:nil]; @@ -1088,6 +1151,9 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ } } // End of if not airplay session + // Register for display reconfiguration events (display disconnect) + CGDisplayRegisterReconfigurationCallback(displayReconfigurationCallback, (__bridge void*)self); + return self; } @@ -2092,6 +2158,12 @@ -(void)stopWindowStream{ */ -(void)stopCameraCapture{ if(!camera_session) return; + + // Remove notification observers before stopping + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:camera_session]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStopRunningNotification object:camera_session]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceWasDisconnectedNotification object:nil]; + [camera_session stopRunning]; // Clean up audio preview @@ -2355,6 +2427,11 @@ -(void)startCameraCapture:(NSString*)deviceId{ camera_format = device.activeFormat; } + // Register for camera disconnect/error notifications + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cameraSessionError:) name:AVCaptureSessionRuntimeErrorNotification object:session]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cameraSessionError:) name:AVCaptureSessionDidStopRunningNotification object:session]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cameraSessionError:) name:AVCaptureDeviceWasDisconnectedNotification object:device]; + // Start session after all inputs/outputs are configured [session startRunning]; @@ -2435,6 +2512,10 @@ - (void)changeWindow:(id)sender{ }; display_stream = CGDisplayStreamCreateWithDispatchQueue(display_id, width, height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)opts, dispatch_get_main_queue(), ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) { + if(status == kCGDisplayStreamFrameStatusStopped){ + if(!self->isWinClosing) [self showDisconnectOverlay:@"Pantalla desconectada"]; + return; + } if(status != kCGDisplayStreamFrameStatusFrameComplete || !self->is_playing || self->isWinClosing) return; [self->imageView setImage:[CIImage imageWithIOSurface:frameSurface]]; }); @@ -2468,16 +2549,130 @@ - (void)changeWindow:(id)sender{ // [imageView setImage:nil]; [imageView setHidden:![self is_capturing]]; [self setOwner:sel.owner withTitle:sel.title]; + + // Hide or show source hint overlay based on capture state + if(sourceHintOverlay){ + [sourceHintOverlay setHidden:[self is_capturing]]; + } } +/** + * Shows the disconnect overlay with a message explaining why the source was lost. + * Stops all capture, resets source state, and displays the overlay with the given message + * plus a hint to right-click for a new source. + * Must be called on the main thread. + * @param message The disconnect reason to display (e.g. "Pantalla desconectada") + */ +- (void)showDisconnectOverlay:(NSString*)message{ + if(isWinClosing) return; + + [self stopTimer]; + [self stopDisplayStream]; + [self stopWindowStream]; + [self stopCameraCapture]; + + window_id = -1; + display_id = -1; + is_playing = false; + [self resetPlaybackSate]; + + [imageView setHidden:YES]; + + if(sourceHintOverlay && hintLabel){ + hintLabel.stringValue = [NSString stringWithFormat:@"%@\nClic derecho para seleccionar fuente", message]; + [sourceHintOverlay setHidden:NO]; + } + + [self setOwner:nil withTitle:message]; +} // End of showDisconnectOverlay: + +/** + * Called when a display is physically disconnected. + * If this window was capturing that display, shows a disconnect overlay. + * @param displayId The ID of the disconnected display + */ +- (void)handleDisplayDisconnected:(CGDirectDisplayID)displayId{ + if(display_id >= 0 && (CGDirectDisplayID)display_id == displayId){ + [self showDisconnectOverlay:@"Pantalla desconectada"]; + } +} // End of handleDisplayDisconnected: + +/** + * Called when the camera capture session encounters an error or the device is disconnected. + * Shows a disconnect overlay with the appropriate message. + * @param notification The notification containing error information + */ +- (void)cameraSessionError:(NSNotification*)notification{ + if(!camera_session) return; + dispatch_async(dispatch_get_main_queue(), ^{ + if(self->isWinClosing) return; + [self showDisconnectOverlay:@"C\u00e1mara no disponible"]; + }); +} // End of cameraSessionError: + +/** + * Clones the current source selection to another window. + * Creates a WindowSel from the current state and triggers changeWindow on the target. + * Camera sources are skipped since AVCaptureDevice is exclusive to one session. + * @param target The window to clone the source to + * @return YES if cloning succeeded, NO if source can't be cloned (camera or no source) + */ +- (BOOL) cloneSourceToWindow:(Window*)target{ + if(![self is_capturing] && !is_hls_session) return NO; + + // Camera can't be shared between sessions + if(camera_id != nil){ + NSLog(@"Cannot clone camera source — AVCaptureDevice is exclusive to one session"); + return NO; + } + + WindowSel* sel = [WindowSel getDefault]; + sel.winId = window_id; + sel.dspId = display_id; + sel.ownerPid = owner_pid; + sel.cameraId = nil; + sel.owner = nil; + sel.title = self.title; + + NSMenuItem* item = [[NSMenuItem alloc] init]; + [item setRepresentedObject:sel]; + [target changeWindow:item]; + return YES; +} // End of cloneSourceToWindow: + +/** + * Returns a localized string describing the type of source this window is capturing. + * @return Source type string (e.g. "Pantalla", "Ventana", "C\u00e1mara", "HLS", "AirPlay") + */ +- (NSString*) sourceType{ + if(is_airplay_session) return @"AirPlay"; + if(is_hls_session) return @"HLS"; + if(camera_id != nil) return @"C\u00e1mara"; + if(display_id >= 0) return @"Pantalla"; + if(window_id >= 0) return @"Ventana"; + return @"Ninguno"; +} // End of sourceType + +/** + * Returns a localized string describing the current status of this window's capture. + * @return Status string (e.g. "Activo", "Pausado", "Desconectado") + */ +- (NSString*) sourceStatus{ + if(![self is_capturing] && !is_hls_session && !is_airplay_session) return @"Sin fuente"; + if(is_playing) return @"Activo"; + return @"Pausado"; +} // End of sourceStatus + #if __has_include() // SCStreamDelegate method - called when stream stops - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error API_AVAILABLE(macos(12.3)) { - if (error) { - NSLog(@"ScreenCaptureKit stream stopped with error: %@", error); - } + NSLog(@"ScreenCaptureKit stream stopped%@", error ? [NSString stringWithFormat:@" with error: %@", error] : @""); + dispatch_async(dispatch_get_main_queue(), ^{ + if(self->isWinClosing) return; + [self showDisconnectOverlay:@"Ventana cerrada"]; + }); } - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)){ @@ -3389,8 +3584,21 @@ - (void)dismissHLSInputView { } - (void)windowWillClose:(NSNotification *)notification{ - // Terminate the app when the window is closed - [[NSApplication sharedApplication] terminate:nil]; + isWinClosing = true; + [self stopTimer]; + [self stopDisplayStream]; + [self stopWindowStream]; + [self stopCameraCapture]; + CGDisplayRemoveReconfigurationCallback(displayReconfigurationCallback, (__bridge void*)self); + + // If this is the last PiP window, terminate the app + NSInteger remainingPipWindows = 0; + for(NSWindow* w in [[NSApplication sharedApplication] windows]){ + if([w isKindOfClass:[Window class]] && w != self) remainingPipWindows++; + } + if(remainingPipWindows == 0){ + [[NSApplication sharedApplication] terminate:nil]; + } } - (void)windowDidBecomeKey:(NSNotification *)notification{ @@ -3402,7 +3610,6 @@ - (void)windowDidBecomeKey:(NSNotification *)notification{ //} - (void)close{ -// NSLog(@"close pvc: %d, isPipCLosing: %d, isWinClosing: %d", (int)pvc, isPipCLosing, isWinClosing); [self dismissHLSInputView]; if(pvc){ if(!isPipCLosing){ @@ -3425,6 +3632,14 @@ - (void)close{ if(isWinClosing) return; isWinClosing = true; + // Unregister display reconfiguration callback + CGDisplayRemoveReconfigurationCallback(displayReconfigurationCallback, (__bridge void*)self); + + // Remove camera disconnect notification observers + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStopRunningNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceWasDisconnectedNotification object:nil]; + #ifndef NO_AIRPLAY if(is_airplay_session) airplay_receiver_session_stop(self.conn); if (airplaySender) { @@ -3455,6 +3670,10 @@ - (void)close{ [popbutt removeFromSuperview]; [playbutt removeFromSuperview]; [selectionView removeFromSuperview]; + if(sourceHintOverlay){ + [sourceHintOverlay removeFromSuperview]; + sourceHintOverlay = nil; + } [rootView removeFromSuperview]; nvc = NULL; From 08280b24f77d77c3fc2aa7ff0fe33ed74ed61056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garci=CC=81a?= Date: Fri, 6 Feb 2026 19:15:04 +0000 Subject: [PATCH 10/19] Translate all UI text from Spanish to English and update LICENSE --- LICENSE | 1 + pip/main.m | 42 +++++++++++++++++++++--------------------- pip/preferences.m | 6 +++--- pip/window.m | 36 ++++++++++++++++++------------------ 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/LICENSE b/LICENSE index fb6936b..a86cb58 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2020 amitv87 +Copyright (c) 2026 ccarpiog Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pip/main.m b/pip/main.m index accaf3b..d67a8b4 100644 --- a/pip/main.m +++ b/pip/main.m @@ -87,7 +87,7 @@ -(id)initWithApp:(NSApplication*) application{ INIT_MENU(@"File"); ADD_ITEM(@"New", newWindow, @"n"); - ADD_ITEM_MASK(@"Clonar ventana", cloneCurrentWindow, @"n", NSEventModifierFlagCommand | NSEventModifierFlagShift); + ADD_ITEM_MASK(@"Clone Window", cloneCurrentWindow, @"n", NSEventModifierFlagCommand | NSEventModifierFlagShift); ADD_ITEM(@"Stream HLS", loadHLSStream:, @"l"); ADD_ITEM(@"Click Through", clickThrough:, @"c"); ADD_ITEM(@"Close", performClose:, @"w"); @@ -120,12 +120,12 @@ -(id)initWithApp:(NSApplication*) application{ ADD_ITEM(@"Bring All to Front", arrangeInFront:, @""); ADD_ITEM(@"Toggle Native PiP", toggleNativePip, @"p"); ADD_SEP(); - ADD_ITEM(@"Organizar en cuadr\u00edcula", arrangeInGrid, @"g"); - ADD_ITEM(@"Cascada", arrangeInCascade, @""); + ADD_ITEM(@"Arrange in Grid", arrangeInGrid, @"g"); + ADD_ITEM(@"Cascade", arrangeInCascade, @""); ADD_SEP(); - ADD_ITEM_MASK(@"Cerrar todas las ventanas", closeAllWindows, @"w", NSEventModifierFlagCommand | NSEventModifierFlagOption); + ADD_ITEM_MASK(@"Close All Windows", closeAllWindows, @"w", NSEventModifierFlagCommand | NSEventModifierFlagOption); ADD_SEP(); - ADD_ITEM_MASK(@"Administrador de ventanas", showWindowManager, @"m", NSEventModifierFlagCommand | NSEventModifierFlagOption); + ADD_ITEM_MASK(@"Window Manager", showWindowManager, @"m", NSEventModifierFlagCommand | NSEventModifierFlagOption); [app setMainMenu:menubar]; [app setWindowsMenu:menu]; @@ -173,10 +173,10 @@ - (NSWindow*) newWindow{ if(!maxWindowsAlertShown){ maxWindowsAlertShown = true; NSAlert* alert = [[NSAlert alloc] init]; - [alert setMessageText:@"L\u00edmite de ventanas alcanzado"]; - [alert setInformativeText:[NSString stringWithFormat:@"Se alcanz\u00f3 el m\u00e1ximo de %ld ventanas. Puedes cambiar el l\u00edmite en Preferencias.", (long)maxWindows]]; + [alert setMessageText:@"Window limit reached"]; + [alert setInformativeText:[NSString stringWithFormat:@"Maximum of %ld windows reached. You can change the limit in Preferences.", (long)maxWindows]]; [alert addButtonWithTitle:@"OK"]; - [alert addButtonWithTitle:@"Preferencias..."]; + [alert addButtonWithTitle:@"Preferences..."]; NSModalResponse response = [alert runModal]; if(response == NSAlertSecondButtonReturn){ [self showPreferencePanel:self]; @@ -191,8 +191,8 @@ - (NSWindow*) newWindow{ if(!performanceWarningShown && (NSInteger)currentWindows.count >= 5){ performanceWarningShown = true; NSAlert* alert = [[NSAlert alloc] init]; - [alert setMessageText:@"Advertencia de rendimiento"]; - [alert setInformativeText:@"Muchas ventanas abiertas. El rendimiento puede verse afectado."]; + [alert setMessageText:@"Performance warning"]; + [alert setInformativeText:@"Many windows open. Performance may be affected."]; [alert addButtonWithTitle:@"OK"]; [alert setAlertStyle:NSAlertStyleInformational]; [alert runModal]; @@ -234,10 +234,10 @@ - (void) cloneCurrentWindow{ if(!maxWindowsAlertShown){ maxWindowsAlertShown = true; NSAlert* alert = [[NSAlert alloc] init]; - [alert setMessageText:@"L\u00edmite de ventanas alcanzado"]; - [alert setInformativeText:[NSString stringWithFormat:@"Se alcanz\u00f3 el m\u00e1ximo de %ld ventanas. Puedes cambiar el l\u00edmite en Preferencias.", (long)maxWindows]]; + [alert setMessageText:@"Window limit reached"]; + [alert setInformativeText:[NSString stringWithFormat:@"Maximum of %ld windows reached. You can change the limit in Preferences.", (long)maxWindows]]; [alert addButtonWithTitle:@"OK"]; - [alert addButtonWithTitle:@"Preferencias..."]; + [alert addButtonWithTitle:@"Preferences..."]; NSModalResponse response = [alert runModal]; if(response == NSAlertSecondButtonReturn){ [self showPreferencePanel:self]; @@ -267,10 +267,10 @@ - (void) cloneCurrentWindow{ BOOL cloned = [sourceWindow cloneSourceToWindow:(Window*)newWin]; if(!cloned){ // If clone failed (camera or no source), show alert for camera - if([[sourceWindow sourceType] isEqualToString:@"C\u00e1mara"]){ + if([[sourceWindow sourceType] isEqualToString:@"Camera"]){ NSAlert* alert = [[NSAlert alloc] init]; - [alert setMessageText:@"No se puede clonar"]; - [alert setInformativeText:@"Las fuentes de c\u00e1mara no se pueden compartir entre ventanas. Selecciona otra fuente con clic derecho."]; + [alert setMessageText:@"Cannot clone"]; + [alert setInformativeText:@"Camera sources cannot be shared between windows. Right-click to select a different source."]; [alert addButtonWithTitle:@"OK"]; [alert runModal]; } @@ -516,7 +516,7 @@ - (id)initWithAppDelegate:(MyApplicationDelegate*)delegate{ self.delegate = self; self.level = NSFloatingWindowLevel; self.collectionBehavior = NSWindowCollectionBehaviorManaged | NSWindowCollectionBehaviorParticipatesInCycle; - [self setTitle:@"Administrador de ventanas"]; + [self setTitle:@"Window Manager"]; self.minSize = NSMakeSize(300, 150); _appDelegate = delegate; @@ -548,7 +548,7 @@ - (id)initWithAppDelegate:(MyApplicationDelegate*)delegate{ _tableView.target = self; NSTableColumn* nameCol = [[NSTableColumn alloc] initWithIdentifier:@"name"]; - nameCol.title = @"Fuente"; + nameCol.title = @"Source"; nameCol.width = 200; [_tableView addTableColumn:nameCol]; @@ -605,7 +605,7 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null text.translatesAutoresizingMaskIntoConstraints = false; if([tableColumn.identifier isEqual:@"name"]){ - text.stringValue = [win title] ?: @"Sin t\u00edtulo"; + text.stringValue = [win title] ?: @"Untitled"; text.lineBreakMode = NSLineBreakByTruncatingTail; } else if([tableColumn.identifier isEqual:@"type"]){ text.stringValue = [win sourceType]; @@ -613,9 +613,9 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null } else if([tableColumn.identifier isEqual:@"status"]){ NSString* status = [win sourceStatus]; text.stringValue = status; - if([status isEqualToString:@"Activo"]){ + if([status isEqualToString:@"Active"]){ text.textColor = [NSColor systemGreenColor]; - } else if([status isEqualToString:@"Sin fuente"]){ + } else if([status isEqualToString:@"No source"]){ text.textColor = [NSColor tertiaryLabelColor]; } else { text.textColor = [NSColor systemOrangeColor]; diff --git a/pip/preferences.m b/pip/preferences.m index 82d0a9e..a153621 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -113,7 +113,7 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ */ NSArray* getDisplayList(void){ NSMutableArray* displays = [[NSMutableArray alloc] init]; - [displays addObject:@{@"name": @"Ninguno", @"id": @-1}]; + [displays addObject:@{@"name": @"None", @"id": @-1}]; for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; @@ -153,8 +153,8 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ OPTION(wfilter_floating, "Exclude windows", CheckBox, [NSNull null], @1, @"that are floating"), OPTION(wfilter_desktop_elemnts, "Exclude windows", CheckBox, [NSNull null], @1, @"that are desktop elements"), OPTION(mouse_capture, "Show mouse cursor", CheckBox, [NSNull null], @0, @"when pipping screen"), - OPTION(new_window_behavior, "Nueva ventana", Select, (@[@"En blanco con pista", @"Clonar ventana actual"]), @0, [NSNull null]), - OPTION(max_windows, "M\u00e1ximo de ventanas", Select, (@[@"2", @"4", @"6", @"8", @"10"]), @3, [NSNull null]), + OPTION(new_window_behavior, "New Window", Select, (@[@"Blank with hint", @"Clone current window"]), @0, [NSNull null]), + OPTION(max_windows, "Max Windows", Select, (@[@"2", @"4", @"6", @"8", @"10"]), @3, [NSNull null]), ]]; // Add ScreenCaptureKit option only on macOS 12.3+ diff --git a/pip/window.m b/pip/window.m index 27d9413..0c02b70 100644 --- a/pip/window.m +++ b/pip/window.m @@ -1094,7 +1094,7 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ sourceHintOverlay.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; hintLabel = [[NSTextField alloc] init]; - hintLabel.stringValue = @"Clic derecho para seleccionar fuente"; + hintLabel.stringValue = @"Right-click to select source"; hintLabel.editable = NO; hintLabel.selectable = NO; hintLabel.bezeled = NO; @@ -2513,7 +2513,7 @@ - (void)changeWindow:(id)sender{ display_stream = CGDisplayStreamCreateWithDispatchQueue(display_id, width, height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)opts, dispatch_get_main_queue(), ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) { if(status == kCGDisplayStreamFrameStatusStopped){ - if(!self->isWinClosing) [self showDisconnectOverlay:@"Pantalla desconectada"]; + if(!self->isWinClosing) [self showDisconnectOverlay:@"Display disconnected"]; return; } if(status != kCGDisplayStreamFrameStatusFrameComplete || !self->is_playing || self->isWinClosing) return; @@ -2561,7 +2561,7 @@ - (void)changeWindow:(id)sender{ * Stops all capture, resets source state, and displays the overlay with the given message * plus a hint to right-click for a new source. * Must be called on the main thread. - * @param message The disconnect reason to display (e.g. "Pantalla desconectada") + * @param message The disconnect reason to display (e.g. "Display disconnected") */ - (void)showDisconnectOverlay:(NSString*)message{ if(isWinClosing) return; @@ -2579,7 +2579,7 @@ - (void)showDisconnectOverlay:(NSString*)message{ [imageView setHidden:YES]; if(sourceHintOverlay && hintLabel){ - hintLabel.stringValue = [NSString stringWithFormat:@"%@\nClic derecho para seleccionar fuente", message]; + hintLabel.stringValue = [NSString stringWithFormat:@"%@\nRight-click to select source", message]; [sourceHintOverlay setHidden:NO]; } @@ -2593,7 +2593,7 @@ - (void)showDisconnectOverlay:(NSString*)message{ */ - (void)handleDisplayDisconnected:(CGDirectDisplayID)displayId{ if(display_id >= 0 && (CGDirectDisplayID)display_id == displayId){ - [self showDisconnectOverlay:@"Pantalla desconectada"]; + [self showDisconnectOverlay:@"Display disconnected"]; } } // End of handleDisplayDisconnected: @@ -2606,7 +2606,7 @@ - (void)cameraSessionError:(NSNotification*)notification{ if(!camera_session) return; dispatch_async(dispatch_get_main_queue(), ^{ if(self->isWinClosing) return; - [self showDisconnectOverlay:@"C\u00e1mara no disponible"]; + [self showDisconnectOverlay:@"Camera unavailable"]; }); } // End of cameraSessionError: @@ -2641,26 +2641,26 @@ - (BOOL) cloneSourceToWindow:(Window*)target{ } // End of cloneSourceToWindow: /** - * Returns a localized string describing the type of source this window is capturing. - * @return Source type string (e.g. "Pantalla", "Ventana", "C\u00e1mara", "HLS", "AirPlay") + * Returns a string describing the type of source this window is capturing. + * @return Source type string (e.g. "Display", "Window", "Camera", "HLS", "AirPlay") */ - (NSString*) sourceType{ if(is_airplay_session) return @"AirPlay"; if(is_hls_session) return @"HLS"; - if(camera_id != nil) return @"C\u00e1mara"; - if(display_id >= 0) return @"Pantalla"; - if(window_id >= 0) return @"Ventana"; - return @"Ninguno"; + if(camera_id != nil) return @"Camera"; + if(display_id >= 0) return @"Display"; + if(window_id >= 0) return @"Window"; + return @"None"; } // End of sourceType /** - * Returns a localized string describing the current status of this window's capture. - * @return Status string (e.g. "Activo", "Pausado", "Desconectado") + * Returns a string describing the current status of this window's capture. + * @return Status string (e.g. "Active", "Paused", "No source") */ - (NSString*) sourceStatus{ - if(![self is_capturing] && !is_hls_session && !is_airplay_session) return @"Sin fuente"; - if(is_playing) return @"Activo"; - return @"Pausado"; + if(![self is_capturing] && !is_hls_session && !is_airplay_session) return @"No source"; + if(is_playing) return @"Active"; + return @"Paused"; } // End of sourceStatus @@ -2671,7 +2671,7 @@ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error API_AVAILABL NSLog(@"ScreenCaptureKit stream stopped%@", error ? [NSString stringWithFormat:@" with error: %@", error] : @""); dispatch_async(dispatch_get_main_queue(), ^{ if(self->isWinClosing) return; - [self showDisconnectOverlay:@"Ventana cerrada"]; + [self showDisconnectOverlay:@"Window closed"]; }); } From ba0977e58d849fb67067631259878a1ec8e52d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 16:26:28 +0000 Subject: [PATCH 11/19] Add HLS streaming pipeline and stabilize live delivery --- CMakeLists.txt | 2 +- pip.xcodeproj/project.pbxproj | 30 ++ pip/frame_capture.m | 20 +- pip/hls.min.js | 2 + pip/hls_writer.c | 348 +++++++++++++ pip/hls_writer.h | 80 +++ pip/main.m | 12 + pip/preferences.h | 2 +- pip/preferences.m | 37 +- pip/stream_manager.h | 105 ++++ pip/stream_manager.m | 739 +++++++++++++++++++++++++++ pip/stream_server.h | 105 ++++ pip/stream_server.m | 752 ++++++++++++++++++++++++++++ pip/ts_muxer.c | 918 ++++++++++++++++++++++++++++++++++ pip/ts_muxer.h | 92 ++++ pip/viewer.html | 195 ++++++++ pip/window.h | 2 + pip/window.m | 142 ++++++ 18 files changed, 3569 insertions(+), 14 deletions(-) create mode 100644 pip/hls.min.js create mode 100644 pip/hls_writer.c create mode 100644 pip/hls_writer.h create mode 100644 pip/stream_manager.h create mode 100644 pip/stream_manager.m create mode 100644 pip/stream_server.h create mode 100644 pip/stream_server.m create mode 100644 pip/ts_muxer.c create mode 100644 pip/ts_muxer.h create mode 100644 pip/viewer.html diff --git a/CMakeLists.txt b/CMakeLists.txt index 69ce50e..cba3d7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ list(TRANSFORM frameworks PREPEND "-framework ") set(AIRPLAY_SUPPORT_ENABLED 1) -file(GLOB_RECURSE pip_src CONFIGURE_DEPENDS "pip/*.m") +file(GLOB_RECURSE pip_src CONFIGURE_DEPENDS "pip/*.m" "pip/*.c") add_executable(pip ${pip_src}) target_include_directories(pip PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/pip.xcodeproj/project.pbxproj b/pip.xcodeproj/project.pbxproj index 01e0edf..f076617 100644 --- a/pip.xcodeproj/project.pbxproj +++ b/pip.xcodeproj/project.pbxproj @@ -147,6 +147,10 @@ 55FEB9A7 /* airplaySender.m in Sources */ = {isa = PBXBuildFile; fileRef = 55FEB9A2 /* airplaySender.m */; }; 55FEB9A8 /* frame_capture.m in Sources */ = {isa = PBXBuildFile; fileRef = 55FEB9A4 /* frame_capture.m */; }; 55FEB9A9 /* video_encoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 55FEB9A6 /* video_encoder.m */; }; + 55FEBC11 /* ts_muxer.c in Sources */ = {isa = PBXBuildFile; fileRef = 55FEBC02 /* ts_muxer.c */; }; + 55FEBC12 /* hls_writer.c in Sources */ = {isa = PBXBuildFile; fileRef = 55FEBC04 /* hls_writer.c */; }; + 55FEBC13 /* stream_server.m in Sources */ = {isa = PBXBuildFile; fileRef = 55FEBC06 /* stream_server.m */; }; + 55FEBC14 /* stream_manager.m in Sources */ = {isa = PBXBuildFile; fileRef = 55FEBC08 /* stream_manager.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -325,6 +329,17 @@ 55FEB9A5 /* video_encoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = video_encoder.h; sourceTree = ""; }; 55FEB9A6 /* video_encoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = video_encoder.m; sourceTree = ""; }; 55FEB9AA /* ScreenCaptureKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScreenCaptureKit.framework; path = /System/Volumes/Data/Library/Developer/CommandLineTools/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/ScreenCaptureKit.framework; sourceTree = ""; }; + 55FEBC01 /* ts_muxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ts_muxer.h; sourceTree = ""; }; + 55FEBC02 /* ts_muxer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = ts_muxer.c; sourceTree = ""; }; + 55FEBC03 /* hls_writer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hls_writer.h; sourceTree = ""; }; + 55FEBC04 /* hls_writer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hls_writer.c; sourceTree = ""; }; + 55FEBC05 /* stream_server.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = stream_server.h; sourceTree = ""; }; + 55FEBC06 /* stream_server.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = stream_server.m; sourceTree = ""; }; + 55FEBC07 /* stream_manager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = stream_manager.h; sourceTree = ""; }; + 55FEBC08 /* stream_manager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = stream_manager.m; sourceTree = ""; }; + 55FEBC09 /* viewer.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = viewer.html; sourceTree = ""; }; + 55FEBC0A /* hls.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "hls.min.js"; sourceTree = ""; }; + 55FEBC0B /* incbin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = incbin.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -576,6 +591,17 @@ 55FEB9A4 /* frame_capture.m */, 55FEB9A5 /* video_encoder.h */, 55FEB9A6 /* video_encoder.m */, + 55FEBC01 /* ts_muxer.h */, + 55FEBC02 /* ts_muxer.c */, + 55FEBC03 /* hls_writer.h */, + 55FEBC04 /* hls_writer.c */, + 55FEBC05 /* stream_server.h */, + 55FEBC06 /* stream_server.m */, + 55FEBC07 /* stream_manager.h */, + 55FEBC08 /* stream_manager.m */, + 55FEBC09 /* viewer.html */, + 55FEBC0A /* hls.min.js */, + 55FEBC0B /* incbin.h */, ); path = pip; sourceTree = ""; @@ -807,6 +833,10 @@ 55FEB9A7 /* airplaySender.m in Sources */, 55FEB9A8 /* frame_capture.m in Sources */, 55FEB9A9 /* video_encoder.m in Sources */, + 55FEBC11 /* ts_muxer.c in Sources */, + 55FEBC12 /* hls_writer.c in Sources */, + 55FEBC13 /* stream_server.m in Sources */, + 55FEBC14 /* stream_manager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/pip/frame_capture.m b/pip/frame_capture.m index e1ab95c..22902dc 100644 --- a/pip/frame_capture.m +++ b/pip/frame_capture.m @@ -96,17 +96,15 @@ static void capture_frame(frame_capture_t *cap) { return; } - // Get the current image from the renderer (must be on main thread) - // __block CIImage *currentImage = nil; - // if ([NSThread isMainThread]) { - // currentImage = [imageView.renderer currentImage]; - // } else { - // dispatch_sync(dispatch_get_main_queue(), ^{ - // currentImage = [imageView.renderer currentImage]; - // }); - // } - - __block CIImage *currentImage = [imageView.renderer currentImage]; + // Get the current image from the renderer on main thread. + __block CIImage *currentImage = nil; + if ([NSThread isMainThread]) { + currentImage = [imageView.renderer currentImage]; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + currentImage = [imageView.renderer currentImage]; + }); + } if (!currentImage) { if (cap->frame_count == 0 || cap->frame_count % 30 == 0) { diff --git a/pip/hls.min.js b/pip/hls.min.js new file mode 100644 index 0000000..4831792 --- /dev/null +++ b/pip/hls.min.js @@ -0,0 +1,2 @@ +!function e(t){var r,i;r=this,i=function(){"use strict";function r(e,t){for(var r=0;r=this.minWeight_},t.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},t.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},t.destroy=function(){},i(e,[{key:"defaultEstimate",get:function(){return this.defaultEstimate_}}])}(),N=function(e,t){this.trace=void 0,this.debug=void 0,this.log=void 0,this.warn=void 0,this.info=void 0,this.error=void 0;var r="["+e+"]:";this.trace=U,this.debug=t.debug.bind(null,r),this.log=t.log.bind(null,r),this.warn=t.warn.bind(null,r),this.info=t.info.bind(null,r),this.error=t.error.bind(null,r)},U=function(){},B={trace:U,debug:U,log:U,warn:U,info:U,error:U};function G(){return a({},B)}function K(e,t,r){return t[e]?t[e].bind(t):function(e,t){var r=self.console[e];return r?r.bind(self.console,(t?"["+t+"] ":"")+"["+e+"] >"):U}(e,r)}var V=G();function H(e,t,r){var i=G();if("object"==typeof console&&!0===e||"object"==typeof e){var n=["debug","log","info","warn","error"];n.forEach((function(t){i[t]=K(t,e,r)}));try{i.log('Debug logs enabled for "'+t+'" in hls.js version 1.6.15')}catch(e){return G()}n.forEach((function(t){V[t]=K(t,e)}))}else a(V,i);return i}var Y=V;function W(e){if(void 0===e&&(e=!0),"undefined"!=typeof self)return(e||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}function j(e,t){var r=Object.keys(e),i=Object.keys(t),n=r.length,a=i.length;return!n||!a||n===a&&!r.some((function(e){return-1===i.indexOf(e)}))}function q(e,t){if(void 0===t&&(t=!1),"undefined"!=typeof TextDecoder){var r=new TextDecoder("utf-8").decode(e);if(t){var i=r.indexOf("\0");return-1!==i?r.substring(0,i):r}return r.replace(/\0/g,"")}for(var n,a,s,o=e.length,l="",u=0;u>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:l+=String.fromCharCode(n);break;case 12:case 13:a=e[u++],l+=String.fromCharCode((31&n)<<6|63&a);break;case 14:a=e[u++],s=e[u++],l+=String.fromCharCode((15&n)<<12|(63&a)<<6|(63&s)<<0)}}return l}function X(e){for(var t="",r=0;r1||1===i&&null!=(t=this.levelkeys[r[0]])&&t.encrypted)return!0}return!1}},{key:"programDateTime",get:function(){return null===this._programDateTime&&this.rawProgramDateTime&&(this.programDateTime=Date.parse(this.rawProgramDateTime)),this._programDateTime},set:function(e){A(e)?this._programDateTime=e:this._programDateTime=this.rawProgramDateTime=null}},{key:"ref",get:function(){return te(this)?(this._ref||(this._ref={base:this.base,start:this.start,duration:this.duration,sn:this.sn,programDateTime:this.programDateTime}),this._ref):null}}])}(ee),ie=function(e){function t(t,r,i,n,a){var s;(s=e.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.duration=t.decimalFloatingPoint("DURATION"),s.gap=t.bool("GAP"),s.independent=t.bool("INDEPENDENT"),s.relurl=t.enumeratedString("URI"),s.fragment=r,s.index=n;var o=t.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return o(t,e),i(t,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var e=this.elementaryStreams;return!!(e.audio||e.video||e.audiovideo)}}])}(ee);function ne(e,t){var r=Object.getPrototypeOf(e);if(r){var i=Object.getOwnPropertyDescriptor(r,t);return i||ne(r,t)}}var ae=Math.pow(2,32)-1,se=[].push,oe={video:1,audio:2,id3:3,text:4};function le(e){return String.fromCharCode.apply(null,e)}function ue(e,t){var r=e[t]<<8|e[t+1];return r<0?65536+r:r}function de(e,t){var r=fe(e,t);return r<0?4294967296+r:r}function he(e,t){var r=de(e,t);return r*=Math.pow(2,32),r+=de(e,t+4)}function fe(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]}function ce(e,t){var r=[];if(!t.length)return r;for(var i=e.byteLength,n=0;n1?n+a:i;if(le(e.subarray(n+4,n+8))===t[0])if(1===t.length)r.push(e.subarray(n+8,s));else{var o=ce(e.subarray(n+8,s),t.slice(1));o.length&&se.apply(r,o)}n=s}return r}function ge(e){var t=[],r=e[0],i=8,n=de(e,i);i+=4;var a=0,s=0;0===r?(a=de(e,i),s=de(e,i+4),i+=8):(a=he(e,i),s=he(e,i+8),i+=16),i+=2;var o=e.length+s,l=ue(e,i);i+=2;for(var u=0;u>>31)return Y.warn("SIDX has hierarchical references (not supported)"),null;var c=de(e,d);d+=4,t.push({referenceSize:f,subsegmentDuration:c,info:{duration:c/n,start:o,end:o+f-1}}),o+=f,i=d+=4}return{earliestPresentationTime:a,timescale:n,version:r,referencesCount:l,references:t}}function ve(e){for(var t=[],r=ce(e,["moov","trak"]),i=0;i3&&(a+="."+Ee(u[1])+Ee(u[2])+Ee(u[3]),t=pe("avc1"===l?"dva1":"dvav",i));break;case"mp4a":var d=ce(r,[n])[0],h=ce(d.subarray(28),["esds"])[0];if(h&&h.length>7){var f=4;if(3!==h[f++])break;f=ye(h,f),f+=2;var c=h[f++];if(128&c&&(f+=2),64&c&&(f+=h[f++]),4!==h[f++])break;f=ye(h,f);var g=h[f++];if(64!==g)break;if(a+="."+Ee(g),f+=12,5!==h[f++])break;f=ye(h,f);var v=h[f++],m=(248&v)>>3;31===m&&(m+=1+((7&v)<<3)+((224&h[f])>>5)),a+="."+m}break;case"hvc1":case"hev1":var p=ce(i,["hvcC"])[0];if(p&&p.length>12){var y=p[1],E=["","A","B","C"][y>>6],T=31&y,S=de(p,2),A=(32&y)>>5?"H":"L",L=p[12],I=p.subarray(6,12);a+="."+E+T,a+="."+function(e){for(var t=0,r=0;r<32;r++)t|=(e>>r&1)<<31-r;return t>>>0}(S).toString(16).toUpperCase(),a+="."+A+L;for(var R="",k=I.length;k--;){var b=I[k];(b||R)&&(R="."+b.toString(16).toUpperCase()+R)}a+=R}t=pe("hev1"==l?"dvhe":"dvh1",i);break;case"dvh1":case"dvhe":case"dvav":case"dva1":case"dav1":a=pe(a,i)||a;break;case"vp09":var D=ce(i,["vpcC"])[0];if(D&&D.length>6){var _=D[4],P=D[5],C=D[6]>>4&15;a+="."+Te(_)+"."+Te(P)+"."+Te(C)}break;case"av01":var w=ce(i,["av1C"])[0];if(w&&w.length>2){var O=w[1]>>>5,x=31&w[1],M=w[2]>>>7?"H":"M",F=(64&w[2])>>6,N=(32&w[2])>>5,U=2===O&&F?N?12:10:F?10:8,B=(16&w[2])>>4,G=(8&w[2])>>3,K=(4&w[2])>>2,V=3&w[2];a+="."+O+"."+Te(x)+M+"."+Te(U)+"."+B+"."+G+K+V+"."+Te(1)+"."+Te(1)+"."+Te(1)+".0",t=pe("dav1",i)}}return{codec:a,encrypted:s,supplemental:t}}function pe(e,t){var r=ce(t,["dvvC"]),i=r.length?r[0]:ce(t,["dvcC"])[0];if(i){var n=i[2]>>1&127,a=i[2]<<5&32|i[3]>>3&31;return e+"."+Te(n)+"."+Te(a)}}function ye(e,t){for(var r=t+5;128&e[t++]&&t0;a||(n=ce(i,["encv"])),n.forEach((function(e){ce(a?e.subarray(28):e.subarray(78),["sinf"]).forEach((function(e){var r=Ae(e);r&&t(r,a)}))}))}}))}function Ae(e){var t=ce(e,["schm"])[0];if(t){var r=le(t.subarray(4,8));if("cbcs"===r||"cenc"===r){var i=ce(e,["schi","tenc"])[0];if(i)return i}}}function Le(e,t){var r=new Uint8Array(e.length+t.length);return r.set(e),r.set(t,e.length),r}function Ie(e,t){var r=[],i=t.samples,n=t.timescale,a=t.id,s=!1;return ce(i,["moof"]).map((function(o){var l=o.byteOffset-8;ce(o,["traf"]).map((function(o){var u=ce(o,["tfdt"]).map((function(e){var t=e[0],r=de(e,4);return 1===t&&(r*=Math.pow(2,32),r+=de(e,8)),r/n}))[0];return void 0!==u&&(e=u),ce(o,["tfhd"]).map((function(u){var d=de(u,4),h=16777215&de(u,0),f=0,c=0!=(16&h),g=0,v=0!=(32&h),m=8;d===a&&(0!=(1&h)&&(m+=8),0!=(2&h)&&(m+=4),0!=(8&h)&&(f=de(u,m),m+=4),c&&(g=de(u,m),m+=4),v&&(m+=4),"video"===t.type&&(s=Re(t.codec)),ce(o,["trun"]).map((function(a){var o=a[0],u=16777215&de(a,0),d=0!=(1&u),h=0,c=0!=(4&u),v=0!=(256&u),m=0,p=0!=(512&u),y=0,E=0!=(1024&u),T=0!=(2048&u),S=0,A=de(a,4),L=8;d&&(h=de(a,L),L+=4),c&&(L+=4);for(var I=h+l,R=0;R>1&63;return 39===r||40===r}return 6==(31&t)}function be(e,t,r,i){var n=De(e),a=0;a+=t;for(var s=0,o=0,l=0;a=n.length)break;s+=l=n[a++]}while(255===l);o=0;do{if(a>=n.length)break;o+=l=n[a++]}while(255===l);var u=n.length-a,d=a;if(ou){Y.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[d++]){var h=ue(n,d);if(d+=2,49===h){var f=de(n,d);if(d+=4,1195456820===f){var c=n[d++];if(3===c){var g=n[d++],v=64&g,m=v?2+3*(31&g):0,p=new Uint8Array(m);if(v){p[0]=g;for(var y=1;y16){for(var E=[],T=0;T<16;T++){var S=n[d++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var A=o-16,L=new Uint8Array(A),I=0;I0&&new DataView(a.buffer).setUint32(0,r.byteLength,!1),function(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(e,4),s=0,a=8;s>>24;if(0!==n&&1!==n)return{offset:r,size:t};var a=e.buffer,s=X(new Uint8Array(a,r+12,16)),o=null,l=0;if(0===n)l=28;else{var u=e.getUint32(28);if(!u||i<32+16*u)return{offset:r,size:t};o=[];for(var d=0;d4||-1!==["ac-3","ec-3","alac","fLaC","Opus"].indexOf(e))&&(He(e,"audio")||He(e,"video")))return e;if(t){var r=t.split(",");if(r.length>1){if(e)for(var i=r.length;i--;)if(r[i].substring(0,4)===e.substring(0,4))return r[i];return r[0]}}return t||e}function He(e,t){return Oe(e,t)&&Me(e,t)}function Ye(e){if(e.startsWith("av01.")){for(var t=e.split("."),r=["0","111","01","01","01","0"],i=t.length;i>4&&i<10;i++)t[i]=r[i-4];return t.join(".")}return e}function We(e){var t=W(e)||{isTypeSupported:function(){return!1}};return{mpeg:t.isTypeSupported("audio/mpeg"),mp3:t.isTypeSupported('audio/mp4; codecs="mp3"'),ac3:t.isTypeSupported('audio/mp4; codecs="ac-3"')}}function je(e){return e.replace(/^.+codecs=["']?([^"']+).*$/,"$1")}var qe={supported:!1,smooth:!1,powerEfficient:!1},Xe={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]};function Qe(e,t){return{supported:!1,configurations:t,decodingInfoResults:[qe],error:e}}function ze(e,t,r,i){void 0===i&&(i={});var n=e.videoCodec;if(!n&&!e.audioCodec||!r)return Promise.resolve(Xe);for(var a=[],s=function(e){var t,r=null==(t=e.videoCodec)?void 0:t.split(","),i=Ze(e),n=e.width||640,a=e.height||480,s=e.frameRate||30,o=e.videoRange.toLowerCase();return r?r.map((function(e){var t={contentType:Fe(Ye(e),"video"),width:n,height:a,bitrate:i,framerate:s};return"sdr"!==o&&(t.transferFunction=o),t})):[]}(e),o=s.length,l=function(e,t,r){var i,n=null==(i=e.audioCodec)?void 0:i.split(","),a=Ze(e);return n&&e.audioGroups?e.audioGroups.reduce((function(e,i){var s,o=i?null==(s=t.groups[i])?void 0:s.tracks:null;return o?o.reduce((function(e,t){if(t.groupId===i){var s=parseFloat(t.channels||"");n.forEach((function(t){var i={contentType:Fe(t,"audio"),bitrate:r?$e(t,a):a};s&&(i.channels=""+s),e.push(i)}))}return e}),e):e}),[]):[]}(e,t,o>0),u=l.length,d=o||1*u||1;d--;){var h={type:"media-source"};if(o&&(h.video=s[d%o]),u){h.audio=l[d%u];var f=h.audio.bitrate;h.video&&f&&(h.video.bitrate-=f)}a.push(h)}if(n){var c=navigator.userAgent;if(n.split(",").some((function(e){return Re(e)}))&&Ce())return Promise.resolve(Qe(new Error("Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent string: ("+c+")"),a))}return Promise.all(a.map((function(e){var t,n,a,s,o=(n="",a=(t=e).audio,(s=t.video)&&(n+=je(s.contentType)+"_r"+s.height+"x"+s.width+"f"+Math.ceil(s.framerate)+(s.transferFunction||"sd")+"_"+Math.ceil(s.bitrate/1e5)),a&&(n+=(s?"_":"")+je(a.contentType)+"_c"+a.channels),n);return i[o]||(i[o]=r.decodingInfo(e))}))).then((function(e){return{supported:!e.some((function(e){return!e.supported})),configurations:a,decodingInfoResults:e}})).catch((function(e){return{supported:!1,configurations:a,decodingInfoResults:[],error:e}}))}function $e(e,t){if(t<=1)return 1;var r=128e3;return"ec-3"===e?r=768e3:"ac-3"===e&&(r=64e4),Math.min(t/2,r)}function Ze(e){return 1e3*Math.ceil(Math.max(.9*e.bitrate,e.averageBitrate)/1e3)||1}var Je=["NONE","TYPE-0","TYPE-1",null],et=["SDR","PQ","HLG"],tt="",rt="YES",it="v2";function nt(e){var t=e.canSkipUntil,r=e.canSkipDateRanges,i=e.age;return t&&i-1;i--)if(r(e[i]))return i;for(var n=t+1;n-1&&v!==g,p=!!e||m;if(p||!l.paused&&l.playbackRate&&l.readyState){var y=s.mainForwardBufferInfo;if(p||null!==y){var E=r.bwEstimator.getEstimateTTFB(),T=Math.abs(l.playbackRate);if(!(f<=Math.max(E,h/(2*T)*1e3))){var S=y?y.len/T:0,L=d.loading.first?d.loading.first-d.loading.start:-1,I=d.loaded&&L>-1,R=r.getBwEstimate(),k=s.levels,D=k[g],_=Math.max(d.loaded,Math.round(h*(n.bitrate||D.averageBitrate)/8)),P=I?f-L:f;P<1&&I&&(P=Math.min(f,8*d.loaded/R));var C=I?1e3*d.loaded/P:0,w=E/1e3,O=C?(_-d.loaded)/C:8*_/R+w;if(!(O<=S)){var x,M=C?8*C:R,F=!0===(null==(t=(null==e?void 0:e.details)||r.hls.latestLevelDetails)?void 0:t.live),N=r.hls.config.abrBandWidthUpFactor,U=Number.POSITIVE_INFINITY;for(x=g-1;x>c;x--){var B=k[x].maxBitrate,G=!k[x].details||F;if((U=r.getTimeToLoadFrag(w,M,h*B,G))=O||U>10*h)){I?r.bwEstimator.sample(f-Math.min(E,L),d.loaded):r.bwEstimator.sampleTTFB(f);var K=k[x].maxBitrate;r.getBwEstimate()*N>K&&r.resetEstimator(K);var V=r.findBestLevel(K,c,x,0,S,1,1);V>-1&&(x=V),r.warn("Fragment "+n.sn+(a?" part "+a.index:"")+" of level "+g+" is loading too slowly;\n Fragment duration: "+n.duration.toFixed(3)+"\n Time to underbuffer: "+S.toFixed(3)+" s\n Estimated load time for current fragment: "+O.toFixed(3)+" s\n Estimated load time for down switch fragment: "+U.toFixed(3)+" s\n TTFB estimate: "+(0|L)+" ms\n Current BW estimate: "+(A(R)?0|R:"Unknown")+" bps\n New BW estimate: "+(0|r.getBwEstimate())+" bps\n Switching to level "+x+" @ "+(0|K)+" bps"),s.nextLoadLevel=s.nextAutoLevel=x,r.clearTimer();var H=function(){if(r.clearTimer(),r.fragCurrent===n&&r.hls.loadLevel===x&&x>0){var e=r.getStarvationDelay();if(r.warn("Aborting inflight request "+(x>0?"and switching down":"")+"\n Fragment duration: "+n.duration.toFixed(3)+" s\n Time to underbuffer: "+e.toFixed(3)+" s"),n.abortRequests(),r.fragCurrent=r.partCurrent=null,x>c){var t=r.findBestLevel(r.hls.levels[c].bitrate,c,x,0,e,1,1);-1===t&&(t=c),r.hls.nextLoadLevel=r.hls.nextAutoLevel=t,r.resetEstimator(r.hls.levels[t].bitrate)}}};m||O>2*U?H():r.timer=self.setInterval(H,1e3*U),s.trigger(b.FRAG_LOAD_EMERGENCY_ABORTED,{frag:n,part:a,stats:d})}}}}}}}},r.hls=t,r.bwEstimator=r.initEstimator(),r.registerListeners(),r}o(t,e);var r=t.prototype;return r.resetEstimator=function(e){e&&(this.log("setting initial bwe to "+e),this.hls.config.abrEwmaDefaultEstimate=e),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},r.initEstimator=function(){var e=this.hls.config;return new F(e.abrEwmaSlowVoD,e.abrEwmaFastVoD,e.abrEwmaDefaultEstimate)},r.registerListeners=function(){var e=this.hls;e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.FRAG_LOADING,this.onFragLoading,this),e.on(b.FRAG_LOADED,this.onFragLoaded,this),e.on(b.FRAG_BUFFERED,this.onFragBuffered,this),e.on(b.LEVEL_SWITCHING,this.onLevelSwitching,this),e.on(b.LEVEL_LOADED,this.onLevelLoaded,this),e.on(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.on(b.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),e.on(b.ERROR,this.onError,this)},r.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.FRAG_LOADING,this.onFragLoading,this),e.off(b.FRAG_LOADED,this.onFragLoaded,this),e.off(b.FRAG_BUFFERED,this.onFragBuffered,this),e.off(b.LEVEL_SWITCHING,this.onLevelSwitching,this),e.off(b.LEVEL_LOADED,this.onLevelLoaded,this),e.off(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.off(b.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),e.off(b.ERROR,this.onError,this))},r.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=this.supportedCache=null,this.fragCurrent=this.partCurrent=null},r.onManifestLoading=function(e,t){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.supportedCache={},this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},r.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},r.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},r.onFragLoading=function(e,t){var r,i=t.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=t.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},r.onLevelSwitching=function(e,t){this.clearTimer()},r.onError=function(e,t){if(!t.fatal)switch(t.details){case k.BUFFER_ADD_CODEC_ERROR:case k.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case k.FRAG_LOAD_TIMEOUT:var r=t.frag,i=this.fragCurrent,n=this.partCurrent;if(r&&i&&r.sn===i.sn&&r.level===i.level){var a=performance.now(),s=n?n.stats:r.stats,o=a-s.loading.start,l=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&l>-1){var u=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(o-Math.min(u,l),s.loaded)}else this.bwEstimator.sampleTTFB(o)}}},r.getTimeToLoadFrag=function(e,t,r,i){return e+r/t+(i?e+this.lastLevelLoadSec:0)},r.onLevelLoaded=function(e,t){var r=this.hls.config,i=t.stats.loading,n=i.end-i.first;A(n)&&(this.lastLevelLoadSec=n/1e3),t.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD),this.timer>-1&&this._abandonRulesCheck(t.levelInfo)},r.onFragLoaded=function(e,t){var r=t.frag,i=t.part,n=i?i.stats:r.stats;if(r.type===w&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),r.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(b.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},r.onFragBuffered=function(e,t){var r=t.frag,i=t.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.getBwEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},r.ignoreFragment=function(e){return e.type!==w||"initSegment"===e.sn},r.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},r.getAutoLevelKey=function(){return this.getBwEstimate()+"_"+this.getStarvationDelay().toFixed(2)},r.getNextABRAutoLevel=function(){var e=this.fragCurrent,t=this.partCurrent,r=this.hls;if(r.levels.length<=1)return r.loadLevel;var i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=t?t.duration:e?e.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay(),u=n.abrBandWidthFactor,d=n.abrBandWidthUpFactor;if(l){var h=this.findBestLevel(o,a,i,l,0,u,d);if(h>=0)return this.rebufferNotice=-1,h}var f=s?Math.min(s,n.maxStarvationDelay):n.maxStarvationDelay;if(!l){var c=this.bitrateTestDelay;c&&(f=(s?Math.min(s,n.maxLoadingDelay):n.maxLoadingDelay)-c,this.info("bitrate test took "+Math.round(1e3*c)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*f)+" ms"),u=d=1)}var g=this.findBestLevel(o,a,i,l,f,u,d);if(this.rebufferNotice!==g&&(this.rebufferNotice=g,this.info((l?"rebuffering expected":"buffer is empty")+", optimal quality level "+g)),g>-1)return g;var v=r.levels[a],m=r.loadLevelObj;return m&&(null==v?void 0:v.bitrate)0),f=Math.min(f,t.minHeight),c=Math.min(c,t.minFramerate),g=Math.min(g,t.minBitrate),T.filter((function(e){return t.videoRanges[e]>0})).length>0&&(h=!0)},L=a.length;L--;)S();f=A(f)?f:0,c=A(c)?c:0;var I=Math.max(1080,f),R=Math.max(30,c);g=A(g)?g:r,r=Math.max(g,r),h||(t=void 0);var k=a.length>1;return{codecSet:a.reduce((function(t,i){var n=e[i];if(i===t)return t;if(p=h?T.filter((function(e){return n.videoRanges[e]>0})):[],k){if(n.minBitrate>r)return dt(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),t;if(!n.hasDefaultAudio)return dt(i,"no renditions with default or auto-select sound found"),t;if(o&&i.indexOf(o.substring(0,4))%5!=0)return dt(i,'audio codec preference "'+o+'" not found'),t;if(s&&!u){if(!n.channels[s])return dt(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),t}else if((!o||u)&&d&&0===n.channels[2])return dt(i,"no renditions with stereo sound found"),t;if(n.minHeight>I)return dt(i,"min resolution of "+n.minHeight+" > maximum of "+I),t;if(n.minFramerate>R)return dt(i,"min framerate of "+n.minFramerate+" > maximum of "+R),t;if(!p.some((function(e){return n.videoRanges[e]>0})))return dt(i,"no variants with VIDEO-RANGE of "+ut(p)+" found"),t;if(l&&i.indexOf(l.substring(0,4))%5!=0)return dt(i,'video codec preference "'+l+'" not found'),t;if(n.maxScore=Ue(t)||n.fragmentError>e[t].fragmentError)?t:(v=n.minIndex,m=n.maxScore,i)}),void 0),videoRanges:p,preferHDR:E,minFramerate:c,minBitrate:g,minIndex:v}}(P,I,e,k,b),w=C.codecSet,O=C.videoRanges,x=C.minFramerate,M=C.minBitrate,F=C.minIndex,N=C.preferHDR;_=F,E=w,I=N?O[O.length-1]:O[0],R=x,e=Math.max(e,M),this.log("picked start tier "+ut(C))}else E=null==T?void 0:T.codecSet,I=null==T?void 0:T.videoRange;for(var U,B=c?c.duration:f?f.duration:0,G=this.bwEstimator.getEstimateTTFB()/1e3,K=[],V=function(){var t,o=v[H],f=H>h;if(!o)return 0;if(y.useMediaCapabilities&&!o.supportedResult&&!o.supportedPromise){var g=navigator.mediaCapabilities;"function"==typeof(null==g?void 0:g.decodingInfo)&&function(e,t,r,i,n,a){var s=e.videoCodec,o=e.audioCodec?e.audioGroups:null,l=null==a?void 0:a.audioCodec,u=null==a?void 0:a.channels,d=u?parseInt(u):l?1/0:2,h=null;if(null!=o&&o.length)try{h=1===o.length&&o[0]?t.groups[o[0]].channels:o.reduce((function(e,r){if(r){var i=t.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(t){e[t]=(e[t]||0)+i.channels[t]}))}return e}),{2:0})}catch(e){return!0}return void 0!==s&&(s.split(",").some((function(e){return Re(e)}))||e.width>1920&&e.height>1088||e.height>1920&&e.width>1088||e.frameRate>Math.max(i,30)||"SDR"!==e.videoRange&&e.videoRange!==r||e.bitrate>Math.max(n,8e6))||!!h&&A(d)&&Object.keys(h).some((function(e){return parseInt(e)>d}))}(o,D,I,R,e,k)?(o.supportedPromise=ze(o,D,g,l.supportedCache),o.supportedPromise.then((function(e){if(l.hls){o.supportedResult=e;var t=l.hls.levels,r=t.indexOf(o);e.error?l.warn('MediaCapabilities decodingInfo error: "'+e.error+'" for level '+r+" "+ut(e)):e.supported?e.decodingInfoResults.some((function(e){return!1===e.smooth||!1===e.powerEfficient}))&&l.log("MediaCapabilities decodingInfo for level "+r+" not smooth or powerEfficient: "+ut(e)):(l.warn("Unsupported MediaCapabilities decodingInfo result for level "+r+" "+ut(e)),r>-1&&t.length>1&&(l.log("Removing unsupported level "+r),l.hls.removeLevel(r),-1===l.hls.loadLevel&&(l.hls.nextLoadLevel=0)))}})).catch((function(e){l.warn("Error handling MediaCapabilities decodingInfo: "+e)}))):o.supportedResult=Xe}if((E&&o.codecSet!==E||I&&o.videoRange!==I||f&&R>o.frameRate||!f&&R>0&&R=2*B&&0===n?o.averageBitrate:o.maxBitrate,C=l.getTimeToLoadFrag(G,m,P*b,void 0===T);if(m>=P&&(H===d||0===o.loadError&&0===o.fragmentError)&&(C<=G||!A(C)||S&&!l.bitrateTestDelay||C"+H+" adjustedbw("+Math.round(m)+")-bitrate="+Math.round(m-P)+" ttfb:"+G.toFixed(1)+" avgDuration:"+b.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+C.toFixed(1)+" firstSelection:"+L+" codecSet:"+o.codecSet+" videoRange:"+o.videoRange+" hls.loadLevel:"+p)),L&&(l.firstSelection=H),{v:H}}},H=r;H>=t;H--)if(0!==(U=V())&&U)return U.v;return-1},r.deriveNextAutoLevel=function(e){var t=this.hls,r=t.maxAutoLevel,i=t.minAutoLevel;return Math.min(Math.max(e,i),r)},i(t,[{key:"firstAutoLevel",get:function(){var e=this.hls,t=e.maxAutoLevel,r=e.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,t,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),t);return this.warn("Could not find best starting auto level. Defaulting to first in playlist "+s+" clamped to "+o),o}},{key:"forcedAutoLevel",get:function(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}},{key:"nextAutoLevel",get:function(){var e=this.forcedAutoLevel,t=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===e||t&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return e;var i=t&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==e){var n=this.hls.levels;if(n.length>Math.max(e,i)&&n[e].loadError<=n[i].loadError)return e}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(e){var t=this.deriveNextAutoLevel(e);this._nextAutoLevel!==t&&(this.nextAutoLevelKey="",this._nextAutoLevel=t)}}])}(N),Et=function(e,t){for(var r=0,i=e.length-1,n=null,a=null;r<=i;){var s=t(a=e[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function Tt(e,t,r,i,n){void 0===r&&(r=0),void 0===i&&(i=0),void 0===n&&(n=.005);var a=null;if(e){a=t[1+e.sn-t[0].sn]||null;var s=e.endDTS-r;s>0&&s<15e-7&&(r+=15e-7),a&&e.level!==a.level&&a.end<=e.end&&(a=t[2+e.sn-t[0].sn]||null)}else 0===r&&0===t[0].start&&(a=t[0]);if(a&&((!e||e.level===a.level)&&0===St(r,i,a)||function(e,t,r){if(t&&0===t.start&&t.level0){var i=t.tagList.reduce((function(e,t){return"INF"===t[0]&&(e+=parseFloat(t[1])),e}),r);return e.start<=i}return!1}(a,e,Math.min(n,i))))return a;var o=Et(t,St.bind(null,r,i));return!o||o===e&&a?a:o}function St(e,t,r){if(void 0===e&&(e=0),void 0===t&&(t=0),r.start<=e&&r.start+r.duration>e)return 0;var i=Math.min(t,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=e?1:r.start-i>e&&r.start?-1:0}function At(e,t,r){var i=1e3*Math.min(t,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>e}function Lt(e,t,r){if(e&&e.startCC<=t&&e.endCC>=t){var i,n=e.fragments,a=e.fragmentHint;return a&&(n=n.concat(a)),Et(n,(function(e){return e.cct?-1:(i=e,e.end<=r?1:e.start>r?-1:0)})),i||null}return null}function It(e){switch(e.details){case k.FRAG_LOAD_TIMEOUT:case k.KEY_LOAD_TIMEOUT:case k.LEVEL_LOAD_TIMEOUT:case k.MANIFEST_LOAD_TIMEOUT:return!0}return!1}function Rt(e){return e.details.startsWith("key")}function kt(e){return Rt(e)&&!!e.frag&&!e.frag.decryptdata}function bt(e,t){var r=It(t);return e.default[(r?"timeout":"error")+"Retry"]}function Dt(e,t){var r="linear"===e.backoff?1:Math.pow(2,t);return Math.min(r*e.retryDelayMs,e.maxRetryDelayMs)}function _t(e){return d(d({},e),{errorRetry:null,timeoutRetry:null})}function Pt(e,t,r,i){if(!e)return!1;var n=null==i?void 0:i.code,a=t499)}(n)||!!r);return e.shouldRetry?e.shouldRetry(e,t,r,i,a):a}function Ct(e){return 0===e&&!1===navigator.onLine}var wt=0,Ot=2,xt=3,Mt=5,Ft=0,Nt=1,Ut=2,Bt=4,Gt=function(e){function t(t){var r;return(r=e.call(this,"error-controller",t.logger)||this).hls=void 0,r.playlistError=0,r.hls=t,r.registerListeners(),r}o(t,e);var r=t.prototype;return r.registerListeners=function(){var e=this.hls;e.on(b.ERROR,this.onError,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.LEVEL_UPDATED,this.onLevelUpdated,this)},r.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.ERROR,this.onError,this),e.off(b.ERROR,this.onErrorOut,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.LEVEL_UPDATED,this.onLevelUpdated,this))},r.destroy=function(){this.unregisterListeners(),this.hls=null},r.startLoad=function(e){},r.stopLoad=function(){this.playlistError=0},r.getVariantLevelIndex=function(e){return(null==e?void 0:e.type)===w?e.level:this.getVariantIndex()},r.getVariantIndex=function(){var e,t=this.hls,r=t.currentLevel;return null!=(e=t.loadLevelObj)&&e.details||-1===r?t.loadLevel:r},r.variantHasKey=function(e,t){if(e){var r;if(null!=(r=e.details)&&r.hasKey(t))return!0;var i=e.audioGroups;if(i)return this.hls.allAudioTracks.filter((function(e){return i.indexOf(e.groupId)>=0})).some((function(e){var r;return null==(r=e.details)?void 0:r.hasKey(t)}))}return!1},r.onManifestLoading=function(){this.playlistError=0},r.onLevelUpdated=function(){this.playlistError=0},r.onError=function(e,t){var r;if(!t.fatal){var i=this.hls,n=t.context;switch(t.details){case k.FRAG_LOAD_ERROR:case k.FRAG_LOAD_TIMEOUT:case k.KEY_LOAD_ERROR:case k.KEY_LOAD_TIMEOUT:return void(t.errorAction=this.getFragRetryOrSwitchAction(t));case k.FRAG_PARSING_ERROR:if(null!=(r=t.frag)&&r.gap)return void(t.errorAction=Kt());case k.FRAG_GAP:case k.FRAG_DECRYPT_ERROR:return t.errorAction=this.getFragRetryOrSwitchAction(t),void(t.errorAction.action=Ot);case k.LEVEL_EMPTY_ERROR:case k.LEVEL_PARSING_ERROR:var a,s=t.parent===w?t.level:i.loadLevel;return void(t.details===k.LEVEL_EMPTY_ERROR&&null!=(a=t.context)&&null!=(a=a.levelDetails)&&a.live?t.errorAction=this.getPlaylistRetryOrSwitchAction(t,s):(t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,s)));case k.LEVEL_LOAD_ERROR:case k.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==n?void 0:n.level)&&(t.errorAction=this.getPlaylistRetryOrSwitchAction(t,n.level)));case k.AUDIO_TRACK_LOAD_ERROR:case k.AUDIO_TRACK_LOAD_TIMEOUT:case k.SUBTITLE_LOAD_ERROR:case k.SUBTITLE_TRACK_LOAD_TIMEOUT:if(n){var o=i.loadLevelObj;if(o&&(n.type===P&&o.hasAudioGroup(n.groupId)||n.type===C&&o.hasSubtitleGroup(n.groupId)))return t.errorAction=this.getPlaylistRetryOrSwitchAction(t,i.loadLevel),t.errorAction.action=Ot,void(t.errorAction.flags=Nt)}return;case k.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:return void(t.errorAction={action:Ot,flags:Ut});case k.KEY_SYSTEM_SESSION_UPDATE_FAILED:case k.KEY_SYSTEM_STATUS_INTERNAL_ERROR:case k.KEY_SYSTEM_NO_SESSION:return void(t.errorAction={action:Ot,flags:Bt});case k.BUFFER_ADD_CODEC_ERROR:case k.REMUX_ALLOC_ERROR:case k.BUFFER_APPEND_ERROR:var l;return void(t.errorAction||(t.errorAction=this.getLevelSwitchAction(t,null!=(l=t.level)?l:i.loadLevel)));case k.INTERNAL_EXCEPTION:case k.BUFFER_APPENDING_ERROR:case k.BUFFER_FULL_ERROR:case k.LEVEL_SWITCH_ERROR:case k.BUFFER_STALLED_ERROR:case k.BUFFER_SEEK_OVER_HOLE:case k.BUFFER_NUDGE_ON_STALL:return void(t.errorAction=Kt())}t.type===R.KEY_SYSTEM_ERROR&&(t.levelRetry=!1,t.errorAction=Kt())}},r.getPlaylistRetryOrSwitchAction=function(e,t){var r=bt(this.hls.config.playlistLoadPolicy,e),i=this.playlistError++;if(Pt(r,i,It(e),e.response))return{action:Mt,flags:Ft,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(e,t);return r&&(n.retryConfig=r,n.retryCount=i),n},r.getFragRetryOrSwitchAction=function(e){var t=this.hls,r=this.getVariantLevelIndex(e.frag),i=t.levels[r],n=t.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=bt(Rt(e)?s:a,e),l=t.levels.reduce((function(e,t){return e+t.fragmentError}),0);if(i&&(e.details!==k.FRAG_GAP&&i.fragmentError++,!kt(e)&&Pt(o,l,It(e),e.response)))return{action:Mt,flags:Ft,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(e,r);return o&&(u.retryConfig=o,u.retryCount=l),u},r.getLevelSwitchAction=function(e,t){var r=this.hls;null==t&&(t=r.loadLevel);var i=this.hls.levels[t];if(i){var n,a,s=e.details;i.loadError++,s===k.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,d=r.minAutoLevel,h=r.maxAutoLevel;r.autoLevelEnabled||r.config.preserveManualLevelOnError||(r.loadLevel=-1);for(var f,c=null==(n=e.frag)?void 0:n.type,g=(c===O&&s===k.FRAG_PARSING_ERROR||"audio"===e.sourceBufferName&&(s===k.BUFFER_ADD_CODEC_ERROR||s===k.BUFFER_APPEND_ERROR))&&l.some((function(e){var t=e.audioCodec;return i.audioCodec!==t})),v="video"===e.sourceBufferName&&(s===k.BUFFER_ADD_CODEC_ERROR||s===k.BUFFER_APPEND_ERROR)&&l.some((function(e){var t=e.codecSet,r=e.audioCodec;return i.codecSet!==t&&i.audioCodec===r})),m=null!=(a=e.context)?a:{},p=m.type,y=m.groupId,E=function(){var t=(T+u)%l.length;if(t!==u&&t>=d&&t<=h&&0===l[t].loadError){var r,n,a=l[t];if(s===k.FRAG_GAP&&c===w&&e.frag){var f=l[t].details;if(f){var m=Tt(e.frag,f.fragments,e.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===P&&a.hasAudioGroup(y)||p===C&&a.hasSubtitleGroup(y))return 0;if(c===O&&null!=(r=i.audioGroups)&&r.some((function(e){return a.hasAudioGroup(e)}))||c===x&&null!=(n=i.subtitleGroups)&&n.some((function(e){return a.hasSubtitleGroup(e)}))||g&&i.audioCodec===a.audioCodec||v&&i.codecSet===a.codecSet||!g&&i.codecSet!==a.codecSet)return 0}return o=t,1}},T=l.length;T--&&(0===(f=E())||1!==f););if(o>-1&&r.loadLevel!==o)return e.levelRetry=!0,this.playlistError=0,{action:Ot,flags:Ft,nextAutoLevel:o}}return{action:Ot,flags:Nt}},r.onErrorOut=function(e,t){var r;switch(null==(r=t.errorAction)?void 0:r.action){case wt:break;case Ot:this.sendAlternateToPenaltyBox(t),t.errorAction.resolved||t.details===k.FRAG_GAP?/MediaSource readyState: ended/.test(t.error.message)&&(this.warn('MediaSource ended after "'+t.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):t.fatal=!0}t.fatal&&this.hls.stopLoad()},r.sendAlternateToPenaltyBox=function(e){var t=this.hls,r=e.errorAction;if(r){var i=r.flags,n=r.nextAutoLevel;switch(i){case Ft:this.switchLevel(e,n);break;case Ut:var a=this.getVariantLevelIndex(e.frag),s=t.levels[a],o=null==s?void 0:s.attrs["HDCP-LEVEL"];if(r.hdcpLevel=o,"NONE"===o)this.warn("HDCP policy resticted output with HDCP-LEVEL=NONE");else if(o){t.maxHdcpLevel=Je[Je.indexOf(o)-1],r.resolved=!0,this.warn('Restricting playback to HDCP-LEVEL of "'+t.maxHdcpLevel+'" or lower');break}case Bt:var l=e.decryptdata;if(l){for(var u=this.hls.levels,d=u.length,h=d;h--;){var f,c;this.variantHasKey(u[h],l)&&(this.log("Banned key found in level "+h+" ("+u[h].bitrate+'bps) or audio group "'+(null==(f=u[h].audioGroups)?void 0:f.join(","))+'" ('+(null==(c=e.frag)?void 0:c.type)+" fragment) "+X(l.keyId||[])),u[h].fragmentError++,u[h].loadError++,this.log("Removing level "+h+" with key error ("+e.error+")"),this.hls.removeLevel(h))}var g=e.frag;if(this.hls.levels.length=o.body.sn))if(o.buffered||o.loaded&&!n){var l=o.range[e];l&&(0!==l.time.length?l.time.some((function(e){var r=!a.isTimeBuffered(e.startPTS,e.endPTS,t);return r&&a.removeFragment(o.body),r})):a.removeFragment(o.body))}else o.body.type===r&&a.removeFragment(o.body)}))},t.detectPartialFragments=function(e){var t=this,r=this.timeRanges;if(r&&"initSegment"!==e.frag.sn){var i=e.frag,n=Xt(i),a=this.fragments[n];if(!(!a||a.buffered&&i.gap)){var s=!i.relurl;Object.keys(r).forEach((function(n){var o=i.elementaryStreams[n];if(o){var l=r[n],u=s||!0===o.partial;a.range[n]=t.getBufferedTimes(i,e.part,u,l)}})),a.loaded=null,Object.keys(a.range).length?(this.bufferedEnd(a,i),qt(a)||this.removeParts(i.sn-1,i.type)):this.removeFragment(a.body)}}},t.bufferedEnd=function(e,t){e.buffered=!0,(e.body.endList=t.endList||e.body.endList)&&(this.endListFragments[e.body.type]=e)},t.removeParts=function(e,t){var r=this.activePartLists[t];r&&(this.activePartLists[t]=Qt(r,(function(t){return t.fragment.sn>=e})))},t.fragBuffered=function(e,t){var r=Xt(e),i=this.fragments[r];!i&&t&&(i=this.fragments[r]={body:e,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},e.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,this.bufferedEnd(i,e))},t.getBufferedTimes=function(e,t,r,i){for(var n={time:[],partial:r},a=e.start,s=e.end,o=e.minEndPTS||s,l=e.maxStartPTS||a,u=0;u=d&&o<=h){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ad){var f=Math.max(a,i.start(u)),c=Math.min(s,i.end(u));c>f&&(n.partial=!0,n.time.push({startPTS:f,endPTS:c}))}else if(s<=d)break}return n},t.getPartialFragment=function(e){var t,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&qt(u)&&(r=u.body.start-s,i=u.body.end+s,e>=r&&e<=i&&(t=Math.min(e-r,i-e),a<=t&&(n=u.body,a=t)))})),n},t.isEndListAppended=function(e){var t=this.endListFragments[e];return void 0!==t&&(t.buffered||qt(t))},t.getState=function(e){var t=Xt(e),r=this.fragments[t];return r?r.buffered?qt(r)?Yt:Wt:Ht:Vt},t.isTimeBuffered=function(e,t,r){for(var i,n,a=0;a=i&&t<=n)return!0;if(t<=i)return!1}return!1},t.onManifestLoading=function(){this.removeAllFragments()},t.onFragLoaded=function(e,t){if("initSegment"!==t.frag.sn&&!t.frag.bitrateTest){var r=t.frag,i=t.part?null:t,n=Xt(r);this.fragments[n]={body:r,appendedPTS:null,loaded:i,buffered:!1,range:Object.create(null)}}},t.onBufferAppended=function(e,t){var r=t.frag,i=t.part,n=t.timeRanges,a=t.type;if("initSegment"!==r.sn){var s=r.type;if(i){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(i)}this.timeRanges=n;var l=n[a];this.detectEvictedFragments(a,l,s,i)}},t.onFragBuffered=function(e,t){this.detectPartialFragments(t)},t.hasFragment=function(e){var t=Xt(e);return!!this.fragments[t]},t.hasFragments=function(e){var t=this.fragments,r=Object.keys(t);if(!e)return r.length>0;for(var i=r.length;i--;){var n=t[r[i]];if((null==n?void 0:n.body.type)===e)return!0}return!1},t.hasParts=function(e){var t;return!(null==(t=this.activePartLists[e])||!t.length)},t.removeFragmentsInRange=function(e,t,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.starte&&(o.buffered||n)&&a.removeFragment(l)}}))},t.removeFragment=function(e){var t=Xt(e);e.clearElementaryStreamInfo();var r=this.activePartLists[e.type];if(r){var i=e.sn;this.activePartLists[e.type]=Qt(r,(function(e){return e.fragment.sn!==i}))}delete this.fragments[t],e.endList&&delete this.endListFragments[e.type]},t.removeAllFragments=function(){var e;this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1;var t=null==(e=this.hls)||null==(e=e.latestLevelDetails)?void 0:e.partList;t&&t.forEach((function(e){return e.clearElementaryStreamInfo()}))},e}();function qt(e){var t,r,i;return e.buffered&&!!(e.body.gap||null!=(t=e.range.video)&&t.partial||null!=(r=e.range.audio)&&r.partial||null!=(i=e.range.audiovideo)&&i.partial)}function Xt(e){return e.type+"_"+e.level+"_"+e.sn}function Qt(e,t){return e.filter((function(e){var r=t(e);return r||e.clearElementaryStreamInfo(),r}))}var zt=0,$t=1,Zt=function(){function e(e,t,r){this.subtle=void 0,this.aesIV=void 0,this.aesMode=void 0,this.subtle=e,this.aesIV=t,this.aesMode=r}return e.prototype.decrypt=function(e,t){switch(this.aesMode){case zt:return this.subtle.decrypt({name:"AES-CBC",iv:this.aesIV},t,e);case $t:return this.subtle.decrypt({name:"AES-CTR",counter:this.aesIV,length:64},t,e);default:throw new Error("[AESCrypto] invalid aes mode "+this.aesMode)}},e}(),Jt=function(){function e(){this.rcon=[0,1,2,4,8,16,32,64,128,27,54],this.subMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.invSubMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.sBox=new Uint32Array(256),this.invSBox=new Uint32Array(256),this.key=new Uint32Array(0),this.ksRows=0,this.keySize=0,this.keySchedule=void 0,this.invKeySchedule=void 0,this.initTable()}var t=e.prototype;return t.uint8ArrayToUint32Array_=function(e){for(var t=new DataView(e),r=new Uint32Array(4),i=0;i<4;i++)r[i]=t.getUint32(4*i);return r},t.initTable=function(){var e=this.sBox,t=this.invSBox,r=this.subMix,i=r[0],n=r[1],a=r[2],s=r[3],o=this.invSubMix,l=o[0],u=o[1],d=o[2],h=o[3],f=new Uint32Array(256),c=0,g=0,v=0;for(v=0;v<256;v++)f[v]=v<128?v<<1:v<<1^283;for(v=0;v<256;v++){var m=g^g<<1^g<<2^g<<3^g<<4;m=m>>>8^255&m^99,e[c]=m,t[m]=c;var p=f[c],y=f[p],E=f[y],T=257*f[m]^16843008*m;i[c]=T<<24|T>>>8,n[c]=T<<16|T>>>16,a[c]=T<<8|T>>>24,s[c]=T,T=16843009*E^65537*y^257*p^16843008*c,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,d[m]=T<<8|T>>>24,h[m]=T,c?(c=p^f[f[f[E^p]]],g^=f[f[g]]):c=g=1}},t.expandKey=function(e){for(var t=this.uint8ArrayToUint32Array_(e),r=!0,i=0;i1&&this.tickImmediate(),this._tickCallCount=0)},r.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},r.doTick=function(){},t}(N),lr=function(e,t,r,i,n,a){void 0===i&&(i=0),void 0===n&&(n=-1),void 0===a&&(a=!1),this.level=void 0,this.sn=void 0,this.part=void 0,this.id=void 0,this.size=void 0,this.partial=void 0,this.transmuxing={start:0,executeStart:0,executeEnd:0,end:0},this.buffering={audio:{start:0,executeStart:0,executeEnd:0,end:0},video:{start:0,executeStart:0,executeEnd:0,end:0},audiovideo:{start:0,executeStart:0,executeEnd:0,end:0}},this.level=e,this.sn=t,this.id=r,this.size=i,this.part=n,this.partial=a},ur={length:0,start:function(){return 0},end:function(){return 0}},dr=function(){function e(){}return e.isBuffered=function(t,r){if(t)for(var i=e.getBuffered(t),n=i.length;n--;)if(r>=i.start(n)&&r<=i.end(n))return!0;return!1},e.bufferedRanges=function(t){if(t){var r=e.getBuffered(t);return e.timeRangesToArray(r)}return[]},e.timeRangesToArray=function(e){for(var t=[],r=0;r1&&e.sort((function(e,t){return e.start-t.start||t.end-e.end}));var i=-1,n=[];if(r)for(var a=0;a=e[a].start&&t<=e[a].end&&(i=a);var s=n.length;if(s){var o=n[s-1].end;e[a].start-oo&&(n[s-1].end=e[a].end):n.push(e[a])}else n.push(e[a])}else n=e;for(var l,u=0,d=t,h=t,f=0;f=c&&t<=g&&(i=f),t+r>=c&&tNumber.MAX_SAFE_INTEGER?1/0:t},t.hexadecimalInteger=function(e){if(this[e]){var t=(this[e]||"0x").slice(2);t=(1&t.length?"0":"")+t;for(var r=new Uint8Array(t.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:t},t.decimalFloatingPoint=function(e){return parseFloat(this[e])},t.optionalFloat=function(e,t){var r=this[e];return r?parseFloat(r):t},t.enumeratedString=function(e){return this[e]},t.enumeratedStringList=function(e,t){var r=this[e];return(r?r.split(/[ ,]+/):[]).reduce((function(e,t){return e[t.toLowerCase()]=!0,e}),t)},t.bool=function(e){return"YES"===this[e]},t.decimalResolution=function(e){var t=mr.exec(this[e]);if(null!==t)return{width:parseInt(t[1],10),height:parseInt(t[2],10)}},e.parseAttrList=function(e,t){var r,i={};for(pr.lastIndex=0;null!==(r=pr.exec(e));){var n=r[1].trim(),a=r[2],s=0===a.indexOf('"')&&a.lastIndexOf('"')===a.length-1,o=!1;if(s)a=a.slice(1,-1);else switch(n){case"IV":case"SCTE35-CMD":case"SCTE35-IN":case"SCTE35-OUT":o=!0}if(t&&(s||o))a=cr(t,a);else if(!o&&!s)switch(n){case"CLOSED-CAPTIONS":if("NONE"===a)break;case"ALLOWED-CPC":case"CLASS":case"ASSOC-LANGUAGE":case"AUDIO":case"BYTERANGE":case"CHANNELS":case"CHARACTERISTICS":case"CODECS":case"DATA-ID":case"END-DATE":case"GROUP-ID":case"ID":case"IMPORT":case"INSTREAM-ID":case"KEYFORMAT":case"KEYFORMATVERSIONS":case"LANGUAGE":case"NAME":case"PATHWAY-ID":case"QUERYPARAM":case"RECENTLY-REMOVED-DATERANGES":case"SERVER-URI":case"STABLE-RENDITION-ID":case"STABLE-VARIANT-ID":case"START-DATE":case"SUBTITLES":case"SUPPLEMENTAL-CODECS":case"URI":case"VALUE":case"VIDEO":case"X-ASSET-LIST":case"X-ASSET-URI":Y.warn(e+": attribute "+n+" is missing quotes")}i[n]=a}return i},i(e,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(e){return"X-"===e.substring(0,2)}))}}])}();function Er(e){return"SCTE35-OUT"===e||"SCTE35-IN"===e||"SCTE35-CMD"===e}var Tr=function(){return i((function(e,t,r){var i;if(void 0===r&&(r=0),this.attr=void 0,this.tagAnchor=void 0,this.tagOrder=void 0,this._startDate=void 0,this._endDate=void 0,this._dateAtEnd=void 0,this._cue=void 0,this._badValueForSameId=void 0,this.tagAnchor=(null==t?void 0:t.tagAnchor)||null,this.tagOrder=null!=(i=null==t?void 0:t.tagOrder)?i:r,t){var n=t.attr;for(var s in n)if(Object.prototype.hasOwnProperty.call(e,s)&&e[s]!==n[s]){Y.warn('DATERANGE tag attribute: "'+s+'" does not match for tags with ID: "'+e.ID+'"'),this._badValueForSameId=s;break}e=a(new yr({}),n,e)}if(this.attr=e,t?(this._startDate=t._startDate,this._cue=t._cue,this._endDate=t._endDate,this._dateAtEnd=t._dateAtEnd):this._startDate=new Date(e["START-DATE"]),"END-DATE"in this.attr){var o=(null==t?void 0:t.endDate)||new Date(this.attr["END-DATE"]);A(o.getTime())&&(this._endDate=o)}}),[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"cue",get:function(){var e=this._cue;return void 0===e?this._cue=this.attr.enumeratedStringList(this.attr.CUE?"CUE":"X-CUE",{pre:!1,post:!1,once:!1}):e}},{key:"startTime",get:function(){var e=this.tagAnchor;return null===e||null===e.programDateTime?(Y.warn('Expected tagAnchor Fragment with PDT set for DateRange "'+this.id+'": '+e),NaN):e.start+(this.startDate.getTime()-e.programDateTime)/1e3}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){var e=this._endDate||this._dateAtEnd;if(e)return e;var t=this.duration;return null!==t?this._dateAtEnd=new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var e=this.attr.decimalFloatingPoint("DURATION");if(A(e))return e}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isInterstitial",get:function(){return"com.apple.hls.interstitial"===this.class}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&A(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)&&(!this.attr.CUE||!this.cue.pre&&!this.cue.post||this.cue.pre!==this.cue.post)&&(!this.isInterstitial||"X-ASSET-URI"in this.attr||"X-ASSET-LIST"in this.attr)}}])}(),Sr=function(){function e(e){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.dateRangeTagCount=0,this.live=!0,this.requestScheduled=-1,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.appliedTimelineOffset=void 0,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=e}var t=e.prototype;return t.reloaded=function(e){if(!e)return this.advanced=!0,void(this.updated=!0);var t=this.lastPartSn-e.lastPartSn,r=this.lastPartIndex-e.lastPartIndex;this.updated=this.endSN!==e.endSN||!!r||!!t||!this.live,this.advanced=this.endSN>e.endSN||t>0||0===t&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*e.misses):this.misses=e.misses+1},t.hasKey=function(e){return this.encryptedFragments.some((function(t){var r=t.decryptdata;return r||(t.setKeyFormat(e.keyFormat),r=t.decryptdata),!!r&&e.matches(r)}))},i(e,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&A(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var e=this.driftEndTime-this.driftStartTime;return e>0?1e3*(this.driftEnd-this.driftStart)/e:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var e;return null!=(e=this.partList)&&e.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){return this.fragments.length?this.fragments[this.fragments.length-1].end:0}},{key:"fragmentStart",get:function(){return this.fragments.length?this.fragments[0].start:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var e;return null!=(e=this.partList)&&e.length?this.partList[this.partList.length-1].index:-1}},{key:"maxPartIndex",get:function(){var e=this.partList;if(e){var t=this.lastPartIndex;if(-1!==t){for(var r=e.length;r--;)if(e[r].index>t)return e[r].index;return t}}return 0}},{key:"lastPartSn",get:function(){var e;return null!=(e=this.partList)&&e.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}},{key:"expired",get:function(){if(this.live&&this.age&&this.misses<3){var e=this.partEnd-this.fragmentStart;return this.age>Math.max(e,this.totalduration)+this.levelTargetDuration}return!1}}])}();function Ar(e,t){return e.length===t.length&&!e.some((function(e,r){return e!==t[r]}))}function Lr(e,t){return!e&&!t||!(!e||!t)&&Ar(e,t)}function Ir(e){return"AES-128"===e||"AES-256"===e||"AES-256-CTR"===e}function Rr(e){switch(e){case"AES-128":case"AES-256":return zt;case"AES-256-CTR":return $t;default:throw new Error("invalid full segment method "+e)}}function kr(e){return Uint8Array.from(atob(e),(function(e){return e.charCodeAt(0)}))}function br(e){return Uint8Array.from(unescape(encodeURIComponent(e)),(function(e){return e.charCodeAt(0)}))}function Dr(e){var t=function(e,t,r){var i=e[t];e[t]=e[r],e[r]=i};t(e,0,3),t(e,1,2),t(e,4,5),t(e,6,7)}function _r(e){var t,r,i=e.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=kr(l)):(t=br(l).subarray(0,16),(r=new Uint8Array(16)).set(t,16-t.length),n=r)}}return n}var Pr="undefined"!=typeof self?self:void 0,Cr={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},wr="org.w3.clearkey",Or="com.apple.streamingkeydelivery",xr="com.microsoft.playready",Mr="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function Fr(e){switch(e){case Or:return Cr.FAIRPLAY;case xr:return Cr.PLAYREADY;case Mr:return Cr.WIDEVINE;case wr:return Cr.CLEARKEY}}function Nr(e){switch(e){case Cr.FAIRPLAY:return Or;case Cr.PLAYREADY:return xr;case Cr.WIDEVINE:return Mr;case Cr.CLEARKEY:return wr}}function Ur(e){var t=e.drmSystems,r=e.widevineLicenseUrl,i=t?[Cr.FAIRPLAY,Cr.WIDEVINE,Cr.PLAYREADY,Cr.CLEARKEY].filter((function(e){return!!t[e]})):[];return!i[Cr.WIDEVINE]&&r&&i.push(Cr.WIDEVINE),i}var Br,Gr=null!=Pr&&null!=(Br=Pr.navigator)&&Br.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function Kr(e){var t=new Uint16Array(e.buffer,e.byteOffset,e.byteLength/2),r=String.fromCharCode.apply(null,Array.from(t)),i=r.substring(r.indexOf("<"),r.length),n=(new DOMParser).parseFromString(i,"text/xml").getElementsByTagName("KID")[0];if(n){var a=n.childNodes[0]?n.childNodes[0].nodeValue:n.getAttribute("VALUE");if(a){var s=kr(a).subarray(0,16);return Dr(s),s}}return null}var Vr={},Hr=function(){function e(e,t,r,i,n,a){void 0===i&&(i=[1]),void 0===n&&(n=null),this.uri=void 0,this.method=void 0,this.keyFormat=void 0,this.keyFormatVersions=void 0,this.encrypted=void 0,this.isCommonEncryption=void 0,this.iv=null,this.key=null,this.keyId=null,this.pssh=null,this.method=e,this.uri=t,this.keyFormat=r,this.keyFormatVersions=i,this.iv=n,this.encrypted=!!e&&"NONE"!==e,this.isCommonEncryption=this.encrypted&&!Ir(e),null!=a&&a.startsWith("0x")&&(this.keyId=new Uint8Array(Q(a)))}e.clearKeyUriToKeyIdMap=function(){Vr={}},e.setKeyIdForUri=function(e,t){Vr[e]=t},e.addKeyIdForUri=function(e){var t=Object.keys(Vr).length%Number.MAX_SAFE_INTEGER,r=new Uint8Array(16);return new DataView(r.buffer,12,4).setUint32(0,t),Vr[e]=r,r};var t=e.prototype;return t.matches=function(e){return e.uri===this.uri&&e.method===this.method&&e.encrypted===this.encrypted&&e.keyFormat===this.keyFormat&&Ar(e.keyFormatVersions,this.keyFormatVersions)&&Lr(e.iv,this.iv)&&Lr(e.keyId,this.keyId)},t.isSupported=function(){if(this.method){if(Ir(this.method)||"NONE"===this.method)return!0;if("identity"===this.keyFormat)return"SAMPLE-AES"===this.method;switch(this.keyFormat){case Or:case Mr:case xr:case wr:return-1!==["SAMPLE-AES","SAMPLE-AES-CENC","SAMPLE-AES-CTR"].indexOf(this.method)}}return!1},t.getDecryptData=function(t,r){if(!this.encrypted||!this.uri)return null;if(Ir(this.method)){var i=this.iv;return i||("number"!=typeof t&&(Y.warn('missing IV for initialization segment with method="'+this.method+'" - compliance issue'),t=0),i=function(e){for(var t=new Uint8Array(16),r=12;r<16;r++)t[r]=e>>8*(15-r)&255;return t}(t)),new e(this.method,this.uri,"identity",this.keyFormatVersions,i)}if(this.keyId){var n=Vr[this.uri];if(n&&!Ar(this.keyId,n)&&e.setKeyIdForUri(this.uri,this.keyId),this.pssh)return this}var a,s=_r(this.uri);if(s)switch(this.keyFormat){case Mr:if(this.pssh=s,!this.keyId){var o=function(e){var t=[];if(e instanceof ArrayBuffer)for(var r=e.byteLength,i=0;i+320&&a.length0&&oi(c,C,l),p=c.startSN=parseInt(w);break;case"SKIP":c.skippedSegments&&si(c,C,l);var x=new yr(w,c),M=x.decimalInteger("SKIPPED-SEGMENTS");if(A(M)){c.skippedSegments+=M;for(var F=M;F--;)g.push(null);p+=M}var N=x.enumeratedString("RECENTLY-REMOVED-DATERANGES");N&&(c.recentlyRemovedDateranges=(c.recentlyRemovedDateranges||[]).concat(N.split("\t")));break;case"TARGETDURATION":0!==c.targetduration&&si(c,C,l),c.targetduration=Math.max(parseInt(w),1);break;case"VERSION":null!==c.version&&si(c,C,l),c.version=parseInt(w);break;case"INDEPENDENT-SEGMENTS":break;case"ENDLIST":c.live||si(c,C,l),c.live=!1;break;case"#":(w||O)&&I.tagList.push(O?[w,O]:[w]);break;case"DISCONTINUITY":T++,I.tagList.push(["DIS"]);break;case"GAP":I.gap=!0,I.tagList.push([C]);break;case"BITRATE":I.tagList.push([C,w]),S=1e3*parseInt(w),A(S)?I.bitrate=S:S=0;break;case"DATERANGE":var U=new yr(w,c),B=new Tr(U,c.dateRanges[U.ID],c.dateRangeTagCount);c.dateRangeTagCount++,B.isValid||c.skippedSegments?c.dateRanges[B.id]=B:Y.warn('Ignoring invalid DATERANGE tag: "'+w+'"'),I.tagList.push(["EXT-X-DATERANGE",w]);break;case"DEFINE":var G=new yr(w,c);"IMPORT"in G?vr(c,G,s):gr(c,G,t);break;case"DISCONTINUITY-SEQUENCE":0!==c.startCC?si(c,C,l):g.length>0&&oi(c,C,l),c.startCC=T=parseInt(w);break;case"KEY":var K=Jr(w,t,c);if(K.isSupported()){if("NONE"===K.method){d=void 0;break}d||(d={});var V=d[K.keyFormat];null!=V&&V.matches(K)||(V&&(d=a({},d)),d[K.keyFormat]=K)}else Y.warn('[Keys] Ignoring unsupported EXT-X-KEY tag: "'+w+'"');break;case"START":c.startTimeOffset=ei(w);break;case"MAP":var H=new yr(w,c);if(I.duration){var W=new re(i,f);ni(W,H,r,d),m=W,I.initSegment=m,m.rawProgramDateTime&&!I.rawProgramDateTime&&(I.rawProgramDateTime=m.rawProgramDateTime)}else{var j=I.byteRangeEndOffset;if(j){var q=I.byteRangeStartOffset;b=j-q+"@"+q}else b=null;ni(I,H,r,d),m=I,k=!0}m.cc=T;break;case"SERVER-CONTROL":h&&si(c,C,l),h=new yr(w),c.canBlockReload=h.bool("CAN-BLOCK-RELOAD"),c.canSkipUntil=h.optionalFloat("CAN-SKIP-UNTIL",0),c.canSkipDateRanges=c.canSkipUntil>0&&h.bool("CAN-SKIP-DATERANGES"),c.partHoldBack=h.optionalFloat("PART-HOLD-BACK",0),c.holdBack=h.optionalFloat("HOLD-BACK",0);break;case"PART-INF":c.partTarget&&si(c,C,l);var X=new yr(w);c.partTarget=X.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=c.partList;Q||(Q=c.partList=[]);var z=y>0?Q[Q.length-1]:void 0,$=y++,Z=new yr(w,c),J=new ie(Z,I,f,$,z);Q.push(J),I.duration+=J.duration;break;case"PRELOAD-HINT":var ee=new yr(w,c);c.preloadHint=ee;break;case"RENDITION-REPORT":var te=new yr(w,c);c.renditionReports=c.renditionReports||[],c.renditionReports.push(te);break;default:Y.warn("line parsed but not handled: "+l)}}}L&&!L.relurl?(g.pop(),E-=L.duration,c.partList&&(c.fragmentHint=L)):c.partList&&(ii(I,L,v),I.cc=T,c.fragmentHint=I,d&&ai(I,d,c)),c.targetduration||(c.playlistParsingError=new Error("Missing Target Duration"));var ne=g.length,ae=g[0],se=g[ne-1];if((E+=c.skippedSegments*c.targetduration)>0&&ne&&se){c.averagetargetduration=E/ne;var oe=se.sn;c.endSN="initSegment"!==oe?oe:0,c.live||(se.endList=!0),R>0&&(function(e,t){for(var r=e[t],i=t;i--;){var n=e[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(g,R),ae&&v.unshift(ae))}return c.fragmentHint&&(E+=c.fragmentHint.duration),c.totalduration=E,v.length&&c.dateRangeTagCount&&ae&&$r(v,c),c.endCC=T,c},e}();function $r(e,t){var r=e.length;if(!r){if(!t.hasProgramDateTime)return;var i=t.fragments[t.fragments.length-1];e.push(i),r++}for(var n=e[r-1],a=t.live?1/0:t.totalduration,s=Object.keys(t.dateRanges),o=s.length;o--;){var l=t.dateRanges[s[o]],u=l.startDate.getTime();l.tagAnchor=n.ref;for(var d=r;d--;){var h;if((null==(h=e[d])?void 0:h.sn)=o||0===i)&&t<=o+1e3*(((null==(s=r[i+1])?void 0:s.start)||n)-a.start)){var l=r[i].sn-e.startSN;if(l<0)return-1;var u=e.fragments;if(u.length>r.length)for(var d=(r[i+1]||u[u.length-1]).sn-e.startSN;d>l;d--){var h=u[d].programDateTime;if(t>=h&&te.sn?(n=r-e.start,i=e):(n=e.start-r,i=t),i.duration!==n&&i.setDuration(n)}else t.sn>e.sn?e.cc===t.cc&&e.minEndPTS?t.setStart(e.start+(e.minEndPTS-e.start)):t.setStart(e.start+e.duration):t.setStart(Math.max(e.start-t.duration,0))}function ui(e,t,r,i,n,a,s){i-r<=0&&(s.warn("Fragment should have a positive duration",t),i=r+t.duration,a=n+t.duration);var o=r,l=i,u=t.startPTS,d=t.endPTS;if(A(u)){var h=Math.abs(u-r);e&&h>e.totalduration?s.warn("media timestamps and playlist times differ by "+h+"s for level "+t.level+" "+e.url):A(t.deltaPTS)?t.deltaPTS=Math.max(h,t.deltaPTS):t.deltaPTS=h,o=Math.max(r,u),r=Math.min(r,u),n=void 0!==t.startDTS?Math.min(n,t.startDTS):n,l=Math.min(i,d),i=Math.max(i,d),a=void 0!==t.endDTS?Math.max(a,t.endDTS):a}var f=r-t.start;0!==t.start&&t.setStart(r),t.setDuration(i-t.start),t.startPTS=r,t.maxStartPTS=o,t.startDTS=n,t.endPTS=i,t.minEndPTS=l,t.endDTS=a;var c,g=t.sn;if(!e||ge.endSN)return 0;var v=g-e.startSN,m=e.fragments;for(m[v]=t,c=v;c>0;c--)li(m[c],m[c-1]);for(c=v;c=0;o--){var l=s[o].initSegment;if(l){n=l;break}}e.fragmentHint&&delete e.fragmentHint.endPTS,function(e,t,r){for(var i=t.skippedSegments,n=Math.max(e.startSN,t.startSN)-t.startSN,a=(e.fragmentHint?1:0)+(i?t.endSN:Math.min(e.endSN,t.endSN))-t.startSN,s=t.startSN-e.startSN,o=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,l=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,u=n;u<=a;u++){var d=l[s+u],h=o[u];if(i&&!h&&d&&(h=t.fragments[u]=d),d&&h){r(d,h,u,o);var f=d.relurl,c=h.relurl;if(f&&Ei(f,c))return void(t.playlistParsingError=hi("media sequence mismatch "+h.sn+":",e,t,0,h));if(d.cc!==h.cc)return void(t.playlistParsingError=hi("discontinuity sequence mismatch ("+d.cc+"!="+h.cc+")",e,t,0,h))}}}(e,t,(function(e,r,a,s){if((!t.startCC||t.skippedSegments)&&r.cc!==e.cc){for(var o=e.cc-r.cc,l=a;l=0,s=0;if(a&&it){var n=1e3*i[i.length-1].duration;ne.startCC)}(t,e)){var r=Math.min(t.endCC,e.endCC),i=Si(t.fragments,r),n=Si(e.fragments,r);i&&n&&(Y.log("Aligning playlist at start of dicontinuity sequence "+r),Li(i.start-n.start,e))}}function Ri(e,t){if(e.hasProgramDateTime&&t.hasProgramDateTime){var r=e.fragments,i=t.fragments;if(r.length&&i.length){var n,a,s=Math.min(t.endCC,e.endCC);t.startCCl.end){var c=o>f;(os.lastCurrentTime&&(s.lastCurrentTime=o),!s.loadingParts)){var g=Math.max(l.end,o),v=s.shouldLoadParts(s.getLevelDetails(),g);v&&(s.log("LL-Part loading ON after seeking to "+o.toFixed(2)+" with buffer @"+g.toFixed(2)),s.loadingParts=v)}s.hls.hasEnoughToStart||(s.log("Setting "+(u?"startPosition":"nextLoadPosition")+" to "+o+" for seek without enough to start"),s.nextLoadPosition=o,u&&(s.startPosition=o)),u&&s.state===_i.IDLE&&s.tickImmediate()},s.onMediaEnded=function(){s.log("setting startPosition to 0 because media ended"),s.startPosition=s.lastCurrentTime=0},s.playlistType=a,s.hls=t,s.fragmentLoader=new ir(t.config),s.keyLoader=i,s.fragmentTracker=r,s.config=t.config,s.decrypter=new tr(t.config),s}o(t,e);var r=t.prototype;return r.registerListeners=function(){var e=this.hls;e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.on(b.ERROR,this.onError,this)},r.unregisterListeners=function(){var e=this.hls;e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.off(b.ERROR,this.onError,this)},r.doTick=function(){this.onTickEnd()},r.onTickEnd=function(){},r.startLoad=function(e){},r.stopLoad=function(){if(this.state!==_i.STOPPED){this.fragmentLoader.abort(),this.keyLoader.abort(this.playlistType);var e=this.fragCurrent;null!=e&&e.loader&&(e.abortRequests(),this.fragmentTracker.removeFragment(e)),this.resetTransmuxer(),this.fragCurrent=null,this.fragPrevious=null,this.clearInterval(),this.clearNextTick(),this.state=_i.STOPPED}},r.pauseBuffering=function(){this.buffering=!1},r.resumeBuffering=function(){this.buffering=!0},r._streamEnded=function(e,t){if(t.live||!this.media)return!1;var r=e.end||0,i=this.config.timelineOffset||0;if(r<=i)return!1;var n=e.buffered;this.config.maxBufferHole&&n&&n.length>1&&(e=dr.bufferedInfo(n,e.start,0));var a=e.nextStart;if(a&&a>i&&a0&&null!=a&&a.key&&a.iv&&Ir(a.method)){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer,Rr(a.method)).catch((function(e){throw t.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:i}),e})).then((function(n){var a=self.performance.now();return t.trigger(b.FRAG_DECRYPTED,{frag:i,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(t){r.state!==_i.STOPPED&&r.state!==_i.ERROR&&(r.warn(t),r.resetFragmentLoading(e))}))},r.completeInitSegmentLoad=function(e){if(!this.levels)throw new Error("init load aborted, missing levels");var t=e.frag.stats;this.state!==_i.STOPPED&&(this.state=_i.IDLE),e.frag.data=new Uint8Array(e.payload),t.parsing.start=t.buffering.start=self.performance.now(),t.parsing.end=t.buffering.end=self.performance.now(),this.tick()},r.unhandledEncryptionError=function(e,t){var r,i,n=e.tracks;if(n&&!t.encrypted&&(null!=(r=n.audio)&&r.encrypted||null!=(i=n.video)&&i.encrypted)&&(!this.config.emeEnabled||!this.keyLoader.emeController)){var a=this.media,s=new Error("Encrypted track with no key in "+this.fragInfo(t)+" (media "+(a?"attached mediaKeys: "+a.mediaKeys:"detached")+")");return this.warn(s.message),!(!a||a.mediaKeys)&&(this.hls.trigger(b.ERROR,{type:R.KEY_SYSTEM_ERROR,details:k.KEY_SYSTEM_NO_KEYS,fatal:!1,error:s,frag:t}),this.resetTransmuxer(),!0)}return!1},r.fragContextChanged=function(e){var t=this.fragCurrent;return!e||!t||e.sn!==t.sn||e.level!==t.level},r.fragBufferedComplete=function(e,t){var r=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+e.type+" sn: "+e.sn+(t?" part: "+t.index:"")+" of "+this.fragInfo(e,!1,t)+" > buffer:"+(r?Di(dr.getBuffered(r)):"(detached)")+")"),te(e)){var i;if(e.type!==x){var n=e.elementaryStreams;if(!Object.keys(n).some((function(e){return!!n[e]})))return void(this.state=_i.IDLE)}var a=null==(i=this.levels)?void 0:i[e.level];null!=a&&a.fragmentError&&(this.log("Resetting level fragment error count of "+a.fragmentError+" on frag buffered"),a.fragmentError=0)}this.state=_i.IDLE},r._handleFragmentLoadComplete=function(e){var t=this.transmuxer;if(t){var r=e.frag,i=e.part,n=e.partsLoaded,a=!n||0===n.length||n.some((function(e){return!e})),s=new lr(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);t.flush(s)}},r._handleFragmentLoadProgress=function(e){},r._doFragLoad=function(e,t,r,i){var n,a=this;void 0===r&&(r=null),this.fragCurrent=e;var s=t.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!e.encrypted||null!=(n=e.decryptdata)&&n.key)e.encrypted||(o=this.keyLoader.loadClear(e,s.encryptedFragments,this.startFragRequested))&&this.log("[eme] blocking frag load until media-keys acquired");else if(this.log("Loading key for "+e.sn+" of ["+s.startSN+"-"+s.endSN+"], "+this.playlistLabel()+" "+e.level),this.state=_i.KEY_LOADING,this.fragCurrent=e,o=this.keyLoader.load(e).then((function(e){if(!a.fragContextChanged(e.frag))return a.hls.trigger(b.KEY_LOADED,e),a.state===_i.KEY_LOADING&&(a.state=_i.IDLE),e})),this.hls.trigger(b.KEY_LOADING,{frag:e}),null===this.fragCurrent)return this.log("context changed in KEY_LOADING"),Promise.resolve(null);var l,u=this.fragPrevious;if(te(e)&&(!u||e.sn!==u.sn)){var d=this.shouldLoadParts(t.details,e.end);d!==this.loadingParts&&(this.log("LL-Part loading "+(d?"ON":"OFF")+" loading sn "+(null==u?void 0:u.sn)+"->"+e.sn),this.loadingParts=d)}if(r=Math.max(e.start,r||0),this.loadingParts&&te(e)){var h=s.partList;if(h&&i){r>s.fragmentEnd&&s.fragmentHint&&(e=s.fragmentHint);var f=this.getNextPart(h,e,r);if(f>-1){var c,g=h[f];return e=this.fragCurrent=g.fragment,this.log("Loading "+e.type+" sn: "+e.sn+" part: "+g.index+" ("+f+"/"+(h.length-1)+") of "+this.fragInfo(e,!1,g)+") cc: "+e.cc+" ["+s.startSN+"-"+s.endSN+"], target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=g.start+g.duration,this.state=_i.FRAG_LOADING,c=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(e,g,t,i)})).catch((function(e){return a.handleFragLoadError(e)})):this.doFragPartsLoad(e,g,t,i).catch((function(e){return a.handleFragLoadError(e)})),this.hls.trigger(b.FRAG_LOADING,{frag:e,part:g,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):c}if(!e.url||this.loadedEndOfParts(h,r))return Promise.resolve(null)}}if(te(e)&&this.loadingParts)this.log("LL-Part loading OFF after next part miss @"+r.toFixed(2)+" Check buffer at sn: "+e.sn+" loaded parts: "+(null==(l=s.partList)?void 0:l.filter((function(e){return e.loaded})).map((function(e){return"["+e.start+"-"+e.end+"]"})))),this.loadingParts=!1;else if(!e.url)return Promise.resolve(null);this.log("Loading "+e.type+" sn: "+e.sn+" of "+this.fragInfo(e,!1)+") cc: "+e.cc+" ["+s.startSN+"-"+s.endSN+"], target: "+parseFloat(r.toFixed(3))),A(e.sn)&&!this.bitrateTest&&(this.nextLoadPosition=e.start+e.duration),this.state=_i.FRAG_LOADING;var v,m=this.config.progressive&&e.type!==x;return v=m&&o?o.then((function(t){return!t||a.fragContextChanged(t.frag)?null:a.fragmentLoader.load(e,i)})).catch((function(e){return a.handleFragLoadError(e)})):Promise.all([this.fragmentLoader.load(e,m?i:void 0),o]).then((function(e){var t=e[0];return!m&&i&&i(t),t})).catch((function(e){return a.handleFragLoadError(e)})),this.hls.trigger(b.FRAG_LOADING,{frag:e,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):v},r.doFragPartsLoad=function(e,t,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList,d=function(t){n.fragmentLoader.loadPart(e,t,i).then((function(i){l[t.index]=i;var s=i.part;n.hls.trigger(b.FRAG_LOADED,i);var o=mi(r.details,e.sn,t.index+1)||pi(u,e.sn,t.index+1);if(!o)return a({frag:e,part:s,partsLoaded:l});d(o)})).catch(s)};d(t)}))},r.handleFragLoadError=function(e){if("data"in e){var t=e.data;t.frag&&t.details===k.INTERNAL_ABORTED?this.handleFragLoadAborted(t.frag,t.part):t.frag&&t.type===R.KEY_SYSTEM_ERROR?(t.frag.abortRequests(),this.resetStartWhenNotLoaded(),this.resetFragmentLoading(t.frag)):this.hls.trigger(b.ERROR,t)}else this.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.INTERNAL_EXCEPTION,err:e,error:e,fatal:!0});return null},r._handleTransmuxerFlush=function(e){var t=this.getCurrentContext(e);if(t&&this.state===_i.PARSING){var r=t.frag,i=t.part,n=t.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a);var s=this.getLevelDetails(),o=s&&r.sn>s.endSN||this.shouldLoadParts(s,r.end);o!==this.loadingParts&&(this.log("LL-Part loading "+(o?"ON":"OFF")+" after parsing segment ending @"+r.end.toFixed(2)),this.loadingParts=o),this.updateLevelTiming(r,i,n,e.partial)}else this.fragCurrent||this.state===_i.STOPPED||this.state===_i.ERROR||(this.state=_i.IDLE)},r.shouldLoadParts=function(e,t){if(this.config.lowLatencyMode){if(!e)return this.loadingParts;if(e.partList){var r,i,n=e.partList[0];if(n.fragment.type===x)return!1;if(t>=n.end+((null==(r=e.fragmentHint)?void 0:r.duration)||0)&&(this.hls.hasEnoughToStart?(null==(i=this.media)?void 0:i.currentTime)||this.lastCurrentTime:this.getLoadPosition())>n.start-n.fragment.duration)return!0}}return!1},r.getCurrentContext=function(e){var t=this.levels,r=this.fragCurrent,i=e.level,n=e.sn,a=e.part;if(null==t||!t[i])return this.warn("Levels object was unset while buffering fragment "+n+" of "+this.playlistLabel()+" "+i+". The current chunk will not be buffered."),null;var s=t[i],o=s.details,l=a>-1?mi(o,n,a):null,u=l?l.fragment:vi(o,n,r);return u?(r&&r!==u&&(u.stats=r.stats),{frag:u,part:l,level:s}):null},r.bufferFragmentData=function(e,t,r,i,n){if(this.state===_i.PARSING){var a=e.data1,s=e.data2,o=a;if(s&&(o=Le(a,s)),o.length){var l=this.initPTS[t.cc],u=l?-l.baseTime/l.timescale:void 0,d={type:e.type,frag:t,part:r,chunkMeta:i,offset:u,parent:t.type,data:o};if(this.hls.trigger(b.BUFFER_APPENDING,d),e.dropped&&e.independent&&!r){if(n)return;this.flushBufferGap(t)}}}},r.flushBufferGap=function(e){var t=this.media;if(t)if(dr.isBuffered(t,t.currentTime)){var r=t.currentTime,i=dr.bufferInfo(t,r,0),n=e.duration,a=Math.min(2*this.config.maxFragLookUpTolerance,.25*n),s=Math.max(Math.min(e.start-a,i.end-a),r+a);e.start-s>a&&this.flushMainBuffer(s,e.start)}else this.flushMainBuffer(0,e.start)},r.getFwdBufferInfo=function(e,t){var r,i=this.getLoadPosition();if(!A(i))return null;var n=this.lastCurrentTime>i||null!=(r=this.media)&&r.paused?0:this.config.maxBufferHole;return this.getFwdBufferInfoAtPos(e,i,t,n)},r.getFwdBufferInfoAtPos=function(e,t,r,i){var n=dr.bufferInfo(e,t,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(t,r);if(a&&(n.nextStart<=a.end||a.gap)){var s=Math.max(Math.min(n.nextStart,a.end)-t,i);return dr.bufferInfo(e,t,s)}}return n},r.getMaxBufferLength=function(e){var t,r=this.config;return t=e?Math.max(8*r.maxBufferSize/e,r.maxBufferLength):r.maxBufferLength,Math.min(t,r.maxMaxBufferLength)},r.reduceMaxBufferLength=function(e,t){var r=this.config,i=Math.max(Math.min(e-t,r.maxBufferLength),t),n=Math.max(e-3*t,r.maxMaxBufferLength/2,i);return n>=i&&(r.maxMaxBufferLength=n,this.warn("Reduce max buffer length to "+n+"s"),!0)},r.getAppendedFrag=function(e,t){void 0===t&&(t=w);var r=this.fragmentTracker?this.fragmentTracker.getAppendedFrag(e,t):null;return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(e,t){var r=t.fragments,i=r.length;if(!i)return null;var n=this.config,a=r[0].start,s=n.lowLatencyMode&&!!t.partList,o=null;if(t.live){var l=n.initialLiveManifestSize;if(i=a?d:h)||o.start:e;this.log("Setting startPosition to "+f+" to match start frag at live edge. mainStart: "+d+" liveSyncPosition: "+h+" frag.start: "+(null==(u=o)?void 0:u.start)),this.startPosition=this.nextLoadPosition=f}}else e<=a&&(o=r[0]);if(!o){var c=this.loadingParts?t.partEnd:t.fragmentEnd;o=this.getFragmentAtPosition(e,c,t)}var g=this.filterReplacedPrimary(o,t);if(!g&&o){var v=o.sn-t.startSN;g=this.filterReplacedPrimary(r[v+1]||null,t)}return this.mapToInitFragWhenRequired(g)},r.isLoopLoading=function(e,t){var r=this.fragmentTracker.getState(e);return(r===Wt||r===Yt&&!!e.gap)&&this.nextLoadPosition>t},r.getNextFragmentLoopLoading=function(e,t,r,i,n){var a=null;if(e.gap&&(a=this.getNextFragment(this.nextLoadPosition,t))&&!a.gap&&r.nextStart){var s=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i,0);if(null!==s&&r.len+s.len>=n){var o=a.sn;return this.loopSn!==o&&(this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+o),this.loopSn=o),null}}return this.loopSn=void 0,a},r.filterReplacedPrimary=function(e,t){if(!e)return e;if(Ci(this.config)&&e.type!==x){var r=this.hls.interstitialsManager,i=null==r?void 0:r.bufferingItem;if(i){var n=i.event;if(n){if(n.appendInPlace||Math.abs(e.start-i.start)>1||0===i.start)return null}else{if(e.end<=i.start&&!1===(null==t?void 0:t.live))return null;if(e.start>i.end&&i.nextEvent&&(i.nextEvent.appendInPlace||e.start-i.end>1))return null}}var a=null==r?void 0:r.playerQueue;if(a)for(var s=a.length;s--;){var o=a[s].interstitial;if(o.appendInPlace&&e.start>=o.startTime&&e.end<=o.resumeTime)return null}}return e},r.mapToInitFragWhenRequired=function(e){return null==e||!e.initSegment||e.initSegment.data||this.bitrateTest?e:e.initSegment},r.getNextPart=function(e,t,r){for(var i=-1,n=!1,a=!0,s=0,o=e.length;s-1&&rr.start)return!0}return!1},r.getInitialLiveFragment=function(e){var t=e.fragments,r=this.fragPrevious,i=null;if(r){if(e.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(e,t,r){if(null===t||!Array.isArray(e)||!e.length||!A(t))return null;if(t<(e[0].programDateTime||0))return null;if(t>=(e[e.length-1].endProgramDateTime||0))return null;for(var i=0;i=e.startSN&&n<=e.endSN){var a=t[n-e.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=Lt(e,r.cc,r.end))&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn)}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?e.fragmentEnd:e.edge,e))}return i},r.getFragmentAtPosition=function(e,t,r){var i,n,a=this.config,s=this.fragPrevious,o=r.fragments,l=r.endSN,u=r.fragmentHint,d=a.maxFragLookUpTolerance,h=r.partList,f=!!(this.loadingParts&&null!=h&&h.length&&u);if(f&&!this.bitrateTest&&h[h.length-1].fragment.sn===u.sn&&(o=o.concat(u),l=u.sn),i=et-d||null!=(n=this.media)&&n.paused||!this.startFragRequested?0:d):o[o.length-1]){var c=i.sn-r.startSN,g=this.fragmentTracker.getState(i);if((g===Wt||g===Yt&&i.gap)&&(s=i),s&&i.sn===s.sn&&(!f||h[0].fragment.sn>i.sn||!r.live)&&i.level===s.level){var v=o[c+1];i=i.sn"+e.startSN+" fragments: "+i),o}return n},r.waitForCdnTuneIn=function(e){return e.live&&e.canBlockReload&&e.partTarget&&e.tuneInGoal>Math.max(e.partHoldBack,3*e.partTarget)},r.setStartPosition=function(e,t){var r=this.startPosition;r=0&&(r=this.nextLoadPosition),r},r.handleFragLoadAborted=function(e,t){this.transmuxer&&e.type===this.playlistType&&te(e)&&e.stats.aborted&&(this.log("Fragment "+e.sn+(t?" part "+t.index:"")+" of "+this.playlistLabel()+" "+e.level+" was aborted"),this.resetFragmentLoading(e))},r.resetFragmentLoading=function(e){this.fragCurrent&&(this.fragContextChanged(e)||this.state===_i.FRAG_LOADING_WAITING_RETRY)||(this.state=_i.IDLE)},r.onFragmentOrKeyLoadError=function(e,t){var r;if(t.chunkMeta&&!t.frag){var i=this.getCurrentContext(t.chunkMeta);i&&(t.frag=i.frag)}var n=t.frag;if(n&&n.type===e&&this.levels)if(this.fragContextChanged(n)){var a;this.warn("Frag load error must match current frag to retry "+n.url+" > "+(null==(a=this.fragCurrent)?void 0:a.url))}else{var s=t.details===k.FRAG_GAP;s&&this.fragmentTracker.fragBuffered(n,!0);var o=t.errorAction;if(o){var l=o.action,u=o.flags,d=o.retryCount,h=void 0===d?0:d,f=o.retryConfig,c=!!f,g=c&&l===Mt,v=c&&!o.resolved&&u===Nt,m=null==(r=this.hls.latestLevelDetails)?void 0:r.live;if(!g&&v&&te(n)&&!n.endList&&m&&!kt(t))this.resetFragmentErrors(e),this.treatAsGap(n),o.resolved=!0;else if((g||v)&&h=t||r&&!Ct(0))&&(r&&this.log("Connection restored (online)"),this.resetStartWhenNotLoaded(),this.state=_i.IDLE)},r.reduceLengthAndFlushBuffer=function(e){if(this.state===_i.PARSING||this.state===_i.PARSED){var t=e.frag,r=e.parent,i=this.getFwdBufferInfo(this.mediaBuffer,r),n=i&&i.len>.5;n&&this.reduceMaxBufferLength(i.len,(null==t?void 0:t.duration)||10);var a=!n;return a&&this.warn("Buffer full error while media.currentTime ("+this.getLoadPosition()+") is not buffered, flush "+r+" buffer"),t&&(this.fragmentTracker.removeFragment(t),this.nextLoadPosition=t.start),this.resetLoadingState(),a}return!1},r.resetFragmentErrors=function(e){e===O&&(this.fragCurrent=null),this.hls.hasEnoughToStart||(this.startFragRequested=!1),this.state!==_i.STOPPED&&(this.state=_i.IDLE)},r.afterBufferFlushed=function(e,t,r){if(e){var i=dr.getBuffered(e);this.fragmentTracker.detectEvictedFragments(t,i,r),this.state===_i.ENDED&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state!==_i.STOPPED&&(this.state=_i.IDLE)},r.resetStartWhenNotLoaded=function(){if(!this.hls.hasEnoughToStart){this.startFragRequested=!1;var e=this.levelLastLoaded,t=e?e.details:null;null!=t&&t.live?(this.log("resetting startPosition for live start"),this.startPosition=-1,this.setStartPosition(t,t.fragmentStart),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(e){this.log("Loading context changed while buffering sn "+e.sn+" of "+this.playlistLabel()+" "+(-1===e.level?"":e.level)+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(),this.resetLoadingState()},r.removeUnbufferedFrags=function(e){void 0===e&&(e=0),this.fragmentTracker.removeFragmentsInRange(e,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(e,t,r,i){var n=this,a=r.details;if(a){if(!Object.keys(e.elementaryStreams).reduce((function(t,s){var o=e.elementaryStreams[s];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return n.warn("Could not parse fragment "+e.sn+" "+s+" duration reliably ("+l+")"),t||!1;var u=i?0:ui(a,e,o.startPTS,o.endPTS,o.startDTS,o.endDTS,n);return n.hls.trigger(b.LEVEL_PTS_UPDATED,{details:a,level:r,drift:u,type:s,frag:e,start:o.startPTS,end:o.endPTS}),!0}return t}),!1)){var s,o=null===(null==(s=this.transmuxer)?void 0:s.error);if((0===r.fragmentError||o&&(r.fragmentError<2||e.endList))&&this.treatAsGap(e,r),o){var l=new Error("Found no media in fragment "+e.sn+" of "+this.playlistLabel()+" "+e.level+" resetting transmuxer to fallback to playlist timing");if(this.warn(l.message),this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_PARSING_ERROR,fatal:!1,error:l,frag:e,reason:"Found no media in msn "+e.sn+" of "+this.playlistLabel()+' "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}}this.state=_i.PARSED,this.log("Parsed "+e.type+" sn: "+e.sn+(t?" part: "+t.index:"")+" of "+this.fragInfo(e,!1,t)+")"),this.hls.trigger(b.FRAG_PARSED,{frag:e,part:t})}else this.warn("level.details undefined")},r.playlistLabel=function(){return this.playlistType===w?"level":"track"},r.fragInfo=function(e,t,r){var i,n;return void 0===t&&(t=!0),this.playlistLabel()+" "+e.level+" ("+(r?"part":"frag")+":["+(null!=(i=t&&!r?e.startPTS:(r||e).start)?i:NaN).toFixed(3)+"-"+(null!=(n=t&&!r?e.endPTS:(r||e).end)?n:NaN).toFixed(3)+"]"+(r&&"main"===e.type?"INDEPENDENT="+(r.independent?"YES":"NO"):"")},r.treatAsGap=function(e,t){t&&t.fragmentError++,e.gap=!0,this.fragmentTracker.removeFragment(e),this.fragmentTracker.fragBuffered(e,!0)},r.resetTransmuxer=function(){var e;null==(e=this.transmuxer)||e.reset()},r.recoverWorkerError=function(e){"demuxerWorker"===e.event&&(this.fragmentTracker.removeAllFragments(),this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null),this.resetStartWhenNotLoaded(),this.resetLoadingState())},i(t,[{key:"startPositionValue",get:function(){var e=this.nextLoadPosition,t=this.startPosition;return-1===t&&e?e:t}},{key:"bufferingEnabled",get:function(){return this.buffering}},{key:"inFlightFrag",get:function(){return{frag:this.fragCurrent,state:this.state}}},{key:"timelineOffset",get:function(){var e,t=this.config.timelineOffset;return t?(null==(e=this.getLevelDetails())?void 0:e.appliedTimelineOffset)||t:0}},{key:"primaryPrefetch",get:function(){var e;return!(!Ci(this.config)||!(null==(e=this.hls.interstitialsManager)||null==(e=e.playingItem)?void 0:e.event))}},{key:"state",get:function(){return this._state},set:function(e){var t=this._state;t!==e&&(this._state=e,this.log(t+"->"+e))}}])}(or);function Ci(e){return!!e.interstitialsController&&!1!==e.enableInterstitialPlayback}var wi=function(){function e(){this.chunks=[],this.dataLength=0}var t=e.prototype;return t.push=function(e){this.chunks.push(e),this.dataLength+=e.length},t.flush=function(){var e,t=this.chunks,r=this.dataLength;return t.length?(e=1===t.length?t[0]:function(e,t){for(var r=new Uint8Array(t),i=0,n=0;n0)return e.subarray(r,r+i)}function Ni(e,t){return 255===e[t]&&240==(246&e[t+1])}function Ui(e,t){return 1&e[t+1]?7:9}function Bi(e,t){return(3&e[t+3])<<11|e[t+4]<<3|(224&e[t+5])>>>5}function Gi(e,t){return t+1=e.length)return!1;var i=Bi(e,t);if(i<=r)return!1;var n=t+i;return n===e.length||Gi(e,n)}return!1}function Vi(e,t,r,i,n){if(!e.samplerate){var s=function(e,t,r,i){var n=t[r+2],a=n>>2&15;if(!(a>12)){var s=1+(n>>6&3),o=t[r+3]>>6&3|(1&n)<<2,l="mp4a.40."+s,u=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350][a],d=a;5!==s&&29!==s||(d-=3);var h=[s<<3|(14&d)>>1,(1&d)<<7|o<<3];return Y.log("manifest codec:"+i+", parsed codec:"+l+", channels:"+o+", rate:"+u+" (ADTS object type:"+s+" sampling index:"+a+")"),{config:h,samplerate:u,channelCount:o,codec:l,parsedCodec:l,manifestCodec:i}}var f=new Error("invalid ADTS sampling index:"+a);e.emit(b.ERROR,b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_PARSING_ERROR,fatal:!0,error:f,reason:f.message})}(t,r,i,n);if(!s)return;a(e,s)}}function Hi(e){return 9216e4/e}function Yi(e,t,r,i,n){var a,s=i+n*Hi(e.samplerate),o=function(e,t){var r=Ui(e,t);if(t+r<=e.length){var i=Bi(e,t)-r;if(i>0)return{headerLength:r,frameLength:i}}}(t,r);if(o){var l=o.frameLength,u=o.headerLength,d=u+l,h=Math.max(0,r+d-t.length);h?(a=new Uint8Array(d-u)).set(t.subarray(r+u,t.length),0):a=t.subarray(r+u,r+d);var f={unit:a,pts:s};return h||e.samples.push(f),{sample:f,length:d,missing:h}}var c=t.length-r;return(a=new Uint8Array(c)).set(t.subarray(r,t.length),0),{sample:{unit:a,pts:s},length:c,missing:-1}}function Wi(e,t){return xi(e,t)&&Mi(e,t+6)+10<=e.length-t}function ji(e,t,r){return void 0===t&&(t=0),void 0===r&&(r=1/0),function(e,t,r,i){var n=function(e){return e instanceof ArrayBuffer?e:e.buffer}(e),a=1;"BYTES_PER_ELEMENT"in i&&(a=i.BYTES_PER_ELEMENT);var s,o=(s=e)&&s.buffer instanceof ArrayBuffer&&void 0!==s.byteLength&&void 0!==s.byteOffset?e.byteOffset:0,l=(o+e.byteLength)/a,u=(o+t)/a,d=Math.floor(Math.max(0,Math.min(u,l))),h=Math.floor(Math.min(d+Math.max(r,0),l));return new i(n,d,h-d)}(e,t,r,Uint8Array)}function qi(e){var t={key:e.type,description:"",data:"",mimeType:null,pictureType:null};if(!(e.size<2))if(3===e.data[0]){var r=e.data.subarray(1).indexOf(0);if(-1!==r){var i=q(ji(e.data,1,r)),n=e.data[2+r],a=e.data.subarray(3+r).indexOf(0);if(-1!==a){var s,o=q(ji(e.data,3+r,a));return s="--\x3e"===i?q(ji(e.data,4+r+a)):function(e){return e instanceof ArrayBuffer?e:0==e.byteOffset&&e.byteLength==e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer}(e.data.subarray(4+r+a)),t.mimeType=i,t.pictureType=n,t.description=o,t.data=s,t}}}else console.log("Ignore frame with unrecognized character encoding")}function Xi(e){return"PRIV"===e.type?function(e){if(!(e.size<2)){var t=q(e.data,!0),r=new Uint8Array(e.data.subarray(t.length+1));return{key:e.type,info:t,data:r.buffer}}}(e):"W"===e.type[0]?function(e){if("WXXX"===e.type){if(e.size<2)return;var t=1,r=q(e.data.subarray(t),!0);t+=r.length+1;var i=q(e.data.subarray(t));return{key:e.type,info:r,data:i}}var n=q(e.data);return{key:e.type,info:"",data:n}}(e):"APIC"===e.type?qi(e):function(e){if(!(e.size<2)){if("TXXX"===e.type){var t=1,r=q(e.data.subarray(t),!0);t+=r.length+1;var i=q(e.data.subarray(t));return{key:e.type,info:r,data:i}}var n=q(e.data.subarray(1));return{key:e.type,info:"",data:n}}}(e)}function Qi(e){var t=String.fromCharCode(e[0],e[1],e[2],e[3]),r=Mi(e,4);return{type:t,size:r,data:e.subarray(10,10+r)}}var zi=10,$i=10;function Zi(e){for(var t=0,r=[];xi(e,t);){var i=Mi(e,t+6);e[t+5]>>6&1&&(t+=zi);for(var n=(t+=zi)+i;t+$i0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:rn.audioId3,duration:Number.POSITIVE_INFINITY});nt.length)){var a=cn(t,r);if(a&&r+a.frameLength<=t.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:t.subarray(r,r+a.frameLength),pts:s,dts:s};return e.config=[],e.channelCount=a.channelCount,e.samplerate=a.sampleRate,e.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function cn(e,t){var r=e[t+1]>>3&3,i=e[t+1]>>1&3,n=e[t+2]>>4&15,a=e[t+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=e[t+2]>>1&1,o=e[t+3]>>6,l=1e3*ln[14*(3===r?3-i:3===i?3:4)+n-1],u=un[3*(3===r?0:2===r?1:2)+a],d=3===o?1:2,h=dn[r][i],f=hn[i],c=8*h*f,g=Math.floor(h*l/u+s)*f;if(null===on){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);on=v?parseInt(v[1]):0}return!!on&&on<=87&&2===i&&l>=224e3&&0===o&&(e[t+3]=128|e[t+3]),{sampleRate:u,channelCount:d,frameLength:g,samplesPerFrame:c}}}function gn(e,t){return 255===e[t]&&224==(224&e[t+1])&&0!=(6&e[t+1])}function vn(e,t){return t+10;){s[0]=e[t];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<t.length)return-1;if(11!==t[r]||119!==t[r+1])return-1;var a=t[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&t[r+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+a];if(r+l>t.length)return-1;var u=t[r+6]>>5,d=0;2===u?d+=2:(1&u&&1!==u&&(d+=2),4&u&&(d+=2));var h=(t[r+6]<<8|t[r+7])>>12-d&1,f=[2,1,2,3,3,4,4,5][u]+h,c=t[r+5]>>3,g=7&t[r+5],v=new Uint8Array([a<<6|c<<1|g>>2,(3&g)<<6|u<<3|h<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=t.subarray(r,r+l);return e.config=v,e.channelCount=f,e.samplerate=s,e.samples.push({unit:p,pts:m}),l}var Sn=function(e){function t(){return e.apply(this,arguments)||this}o(t,e);var r=t.prototype;return r.resetInitSegment=function(t,r,i,n){e.prototype.resetInitSegment.call(this,t,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},t.probe=function(e){if(!e)return!1;var t=Fi(e,0),r=(null==t?void 0:t.length)||0;if(t&&11===e[r]&&119===e[r+1]&&void 0!==tn(t)&&yn(e,r)<=16)return!1;for(var i=e.length;r8&&109===e[r+4]&&111===e[r+5]&&111===e[r+6]&&102===e[r+7])return!0;r=i>1?r+i:t}return!1}(e)},t.demux=function(e,t){this.timeOffset=t;var r=e,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Le(this.remainderData,e));var a=function(e){var t={valid:null,remainder:null},r=ce(e,["moof"]);if(r.length<2)return t.remainder=e,t;var i=r[r.length-1];return t.valid=e.slice(0,i.byteOffset-8),t.remainder=e.slice(i.byteOffset-8),t}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,t);return n.samples=Ie(t,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},t.flush=function(){var e=this.timeOffset,t=this.videoTrack,r=this.txtTrack;t.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(t,this.timeOffset);return r.samples=Ie(e,t),{videoTrack:t,audioTrack:nn(),id3Track:i,textTrack:nn()}},t.extractID3Track=function(e,t){var r=this,i=this.id3Track;if(e.samples.length){var n=ce(e.samples,["emsg"]);n&&n.forEach((function(e){var n=function(e){var t=e[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===t){for(;"\0"!==le(e.subarray(u,u+1));)r+=le(e.subarray(u,u+1)),u+=1;for(r+=le(e.subarray(u,u+1)),u+=1;"\0"!==le(e.subarray(u,u+1));)i+=le(e.subarray(u,u+1)),u+=1;i+=le(e.subarray(u,u+1)),u+=1,n=de(e,12),a=de(e,16),o=de(e,20),l=de(e,24),u=28}else if(1===t){n=de(e,u+=4);var d=de(e,u+=4),h=de(e,u+=4);for(u+=4,s=Math.pow(2,32)*d+h,L(s)||(s=Number.MAX_SAFE_INTEGER,Y.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=de(e,u),l=de(e,u+=4),u+=4;"\0"!==le(e.subarray(u,u+1));)r+=le(e.subarray(u,u+1)),u+=1;for(r+=le(e.subarray(u,u+1)),u+=1;"\0"!==le(e.subarray(u,u+1));)i+=le(e.subarray(u,u+1)),u+=1;i+=le(e.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:e.subarray(u,e.byteLength)}}(e);if(An.test(n.schemeIdUri)){var a=In(n,t),s=4294967295===n.eventDuration?Number.POSITIVE_INFINITY:n.eventDuration/n.timeScale;s<=.001&&(s=Number.POSITIVE_INFINITY);var o=n.payload;i.samples.push({data:o,len:o.byteLength,dts:a,pts:a,type:rn.emsg,duration:s})}else if(r.config.enableEmsgKLVMetadata&&n.schemeIdUri.startsWith("urn:misb:KLV:bin:1910.1")){var l=In(n,t);i.samples.push({data:n.payload,len:n.payload.byteLength,dts:l,pts:l,type:rn.misbklv,duration:Number.POSITIVE_INFINITY})}}))}return i},t.demuxSampleAes=function(e,t,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},t.destroy=function(){this.config=null,this.remainderData=null,this.videoTrack=this.audioTrack=this.id3Track=this.txtTrack=void 0},e}();function In(e,t){return A(e.presentationTime)?e.presentationTime/e.timeScale:t+e.presentationTimeDelta/e.timeScale}var Rn=function(){function e(e,t,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new tr(t,{removePKCS7Padding:!1})}var t=e.prototype;return t.decryptBuffer=function(e){return this.decrypter.decrypt(e,this.keyData.key.buffer,this.keyData.iv.buffer,zt)},t.decryptAacSample=function(e,t,r){var i=this,n=e[t].unit;if(!(n.length<=16)){var a=n.subarray(16,n.length-n.length%16),s=a.buffer.slice(a.byteOffset,a.byteOffset+a.length);this.decryptBuffer(s).then((function(a){var s=new Uint8Array(a);n.set(s,16),i.decrypter.isSync()||i.decryptAacSamples(e,t+1,r)})).catch(r)}},t.decryptAacSamples=function(e,t,r){for(;;t++){if(t>=e.length)return void r();if(!(e[t].unit.length<32||(this.decryptAacSample(e,t,r),this.decrypter.isSync())))return}},t.getAvcEncryptedData=function(e){for(var t=16*Math.floor((e.length-48)/160)+16,r=new Int8Array(t),i=0,n=32;n=e.length)return void i();for(var n=e[t].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(e,t,r,i,a),this.decrypter.isSync())))return}}},e}(),kn=function(){function e(){this.VideoSample=null}var t=e.prototype;return t.createVideoSample=function(e,t,r){return{key:e,frame:!1,pts:t,dts:r,units:[],length:0}},t.getLastNalUnit=function(e){var t,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=e[e.length-1]),null!=(t=i)&&t.units){var n=i.units;r=n[n.length-1]}return r},t.pushAccessUnit=function(e,t){if(e.units.length&&e.frame){if(void 0===e.pts){var r=t.samples,i=r.length;if(!i)return void t.dropped++;var n=r[i-1];e.pts=n.pts,e.dts=n.dts}t.samples.push(e)}},t.parseNALu=function(e,t,r){var i,n,a=t.byteLength,s=e.naluState||0,o=s,l=[],u=0,d=-1,h=0;for(-1===s&&(d=0,h=this.getNALuType(t,0),s=0,u=1);u=0){var f={data:t.subarray(d,n),type:h};l.push(f)}else{var c=this.getLastNalUnit(e.samples);c&&(o&&u<=4-o&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-o)),n>0&&(c.data=Le(c.data,t.subarray(0,n)),c.state=0))}u=0&&s>=0){var g={data:t.subarray(d,a),type:h,state:s};l.push(g)}if(0===l.length){var v=this.getLastNalUnit(e.samples);v&&(v.data=Le(v.data,t))}return e.naluState=s,l},e}(),bn=function(){function e(e){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=e,this.bytesAvailable=e.byteLength,this.word=0,this.bitsAvailable=0}var t=e.prototype;return t.loadWord=function(){var e=this.data,t=this.bytesAvailable,r=e.byteLength-t,i=new Uint8Array(4),n=Math.min(4,t);if(0===n)throw new Error("no bytes available");i.set(e.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},t.skipBits=function(e){var t;e=Math.min(e,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>e?(this.word<<=e,this.bitsAvailable-=e):(e-=this.bitsAvailable,e-=(t=e>>3)<<3,this.bytesAvailable-=t,this.loadWord(),this.word<<=e,this.bitsAvailable-=e)},t.readBits=function(e){var t=Math.min(this.bitsAvailable,e),r=this.word>>>32-t;if(e>32&&Y.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=t,this.bitsAvailable>0)this.word<<=t;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(t=e-t)>0&&this.bitsAvailable?r<>>e))return this.word<<=e,this.bitsAvailable-=e,e;return this.loadWord(),e+this.skipLZ()},t.skipUEG=function(){this.skipBits(1+this.skipLZ())},t.skipEG=function(){this.skipBits(1+this.skipLZ())},t.readUEG=function(){var e=this.skipLZ();return this.readBits(e+1)-1},t.readEG=function(){var e=this.readUEG();return 1&e?1+e>>>1:-1*(e>>>1)},t.readBoolean=function(){return 1===this.readBits(1)},t.readUByte=function(){return this.readBits(8)},t.readUShort=function(){return this.readBits(16)},t.readUInt=function(){return this.readBits(32)},e}(),Dn=function(e){function t(){return e.apply(this,arguments)||this}o(t,e);var r=t.prototype;return r.parsePES=function(e,t,r,i){var n,a=this,s=this.parseNALu(e,r.data,i),o=this.VideoSample,l=!1;r.data=null,o&&s.length&&!e.audFound&&(this.pushAccessUnit(o,e),o=this.VideoSample=this.createVideoSample(!1,r.pts,r.dts)),s.forEach((function(i){var s,u;switch(i.type){case 1:var d=!1;n=!0;var h,f=i.data;if(l&&f.length>4){var c=a.readSliceType(f);2!==c&&4!==c&&7!==c&&9!==c||(d=!0)}d&&null!=(h=o)&&h.frame&&!o.key&&(a.pushAccessUnit(o,e),o=a.VideoSample=null),o||(o=a.VideoSample=a.createVideoSample(!0,r.pts,r.dts)),o.frame=!0,o.key=d;break;case 5:n=!0,null!=(s=o)&&s.frame&&!o.key&&(a.pushAccessUnit(o,e),o=a.VideoSample=null),o||(o=a.VideoSample=a.createVideoSample(!0,r.pts,r.dts)),o.key=!0,o.frame=!0;break;case 6:n=!0,be(i.data,1,r.pts,t.samples);break;case 7:var g,v;n=!0,l=!0;var m=i.data,p=a.readSPS(m);if(!e.sps||e.width!==p.width||e.height!==p.height||(null==(g=e.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=e.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){e.width=p.width,e.height=p.height,e.pixelRatio=p.pixelRatio,e.sps=[m];for(var y=m.subarray(1,4),E="avc1.",T=0;T<3;T++){var S=y[T].toString(16);S.length<2&&(S="0"+S),E+=S}e.codec=E}break;case 8:n=!0,e.pps=[i.data];break;case 9:n=!0,e.audFound=!0,null!=(u=o)&&u.frame&&(a.pushAccessUnit(o,e),o=null),o||(o=a.VideoSample=a.createVideoSample(!1,r.pts,r.dts));break;case 12:n=!0;break;default:n=!1}o&&n&&o.units.push(i)})),i&&o&&(this.pushAccessUnit(o,e),this.VideoSample=null)},r.getNALuType=function(e,t){return 31&e[t]},r.readSliceType=function(e){var t=new bn(e);return t.readUByte(),t.readUEG(),t.readUEG()},r.skipScalingList=function(e,t){for(var r=8,i=8,n=0;n>>1},r.ebsp2rbsp=function(e){for(var t=new Uint8Array(e.byteLength),r=0,i=0;i=2&&3===e[i]&&0===e[i-1]&&0===e[i-2]||(t[r]=e[i],r++);return new Uint8Array(t.buffer,0,r)},r.pushAccessUnit=function(t,r){e.prototype.pushAccessUnit.call(this,t,r),this.initVPS&&(this.initVPS=null)},r.readVPS=function(e){var t=new bn(e);return t.readUByte(),t.readUByte(),t.readBits(4),t.skipBits(2),t.readBits(6),{numTemporalLayers:t.readBits(3)+1,temporalIdNested:t.readBoolean()}},r.readSPS=function(e){var t=new bn(this.ebsp2rbsp(e));t.readUByte(),t.readUByte(),t.readBits(4);var r=t.readBits(3);t.readBoolean();for(var i=t.readBits(2),n=t.readBoolean(),a=t.readBits(5),s=t.readUByte(),o=t.readUByte(),l=t.readUByte(),u=t.readUByte(),d=t.readUByte(),h=t.readUByte(),f=t.readUByte(),c=t.readUByte(),g=t.readUByte(),v=t.readUByte(),m=t.readUByte(),p=[],y=[],E=0;E0)for(var T=r;T<8;T++)t.readBits(2);for(var S=0;S1&&t.readEG();for(var N=0;N0&&ae<16?(ee=[1,12,10,16,40,24,20,32,80,18,15,64,160,4,3,2][ae-1],te=[1,11,11,11,33,11,11,11,33,11,11,33,99,3,2,1][ae-1]):255===ae&&(ee=t.readBits(16),te=t.readBits(16))}if(t.readBoolean()&&t.readBoolean(),t.readBoolean()&&(t.readBits(3),t.readBoolean(),t.readBoolean()&&(t.readUByte(),t.readUByte(),t.readUByte())),t.readBoolean()&&(t.readUEG(),t.readUEG()),t.readBoolean(),t.readBoolean(),t.readBoolean(),t.readBoolean()&&(t.skipUEG(),t.skipUEG(),t.skipUEG(),t.skipUEG()),t.readBoolean()&&(ie=t.readBits(32),ne=t.readBits(32),t.readBoolean()&&t.readUEG(),t.readBoolean())){var se=t.readBoolean(),oe=t.readBoolean(),le=!1;(se||oe)&&((le=t.readBoolean())&&(t.readUByte(),t.readBits(5),t.readBoolean(),t.readBits(5)),t.readBits(4),t.readBits(4),le&&t.readBits(4),t.readBits(5),t.readBits(5),t.readBits(5));for(var ue=0;ue<=r;ue++){var de=!1;(re=t.readBoolean())||t.readBoolean()?t.readEG():de=t.readBoolean();var he=de?1:t.readUEG()+1;if(se)for(var fe=0;fe>Se&1)<<31-Se)>>>0;var Ae=Te.toString(16);return 1===a&&"2"===Ae&&(Ae="6"),{codecString:"hvc1."+ye+a+"."+Ae+"."+(n?"H":"L")+m+".B0",params:{general_tier_flag:n,general_profile_idc:a,general_profile_space:i,general_profile_compatibility_flags:[s,o,l,u],general_constraint_indicator_flags:[d,h,f,c,g,v],general_level_idc:m,bit_depth:P+8,bit_depth_luma_minus8:P,bit_depth_chroma_minus8:C,min_spatial_segmentation_idc:J,chroma_format_idc:A,frame_rate:{fixed:re,fps:ne/ie}},width:ge,height:ve,pixelRatio:[ee,te]}},r.readPPS=function(e){var t=new bn(this.ebsp2rbsp(e));t.readUByte(),t.readUByte(),t.skipUEG(),t.skipUEG(),t.skipBits(2),t.skipBits(3),t.skipBits(2),t.skipUEG(),t.skipUEG(),t.skipEG(),t.skipBits(2),t.readBoolean()&&t.skipUEG(),t.skipEG(),t.skipEG(),t.skipBits(4);var r=t.readBoolean(),i=t.readBoolean(),n=1;return i&&r?n=0:i?n=3:r&&(n=2),{parallelismType:n}},r.matchSPS=function(e,t){return String.fromCharCode.apply(null,e).substr(3)===String.fromCharCode.apply(null,t).substr(3)},t}(kn),Pn=188,Cn=function(){function e(e,t,r,i){this.logger=void 0,this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=e,this.config=t,this.typeSupported=r,this.logger=i,this.videoParser=null}e.probe=function(t,r){var i=e.syncOffset(t);return i>0&&r.warn("MPEG2-TS detected but first sync word found @ offset "+i),-1!==i},e.syncOffset=function(e){for(var t=e.length,r=Math.min(940,t-Pn)+1,i=0;i1&&(0===a&&s>2||o+Pn>r))return a}i++}return-1},e.createTrack=function(e,t){return{container:"video"===e||"audio"===e?"video/mp2t":void 0,type:e,id:oe[e],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===e?t:void 0}};var t=e.prototype;return t.resetInitSegment=function(t,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=e.createTrack("video"),this._videoTrack.duration=n,this._audioTrack=e.createTrack("audio",n),this._id3Track=e.createTrack("id3"),this._txtTrack=e.createTrack("text"),this._audioTrack.segmentCodec="aac",this.videoParser=null,this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i},t.resetTimeStamp=function(){},t.resetContiguity=function(){var e=this._audioTrack,t=this._videoTrack,r=this._id3Track;e&&(e.pesData=null),t&&(t.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},t.demux=function(t,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._videoTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,d=s.pid,h=s.pesData,f=o.pid,c=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=t.length;if(this.remainderData&&(E=(t=Le(this.remainderData,t)).length,this.remainderData=null),E>4>1){if((R=A+5+t[A+4])===A+Pn)continue}else R=A+4;switch(I){case d:L&&(h&&(a=Nn(h,this.logger))&&(this.readyVideoParser(s.segmentCodec),null!==this.videoParser&&this.videoParser.parsePES(s,u,a,!1)),h={data:[],size:0}),h&&(h.data.push(t.subarray(R,A+Pn)),h.size+=A+Pn-R);break;case f:if(L){if(g&&(a=Nn(g,this.logger)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a);break;case"ac3":this.parseAC3PES(o,a)}g={data:[],size:0}}g&&(g.data.push(t.subarray(R,A+Pn)),g.size+=A+Pn-R);break;case c:L&&(v&&(a=Nn(v,this.logger))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(t.subarray(R,A+Pn)),v.size+=A+Pn-R);break;case 0:L&&(R+=t[R]+1),y=this._pmtId=On(t,R);break;case y:L&&(R+=t[R]+1);var k=xn(t,R,this.typeSupported,i,this.observer,this.logger);(d=k.videoPid)>0&&(s.pid=d,s.segmentCodec=k.segmentVideoCodec),(f=k.audioPid)>0&&(o.pid=f,o.segmentCodec=k.segmentAudioCodec),(c=k.id3Pid)>0&&(l.pid=c),null===m||p||(this.logger.warn("MPEG-TS PMT found at "+A+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,A=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=I}}else S++;S>0&&Mn(this.observer,new Error("Found "+S+" TS packet/s that do not start with 0x47"),void 0,this.logger),s.pesData=h,o.pesData=g,l.pesData=v;var b={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(b),b},t.flush=function(){var e,t=this.remainderData;return this.remainderData=null,e=t?this.demux(t,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(e),this.sampleAes?this.decrypt(e,this.sampleAes):e},t.extractRemainingSamples=function(e){var t,r=e.audioTrack,i=e.videoTrack,n=e.id3Track,a=e.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(t=Nn(s,this.logger))?(this.readyVideoParser(i.segmentCodec),null!==this.videoParser&&(this.videoParser.parsePES(i,a,t,!0),i.pesData=null)):i.pesData=s,o&&(t=Nn(o,this.logger))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,t);break;case"mp3":this.parseMPEGPES(r,t);break;case"ac3":this.parseAC3PES(r,t)}r.pesData=null}else null!=o&&o.size&&this.logger.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(t=Nn(l,this.logger))?(this.parseID3PES(n,t),n.pesData=null):n.pesData=l},t.demuxSampleAes=function(e,t,r){var i=this.demux(e,r,!0,!this.config.progressive),n=this.sampleAes=new Rn(this.observer,this.config,t);return this.decrypt(i,n)},t.readyVideoParser=function(e){null===this.videoParser&&("avc"===e?this.videoParser=new Dn:"hevc"===e&&(this.videoParser=new _n))},t.decrypt=function(e,t){return new Promise((function(r){var i=e.audioTrack,n=e.videoTrack;i.samples&&"aac"===i.segmentCodec?t.decryptAacSamples(i.samples,0,(function(){n.samples?t.decryptAvcSamples(n.samples,0,0,(function(){r(e)})):r(e)})):n.samples&&t.decryptAvcSamples(n.samples,0,0,(function(){r(e)}))}))},t.destroy=function(){this.observer&&this.observer.removeAllListeners(),this.config=this.logger=this.observer=null,this.aacOverFlow=this.videoParser=this.remainderData=this.sampleAes=null,this._videoTrack=this._audioTrack=this._id3Track=this._txtTrack=void 0},t.parseAACPES=function(e,t){var r,i,n,a=0,s=this.aacOverFlow,o=t.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Le(s.sample.unit,o);else{var d=u-l;s.sample.unit.set(o.subarray(0,l),d),e.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else this.logger.warn("[tsdemuxer]: AC3 PES unknown PTS")},t.parseID3PES=function(e,t){if(void 0!==t.pts){var r=a({},t,{type:this._videoTrack?rn.emsg:rn.audioId3,duration:Number.POSITIVE_INFINITY});e.samples.push(r)}else this.logger.warn("[tsdemuxer]: ID3 PES unknown PTS")},e}();function wn(e,t){return((31&e[t+1])<<8)+e[t+2]}function On(e,t){return(31&e[t+10])<<8|e[t+11]}function xn(e,t,r,i,n,a){var s={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},o=t+3+((15&e[t+1])<<8|e[t+2])-4;for(t+=12+((15&e[t+10])<<8|e[t+11]);t0)for(var d=t+5,h=u;h>2;){106===e[d]&&(!0!==r.ac3?a.log("AC-3 audio found, not supported in this browser for now"):(s.audioPid=l,s.segmentAudioCodec="ac3"));var f=e[d+1]+2;d+=f,h-=f}break;case 194:case 135:return Mn(n,new Error("Unsupported EC-3 in M2TS found"),void 0,a),s;case 36:-1===s.videoPid&&(s.videoPid=l,s.segmentVideoCodec="hevc",a.log("HEVC in M2TS found"))}t+=u+5}return s}function Mn(e,t,r,i){i.warn("parsing error: "+t.message),e.emit(b.ERROR,b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_PARSING_ERROR,fatal:!1,levelRetry:r,error:t,reason:t.message})}function Fn(e,t){t.log(e+" with AES-128-CBC encryption found in unencrypted stream")}function Nn(e,t){var r,i,n,a,s,o=0,l=e.data;if(!e||0===e.size)return null;for(;l[0].length<19&&l.length>1;)l[0]=Le(l[0],l[1]),l.splice(1,1);if(1===((r=l[0])[0]<<16)+(r[1]<<8)+r[2]){if((i=(r[4]<<8)+r[5])&&i>e.size-6)return null;var u=r[7];192&u&&(a=536870912*(14&r[9])+4194304*(255&r[10])+16384*(254&r[11])+128*(255&r[12])+(254&r[13])/2,64&u?a-(s=536870912*(14&r[14])+4194304*(255&r[15])+16384*(254&r[16])+128*(255&r[17])+(254&r[18])/2)>54e5&&(t.warn(Math.round((a-s)/9e4)+"s delta between PTS and DTS, align them"),a=s):s=a);var d=(n=r[8])+9;if(e.size<=d)return null;e.size-=d;for(var h=new Uint8Array(e.size),f=0,c=l.length;fg){d-=g;continue}r=r.subarray(d),g-=d,d=0}h.set(r,o),o+=g}return i&&(i-=n+3),{data:h,pts:a,dts:s,len:i}}return null}var Un=function(){function e(){}return e.getSilentFrame=function(e,t){if("mp4a.40.2"===e){if(1===t)return new Uint8Array([0,200,0,128,35,128]);if(2===t)return new Uint8Array([33,0,73,144,2,25,0,35,128]);if(3===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,142]);if(4===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,128,44,128,8,2,56]);if(5===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,56]);if(6===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,0,178,0,32,8,224])}else{if(1===t)return new Uint8Array([1,64,34,128,163,78,230,128,186,8,0,0,0,28,6,241,193,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(2===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(3===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94])}},e}(),Bn=Math.pow(2,32)-1,Gn=function(){function e(){}return e.init=function(){var t;for(t in e.types={avc1:[],avcC:[],hvc1:[],hvcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],".mp3":[],dac3:[],"ac-3":[],mvex:[],mvhd:[],pasp:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[]},e.types)e.types.hasOwnProperty(t)&&(e.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);var r=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),i=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]);e.HDLR_TYPES={video:r,audio:i};var n=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),a=new Uint8Array([0,0,0,0,0,0,0,0]);e.STTS=e.STSC=e.STCO=a,e.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),e.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0]),e.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),e.STSD=new Uint8Array([0,0,0,0,0,0,0,1]);var s=new Uint8Array([105,115,111,109]),o=new Uint8Array([97,118,99,49]),l=new Uint8Array([0,0,0,1]);e.FTYP=e.box(e.types.ftyp,s,l,s,o),e.DINF=e.box(e.types.dinf,e.box(e.types.dref,n))},e.box=function(e){for(var t=8,r=arguments.length,i=new Array(r>1?r-1:0),n=1;n>24&255,o[1]=t>>16&255,o[2]=t>>8&255,o[3]=255&t,o.set(e,4),a=0,t=8;a>24&255,t>>16&255,t>>8&255,255&t,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},e.mdia=function(t){return e.box(e.types.mdia,e.mdhd(t.timescale||0,t.duration||0),e.hdlr(t.type),e.minf(t))},e.mfhd=function(t){return e.box(e.types.mfhd,new Uint8Array([0,0,0,0,t>>24,t>>16&255,t>>8&255,255&t]))},e.minf=function(t){return"audio"===t.type?e.box(e.types.minf,e.box(e.types.smhd,e.SMHD),e.DINF,e.stbl(t)):e.box(e.types.minf,e.box(e.types.vmhd,e.VMHD),e.DINF,e.stbl(t))},e.moof=function(t,r,i){return e.box(e.types.moof,e.mfhd(t),e.traf(i,r))},e.moov=function(t){for(var r=t.length,i=[];r--;)i[r]=e.trak(t[r]);return e.box.apply(null,[e.types.moov,e.mvhd(t[0].timescale||0,t[0].duration||0)].concat(i).concat(e.mvex(t)))},e.mvex=function(t){for(var r=t.length,i=[];r--;)i[r]=e.trex(t[r]);return e.box.apply(null,[e.types.mvex].concat(i))},e.mvhd=function(t,r){r*=t;var i=Math.floor(r/(Bn+1)),n=Math.floor(r%(Bn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,t>>24&255,t>>16&255,t>>8&255,255&t,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return e.box(e.types.mvhd,a)},e.sdtp=function(t){var r,i,n=t.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=e.box(e.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|t.sps.length].concat(a).concat([t.pps.length]).concat(s))),l=t.width,u=t.height,d=t.pixelRatio[0],h=t.pixelRatio[1];return e.box(e.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,e.box(e.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),e.box(e.types.pasp,new Uint8Array([d>>24,d>>16&255,d>>8&255,255&d,h>>24,h>>16&255,h>>8&255,255&h])))},e.esds=function(e){var t=e.config;return new Uint8Array([0,0,0,0,3,25,0,1,0,4,17,64,21,0,0,0,0,0,0,0,0,0,0,0,5,2].concat(t,[6,1,2]))},e.audioStsd=function(e){var t=e.samplerate||0;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,e.channelCount||0,0,16,0,0,0,0,t>>8&255,255&t,0,0])},e.mp4a=function(t){return e.box(e.types.mp4a,e.audioStsd(t),e.box(e.types.esds,e.esds(t)))},e.mp3=function(t){return e.box(e.types[".mp3"],e.audioStsd(t))},e.ac3=function(t){return e.box(e.types["ac-3"],e.audioStsd(t),e.box(e.types.dac3,t.config))},e.stsd=function(t){var r=t.segmentCodec;if("audio"===t.type){if("aac"===r)return e.box(e.types.stsd,e.STSD,e.mp4a(t));if("ac3"===r&&t.config)return e.box(e.types.stsd,e.STSD,e.ac3(t));if("mp3"===r&&"mp3"===t.codec)return e.box(e.types.stsd,e.STSD,e.mp3(t))}else{if(!t.pps||!t.sps)throw new Error("video track missing pps or sps");if("avc"===r)return e.box(e.types.stsd,e.STSD,e.avc1(t));if("hevc"===r&&t.vps)return e.box(e.types.stsd,e.STSD,e.hvc1(t))}throw new Error("unsupported "+t.type+" segment codec ("+r+"/"+t.codec+")")},e.tkhd=function(t){var r=t.id,i=(t.duration||0)*(t.timescale||0),n=t.width||0,a=t.height||0,s=Math.floor(i/(Bn+1)),o=Math.floor(i%(Bn+1));return e.box(e.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},e.traf=function(t,r){var i=e.sdtp(t),n=t.id,a=Math.floor(r/(Bn+1)),s=Math.floor(r%(Bn+1));return e.box(e.types.traf,e.box(e.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),e.box(e.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),e.trun(t,i.length+16+20+8+16+8+8),i)},e.trak=function(t){return t.duration=t.duration||4294967295,e.box(e.types.trak,e.tkhd(t),e.mdia(t))},e.trex=function(t){var r=t.id;return e.box(e.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},e.trun=function(t,r){var i,n,a,s,o,l,u=t.samples||[],d=u.length,h=12+16*d,f=new Uint8Array(h);for(r+=8+h,f.set(["video"===t.type?1:0,0,15,1,d>>>24&255,d>>>16&255,d>>>8&255,255&d,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return e.box(e.types.trun,f)},e.initSegment=function(t){e.types||e.init();var r=e.moov(t);return Le(e.FTYP,r)},e.hvc1=function(t){for(var r=t.params,i=[t.vps,t.sps,t.pps],n=new Uint8Array([1,r.general_profile_space<<6|(r.general_tier_flag?32:0)|r.general_profile_idc,r.general_profile_compatibility_flags[0],r.general_profile_compatibility_flags[1],r.general_profile_compatibility_flags[2],r.general_profile_compatibility_flags[3],r.general_constraint_indicator_flags[0],r.general_constraint_indicator_flags[1],r.general_constraint_indicator_flags[2],r.general_constraint_indicator_flags[3],r.general_constraint_indicator_flags[4],r.general_constraint_indicator_flags[5],r.general_level_idc,240|r.min_spatial_segmentation_idc>>8,255&r.min_spatial_segmentation_idc,252|r.parallelismType,252|r.chroma_format_idc,248|r.bit_depth_luma_minus8,248|r.bit_depth_chroma_minus8,0,parseInt(r.frame_rate.fps),3|r.temporal_id_nested<<2|r.num_temporal_layers<<3|(r.frame_rate.fixed?64:0),i.length]),a=n.length,s=0;s>8,255&i[d][h].length]),a),a+=2,l.set(i[d][h],a),a+=i[d][h].length}var f=e.box(e.types.hvcC,l),c=t.width,g=t.height,v=t.pixelRatio[0],m=t.pixelRatio[1];return e.box(e.types.hvc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,c>>8&255,255&c,g>>8&255,255&g,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),f,e.box(e.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),e.box(e.types.pasp,new Uint8Array([v>>24,v>>16&255,v>>8&255,255&v,m>>24,m>>16&255,m>>8&255,255&m])))},e}();Gn.types=void 0,Gn.HDLR_TYPES=void 0,Gn.STTS=void 0,Gn.STSC=void 0,Gn.STCO=void 0,Gn.STSZ=void 0,Gn.VMHD=void 0,Gn.SMHD=void 0,Gn.STSD=void 0,Gn.FTYP=void 0,Gn.DINF=void 0;var Kn=9e4;function Vn(e,t,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=e*t*r;return i?Math.round(n):n}function Hn(e,t){return Vn(e,1e3,1/Kn,t)}function Yn(e){var t=e.baseTime,r=e.timescale;return t/r+" ("+t+"/"+r+") trackId: "+e.trackId}var Wn=null,jn=null;function qn(e,t,r,i){return{duration:t,size:r,cts:i,flags:{isLeading:0,isDependedOn:0,hasRedundancy:0,degradPrio:0,dependsOn:e?2:1,isNonSync:e?0:1}}}var Xn=function(e){function t(t,r,i,n){var a;if((a=e.call(this,"mp4-remuxer",n)||this).observer=void 0,a.config=void 0,a.typeSupported=void 0,a.ISGenerated=!1,a._initPTS=null,a._initDTS=null,a.nextVideoTs=null,a.nextAudioTs=null,a.videoSampleDuration=null,a.isAudioContiguous=!1,a.isVideoContiguous=!1,a.videoTrackConfig=void 0,a.observer=t,a.config=r,a.typeSupported=i,a.ISGenerated=!1,null===Wn){var s=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Wn=s?parseInt(s[1]):0}if(null===jn){var o=navigator.userAgent.match(/Safari\/(\d+)/i);jn=o?parseInt(o[1]):0}return a}o(t,e);var r=t.prototype;return r.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},r.resetTimeStamp=function(e){var t=this._initPTS;t&&e&&e.trackId===t.trackId&&e.baseTime===t.baseTime&&e.timescale===t.timescale||this.log("Reset initPTS: "+(t?Yn(t):t)+" > "+(e?Yn(e):e)),this._initPTS=this._initDTS=e},r.resetNextTimestamp=function(){this.log("reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},r.resetInitSegment=function(){this.log("ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},r.getVideoStartPts=function(e){var t=!1,r=e[0].pts,i=e.reduce((function(e,i){var n=i.pts,a=n-e;return a<-4294967296&&(t=!0,a=(n=Qn(n,r))-e),a>0?e:n}),r);return t&&this.debug("PTS rollover detected"),i},r.remux=function(e,t,r,i,n,a,s,o){var l,u,d,h,f,c,g=n,v=n,m=e.pid>-1,p=t.pid>-1,y=t.samples.length,E=e.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,A,L,I,R=this.videoTrackConfig;(R&&(t.width!==R.width||t.height!==R.height||(null==(S=t.pixelRatio)?void 0:S[0])!==(null==(A=R.pixelRatio)?void 0:A[0])||(null==(L=t.pixelRatio)?void 0:L[1])!==(null==(I=R.pixelRatio)?void 0:I[1]))||!R&&T||null===this.nextAudioTs&&E)&&this.resetInitSegment()}this.ISGenerated||(d=this.generateIS(e,t,n,a));var k,b=this.isVideoContiguous,D=-1;if(T&&(D=function(e){for(var t=0;t0){this.warn("Dropped "+D+" out of "+y+" video samples due to a missing keyframe");var _=this.getVideoStartPts(t.samples);t.samples=t.samples.slice(D),t.dropped+=D,k=v+=(t.samples[0].pts-_)/t.inputTimeScale}else-1===D&&(this.warn("No keyframe found out of "+y+" video samples"),c=!1);if(this.ISGenerated){if(E&&T){var P=this.getVideoStartPts(t.samples),C=(Qn(e.samples[0].pts,P)-P)/t.inputTimeScale;g+=Math.max(0,C),v+=Math.max(0,-C)}if(E){if(e.samplerate||(this.warn("regenerate InitSegment as audio detected"),d=this.generateIS(e,t,n,a)),u=this.remuxAudio(e,g,this.isAudioContiguous,a,p||T||o===O?v:void 0),T){var w=u?u.endPTS-u.startPTS:0;t.inputTimeScale||(this.warn("regenerate InitSegment as video detected"),d=this.generateIS(e,t,n,a)),l=this.remuxVideo(t,v,b,w)}}else T&&(l=this.remuxVideo(t,v,b,0));l&&(l.firstKeyFrame=D,l.independent=-1!==D,l.firstKeyFramePTS=k)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(f=zn(r,n,this._initPTS,this._initDTS)),i.samples.length&&(h=$n(i,n,this._initPTS))),{audio:u,video:l,initSegment:d,independent:c,text:h,id3:f}},r.computeInitPts=function(e,t,r,i){var n=Math.round(r*t),a=Qn(e,n);if(a0?A-1:A].dts&&(y=!0)}y&&l.sort((function(e,t){var r=e.dts-t.dts,i=e.pts-t.pts;return r||i})),n=l[0].dts;var I=(s=l[l.length-1].dts)-n,D=I?Math.round(I/(d-1)):v||e.inputTimeScale/30;if(r){var _=n-S,P=_>D,C=_<-1;if((P||C)&&(P?this.warn((e.segmentCodec||"").toUpperCase()+": "+Hn(_,!0)+" ms ("+_+"dts) hole between fragments detected at "+t.toFixed(3)):this.warn((e.segmentCodec||"").toUpperCase()+": "+Hn(-_,!0)+" ms ("+_+"dts) overlapping between fragments detected at "+t.toFixed(3)),!C||S>=l[0].pts||Wn)){n=S;var w=l[0].pts-_;if(P)l[0].dts=n,l[0].pts=w;else for(var O=!0,x=0;xw&&O);x++){var M=l[x].pts;if(l[x].dts-=_,l[x].pts-=_,x0?te.dts-l[ee-1].dts:D;if(ue=ee>0?te.pts-l[ee-1].pts:D,de.stretchShortVideoTrack&&null!==this.nextAudioTs){var fe=Math.floor(de.maxBufferHole*o),ce=(i?m+i*o:this.nextAudioTs+f)-te.pts;ce>fe?((v=ce-he)<0?v=he:Q=!0,this.log("It is approximately "+ce/90+" ms to the next segment; using duration "+v/90+" ms for the last video frame.")):v=he}else v=he}var ge=Math.round(te.pts-te.dts);z=Math.min(z,v),Z=Math.max(Z,v),$=Math.min($,ue),J=Math.max(J,ue),u.push(qn(te.key,v,ie,ge))}if(u.length)if(Wn){if(Wn<70){var ve=u[0].flags;ve.dependsOn=2,ve.isNonSync=0}}else if(jn&&J-$0&&(i&&Math.abs(y-(m+p))<9e3||Math.abs(Qn(g[0].pts,y)-(m+p))<20*u),g.forEach((function(e){e.pts=Qn(e.pts,y)})),!r||m<0){var E=g.length;if(g=g.filter((function(e){return e.pts>=0})),E!==g.length&&this.warn("Removed "+(g.length-E)+" of "+E+" samples (initPTS "+p+" / "+s+")"),!g.length)return;m=0===n?0:i&&!c?Math.max(0,y-p):g[0].pts-p}if("aac"===e.segmentCodec)for(var T=this.config.maxAudioFramesDrift,S=0,A=m+p;S=T*u&&_<1e4&&c){var P=Math.round(D/u);for(A=I-P*u;A<0&&P&&u;)P--,A+=u;0===S&&(this.nextAudioTs=m=A-p),this.warn("Injecting "+P+" audio frames @ "+((A-p)/s).toFixed(3)+"s due to "+Math.round(1e3*D/s)+" ms gap.");for(var C=0;C0))return;F+=v;try{O=new Uint8Array(F)}catch(e){return void this.observer.emit(b.ERROR,b.ERROR,{type:R.MUX_ERROR,details:k.REMUX_ALLOC_ERROR,fatal:!1,error:e,bytes:F,reason:"fail allocating audio mdat "+F})}h||(new DataView(O.buffer).setUint32(0,F),O.set(Gn.types.mdat,4))}O.set(K,v);var H=K.byteLength;v+=H,f.push(qn(!0,l,H,0)),M=V}var Y=f.length;if(Y){var W=f[f.length-1];m=M-p,this.nextAudioTs=m+o*W.duration;var j=h?new Uint8Array(0):Gn.moof(e.sequenceNumber++,x/o,a({},e,{samples:f}));e.samples=[];var q=(x-p)/s,X=this.nextAudioTs/s,Q={data1:j,data2:O,startPTS:q,endPTS:X,startDTS:q,endDTS:X,type:"audio",hasAudio:!0,hasVideo:!1,nb:Y};return this.isAudioContiguous=!0,Q}},t}(N);function Qn(e,t){var r;if(null===t)return e;for(r=t4294967296;)e+=r;return e}function zn(e,t,r,i){var n=e.samples.length;if(n){for(var a=e.inputTimeScale,s=0;ssinf>>tenc' box: "+X(i)+" -> "+X(r)),e.set(r,8))}))}}(e,t);else{var o=a||s;null!=o&&o.encrypted&&this.warn('Init segment with encrypted track with has no key ("'+o.codec+'")!')}a&&(r=ta(a,$,this)),s&&(i=ta(s,Z,this));var l={};a&&s?l.audiovideo={container:"video/mp4",codec:r+","+i,supplemental:s.supplemental,encrypted:s.encrypted,initSegment:e,id:"main"}:a?l.audio={container:"audio/mp4",codec:r,encrypted:a.encrypted,initSegment:e,id:"audio"}:s?l.video={container:"video/mp4",codec:i,supplemental:s.supplemental,encrypted:s.encrypted,initSegment:e,id:"main"}:this.warn("initSegment does not contain moov or trak boxes."),this.initTracks=l},r.remux=function(e,t,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,d={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};A(u)||(u=this.lastEndTime=n||0);var h=t.samples;if(!h.length)return d;var f={initPTS:void 0,timescale:void 0,trackId:void 0},c=this.initData;if(null!=(s=c)&&s.length||(this.generateInitSegment(h),c=this.initData),null==(o=c)||!o.length)return this.warn("Failed to generate initSegment."),d;this.emitInitSegment&&(f.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(e,t,r){for(var i={},n=ce(e,["moof","traf"]),a=0;an}(l,S,n,L)&&k===l.timescale||(l&&this.warn("Timestamps at playlist time: "+(a?"":"~")+n+" "+b/k+" != initPTS: "+l.baseTime/l.timescale+" ("+l.baseTime+"/"+l.timescale+") trackId: "+l.trackId),this.log("Found initPTS at playlist time: "+n+" offset: "+(S-n)+" ("+b+"/"+k+") trackId: "+D),l=null,f.initPTS=b,f.timescale=k,f.trackId=D)}else this.warn("No audio or video samples found for initPTS at playlist time: "+n);l?(f.initPTS=l.baseTime,f.timescale=l.timescale,f.trackId=l.trackId):(f.timescale&&void 0!==f.trackId&&void 0!==f.initPTS||(this.warn("Could not set initPTS"),f.initPTS=S,f.timescale=1,f.trackId=-1),this.initPTS=l={baseTime:f.initPTS,timescale:f.timescale,trackId:f.trackId});var _=S-l.baseTime/l.timescale,P=_+L;L>0?this.lastEndTime=P:(this.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var C=!!c.audio,w=!!c.video,O="";C&&(O+="audio"),w&&(O+="video");var x={data1:h,startPTS:_,startDTS:_,endPTS:P,endDTS:P,type:O,hasAudio:C,hasVideo:w,nb:1,dropped:0,encrypted:!!c.audio&&c.audio.encrypted||!!c.video&&c.video.encrypted};d.audio=C&&!w?x:void 0,d.video=w?x:void 0;var M=null==m?void 0:m.sampleCount;if(M){var F=m.keyFrameIndex,N=-1!==F;x.nb=M,x.dropped=0===F||this.isVideoContiguous?0:N?F:M,x.independent=N,x.firstKeyFrame=F,N&&m.keyFrameStart&&(x.firstKeyFramePTS=(m.keyFrameStart-l.baseTime)/l.timescale),this.isVideoContiguous||(d.independent=N),this.isVideoContiguous||(this.isVideoContiguous=N),x.dropped&&this.warn("fmp4 does not start with IDR: firstIDR "+F+"/"+M+" dropped: "+x.dropped+" start: "+(x.firstKeyFramePTS||"NA"))}return d.initSegment=f,d.id3=zn(r,n,l,l),i.samples.length&&(d.text=$n(i,n,l)),d},t}(N);function ea(e,t,r){return void 0===r&&(r=!1),void 0!==(null==e?void 0:e.start)?(e.start+(r?e.duration:0))/e.timescale:t}function ta(e,t,r){var i=e.codec;return i&&i.length>4?i:t===$?"ec-3"===i||"ac-3"===i||"alac"===i?i:"fLaC"===i||"Opus"===i?Ke(i,!1):(r.warn('Unhandled audio codec "'+i+'" in mp4 MAP'),i||"mp4a"):(r.warn('Unhandled video codec "'+i+'" in mp4 MAP'),i||"avc1")}try{Zn=self.performance.now.bind(self.performance)}catch(e){Zn=Date.now}var ra=[{demux:Ln,remux:Jn},{demux:Cn,remux:Xn},{demux:pn,remux:Xn},{demux:Sn,remux:Xn}];ra.splice(2,0,{demux:En,remux:Xn});var ia=function(){function e(e,t,r,i,n,a){this.asyncResult=!1,this.logger=void 0,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=e,this.typeSupported=t,this.config=r,this.id=n,this.logger=a}var t=e.prototype;return t.configure=function(e){this.transmuxConfig=e,this.decrypter&&this.decrypter.reset()},t.push=function(e,t,r,i){var n=this,a=r.transmuxing;a.executeStart=Zn();var s=new Uint8Array(e),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,d=u.contiguous,h=u.discontinuity,f=u.trackSwitch,c=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,S=function(e,t){var r=null;return e.byteLength>0&&null!=(null==t?void 0:t.key)&&null!==t.iv&&null!=t.method&&(r=t),r}(s,t);if(S&&Ir(S.method)){var A=this.getDecrypter(),L=Rr(S.method);if(!A.isSync())return this.asyncResult=!0,this.decryptionPromise=A.webCryptoDecrypt(s,S.key.buffer,S.iv.buffer,L).then((function(e){var t=n.push(e,null,r);return n.decryptionPromise=null,t})),this.decryptionPromise;var I=A.softwareDecrypt(s,S.key.buffer,S.iv.buffer,L);if(r.part>-1){var D=A.flush();I=D?D.buffer:D}if(!I)return a.executeEnd=Zn(),na(r);s=new Uint8Array(I)}var _=this.needsProbing(h,f);if(_){var P=this.configureTransmuxer(s);if(P)return this.logger.warn("[transmuxer] "+P.message),this.observer.emit(b.ERROR,b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_PARSING_ERROR,fatal:!1,error:P,reason:P.message}),a.executeEnd=Zn(),na(r)}(h||f||v||_)&&this.resetInitSegment(T,m,p,E,t),(h||v||_)&&this.resetInitialTimestamp(y),d||this.resetContiguity();var C=this.transmux(s,S,g,c,r);this.asyncResult=aa(C);var w=this.currentTransmuxState;return w.contiguous=!0,w.discontinuity=!1,w.trackSwitch=!1,a.executeEnd=Zn(),C},t.flush=function(e){var t=this,r=e.transmuxing;r.executeStart=Zn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return this.asyncResult=!0,a.then((function(){return t.flush(e)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l.buffer,null,e))}var u=this.demuxer,d=this.remuxer;if(!u||!d){r.executeEnd=Zn();var h=[na(e)];return this.asyncResult?Promise.resolve(h):h}var f=u.flush(o);return aa(f)?(this.asyncResult=!0,f.then((function(r){return t.flushRemux(s,r,e),s}))):(this.flushRemux(s,f,e),this.asyncResult?Promise.resolve(s):s)},t.flushRemux=function(e,t,r){var i=t.audioTrack,n=t.videoTrack,a=t.id3Track,s=t.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;this.logger.log("[transmuxer.ts]: Flushed "+this.id+" sn: "+r.sn+(r.part>-1?" part: "+r.part:"")+" of "+(this.id===w?"level":"track")+" "+r.level);var d=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);e.push({remuxResult:d,chunkMeta:r}),r.transmuxing.executeEnd=Zn()},t.resetInitialTimestamp=function(e){var t=this.demuxer,r=this.remuxer;t&&r&&(t.resetTimeStamp(e),r.resetTimeStamp(e))},t.resetContiguity=function(){var e=this.demuxer,t=this.remuxer;e&&t&&(e.resetContiguity(),t.resetNextTimestamp())},t.resetInitSegment=function(e,t,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(e,t,r,i),s.resetInitSegment(e,t,r,n))},t.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},t.transmux=function(e,t,r,i,n){return t&&"SAMPLE-AES"===t.method?this.transmuxSampleAes(e,t,r,i,n):this.transmuxUnencrypted(e,r,i,n)},t.transmuxUnencrypted=function(e,t,r,i){var n=this.demuxer.demux(e,t,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,t,r,!1,this.id),chunkMeta:i}},t.transmuxSampleAes=function(e,t,r,i,n){var a=this;return this.demuxer.demuxSampleAes(e,t,r).then((function(e){return{remuxResult:a.remuxer.remux(e.audioTrack,e.videoTrack,e.id3Track,e.textTrack,r,i,!1,a.id),chunkMeta:n}}))},t.configureTransmuxer=function(e){for(var t,r=this.config,i=this.observer,n=this.typeSupported,a=0,s=ra.length;a1&&l.id===(null==p?void 0:p.stats.chunkCount),L=!E&&(1===T||0===T&&(1===S||A&&S<=0)),I=self.performance.now();(E||T||0===n.stats.parsing.start)&&(n.stats.parsing.start=I),!a||!S&&L||(a.stats.parsing.start=I);var R=!(p&&(null==(d=n.initSegment)?void 0:d.url)===(null==(h=p.initSegment)?void 0:h.url)),k=new oa(y,L,o,E,v,R);if(!L||y||R){this.hls.logger.log("[transmuxer-interface]: Starting new transmux session for "+n.type+" sn: "+l.sn+(l.part>-1?" part: "+l.part:"")+" "+(this.id===w?"level":"track")+": "+l.level+" id: "+l.id+"\n discontinuity: "+y+"\n trackSwitch: "+E+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+v+"\n initSegmentChange: "+R);var b=new sa(r,i,t,s,u);this.configureTransmuxer(b)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({instanceNo:c,cmd:"demux",data:e,decryptdata:m,chunkMeta:l,state:k},e instanceof ArrayBuffer?[e]:[]);else if(g){var D=g.push(e,m,l,k);aa(D)?D.then((function(e){f.handleTransmuxComplete(e)})).catch((function(e){f.transmuxerError(e,l,"transmuxer-interface push error")})):this.handleTransmuxComplete(D)}},r.flush=function(e){var t=this;e.transmuxing.start=self.performance.now();var r=this.instanceNo,i=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({instanceNo:r,cmd:"flush",chunkMeta:e});else if(i){var n=i.flush(e);aa(n)?n.then((function(r){t.handleFlushResult(r,e)})).catch((function(r){t.transmuxerError(r,e,"transmuxer-interface flush error")})):this.handleFlushResult(n,e)}},r.transmuxerError=function(e,t,r){this.hls&&(this.error=e,this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_PARSING_ERROR,chunkMeta:t,frag:this.frag||void 0,part:this.part||void 0,fatal:!1,error:e,err:e,reason:r}))},r.handleFlushResult=function(e,t){var r=this;e.forEach((function(e){r.handleTransmuxComplete(e)})),this.onFlush(t)},r.configureTransmuxer=function(e){var t=this.instanceNo,r=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({instanceNo:t,cmd:"configure",config:e}):r&&r.configure(e)},r.handleTransmuxComplete=function(e){e.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(e)},t}(),pa=function(e){function t(t,r,i){var n;return(n=e.call(this,t,r,i,"audio-stream-controller",O)||this).mainAnchor=null,n.mainFragLoading=null,n.audioOnly=!1,n.bufferedTrack=null,n.switchingTrack=null,n.trackId=-1,n.waitingData=null,n.mainDetails=null,n.flushing=!1,n.bufferFlushed=!1,n.cachedTrackLoadedData=null,n.registerListeners(),n}o(t,e);var r=t.prototype;return r.onHandlerDestroying=function(){this.unregisterListeners(),e.prototype.onHandlerDestroying.call(this),this.resetItem()},r.resetItem=function(){this.mainDetails=this.mainAnchor=this.mainFragLoading=this.bufferedTrack=this.switchingTrack=this.waitingData=this.cachedTrackLoadedData=null},r.registerListeners=function(){e.prototype.registerListeners.call(this);var t=this.hls;t.on(b.LEVEL_LOADED,this.onLevelLoaded,this),t.on(b.AUDIO_TRACKS_UPDATED,this.onAudioTracksUpdated,this),t.on(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),t.on(b.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.on(b.BUFFER_RESET,this.onBufferReset,this),t.on(b.BUFFER_CREATED,this.onBufferCreated,this),t.on(b.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(b.BUFFER_FLUSHED,this.onBufferFlushed,this),t.on(b.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(b.FRAG_LOADING,this.onFragLoading,this),t.on(b.FRAG_BUFFERED,this.onFragBuffered,this)},r.unregisterListeners=function(){var t=this.hls;t&&(e.prototype.unregisterListeners.call(this),t.off(b.LEVEL_LOADED,this.onLevelLoaded,this),t.off(b.AUDIO_TRACKS_UPDATED,this.onAudioTracksUpdated,this),t.off(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),t.off(b.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.off(b.BUFFER_RESET,this.onBufferReset,this),t.off(b.BUFFER_CREATED,this.onBufferCreated,this),t.off(b.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(b.BUFFER_FLUSHED,this.onBufferFlushed,this),t.off(b.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(b.FRAG_LOADING,this.onFragLoading,this),t.off(b.FRAG_BUFFERED,this.onFragBuffered,this))},r.onInitPtsFound=function(e,t){var r=t.frag,i=t.id,n=t.initPTS,a=t.timescale,s=t.trackId;if(i===w){var o=r.cc,l=this.fragCurrent;if(this.initPTS[o]={baseTime:n,timescale:a,trackId:s},this.log("InitPTS for cc: "+o+" found from main: "+n/a+" ("+n+"/"+a+") trackId: "+s),this.mainAnchor=r,this.state===_i.WAITING_INIT_PTS){var u=this.waitingData;(!u&&!this.loadingParts||u&&u.frag.cc!==o)&&this.syncWithAnchor(r,null==u?void 0:u.frag)}else!this.hls.hasEnoughToStart&&l&&l.cc!==o?(l.abortRequests(),this.syncWithAnchor(r,l)):this.state===_i.IDLE&&this.tick()}},r.getLoadPosition=function(){return!this.startFragRequested&&this.nextLoadPosition>=0?this.nextLoadPosition:e.prototype.getLoadPosition.call(this)},r.syncWithAnchor=function(e,t){var r,i=(null==(r=this.mainFragLoading)?void 0:r.frag)||null;if(!t||(null==i?void 0:i.cc)!==t.cc){var n=(i||e).cc,a=Lt(this.getLevelDetails(),n,this.getLoadPosition());a&&(this.log("Syncing with main frag at "+a.start+" cc "+a.cc),this.startFragRequested=!1,this.nextLoadPosition=a.start,this.resetLoadingState(),this.state===_i.IDLE&&this.doTickIdle())}},r.startLoad=function(e,t){if(!this.levels)return this.startPosition=e,void(this.state=_i.STOPPED);var r=this.lastCurrentTime;this.stopLoad(),this.setInterval(100),r>0&&-1===e?(this.log("Override startPosition with lastCurrentTime @"+r.toFixed(3)),e=r,this.state=_i.IDLE):this.state=_i.WAITING_TRACK,this.nextLoadPosition=this.lastCurrentTime=e+this.timelineOffset,this.startPosition=t?-1:e,this.tick()},r.doTick=function(){switch(this.state){case _i.IDLE:this.doTickIdle();break;case _i.WAITING_TRACK:var t=this.levels,r=this.trackId,i=null==t?void 0:t[r],n=null==i?void 0:i.details;if(n&&!this.waitForLive(i)){if(this.waitForCdnTuneIn(n))break;this.state=_i.WAITING_INIT_PTS}break;case _i.FRAG_LOADING_WAITING_RETRY:this.checkRetryDate();break;case _i.WAITING_INIT_PTS:var a=this.waitingData;if(a){var s=a.frag,o=a.part,l=a.cache,u=a.complete,d=this.mainAnchor;if(void 0!==this.initPTS[s.cc]){this.waitingData=null,this.state=_i.FRAG_LOADING;var h={frag:s,part:o,payload:l.flush().buffer,networkDetails:null};this._handleFragmentLoadProgress(h),u&&e.prototype._handleFragmentLoadComplete.call(this,h)}else d&&d.cc!==a.frag.cc&&this.syncWithAnchor(d,a.frag)}else this.state=_i.IDLE}this.onTickEnd()},r.resetLoadingState=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null),e.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var e=this.media;null!=e&&e.readyState&&(this.lastCurrentTime=e.currentTime)},r.doTickIdle=function(){var e,t=this.hls,r=this.levels,i=this.media,n=this.trackId,a=t.config;if(this.buffering&&(i||this.primaryPrefetch||!this.startFragRequested&&a.startFragPrefetch)&&null!=r&&r[n]){var s=r[n],o=s.details;if(!o||this.waitForLive(s)||this.waitForCdnTuneIn(o))return this.state=_i.WAITING_TRACK,void(this.startFragRequested=!1);var l=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&l&&(this.bufferFlushed=!1,this.afterBufferFlushed(l,$,O));var u=this.getFwdBufferInfo(l,O);if(null!==u){if(!this.switchingTrack&&this._streamEnded(u,o))return t.trigger(b.BUFFER_EOS,{type:"audio"}),void(this.state=_i.ENDED);var d=u.len,h=t.maxBufferLength,f=o.fragments,c=f[0].start,g=this.getLoadPosition(),v=this.flushing?g:u.end;if(this.switchingTrack&&i){var m=g;o.PTSKnown&&mc||u.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),i.currentTime=c+.05)}if(!(d>=h&&!this.switchingTrack&&vy.end){var E=this.fragmentTracker.getFragAtPos(v,w);E&&E.end>y.end&&(y=E,this.mainFragLoading={frag:E,targetBufferTime:null})}if(p.start>y.end)return}this.loadFragment(p,s,v)}else this.bufferFlushed=!0}}}},r.onMediaDetaching=function(t,r){this.bufferFlushed=this.flushing=!1,e.prototype.onMediaDetaching.call(this,t,r)},r.onAudioTracksUpdated=function(e,t){var r=t.audioTracks;this.resetTransmuxer(),this.levels=r.map((function(e){return new st(e)}))},r.onAudioTrackSwitching=function(e,t){var r=!!t.url;this.trackId=t.id;var i=this.fragCurrent;i&&(i.abortRequests(),this.removeUnbufferedFrags(i.start)),this.resetLoadingState(),r?(this.switchingTrack=t,this.flushAudioIfNeeded(t),this.state!==_i.STOPPED&&(this.setInterval(100),this.state=_i.IDLE,this.tick())):(this.resetTransmuxer(),this.switchingTrack=null,this.bufferedTrack=t,this.clearInterval())},r.onManifestLoading=function(){e.prototype.onManifestLoading.call(this),this.bufferFlushed=this.flushing=this.audioOnly=!1,this.resetItem(),this.trackId=-1},r.onLevelLoaded=function(e,t){this.mainDetails=t.details;var r=this.cachedTrackLoadedData;r&&(this.cachedTrackLoadedData=null,this.onAudioTrackLoaded(b.AUDIO_TRACK_LOADED,r))},r.onAudioTrackLoaded=function(e,t){var r,i=this.levels,n=t.details,a=t.id,s=t.groupId,o=t.track;if(i){var l=this.mainDetails;if(!l||n.endCC>l.endCC||l.expired)return this.cachedTrackLoadedData=t,void(this.state!==_i.STOPPED&&(this.state=_i.WAITING_TRACK));this.cachedTrackLoadedData=null,this.log("Audio track "+a+' "'+o.name+'" of "'+s+'" loaded ['+n.startSN+","+n.endSN+"]"+(n.lastPartSn?"[part-"+n.lastPartSn+"-"+n.lastPartIndex+"]":"")+",duration:"+n.totalduration);var u=i[a],d=0;if(n.live||null!=(r=u.details)&&r.live){if(this.checkLiveUpdate(n),n.deltaUpdateFailed)return;var h;u.details&&(d=this.alignPlaylists(n,u.details,null==(h=this.levelLastLoaded)?void 0:h.details)),n.alignedSliding||(Ii(n,l),n.alignedSliding||Ri(n,l),d=n.fragmentStart)}u.details=n,this.levelLastLoaded=u,this.startFragRequested||this.setStartPosition(l,d),this.hls.trigger(b.AUDIO_TRACK_UPDATED,{details:n,id:a,groupId:t.groupId}),this.state!==_i.WAITING_TRACK||this.waitForCdnTuneIn(n)||(this.state=_i.IDLE),this.tick()}else this.warn("Audio tracks reset while loading track "+a+' "'+o.name+'" of "'+s+'"')},r._handleFragmentLoadProgress=function(e){var t,r=e.frag,i=e.part,n=e.payload,a=this.config,s=this.trackId,o=this.levels;if(o){var l=o[s];if(l){var u=l.details;if(!u)return this.warn("Audio track details undefined on fragment load progress"),void this.removeUnbufferedFrags(r.start);var d=a.defaultAudioCodec||l.audioCodec||"mp4a.40.2",h=this.transmuxer;h||(h=this.transmuxer=new ma(this.hls,O,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)));var f=this.initPTS[r.cc],c=null==(t=r.initSegment)?void 0:t.data;if(void 0!==f){var g=i?i.index:-1,v=-1!==g,m=new lr(r.level,r.sn,r.stats.chunkCount,n.byteLength,g,v);h.push(n,c,d,"",r,i,u.totalduration,!1,m,f)}else this.log("Unknown video PTS for cc "+r.cc+", waiting for video PTS before demuxing audio frag "+r.sn+" of ["+u.startSN+" ,"+u.endSN+"],track "+s),(this.waitingData=this.waitingData||{frag:r,part:i,cache:new wi,complete:!1}).cache.push(new Uint8Array(n)),this.state!==_i.STOPPED&&(this.state=_i.WAITING_INIT_PTS)}else this.warn("Audio track is undefined on fragment load progress")}else this.warn("Audio tracks were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r._handleFragmentLoadComplete=function(t){this.waitingData?this.waitingData.complete=!0:e.prototype._handleFragmentLoadComplete.call(this,t)},r.onBufferReset=function(){this.mediaBuffer=null},r.onBufferCreated=function(e,t){this.bufferFlushed=this.flushing=!1;var r=t.tracks.audio;r&&(this.mediaBuffer=r.buffer||null)},r.onFragLoading=function(e,t){!this.audioOnly&&t.frag.type===w&&te(t.frag)&&(this.mainFragLoading=t,this.state===_i.IDLE&&this.tick())},r.onFragBuffered=function(e,t){var r=t.frag,i=t.part;if(r.type===O)if(this.fragContextChanged(r))this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state+", audioSwitch: "+(this.switchingTrack?this.switchingTrack.name:"false"));else{if(te(r)){this.fragPrevious=r;var n=this.switchingTrack;n&&(this.bufferedTrack=n,this.switchingTrack=null,this.hls.trigger(b.AUDIO_TRACK_SWITCHED,d({},n)))}this.fragBufferedComplete(r,i),this.media&&this.tick()}else this.audioOnly||r.type!==w||r.elementaryStreams.video||r.elementaryStreams.audiovideo||(this.audioOnly=!0,this.mainFragLoading=null)},r.onError=function(t,r){var i;if(r.fatal)this.state=_i.ERROR;else switch(r.details){case k.FRAG_GAP:case k.FRAG_PARSING_ERROR:case k.FRAG_DECRYPT_ERROR:case k.FRAG_LOAD_ERROR:case k.FRAG_LOAD_TIMEOUT:case k.KEY_LOAD_ERROR:case k.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(O,r);break;case k.AUDIO_TRACK_LOAD_ERROR:case k.AUDIO_TRACK_LOAD_TIMEOUT:case k.LEVEL_PARSING_ERROR:r.levelRetry||this.state!==_i.WAITING_TRACK||(null==(i=r.context)?void 0:i.type)!==P||(this.state=_i.IDLE);break;case k.BUFFER_ADD_CODEC_ERROR:case k.BUFFER_APPEND_ERROR:if("audio"!==r.parent)return;this.reduceLengthAndFlushBuffer(r)||this.resetLoadingState();break;case k.BUFFER_FULL_ERROR:if("audio"!==r.parent)return;this.reduceLengthAndFlushBuffer(r)&&(this.bufferedTrack=null,e.prototype.flushMainBuffer.call(this,0,Number.POSITIVE_INFINITY,"audio"));break;case k.INTERNAL_EXCEPTION:this.recoverWorkerError(r)}},r.onBufferFlushing=function(e,t){t.type!==Z&&(this.flushing=!0)},r.onBufferFlushed=function(e,t){var r=t.type;if(r!==Z){this.flushing=!1,this.bufferFlushed=!0,this.state===_i.ENDED&&(this.state=_i.IDLE);var i=this.mediaBuffer||this.media;i&&(this.afterBufferFlushed(i,r,O),this.tick())}},r._handleTransmuxComplete=function(e){var t,r="audio",i=this.hls,n=e.remuxResult,s=e.chunkMeta,o=this.getCurrentContext(s);if(o){var l=o.frag,u=o.part,d=o.level,h=d.details,f=n.audio,c=n.text,g=n.id3,v=n.initSegment;if(!this.fragContextChanged(l)&&h){if(this.state=_i.PARSING,this.switchingTrack&&f&&this.completeAudioSwitch(this.switchingTrack),null!=v&&v.tracks){var m=l.initSegment||l;if(this.unhandledEncryptionError(v,l))return;this._bufferInitSegment(d,v.tracks,m,s),i.trigger(b.FRAG_PARSING_INIT_SEGMENT,{frag:m,id:r,tracks:v.tracks})}if(f){var p=f.startPTS,y=f.endPTS,E=f.startDTS,T=f.endDTS;u&&(u.elementaryStreams[$]={startPTS:p,endPTS:y,startDTS:E,endDTS:T}),l.setElementaryStreamInfo($,p,y,E,T),this.bufferFragmentData(f,l,u,s)}if(null!=g&&null!=(t=g.samples)&&t.length){var S=a({id:r,frag:l,details:h},g);i.trigger(b.FRAG_PARSING_METADATA,S)}if(c){var A=a({id:r,frag:l,details:h},c);i.trigger(b.FRAG_PARSING_USERDATA,A)}}else this.fragmentTracker.removeFragment(l)}else this.resetWhenMissingContext(s)},r._bufferInitSegment=function(e,t,r,i){if(this.state===_i.PARSING&&(t.video&&delete t.video,t.audiovideo&&delete t.audiovideo,t.audio)){var n=t.audio;n.id=O;var a=e.audioCodec;this.log("Init audio buffer, container:"+n.container+", codecs[level/parsed]=["+a+"/"+n.codec+"]"),a&&1===a.split(",").length&&(n.levelCodec=a),this.hls.trigger(b.BUFFER_CODECS,t);var s=n.initSegment;if(null!=s&&s.byteLength){var o={type:"audio",frag:r,part:null,chunkMeta:i,parent:r.type,data:s};this.hls.trigger(b.BUFFER_APPENDING,o)}this.tickImmediate()}},r.loadFragment=function(t,r,i){var n,a=this.fragmentTracker.getState(t);if(this.switchingTrack||a===Vt||a===Yt)if(te(t))if(null!=(n=r.details)&&n.live&&!this.initPTS[t.cc]){this.log("Waiting for video PTS in continuity counter "+t.cc+" of live stream before loading audio fragment "+t.sn+" of level "+this.trackId),this.state=_i.WAITING_INIT_PTS;var s=this.mainDetails;s&&s.fragmentStart!==r.details.fragmentStart&&Ri(r.details,s)}else e.prototype.loadFragment.call(this,t,r,i);else this._loadInitSegment(t,r);else this.clearTrackerIfNeeded(t)},r.flushAudioIfNeeded=function(t){if(this.media&&this.bufferedTrack){var r=this.bufferedTrack;gt({name:r.name,lang:r.lang,assocLang:r.assocLang,characteristics:r.characteristics,audioCodec:r.audioCodec,channels:r.channels},t,vt)||(pt(t.url,this.hls)?(this.log("Switching audio track : flushing all audio"),e.prototype.flushMainBuffer.call(this,0,Number.POSITIVE_INFINITY,"audio"),this.bufferedTrack=null):this.bufferedTrack=t)}},r.completeAudioSwitch=function(e){var t=this.hls;this.flushAudioIfNeeded(e),this.bufferedTrack=e,this.switchingTrack=null,t.trigger(b.AUDIO_TRACK_SWITCHED,d({},e))},t}(Pi),ya=function(e){function t(t,r){var i;return(i=e.call(this,r,t.logger)||this).hls=void 0,i.canLoad=!1,i.timer=-1,i.hls=t,i}o(t,e);var r=t.prototype;return r.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},r.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},r.startLoad=function(){this.canLoad=!0,this.loadPlaylist()},r.stopLoad=function(){this.canLoad=!1,this.clearTimer()},r.switchParams=function(e,t,r){var i=null==t?void 0:t.renditionReports;if(i){for(var n=-1,a=0;a=0&&h>t.partTarget&&(d+=1)}var f=r&&nt(r);return new at(u,d>=0?d:void 0,f)}}},r.loadPlaylist=function(e){this.clearTimer()},r.loadingPlaylist=function(e,t){this.clearTimer()},r.shouldLoadPlaylist=function(e){return this.canLoad&&!!e&&!!e.url&&(!e.details||e.details.live)},r.getUrlWithDirectives=function(e,t){if(t)try{return t.addDirectives(e)}catch(e){this.warn("Could not construct new URL with HLS Delivery Directives: "+e)}return e},r.playlistLoaded=function(e,t,r){var i=t.details,n=t.stats,a=self.performance.now(),s=n.loading.first?Math.max(0,a-n.loading.first):0;i.advancedDateTime=Date.now()-s;var o=this.hls.config.timelineOffset;if(o!==i.appliedTimelineOffset){var l=Math.max(o||0,0);i.appliedTimelineOffset=l,i.fragments.forEach((function(e){e.setStart(e.playlistOffset+l)}))}if(i.live||null!=r&&r.live){var u="levelInfo"in t?t.levelInfo:t.track;if(i.reloaded(r),r&&i.fragments.length>0){di(r,i,this);var d=i.playlistParsingError;if(d){this.warn(d);var h=this.hls;if(!h.config.ignorePlaylistParsingErrors){var f,c=t.networkDetails;return void h.trigger(b.ERROR,{type:R.NETWORK_ERROR,details:k.LEVEL_PARSING_ERROR,fatal:!1,url:i.url,error:d,reason:d.message,level:t.level||void 0,parent:null==(f=i.fragments[0])?void 0:f.type,networkDetails:c,stats:n})}i.playlistParsingError=null}}-1===i.requestScheduled&&(i.requestScheduled=n.loading.start);var g,v=this.hls.mainForwardBufferInfo,m=v?v.end-v.len:0,p=gi(i,1e3*(i.edge-m));if(i.requestScheduled+p0){if(_>3*i.targetduration)this.log("Playlist last advanced "+D.toFixed(2)+"s ago. Omitting segment and part directives."),y=void 0,E=void 0;else if(null!=r&&r.tuneInGoal&&_-i.partTarget>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+P+" with playlist age: "+i.age),P=0;else{var C=Math.floor(P/i.targetduration);y+=C,void 0!==E&&(E+=Math.round(P%i.targetduration/i.partTarget)),this.log("CDN Tune-in age: "+i.ageHeader+"s last advanced "+D.toFixed(2)+"s goal: "+P+" skip sn "+C+" to part "+E)}i.tuneInGoal=P}if(g=this.getDeliveryDirectives(i,t.deliveryDirectives,y,E),T||!I)return i.requestScheduled=a,void this.loadingPlaylist(u,g)}else(i.canBlockReload||i.canSkipUntil)&&(g=this.getDeliveryDirectives(i,t.deliveryDirectives,y,E));g&&void 0!==y&&i.canBlockReload&&(i.requestScheduled=n.loading.first+Math.max(p-2*s,p/2)),this.scheduleLoading(u,g,i)}else this.clearTimer()},r.scheduleLoading=function(e,t,r){var i=this,n=r||e.details;if(n){var a=self.performance.now(),s=n.requestScheduled;if(a>=s)this.loadingPlaylist(e,t);else{var o=s-a;this.log("reload live playlist "+(e.name||e.bitrate+"bps")+" in "+Math.round(o)+" ms"),this.clearTimer(),this.timer=self.setTimeout((function(){return i.loadingPlaylist(e,t)}),o)}}else this.loadingPlaylist(e,t)},r.getDeliveryDirectives=function(e,t,r,i){var n=nt(e);return null!=t&&t.skip&&e.deltaUpdateFailed&&(r=t.msn,i=t.part,n=tt),new at(r,i,n)},r.checkRetry=function(e){var t=this,r=e.details,i=It(e),n=e.errorAction,a=n||{},s=a.action,o=a.retryCount,l=void 0===o?0:o,u=a.retryConfig,d=!!n&&!!u&&(s===Mt||!n.resolved&&s===Ot);if(d){var h;if(l>=u.maxNumRetry)return!1;if(i&&null!=(h=e.context)&&h.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var f=Dt(u,l);this.clearTimer(),this.timer=self.setTimeout((function(){return t.loadPlaylist()}),f),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+f+"ms")}e.levelRetry=!0,n.resolved=!0}return d},t}(N);function Ea(e,t){if(e.length!==t.length)return!1;for(var r=0;r-1)n=a[o];else{var l=ct(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var d={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(b.AUDIO_TRACKS_UPDATED,d);var h=this.trackId;if(-1!==u&&-1===h)this.setAudioTrack(u);else if(a.length&&-1===h){var f,c=new Error("No audio track selected for current audio group-ID(s): "+(null==(f=this.groupIds)?void 0:f.join(","))+" track count: "+a.length);this.warn(c.message),this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:c})}}}},r.onError=function(e,t){!t.fatal&&t.context&&(t.context.type!==P||t.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(t.context.groupId)||this.checkRetry(t))},r.setAudioOption=function(e){var t=this.hls;if(t.config.audioPreference=e,e){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&>(e,i,vt))return i;var n=ct(e,this.tracksInGroup,vt);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=t.loadLevel;-1===s&&(s=t.firstAutoLevel);var o=function(e,t,r,i,n){var a=t[i],s=t.reduce((function(e,t,r){var i=t.uri;return(e[i]||(e[i]=[])).push(r),e}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),d=mt(t,i,(function(t){if(t.videoRange!==o||t.frameRate!==l||t.codecSet.substring(0,4)!==u)return!1;var i=t.audioGroups,a=r.filter((function(e){return!i||-1!==i.indexOf(e.groupId)}));return ct(e,a,n)>-1}));return d>-1?d:mt(t,i,(function(t){var i=t.audioGroups,a=r.filter((function(e){return!i||-1!==i.indexOf(e.groupId)}));return ct(e,a,n)>-1}))}(e,t.levels,r,s,vt);if(-1===o)return null;t.nextLoadLevel=o}if(e.channels||e.audioCodec){var l=ct(e,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(e){var t=this.tracksInGroup;if(e<0||e>=t.length)this.warn("Invalid audio track id: "+e);else{this.selectDefaultTrack=!1;var r=this.currentTrack,i=t[e],n=i.details&&!i.details.live;if(!(e===this.trackId&&i===r&&n||(this.log("Switching to audio-track "+e+' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId+" channels:"+i.channels),this.trackId=e,this.currentTrack=i,this.hls.trigger(b.AUDIO_TRACK_SWITCHING,d({},i)),n))){var a=this.switchParams(i.url,null==r?void 0:r.details,i.details);this.loadPlaylist(a)}}},r.findTrackId=function(e){for(var t=this.tracksInGroup,r=0;r":"\n"+this.list("video")+"\n"+this.list("audio")+"\n"+this.list("audiovideo")+"}"},t.list=function(e){var t,r;return null!=(t=this.queues)&&t[e]||null!=(r=this.tracks)&&r[e]?e+": ("+this.listSbInfo(e)+") "+this.listOps(e):""},t.listSbInfo=function(e){var t,r=null==(t=this.tracks)?void 0:t[e],i=null==r?void 0:r.buffer;return i?"SourceBuffer"+(i.updating?" updating":"")+(r.ended?" ended":"")+(r.ending?" ending":""):"none"},t.listOps=function(e){var t;return(null==(t=this.queues)?void 0:t[e].map((function(e){return e.label})).join(", "))||""},e}(),Ia=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,Ra="HlsJsTrackRemovedError",ka=function(e){function t(t){var r;return(r=e.call(this,t)||this).name=Ra,r}return o(t,e),t}(c(Error)),ba=function(e){function t(t,r){var i,n;return(i=e.call(this,"buffer-controller",t.logger)||this).hls=void 0,i.fragmentTracker=void 0,i.details=null,i._objectUrl=null,i.operationQueue=null,i.bufferCodecEventsTotal=0,i.media=null,i.mediaSource=null,i.lastMpegAudioChunk=null,i.blockedAudioAppend=null,i.lastVideoAppendEnd=0,i.appendSource=void 0,i.transferData=void 0,i.overrides=void 0,i.appendErrors={audio:0,video:0,audiovideo:0},i.tracks={},i.sourceBuffers=[[null,null],[null,null]],i._onEndStreaming=function(e){var t;i.hls&&"open"===(null==(t=i.mediaSource)?void 0:t.readyState)&&i.hls.pauseBuffering()},i._onStartStreaming=function(e){i.hls&&i.hls.resumeBuffering()},i._onMediaSourceOpen=function(e){var t=i,r=t.media,n=t.mediaSource;e&&i.log("Media source opened"),r&&n&&(n.removeEventListener("sourceopen",i._onMediaSourceOpen),r.removeEventListener("emptied",i._onMediaEmptied),i.updateDuration(),i.hls.trigger(b.MEDIA_ATTACHED,{media:r,mediaSource:n}),null!==i.mediaSource&&i.checkPendingTracks())},i._onMediaSourceClose=function(){i.log("Media source closed")},i._onMediaSourceEnded=function(){i.log("Media source ended")},i._onMediaEmptied=function(){var e=i,t=e.mediaSrc,r=e._objectUrl;t!==r&&i.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},i.hls=t,i.fragmentTracker=r,i.appendSource=(n=W(t.config.preferManagedMediaSource),"undefined"!=typeof self&&n===self.ManagedMediaSource),i.initTracks(),i.registerListeners(),i}o(t,e);var r=t.prototype;return r.hasSourceTypes=function(){return Object.keys(this.tracks).length>0},r.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=this.blockedAudioAppend=null,this.transferData=this.overrides=void 0,this.operationQueue&&(this.operationQueue.destroy(),this.operationQueue=null),this.hls=this.fragmentTracker=null,this._onMediaSourceOpen=this._onMediaSourceClose=null,this._onMediaSourceEnded=null,this._onStartStreaming=this._onEndStreaming=null},r.registerListeners=function(){var e=this.hls;e.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.MANIFEST_PARSED,this.onManifestParsed,this),e.on(b.BUFFER_RESET,this.onBufferReset,this),e.on(b.BUFFER_APPENDING,this.onBufferAppending,this),e.on(b.BUFFER_CODECS,this.onBufferCodecs,this),e.on(b.BUFFER_EOS,this.onBufferEos,this),e.on(b.BUFFER_FLUSHING,this.onBufferFlushing,this),e.on(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.on(b.FRAG_PARSED,this.onFragParsed,this),e.on(b.FRAG_CHANGED,this.onFragChanged,this),e.on(b.ERROR,this.onError,this)},r.unregisterListeners=function(){var e=this.hls;e.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.MANIFEST_PARSED,this.onManifestParsed,this),e.off(b.BUFFER_RESET,this.onBufferReset,this),e.off(b.BUFFER_APPENDING,this.onBufferAppending,this),e.off(b.BUFFER_CODECS,this.onBufferCodecs,this),e.off(b.BUFFER_EOS,this.onBufferEos,this),e.off(b.BUFFER_FLUSHING,this.onBufferFlushing,this),e.off(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.off(b.FRAG_PARSED,this.onFragParsed,this),e.off(b.FRAG_CHANGED,this.onFragChanged,this),e.off(b.ERROR,this.onError,this)},r.transferMedia=function(){var e=this,t=this.media,r=this.mediaSource;if(!t)return null;var i={};if(this.operationQueue){var n=this.isUpdating();n||this.operationQueue.removeBlockers();var s=this.isQueued();(n||s)&&this.warn("Transfering MediaSource with"+(s?" operations in queue":"")+(n?" updating SourceBuffer(s)":"")+" "+this.operationQueue),this.operationQueue.destroy()}var o=this.transferData;return!this.sourceBufferCount&&o&&o.mediaSource===r?a(i,o.tracks):this.sourceBuffers.forEach((function(t){var r=t[0];r&&(i[r]=a({},e.tracks[r]),e.removeBuffer(r)),t[0]=t[1]=null})),{media:t,mediaSource:r,tracks:i}},r.initTracks=function(){this.sourceBuffers=[[null,null],[null,null]],this.tracks={},this.resetQueue(),this.resetAppendErrors(),this.lastMpegAudioChunk=this.blockedAudioAppend=null,this.lastVideoAppendEnd=0},r.onManifestLoading=function(){this.bufferCodecEventsTotal=0,this.details=null},r.onManifestParsed=function(e,t){var r,i=2;(t.audio&&!t.video||!t.altAudio)&&(i=1),this.bufferCodecEventsTotal=i,this.log(i+" bufferCodec event(s) expected."),null!=(r=this.transferData)&&r.mediaSource&&this.sourceBufferCount&&i&&this.bufferCreated()},r.onMediaAttaching=function(e,t){var r=this.media=t.media;this.transferData=this.overrides=void 0;var i=W(this.appendSource);if(i){var n=!!t.mediaSource;(n||t.overrides)&&(this.transferData=t,this.overrides=t.overrides);var a=this.mediaSource=t.mediaSource||new i;if(this.assignMediaSource(a),n)this._objectUrl=r.src,this.attachTransferred();else{var s=this._objectUrl=self.URL.createObjectURL(a);if(this.appendSource)try{r.removeAttribute("src");var o=self.ManagedMediaSource;r.disableRemotePlayback=r.disableRemotePlayback||o&&a instanceof o,Da(r),function(e,t){var r=self.document.createElement("source");r.type="video/mp4",r.src=t,e.appendChild(r)}(r,s),r.load()}catch(e){r.src=s}else r.src=s}r.addEventListener("emptied",this._onMediaEmptied)}},r.assignMediaSource=function(e){var t,r;this.log(((null==(t=this.transferData)?void 0:t.mediaSource)===e?"transferred":"created")+" media source: "+(null==(r=e.constructor)?void 0:r.name)),e.addEventListener("sourceopen",this._onMediaSourceOpen),e.addEventListener("sourceended",this._onMediaSourceEnded),e.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.addEventListener("startstreaming",this._onStartStreaming),e.addEventListener("endstreaming",this._onEndStreaming))},r.attachTransferred=function(){var e=this,t=this.media,r=this.transferData;if(r&&t){var i=this.tracks,n=r.tracks,a=n?Object.keys(n):null,s=a?a.length:0,o=function(){Promise.resolve().then((function(){e.media&&e.mediaSourceOpenOrEnded&&e._onMediaSourceOpen()}))};if(n&&a&&s){if(!this.tracksReady)return this.hls.config.startFragPrefetch=!0,void this.log("attachTransferred: waiting for SourceBuffer track info");if(this.log("attachTransferred: (bufferCodecEventsTotal "+this.bufferCodecEventsTotal+")\nrequired tracks: "+ut(i,(function(e,t){return"initSegment"===e?void 0:t}))+";\ntransfer tracks: "+ut(n,(function(e,t){return"initSegment"===e?void 0:t}))+"}"),!j(n,i)){r.mediaSource=null,r.tracks=void 0;var l=t.currentTime,u=this.details,d=Math.max(l,(null==u?void 0:u.fragments[0].start)||0);return d-l>1?void this.log("attachTransferred: waiting for playback to reach new tracks start time "+l+" -> "+d):(this.warn('attachTransferred: resetting MediaSource for incompatible tracks ("'+Object.keys(n)+'"->"'+Object.keys(i)+'") start time: '+d+" currentTime: "+l),this.onMediaDetaching(b.MEDIA_DETACHING,{}),this.onMediaAttaching(b.MEDIA_ATTACHING,r),void(t.currentTime=d))}this.transferData=void 0,a.forEach((function(t){var r=t,i=n[r];if(i){var a=i.buffer;if(a){var s=e.fragmentTracker,o=i.id;if(s.hasFragments(o)||s.hasParts(o)){var l=dr.getBuffered(a);s.detectEvictedFragments(r,l,o,null,!0)}var u=_a(r),d=[r,a];e.sourceBuffers[u]=d,a.updating&&e.operationQueue&&e.operationQueue.prependBlocker(r),e.trackSourceBuffer(r,i)}}})),o(),this.bufferCreated()}else this.log("attachTransferred: MediaSource w/o SourceBuffers"),o()}},r.onMediaDetaching=function(e,t){var r=this,i=!!t.transferMedia;this.transferData=this.overrides=void 0;var n=this.media,a=this.mediaSource,s=this._objectUrl;if(a){if(this.log("media source "+(i?"transferring":"detaching")),i)this.sourceBuffers.forEach((function(e){var t=e[0];t&&r.removeBuffer(t)})),this.resetQueue();else{if(this.mediaSourceOpenOrEnded){var o="open"===a.readyState;try{for(var l=a.sourceBuffers,u=l.length;u--;)o&&l[u].abort(),a.removeSourceBuffer(l[u]);o&&a.endOfStream()}catch(e){this.warn("onMediaDetaching: "+e.message+" while calling endOfStream")}}this.sourceBufferCount&&this.onBufferReset()}a.removeEventListener("sourceopen",this._onMediaSourceOpen),a.removeEventListener("sourceended",this._onMediaSourceEnded),a.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(a.removeEventListener("startstreaming",this._onStartStreaming),a.removeEventListener("endstreaming",this._onEndStreaming)),this.mediaSource=null,this._objectUrl=null}n&&(n.removeEventListener("emptied",this._onMediaEmptied),i||(s&&self.URL.revokeObjectURL(s),this.mediaSrc===s?(n.removeAttribute("src"),this.appendSource&&Da(n),n.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.media=null),this.hls.trigger(b.MEDIA_DETACHED,t)},r.onBufferReset=function(){var e=this;this.sourceBuffers.forEach((function(t){var r=t[0];r&&e.resetBuffer(r)})),this.initTracks()},r.resetBuffer=function(e){var t,r=null==(t=this.tracks[e])?void 0:t.buffer;if(this.removeBuffer(e),r)try{var i;null!=(i=this.mediaSource)&&i.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(r)}catch(t){this.warn("onBufferReset "+e,t)}delete this.tracks[e]},r.removeBuffer=function(e){this.removeBufferListeners(e),this.sourceBuffers[_a(e)]=[null,null];var t=this.tracks[e];t&&(t.buffer=void 0)},r.resetQueue=function(){this.operationQueue&&this.operationQueue.destroy(),this.operationQueue=new La(this.tracks)},r.onBufferCodecs=function(e,t){var r,i=this,n=this.tracks,a=Object.keys(t);this.log('BUFFER_CODECS: "'+a+'" (current SB count '+this.sourceBufferCount+")");var s="audiovideo"in t&&(n.audio||n.video)||n.audiovideo&&("audio"in t||"video"in t),o=!s&&this.sourceBufferCount&&this.media&&a.some((function(e){return!n[e]}));s||o?this.warn('Unsupported transition between "'+Object.keys(n)+'" and "'+a+'" SourceBuffers'):(a.forEach((function(e){var r,a,s=t[e],o=s.id,l=s.codec,u=s.levelCodec,d=s.container,h=s.metadata,f=s.supplemental,c=n[e],g=null==(r=i.transferData)||null==(r=r.tracks)?void 0:r[e],v=null!=g&&g.buffer?g:c,m=(null==v?void 0:v.pendingCodec)||(null==v?void 0:v.codec),p=null==v?void 0:v.levelCodec;c||(c=n[e]={buffer:void 0,listeners:[],codec:l,supplemental:f,container:d,levelCodec:u,metadata:h,id:o});var y=Ve(m,p),E=null==y?void 0:y.replace(Ia,"$1"),T=Ve(l,u),S=null==(a=T)?void 0:a.replace(Ia,"$1");T&&y&&E!==S&&("audio"===e.slice(0,5)&&(T=Ke(T,i.appendSource)),i.log("switching codec "+m+" to "+T),T!==(c.pendingCodec||c.codec)&&(c.pendingCodec=T),c.container=d,i.appendChangeType(e,d,T))})),(this.tracksReady||this.sourceBufferCount)&&(t.tracks=this.sourceBufferTracks),this.sourceBufferCount||(this.bufferCodecEventsTotal>1&&!this.tracks.video&&!t.video&&"main"===(null==(r=t.audio)?void 0:r.id)&&(this.log("Main audio-only"),this.bufferCodecEventsTotal=1),this.mediaSourceOpenOrEnded&&this.checkPendingTracks()))},r.appendChangeType=function(e,t,r){var i=this,n=t+";codecs="+r,a={label:"change-type="+n,execute:function(){var a=i.tracks[e];if(a){var s=a.buffer;null!=s&&s.changeType&&(i.log("changing "+e+" sourceBuffer type to "+n),s.changeType(n),a.codec=r,a.container=t)}i.shiftAndExecuteNext(e)},onStart:function(){},onComplete:function(){},onError:function(t){i.warn("Failed to change "+e+" SourceBuffer type",t)}};this.append(a,e,this.isPending(this.tracks[e]))},r.blockAudio=function(e){var t,r=this,i=e.start,n=i+.05*e.duration;if(!0!==(null==(t=this.fragmentTracker.getAppendedFrag(i,w))?void 0:t.gap)){var a={label:"block-audio",execute:function(){var e,t=r.tracks.video;(r.lastVideoAppendEnd>n||null!=t&&t.buffer&&dr.isBuffered(t.buffer,n)||!0===(null==(e=r.fragmentTracker.getAppendedFrag(n,w))?void 0:e.gap))&&(r.blockedAudioAppend=null,r.shiftAndExecuteNext("audio"))},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Error executing block-audio operation",e)}};this.blockedAudioAppend={op:a,frag:e},this.append(a,"audio",!0)}},r.unblockAudio=function(){var e=this.blockedAudioAppend,t=this.operationQueue;e&&t&&(this.blockedAudioAppend=null,t.unblockAudio(e.op))},r.onBufferAppending=function(e,t){var r=this,i=this.tracks,n=t.data,a=t.type,s=t.parent,o=t.frag,l=t.part,u=t.chunkMeta,d=t.offset,h=u.buffering[a],f=o.sn,c=o.cc,g=self.performance.now();h.start=g;var v=o.stats.buffering,m=l?l.stats.buffering:null;0===v.start&&(v.start=g),m&&0===m.start&&(m.start=g);var p=i.audio,y=!1;"audio"===a&&"audio/mpeg"===(null==p?void 0:p.container)&&(y=!this.lastMpegAudioChunk||1===u.id||this.lastMpegAudioChunk.sn!==u.sn,this.lastMpegAudioChunk=u);var E=i.video,T=null==E?void 0:E.buffer;if(T&&"initSegment"!==f){var S=l||o,L=this.blockedAudioAppend;if("audio"!==a||"main"===s||this.blockedAudioAppend||E.ending||E.ended){if("video"===a){var I=S.end;if(L){var D=L.frag.start;(I>D||I=r.hls.config.appendErrorMaxRetry||n)&&(i.fatal=!0)}r.hls.trigger(b.ERROR,i)}};this.log('queuing "'+a+'" append sn: '+f+(l?" p: "+l.index:"")+" of "+(o.type===w?"level":"track")+" "+o.level+" cc: "+c),this.append(x,a,this.isPending(this.tracks[a]))},r.getFlushOp=function(e,t,r){var i=this;return this.log('queuing "'+e+'" remove '+t+"-"+r),{label:"remove",execute:function(){i.removeExecutor(e,t,r)},onStart:function(){},onComplete:function(){i.hls.trigger(b.BUFFER_FLUSHED,{type:e})},onError:function(n){i.warn("Failed to remove "+t+"-"+r+' from "'+e+'" SourceBuffer',n)}}},r.onBufferFlushing=function(e,t){var r=this,i=t.type,n=t.startOffset,a=t.endOffset;i?this.append(this.getFlushOp(i,n,a),i):this.sourceBuffers.forEach((function(e){var t=e[0];t&&r.append(r.getFlushOp(t,n,a),t)}))},r.onFragParsed=function(e,t){var r=this,i=t.frag,n=t.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[J]?a.push("audiovideo"):(s[$]&&a.push("audio"),s[Z]&&a.push("video")),0===a.length&&this.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var e=self.performance.now();i.stats.buffering.end=e,n&&(n.stats.buffering.end=e);var t=n?n.stats:i.stats;r.hls.trigger(b.FRAG_BUFFERED,{frag:i,part:n,stats:t,id:i.type})}),a).catch((function(e){r.warn("Fragment buffered callback "+e),r.stepOperationQueue(r.sourceBufferTypes)}))},r.onFragChanged=function(e,t){this.trimBuffers()},r.onBufferEos=function(e,t){var r,i=this;this.sourceBuffers.forEach((function(e){var r=e[0];if(r){var n=i.tracks[r];t.type&&t.type!==r||(n.ending=!0,n.ended||(n.ended=!0,i.log(r+" buffer reached EOS")))}}));var n=!1!==(null==(r=this.overrides)?void 0:r.endOfStream);this.sourceBufferCount>0&&!this.sourceBuffers.some((function(e){var t,r=e[0];return r&&!(null!=(t=i.tracks[r])&&t.ended)}))?n?(this.log("Queueing EOS"),this.blockUntilOpen((function(){i.tracksEnded();var e=i.mediaSource;e&&"open"===e.readyState?(i.log("Calling mediaSource.endOfStream()"),e.endOfStream(),i.hls.trigger(b.BUFFERED_TO_END,void 0)):e&&i.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+e.readyState)}))):(this.tracksEnded(),this.hls.trigger(b.BUFFERED_TO_END,void 0)):"video"===t.type&&this.unblockAudio()},r.tracksEnded=function(){var e=this;this.sourceBuffers.forEach((function(t){var r=t[0];if(null!==r){var i=e.tracks[r];i&&(i.ending=!1)}}))},r.onLevelUpdated=function(e,t){var r=t.details;r.fragments.length&&(this.details=r,this.updateDuration())},r.updateDuration=function(){var e=this;this.blockUntilOpen((function(){var t=e.getDurationAndRange();t&&e.updateMediaSource(t)}))},r.onError=function(e,t){if(t.details===k.BUFFER_APPEND_ERROR&&t.frag){var r,i=null==(r=t.errorAction)?void 0:r.nextAutoLevel;A(i)&&i!==t.frag.level&&this.resetAppendErrors()}},r.resetAppendErrors=function(){this.appendErrors={audio:0,video:0,audiovideo:0}},r.trimBuffers=function(){var e=this.hls,t=this.details,r=this.media;if(r&&null!==t&&this.sourceBufferCount){var i=e.config,n=r.currentTime,a=t.levelTargetDuration,s=t.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(A(s)&&s>=0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}var u=i.frontBufferFlushThreshold;if(A(u)&&u>0){var d=Math.max(i.maxBufferLength,u),h=Math.max(d,a),f=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,f)}}},r.flushBackBuffer=function(e,t,r){var i=this;this.sourceBuffers.forEach((function(e){var t=e[0],n=e[1];if(n){var a=dr.getBuffered(n);if(a.length>0&&r>a.start(0)){var s;i.hls.trigger(b.BACK_BUFFER_REACHED,{bufferEnd:r});var o=i.tracks[t];if(null!=(s=i.details)&&s.live)i.hls.trigger(b.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(null!=o&&o.ended)return void i.log("Cannot flush "+t+" back buffer while SourceBuffer is in ended state");i.hls.trigger(b.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:t})}}}))},r.flushFrontBuffer=function(e,t,r){var i=this;this.sourceBuffers.forEach((function(t){var n=t[0],a=t[1];if(a){var s=dr.getBuffered(a),o=s.length;if(o<2)return;var l=s.start(o-1),u=s.end(o-1);if(r>l||e>=l&&e<=u)return;i.hls.trigger(b.BUFFER_FLUSHING,{startOffset:l,endOffset:1/0,type:n})}}))},r.getDurationAndRange=function(){var e,t=this.details,r=this.mediaSource;if(!t||!this.media||"open"!==(null==r?void 0:r.readyState))return null;var i=t.edge;if(t.live&&this.hls.config.liveDurationInfinity){if(t.fragments.length&&r.setLiveSeekableRange){var n=Math.max(0,t.fragmentStart);return{duration:1/0,start:n,end:Math.max(n,i)}}return{duration:1/0}}var a=null==(e=this.overrides)?void 0:e.duration;if(a)return A(a)?{duration:a}:null;var s=this.media.duration;return i>(A(r.duration)?r.duration:0)&&i>s||!A(s)?{duration:i}:null},r.updateMediaSource=function(e){var t=e.duration,r=e.start,i=e.end,n=this.mediaSource;this.media&&n&&"open"===n.readyState&&(n.duration!==t&&(A(t)&&this.log("Updating MediaSource duration to "+t.toFixed(3)),n.duration=t),void 0!==r&&void 0!==i&&(this.log("MediaSource duration is set to "+n.duration+". Setting seekable range to "+r+"-"+i+"."),n.setLiveSeekableRange(r,i)))},r.checkPendingTracks=function(){var e=this.bufferCodecEventsTotal,t=this.pendingTrackCount,r=this.tracks;if(this.log("checkPendingTracks (pending: "+t+" codec events expected: "+e+") "+ut(r)),this.tracksReady){var i,n=null==(i=this.transferData)?void 0:i.tracks;n&&Object.keys(n).length?this.attachTransferred():this.createSourceBuffers()}},r.bufferCreated=function(){var e=this;if(this.sourceBufferCount){var t={};this.sourceBuffers.forEach((function(r){var i=r[0],n=r[1];if(i){var a=e.tracks[i];t[i]={buffer:n,container:a.container,codec:a.codec,supplemental:a.supplemental,levelCodec:a.levelCodec,id:a.id,metadata:a.metadata}}})),this.hls.trigger(b.BUFFER_CREATED,{tracks:t}),this.log("SourceBuffers created. Running queue: "+this.operationQueue),this.sourceBuffers.forEach((function(t){var r=t[0];e.executeNext(r)}))}else{var r=new Error("could not create source buffer for media codec(s)");this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:r,reason:r.message})}},r.createSourceBuffers=function(){var e=this.tracks,t=this.sourceBuffers,r=this.mediaSource;if(!r)throw new Error("createSourceBuffers called when mediaSource was null");for(var i in e){var n=i,a=e[n];if(this.isPending(a)){var s=this.getTrackCodec(a,n),o=a.container+";codecs="+s;a.codec=s,this.log("creating sourceBuffer("+o+")"+(this.currentOp(n)?" Queued":"")+" "+ut(a));try{var l=r.addSourceBuffer(o),u=_a(n),d=[n,l];t[u]=d,a.buffer=l}catch(e){var h;return this.error("error while trying to add sourceBuffer: "+e.message),this.shiftAndExecuteNext(n),null==(h=this.operationQueue)||h.removeBlockers(),delete this.tracks[n],void this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:e,sourceBufferName:n,mimeType:o,parent:a.id})}this.trackSourceBuffer(n,a)}}this.bufferCreated()},r.getTrackCodec=function(e,t){var r=e.supplemental,i=e.codec;r&&("video"===t||"audiovideo"===t)&&xe(r,"video")&&(i=function(e,t){var r=[];if(e)for(var i=e.split(","),n=0;n=r&&(this.log("Updating "+i+" SourceBuffer timestampOffset to "+t+" (sn: "+n+" cc: "+a+")"),e.timestampOffset=t)},r.removeExecutor=function(e,t,r){var i=this.media,n=this.mediaSource,a=this.tracks[e],s=null==a?void 0:a.buffer;if(!i||!n||!s)return this.warn("Attempting to remove from the "+e+" SourceBuffer, but it does not exist"),void this.shiftAndExecuteNext(e);var o=A(i.duration)?i.duration:1/0,l=A(n.duration)?n.duration:1/0,u=Math.max(0,t),d=Math.min(r,o,l);d>u&&(!a.ending||a.ended)?(a.ended=!1,this.log("Removing ["+u+","+d+"] from the "+e+" SourceBuffer"),s.remove(u,d)):this.shiftAndExecuteNext(e)},r.appendExecutor=function(e,t){var r=this.tracks[t],i=null==r?void 0:r.buffer;if(!i)throw new ka("Attempting to append to the "+t+" SourceBuffer, but it does not exist");r.ending=!1,r.ended=!1,i.appendBuffer(e)},r.blockUntilOpen=function(e){var t=this;if(this.isUpdating()||this.isQueued())this.blockBuffers(e).catch((function(e){t.warn("SourceBuffer blocked callback "+e),t.stepOperationQueue(t.sourceBufferTypes)}));else try{e()}catch(e){this.warn("Callback run without blocking "+this.operationQueue+" "+e)}},r.isUpdating=function(){return this.sourceBuffers.some((function(e){var t=e[0],r=e[1];return t&&r.updating}))},r.isQueued=function(){var e=this;return this.sourceBuffers.some((function(t){var r=t[0];return r&&!!e.currentOp(r)}))},r.isPending=function(e){return!!e&&!e.buffer},r.blockBuffers=function(e,t){var r=this;if(void 0===t&&(t=this.sourceBufferTypes),!t.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),Promise.resolve().then(e);var i=this.operationQueue,n=t.map((function(e){return r.appendBlocker(e)}));return t.length>1&&!!this.blockedAudioAppend&&this.unblockAudio(),Promise.all(n).then((function(t){i===r.operationQueue&&(e(),r.stepOperationQueue(r.sourceBufferTypes))}))},r.stepOperationQueue=function(e){var t=this;e.forEach((function(e){var r,i=null==(r=t.tracks[e])?void 0:r.buffer;i&&!i.updating&&t.shiftAndExecuteNext(e)}))},r.append=function(e,t,r){this.operationQueue&&this.operationQueue.append(e,t,r)},r.appendBlocker=function(e){if(this.operationQueue)return this.operationQueue.appendBlocker(e)},r.currentOp=function(e){return this.operationQueue?this.operationQueue.current(e):null},r.executeNext=function(e){e&&this.operationQueue&&this.operationQueue.executeNext(e)},r.shiftAndExecuteNext=function(e){this.operationQueue&&this.operationQueue.shiftAndExecuteNext(e)},r.addBufferListener=function(e,t,r){var i=this.tracks[e];if(i){var n=i.buffer;if(n){var a=r.bind(this,e);i.listeners.push({event:t,listener:a}),n.addEventListener(t,a)}}},r.removeBufferListeners=function(e){var t=this.tracks[e];if(t){var r=t.buffer;r&&(t.listeners.forEach((function(e){r.removeEventListener(e.event,e.listener)})),t.listeners.length=0)}},i(t,[{key:"mediaSourceOpenOrEnded",get:function(){var e,t=null==(e=this.mediaSource)?void 0:e.readyState;return"open"===t||"ended"===t}},{key:"sourceBufferTracks",get:function(){var e=this;return Object.keys(this.tracks).reduce((function(t,r){var i=e.tracks[r];return t[r]={id:i.id,container:i.container,codec:i.codec,levelCodec:i.levelCodec},t}),{})}},{key:"bufferedToEnd",get:function(){var e=this;return this.sourceBufferCount>0&&!this.sourceBuffers.some((function(t){var r=t[0];if(r){var i=e.tracks[r];if(i)return!i.ended||i.ending}return!1}))}},{key:"tracksReady",get:function(){var e=this.pendingTrackCount;return e>0&&(e>=this.bufferCodecEventsTotal||this.isPending(this.tracks.audiovideo))}},{key:"mediaSrc",get:function(){var e,t,r=(null==(e=this.media)||null==(t=e.querySelector)?void 0:t.call(e,"source"))||this.media;return null==r?void 0:r.src}},{key:"pendingTrackCount",get:function(){var e=this;return Object.keys(this.tracks).reduce((function(t,r){return t+(e.isPending(e.tracks[r])?1:0)}),0)}},{key:"sourceBufferCount",get:function(){return this.sourceBuffers.reduce((function(e,t){return e+(t[0]?1:0)}),0)}},{key:"sourceBufferTypes",get:function(){return this.sourceBuffers.map((function(e){return e[0]})).filter((function(e){return!!e}))}}])}(N);function Da(e){var t=e.querySelectorAll("source");[].slice.call(t).forEach((function(t){e.removeChild(t)}))}function _a(e){return"audio"===e?1:0}var Pa=function(){function e(e){this.hls=void 0,this.autoLevelCapping=void 0,this.firstLevel=void 0,this.media=void 0,this.restrictedLevels=void 0,this.timer=void 0,this.clientRect=void 0,this.streamController=void 0,this.hls=e,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.firstLevel=-1,this.media=null,this.restrictedLevels=[],this.timer=void 0,this.clientRect=null,this.registerListeners()}var t=e.prototype;return t.setStreamController=function(e){this.streamController=e},t.destroy=function(){this.hls&&this.unregisterListener(),this.timer&&this.stopCapping(),this.media=null,this.clientRect=null,this.hls=this.streamController=null},t.registerListeners=function(){var e=this.hls;e.on(b.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),e.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.on(b.MANIFEST_PARSED,this.onManifestParsed,this),e.on(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.on(b.BUFFER_CODECS,this.onBufferCodecs,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this)},t.unregisterListener=function(){var e=this.hls;e.off(b.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),e.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.off(b.MANIFEST_PARSED,this.onManifestParsed,this),e.off(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.off(b.BUFFER_CODECS,this.onBufferCodecs,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this)},t.onFpsDropLevelCapping=function(e,t){var r=this.hls.levels[t.droppedLevel];this.isLevelAllowed(r)&&this.restrictedLevels.push({bitrate:r.bitrate,height:r.height,width:r.width})},t.onMediaAttaching=function(e,t){this.media=t.media instanceof HTMLVideoElement?t.media:null,this.clientRect=null,this.timer&&this.hls.levels.length&&this.detectPlayerSize()},t.onManifestParsed=function(e,t){var r=this.hls;this.restrictedLevels=[],this.firstLevel=t.firstLevel,r.config.capLevelToPlayerSize&&t.video&&this.startCapping()},t.onLevelsUpdated=function(e,t){this.timer&&A(this.autoLevelCapping)&&this.detectPlayerSize()},t.onBufferCodecs=function(e,t){this.hls.config.capLevelToPlayerSize&&t.video&&this.startCapping()},t.onMediaDetaching=function(){this.stopCapping(),this.media=null},t.detectPlayerSize=function(){if(this.media){if(this.mediaHeight<=0||this.mediaWidth<=0)return void(this.clientRect=null);var e=this.hls.levels;if(e.length){var t=this.hls,r=this.getMaxLevel(e.length-1);r!==this.autoLevelCapping&&t.logger.log("Setting autoLevelCapping to "+r+": "+e[r].height+"p@"+e[r].bitrate+" for media "+this.mediaWidth+"x"+this.mediaHeight),t.autoLevelCapping=r,t.autoLevelEnabled&&t.autoLevelCapping>this.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=t.autoLevelCapping}}},t.getMaxLevel=function(t){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(e,i){return r.isLevelAllowed(e)&&i<=t}));return this.clientRect=null,e.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},t.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},t.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},t.getDimensions=function(){if(this.clientRect)return this.clientRect;var e=this.media,t={width:0,height:0};if(e){var r=e.getBoundingClientRect();t.width=r.width,t.height=r.height,t.width||t.height||(t.width=r.right-r.left||e.width||0,t.height=r.bottom-r.top||e.height||0)}return this.clientRect=t,t},t.isLevelAllowed=function(e){return!this.restrictedLevels.some((function(t){return e.bitrate===t.bitrate&&e.width===t.width&&e.height===t.height}))},e.getMaxLevelByMediaSize=function(e,t,r){if(null==e||!e.length)return-1;for(var i,n,a=e.length-1,s=Math.max(t,r),o=0;o=s||l.height>=s)&&(i=l,!(n=e[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},i(e,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var e=1;if(!this.hls.config.ignoreDevicePixelRatio)try{e=self.devicePixelRatio}catch(e){}return Math.min(e,this.hls.config.maxDevicePixelRatio)}}])}(),Ca={MANIFEST:"m",AUDIO:"a",VIDEO:"v",MUXED:"av",INIT:"i",CAPTION:"c",TIMED_TEXT:"tt",KEY:"k",OTHER:"o"},wa={HLS:"h"},Oa=function e(t,r){Array.isArray(t)&&(t=t.map((function(t){return t instanceof e?t:new e(t)}))),this.value=t,this.params=r},xa="Dict";function Ma(e,t,r,i){return new Error("failed to "+e+' "'+(n=t,(Array.isArray(n)?JSON.stringify(n):n instanceof Map?"Map{}":n instanceof Set?"Set{}":"object"==typeof n?JSON.stringify(n):String(n))+'" as ')+r,{cause:i});var n}function Fa(e,t,r){return Ma("serialize",e,t,r)}var Na=function(e){this.description=e},Ua="Bare Item",Ba="Boolean",Ga="Byte Sequence";function Ka(e){if(!1===ArrayBuffer.isView(e))throw Fa(e,Ga);return":"+(t=e,btoa(String.fromCharCode.apply(String,t))+":");var t}var Va="Integer";function Ha(e){if(function(e){return e<-999999999999999||99999999999999912)throw Fa(e,Wa);var r=t.toString();return r.includes(".")?r:r+".0"}var qa="String",Xa=/[\x00-\x1f\x7f]+/,Qa="Token";function za(e){var t,r=(t=e).description||t.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw Fa(r,Qa);return r}function $a(e){switch(typeof e){case"number":if(!A(e))throw Fa(e,Ua);return Number.isInteger(e)?Ha(e):ja(e);case"string":return function(e){if(Xa.test(e))throw Fa(e,qa);return'"'+e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(e);case"symbol":return za(e);case"boolean":return function(e){if("boolean"!=typeof e)throw Fa(e,Ba);return e?"?1":"?0"}(e);case"object":if(e instanceof Date)return function(e){return"@"+Ha(e.getTime()/1e3)}(e);if(e instanceof Uint8Array)return Ka(e);if(e instanceof Na)return za(e);default:throw Fa(e,Ua)}}var Za="Key";function Ja(e){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(e))throw Fa(e,Za);return e}function es(e){return null==e?"":Object.entries(e).map((function(e){var t=e[0],r=e[1];return!0===r?";"+Ja(t):";"+Ja(t)+"="+$a(r)})).join("")}function ts(e){return e instanceof Oa?""+$a(e.value)+es(e.params):$a(e)}function rs(e,t){if(void 0===t&&(t={whitespace:!0}),"object"!=typeof e||null==e)throw Fa(e,xa);var r=e instanceof Map?e.entries():Object.entries(e),i=(null==t?void 0:t.whitespace)?" ":"";return Array.from(r).map((function(e){var t=e[0],r=e[1];r instanceof Oa==0&&(r=new Oa(r));var i,n=Ja(t);return!0===r.value?n+=es(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(ts).join(" ")+")"+es(i.params):n+=ts(r)),n})).join(","+i)}function is(e,t){return rs(e,t)}var ns="CMCD-Object",as="CMCD-Request",ss="CMCD-Session",os="CMCD-Status",ls={br:ns,ab:ns,d:ns,ot:ns,tb:ns,tpb:ns,lb:ns,tab:ns,lab:ns,url:ns,pb:as,bl:as,tbl:as,dl:as,ltc:as,mtp:as,nor:as,nrr:as,rc:as,sn:as,sta:as,su:as,ttfb:as,ttfbb:as,ttlb:as,cmsdd:as,cmsds:as,smrt:as,df:as,cs:as,ts:as,cid:ss,pr:ss,sf:ss,sid:ss,st:ss,v:ss,msd:ss,bs:os,bsd:os,cdn:os,rtp:os,bg:os,pt:os,ec:os,e:os},us={REQUEST:as};function ds(e,t){var r={};if(!e)return r;var i,n=Object.keys(e),a=t?(i=t,Object.keys(i).reduce((function(e,t){var r;return null===(r=i[t])||void 0===r||r.forEach((function(r){return e[r]=t})),e}),{})):{};return n.reduce((function(t,r){var i,n=ls[r]||a[r]||us.REQUEST;return(null!==(i=t[n])&&void 0!==i?i:t[n]={})[r]=e[r],t}),r)}var hs="event",fs=function(e){return Math.round(e)},cs=function(e,t){return Array.isArray(e)?e.map((function(e){return cs(e,t)})):e instanceof Oa&&"string"==typeof e.value?new Oa(cs(e.value,t),e.params):(t.baseUrl&&(e=function(e,t){var r=new URL(e),i=new URL(t);if(r.origin!==i.origin)return e;for(var n=r.pathname.split("/").slice(1),a=i.pathname.split("/").slice(1,-1);n[0]===a[0];)n.shift(),a.shift();for(;a.length;)a.shift(),n.unshift("..");return n.join("/")+r.search+r.hash}(e,t.baseUrl)),1===t.version?encodeURIComponent(e):e)},gs=function(e){return 100*fs(e/100)},vs={br:fs,d:fs,bl:gs,dl:gs,mtp:gs,nor:function(e,t){var r=e;return t.version>=2&&(e instanceof Oa&&"string"==typeof e.value?r=new Oa([e]):"string"==typeof e&&(r=[e])),cs(r,t)},rtp:gs,tb:fs},ms="request",ps="response",ys=["ab","bg","bl","br","bs","bsd","cdn","cid","cs","df","ec","lab","lb","ltc","msd","mtp","pb","pr","pt","sf","sid","sn","st","sta","tab","tb","tbl","tpb","ts","v"],Es=["e"],Ts=/^[a-zA-Z0-9-.]+-[a-zA-Z0-9-.]+$/;function Ss(e){return Ts.test(e)}var As,Ls=["d","dl","nor","ot","rtp","su"],Is=["cmsdd","cmsds","rc","smrt","ttfb","ttfbb","ttlb","url"],Rs=["bl","br","bs","cid","d","dl","mtp","nor","nrr","ot","pr","rtp","sf","sid","st","su","tb","v"];function ks(e){return Rs.includes(e)||Ss(e)}var bs=((As={})[ps]=function(e){return ys.includes(e)||Ls.includes(e)||Is.includes(e)||Ss(e)},As[hs]=function(e){return ys.includes(e)||Es.includes(e)||Ss(e)},As[ms]=function(e){return ys.includes(e)||Ls.includes(e)||Ss(e)},As);function Ds(e,t){void 0===t&&(t={});var r={};if(null==e||"object"!=typeof e)return r;var i=t.version||e.v||1,n=t.reportingMode||ms,s=1===i?ks:bs[n],o=Object.keys(e).filter(s),l=t.filter;"function"==typeof l&&(o=o.filter(l));var u=n===ps||n===hs;u&&!o.includes("ts")&&o.push("ts"),i>1&&!o.includes("v")&&o.push("v");var d=a({},vs,t.formatters),h={version:i,reportingMode:n,baseUrl:t.baseUrl};return o.sort().forEach((function(t){var n=e[t],a=d[t];if("function"==typeof a&&(n=a(n,h)),"v"===t){if(1===i)return;n=i}"pr"==t&&1===n||(u&&"ts"===t&&!A(n)&&(n=Date.now()),function(e){return"number"==typeof e?A(e):null!=e&&""!==e&&!1!==e}(n)&&(function(e){return["ot","sf","st","e","sta"].includes(e)}(t)&&"string"==typeof n&&(n=new Na(n)),r[t]=n))})),r}function _s(e,t,r){return a(e,function(e,t){void 0===t&&(t={});var r={};if(!e)return r;var i=ds(Ds(e,t),null==t?void 0:t.customHeaderMap);return Object.entries(i).reduce((function(e,t){var r=t[0],i=is(t[1],{whitespace:!1});return i&&(e[r]=i),e}),r)}(t,r))}var Ps="CMCD";function Cs(e,t){if(void 0===t&&(t={}),!e)return"";var r=function(e,t){return void 0===t&&(t={}),e?is(Ds(e,t),{whitespace:!1}):""}(e,t);return encodeURIComponent(r)}var ws=/CMCD=[^&#]+/;function Os(e,t,r){var i=function(e,t){if(void 0===t&&(t={}),!e)return"";var r=Cs(e,t);return Ps+"="+r}(t,r);if(!i)return e;if(ws.test(e))return e.replace(ws,i);var n=e.includes("?")?"&":"?";return""+e+n+i}var xs=function(){function e(e){var t=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.includeKeys=void 0,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){t.initialized&&(t.starved=!0),t.buffering=!0},this.onPlaying=function(){t.initialized||(t.initialized=!0),t.buffering=!1},this.applyPlaylistData=function(e){try{t.apply(e,{ot:Ca.MANIFEST,su:!t.initialized})}catch(e){t.hls.logger.warn("Could not generate manifest CMCD data.",e)}},this.applyFragmentData=function(e){try{var r=e.frag,i=e.part,n=t.hls.levels[r.level],a=t.getObjectType(r),s={d:1e3*(i||r).duration,ot:a};a!==Ca.VIDEO&&a!==Ca.AUDIO&&a!=Ca.MUXED||(s.br=n.bitrate/1e3,s.tb=t.getTopBandwidth(a)/1e3,s.bl=t.getBufferLength(a));var o=i?t.getNextPart(i):t.getNextFrag(r);null!=o&&o.url&&o.url!==r.url&&(s.nor=o.url),t.apply(e,s)}catch(e){t.hls.logger.warn("Could not generate segment CMCD data.",e)}},this.hls=e;var r=this.config=e.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||e.sessionId,this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var t=e.prototype;return t.registerListeners=function(){var e=this.hls;e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHED,this.onMediaDetached,this),e.on(b.BUFFER_CREATED,this.onBufferCreated,this)},t.unregisterListeners=function(){var e=this.hls;e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHED,this.onMediaDetached,this),e.off(b.BUFFER_CREATED,this.onBufferCreated,this)},t.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=this.media=null},t.onMediaAttached=function(e,t){this.media=t.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},t.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},t.onBufferCreated=function(e,t){var r,i;this.audioBuffer=null==(r=t.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=t.tracks.video)?void 0:i.buffer},t.createData=function(){var e;return{v:1,sf:wa.HLS,sid:this.sid,cid:this.cid,pr:null==(e=this.media)?void 0:e.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},t.apply=function(e,t){void 0===t&&(t={}),a(t,this.createData());var r=t.ot===Ca.INIT||t.ot===Ca.VIDEO||t.ot===Ca.MUXED;this.starved&&r&&(t.bs=!0,t.su=!0,this.starved=!1),null==t.su&&(t.su=this.buffering);var i=this.includeKeys;i&&(t=Object.keys(t).reduce((function(e,r){return i.includes(r)&&(e[r]=t[r]),e}),{}));var n={baseUrl:e.url};this.useHeaders?(e.headers||(e.headers={}),_s(e.headers,t,n)):e.url=Os(e.url,t,n)},t.getNextFrag=function(e){var t,r=null==(t=this.hls.levels[e.level])?void 0:t.details;if(r){var i=e.sn-r.startSN;return r.fragments[i+1]}},t.getNextPart=function(e){var t,r=e.index,i=e.fragment,n=null==(t=this.hls.levels[i.level])||null==(t=t.details)?void 0:t.partList;if(n)for(var a=i.sn,s=n.length-1;s>=0;s--){var o=n[s];if(o.index===r&&o.fragment.sn===a)return n[s+1]}},t.getObjectType=function(e){var t=e.type;return"subtitle"===t?Ca.TIMED_TEXT:"initSegment"===e.sn?Ca.INIT:"audio"===t?Ca.AUDIO:"main"===t?this.hls.audioTracks.length?Ca.VIDEO:Ca.MUXED:void 0},t.getTopBandwidth=function(e){var t,r=0,i=this.hls;if(e===Ca.AUDIO)t=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;t=i.levels.slice(0,a)}return t.forEach((function(e){e.bitrate>r&&(r=e.bitrate)})),r>0?r:NaN},t.getBufferLength=function(e){var t=this.media,r=e===Ca.AUDIO?this.audioBuffer:this.videoBuffer;return r&&t?1e3*dr.bufferInfo(r,t.currentTime,this.config.maxBufferHole).len:NaN},t.createPlaylistLoader=function(){var e=this.config.pLoader,t=this.applyPlaylistData,r=e||this.config.loader;return function(){function e(e){this.loader=void 0,this.loader=new r(e)}var n=e.prototype;return n.destroy=function(){this.loader.destroy()},n.abort=function(){this.loader.abort()},n.load=function(e,r,i){t(e),this.loader.load(e,r,i)},i(e,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}])}()},t.createFragmentLoader=function(){var e=this.config.fLoader,t=this.applyFragmentData,r=e||this.config.loader;return function(){function e(e){this.loader=void 0,this.loader=new r(e)}var n=e.prototype;return n.destroy=function(){this.loader.destroy()},n.abort=function(){this.loader.abort()},n.load=function(e,r,i){t(e),this.loader.load(e,r,i)},i(e,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}])}()},e}(),Ms=function(e){function t(t){var r;return(r=e.call(this,"content-steering",t.logger)||this).hls=void 0,r.loader=null,r.uri=null,r.pathwayId=".",r._pathwayPriority=null,r.timeToLoad=300,r.reloadTimer=-1,r.updated=0,r.started=!1,r.enabled=!0,r.levels=null,r.audioTracks=null,r.subtitleTracks=null,r.penalizedPathways={},r.hls=t,r.registerListeners(),r}o(t,e);var r=t.prototype;return r.registerListeners=function(){var e=this.hls;e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.on(b.MANIFEST_PARSED,this.onManifestParsed,this),e.on(b.ERROR,this.onError,this)},r.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.off(b.MANIFEST_PARSED,this.onManifestParsed,this),e.off(b.ERROR,this.onError,this))},r.pathways=function(){return(this.levels||[]).reduce((function(e,t){return-1===e.indexOf(t.pathwayId)&&e.push(t.pathwayId),e}),[])},r.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var e=1e3*this.timeToLoad-(performance.now()-this.updated);if(e>0)return void this.scheduleRefresh(this.uri,e)}this.loadSteeringManifest(this.uri)}},r.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},r.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},r.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},r.removeLevel=function(e){var t=this.levels;t&&(this.levels=t.filter((function(t){return t!==e})))},r.onManifestLoading=function(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null},r.onManifestLoaded=function(e,t){var r=t.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},r.onManifestParsed=function(e,t){this.audioTracks=t.audioTracks,this.subtitleTracks=t.subtitleTracks},r.onError=function(e,t){var r=t.errorAction;if((null==r?void 0:r.action)===Ot&&r.flags===Nt){var i=this.levels,n=this._pathwayPriority,a=this.pathwayId;if(t.context){var s=t.context,o=s.groupId,l=s.pathwayId,u=s.type;o&&i?a=this.getPathwayForGroupId(o,u,a):l&&(a=l)}a in this.penalizedPathways||(this.penalizedPathways[a]=performance.now()),!n&&i&&(n=this.pathways()),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),t.details!==k.BUFFER_APPEND_ERROR||t.fatal?r.resolved||this.warn("Could not resolve "+t.details+' ("'+t.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+ut(n)+" penalized: "+ut(this.penalizedPathways)):r.resolved=!0}},r.filterParsedLevels=function(e){this.levels=e;var t=this.getLevelsForPathway(this.pathwayId);if(0===t.length){var r=e[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),t=this.getLevelsForPathway(r),this.pathwayId=r}return t.length!==e.length&&this.log("Found "+t.length+"/"+e.length+' levels in Pathway "'+this.pathwayId+'"'),t},r.getLevelsForPathway=function(e){return null===this.levels?[]:this.levels.filter((function(t){return e===t.pathwayId}))},r.updatePathwayPriority=function(e){var t;this._pathwayPriority=e;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(e){i-r[e]>3e5&&delete r[e]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,yi(t),this.hls.trigger(b.LEVELS_UPDATED,{levels:t});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},r.getPathwayForGroupId=function(e,t,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n tenc");o=new Uint8Array(u.subarray(8,24))}catch(e){return void i.warn(n+" Failed to parse sinf: "+e)}for(var d,h=X(o),f=i,c=f.keyIdToKeySessionPromise,g=f.mediaKeySessions,v=c[h],m=function(){var e=g[p],n=e.decryptdata;if(!n.keyId)return 0;var a=X(n.keyId);return Ar(o,n.keyId)||-1!==n.uri.replace(/-/g,"").indexOf(h)?(v=c[a])?(n.pssh||(delete c[a],n.pssh=new Uint8Array(r),n.keyId=o,(v=c[h]=v.then((function(){return i.generateRequestWithPreferredKeySession(e,t,r,"encrypted-event-key-match")}))).catch((function(e){return i.handleError(e)}))),1):0:void 0},p=0;p0)for(var a,s=0,o=n.length;s in key message");return br(atob(c))},r.setupLicenseXHR=function(e,t,r,i){var n=this,a=this.config.licenseXhrSetup;return a?Promise.resolve().then((function(){if(!r.decryptdata)throw new Error("Key removed");return a.call(n.hls,e,t,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return e.open("POST",t,!0),a.call(n.hls,e,t,r,i)})).then((function(r){return e.readyState||e.open("POST",t,!0),{xhr:e,licenseChallenge:r||i}})):(e.open("POST",t,!0),Promise.resolve({xhr:e,licenseChallenge:i}))},r.requestLicense=function(e,t){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrlOrThrow(e.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!e.mediaKeysSession)return a(new Error("invalid state"));if(4===o.readyState)if(200===o.status){r._requestLicenseFailureCount=0;var l=o.response;r.log("License received "+(l instanceof ArrayBuffer?l.byteLength:l));var u=r.config.licenseResponseCallback;if(u)try{l=u.call(r.hls,o,s,e)}catch(e){r.error(e)}n(l)}else{var d=i.errorRetry,h=d?d.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>h||o.status>=400&&o.status<500)a(new Ks({type:R.KEY_SYSTEM_ERROR,details:k.KEY_SYSTEM_LICENSE_REQUEST_FAILED,decryptdata:e.decryptdata,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var f=h-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+f+" attempts left"),r.requestLicense(e,t).then(n,a)}}},e.licenseXhr&&e.licenseXhr.readyState!==XMLHttpRequest.DONE&&e.licenseXhr.abort(),e.licenseXhr=o,r.setupLicenseXHR(o,s,e,t).then((function(t){var i=t.xhr,n=t.licenseChallenge;e.keySystem==Cr.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)})).catch(a)}))},r.onDestroying=function(){this.unregisterListeners(),this._clear()},r.onMediaAttached=function(e,t){if(this.config.emeEnabled){var r=t.media;this.media=r,ki(r,"encrypted",this.onMediaEncrypted),ki(r,"waitingforkey",this.onWaitingForKey);var i=this.mediaResolved;i?i():this.mediaKeys=r.mediaKeys}},r.onMediaDetached=function(){var e=this.media;e&&(bi(e,"encrypted",this.onMediaEncrypted),bi(e,"waitingforkey",this.onWaitingForKey),this.media=null,this.mediaKeys=null)},r._clear=function(){var e,r=this;this._requestLicenseFailureCount=0,this.keyIdToKeySessionPromise={},this.bannedKeyIds={};var i=this.mediaResolved;if(i&&i(),this.mediaKeys||this.mediaKeySessions.length){var n=this.media,a=this.mediaKeySessions.slice();this.mediaKeySessions=[],this.mediaKeys=null,Hr.clearKeyUriToKeyIdMap();var s=a.length;t.CDMCleanupPromise=Promise.all(a.map((function(e){return r.removeSession(e)})).concat((null==n||null==(e=n.setMediaKeys(null))?void 0:e.catch((function(e){r.log("Could not clear media keys: "+e),r.hls&&r.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR,fatal:!1,error:new Error("Could not clear media keys: "+e)})})))||Promise.resolve())).catch((function(e){r.log("Could not close sessions and clear media keys: "+e),r.hls&&r.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,fatal:!1,error:new Error("Could not close sessions and clear media keys: "+e)})})).then((function(){s&&r.log("finished closing key sessions and clearing media keys")}))}},r.onManifestLoading=function(){this._clear()},r.onManifestLoaded=function(e,t){var r=t.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(e,t){return-1===e.indexOf(t.keyFormat)&&e.push(t.keyFormat),e}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},r.removeSession=function(e){var t=this,r=e.mediaKeysSession,i=e.licenseXhr,n=e.decryptdata;if(r){this.log('Remove licenses and keys and close session "'+r.sessionId+'" keyId: '+X((null==n?void 0:n.keyId)||[])),e._onmessage&&(r.removeEventListener("message",e._onmessage),e._onmessage=void 0),e._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",e._onkeystatuseschange),e._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),e.mediaKeysSession=e.decryptdata=e.licenseXhr=void 0;var a=this.mediaKeySessions.indexOf(e);a>-1&&this.mediaKeySessions.splice(a,1);var s=e.keyStatusTimeouts;s&&Object.keys(s).forEach((function(e){return self.clearTimeout(s[e])}));var o=function(e){var t;return!(!e||"persistent-license"!==e.sessionType&&(null==(t=e.sessionTypes)||!t.some((function(e){return"persistent-license"===e}))))}(this.config.drmSystemOptions)?new Promise((function(e,t){self.setTimeout((function(){return t(new Error("MediaKeySession.remove() timeout"))}),8e3),r.remove().then(e).catch(t)})):Promise.resolve();return o.catch((function(e){t.log("Could not remove session: "+e),t.hls&&t.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR,fatal:!1,error:new Error("Could not remove session: "+e)})})).then((function(){return r.close()})).catch((function(e){t.log("Could not close session: "+e),t.hls&&t.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,fatal:!1,error:new Error("Could not close session: "+e)})}))}return Promise.resolve()},t}(N);function Bs(e){if(!e)throw new Error("Could not read keyId of undefined decryptdata");if(null===e.keyId)throw new Error("keyId is null");return X(e.keyId)}function Gs(e,t){return e.keyId&&t.mediaKeysSession.keyStatuses.has(e.keyId)?t.mediaKeysSession.keyStatuses.get(e.keyId):e.matches(t.decryptdata)?t.keyStatus:void 0}Us.CDMCleanupPromise=void 0;var Ks=function(e){function t(t,r){var i;return(i=e.call(this,r)||this).data=void 0,t.error||(t.error=new Error(r)),i.data=t,t.err=t.error,i}return o(t,e),t}(c(Error));function Vs(e,t){var r="output-restricted"===e,i=r?k.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:k.KEY_SYSTEM_STATUS_INTERNAL_ERROR;return new Ks({type:R.KEY_SYSTEM_ERROR,details:i,fatal:!1,decryptdata:t},r?"HDCP level output restricted":'key status changed to "'+e+'"')}var Hs=function(){function e(e){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=e,this.registerListeners()}var t=e.prototype;return t.setStreamController=function(e){this.streamController=e},t.registerListeners=function(){this.hls.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),this.hls.on(b.MEDIA_DETACHING,this.onMediaDetaching,this)},t.unregisterListeners=function(){this.hls.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),this.hls.off(b.MEDIA_DETACHING,this.onMediaDetaching,this)},t.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},t.onMediaAttaching=function(e,t){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=t.media instanceof self.HTMLVideoElement?t.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},t.onMediaDetaching=function(){this.media=null},t.checkFPS=function(e,t,r){var i=performance.now();if(t){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=t-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(b.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;l.logger.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(b.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=t}},t.checkFPSInterval=function(){var e=this.media;if(e)if(this.isVideoPlaybackQualityAvailable){var t=e.getVideoPlaybackQuality();this.checkFPS(e,t.totalVideoFrames,t.droppedVideoFrames)}else this.checkFPS(e,e.webkitDecodedFrameCount,e.webkitDroppedFrameCount)},e}();function Ys(e){for(var t=5381,r=e.length;r;)t=33*t^e.charCodeAt(--r);return(t>>>0).toString()}var Ws=.025,js=function(e){return e[e.Point=0]="Point",e[e.Range=1]="Range",e}({});function qs(e,t,r){return e.identifier+"-"+(r+1)+"-"+Ys(t)}var Xs=function(){function e(e,t){this.base=void 0,this._duration=null,this._timelineStart=null,this.appendInPlaceDisabled=void 0,this.appendInPlaceStarted=void 0,this.dateRange=void 0,this.hasPlayed=!1,this.cumulativeDuration=0,this.resumeOffset=NaN,this.playoutLimit=NaN,this.restrictions={skip:!1,jump:!1},this.snapOptions={out:!1,in:!1},this.assetList=[],this.assetListLoader=void 0,this.assetListResponse=null,this.resumeAnchor=void 0,this.error=void 0,this.resetOnResume=void 0,this.base=t,this.dateRange=e,this.setDateRange(e)}var t=e.prototype;return t.setDateRange=function(e){this.dateRange=e,this.resumeOffset=e.attr.optionalFloat("X-RESUME-OFFSET",this.resumeOffset),this.playoutLimit=e.attr.optionalFloat("X-PLAYOUT-LIMIT",this.playoutLimit),this.restrictions=e.attr.enumeratedStringList("X-RESTRICT",this.restrictions),this.snapOptions=e.attr.enumeratedStringList("X-SNAP",this.snapOptions)},t.reset=function(){var e;this.appendInPlaceStarted=!1,null==(e=this.assetListLoader)||e.destroy(),this.assetListLoader=void 0,this.supplementsPrimary||(this.assetListResponse=null,this.assetList=[],this._duration=null)},t.isAssetPastPlayoutLimit=function(e){var t;if(e>0&&e>=this.assetList.length)return!0;var r=this.playoutLimit;return!(e<=0||isNaN(r))&&(0===r||((null==(t=this.assetList[e])?void 0:t.startOffset)||0)>r)},t.findAssetIndex=function(e){return this.assetList.indexOf(e)},t.toString=function(){return'["'+(e=this).identifier+'" '+(e.cue.pre?"
":e.cue.post?"":"")+e.timelineStart.toFixed(2)+"-"+e.resumeTime.toFixed(2)+"]";var e},i(e,[{key:"identifier",get:function(){return this.dateRange.id}},{key:"startDate",get:function(){return this.dateRange.startDate}},{key:"startTime",get:function(){var e=this.dateRange.startTime;if(this.snapOptions.out){var t=this.dateRange.tagAnchor;if(t)return Qs(e,t)}return e}},{key:"startOffset",get:function(){return this.cue.pre?0:this.startTime}},{key:"startIsAligned",get:function(){if(0===this.startTime||this.snapOptions.out)return!0;var e=this.dateRange.tagAnchor;if(e){var t=this.dateRange.startTime;return t-Qs(t,e)<.1}return!1}},{key:"resumptionOffset",get:function(){var e=this.resumeOffset,t=A(e)?e:this.duration;return this.cumulativeDuration+t}},{key:"resumeTime",get:function(){var e=this.startOffset+this.resumptionOffset;if(this.snapOptions.in){var t=this.resumeAnchor;if(t)return Qs(e,t)}return e}},{key:"appendInPlace",get:function(){return!!this.appendInPlaceStarted||!this.appendInPlaceDisabled&&!(this.cue.once||this.cue.pre||!this.startIsAligned||!(isNaN(this.playoutLimit)&&isNaN(this.resumeOffset)||this.resumeOffset&&this.duration&&Math.abs(this.resumeOffset-this.duration)0||null!==this.assetListResponse}}])}();function Qs(e,t){return e-t.start=r-.02},t.reachedPlayout=function(e){var t=this.interstitial.playoutLimit;return this.startOffset+e>=t},t.getAssetTime=function(e){var t=this.timelineOffset,r=this.duration;return Math.min(Math.max(0,e-t),r)},t.removeMediaListeners=function(){var e=this.mediaAttached;e&&(this._currentTime=e.currentTime,this.bufferSnapShot(),e.removeEventListener("timeupdate",this.checkPlayout))},t.bufferSnapShot=function(){var e;this.mediaAttached&&null!=(e=this.hls)&&e.bufferedToEnd&&(this._bufferedEosTime=this.bufferedEnd)},t.destroy=function(){this.removeMediaListeners(),this.hls&&this.hls.destroy(),this.hls=null,this.tracks=this.mediaAttached=this.checkPlayout=null},t.attachMedia=function(e){var t;this.loadSource(),null==(t=this.hls)||t.attachMedia(e)},t.detachMedia=function(){var e;this.removeMediaListeners(),this.mediaAttached=null,null==(e=this.hls)||e.detachMedia()},t.resumeBuffering=function(){var e;null==(e=this.hls)||e.resumeBuffering()},t.pauseBuffering=function(){var e;null==(e=this.hls)||e.pauseBuffering()},t.transferMedia=function(){var e;return this.bufferSnapShot(),(null==(e=this.hls)?void 0:e.transferMedia())||null},t.resetDetails=function(){var e=this.hls;if(e&&this.hasDetails){e.stopLoad();var t=function(e){return delete e.details};e.levels.forEach(t),e.allAudioTracks.forEach(t),e.allSubtitleTracks.forEach(t),this.hasDetails=!1}},t.on=function(e,t,r){var i;null==(i=this.hls)||i.on(e,t)},t.once=function(e,t,r){var i;null==(i=this.hls)||i.once(e,t)},t.off=function(e,t,r){var i;null==(i=this.hls)||i.off(e,t)},t.toString=function(){var e;return"HlsAssetPlayer: "+Zs(this.assetItem)+" "+(null==(e=this.hls)?void 0:e.sessionId)+" "+(this.appendInPlace?"append-in-place":"")},i(e,[{key:"appendInPlace",get:function(){return this.interstitial.appendInPlace}},{key:"destroyed",get:function(){var e;return!(null!=(e=this.hls)&&e.userConfig)}},{key:"assetId",get:function(){return this.assetItem.identifier}},{key:"interstitialId",get:function(){return this.assetItem.parentIdentifier}},{key:"media",get:function(){var e;return(null==(e=this.hls)?void 0:e.media)||null}},{key:"bufferedEnd",get:function(){var e=this.media||this.mediaAttached;if(!e)return this._bufferedEosTime?this._bufferedEosTime:this.currentTime;var t=dr.bufferInfo(e,e.currentTime,.001);return this.getAssetTime(t.end)}},{key:"currentTime",get:function(){var e=this.media||this.mediaAttached;return e?this.getAssetTime(e.currentTime):this._currentTime||0}},{key:"duration",get:function(){var e=this.assetItem.duration;if(!e)return 0;var t=this.interstitial.playoutLimit;if(t){var r=t-this.startOffset;if(r>0&&r1/9e4&&this.hls){if(this.hasDetails)throw new Error("Cannot set timelineOffset after playlists are loaded");this.hls.config.timelineOffset=e}}}}])}(),eo=function(e){function t(t,r){var i;return(i=e.call(this,"interstitials-sched",r)||this).onScheduleUpdate=void 0,i.eventMap={},i.events=null,i.items=null,i.durations={primary:0,playout:0,integrated:0},i.onScheduleUpdate=t,i}o(t,e);var r=t.prototype;return r.destroy=function(){this.reset(),this.onScheduleUpdate=null},r.reset=function(){this.eventMap={},this.setDurations(0,0,0),this.events&&this.events.forEach((function(e){return e.reset()})),this.events=this.items=null},r.resetErrorsInRange=function(e,t){return this.events?this.events.reduce((function(r,i){return e<=i.startOffset&&t>i.startOffset?(delete i.error,r+1):r}),0):0},r.getEvent=function(e){return e&&this.eventMap[e]||null},r.hasEvent=function(e){return e in this.eventMap},r.findItemIndex=function(e,t){if(e.event)return this.findEventIndex(e.event.identifier);var r=-1;e.nextEvent?r=this.findEventIndex(e.nextEvent.identifier)-1:e.previousEvent&&(r=this.findEventIndex(e.previousEvent.identifier)+1);var i=this.items;if(i)for(i[r]||(void 0===t&&(t=e.start),r=this.findItemIndexAtTime(t));r>=0&&null!=(n=i[r])&&n.event;){var n;r--}return r},r.findItemIndexAtTime=function(e,t){var r=this.items;if(r)for(var i=0;in.start&&e1)for(var n=0;ns&&(t.005||Math.abs(e.playout.end-n[t].playout.end)>.005})))&&(this.items=a,this.onScheduleUpdate(t,n))}},r.parseDateRanges=function(e,t,r){for(var i=[],n=Object.keys(e),a=0;a.033){var A=s,L=o;o+=S;var I=a;a+=S;var R={previousEvent:e[i-1]||null,nextEvent:t,start:A,end:A+S,playout:{start:I,end:a},integrated:{start:L,end:o}};r.push(R)}else S>0&&d&&(d.cumulativeDuration+=S,r[r.length-1].end=f)}u&&(y=p),t.timelineStart=p;var k=o;o+=g;var b=a;a+=c,r.push({event:t,start:p,end:y,playout:{start:b,end:a},integrated:{start:k,end:o}})}var D=t.resumeTime;s=u||D>n?n:D})),sWs?(this.log('"'+e.identifier+'" resumption '+i+" not aligned with estimated timeline end "+n),!1):!Object.keys(t).some((function(n){var a=t[n].details,s=a.edge;if(i>=s)return r.log('"'+e.identifier+'" resumption '+i+" past "+n+" playlist end "+s),!1;var o=Tt(null,a.fragments,i);if(!o)return r.log('"'+e.identifier+'" resumption '+i+" does not align with any fragments in "+n+" playlist ("+a.fragStart+"-"+a.fragmentEnd+")"),!0;var l="audio"===n?.175:0;return!(Math.abs(o.start-i)=n.end){var a,s=i.findItemIndex(n),o=i.schedule.findItemIndexAtTime(e);if(-1===o&&(o=s+(r?-1:1),i.log("seeked "+(r?"back ":"")+"to position not covered by schedule "+e+" (resolving from "+s+" to "+o+")")),!i.isInterstitial(n)&&null!=(a=i.media)&&a.paused&&(i.shouldPlay=!1),!r&&o>s){var l=i.schedule.findJumpRestrictedIndex(s+1,o);if(l>s)return void i.setSchedulePosition(l)}i.setSchedulePosition(o)}else{var u=i.playingAsset;if(u){var d,h=u.timelineStart,f=u.duration||0;(r&&e=h+f)&&(null!=(d=n.event)&&d.appendInPlace&&(i.clearAssetPlayers(n.event,n),i.flushFrontBuffer(e)),i.setScheduleToAssetAtTime(e,u))}else if(i.playingLastItem&&i.isInterstitial(n)){var c=n.event.assetList[0];c&&(i.endedItem=i.playingItem,i.playingItem=null,i.setScheduleToAssetAtTime(e,c))}}else i.checkBuffer()}}},i.onTimeupdate=function(){var e=i.currentTime;if(void 0!==e&&!i.playbackDisabled&&e>i.timelinePos){i.timelinePos=e,e>i.bufferedPos&&i.checkBuffer();var t=i.playingItem;if(t&&!i.playingLastItem){if(e>=t.end){i.timelinePos=t.end;var r=i.findItemIndex(t);i.setSchedulePosition(r+1)}var n=i.playingAsset;n&&e>=n.timelineStart+(n.duration||0)&&i.setScheduleToAssetAtTime(e,n)}}},i.onScheduleUpdate=function(e,t){var r=i.schedule;if(r){var n=i.playingItem,a=r.events||[],s=r.items||[],o=r.durations,l=e.map((function(e){return e.identifier})),u=!(!a.length&&!l.length);(u||t)&&i.log("INTERSTITIALS_UPDATED ("+a.length+"): "+a+"\nSchedule: "+s.map((function(e){return to(e)}))+" pos: "+i.timelinePos),l.length&&i.log("Removed events "+l);var d=null,h=null;n&&(d=i.updateItem(n,i.timelinePos),i.itemsMatch(n,d)?i.playingItem=d:i.waitingItem=i.endedItem=null),i.waitingItem=i.updateItem(i.waitingItem),i.endedItem=i.updateItem(i.endedItem);var f=i.bufferingItem;if(f&&(h=i.updateItem(f,i.bufferedPos),i.itemsMatch(f,h)?i.bufferingItem=h:f.event&&(i.bufferingItem=i.playingItem,i.clearInterstitial(f.event,null))),e.forEach((function(e){e.assetList.forEach((function(e){i.clearAssetPlayer(e.identifier,null)}))})),i.playerQueue.forEach((function(e){if(e.interstitial.appendInPlace){var t=e.assetItem.timelineStart,r=e.timelineOffset-t;if(r)try{e.timelineOffset=t}catch(n){Math.abs(r)>Ws&&i.warn(n+' ("'+e.assetId+'" '+e.timelineOffset+"->"+t+")")}}})),u||t){if(i.hls.trigger(b.INTERSTITIALS_UPDATED,{events:a.slice(0),schedule:s.slice(0),durations:o,removedIds:l}),i.isInterstitial(n)&&l.includes(n.event.identifier))return i.warn('Interstitial "'+n.event.identifier+'" removed while playing'),void i.primaryFallback(n.event);n&&i.trimInPlace(d,n),f&&h!==d&&i.trimInPlace(h,f),i.checkBuffer()}}},i.hls=t,i.HlsPlayerClass=r,i.assetListLoader=new ro(t),i.schedule=new eo(i.onScheduleUpdate,t.logger),i.registerListeners(),i}o(t,e);var r=t.prototype;return r.registerListeners=function(){var e=this.hls;e&&(e.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.on(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),e.on(b.AUDIO_TRACK_UPDATED,this.onAudioTrackUpdated,this),e.on(b.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),e.on(b.SUBTITLE_TRACK_UPDATED,this.onSubtitleTrackUpdated,this),e.on(b.EVENT_CUE_ENTER,this.onInterstitialCueEnter,this),e.on(b.ASSET_LIST_LOADED,this.onAssetListLoaded,this),e.on(b.BUFFER_APPENDED,this.onBufferAppended,this),e.on(b.BUFFER_FLUSHED,this.onBufferFlushed,this),e.on(b.BUFFERED_TO_END,this.onBufferedToEnd,this),e.on(b.MEDIA_ENDED,this.onMediaEnded,this),e.on(b.ERROR,this.onError,this),e.on(b.DESTROYING,this.onDestroying,this))},r.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.off(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),e.off(b.AUDIO_TRACK_UPDATED,this.onAudioTrackUpdated,this),e.off(b.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),e.off(b.SUBTITLE_TRACK_UPDATED,this.onSubtitleTrackUpdated,this),e.off(b.EVENT_CUE_ENTER,this.onInterstitialCueEnter,this),e.off(b.ASSET_LIST_LOADED,this.onAssetListLoaded,this),e.off(b.BUFFER_CODECS,this.onBufferCodecs,this),e.off(b.BUFFER_APPENDED,this.onBufferAppended,this),e.off(b.BUFFER_FLUSHED,this.onBufferFlushed,this),e.off(b.BUFFERED_TO_END,this.onBufferedToEnd,this),e.off(b.MEDIA_ENDED,this.onMediaEnded,this),e.off(b.ERROR,this.onError,this),e.off(b.DESTROYING,this.onDestroying,this))},r.startLoad=function(){this.resumeBuffering()},r.stopLoad=function(){this.pauseBuffering()},r.resumeBuffering=function(){var e;null==(e=this.getBufferingPlayer())||e.resumeBuffering()},r.pauseBuffering=function(){var e;null==(e=this.getBufferingPlayer())||e.pauseBuffering()},r.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.assetListLoader&&this.assetListLoader.destroy(),this.emptyPlayerQueue(),this.clearScheduleState(),this.schedule&&this.schedule.destroy(),this.media=this.detachedData=this.mediaSelection=this.requiredTracks=this.altSelection=this.schedule=this.manager=null,this.hls=this.HlsPlayerClass=this.log=null,this.assetListLoader=null,this.onPlay=this.onPause=this.onSeeking=this.onTimeupdate=null,this.onScheduleUpdate=null},r.onDestroying=function(){var e=this.primaryMedia||this.media;e&&this.removeMediaListeners(e)},r.removeMediaListeners=function(e){bi(e,"play",this.onPlay),bi(e,"pause",this.onPause),bi(e,"seeking",this.onSeeking),bi(e,"timeupdate",this.onTimeupdate)},r.onMediaAttaching=function(e,t){var r=this.media=t.media;ki(r,"seeking",this.onSeeking),ki(r,"timeupdate",this.onTimeupdate),ki(r,"play",this.onPlay),ki(r,"pause",this.onPause)},r.onMediaAttached=function(e,t){var r=this.effectivePlayingItem,i=this.detachedData;if(this.detachedData=null,null===r)this.checkStart();else if(!i){this.clearScheduleState();var n=this.findItemIndex(r);this.setSchedulePosition(n)}},r.clearScheduleState=function(){this.log("clear schedule state"),this.playingItem=this.bufferingItem=this.waitingItem=this.endedItem=this.playingAsset=this.endedAsset=this.bufferingAsset=null},r.onMediaDetaching=function(e,t){var r=!!t.transferMedia,i=this.media;if(this.media=null,!r&&(i&&this.removeMediaListeners(i),this.detachedData)){var n=this.getBufferingPlayer();n&&(this.log("Removing schedule state for detachedData and "+n),this.playingAsset=this.endedAsset=this.bufferingAsset=this.bufferingItem=this.waitingItem=this.detachedData=null,n.detachMedia()),this.shouldPlay=!1}},r.isInterstitial=function(e){return!(null==e||!e.event)},r.retreiveMediaSource=function(e,t){var r=this.getAssetPlayer(e);r&&this.transferMediaFromPlayer(r,t)},r.transferMediaFromPlayer=function(e,t){var r=e.interstitial.appendInPlace,i=e.media;if(r&&i===this.primaryMedia){if(this.bufferingAsset=null,(!t||this.isInterstitial(t)&&!t.event.appendInPlace)&&t&&i)return void(this.detachedData={media:i});var n=e.transferMedia();this.log("transfer MediaSource from "+e+" "+ut(n)),this.detachedData=n}else t&&i&&(this.shouldPlay||(this.shouldPlay=!i.paused))},r.transferMediaTo=function(e,t){var r,i,n=this;if(e.media!==t){var a,s=null,o=this.hls,l=e!==o,u=l&&e.interstitial.appendInPlace,d=null==(r=this.detachedData)?void 0:r.mediaSource;if(o.media)u&&(s=o.transferMedia(),this.detachedData=s),a="Primary";else if(d){var h=this.getBufferingPlayer();h?(s=h.transferMedia(),a=""+h):a="detached MediaSource"}else a="detached media";if(!s)if(d)s=this.detachedData,this.log("using detachedData: MediaSource "+ut(s));else if(!this.detachedData||o.media===t){var f=this.playerQueue;f.length>1&&f.forEach((function(e){if(l&&e.interstitial.appendInPlace!==u){var t=e.interstitial;n.clearInterstitial(e.interstitial,null),t.appendInPlace=!1,t.appendInPlace&&n.warn("Could not change append strategy for queued assets "+t)}})),this.hls.detachMedia(),this.detachedData={media:t}}var c=s&&"mediaSource"in s&&"closed"!==(null==(i=s.mediaSource)?void 0:i.readyState),g=c&&s?s:t;this.log((c?"transfering MediaSource":"attaching media")+" to "+(l?e:"Primary")+" from "+a+" (media.currentTime: "+t.currentTime+")");var v=this.schedule;if(g===s&&v){var m=l&&e.assetId===v.assetIdAtEnd;g.overrides={duration:v.duration,endOfStream:!l||m,cueRemoval:!l}}e.attachMedia(g)}},r.onInterstitialCueEnter=function(){this.onTimeupdate()},r.checkStart=function(){var e=this.schedule,t=null==e?void 0:e.events;if(t&&!this.playbackDisabled&&this.media){-1===this.bufferedPos&&(this.bufferedPos=0);var r=this.timelinePos,i=this.effectivePlayingItem;if(-1===r){var n=this.hls.startPosition;if(this.log(no("checkStart",n)),this.timelinePos=n,t.length&&t[0].cue.pre){var a=e.findEventIndex(t[0].identifier);this.setSchedulePosition(a)}else if(n>=0||!this.primaryLive){var s=this.timelinePos=n>0?n:0,o=e.findItemIndexAtTime(s);this.setSchedulePosition(o)}}else if(i&&!this.playingItem){var l=e.findItemIndex(i);this.setSchedulePosition(l)}}},r.advanceAssetBuffering=function(e,t){var r=e.event,i=r.findAssetIndex(t),n=$s(r,i);if(r.isAssetPastPlayoutLimit(n)){if(this.schedule){var a,s=null==(a=this.schedule.items)?void 0:a[this.findItemIndex(e)+1];s&&this.bufferedToItem(s)}}else this.bufferedToEvent(e,n)},r.advanceAfterAssetEnded=function(e,t,r){var i=$s(e,r);if(e.isAssetPastPlayoutLimit(i)){if(this.schedule){var n=this.schedule.items;if(n){var a=t+1;if(a>=n.length)return void this.setSchedulePosition(-1);var s=e.resumeTime;this.timelinePos=0?i[e]:null;this.log("setSchedulePosition "+e+", "+t+" ("+(n?to(n):n)+") pos: "+this.timelinePos);var a=this.waitingItem||this.playingItem,s=this.playingLastItem;if(this.isInterstitial(a)){var o=a.event,l=this.playingAsset,u=null==l?void 0:l.identifier,d=u?this.getAssetPlayer(u):null;if(d&&u&&(!this.eventItemsMatch(a,n)||void 0!==t&&u!==o.assetList[t].identifier)){var h,f=o.findAssetIndex(l);if(this.log("INTERSTITIAL_ASSET_ENDED "+(f+1)+"/"+o.assetList.length+" "+Zs(l)),this.endedAsset=l,this.playingAsset=null,this.hls.trigger(b.INTERSTITIAL_ASSET_ENDED,{asset:l,assetListIndex:f,event:o,schedule:i.slice(0),scheduleIndex:e,player:d}),a!==this.playingItem)return void(this.itemsMatch(a,this.playingItem)&&!this.playingAsset&&this.advanceAfterAssetEnded(o,this.findItemIndex(this.playingItem),f));this.retreiveMediaSource(u,n),!d.media||null!=(h=this.detachedData)&&h.mediaSource||d.detachMedia()}if(!this.eventItemsMatch(a,n)&&(this.endedItem=a,this.playingItem=null,this.log("INTERSTITIAL_ENDED "+o+" "+to(a)),o.hasPlayed=!0,this.hls.trigger(b.INTERSTITIAL_ENDED,{event:o,schedule:i.slice(0),scheduleIndex:e}),o.cue.once)){var c;this.updateSchedule();var g=null==(c=this.schedule)?void 0:c.items;if(n&&g){var v=this.findItemIndex(n);this.advanceSchedule(v,g,t,a,s)}return}}this.advanceSchedule(e,i,t,a,s)}},r.advanceSchedule=function(e,t,r,i,n){var a=this,s=this.schedule;if(s){var o=t[e]||null,l=this.primaryMedia,u=this.playerQueue;if(u.length&&u.forEach((function(t){var r=t.interstitial,i=s.findEventIndex(r.identifier);(ie+1)&&a.clearInterstitial(r,o)})),this.isInterstitial(o)){this.timelinePos=Math.min(Math.max(this.timelinePos,o.start),o.end);var d=o.event;if(void 0===r){var h=$s(d,(r=s.findAssetIndex(d,this.timelinePos))-1);if(d.isAssetPastPlayoutLimit(h)||d.appendInPlace&&this.timelinePos===o.end)return void this.advanceAfterAssetEnded(d,e,r);r=h}var f=this.waitingItem;this.assetsBuffered(o,l)||this.setBufferingItem(o);var c=this.preloadAssets(d,r);if(this.eventItemsMatch(o,f||i)||(this.waitingItem=o,this.log("INTERSTITIAL_STARTED "+to(o)+" "+(d.appendInPlace?"append in place":"")),this.hls.trigger(b.INTERSTITIAL_STARTED,{event:d,schedule:t.slice(0),scheduleIndex:e})),!d.assetListLoaded)return void this.log("Waiting for ASSET-LIST to complete loading "+d);if(d.assetListLoader&&(d.assetListLoader.destroy(),d.assetListLoader=void 0),!l)return void this.log("Waiting for attachMedia to start Interstitial "+d);this.waitingItem=this.endedItem=null,this.playingItem=o;var g=d.assetList[r];if(!g)return void this.advanceAfterAssetEnded(d,e,r||0);if(c||(c=this.getAssetPlayer(g.identifier)),null===c||c.destroyed){var v=d.assetList.length;this.warn("asset "+(r+1)+"/"+v+" player destroyed "+d),(c=this.createAssetPlayer(d,g,r)).loadSource()}if(!this.eventItemsMatch(o,this.bufferingItem)&&d.appendInPlace&&this.isAssetBuffered(g))return;this.startAssetPlayer(c,r,t,e,l),this.shouldPlay&&io(c.media)}else o?(this.resumePrimary(o,e,i),this.shouldPlay&&io(this.hls.media)):n&&this.isInterstitial(i)&&(this.endedItem=null,this.playingItem=i,i.event.appendInPlace||this.attachPrimary(s.durations.primary,null))}},r.resumePrimary=function(e,t,r){var i,n;if(this.playingItem=e,this.playingAsset=this.endedAsset=null,this.waitingItem=this.endedItem=null,this.bufferedToItem(e),this.log("resuming "+to(e)),null==(i=this.detachedData)||!i.mediaSource){var a=this.timelinePos;(a=e.end)&&(a=this.getPrimaryResumption(e,t),this.log(no("resumePrimary",a)),this.timelinePos=a),this.attachPrimary(a,e)}if(r){var s=null==(n=this.schedule)?void 0:n.items;s&&(this.log("INTERSTITIALS_PRIMARY_RESUMED "+to(e)),this.hls.trigger(b.INTERSTITIALS_PRIMARY_RESUMED,{schedule:s.slice(0),scheduleIndex:t}),this.checkBuffer())}},r.getPrimaryResumption=function(e,t){var r=e.start;if(this.primaryLive){var i=this.primaryDetails;if(0===t)return this.hls.startPosition;if(i&&(ri.edge))return this.hls.liveSyncPosition||-1}return r},r.isAssetBuffered=function(e){var t=this.getAssetPlayer(e.identifier);return null!=t&&t.hls?t.hls.bufferedToEnd:dr.bufferInfo(this.primaryMedia,this.timelinePos,0).end+1>=e.timelineStart+(e.duration||0)},r.attachPrimary=function(e,t,r){t?this.setBufferingItem(t):this.bufferingItem=this.playingItem,this.bufferingAsset=null;var i=this.primaryMedia;if(i){var n=this.hls;n.media?this.checkBuffer():(this.transferMediaTo(n,i),r&&this.startLoadingPrimaryAt(e,r)),r||(this.log(no("attachPrimary",e)),this.timelinePos=e,this.startLoadingPrimaryAt(e,r))}},r.startLoadingPrimaryAt=function(e,t){var r,i=this.hls;!i.loadingEnabled||!i.media||Math.abs(((null==(r=i.mainForwardBufferInfo)?void 0:r.start)||i.media.currentTime)-e)>.5?i.startLoad(e,t):i.bufferingEnabled||i.resumeBuffering()},r.onManifestLoading=function(){var e;this.stopLoad(),null==(e=this.schedule)||e.reset(),this.emptyPlayerQueue(),this.clearScheduleState(),this.shouldPlay=!1,this.bufferedPos=this.timelinePos=-1,this.mediaSelection=this.altSelection=this.manager=this.requiredTracks=null,this.hls.off(b.BUFFER_CODECS,this.onBufferCodecs,this),this.hls.on(b.BUFFER_CODECS,this.onBufferCodecs,this)},r.onLevelUpdated=function(e,t){if(-1!==t.level&&this.schedule){var r=this.hls.levels[t.level];if(r.details){var i=d(d({},this.mediaSelection||this.altSelection),{},{main:r});this.mediaSelection=i,this.schedule.parseInterstitialDateRanges(i,this.hls.config.interstitialAppendInPlace),!this.effectivePlayingItem&&this.schedule.items&&this.checkStart()}}},r.onAudioTrackUpdated=function(e,t){var r=this.hls.audioTracks[t.id],i=this.mediaSelection;if(i){var n=d(d({},i),{},{audio:r});this.mediaSelection=n}else this.altSelection=d(d({},this.altSelection),{},{audio:r})},r.onSubtitleTrackUpdated=function(e,t){var r=this.hls.subtitleTracks[t.id],i=this.mediaSelection;if(i){var n=d(d({},i),{},{subtitles:r});this.mediaSelection=n}else this.altSelection=d(d({},this.altSelection),{},{subtitles:r})},r.onAudioTrackSwitching=function(e,t){var r=ft(t);this.playerQueue.forEach((function(e){var i=e.hls;return i&&(i.setAudioOption(t)||i.setAudioOption(r))}))},r.onSubtitleTrackSwitch=function(e,t){var r=ft(t);this.playerQueue.forEach((function(e){var i=e.hls;return i&&(i.setSubtitleOption(t)||-1!==t.id&&i.setSubtitleOption(r))}))},r.onBufferCodecs=function(e,t){var r=t.tracks;r&&(this.requiredTracks=r)},r.onBufferAppended=function(e,t){this.checkBuffer()},r.onBufferFlushed=function(e,t){var r=this.playingItem;if(r&&!this.itemsMatch(r,this.bufferingItem)&&!this.isInterstitial(r)){var i=this.timelinePos;this.bufferedPos=i,this.checkBuffer()}},r.onBufferedToEnd=function(e){if(this.schedule){var t=this.schedule.events;if(this.bufferedPos.25){e.event.assetList.forEach((function(t,i){e.event.isAssetPastPlayoutLimit(i)&&r.clearAssetPlayer(t.identifier,null)}));var i=e.end+.25,n=dr.bufferInfo(this.primaryMedia,i,0);(n.end>i||(n.nextStart||0)>i)&&(this.log("trim buffered interstitial "+to(e)+" (was "+to(t)+")"),this.attachPrimary(i,null,!0),this.flushFrontBuffer(i))}},r.itemsMatch=function(e,t){return!!t&&(e===t||e.event&&t.event&&this.eventItemsMatch(e,t)||!e.event&&!t.event&&this.findItemIndex(e)===this.findItemIndex(t))},r.eventItemsMatch=function(e,t){var r;return!!t&&(e===t||e.event.identifier===(null==(r=t.event)?void 0:r.identifier))},r.findItemIndex=function(e,t){return e&&this.schedule?this.schedule.findItemIndex(e,t):-1},r.updateSchedule=function(e){var t;void 0===e&&(e=!1);var r=this.mediaSelection;r&&(null==(t=this.schedule)||t.updateSchedule(r,[],e))},r.checkBuffer=function(e){var t,r=null==(t=this.schedule)?void 0:t.items;if(r){var i=dr.bufferInfo(this.primaryMedia,this.timelinePos,0);e&&(this.bufferedPos=this.timelinePos),e||(e=i.len<1),this.updateBufferedPos(i.end,r,e)}},r.updateBufferedPos=function(e,t,r){var i=this.schedule,n=this.bufferingItem;if(!(this.bufferedPos>e)&&i)if(1===t.length&&this.itemsMatch(t[0],n))this.bufferedPos=e;else{var a=this.playingItem,s=this.findItemIndex(a),o=i.findItemIndexAtTime(e);if(this.bufferedPos=n.end||null!=(l=h.event)&&l.appendInPlace&&e+.01>=h.start)&&(o=d),this.isInterstitial(n)){var f=n.event;if(d-s>1&&!1===f.appendInPlace)return;if(0===f.assetList.length&&f.assetListLoader)return}if(this.bufferedPos=e,o>u&&o>s)this.bufferedToItem(h);else{var c=this.primaryDetails;this.primaryLive&&c&&e>c.edge-c.targetduration&&h.start0&&(s=Math.round(1e3*h)/1e3)}if(this.log("Load interstitial asset "+(t+1)+"/"+(r?1:i)+" "+e+(s?" live-start: "+d+" start-offset: "+s:"")),r)return this.createAsset(e,0,0,o,e.duration,r);var f=this.assetListLoader.loadAssetList(e,s);f&&(e.assetListLoader=f)}else if(!a&&i){for(var c=t;c1){var g=t.duration;g&&cd)&&(E=!1,i.log('Interstitial asset "'+v+'" duration change '+d+" > "+u),t.duration=u,i.updateSchedule())}};y.on(b.LEVEL_UPDATED,(function(e,t){var r=t.details;return T(r)})),y.on(b.LEVEL_PTS_UPDATED,(function(e,t){var r=t.details;return T(r)})),y.on(b.EVENT_CUE_ENTER,(function(){return i.onInterstitialCueEnter()}));var S=function(e,t){var r=i.getAssetPlayer(v);if(r&&t.tracks){r.off(b.BUFFER_CODECS,S),r.tracks=t.tracks;var n=i.primaryMedia;i.bufferingAsset===r.assetItem&&n&&!r.media&&i.bufferAssetPlayer(r,n)}};y.on(b.BUFFER_CODECS,S),y.on(b.BUFFERED_TO_END,(function(){var r,n=i.getAssetPlayer(v);if(i.log("buffered to end of asset "+n),n&&i.schedule){var a=i.schedule.findEventIndex(e.identifier),s=null==(r=i.schedule.items)?void 0:r[a];i.isInterstitial(s)&&i.advanceAssetBuffering(s,t)}}));var A=function(t){return function(){if(i.getAssetPlayer(v)&&i.schedule){i.shouldPlay=!0;var r=i.schedule.findEventIndex(e.identifier);i.advanceAfterAssetEnded(e,r,t)}}};return y.once(b.MEDIA_ENDED,A(r)),y.once(b.PLAYOUT_LIMIT_REACHED,A(1/0)),y.on(b.ERROR,(function(t,n){if(i.schedule){var a=i.getAssetPlayer(v);if(n.details===k.BUFFER_STALLED_ERROR)return null!=a&&a.appendInPlace?void i.handleInPlaceStall(e):(i.onTimeupdate(),void i.checkBuffer(!0));i.handleAssetItemError(n,e,i.schedule.findEventIndex(e.identifier),r,"Asset player error "+n.error+" "+e)}})),y.on(b.DESTROYING,(function(){if(i.getAssetPlayer(v)&&i.schedule){var t=new Error("Asset player destroyed unexpectedly "+v),n={fatal:!0,type:R.OTHER_ERROR,details:k.INTERSTITIAL_ASSET_ITEM_ERROR,error:t};i.handleAssetItemError(n,e,i.schedule.findEventIndex(e.identifier),r,t.message)}})),this.log("INTERSTITIAL_ASSET_PLAYER_CREATED "+Zs(t)),this.hls.trigger(b.INTERSTITIAL_ASSET_PLAYER_CREATED,{asset:t,assetListIndex:r,event:e,player:y}),y},r.clearInterstitial=function(e,t){this.clearAssetPlayers(e,t),e.reset()},r.clearAssetPlayers=function(e,t){var r=this;e.assetList.forEach((function(e){r.clearAssetPlayer(e.identifier,t)}))},r.resetAssetPlayer=function(e){var t=this.getAssetPlayerQueueIndex(e);if(-1!==t){this.log('reset asset player "'+e+'" after error');var r=this.playerQueue[t];this.transferMediaFromPlayer(r,null),r.resetDetails()}},r.clearAssetPlayer=function(e,t){var r=this.getAssetPlayerQueueIndex(e);if(-1!==r){var i=this.playerQueue[r];this.log("clear "+i+" toSegment: "+(t?to(t):t)),this.transferMediaFromPlayer(i,t),this.playerQueue.splice(r,1),i.destroy()}},r.emptyPlayerQueue=function(){for(var e;e=this.playerQueue.pop();)e.destroy();this.playerQueue=[]},r.startAssetPlayer=function(e,t,r,i,n){var a=e.interstitial,s=e.assetItem,o=e.assetId,l=a.assetList.length,u=this.playingAsset;this.endedAsset=null,this.playingAsset=s,u&&u.identifier===o||(u&&(this.clearAssetPlayer(u.identifier,r[i]),delete u.error),this.log("INTERSTITIAL_ASSET_STARTED "+(t+1)+"/"+l+" "+Zs(s)),this.hls.trigger(b.INTERSTITIAL_ASSET_STARTED,{asset:s,assetListIndex:t,event:a,schedule:r.slice(0),scheduleIndex:i,player:e})),this.bufferAssetPlayer(e,n)},r.bufferAssetPlayer=function(e,t){var r,i;if(this.schedule){var n=e.interstitial,a=e.assetItem,s=this.schedule.findEventIndex(n.identifier),o=null==(r=this.schedule.items)?void 0:r[s];if(o){e.loadSource(),this.setBufferingItem(o),this.bufferingAsset=a;var l=this.getBufferingPlayer();if(l!==e){var u=n.appendInPlace;if(!u||!1!==(null==l?void 0:l.interstitial.appendInPlace)){var d=(null==l?void 0:l.tracks)||(null==(i=this.detachedData)?void 0:i.tracks)||this.requiredTracks;if(u&&a!==this.playingAsset){if(!e.tracks)return void this.log("Waiting for track info before buffering "+e);if(d&&!j(d,e.tracks)){var h=new Error("Asset "+Zs(a)+" SourceBuffer tracks ('"+Object.keys(e.tracks)+"') are not compatible with primary content tracks ('"+Object.keys(d)+"')"),f={fatal:!0,type:R.OTHER_ERROR,details:k.INTERSTITIAL_ASSET_ITEM_ERROR,error:h},c=n.findAssetIndex(a);return void this.handleAssetItemError(f,n,s,c,h.message)}}this.transferMediaTo(e,t)}}}}},r.handleInPlaceStall=function(e){var t=this.schedule,r=this.primaryMedia;if(t&&r){var i=r.currentTime,n=t.findAssetIndex(e,i),a=e.assetList[n];if(a){var s=this.getAssetPlayer(a.identifier);if(s){var o=s.currentTime||i-a.timelineStart,l=s.duration-o;if(this.warn("Stalled at "+o+" of "+(o+l)+" in "+s+" "+e+" (media.currentTime: "+i+")"),o&&(l/r.playbackRate<.5||s.bufferedInPlaceToEnd(r))&&s.hls){var u=t.findEventIndex(e.identifier);this.advanceAfterAssetEnded(e,u,n)}}}}},r.advanceInPlace=function(e){var t=this.primaryMedia;t&&t.currentTimem.end&&this.schedule.findItemIndexAtTime(this.timelinePos)!==v)return a.error=new Error("Interstitial "+(o.length?"no longer within playback range":"asset-list is empty")+" "+this.timelinePos+" "+a),this.log(a.error.message),this.updateSchedule(!0),void this.primaryFallback(a);this.setBufferingItem(m)}this.setSchedulePosition(v)}else if((null==c?void 0:c.identifier)===s){var p=a.assetList[0];if(p){var y=this.getAssetPlayer(p.identifier);if(c.appendInPlace){var E=this.primaryMedia;y&&E&&this.bufferAssetPlayer(y,E)}else y&&y.loadSource()}}}},r.onError=function(e,t){if(this.schedule)switch(t.details){case k.ASSET_LIST_PARSING_ERROR:case k.ASSET_LIST_LOAD_ERROR:case k.ASSET_LIST_LOAD_TIMEOUT:var r=t.interstitial;r&&(this.updateSchedule(!0),this.primaryFallback(r));break;case k.BUFFER_STALLED_ERROR:var i=this.endedItem||this.waitingItem||this.playingItem;if(this.isInterstitial(i)&&i.event.appendInPlace)return void this.handleInPlaceStall(i.event);this.log("Primary player stall @"+this.timelinePos+" bufferedPos: "+this.bufferedPos),this.onTimeupdate(),this.checkBuffer(!0)}},i(t,[{key:"interstitialsManager",get:function(){if(!this.hls)return null;if(this.manager)return this.manager;var e=this,t=function(){return e.bufferingItem||e.waitingItem},r=function(t){return t?e.getAssetPlayer(t.identifier):t},i=function(t,i,a,s,o){if(t){var l=t[i].start,u=t.event;if(u){if("playout"===i||u.timelineOccupancy!==js.Point){var d=r(a);(null==d?void 0:d.interstitial)===u&&(l+=d.assetItem.startOffset+d[o])}}else l+=("bufferedPos"===s?n():e[s])-t.start;return l}return 0},n=function(){var t=e.bufferedPos;return t===Number.MAX_VALUE?a("primary"):Math.max(t,0)},a=function(t){var r,i;return null!=(r=e.primaryDetails)&&r.live?e.primaryDetails.edge:(null==(i=e.schedule)?void 0:i.durations[t])||0},s=function(t,n){var a,s,o=e.effectivePlayingItem;if((null==o||null==(a=o.event)||!a.restrictions.skip)&&e.schedule){e.log("seek to "+t+' "'+n+'"');var l=e.effectivePlayingItem,u=e.schedule.findItemIndexAtTime(t,n),d=null==(s=e.schedule.items)?void 0:s[u],h=e.getBufferingPlayer(),f=null==h?void 0:h.interstitial,c=null==f?void 0:f.appendInPlace,g=l&&e.itemsMatch(l,d);if(l&&(c||g)){var v=r(e.playingAsset),m=(null==v?void 0:v.media)||e.primaryMedia;if(m){var p="primary"===n?m.currentTime:i(l,n,e.playingAsset,"timelinePos","currentTime"),y=t-p,E=(c?p:m.currentTime)+y;if(E>=0&&(!v||c||E<=v.duration))return void(m.currentTime=E)}}if(d){var T=t;if("primary"!==n){var S=t-d[n].start;T=d.start+S}var A=!e.isInterstitial(d);if(e.isInterstitial(l)&&!l.event.appendInPlace||!A&&!d.event.appendInPlace){if(l){var L=e.findItemIndex(l);if(u>L){var I=e.schedule.findJumpRestrictedIndex(L+1,u);if(I>L)return void e.setSchedulePosition(I)}var R=0;if(A)e.timelinePos=T,e.checkBuffer();else for(var k=d.event.assetList,b=t-(d[n]||d).start,D=k.length;D--;){var _=k[D];if(_.duration&&b>=_.startOffset&&b<_.startOffset+_.duration){R=D;break}}e.setSchedulePosition(u,R)}}else{var P=e.media||(c?null==h?void 0:h.media:null);P&&(P.currentTime=T)}}}},o=function(){var r=e.effectivePlayingItem;if(e.isInterstitial(r))return r;var i=t();return e.isInterstitial(i)?i:null},l={get bufferedEnd(){var r,n=t(),a=e.bufferingItem;return a&&a===n&&(i(a,"playout",e.bufferingAsset,"bufferedPos","bufferedEnd")-a.playout.start||(null==(r=e.bufferingAsset)?void 0:r.startOffset))||0},get currentTime(){var t=o(),r=e.effectivePlayingItem;return r&&r===t?i(r,"playout",e.effectivePlayingAsset,"timelinePos","currentTime")-r.playout.start:0},set currentTime(t){var r=o(),i=e.effectivePlayingItem;i&&i===r&&s(t+i.playout.start,"playout")},get duration(){var e=o();return e?e.playout.end-e.playout.start:0},get assetPlayers(){var t,r=null==(t=o())?void 0:t.event.assetList;return r?r.map((function(t){return e.getAssetPlayer(t.identifier)})):[]},get playingIndex(){var t,r=null==(t=o())?void 0:t.event;return r&&e.effectivePlayingAsset?r.findAssetIndex(e.effectivePlayingAsset):-1},get scheduleItem(){return o()}};return this.manager={get events(){var t;return(null==(t=e.schedule)||null==(t=t.events)?void 0:t.slice(0))||[]},get schedule(){var t;return(null==(t=e.schedule)||null==(t=t.items)?void 0:t.slice(0))||[]},get interstitialPlayer(){return o()?l:null},get playerQueue(){return e.playerQueue.slice(0)},get bufferingAsset(){return e.bufferingAsset},get bufferingItem(){return t()},get bufferingIndex(){var r=t();return e.findItemIndex(r)},get playingAsset(){return e.effectivePlayingAsset},get playingItem(){return e.effectivePlayingItem},get playingIndex(){var t=e.effectivePlayingItem;return e.findItemIndex(t)},primary:{get bufferedEnd(){return n()},get currentTime(){var t=e.timelinePos;return t>0?t:0},set currentTime(e){s(e,"primary")},get duration(){return a("primary")},get seekableStart(){var t;return(null==(t=e.primaryDetails)?void 0:t.fragmentStart)||0}},integrated:{get bufferedEnd(){return i(t(),"integrated",e.bufferingAsset,"bufferedPos","bufferedEnd")},get currentTime(){return i(e.effectivePlayingItem,"integrated",e.effectivePlayingAsset,"timelinePos","currentTime")},set currentTime(e){s(e,"integrated")},get duration(){return a("integrated")},get seekableStart(){var t;return function(t,r){var i;if(0!==t&&"primary"!==r&&null!=(i=e.schedule)&&i.length){var n,a=e.schedule.findItemIndexAtTime(t),s=null==(n=e.schedule.items)?void 0:n[a];if(s)return t+(s[r].start-s.start)}return t}((null==(t=e.primaryDetails)?void 0:t.fragmentStart)||0,"integrated")}},skip:function(){var t=e.effectivePlayingItem,r=null==t?void 0:t.event;if(r&&!r.restrictions.skip){var i=e.findItemIndex(t);if(r.appendInPlace){var n=t.playout.start+t.event.duration;s(n+.001,"playout")}else e.advanceAfterAssetEnded(r,i,1/0)}}}}},{key:"effectivePlayingItem",get:function(){return this.waitingItem||this.playingItem||this.endedItem}},{key:"effectivePlayingAsset",get:function(){return this.playingAsset||this.endedAsset}},{key:"playingLastItem",get:function(){var e,t=this.playingItem,r=null==(e=this.schedule)?void 0:e.items;return!!(this.playbackStarted&&t&&r)&&this.findItemIndex(t)===r.length-1}},{key:"playbackStarted",get:function(){return null!==this.effectivePlayingItem}},{key:"currentTime",get:function(){var e,t;if(null!==this.mediaSelection){var r=this.waitingItem||this.playingItem;if(!this.isInterstitial(r)||r.event.appendInPlace){var i=this.media;!i&&null!=(e=this.bufferingItem)&&null!=(e=e.event)&&e.appendInPlace&&(i=this.primaryMedia);var n=null==(t=i)?void 0:t.currentTime;if(void 0!==n&&A(n))return n}}}},{key:"primaryMedia",get:function(){var e;return this.media||(null==(e=this.detachedData)?void 0:e.media)||null}},{key:"playbackDisabled",get:function(){return!1===this.hls.config.enableInterstitialPlayback}},{key:"primaryDetails",get:function(){var e;return null==(e=this.mediaSelection)?void 0:e.main.details}},{key:"primaryLive",get:function(){var e;return!(null==(e=this.primaryDetails)||!e.live)}}])}(N),so=function(e){function t(t,r,i){var n;return(n=e.call(this,t,r,i,"subtitle-stream-controller",x)||this).currentTrackId=-1,n.tracksBuffered=[],n.mainDetails=null,n.registerListeners(),n}o(t,e);var r=t.prototype;return r.onHandlerDestroying=function(){this.unregisterListeners(),e.prototype.onHandlerDestroying.call(this),this.mainDetails=null},r.registerListeners=function(){e.prototype.registerListeners.call(this);var t=this.hls;t.on(b.LEVEL_LOADED,this.onLevelLoaded,this),t.on(b.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(b.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.on(b.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(b.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.on(b.BUFFER_FLUSHING,this.onBufferFlushing,this)},r.unregisterListeners=function(){e.prototype.unregisterListeners.call(this);var t=this.hls;t.off(b.LEVEL_LOADED,this.onLevelLoaded,this),t.off(b.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(b.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.off(b.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(b.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.off(b.BUFFER_FLUSHING,this.onBufferFlushing,this)},r.startLoad=function(e,t){this.stopLoad(),this.state=_i.IDLE,this.setInterval(500),this.nextLoadPosition=this.lastCurrentTime=e+this.timelineOffset,this.startPosition=t?-1:e,this.tick()},r.onManifestLoading=function(){e.prototype.onManifestLoading.call(this),this.mainDetails=null},r.onMediaDetaching=function(t,r){this.tracksBuffered=[],e.prototype.onMediaDetaching.call(this,t,r)},r.onLevelLoaded=function(e,t){this.mainDetails=t.details},r.onSubtitleFragProcessed=function(e,t){var r=t.frag,i=t.success;if(this.fragContextChanged(r)||(te(r)&&(this.fragPrevious=r),this.state=_i.IDLE),i){var n=this.tracksBuffered[this.currentTrackId];if(n){for(var a,s=r.start,o=0;o=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r),this.fragBufferedComplete(r,null),this.media&&this.tick()}}},r.onBufferFlushing=function(e,t){var r=t.startOffset,i=t.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;t.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(e){for(var t=0;t=n.length)&&o){this.log("Subtitle track "+s+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+",duration:"+a.totalduration),this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(a.live||null!=(r=o.details)&&r.live){if(a.deltaUpdateFailed)return;var u=this.mainDetails;if(!u)return void(this.startFragRequested=!1);var d,h=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(d=this.levelLastLoaded)?void 0:d.details))&&h&&ci(a,l=h.start):a.hasProgramDateTime&&u.hasProgramDateTime?(Ri(a,u),l=a.fragmentStart):h&&ci(a,l=h.start),u&&!this.startFragRequested&&this.setStartPosition(u,l)}o.details=a,this.levelLastLoaded=o,s===i&&(this.hls.trigger(b.SUBTITLE_TRACK_UPDATED,{details:a,id:s,groupId:t.groupId}),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===_i.IDLE&&(Tt(null,a.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0)))}}else this.warn("Subtitle tracks were reset while loading level "+s)},r._handleFragmentLoadComplete=function(e){var t=this,r=e.frag,i=e.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&Ir(n.method)){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer,Rr(n.method)).catch((function(e){throw a.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:r}),e})).then((function(e){var t=performance.now();a.trigger(b.FRAG_DECRYPTED,{frag:r,payload:e,stats:{tstart:s,tdecrypt:t}})})).catch((function(e){t.warn(e.name+": "+e.message),t.state=_i.IDLE}))}},r.doTick=function(){if(this.media){if(this.state===_i.IDLE){var e=this.currentTrackId,t=this.levels,r=null==t?void 0:t[e];if(!r||!t.length||!r.details)return;if(this.waitForLive(r))return;var i=this.config,n=this.getLoadPosition(),a=dr.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=r.details;if(o>this.hls.maxBufferLength+l.levelTargetDuration)return;var u=l.fragments,d=u.length,h=l.edge,f=null,c=this.fragPrevious;if(sh-g?0:g;!(f=Tt(c,u,Math.max(u[0].start,s),v))&&c&&c.start>>=0)>i-1)throw new DOMException("Failed to execute '"+t+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return e[r][t]};this.buffered={get length(){return e.length},end:function(r){return t("end",r,e.length)},start:function(r){return t("start",r,e.length)}}};function lo(e,t){var r;try{r=new Event("addtrack")}catch(e){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=e,t.dispatchEvent(r)}function uo(e,t){var r=e.mode;if("disabled"===r&&(e.mode="hidden"),e.cues&&!e.cues.getCueById(t.id))try{if(e.addCue(t),!e.cues.getCueById(t.id))throw new Error("addCue is failed for: "+t)}catch(r){Y.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(t.startTime,t.endTime,t.text);i.id=t.id,e.addCue(i)}catch(e){Y.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+e)}}"disabled"===r&&(e.mode=r)}function ho(e,t){var r=e.mode;if("disabled"===r&&(e.mode="hidden"),e.cues)for(var i=e.cues.length;i--;)t&&e.cues[i].removeEventListener("enter",t),e.removeCue(e.cues[i]);"disabled"===r&&(e.mode=r)}function fo(e,t,r,i){var n=e.mode;if("disabled"===n&&(e.mode="hidden"),e.cues&&e.cues.length>0)for(var a=function(e,t,r){var i=[],n=function(e,t){if(t<=e[0].startTime)return 0;var r=e.length-1;if(t>e[r].endTime)return-1;for(var i,n=0,a=r;n<=a;)if(te[i].startTime&&n-1)for(var a=n,s=e.length;a=t&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(e.cues,t,r),s=0;s-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(e){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,e)},r.onMediaDetaching=function(e,t){var r=this.media;if(r){var i=!!t.transferMedia;self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||r.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),this.subtitleTrack=-1,this.media=null,i||co(r.textTracks).forEach((function(e){ho(e)}))}},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(e,t){this.tracks=t.subtitleTracks},r.onSubtitleTrackLoaded=function(e,t){var r=t.id,i=t.groupId,n=t.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=t.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,t,s)}else this.warn("Subtitle track with id:"+r+" and group:"+i+" not found in active group "+(null==a?void 0:a.groupId))},r.onLevelLoading=function(e,t){this.switchLevel(t.level)},r.onLevelSwitching=function(e,t){this.switchLevel(t.level)},r.switchLevel=function(e){var t=this.hls.levels[e];if(t){var r=t.subtitleGroups||null,i=this.groupIds,n=this.currentTrack;if(!r||(null==i?void 0:i.length)!==(null==r?void 0:r.length)||null!=r&&r.some((function(e){return-1===(null==i?void 0:i.indexOf(e))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(e){return!r||-1!==r.indexOf(e.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(e){return e.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(e,t){e.id=t}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=ct(s,a);if(o>-1)n=a[o];else{var l=ct(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var d={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(b.SUBTITLE_TRACKS_UPDATED,d),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}}},r.findTrackId=function(e){for(var t=this.tracksInGroup,r=this.selectDefaultTrack,i=0;i-1){var n=this.tracksInGroup[i];return this.setSubtitleTrack(i),n}if(r)return null;var a=ct(e,t);if(a>-1)return t[a]}}return null},r.loadPlaylist=function(t){e.prototype.loadPlaylist.call(this),this.shouldLoadPlaylist(this.currentTrack)&&this.scheduleLoading(this.currentTrack,t)},r.loadingPlaylist=function(t,r){e.prototype.loadingPlaylist.call(this,t,r);var i=t.id,n=t.groupId,a=this.getUrlWithDirectives(t.url,r),s=t.details,o=null==s?void 0:s.age;this.log("Loading subtitle "+i+' "'+t.name+'" lang:'+t.lang+" group:"+n+(void 0!==(null==r?void 0:r.msn)?" at sn "+r.msn+" part "+r.part:"")+(o&&s.live?" age "+o.toFixed(1)+(s.type&&" "+s.type||""):"")+" "+a),this.hls.trigger(b.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:r||null,track:t})},r.toggleTrackModes=function(){var e=this.media;if(e){var t,r=co(e.textTracks),i=this.currentTrack;if(i&&((t=r.filter((function(e){return Sa(i,e)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(e){"disabled"!==e.mode&&e!==t&&(e.mode="disabled")})),t){var n=this.subtitleDisplay?"showing":"hidden";t.mode!==n&&(t.mode=n)}}},r.setSubtitleTrack=function(e){var t=this.tracksInGroup;if(this.media)if(e<-1||e>=t.length||!A(e))this.warn("Invalid subtitle track id: "+e);else{this.selectDefaultTrack=!1;var r=this.currentTrack,i=t[e]||null;if(this.trackId=e,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(e!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+e+(i?' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId:""));var a=i.id,s=i.groupId,o=void 0===s?"":s,l=i.name,u=i.type,d=i.url;this.hls.trigger(b.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:d});var h=this.switchParams(i.url,null==r?void 0:r.details,i.details);this.loadPlaylist(h)}}else this.hls.trigger(b.SUBTITLE_TRACK_SWITCH,{id:e})}else this.queuedDefaultTrack=e},i(t,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(e){this._subtitleDisplay=e,this.trackId>-1&&this.toggleTrackModes()}},{key:"allSubtitleTracks",get:function(){return this.tracks}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(e){this.selectDefaultTrack=!1,this.setSubtitleTrack(e)}}])}(ya),vo={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},mo=function(e){return String.fromCharCode(vo[e]||e)},po=15,yo=100,Eo={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},To={17:2,18:4,21:6,22:8,23:10,19:13,20:15},So={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},Ao={25:2,26:4,29:6,30:8,31:10,27:13,28:15},Lo=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],Io=function(){function e(){this.time=null,this.verboseLevel=0}return e.prototype.log=function(e,t){if(this.verboseLevel>=e){var r="function"==typeof t?t():t;Y.log(this.time+" ["+e+"] "+r)}},e}(),Ro=function(e){for(var t=[],r=0;ryo&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=yo)},t.moveCursor=function(e){var t=this.pos+e;if(e>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=mo(e);this.pos>=yo?this.logger.log(0,(function(){return"Cannot insert "+e.toString(16)+" ("+r+") at position "+t.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},t.clearFromPos=function(e){var t;for(t=e;t0&&(r=e?"["+t.join(" | ")+"]":t.join("\n")),r},t.getTextAndFormat=function(){return this.rows},e}(),Po=function(){function e(e,t,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=e,this.outputFilter=t,this.mode=null,this.verbose=0,this.displayedMemory=new _o(r),this.nonDisplayedMemory=new _o(r),this.lastOutputScreen=new _o(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var t=e.prototype;return t.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},t.getHandler=function(){return this.outputFilter},t.setHandler=function(e){this.outputFilter=e},t.setPAC=function(e){this.writeScreen.setPAC(e)},t.setBkgData=function(e){this.writeScreen.setBkgData(e)},t.setMode=function(e){e!==this.mode&&(this.mode=e,this.logger.log(2,(function(){return"MODE="+e})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=e)},t.insertChars=function(e){for(var t=this,r=0;r=46,t.italics)t.foreground="white";else{var r=Math.floor(e/2)-16;t.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+ut(t)),this.writeScreen.setPen(t)},t.outputDataUpdate=function(e){void 0===e&&(e=!1);var t=this.logger.time;null!==t&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,t,this.lastOutputScreen),e&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:t):this.cueStartTime=t,this.lastOutputScreen.copy(this.displayedMemory))},t.cueSplitAtTime=function(e){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,e,this.displayedMemory),this.cueStartTime=e))},e}(),Co=function(){function e(e,t,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new Io;this.channels=[null,new Po(e,t,i),new Po(e+1,r,i)]}var t=e.prototype;return t.getHandler=function(e){return this.channels[e].getHandler()},t.setHandler=function(e,t){this.channels[e].setHandler(t)},t.addData=function(e,t){var r=this;this.logger.time=e;for(var i=function(e){var i=127&t[e],n=127&t[e+1],a=!1,s=null;if(0===i&&0===n)return 0;r.logger.log(3,(function(){return"["+Ro([t[e],t[e+1]])+"] -> ("+Ro([i,n])+")"}));var o=r.cmdHistory;if(i>=16&&i<=31){if(function(e,t,r){return r.a===e&&r.b===t}(i,n,o))return wo(null,null,o),r.logger.log(3,(function(){return"Repeated command ("+Ro([i,n])+") is dropped"})),0;wo(i,n,r.cmdHistory),(a=r.parseCmd(i,n))||(a=r.parseMidrow(i,n)),a||(a=r.parsePAC(i,n)),a||(a=r.parseBackgroundAttributes(i,n))}else wo(null,null,o);if(!a&&(s=r.parseChars(i,n))){var l=r.currentChannel;l&&l>0?r.channels[l].insertChars(s):r.logger.log(2,"No channel found yet. TEXT-MODE?")}a||s||r.logger.log(2,(function(){return"Couldn't parse cleaned data "+Ro([i,n])+" orig: "+Ro([t[e],t[e+1]])}))},n=0;n=32&&t<=47||(23===e||31===e)&&t>=33&&t<=35))return!1;var r=20===e||21===e||23===e?1:2,i=this.channels[r];return 20===e||21===e||28===e||29===e?32===t?i.ccRCL():33===t?i.ccBS():34===t?i.ccAOF():35===t?i.ccAON():36===t?i.ccDER():37===t?i.ccRU(2):38===t?i.ccRU(3):39===t?i.ccRU(4):40===t?i.ccFON():41===t?i.ccRDC():42===t?i.ccTR():43===t?i.ccRTD():44===t?i.ccEDM():45===t?i.ccCR():46===t?i.ccENM():47===t&&i.ccEOC():i.ccTO(t-32),this.currentChannel=r,!0},t.parseMidrow=function(e,t){var r=0;if((17===e||25===e)&&t>=32&&t<=47){if((r=17===e?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(t),this.logger.log(3,(function(){return"MIDROW ("+Ro([e,t])+")"})),!0)}return!1},t.parsePAC=function(e,t){var r;if(!((e>=17&&e<=23||e>=25&&e<=31)&&t>=64&&t<=127||(16===e||24===e)&&t>=64&&t<=95))return!1;var i=e<=23?1:2;r=t>=64&&t<=95?1===i?Eo[e]:So[e]:1===i?To[e]:Ao[e];var n=this.channels[i];return!!n&&(n.setPAC(this.interpretPAC(r,t)),this.currentChannel=i,!0)},t.interpretPAC=function(e,t){var r,i={color:null,italics:!1,indent:null,underline:!1,row:e};return r=t>95?t-96:t-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},t.parseChars=function(e,t){var r,i,n=null,a=null;return e>=25?(r=2,a=e-8):(r=1,a=e),a>=17&&a<=19?(i=17===a?t+80:18===a?t+112:t+144,this.logger.log(2,(function(){return"Special char '"+mo(i)+"' in channel "+r})),n=[i]):e>=32&&e<=127&&(n=0===t?[e]:[e,t]),n&&this.logger.log(3,(function(){return"Char codes =  "+Ro(n).join(",")})),n},t.parseBackgroundAttributes=function(e,t){var r;if(!((16===e||24===e)&&t>=32&&t<=47||(23===e||31===e)&&t>=45&&t<=47))return!1;var i={};16===e||24===e?(r=Math.floor((t-32)/2),i.background=Lo[r],t%2==1&&(i.background=i.background+"_semi")):45===t?i.background="transparent":(i.foreground="black",47===t&&(i.underline=!0));var n=e<=23?1:2;return this.channels[n].setBkgData(i),!0},t.reset=function(){for(var e=0;e1?t-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(e){var t=i(e);if(!t)throw new SyntaxError("An invalid or illegal string was specified.");T=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(e){if(e<0||e>100)throw new Error("Size must be between 0 and 100.");S=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return A},set:function(e){var t=i(e);if(!t)throw new SyntaxError("An invalid or illegal string was specified.");A=t,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),xo=function(){function e(){}return e.prototype.decode=function(e,t){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))},e}();function Mo(e){function t(e,t,r,i){return 3600*(0|e)+60*(0|t)+(0|r)+parseFloat(i||0)}var r=e.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?t(r[2],r[3],0,r[4]):t(r[1],r[2],r[3],r[4]):null}var Fo=function(){function e(){this.values=Object.create(null)}var t=e.prototype;return t.set=function(e,t){this.get(e)||""===t||(this.values[e]=t)},t.get=function(e,t,r){return r?this.has(e)?this.values[e]:t[r]:this.has(e)?this.values[e]:t},t.has=function(e){return e in this.values},t.alt=function(e,t,r){for(var i=0;i=0&&r<=100)return this.set(e,r),!0}return!1},e}();function No(e,t,r,i){var n=i?e.split(i):[e];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&t(s[0],s[1])}}var Uo=new Oo(0,0,""),Bo="middle"===Uo.align?"middle":"center";function Go(e,t,r){var i=e;function n(){var t=Mo(e);if(null===t)throw new Error("Malformed timestamp: "+i);return e=e.replace(/^[^\sa-zA-Z-]+/,""),t}function a(){e=e.replace(/^\s+/,"")}if(a(),t.startTime=n(),a(),"--\x3e"!==e.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);e=e.slice(3),a(),t.endTime=n(),a(),function(e,t){var i=new Fo;No(e,(function(e,t){var n;switch(e){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===t){i.set(e,r[a].region);break}break;case"vertical":i.alt(e,t,["rl","lr"]);break;case"line":n=t.split(","),i.integer(e,n[0]),i.percent(e,n[0])&&i.set("snapToLines",!1),i.alt(e,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",Bo,"end"]);break;case"position":n=t.split(","),i.percent(e,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",Bo,"end","line-left","line-right","auto"]);break;case"size":i.percent(e,t);break;case"align":i.alt(e,t,["start",Bo,"end","left","right"])}}),/:/,/\s/),t.region=i.get("region",null),t.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===Uo.line&&(n=-1),t.line=n,t.lineAlign=i.get("lineAlign","start"),t.snapToLines=i.get("snapToLines",!0),t.size=i.get("size",100),t.align=i.get("align",Bo);var a=i.get("position","auto");"auto"===a&&50===Uo.position&&(a="start"===t.align||"left"===t.align?0:"end"===t.align||"right"===t.align?100:50),t.position=a}(e,t)}function Ko(e){return e.replace(//gi,"\n")}var Vo=function(){function e(){this.state="INITIAL",this.buffer="",this.decoder=new xo,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var t=e.prototype;return t.parse=function(e){var t=this;function r(){var e=t.buffer,r=0;for(e=Ko(e);r0&&f.push(e)},d.onparsingerror=function(e){u=e},d.onflush=function(){u?s(u):a(f)},h.forEach((function(e){if(p){if(Yo(e,"X-TIMESTAMP-MAP=")){p=!1,e.slice(16).split(",").forEach((function(e){Yo(e,"LOCAL:")?g=e.slice(6):Yo(e,"MPEGTS:")&&(v=parseInt(e.slice(7)))}));try{m=function(e){var t=parseInt(e.slice(-3)),r=parseInt(e.slice(-6,-4)),i=parseInt(e.slice(-9,-7)),n=e.length>9?parseInt(e.substring(0,e.indexOf(":"))):0;if(!(A(t)&&A(r)&&A(i)&&A(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+e);return t+=1e3*r,(t+=6e4*i)+36e5*n}(g)/1e3}catch(e){u=e}return}""===e&&(p=!1)}d.parse(e+"\n")})),d.flush()}var qo="stpp.ttml.im1t",Xo=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Qo=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,zo={left:"start",center:"center",right:"end",start:"start",end:"end"};function $o(e,t,r,i){var n=ce(new Uint8Array(e),["mdat"]);if(0!==n.length){var s,o,l,u,d=n.map((function(e){return q(e)})),h=(s=t.baseTime,o=1,void 0===(l=t.timescale)&&(l=1),void 0===u&&(u=!1),Vn(s,o,1/l,u));try{d.forEach((function(e){return r(function(e,t){var r=(new DOMParser).parseFromString(e,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},s=Object.keys(n).reduce((function(e,t){return e[t]=i.getAttribute("ttp:"+t)||n[t],e}),{}),o="preserve"!==i.getAttribute("xml:space"),l=Jo(Zo(i,"styling","style")),u=Jo(Zo(i,"layout","region")),d=Zo(i,"body","[begin]");return[].map.call(d,(function(e){var r=el(e,o);if(!r||!e.hasAttribute("begin"))return null;var i=il(e.getAttribute("begin"),s),n=il(e.getAttribute("dur"),s),d=il(e.getAttribute("end"),s);if(null===i)throw rl(e);if(null===d){if(null===n)throw rl(e);d=i+n}var h=new Oo(i-t,d-t,r);h.id=Wo(h.startTime,h.endTime,h.text);var f=function(e,t,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=e&&e.hasAttribute("style")?e.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=tl(t,i,a)||tl(e,i,a)||tl(n,i,a);return s&&(r[a]=s),r}),{})}(u[e.getAttribute("region")],l[e.getAttribute("style")],l),c=f.textAlign;if(c){var g=zo[c];g&&(h.lineAlign=g),h.align=c}return a(h,f),h})).filter((function(e){return null!==e}))}(e,h))}))}catch(e){i(e)}}else i(new Error("Could not parse IMSC1 mdat"))}function Zo(e,t,r){var i=e.getElementsByTagName(t)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function Jo(e){return e.reduce((function(e,t){var r=t.getAttribute("xml:id");return r&&(e[r]=t),e}),{})}function el(e,t){return[].slice.call(e.childNodes).reduce((function(e,r,i){var n;return"br"===r.nodeName&&i?e+"\n":null!=(n=r.childNodes)&&n.length?el(r,t):t?e+r.textContent.trim().replace(/\s+/g," "):e+r.textContent}),"")}function tl(e,t,r){return e&&e.hasAttributeNS(t,r)?e.getAttributeNS(t,r):null}function rl(e){return new Error("Could not parse ttml timestamp "+e)}function il(e,t){if(!e)return null;var r=Mo(e);return null===r&&(Xo.test(e)?r=function(e,t){var r=Xo.exec(e),i=(0|r[4])+(0|r[5])/t.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/t.frameRate}(e,t):Qo.test(e)&&(r=function(e,t){var r=Qo.exec(e),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/t.frameRate;case"t":return i/t.tickRate}return i}(e,t))),r}var nl=function(){function e(e,t){this.timelineController=void 0,this.cueRanges=[],this.trackName=void 0,this.startTime=null,this.endTime=null,this.screen=null,this.timelineController=e,this.trackName=t}var t=e.prototype;return t.dispatchCue=function(){null!==this.startTime&&(this.timelineController.addCues(this.trackName,this.startTime,this.endTime,this.screen,this.cueRanges),this.startTime=null)},t.newCue=function(e,t,r){(null===this.startTime||this.startTime>e)&&(this.startTime=e),this.endTime=t,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},t.reset=function(){this.cueRanges=[],this.startTime=null},e}(),al=function(){function e(e){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=e,this.config=e.config,this.Cues=e.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},e.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.on(b.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),e.on(b.FRAG_LOADING,this.onFragLoading,this),e.on(b.FRAG_LOADED,this.onFragLoaded,this),e.on(b.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),e.on(b.FRAG_DECRYPTED,this.onFragDecrypted,this),e.on(b.INIT_PTS_FOUND,this.onInitPtsFound,this),e.on(b.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),e.on(b.BUFFER_FLUSHING,this.onBufferFlushing,this)}var t=e.prototype;return t.destroy=function(){var e=this.hls;e.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.off(b.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),e.off(b.FRAG_LOADING,this.onFragLoading,this),e.off(b.FRAG_LOADED,this.onFragLoaded,this),e.off(b.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),e.off(b.FRAG_DECRYPTED,this.onFragDecrypted,this),e.off(b.INIT_PTS_FOUND,this.onInitPtsFound,this),e.off(b.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),e.off(b.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=this.media=null,this.cea608Parser1=this.cea608Parser2=void 0},t.initCea608Parsers=function(){var e=new nl(this,"textTrack1"),t=new nl(this,"textTrack2"),r=new nl(this,"textTrack3"),i=new nl(this,"textTrack4");this.cea608Parser1=new Co(1,e,t),this.cea608Parser2=new Co(3,r,i)},t.addCues=function(e,t,r,i,n){for(var a,s,o,l,u=!1,d=n.length;d--;){var h=n[d],f=(a=h[0],s=h[1],o=t,l=r,Math.min(s,l)-Math.max(a,o));if(f>=0&&(h[0]=Math.min(h[0],t),h[1]=Math.max(h[1],r),u=!0,f/(r-t)>.5))return}if(u||n.push([t,r]),this.config.renderTextTracksNatively){var c=this.captionsTracks[e];this.Cues.newCue(c,t,r,i)}else{var g=this.Cues.newCue(null,t,r,i);this.hls.trigger(b.CUES_PARSED,{type:"captions",cues:g,track:e})}},t.onInitPtsFound=function(e,t){var r=this,i=t.frag,n=t.id,a=t.initPTS,s=t.timescale,o=t.trackId,l=this.unparsedVttFrags;n===w&&(this.initPTS[i.cc]={baseTime:a,timescale:s,trackId:o}),l.length&&(this.unparsedVttFrags=[],l.forEach((function(e){r.initPTS[e.frag.cc]?r.onFragLoaded(b.FRAG_LOADED,e):r.hls.trigger(b.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:e.frag,error:new Error("Subtitle discontinuity domain does not match main")})})))},t.getExistingTrack=function(e,t){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(b.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:t})}))}else s.push(e)},t._fallbackToIMSC1=function(e,t){var r=this,i=this.tracks[e.level];i.textCodec||$o(t,this.initPTS[e.cc],(function(){i.textCodec=qo,r._parseIMSC1(e,t)}),(function(){i.textCodec="wvtt"}))},t._appendCues=function(e,t){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[t];if(!i||"disabled"===i.mode)return;e.forEach((function(e){return uo(i,e)}))}else{var n=this.tracks[t];if(!n)return;var a=n.default?"default":"subtitles"+t;r.trigger(b.CUES_PARSED,{type:"subtitles",cues:e,track:a})}},t.onFragDecrypted=function(e,t){t.frag.type===x&&this.onFragLoaded(b.FRAG_LOADED,t)},t.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},t.onFragParsingUserdata=function(e,t){if(this.enabled&&this.config.enableCEA708Captions){var r=t.frag,i=t.samples;if(r.type!==w||"NONE"!==this.closedCaptionsForLevel(r))for(var n=0;n=16?o--:o++;var g=Ko(l.trim()),v=Wo(t,r,g);null!=e&&null!=(f=e.cues)&&f.getCueById(v)||((a=new d(t,r,g)).id=v,a.line=h+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return e&&u.length&&(u.sort((function(e,t){return"auto"===e.line||"auto"===t.line?0:e.line>8&&t.line>8?t.line-e.line:e.line-t.line})),u.forEach((function(t){return uo(e,t)}))),u}},dl=/(\d+)-(\d+)\/(\d+)/,hl=function(){function e(e){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=e.fetchSetup||fl,this.controller=new self.AbortController,this.stats=new z}var t=e.prototype;return t.destroy=function(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null},t.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},t.abort=function(){var e;this.abortInternal(),null!=(e=this.callbacks)&&e.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},t.load=function(e,t,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var s=function(e,t){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:t,headers:new self.Headers(a({},e.headers))};return e.rangeEnd&&r.headers.set("Range","bytes="+e.rangeStart+"-"+String(e.rangeEnd-1)),r}(e,this.controller.signal),o="arraybuffer"===e.responseType,l=o?"byteLength":"length",u=t.loadPolicy,d=u.maxTimeToFirstByteMs,h=u.maxLoadTimeMs;this.context=e,this.config=t,this.callbacks=r,this.request=this.fetchSetup(e,s),self.clearTimeout(this.requestTimeout),t.timeout=d&&A(d)?d:h,this.requestTimeout=self.setTimeout((function(){i.callbacks&&(i.abortInternal(),i.callbacks.onTimeout(n,e,i.response))}),t.timeout),(aa(this.request)?this.request.then(self.fetch):self.fetch(this.request)).then((function(r){var a;i.response=i.loader=r;var s=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),t.timeout=h,i.requestTimeout=self.setTimeout((function(){i.callbacks&&(i.abortInternal(),i.callbacks.onTimeout(n,e,i.response))}),h-(s-n.loading.start)),!r.ok){var l=r.status,u=r.statusText;throw new cl(u||"fetch, bad network response",l,r)}n.loading.first=s,n.total=function(e){var t=e.get("Content-Range");if(t){var r=function(e){var t=dl.exec(e);if(t)return parseInt(t[2])-parseInt(t[1])+1}(t);if(A(r))return r}var i=e.get("Content-Length");if(i)return parseInt(i)}(r.headers)||n.total;var d=null==(a=i.callbacks)?void 0:a.onProgress;return d&&A(t.highWaterMark)?i.loadProgressively(r,n,e,t.highWaterMark,d):o?r.arrayBuffer():"json"===e.responseType?r.json():r.text()})).then((function(r){var a,s,o=i.response;if(!o)throw new Error("loader destroyed");self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var u=r[l];u&&(n.loaded=n.total=u);var d={url:o.url,data:r,code:o.status},h=null==(a=i.callbacks)?void 0:a.onProgress;h&&!A(t.highWaterMark)&&h(n,e,r,o),null==(s=i.callbacks)||s.onSuccess(d,n,e,o)})).catch((function(t){var r;if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=t&&t.code||0,s=t?t.message:null;null==(r=i.callbacks)||r.onError({code:a,text:s},e,t?t.details:null,n)}}))},t.getCacheAge=function(){var e=null;if(this.response){var t=this.response.headers.get("age");e=t?parseFloat(t):null}return e},t.getResponseHeader=function(e){return this.response?this.response.headers.get(e):null},t.loadProgressively=function(e,t,r,i,n){void 0===i&&(i=0);var a=new wi,s=e.body.getReader(),o=function(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(t,r,a.flush().buffer,e),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return t.loaded+=u,u=i&&n(t,r,a.flush().buffer,e)):n(t,r,l.buffer,e),o()})).catch((function(){return Promise.reject()}))};return o()},e}();function fl(e,t){return new self.Request(e.url,t)}var cl=function(e){function t(t,r,i){var n;return(n=e.call(this,t)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return o(t,e),t}(c(Error)),gl=/^age:\s*[\d.]+\s*$/im,vl=function(){function e(e){this.xhrSetup=void 0,this.requestTimeout=void 0,this.retryTimeout=void 0,this.retryDelay=void 0,this.config=null,this.callbacks=null,this.context=null,this.loader=null,this.stats=void 0,this.xhrSetup=e&&e.xhrSetup||null,this.stats=new z,this.retryDelay=0}var t=e.prototype;return t.destroy=function(){this.callbacks=null,this.abortInternal(),this.loader=null,this.config=null,this.context=null,this.xhrSetup=null},t.abortInternal=function(){var e=this.loader;self.clearTimeout(this.requestTimeout),self.clearTimeout(this.retryTimeout),e&&(e.onreadystatechange=null,e.onprogress=null,4!==e.readyState&&(this.stats.aborted=!0,e.abort()))},t.abort=function(){var e;this.abortInternal(),null!=(e=this.callbacks)&&e.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.loader)},t.load=function(e,t,r){if(this.stats.loading.start)throw new Error("Loader can only be used once.");this.stats.loading.start=self.performance.now(),this.context=e,this.config=t,this.callbacks=r,this.loadInternal()},t.loadInternal=function(){var e=this,t=this.config,r=this.context;if(t&&r){var i=this.loader=new self.XMLHttpRequest,n=this.stats;n.loading.first=0,n.loaded=0,n.aborted=!1;var a=this.xhrSetup;a?Promise.resolve().then((function(){if(e.loader===i&&!e.stats.aborted)return a(i,r.url)})).catch((function(t){if(e.loader===i&&!e.stats.aborted)return i.open("GET",r.url,!0),a(i,r.url)})).then((function(){e.loader!==i||e.stats.aborted||e.openAndSendXhr(i,r,t)})).catch((function(t){var a;null==(a=e.callbacks)||a.onError({code:i.status,text:t.message},r,i,n)})):this.openAndSendXhr(i,r,t)}},t.openAndSendXhr=function(e,t,r){e.readyState||e.open("GET",t.url,!0);var i=t.headers,n=r.loadPolicy,a=n.maxTimeToFirstByteMs,s=n.maxLoadTimeMs;if(i)for(var o in i)e.setRequestHeader(o,i[o]);t.rangeEnd&&e.setRequestHeader("Range","bytes="+t.rangeStart+"-"+(t.rangeEnd-1)),e.onreadystatechange=this.readystatechange.bind(this),e.onprogress=this.loadprogress.bind(this),e.responseType=t.responseType,self.clearTimeout(this.requestTimeout),r.timeout=a&&A(a)?a:s,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),r.timeout),e.send()},t.readystatechange=function(){var e=this.context,t=this.loader,r=this.stats;if(e&&t){var i=t.readyState,n=this.config;if(!r.aborted&&i>=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),t.onreadystatechange=null,t.onprogress=null;var a=t.status,s="text"===t.responseType?t.responseText:null;if(a>=200&&a<300){var o=null!=s?s:t.response;if(null!=o){var l,u;r.loading.end=Math.max(self.performance.now(),r.loading.first);var d="arraybuffer"===t.responseType?o.byteLength:o.length;r.loaded=r.total=d,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first);var h=null==(l=this.callbacks)?void 0:l.onProgress;h&&h(r,e,o,t);var f={url:t.responseURL,data:o,code:a};return void(null==(u=this.callbacks)||u.onSuccess(f,r,e,t))}}var c,g=n.loadPolicy.errorRetry;Pt(g,r.retry,!1,{url:e.url,data:void 0,code:a})?this.retry(g):(Y.error(a+" while loading "+e.url),null==(c=this.callbacks)||c.onError({code:a,text:t.statusText},e,t,r))}}},t.loadtimeout=function(){if(this.config){var e=this.config.loadPolicy.timeoutRetry;if(Pt(e,this.stats.retry,!0))this.retry(e);else{var t;Y.warn("timeout while loading "+(null==(t=this.context)?void 0:t.url));var r=this.callbacks;r&&(this.abortInternal(),r.onTimeout(this.stats,this.context,this.loader))}}},t.retry=function(e){var t=this.context,r=this.stats;this.retryDelay=Dt(e,r.retry),r.retry++,Y.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==t?void 0:t.url)+", retrying "+r.retry+"/"+e.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},t.loadprogress=function(e){var t=this.stats;t.loaded=e.loaded,e.lengthComputable&&(t.total=e.total)},t.getCacheAge=function(){var e=null;if(this.loader&&gl.test(this.loader.getAllResponseHeaders())){var t=this.loader.getResponseHeader("age");e=t?parseFloat(t):null}return e},t.getResponseHeader=function(e){return this.loader&&new RegExp("^"+e+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(e):null},e}(),ml=d(d({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,maxDevicePixelRatio:Number.POSITIVE_INFINITY,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,startOnSegmentBoundary:!1,maxBufferSize:6e7,maxFragLookUpTolerance:.25,maxBufferHole:.1,detectStallWithCurrentTimeMs:1250,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,nudgeOnVideoHole:!0,liveSyncMode:"edge",liveSyncDurationCount:3,liveSyncOnStallIncrease:1,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,ignorePlaylistParsingErrors:!1,loader:vl,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:yt,bufferController:ba,capLevelController:Pa,errorController:Gt,fpsController:Hs,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:Gr,requireKeySystemAccessOnStart:!1,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableEmsgKLVMetadata:!1,enableID3MetadataCues:!0,enableInterstitialPlayback:!0,interstitialAppendInPlace:!0,interstitialLiveLookAhead:10,useMediaCapabilities:!0,preserveManualLevelOnError:!1,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},interstitialAssetListLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:3e4,timeoutRetry:{maxNumRetry:0,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:0,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:ul,enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:so,subtitleTrackController:go,timelineController:al,audioStreamController:pa,audioTrackController:Aa,emeController:Us,cmcdController:xs,contentSteeringController:Ms,interstitialsController:ao});function pl(e){return e&&"object"==typeof e?Array.isArray(e)?e.map(pl):Object.keys(e).reduce((function(t,r){return t[r]=pl(e[r]),t}),{}):e}function yl(e,t){var r=e.loader;r!==hl&&r!==vl?(t.log("[config]: Custom loader detected, cannot enable progressive streaming"),e.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(e){}return!1}()&&(e.loader=hl,e.progressive=!0,e.enableSoftwareAES=!0,t.log("[config]: Progressive streaming enabled, using FetchLoader"))}var El=function(e){function t(t,r){var i;return(i=e.call(this,"gap-controller",t.logger)||this).hls=void 0,i.fragmentTracker=void 0,i.media=null,i.mediaSource=void 0,i.nudgeRetry=0,i.stallReported=!1,i.stalled=null,i.moved=!1,i.seeking=!1,i.buffered={},i.lastCurrentTime=0,i.ended=0,i.waiting=0,i.onMediaPlaying=function(){i.ended=0,i.waiting=0},i.onMediaWaiting=function(){var e;null!=(e=i.media)&&e.seeking||(i.waiting=self.performance.now(),i.tick())},i.onMediaEnded=function(){var e;i.hls&&(i.ended=(null==(e=i.media)?void 0:e.currentTime)||1,i.hls.trigger(b.MEDIA_ENDED,{stalled:!1}))},i.hls=t,i.fragmentTracker=r,i.registerListeners(),i}o(t,e);var r=t.prototype;return r.registerListeners=function(){var e=this.hls;e&&(e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.BUFFER_APPENDED,this.onBufferAppended,this))},r.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.BUFFER_APPENDED,this.onBufferAppended,this))},r.destroy=function(){e.prototype.destroy.call(this),this.unregisterListeners(),this.media=this.hls=this.fragmentTracker=null,this.mediaSource=void 0},r.onMediaAttached=function(e,t){this.setInterval(100),this.mediaSource=t.mediaSource;var r=this.media=t.media;ki(r,"playing",this.onMediaPlaying),ki(r,"waiting",this.onMediaWaiting),ki(r,"ended",this.onMediaEnded)},r.onMediaDetaching=function(e,t){this.clearInterval();var r=this.media;r&&(bi(r,"playing",this.onMediaPlaying),bi(r,"waiting",this.onMediaWaiting),bi(r,"ended",this.onMediaEnded),this.media=null),this.mediaSource=void 0},r.onBufferAppended=function(e,t){this.buffered=t.timeRanges},r.tick=function(){var e;if(null!=(e=this.media)&&e.readyState&&this.hasBuffered){var t=this.media.currentTime;this.poll(t,this.lastCurrentTime),this.lastCurrentTime=t}},r.poll=function(e,t){var r,i,n=null==(r=this.hls)?void 0:r.config;if(n){var a=this.media;if(a){var s=a.seeking,o=this.seeking&&!s,l=!this.seeking&&s,u=a.paused&&!s||a.ended||0===a.playbackRate;if(this.seeking=s,e!==t)return t&&(this.ended=0),this.moved=!0,s||(this.nudgeRetry=0,n.nudgeOnVideoHole&&!u&&e>t&&this.nudgeOnVideoHole(e,t)),void(0===this.waiting&&this.stallResolved(e));if(l||o)o&&this.stallResolved(e);else{if(u)return this.nudgeRetry=0,this.stallResolved(e),void(!this.ended&&a.ended&&this.hls&&(this.ended=e||1,this.hls.trigger(b.MEDIA_ENDED,{stalled:!1})));if(dr.getBuffered(a).length){var d=dr.bufferInfo(a,e,0),h=d.nextStart||0,f=this.fragmentTracker;if(s&&f&&this.hls){var c=Tl(this.hls.inFlightFragments,e),g=d.len>2,v=!h||c||h-e>2&&!f.getPartialFragment(e);if(g||v)return;this.moved=!1}var m=null==(i=this.hls)?void 0:i.latestLevelDetails;if(!this.moved&&null!==this.stalled&&f){if(!(d.len>0||h))return;var p=Math.max(h,d.start||0)-e,y=null!=m&&m.live?2*m.targetduration:2,E=Al(e,f);if(p>0&&(p<=y||E))return void(a.paused||this._trySkipBufferHole(E))}var T=n.detectStallWithCurrentTimeMs,S=self.performance.now(),A=this.waiting,L=this.stalled;if(null===L){if(!(A>0&&S-A=T||A)&&this.hls){var R;if("ended"===(null==(R=this.mediaSource)?void 0:R.readyState)&&(null==m||!m.live)&&Math.abs(e-((null==m?void 0:m.edge)||0))<1){if(this.ended)return;return this.ended=e||1,void this.hls.trigger(b.MEDIA_ENDED,{stalled:!0})}if(this._reportStall(d),!this.media||!this.hls)return}var k=dr.bufferInfo(a,e,n.maxBufferHole);this._tryFixBufferStall(k,I,e)}else this.nudgeRetry=0}}}},r.stallResolved=function(e){var t=this.stalled;if(t&&this.hls&&(this.stalled=null,this.stallReported)){var r=self.performance.now()-t;this.log("playback not stuck anymore @"+e+", after "+Math.round(r)+"ms"),this.stallReported=!1,this.waiting=0,this.hls.trigger(b.STALL_RESOLVED,{})}},r.nudgeOnVideoHole=function(e,t){var r,i=this.buffered.video;if(this.hls&&this.media&&this.fragmentTracker&&null!=(r=this.buffered.audio)&&r.length&&i&&i.length>1&&e>i.end(0)){var n=dr.bufferedInfo(dr.timeRangesToArray(this.buffered.audio),e,0);if(n.len>1&&t>=n.start){var a=dr.timeRangesToArray(i),s=dr.bufferedInfo(a,t,0).bufferedIndex;if(s>-1&&ss)&&u-l<1&&e-l<2){var d=new Error("nudging playhead to flush pipeline after video hole. currentTime: "+e+" hole: "+l+" -> "+u+" buffered index: "+o);this.warn(d.message),this.media.currentTime+=1e-6;var h=Al(e,this.fragmentTracker);h&&"fragment"in h?h=h.fragment:h||(h=void 0);var f=dr.bufferInfo(this.media,e,0);this.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.BUFFER_SEEK_OVER_HOLE,fatal:!1,error:d,reason:d.message,frag:h,buffer:f.len,bufferInfo:f})}}}}},r._tryFixBufferStall=function(e,t,r){var i,n,a=this.fragmentTracker,s=this.media,o=null==(i=this.hls)?void 0:i.config;if(s&&a&&o){var l=null==(n=this.hls)?void 0:n.latestLevelDetails,u=Al(r,a);if((u||null!=l&&l.live&&r1&&e.len>o.maxBufferHole||e.nextStart&&(e.nextStart-r1e3*o.highBufferWatchdogPeriod||this.waiting)&&(this.warn("Trying to nudge playhead over buffer-hole"),this._tryNudgeBuffer(e))}},r.adjacentTraversal=function(e,t){var r=this.fragmentTracker,i=e.nextStart;if(r&&i){var n=r.getFragAtPos(t,w),a=r.getFragAtPos(i,w);if(n&&a)return a.sn-n.sn<2}return!1},r._reportStall=function(e){var t=this.hls,r=this.media,i=this.stallReported,n=this.stalled;if(!i&&null!==n&&r&&t){this.stallReported=!0;var a=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+ut(e)+")");this.warn(a.message),t.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.BUFFER_STALLED_ERROR,fatal:!1,error:a,buffer:e.len,bufferInfo:e,stalled:{start:n}})}},r._trySkipBufferHole=function(e){var t,r=this.fragmentTracker,i=this.media,n=null==(t=this.hls)?void 0:t.config;if(!i||!r||!n)return 0;var a=i.currentTime,s=dr.bufferInfo(i,a,0),o=a0&&s.len<1&&i.readyState<3,d=o-a;if(d>0&&(l||u)){if(d>n.maxBufferHole){var h=!1;if(0===a){var f=r.getAppendedFrag(0,w);f&&o0}}])}(or);function Tl(e,t){var r=Sl(e.main);if(r&&r.start<=t)return r;var i=Sl(e.audio);return i&&i.start<=t?i:null}function Sl(e){if(!e)return null;switch(e.state){case _i.IDLE:case _i.STOPPED:case _i.ENDED:case _i.ERROR:return null}return e.frag}function Al(e,t){return t.getAppendedFrag(e,w)||t.getPartialFragment(e)}function Ll(){if("undefined"!=typeof self)return self.VTTCue||self.TextTrackCue}function Il(e,t,r,i,n){var a=new e(t,r,"");try{a.value=i,n&&(a.type=n)}catch(s){a=new e(t,r,ut(n?d({type:n},i):i))}return a}var Rl=function(){var e=Ll();try{e&&new e(0,Number.POSITIVE_INFINITY,"")}catch(e){return Number.MAX_VALUE}return Number.POSITIVE_INFINITY}(),kl=function(){function e(e){var t=this;this.hls=void 0,this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.removeCues=!0,this.assetCue=void 0,this.onEventCueEnter=function(){t.hls&&t.hls.trigger(b.EVENT_CUE_ENTER,{})},this.hls=e,this._registerListeners()}var t=e.prototype;return t.destroy=function(){this._unregisterListeners(),this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.hls=this.onEventCueEnter=null},t._registerListeners=function(){var e=this.hls;e&&(e.on(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),e.on(b.BUFFER_FLUSHING,this.onBufferFlushing,this),e.on(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.on(b.LEVEL_PTS_UPDATED,this.onLevelPtsUpdated,this))},t._unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MEDIA_ATTACHING,this.onMediaAttaching,this),e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),e.off(b.BUFFER_FLUSHING,this.onBufferFlushing,this),e.off(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.off(b.LEVEL_PTS_UPDATED,this.onLevelPtsUpdated,this))},t.onMediaAttaching=function(e,t){var r;this.media=t.media,!1===(null==(r=t.overrides)?void 0:r.cueRemoval)&&(this.removeCues=!1)},t.onMediaAttached=function(){var e,t=null==(e=this.hls)?void 0:e.latestLevelDetails;t&&this.updateDateRangeCues(t)},t.onMediaDetaching=function(e,t){this.media=null,t.transferMedia||(this.id3Track&&(this.removeCues&&ho(this.id3Track,this.onEventCueEnter),this.id3Track=null),this.dateRangeCuesAppended={})},t.onManifestLoading=function(){this.dateRangeCuesAppended={}},t.createTrack=function(e){var t=this.getID3Track(e.textTracks);return t.mode="hidden",t},t.getID3Track=function(e){if(this.media){for(var t=0;tRl&&(h=Rl),h-d<=0&&(h=d+.25);for(var f=0;f.01&&this.updateDateRangeCues(t.details)},t.updateDateRangeCues=function(e,t){var r=this;if(this.hls&&this.media){var i=this.hls.config,n=i.assetPlayerId,a=i.timelineOffset,s=i.enableDateRangeMetadataCues,o=i.interstitialsController;if(s){var l=Ll();if(n&&a&&!o){var u=e.fragmentStart,d=e.fragmentEnd,h=this.assetCue;h?(h.startTime=u,h.endTime=d):l&&(h=this.assetCue=Il(l,u,d,{assetPlayerId:this.hls.config.assetPlayerId},"hlsjs.interstitial.asset"))&&(h.id=n,this.id3Track||(this.id3Track=this.createTrack(this.media)),this.id3Track.addCue(h),h.addEventListener("enter",this.onEventCueEnter))}if(e.hasProgramDateTime){var f,c=this.id3Track,g=e.dateRanges,v=Object.keys(g),m=this.dateRangeCuesAppended;if(c&&t)if(null!=(f=c.cues)&&f.length)for(var p=Object.keys(m).filter((function(e){return!v.includes(e)})),y=function(){var e,t=p[E],i=null==(e=m[t])?void 0:e.cues;delete m[t],i&&Object.keys(i).forEach((function(e){var t=i[e];if(t){t.removeEventListener("enter",r.onEventCueEnter);try{c.removeCue(t)}catch(e){}}}))},E=p.length;E--;)y();else m=this.dateRangeCuesAppended={};var T=e.fragments[e.fragments.length-1];if(0!==v.length&&A(null==T?void 0:T.programDateTime)){this.id3Track||(this.id3Track=this.createTrack(this.media));for(var S=function(){var e=v[L],t=g[e],i=t.startTime,n=m[e],a=(null==n?void 0:n.cues)||{},s=(null==n?void 0:n.durationKnown)||!1,u=Rl,d=t.duration;if(t.endDate&&null!==d)u=i+d,s=!0;else if(t.endOnNext&&!s){var h=v.reduce((function(e,r){if(r!==t.id){var i=g[r];if(i.class===t.class&&i.startDate>t.startDate&&(!e||t.startDate.01&&(E.startTime=i,E.endTime=u):E.endTime=u;else if(l){var T=t.attr[y];Er(y)&&(T=Q(T));var S=Il(l,i,u,{key:y,data:T},rn.dateRange);S&&(S.id=e,r.id3Track.addCue(S),a[y]=S,o&&("X-ASSET-LIST"!==y&&"X-ASSET-URL"!==y||S.addEventListener("enter",r.onEventCueEnter)))}}}m[e]={cues:a,dateRange:t,durationKnown:s}},L=0;L.05&&t.forwardBufferLength>1){var u=Math.min(2,Math.max(1,s)),d=Math.round(2/(1+Math.exp(-.75*l-t.edgeStalled))*20)/20,h=Math.min(u,Math.max(1,d));t.changeMediaPlaybackRate(e,h)}else 1!==e.playbackRate&&0!==e.playbackRate&&t.changeMediaPlaybackRate(e,1)}}}}},this.hls=e,this.config=e.config,this.registerListeners()}var t=e.prototype;return t.destroy=function(){this.unregisterListeners(),this.onMediaDetaching(),this.hls=null},t.registerListeners=function(){var e=this.hls;e&&(e.on(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.on(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.on(b.ERROR,this.onError,this))},t.unregisterListeners=function(){var e=this.hls;e&&(e.off(b.MEDIA_ATTACHED,this.onMediaAttached,this),e.off(b.MEDIA_DETACHING,this.onMediaDetaching,this),e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.LEVEL_UPDATED,this.onLevelUpdated,this),e.off(b.ERROR,this.onError,this))},t.onMediaAttached=function(e,t){this.media=t.media,this.media.addEventListener("timeupdate",this.onTimeupdate)},t.onMediaDetaching=function(){this.media&&(this.media.removeEventListener("timeupdate",this.onTimeupdate),this.media=null)},t.onManifestLoading=function(){this._latency=null,this.stallCount=0},t.onLevelUpdated=function(e,t){var r=t.details;r.advanced&&this.onTimeupdate(),!r.live&&this.media&&this.media.removeEventListener("timeupdate",this.onTimeupdate)},t.onError=function(e,t){var r;t.details===k.BUFFER_STALLED_ERROR&&(this.stallCount++,this.hls&&null!=(r=this.levelDetails)&&r.live&&this.hls.logger.warn("[latency-controller]: Stall detected, adjusting target latency"))},t.changeMediaPlaybackRate=function(e,t){var r,i;e.playbackRate!==t&&(null==(r=this.hls)||r.logger.debug("[latency-controller]: latency="+this.latency.toFixed(3)+", targetLatency="+(null==(i=this.targetLatency)?void 0:i.toFixed(3))+", forwardBufferLength="+this.forwardBufferLength.toFixed(3)+": adjusting playback rate from "+e.playbackRate+" to "+t),e.playbackRate=t)},t.estimateLiveEdge=function(){var e=this.levelDetails;return null===e?null:e.edge+e.age},t.computeLatency=function(){var e=this.estimateLiveEdge();return null===e?null:e-this.currentTime},i(e,[{key:"levelDetails",get:function(){var e;return(null==(e=this.hls)?void 0:e.latestLevelDetails)||null}},{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var e=this.config;if(void 0!==e.liveMaxLatencyDuration)return e.liveMaxLatencyDuration;var t=this.levelDetails;return t?e.liveMaxLatencyDurationCount*t.targetduration:0}},{key:"targetLatency",get:function(){var e=this.levelDetails;if(null===e||null===this.hls)return null;var t=e.holdBack,r=e.partHoldBack,i=e.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||t;(this._targetLatencyUpdated||l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var d=i;return u+Math.min(this.stallCount*this.config.liveSyncOnStallIncrease,d)},set:function(e){this.stallCount=0,this.config.liveSyncDuration=e,this._targetLatencyUpdated=!0}},{key:"liveSyncPosition",get:function(){var e=this.estimateLiveEdge(),t=this.targetLatency;if(null===e||null===t)return null;var r=this.levelDetails;if(null===r)return null;var i=r.edge,n=e-t-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var e=this.levelDetails;return null===e?1:e.drift}},{key:"edgeStalled",get:function(){var e=this.levelDetails;if(null===e)return 0;var t=3*(this.config.lowLatencyMode&&e.partTarget||e.targetduration);return Math.max(e.age-t,0)}},{key:"forwardBufferLength",get:function(){var e=this.media,t=this.levelDetails;if(!e||!t)return 0;var r=e.buffered.length;return(r?e.buffered.end(r-1):t.edge)-this.currentTime}}])}(),Dl=function(e){function t(t,r){var i;return(i=e.call(this,t,"level-controller")||this)._levels=[],i._firstLevel=-1,i._maxAutoLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}o(t,e);var r=t.prototype;return r._registerListeners=function(){var e=this.hls;e.on(b.MANIFEST_LOADING,this.onManifestLoading,this),e.on(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.on(b.LEVEL_LOADED,this.onLevelLoaded,this),e.on(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.on(b.FRAG_BUFFERED,this.onFragBuffered,this),e.on(b.ERROR,this.onError,this)},r._unregisterListeners=function(){var e=this.hls;e.off(b.MANIFEST_LOADING,this.onManifestLoading,this),e.off(b.MANIFEST_LOADED,this.onManifestLoaded,this),e.off(b.LEVEL_LOADED,this.onLevelLoaded,this),e.off(b.LEVELS_UPDATED,this.onLevelsUpdated,this),e.off(b.FRAG_BUFFERED,this.onFragBuffered,this),e.off(b.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),e.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(e){e.loadError=0,e.fragmentError=0})),e.prototype.stopLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1},r.onManifestLoading=function(e,t){this.resetLevels()},r.onManifestLoaded=function(e,t){var r=this,i=this.hls.config.preferManagedMediaSource,n=[],a={},s={},o=!1,l=!1,u=!1;t.levels.forEach((function(e){var t=e.attrs,d=e.audioCodec,h=e.videoCodec;d&&(e.audioCodec=d=Ke(d,i)||void 0),h&&(h=e.videoCodec=function(e){for(var t=e.split(","),r=0;r2&&"avc1"===i[0]&&(t[r]="avc1."+parseInt(i[1]).toString(16)+("000"+parseInt(i[2]).toString(16)).slice(-4))}return t.join(",")}(h));var f=e.width,c=e.height,g=e.unknownCodecs,v=(null==g?void 0:g.length)||0;if(o||(o=!(!f||!c)),l||(l=!!h),u||(u=!!d),v||d&&!r.isAudioSupported(d)||h&&!r.isVideoSupported(h))r.log('Some or all CODECS not supported "'+t.CODECS+'"');else{var m=t.CODECS,p=t["FRAME-RATE"],y=t["HDCP-LEVEL"],E=t["PATHWAY-ID"],T=t.RESOLUTION,S=t["VIDEO-RANGE"],A=(E||".")+"-"+e.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(a[A])if(a[A].uri===e.url||e.attrs["PATHWAY-ID"])a[A].addGroupId("audio",t.AUDIO),a[A].addGroupId("text",t.SUBTITLES);else{var L=s[A]+=1;e.attrs["PATHWAY-ID"]=new Array(L+1).join(".");var I=r.createLevel(e);a[A]=I,n.push(I)}else{var R=r.createLevel(e);a[A]=R,s[A]=1,n.push(R)}}})),this.filterAndSortMediaOptions(n,t,o,l,u)},r.createLevel=function(e){var t=new st(e),r=e.supplemental;if(null!=r&&r.videoCodec&&!this.isVideoSupported(r.videoCodec)){var i=new Error('SUPPLEMENTAL-CODECS not supported "'+r.videoCodec+'"');this.log(i.message),t.supportedResult=Qe(i,[])}return t},r.isAudioSupported=function(e){return xe(e,"audio",this.hls.config.preferManagedMediaSource)},r.isVideoSupported=function(e){return xe(e,"video",this.hls.config.preferManagedMediaSource)},r.filterAndSortMediaOptions=function(e,t,r,i,n){var a,s=this,o=[],l=[],u=e,d=(null==(a=t.stats)?void 0:a.parsing)||{};if((r||i)&&n&&(u=u.filter((function(e){var t,r=e.videoCodec,i=e.videoRange,n=e.width,a=e.height;return(!!r||!(!n||!a))&&!!(t=i)&&et.indexOf(t)>-1}))),0===u.length)return Promise.resolve().then((function(){if(s.hls){var e="no level with compatible codecs found in manifest",r=e;t.levels.length&&(r="one or more CODECS in variant not supported: "+ut(t.levels.map((function(e){return e.attrs.CODECS})).filter((function(e,t,r){return r.indexOf(e)===t}))),s.warn(r),e+=" ("+r+")");var i=new Error(e);s.hls.trigger(b.ERROR,{type:R.MEDIA_ERROR,details:k.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:t.url,error:i,reason:r})}})),void(d.end=performance.now());t.audioTracks&&_l(o=t.audioTracks.filter((function(e){return!e.audioCodec||s.isAudioSupported(e.audioCodec)}))),t.subtitles&&_l(l=t.subtitles);var h=u.slice(0);u.sort((function(e,t){if(e.attrs["HDCP-LEVEL"]!==t.attrs["HDCP-LEVEL"])return(e.attrs["HDCP-LEVEL"]||"")>(t.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&e.height!==t.height)return e.height-t.height;if(e.frameRate!==t.frameRate)return e.frameRate-t.frameRate;if(e.videoRange!==t.videoRange)return et.indexOf(e.videoRange)-et.indexOf(t.videoRange);if(e.videoCodec!==t.videoCodec){var i=Ne(e.videoCodec),n=Ne(t.videoCodec);if(i!==n)return n-i}if(e.uri===t.uri&&e.codecSet!==t.codecSet){var a=Ue(e.codecSet),s=Ue(t.codecSet);if(a!==s)return s-a}return e.averageBitrate!==t.averageBitrate?e.averageBitrate-t.averageBitrate:0}));var f=h[0];if(this.steering&&(u=this.steering.filterParsedLevels(u)).length!==h.length)for(var c=0;cp&&p===this.hls.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=y)}break}var E=n&&!i,T=this.hls.config,S=!(!T.audioStreamController||!T.audioTrackController),A={levels:u,audioTracks:o,subtitleTracks:l,sessionData:t.sessionData,sessionKeys:t.sessionKeys,firstLevel:this._firstLevel,stats:t.stats,audio:n,video:i,altAudio:S&&!E&&o.some((function(e){return!!e.url}))};d.end=performance.now(),this.hls.trigger(b.MANIFEST_PARSED,A)},r.onError=function(e,t){!t.fatal&&t.context&&t.context.type===_&&t.context.level===this.level&&this.checkRetry(t)},r.onFragBuffered=function(e,t){var r=t.frag;if(void 0!==r&&r.type===w){var i=r.elementaryStreams;if(!Object.keys(i).some((function(e){return!!i[e]})))return;var n=this._levels[r.level];null!=n&&n.loadError&&(this.log("Resetting level error count of "+n.loadError+" on frag buffered"),n.loadError=0)}},r.onLevelLoaded=function(e,t){var r,i,n=t.level,a=t.details,s=t.levelInfo;if(!s)return this.warn("Invalid level index "+n),void(null!=(i=t.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));if(s===this.currentLevel||t.withoutMultiVariant){0===s.fragmentError&&(s.loadError=0);var o=s.details;o===t.details&&o.advanced&&(o=void 0),this.playlistLoaded(n,t,o)}else null!=(r=t.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(t){e.prototype.loadPlaylist.call(this),this.shouldLoadPlaylist(this.currentLevel)&&this.scheduleLoading(this.currentLevel,t)},r.loadingPlaylist=function(t,r){e.prototype.loadingPlaylist.call(this,t,r);var i=this.getUrlWithDirectives(t.uri,r),n=this.currentLevelIndex,a=t.attrs["PATHWAY-ID"],s=t.details,o=null==s?void 0:s.age;this.log("Loading level index "+n+(void 0!==(null==r?void 0:r.msn)?" at sn "+r.msn+" part "+r.part:"")+(a?" Pathway "+a:"")+(o&&s.live?" age "+o.toFixed(1)+(s.type&&" "+s.type||""):"")+" "+i),this.hls.trigger(b.LEVEL_LOADING,{url:i,level:n,levelInfo:t,pathwayId:t.attrs["PATHWAY-ID"],id:0,deliveryDirectives:r||null})},r.removeLevel=function(e){var t,r=this;if(1!==this._levels.length){var i=this._levels.filter((function(t,i){return i!==e||(r.steering&&r.steering.removeLevel(t),t===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,t.details&&t.details.fragments.forEach((function(e){return e.level=-1}))),!1)}));yi(i),this._levels=i,this.currentLevelIndex>-1&&null!=(t=this.currentLevel)&&t.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.manualLevelIndex>-1&&(this.manualLevelIndex=this.currentLevelIndex);var n=i.length-1;this._firstLevel=Math.min(this._firstLevel,n),this._startLevel&&(this._startLevel=Math.min(this._startLevel,n)),this.hls.trigger(b.LEVELS_UPDATED,{levels:i})}},r.onLevelsUpdated=function(e,t){var r=t.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var e=this.hls,t=e.autoLevelCapping,r=e.maxAutoLevel,i=e.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(b.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:t,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},i(t,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"loadLevelObj",get:function(){return this.currentLevel}},{key:"level",get:function(){return this.currentLevelIndex},set:function(e){var t=this._levels;if(0!==t.length){if(e<0||e>=t.length){var r=new Error("invalid level idx"),i=e<0;if(this.hls.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.LEVEL_SWITCH_ERROR,level:e,fatal:i,error:r,reason:r.message}),i)return;e=Math.min(e,t.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=t[e],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=e,this.currentLevel=o,n!==e||!a||s!==l){this.log("Switching to level "+e+" ("+(o.height?o.height+"p ":"")+(o.videoRange?o.videoRange+" ":"")+(o.codecSet?o.codecSet+" ":"")+"@"+o.bitrate+")"+(l?" with Pathway "+l:"")+" from level "+n+(s?" with Pathway "+s:""));var u={level:e,attrs:o.attrs,details:o.details,bitrate:o.bitrate,averageBitrate:o.averageBitrate,maxBitrate:o.maxBitrate,realBitrate:o.realBitrate,width:o.width,height:o.height,codecSet:o.codecSet,audioCodec:o.audioCodec,videoCodec:o.videoCodec,audioGroups:o.audioGroups,subtitleGroups:o.subtitleGroups,loaded:o.loaded,loadError:o.loadError,fragmentError:o.fragmentError,name:o.name,id:o.id,uri:o.uri,url:o.url,urlId:0,audioGroupIds:o.audioGroupIds,textGroupIds:o.textGroupIds};this.hls.trigger(b.LEVEL_SWITCHING,u);var d=o.details;if(!d||d.live){var h=this.switchParams(o.uri,null==a?void 0:a.details,d);this.loadPlaylist(h)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(e){this.manualLevelIndex=e,void 0===this._startLevel&&(this._startLevel=e),-1!==e&&(this.level=e)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(e){this._firstLevel=e}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var e=this.hls.config.startLevel;return void 0!==e?e:this.hls.firstAutoLevel}return this._startLevel},set:function(e){this._startLevel=e}},{key:"pathways",get:function(){return this.steering?this.steering.pathways():[]}},{key:"pathwayPriority",get:function(){return this.steering?this.steering.pathwayPriority:null},set:function(e){if(this.steering){var t=this.steering.pathways(),r=e.filter((function(e){return-1!==t.indexOf(e)}));if(e.length<1)return void this.warn("pathwayPriority "+e+" should contain at least one pathway from list: "+t);this.steering.pathwayPriority=r}}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(e){this.level=e,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=e)}}])}(ya);function _l(e){var t={};e.forEach((function(e){var r=e.groupId||"";e.id=t[r]=t[r]||0,t[r]++}))}function Pl(){return self.SourceBuffer||self.WebKitSourceBuffer}function Cl(){if(!W())return!1;var e=Pl();return!e||e.prototype&&"function"==typeof e.prototype.appendBuffer&&"function"==typeof e.prototype.remove}var wl=function(e){function t(t,r,i){var n;return(n=e.call(this,t,r,i,"stream-controller",w)||this).audioCodecSwap=!1,n.level=-1,n._forceStartLoad=!1,n._hasEnoughToStart=!1,n.altAudio=0,n.audioOnly=!1,n.fragPlaying=null,n.fragLastKbps=0,n.couldBacktrack=!1,n.backtrackFragment=null,n.audioCodecSwitch=!1,n.videoBuffer=null,n.onMediaPlaying=function(){n.tick()},n.onMediaSeeked=function(){var e=n.media,t=e?e.currentTime:null;if(null!==t&&A(t)&&(n.log("Media seeked to "+t.toFixed(3)),n.getBufferedFrag(t))){var r=n.getFwdBufferInfoAtPos(e,t,w,0);null!==r&&0!==r.len?n.tick():n.warn("Main forward buffer length at "+t+' on "seeked" event '+(r?r.len:"empty")+")")}},n.registerListeners(),n}o(t,e);var r=t.prototype;return r.registerListeners=function(){e.prototype.registerListeners.call(this);var t=this.hls;t.on(b.MANIFEST_PARSED,this.onManifestParsed,this),t.on(b.LEVEL_LOADING,this.onLevelLoading,this),t.on(b.LEVEL_LOADED,this.onLevelLoaded,this),t.on(b.FRAG_LOAD_EMERGENCY_ABORTED,this.onFragLoadEmergencyAborted,this),t.on(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),t.on(b.AUDIO_TRACK_SWITCHED,this.onAudioTrackSwitched,this),t.on(b.BUFFER_CREATED,this.onBufferCreated,this),t.on(b.BUFFER_FLUSHED,this.onBufferFlushed,this),t.on(b.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(b.FRAG_BUFFERED,this.onFragBuffered,this)},r.unregisterListeners=function(){e.prototype.unregisterListeners.call(this);var t=this.hls;t.off(b.MANIFEST_PARSED,this.onManifestParsed,this),t.off(b.LEVEL_LOADED,this.onLevelLoaded,this),t.off(b.FRAG_LOAD_EMERGENCY_ABORTED,this.onFragLoadEmergencyAborted,this),t.off(b.AUDIO_TRACK_SWITCHING,this.onAudioTrackSwitching,this),t.off(b.AUDIO_TRACK_SWITCHED,this.onAudioTrackSwitched,this),t.off(b.BUFFER_CREATED,this.onBufferCreated,this),t.off(b.BUFFER_FLUSHED,this.onBufferFlushed,this),t.off(b.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(b.FRAG_BUFFERED,this.onFragBuffered,this)},r.onHandlerDestroying=function(){this.onMediaPlaying=this.onMediaSeeked=null,this.unregisterListeners(),e.prototype.onHandlerDestroying.call(this)},r.startLoad=function(e,t){if(this.levels){var r=this.lastCurrentTime,i=this.hls;if(this.stopLoad(),this.setInterval(100),this.level=-1,!this.startFragRequested){var n=i.startLevel;-1===n&&(i.config.testBandwidth&&this.levels.length>1?(n=0,this.bitrateTest=!0):n=i.firstAutoLevel),i.nextLoadLevel=n,this.level=i.loadLevel,this._hasEnoughToStart=!!t}r>0&&-1===e&&!t&&(this.log("Override startPosition with lastCurrentTime @"+r.toFixed(3)),e=r),this.state=_i.IDLE,this.nextLoadPosition=this.lastCurrentTime=e+this.timelineOffset,this.startPosition=t?-1:e,this.tick()}else this._forceStartLoad=!0,this.state=_i.STOPPED},r.stopLoad=function(){this._forceStartLoad=!1,e.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case _i.WAITING_LEVEL:var e=this.levels,t=this.level,r=null==e?void 0:e[t],i=null==r?void 0:r.details;if(i&&(!i.live||this.levelLastLoaded===r&&!this.waitForLive(r))){if(this.waitForCdnTuneIn(i))break;this.state=_i.IDLE;break}if(this.hls.nextLoadLevel!==this.level){this.state=_i.IDLE;break}break;case _i.FRAG_LOADING_WAITING_RETRY:this.checkRetryDate()}this.state===_i.IDLE&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){var t;e.prototype.onTickEnd.call(this),null!=(t=this.media)&&t.readyState&&!1===this.media.seeking&&(this.lastCurrentTime=this.media.currentTime),this.checkFragmentChanged()},r.doTickIdle=function(){var e=this.hls,t=this.levelLastLoaded,r=this.levels,i=this.media;if(null!==t&&(i||this.primaryPrefetch||!this.startFragRequested&&e.config.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)){var n=this.buffering?e.nextLoadLevel:e.loadLevel;if(null!=r&&r[n]){var a=r[n],s=this.getMainFwdBufferInfo();if(null!==s){var o=this.getLevelDetails();if(o&&this._streamEnded(s,o)){var l={};return 2===this.altAudio&&(l.type="video"),this.hls.trigger(b.BUFFER_EOS,l),void(this.state=_i.ENDED)}if(this.buffering){e.loadLevel!==n&&-1===e.manualLevel&&this.log("Adapting to level "+n+" from level "+this.level),this.level=e.nextLoadLevel=n;var u=a.details;if(!u||this.state===_i.WAITING_LEVEL||this.waitForLive(a))return this.level=n,this.state=_i.WAITING_LEVEL,void(this.startFragRequested=!1);var d=s.len,h=this.getMaxBufferLength(a.maxBitrate);if(!(d>=h)){this.backtrackFragment&&this.backtrackFragment.start>s.end&&(this.backtrackFragment=null);var f=this.backtrackFragment?this.backtrackFragment.start:s.end,c=this.getNextFragment(f,u);if(this.couldBacktrack&&!this.fragPrevious&&c&&te(c)&&this.fragmentTracker.getState(c)!==Wt){var g,v=(null!=(g=this.backtrackFragment)?g:c).sn-u.startSN,m=u.fragments[v-1];m&&c.cc===m.cc&&(c=m,this.fragmentTracker.removeFragment(m))}else this.backtrackFragment&&s.len&&(this.backtrackFragment=null);if(c&&this.isLoopLoading(c,f)){if(!c.gap){var p=this.audioOnly&&!this.altAudio?$:Z,y=(p===Z?this.videoBuffer:this.mediaBuffer)||this.media;y&&this.afterBufferFlushed(y,p,w)}c=this.getNextFragmentLoopLoading(c,u,s,w,h)}c&&(!c.initSegment||c.initSegment.data||this.bitrateTest||(c=c.initSegment),this.loadFragment(c,a,f))}}}}}},r.loadFragment=function(t,r,i){var n=this.fragmentTracker.getState(t);n===Vt||n===Yt?te(t)?this.bitrateTest?(this.log("Fragment "+t.sn+" of level "+t.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(t,r)):e.prototype.loadFragment.call(this,t,r,i):this._loadInitSegment(t,r):this.clearTrackerIfNeeded(t)},r.getBufferedFrag=function(e){return this.fragmentTracker.getBufferedFrag(e,w)},r.followingBufferedFrag=function(e){return e?this.getBufferedFrag(e.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var e=this.levels,t=this.media;if(null!=t&&t.readyState){var r,i=this.getAppendedFrag(t.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);var n=this.getLevelDetails();if(null!=n&&n.live){var a=this.getMainFwdBufferInfo();if(!a||a.len<2*n.targetduration)return}if(!t.paused&&e){var s=e[this.hls.nextLoadLevel],o=this.fragLastKbps;r=o&&this.fragCurrent?this.fragCurrent.duration*s.maxBitrate/(1e3*o)+1:0}else r=0;var l=this.getBufferedFrag(t.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var d=u.maxStartPTS?u.maxStartPTS:u.start,h=u.duration,f=Math.max(l.end,d+Math.min(Math.max(h-this.config.maxFragLookUpTolerance,h*(this.couldBacktrack?.5:.125)),h*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(f,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var e=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,e&&(e.abortRequests(),this.fragmentTracker.removeFragment(e)),this.state){case _i.KEY_LOADING:case _i.FRAG_LOADING:case _i.FRAG_LOADING_WAITING_RETRY:case _i.PARSING:case _i.PARSED:this.state=_i.IDLE}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(t,r){e.prototype.flushMainBuffer.call(this,t,r,2===this.altAudio?"video":null)},r.onMediaAttached=function(t,r){e.prototype.onMediaAttached.call(this,t,r);var i=r.media;ki(i,"playing",this.onMediaPlaying),ki(i,"seeked",this.onMediaSeeked)},r.onMediaDetaching=function(t,r){var i=this.media;i&&(bi(i,"playing",this.onMediaPlaying),bi(i,"seeked",this.onMediaSeeked)),this.videoBuffer=null,this.fragPlaying=null,e.prototype.onMediaDetaching.call(this,t,r),r.transferMedia||(this._hasEnoughToStart=!1)},r.onManifestLoading=function(){e.prototype.onManifestLoading.call(this),this.log("Trigger BUFFER_RESET"),this.hls.trigger(b.BUFFER_RESET,void 0),this.couldBacktrack=!1,this.fragLastKbps=0,this.fragPlaying=this.backtrackFragment=null,this.altAudio=0,this.audioOnly=!1},r.onManifestParsed=function(e,t){for(var r,i,n=!1,a=!1,s=0;s=a-t.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n-1&&this.fragCurrent&&(this.level=this.fragCurrent.level,-1===this.level&&this.resetWhenMissingContext(this.fragCurrent)),this.levels=t.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var e=this.media;if(e){var t=e.currentTime,r=this.startPosition;if(r>=0&&t0&&(oS.cc;if(!1!==i.independent){var R=u.startPTS,k=u.endPTS,D=u.startDTS,_=u.endDTS;if(o)o.elementaryStreams[u.type]={startPTS:R,endPTS:k,startDTS:D,endDTS:_};else if(u.firstKeyFrame&&u.independent&&1===n.id&&!I&&(this.couldBacktrack=!0),u.dropped&&u.independent){var P=this.getMainFwdBufferInfo(),C=(P?P.end:this.getLoadPosition())+this.config.maxBufferHole,w=u.firstKeyFramePTS?u.firstKeyFramePTS:R;if(!L&&C2&&(s.gap=!0);s.setElementaryStreamInfo(u.type,R,k,D,_),this.backtrackFragment&&(this.backtrackFragment=s),this.bufferFragmentData(u,s,o,n,L||I)}else{if(!L&&!I)return void this.backtrack(s);s.gap=!0}}if(g){var O=g.startPTS,x=g.endPTS,M=g.startDTS,F=g.endDTS;o&&(o.elementaryStreams[$]={startPTS:O,endPTS:x,startDTS:M,endDTS:F}),s.setElementaryStreamInfo($,O,x,M,F),this.bufferFragmentData(g,s,o,n)}if(c&&null!=h&&h.samples.length){var N={id:t,frag:s,details:c,samples:h.samples};r.trigger(b.FRAG_PARSING_METADATA,N)}if(c&&d){var U={id:t,frag:s,details:c,samples:d.samples};r.trigger(b.FRAG_PARSING_USERDATA,U)}}}else this.resetWhenMissingContext(n)},r.logMuxedErr=function(e){this.warn((te(e)?"Media":"Init")+" segment with muxed audiovideo where only video expected: "+e.url)},r._bufferInitSegment=function(e,t,r,i){var n=this;if(this.state===_i.PARSING){this.audioOnly=!!t.audio&&!t.video,this.altAudio&&!this.audioOnly&&(delete t.audio,t.audiovideo&&this.logMuxedErr(r));var a=t.audio,s=t.video,o=t.audiovideo;if(a){var l=e.audioCodec,u=Ve(a.codec,l);"mp4a"===u&&(u="mp4a.40.5");var d=navigator.userAgent.toLowerCase();if(this.audioCodecSwitch){u&&(u=-1!==u.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5");var h=a.metadata;h&&"channelCount"in h&&1!==(h.channelCount||1)&&-1===d.indexOf("firefox")&&(u="mp4a.40.5")}u&&-1!==u.indexOf("mp4a.40.5")&&-1!==d.indexOf("android")&&"audio/mpeg"!==a.container&&(u="mp4a.40.2",this.log("Android: force audio codec to "+u)),l&&l!==u&&this.log('Swapping manifest audio codec "'+l+'" for "'+u+'"'),a.levelCodec=u,a.id=w,this.log("Init audio buffer, container:"+a.container+", codecs[selected/level/parsed]=["+(u||"")+"/"+(l||"")+"/"+a.codec+"]"),delete t.audiovideo}if(s){s.levelCodec=e.videoCodec,s.id=w;var f=s.codec;if(4===(null==f?void 0:f.length))switch(f){case"hvc1":case"hev1":s.codec="hvc1.1.6.L120.90";break;case"av01":s.codec="av01.0.04M.08";break;case"avc1":s.codec="avc1.42e01e"}this.log("Init video buffer, container:"+s.container+", codecs[level/parsed]=["+(e.videoCodec||"")+"/"+f+"]"+(s.codec!==f?" parsed-corrected="+s.codec:"")+(s.supplemental?" supplemental="+s.supplemental:"")),delete t.audiovideo}o&&(this.log("Init audiovideo buffer, container:"+o.container+", codecs[level/parsed]=["+e.codecs+"/"+o.codec+"]"),delete t.video,delete t.audio);var c=Object.keys(t);if(c.length){if(this.hls.trigger(b.BUFFER_CODECS,t),!this.hls)return;c.forEach((function(e){var a=t[e].initSegment;null!=a&&a.byteLength&&n.hls.trigger(b.BUFFER_APPENDING,{type:e,data:a,frag:r,part:null,chunkMeta:i,parent:r.type})}))}this.tickImmediate()}},r.getMainFwdBufferInfo=function(){var e=this.mediaBuffer&&2===this.altAudio?this.mediaBuffer:this.media;return this.getFwdBufferInfo(e,w)},r.backtrack=function(e){this.couldBacktrack=!0,this.backtrackFragment=e,this.resetTransmuxer(),this.flushBufferGap(e),this.fragmentTracker.removeFragment(e),this.fragPrevious=null,this.nextLoadPosition=e.start,this.state=_i.IDLE},r.checkFragmentChanged=function(){var e=this.media,t=null;if(e&&e.readyState>1&&!1===e.seeking){var r=e.currentTime;if(dr.isBuffered(e,r)?t=this.getAppendedFrag(r):dr.isBuffered(e,r+.1)&&(t=this.getAppendedFrag(r+.1)),t){this.backtrackFragment=null;var i=this.fragPlaying,n=t.level;i&&t.sn===i.sn&&i.level===n||(this.fragPlaying=t,this.hls.trigger(b.FRAG_CHANGED,{frag:t}),i&&i.level===n||this.hls.trigger(b.LEVEL_SWITCHED,{level:n}))}}},i(t,[{key:"hasEnoughToStart",get:function(){return this._hasEnoughToStart}},{key:"maxBufferLength",get:function(){var e=this.levels,t=this.level,r=null==e?void 0:e[t];return r?this.getMaxBufferLength(r.maxBitrate):this.config.maxBufferLength}},{key:"nextLevel",get:function(){var e=this.nextBufferedFrag;return e?e.level:-1}},{key:"currentFrag",get:function(){var e;if(this.fragPlaying)return this.fragPlaying;var t=(null==(e=this.media)?void 0:e.currentTime)||this.lastCurrentTime;return A(t)?this.getAppendedFrag(t):null}},{key:"currentProgramDateTime",get:function(){var e,t=(null==(e=this.media)?void 0:e.currentTime)||this.lastCurrentTime;if(A(t)){var r=this.getLevelDetails(),i=this.currentFrag||(r?Tt(null,r.fragments,t):null);if(i){var n=i.programDateTime;if(null!==n){var a=n+1e3*(t-i.start);return new Date(a)}}}return null}},{key:"currentLevel",get:function(){var e=this.currentFrag;return e?e.level:-1}},{key:"nextBufferedFrag",get:function(){var e=this.currentFrag;return e?this.followingBufferedFrag(e):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}])}(Pi),Ol=function(e){function t(t,r){var i;return(i=e.call(this,"key-loader",r)||this).config=void 0,i.keyIdToKeyInfo={},i.emeController=null,i.config=t,i}o(t,e);var r=t.prototype;return r.abort=function(e){for(var t in this.keyIdToKeyInfo){var r=this.keyIdToKeyInfo[t].loader;if(r){var i;if(e&&e!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},r.detach=function(){for(var e in this.keyIdToKeyInfo){var t=this.keyIdToKeyInfo[e];(t.mediaKeySessionContext||t.decryptdata.isCommonEncryption)&&delete this.keyIdToKeyInfo[e]}},r.destroy=function(){for(var e in this.detach(),this.keyIdToKeyInfo){var t=this.keyIdToKeyInfo[e].loader;t&&t.destroy()}this.keyIdToKeyInfo={}},r.createKeyLoadError=function(e,t,r,i,n){return void 0===t&&(t=k.KEY_LOAD_ERROR),new sr({type:R.NETWORK_ERROR,details:t,fatal:!1,frag:e,response:n,error:r,networkDetails:i})},r.loadClear=function(e,t,r){var i=this;if(this.emeController&&this.config.emeEnabled&&!this.emeController.getSelectedKeySystemFormats().length){if(t.length)for(var n,a=function(){var n=t[s];if(e.cc<=n.cc&&(!te(e)||!te(n)||e.sn-1&&(v=p)}}else v=0;s.trigger(b.LEVEL_LOADED,{details:e,levelInfo:u||s.levels[0],level:v||0,id:d||0,stats:r,networkDetails:n,deliveryDirectives:f,withoutMultiVariant:o===D});break;case P:s.trigger(b.AUDIO_TRACK_LOADED,{details:e,track:u,id:d||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:f});break;case C:s.trigger(b.SUBTITLE_TRACK_LOADED,{details:e,track:u,id:d||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:f})}else{var y=e.playlistParsingError=new Error("No Segments found in Playlist");s.trigger(b.ERROR,{type:R.NETWORK_ERROR,details:k.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:y,reason:y.message,response:t,context:i,level:v,parent:g,networkDetails:n,stats:r})}},e}(),Ul=function(){function e(t){void 0===t&&(t={}),this.config=void 0,this.userConfig=void 0,this.logger=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this._emitter=new E,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioStreamController=void 0,this.subtititleStreamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.interstitialsController=void 0,this.gapController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this._url=null,this._sessionId=void 0,this.triggeringException=void 0,this.started=!1;var r=this.logger=H(t.debug||!1,"Hls instance",t.assetPlayerId),i=this.config=function(e,t,r){if((t.liveSyncDurationCount||t.liveMaxLatencyDurationCount)&&(t.liveSyncDuration||t.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==t.liveMaxLatencyDurationCount&&(void 0===t.liveSyncDurationCount||t.liveMaxLatencyDurationCount<=t.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==t.liveMaxLatencyDuration&&(void 0===t.liveSyncDuration||t.liveMaxLatencyDuration<=t.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var i=pl(e),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(e){var a=("level"===e?"playlist":e)+"LoadPolicy",s=void 0===t[a],o=[];n.forEach((function(r){var n=e+"Loading"+r,l=t[n];if(void 0!==l&&s){o.push(n);var u=i[a].default;switch(t[a]={default:u},r){case"TimeOut":u.maxLoadTimeMs=l,u.maxTimeToFirstByteMs=l;break;case"MaxRetry":u.errorRetry.maxNumRetry=l,u.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":u.errorRetry.retryDelayMs=l,u.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":u.errorRetry.maxRetryDelayMs=l,u.timeoutRetry.maxRetryDelayMs=l}}})),o.length&&r.warn('hls.js config: "'+o.join('", "')+'" setting(s) are deprecated, use "'+a+'": '+ut(t[a]))})),d(d({},i),t)}(e.DefaultConfig,t,r);this.userConfig=t,i.progressive&&yl(i,r);var n=i.abrController,a=i.bufferController,s=i.capLevelController,o=i.errorController,l=i.fpsController,u=new o(this),h=this.abrController=new n(this),f=new jt(this),c=i.interstitialsController,g=c?this.interstitialsController=new c(this,e):null,v=this.bufferController=new a(this,f),m=this.capLevelController=new s(this),p=new l(this),y=new Nl(this),T=i.contentSteeringController,S=T?new T(this):null,A=this.levelController=new Dl(this,S),L=new kl(this),I=new Ol(this.config,this.logger),R=this.streamController=new wl(this,f,I),k=this.gapController=new El(this,f);m.setStreamController(R),p.setStreamController(R);var D=[y,A,R];g&&D.splice(1,0,g),S&&D.splice(1,0,S),this.networkControllers=D;var _=[h,v,k,m,p,L,f];this.audioTrackController=this.createController(i.audioTrackController,D);var P=i.audioStreamController;P&&D.push(this.audioStreamController=new P(this,f,I)),this.subtitleTrackController=this.createController(i.subtitleTrackController,D);var C=i.subtitleStreamController;C&&D.push(this.subtititleStreamController=new C(this,f,I)),this.createController(i.timelineController,_),I.emeController=this.emeController=this.createController(i.emeController,_),this.cmcdController=this.createController(i.cmcdController,_),this.latencyController=this.createController(bl,_),this.coreComponents=_,D.push(u);var w=u.onErrorOut;"function"==typeof w&&this.on(b.ERROR,w,u),this.on(b.MANIFEST_LOADED,y.onManifestLoaded,y)}e.isMSESupported=function(){return Cl()},e.isSupported=function(){return function(){if(!Cl())return!1;var e=W();return"function"==typeof(null==e?void 0:e.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((function(t){return e.isTypeSupported(Fe(t,"video"))}))||["mp4a.40.2","fLaC"].some((function(t){return e.isTypeSupported(Fe(t,"audio"))})))}()},e.getMediaSource=function(){return W()};var t=e.prototype;return t.createController=function(e,t){if(e){var r=new e(this);return t&&t.push(r),r}return null},t.on=function(e,t,r){void 0===r&&(r=this),this._emitter.on(e,t,r)},t.once=function(e,t,r){void 0===r&&(r=this),this._emitter.once(e,t,r)},t.removeAllListeners=function(e){this._emitter.removeAllListeners(e)},t.off=function(e,t,r,i){void 0===r&&(r=this),this._emitter.off(e,t,r,i)},t.listeners=function(e){return this._emitter.listeners(e)},t.emit=function(e,t,r){return this._emitter.emit(e,t,r)},t.trigger=function(e,t){if(this.config.debug)return this.emit(e,e,t);try{return this.emit(e,e,t)}catch(t){if(this.logger.error("An internal error happened while handling event "+e+'. Error message: "'+t.message+'". Here is a stacktrace:',t),!this.triggeringException){this.triggeringException=!0;var r=e===b.ERROR;this.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.INTERNAL_EXCEPTION,fatal:r,event:e,error:t}),this.triggeringException=!1}}return!1},t.listenerCount=function(e){return this._emitter.listenerCount(e)},t.destroy=function(){this.logger.log("destroy"),this.trigger(b.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this._url=null,this.networkControllers.forEach((function(e){return e.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(e){return e.destroy()})),this.coreComponents.length=0;var e=this.config;e.xhrSetup=e.fetchSetup=void 0,this.userConfig=null},t.attachMedia=function(e){if(!e||"media"in e&&!e.media){var t=new Error("attachMedia failed: invalid argument ("+e+")");this.trigger(b.ERROR,{type:R.OTHER_ERROR,details:k.ATTACH_MEDIA_ERROR,fatal:!0,error:t})}else{this.logger.log("attachMedia"),this._media&&(this.logger.warn("media must be detached before attaching"),this.detachMedia());var r="media"in e,i=r?e.media:e,n=r?e:{media:i};this._media=i,this.trigger(b.MEDIA_ATTACHING,n)}},t.detachMedia=function(){this.logger.log("detachMedia"),this.trigger(b.MEDIA_DETACHING,{}),this._media=null},t.transferMedia=function(){this._media=null;var e=this.bufferController.transferMedia();return this.trigger(b.MEDIA_DETACHING,{transferMedia:e}),e},t.loadSource=function(e){this.stopLoad();var t=this.media,r=this._url,i=this._url=S.buildAbsoluteURL(self.location.href,e,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.logger.log("loadSource:"+i),t&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(t)),this.trigger(b.MANIFEST_LOADING,{url:e})},t.startLoad=function(e,t){void 0===e&&(e=-1),this.logger.log("startLoad("+e+(t?", ":"")+")"),this.started=!0,this.resumeBuffering();for(var r=0;r-1?this.abrController.forcedAutoLevel:e},set:function(e){this.logger.log("set startLevel:"+e),-1!==e&&(e=Math.max(e,this.minAutoLevel)),this.levelController.startLevel=e}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(e){var t=!!e;t!==this.config.capLevelToPlayerSize&&(t?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=t)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(e){this._autoLevelCapping!==e&&(this.logger.log("set autoLevelCapping:"+e),this._autoLevelCapping=e,this.levelController.checkMaxAutoUpdated())}},{key:"bandwidthEstimate",get:function(){var e=this.abrController.bwEstimator;return e?e.getEstimate():NaN},set:function(e){this.abrController.resetEstimator(e)}},{key:"abrEwmaDefaultEstimate",get:function(){var e=this.abrController.bwEstimator;return e?e.defaultEstimate:NaN}},{key:"ttfbEstimate",get:function(){var e=this.abrController.bwEstimator;return e?e.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(e){(function(e){return Je.indexOf(e)>-1})(e)&&this._maxHdcpLevel!==e&&(this._maxHdcpLevel=e,this.levelController.checkMaxAutoUpdated())}},{key:"autoLevelEnabled",get:function(){return-1===this.levelController.manualLevel}},{key:"manualLevel",get:function(){return this.levelController.manualLevel}},{key:"minAutoLevel",get:function(){var e=this.levels,t=this.config.minAutoBitrate;if(!e)return 0;for(var r=e.length,i=0;i=t)return i;return 0}},{key:"maxAutoLevel",get:function(){var e,t=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(e=-1===r&&null!=t&&t.length?t.length-1:r,i)for(var n=e;n--;){var a=t[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return e}},{key:"firstAutoLevel",get:function(){return this.abrController.firstAutoLevel}},{key:"nextAutoLevel",get:function(){return this.abrController.nextAutoLevel},set:function(e){this.abrController.nextAutoLevel=e}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"maxBufferLength",get:function(){return this.streamController.maxBufferLength}},{key:"allAudioTracks",get:function(){var e=this.audioTrackController;return e?e.allAudioTracks:[]}},{key:"audioTracks",get:function(){var e=this.audioTrackController;return e?e.audioTracks:[]}},{key:"audioTrack",get:function(){var e=this.audioTrackController;return e?e.audioTrack:-1},set:function(e){var t=this.audioTrackController;t&&(t.audioTrack=e)}},{key:"allSubtitleTracks",get:function(){var e=this.subtitleTrackController;return e?e.allSubtitleTracks:[]}},{key:"subtitleTracks",get:function(){var e=this.subtitleTrackController;return e?e.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var e=this.subtitleTrackController;return e?e.subtitleTrack:-1},set:function(e){var t=this.subtitleTrackController;t&&(t.subtitleTrack=e)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var e=this.subtitleTrackController;return!!e&&e.subtitleDisplay},set:function(e){var t=this.subtitleTrackController;t&&(t.subtitleDisplay=e)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(e){this.config.lowLatencyMode=e}},{key:"liveSyncPosition",get:function(){return this.latencyController.liveSyncPosition}},{key:"latency",get:function(){return this.latencyController.latency}},{key:"maxLatency",get:function(){return this.latencyController.maxLatency}},{key:"targetLatency",get:function(){return this.latencyController.targetLatency},set:function(e){this.latencyController.targetLatency=e}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}},{key:"pathways",get:function(){return this.levelController.pathways}},{key:"pathwayPriority",get:function(){return this.levelController.pathwayPriority},set:function(e){this.levelController.pathwayPriority=e}},{key:"bufferedToEnd",get:function(){var e;return!(null==(e=this.bufferController)||!e.bufferedToEnd)}},{key:"interstitialsManager",get:function(){var e;return(null==(e=this.interstitialsController)?void 0:e.interstitialsManager)||null}}],[{key:"version",get:function(){return ca}},{key:"Events",get:function(){return b}},{key:"MetadataSchema",get:function(){return rn}},{key:"ErrorTypes",get:function(){return R}},{key:"ErrorDetails",get:function(){return k}},{key:"DefaultConfig",get:function(){return e.defaultConfig?e.defaultConfig:ml},set:function(t){e.defaultConfig=t}}])}();return Ul.defaultConfig=void 0,Ul},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(r="undefined"!=typeof globalThis?globalThis:r||self).Hls=i()}(!1);
+//# sourceMappingURL=hls.min.js.map
diff --git a/pip/hls_writer.c b/pip/hls_writer.c
new file mode 100644
index 0000000..ef37c6b
--- /dev/null
+++ b/pip/hls_writer.c
@@ -0,0 +1,348 @@
+/**
+ *  hls_writer.c
+ *  PiP
+ *
+ *  HLS playlist generator and segment manager implementation.
+ *  Uses a fixed-size ring buffer to store MPEG-TS segments and generates
+ *  live HLS .m3u8 playlists. All public functions are thread-safe using
+ *  a pthread mutex.
+ */
+
+#include "hls_writer.h"
+#include 
+#include 
+#include 
+#include 
+
+/* ------------------------------------------------------------------ */
+/* Ring buffer segment entry                                           */
+/* ------------------------------------------------------------------ */
+
+typedef struct {
+    uint8_t  *data;      /* copied segment data (malloc'd) */
+    size_t    size;      /* segment size in bytes */
+    double    duration;  /* segment duration in seconds */
+    uint64_t  index;     /* segment index from the TS muxer */
+    int       valid;     /* non-zero if this slot contains data */
+} hls_segment_entry_t;
+
+/* ------------------------------------------------------------------ */
+/* Writer state structure                                              */
+/* ------------------------------------------------------------------ */
+
+struct hls_writer_s {
+    /* Ring buffer of segment entries */
+    hls_segment_entry_t *segments;
+    int max_segments;       /* capacity of the ring buffer */
+    int playlist_size;      /* max segments to list in the .m3u8 playlist */
+    int write_pos;          /* next slot to write into (circular) */
+    int count;              /* number of valid segments currently stored */
+
+    /* HLS playlist parameters */
+    int target_duration;    /* EXT-X-TARGETDURATION value in seconds */
+
+    /* Thread safety */
+    pthread_mutex_t lock;
+};
+
+/* ------------------------------------------------------------------ */
+/* Public API                                                          */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Create an HLS writer instance.
+ * @param max_segments    Maximum number of segments to keep in the ring buffer
+ * @param playlist_size   Maximum number of segments to list in the .m3u8 playlist (must be <= max_segments)
+ * @param target_duration Target segment duration in seconds (for EXT-X-TARGETDURATION)
+ * @return New writer instance, or NULL on allocation failure
+ */
+hls_writer_t *
+hls_writer_create(int max_segments, int playlist_size, int target_duration)
+{
+    if (max_segments <= 0 || target_duration <= 0 || playlist_size <= 0) {
+        return NULL;
+    }
+
+    if (playlist_size > max_segments) {
+        playlist_size = max_segments;
+    }
+
+    hls_writer_t *writer = calloc(1, sizeof(hls_writer_t));
+    if (!writer) {
+        return NULL;
+    }
+
+    writer->segments = calloc((size_t)max_segments, sizeof(hls_segment_entry_t));
+    if (!writer->segments) {
+        free(writer);
+        return NULL;
+    }
+
+    writer->max_segments = max_segments;
+    writer->playlist_size = playlist_size;
+    writer->write_pos = 0;
+    writer->count = 0;
+    writer->target_duration = target_duration;
+
+    if (pthread_mutex_init(&writer->lock, NULL) != 0) {
+        free(writer->segments);
+        free(writer);
+        return NULL;
+    }
+
+    return writer;
+} // end of function hls_writer_create()
+
+/**
+ * Destroy a writer instance and free all resources.
+ * @param writer The writer instance (may be NULL)
+ */
+void
+hls_writer_destroy(hls_writer_t *writer)
+{
+    if (!writer) {
+        return;
+    }
+
+    /* Free all segment data in the ring buffer */
+    for (int i = 0; i < writer->max_segments; i++) {
+        if (writer->segments[i].valid && writer->segments[i].data) {
+            free(writer->segments[i].data);
+            writer->segments[i].data = NULL;
+        }
+    } // end of loop freeing segment data
+
+    free(writer->segments);
+    writer->segments = NULL;
+
+    pthread_mutex_destroy(&writer->lock);
+
+    free(writer);
+} // end of function hls_writer_destroy()
+
+/**
+ * Add a new segment to the ring buffer. If the buffer is full, the oldest
+ * segment is evicted. This function copies the segment data.
+ * Thread-safe: can be called from the muxer thread while HTTP serves.
+ * @param writer   The writer instance
+ * @param data     Segment data (will be copied)
+ * @param size     Size of segment data in bytes
+ * @param duration Duration of the segment in seconds
+ * @param index    Segment index from the TS muxer
+ */
+void
+hls_writer_add_segment(hls_writer_t *writer, uint8_t *data, size_t size,
+                       double duration, uint64_t index)
+{
+    if (!writer || !data || size == 0) {
+        return;
+    }
+
+    /* Allocate a copy of the segment data before locking */
+    uint8_t *data_copy = malloc(size);
+    if (!data_copy) {
+        return;
+    }
+    memcpy(data_copy, data, size);
+
+    pthread_mutex_lock(&writer->lock);
+
+    /* Get the current write slot */
+    hls_segment_entry_t *slot = &writer->segments[writer->write_pos];
+
+    /* If the slot is occupied, free the old segment data */
+    if (slot->valid && slot->data) {
+        free(slot->data);
+        slot->data = NULL;
+    }
+
+    /* Store the new segment */
+    slot->data = data_copy;
+    slot->size = size;
+    slot->duration = duration;
+    slot->index = index;
+    slot->valid = 1;
+
+    /* Advance the write position circularly */
+    writer->write_pos = (writer->write_pos + 1) % writer->max_segments;
+
+    /* Track the number of valid segments */
+    if (writer->count < writer->max_segments) {
+        writer->count++;
+    }
+
+    pthread_mutex_unlock(&writer->lock);
+} // end of function hls_writer_add_segment()
+
+/**
+ * Generate the current HLS playlist (.m3u8) as a string.
+ * Thread-safe. Caller must free the returned string.
+ *
+ * Produces a live HLS playlist (no EXT-X-ENDLIST) with format:
+ *   #EXTM3U
+ *   #EXT-X-VERSION:3
+ *   #EXT-X-TARGETDURATION:{target_duration}
+ *   #EXT-X-MEDIA-SEQUENCE:{oldest_segment_index}
+ *   #EXTINF:{duration},
+ *   segment_{index}.ts
+ *   ...
+ *
+ * @param writer The writer instance
+ * @return Allocated m3u8 playlist string, or NULL on failure
+ */
+char *
+hls_writer_get_playlist(hls_writer_t *writer)
+{
+    if (!writer) {
+        return NULL;
+    }
+
+    pthread_mutex_lock(&writer->lock);
+
+    if (writer->count == 0) {
+        pthread_mutex_unlock(&writer->lock);
+        return NULL;
+    }
+
+    /* Only list the most recent playlist_size segments (not all buffered segments).
+       The ring buffer may hold more segments as a safety margin so that recently-
+       removed playlist entries are still available for slow clients. */
+    int list_count = writer->count;
+    if (list_count > writer->playlist_size) {
+        list_count = writer->playlist_size;
+    }
+
+    /* Estimate buffer size: header ~128 bytes + ~50 bytes per segment entry */
+    size_t buf_size = 128 + (size_t)list_count * 50;
+    char *playlist = malloc(buf_size);
+    if (!playlist) {
+        pthread_mutex_unlock(&writer->lock);
+        return NULL;
+    }
+
+    /* Determine the read position for the OLDEST segment we want to list.
+       write_pos points to the next slot to be overwritten (oldest in buffer when full).
+       We want the most recent list_count segments, so we start from (write_pos - list_count). */
+    int oldest_buf_pos;
+    if (writer->count < writer->max_segments) {
+        oldest_buf_pos = 0;
+    } else {
+        oldest_buf_pos = writer->write_pos;
+    }
+    /* Skip ahead to only list the most recent list_count segments */
+    int skip = writer->count - list_count;
+    int read_pos = (oldest_buf_pos + skip) % writer->max_segments;
+
+    /* Find the media sequence number (index of the oldest listed segment) */
+    uint64_t media_sequence = writer->segments[read_pos].index;
+
+    /* Compute the actual maximum segment duration (rounded up) among listed segments.
+       HLS spec requires EXT-X-TARGETDURATION >= ceil(max segment duration). */
+    int actual_target = writer->target_duration;
+    for (int i = 0; i < list_count; i++) {
+        int slot_idx = (read_pos + i) % writer->max_segments;
+        hls_segment_entry_t *entry = &writer->segments[slot_idx];
+        if (entry->valid) {
+            int dur_ceil = (int)(entry->duration + 0.999);
+            if (dur_ceil > actual_target) {
+                actual_target = dur_ceil;
+            }
+        }
+    } // end of loop computing actual target duration
+
+    /* Write playlist header */
+    int offset = snprintf(playlist, buf_size,
+                          "#EXTM3U\n"
+                          "#EXT-X-VERSION:3\n"
+                          "#EXT-X-TARGETDURATION:%d\n"
+                          "#EXT-X-MEDIA-SEQUENCE:%llu\n",
+                          actual_target,
+                          (unsigned long long)media_sequence);
+
+    /* Write segment entries in order from oldest to newest (only the listed subset) */
+    for (int i = 0; i < list_count; i++) {
+        int slot_idx = (read_pos + i) % writer->max_segments;
+        hls_segment_entry_t *entry = &writer->segments[slot_idx];
+
+        if (!entry->valid) {
+            continue;
+        }
+
+        int remaining = (int)buf_size - offset;
+        if (remaining <= 0) {
+            break;
+        }
+
+        offset += snprintf(playlist + offset, (size_t)remaining,
+                           "#EXTINF:%.3f,\n"
+                           "segment_%llu.ts\n",
+                           entry->duration,
+                           (unsigned long long)entry->index);
+    } // end of loop writing segment entries
+
+    pthread_mutex_unlock(&writer->lock);
+
+    return playlist;
+} // end of function hls_writer_get_playlist()
+
+/**
+ * Retrieve a segment by its index.
+ * Thread-safe. Returns a malloc'd copy of the segment data so it remains
+ * valid even if the ring buffer evicts the original. Caller must free *data.
+ * @param writer The writer instance
+ * @param index  Segment index to retrieve
+ * @param data   Output pointer to segment data copy (caller must free)
+ * @param size   Output pointer to segment size
+ * @return 0 on success, -1 if segment not found
+ */
+int
+hls_writer_get_segment(hls_writer_t *writer, uint64_t index,
+                       uint8_t **data, size_t *size)
+{
+    if (!writer || !data || !size) {
+        return -1;
+    }
+
+    pthread_mutex_lock(&writer->lock);
+
+    /* Search the ring buffer for a segment matching the requested index */
+    for (int i = 0; i < writer->max_segments; i++) {
+        hls_segment_entry_t *entry = &writer->segments[i];
+        if (entry->valid && entry->index == index) {
+            /* Copy the data while holding the lock to prevent use-after-free
+               if the muxer thread evicts this segment concurrently */
+            uint8_t *copy = malloc(entry->size);
+            if (!copy) {
+                pthread_mutex_unlock(&writer->lock);
+                return -1;
+            }
+            memcpy(copy, entry->data, entry->size);
+            *data = copy;
+            *size = entry->size;
+            pthread_mutex_unlock(&writer->lock);
+            return 0;
+        }
+    } // end of loop searching for segment by index
+
+    pthread_mutex_unlock(&writer->lock);
+    return -1;
+} // end of function hls_writer_get_segment()
+
+/**
+ * Get the number of currently stored segments.
+ * @param writer The writer instance
+ * @return Number of segments in the ring buffer
+ */
+int
+hls_writer_segment_count(hls_writer_t *writer)
+{
+    if (!writer) {
+        return 0;
+    }
+
+    pthread_mutex_lock(&writer->lock);
+    int count = writer->count;
+    pthread_mutex_unlock(&writer->lock);
+
+    return count;
+} // end of function hls_writer_segment_count()
diff --git a/pip/hls_writer.h b/pip/hls_writer.h
new file mode 100644
index 0000000..056adda
--- /dev/null
+++ b/pip/hls_writer.h
@@ -0,0 +1,80 @@
+/**
+ *  hls_writer.h
+ *  PiP
+ *
+ *  HLS playlist generator and segment manager. Maintains a rolling window
+ *  of MPEG-TS segments in a ring buffer and generates live HLS .m3u8
+ *  playlists on demand. Designed to receive segments from the TS muxer
+ *  callback and serve them to the HTTP server.
+ */
+
+#ifndef HLS_WRITER_H
+#define HLS_WRITER_H
+
+#include 
+#include 
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct hls_writer_s hls_writer_t;
+
+/**
+ * Create an HLS writer instance.
+ * @param max_segments    Maximum number of segments to keep in the ring buffer
+ * @param playlist_size   Maximum number of segments to list in the .m3u8 playlist (must be <= max_segments)
+ * @param target_duration Target segment duration in seconds (for EXT-X-TARGETDURATION)
+ * @return New writer instance, or NULL on allocation failure
+ */
+hls_writer_t *hls_writer_create(int max_segments, int playlist_size, int target_duration);
+
+/**
+ * Destroy a writer instance and free all resources.
+ * @param writer The writer instance (may be NULL)
+ */
+void hls_writer_destroy(hls_writer_t *writer);
+
+/**
+ * Add a new segment to the ring buffer. If the buffer is full, the oldest
+ * segment is evicted. This function copies the segment data.
+ * Thread-safe: can be called from the muxer thread while HTTP serves.
+ * @param writer   The writer instance
+ * @param data     Segment data (will be copied)
+ * @param size     Size of segment data in bytes
+ * @param duration Duration of the segment in seconds
+ * @param index    Segment index from the TS muxer
+ */
+void hls_writer_add_segment(hls_writer_t *writer, uint8_t *data, size_t size, double duration, uint64_t index);
+
+/**
+ * Generate the current HLS playlist (.m3u8) as a string.
+ * Thread-safe. Caller must free the returned string.
+ * @param writer The writer instance
+ * @return Allocated m3u8 playlist string, or NULL on failure
+ */
+char *hls_writer_get_playlist(hls_writer_t *writer);
+
+/**
+ * Retrieve a segment by its index.
+ * Thread-safe. Returns a malloc'd copy of the segment data that the caller must free.
+ * @param writer The writer instance
+ * @param index  Segment index to retrieve
+ * @param data   Output pointer to segment data copy (caller must free)
+ * @param size   Output pointer to segment size
+ * @return 0 on success, -1 if segment not found
+ */
+int hls_writer_get_segment(hls_writer_t *writer, uint64_t index, uint8_t **data, size_t *size);
+
+/**
+ * Get the number of currently stored segments.
+ * @param writer The writer instance
+ * @return Number of segments in the ring buffer
+ */
+int hls_writer_segment_count(hls_writer_t *writer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HLS_WRITER_H */
diff --git a/pip/main.m b/pip/main.m
index d67a8b4..06bbd61 100644
--- a/pip/main.m
+++ b/pip/main.m
@@ -8,6 +8,7 @@
 
 #import "window.h"
 #import "preferences.h"
+#import "stream_manager.h"
 #import 
 #import 
 #ifndef NO_AIRPLAY
@@ -89,6 +90,7 @@ -(id)initWithApp:(NSApplication*) application{
   ADD_ITEM(@"New", newWindow, @"n");
   ADD_ITEM_MASK(@"Clone Window", cloneCurrentWindow, @"n", NSEventModifierFlagCommand | NSEventModifierFlagShift);
   ADD_ITEM(@"Stream HLS", loadHLSStream:, @"l");
+  ADD_ITEM_MASK(@"Start Streaming", startStreamCurrentWindow, @"s", NSEventModifierFlagCommand | NSEventModifierFlagShift);
   ADD_ITEM(@"Click Through", clickThrough:, @"c");
   ADD_ITEM(@"Close", performClose:, @"w");
 
@@ -290,6 +292,16 @@ -(void) clickThrough:(id)sender{
   }
 }
 
+/**
+ * Start streaming on the active PiP window.
+ * Finds the frontmost PiP window and triggers streaming via its startStreamAction: method.
+ */
+- (void)startStreamCurrentWindow{
+  [self getActiveWindow:^(Window *window) {
+    [window startStreamAction:nil];
+  }];
+} // End of startStreamCurrentWindow
+
 /**
  * Returns all PiP Window instances, including minimized/hidden ones.
  * Used for counting (max windows) and closing all.
diff --git a/pip/preferences.h b/pip/preferences.h
index a3f3c70..d415ae3 100644
--- a/pip/preferences.h
+++ b/pip/preferences.h
@@ -23,7 +23,7 @@ NSString* getDisplayNameForId(CGDirectDisplayID displayId);
 void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name);
 void showDisplayNamesPanel(void);
 
-@interface Preferences : NSPanel
+@interface Preferences : NSPanel
 
 @end
 
diff --git a/pip/preferences.m b/pip/preferences.m
index a153621..497c27c 100644
--- a/pip/preferences.m
+++ b/pip/preferences.m
@@ -155,6 +155,8 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){
     OPTION(mouse_capture, "Show mouse cursor", CheckBox, [NSNull null], @0, @"when pipping screen"),
     OPTION(new_window_behavior, "New Window", Select, (@[@"Blank with hint", @"Clone current window"]), @0, [NSNull null]),
     OPTION(max_windows, "Max Windows", Select, (@[@"2", @"4", @"6", @"8", @"10"]), @3, [NSNull null]),
+    OPTION(stream_port, "Streaming Port", TextInput, [NSNull null], @"8080", @"HTTP server port"),
+    OPTION(stream_quality, "Streaming Quality", Select, (@[@"Low (720p)", @"Medium (1080p)", @"High (native)"]), @1, [NSNull null]),
   ]];
 
   // Add ScreenCaptureKit option only on macOS 12.3+
@@ -298,6 +300,18 @@ - (void)onButtonClick:(NSButton*)sender{
   }
 } // End of onButtonClick()
 
+/**
+ * Handles text input end editing for TextInput preferences.
+ * Saves the text field value as a string preference.
+ * @param notification The notification containing the text field
+ */
+- (void)controlTextDidEndEditing:(NSNotification *)notification{
+  NSTextField* textField = notification.object;
+  if(textField.identifier){
+    setPref(textField.identifier, textField.stringValue);
+  }
+} // End of controlTextDidEndEditing:
+
 - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{
   NSInteger col = [[tableView tableColumns] indexOfObject:tableColumn];
 //  NSLog(@"row: %ld, col: %ld", row, col);
@@ -347,8 +361,29 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null
         view = checkBox;
         break;
       }
-      case OptionTypeTextInput:
+      case OptionTypeTextInput:{
+        NSTextField* textField = [[NSTextField alloc] init];
+        textField.translatesAutoresizingMaskIntoConstraints = false;
+        textField.editable = YES;
+        textField.selectable = YES;
+        textField.bezeled = YES;
+        textField.bezelStyle = NSTextFieldSquareBezel;
+        textField.drawsBackground = YES;
+        textField.identifier = key;
+        textField.delegate = self;
+        if ([value isKindOfClass:[NSString class]]) {
+          textField.stringValue = (NSString*)value;
+        } else if ([value isKindOfClass:[NSNumber class]]) {
+          textField.stringValue = [(NSNumber*)value stringValue];
+        } else {
+          textField.stringValue = @"";
+        }
+        if (pref[@"desc"] && pref[@"desc"] != [NSNull null]) {
+          textField.placeholderString = (NSString*)pref[@"desc"];
+        }
+        view = textField;
         break;
+      }
       case OptionTypeDisplaySelect:{
         NSPopUpButton* button = [[NSPopUpButton alloc] init];
         button.translatesAutoresizingMaskIntoConstraints = false;
diff --git a/pip/stream_manager.h b/pip/stream_manager.h
new file mode 100644
index 0000000..fae2513
--- /dev/null
+++ b/pip/stream_manager.h
@@ -0,0 +1,105 @@
+/**
+ *  stream_manager.h
+ *  PiP
+ *
+ *  High-level streaming pipeline manager that wires together:
+ *  frame capture -> video encoder -> TS muxer -> HLS writer -> HTTP server.
+ *
+ *  Owns the full lifecycle of every pipeline component and exposes a simple
+ *  Objective-C interface for start/stop/quality control.
+ */
+
+#ifndef stream_manager_h
+#define stream_manager_h
+
+#import 
+
+@class ImageView;
+
+/**
+ * Quality presets for the streaming pipeline.
+ * Each preset determines resolution, bitrate, and frame rate.
+ */
+typedef enum {
+    StreamQualityLow,     /**< 720p,  1.5 Mbps, 24 fps */
+    StreamQualityMedium,  /**< 1080p, 3 Mbps,   30 fps */
+    StreamQualityHigh,    /**< Native resolution, 6 Mbps, 30 fps */
+} StreamQuality;
+
+@interface StreamManager : NSObject
+
+/**
+ * Initialize the stream manager with an ImageView to capture from.
+ * @param imageView The source ImageView whose content will be streamed
+ * @return A new StreamManager instance, or nil on failure
+ */
+- (instancetype)initWithImageView:(ImageView *)imageView;
+
+/**
+ * Start the full streaming pipeline on the given port with the specified quality.
+ * Creates and wires all pipeline components (capture, encoder, muxer, writer, server).
+ * @param port    TCP port for the HTTP server (e.g. 8080)
+ * @param quality Quality preset controlling resolution, bitrate, and frame rate
+ * @return YES on success, NO on failure
+ */
+- (BOOL)startStreamingOnPort:(int)port withQuality:(StreamQuality)quality;
+
+/**
+ * Stop the streaming pipeline and destroy all components in reverse order.
+ */
+- (void)stopStreaming;
+
+/**
+ * Check whether the pipeline is currently active and streaming.
+ * @return YES if streaming, NO otherwise
+ */
+- (BOOL)isStreaming;
+
+/**
+ * Get the port the HTTP server is listening on.
+ * @return Port number, or 0 if not streaming
+ */
+- (int)port;
+
+/**
+ * Get the number of currently connected viewers.
+ * @return Active viewer/connection count
+ */
+- (int)viewerCount;
+
+/**
+ * Get the full stream URL including the local IP address and port.
+ * Uses the first non-loopback IPv4 address found on an active interface.
+ * @return URL string (e.g. "http://192.168.1.42:8080"), or nil if not streaming
+ */
+- (NSString *)streamURL;
+
+/**
+ * Change the streaming quality. If the pipeline is running, it will be
+ * restarted with the new quality settings.
+ * @param quality The new quality preset to apply
+ */
+- (void)setQuality:(StreamQuality)quality;
+
+/**
+ * Get the current quality preset.
+ * @return The active StreamQuality value
+ */
+- (StreamQuality)currentQuality;
+
+/**
+ * Display a QR code of the stream URL in a floating window.
+ * The window contains the QR code image and the URL text below it.
+ * Does nothing if the pipeline is not streaming.
+ */
+- (void)showQRCode;
+
+/**
+ * Get all local non-loopback IPv4 addresses from active network interfaces.
+ * @return Array of IP address strings
+ */
+- (NSArray *)localIPAddresses;
+
+@end
+
+#endif /* stream_manager_h */
diff --git a/pip/stream_manager.m b/pip/stream_manager.m
new file mode 100644
index 0000000..94cd76f
--- /dev/null
+++ b/pip/stream_manager.m
@@ -0,0 +1,739 @@
+/**
+ *  stream_manager.m
+ *  PiP
+ *
+ *  Streaming pipeline manager implementation.
+ *  Wires together: frame capture -> video encoder -> TS muxer -> HLS writer -> HTTP server.
+ *
+ *  The pipeline is driven by C callbacks that chain each stage:
+ *    1. frame_capture fires frame_capture_cb() on each captured frame
+ *    2. frame_capture_cb() feeds RGBA data into the video encoder
+ *    3. The encoder fires encoded_frame_cb() with H.264 NAL units
+ *    4. encoded_frame_cb() pushes NALs into the TS muxer
+ *    5. The muxer fires segment_cb() when a complete MPEG-TS segment is ready
+ *    6. segment_cb() stores the segment in the HLS writer ring buffer
+ *    7. The HTTP server serves the HLS playlist and segments on demand
+ */
+
+#import "stream_manager.h"
+#import "imageView.h"
+#import "frame_capture.h"
+#import "video_encoder.h"
+#import "ts_muxer.h"
+#import "hls_writer.h"
+#import "stream_server.h"
+
+#import 
+#import 
+
+#include 
+#include 
+#include 
+
+/* ------------------------------------------------------------------ */
+/* Embedded viewer resources via incbin                                */
+/* ------------------------------------------------------------------ */
+
+#define INCBIN_SILENCE_BITCODE_WARNING
+#include "incbin.h"
+
+INCBIN(viewer_html, "pip/viewer.html");
+INCBIN(hls_js, "pip/hls.min.js");
+
+/* ------------------------------------------------------------------ */
+/* Pipeline constants                                                  */
+/* ------------------------------------------------------------------ */
+
+#define HLS_MAX_SEGMENTS     15  /* ring buffer size for HLS writer (keeps extra for slow clients) */
+#define HLS_PLAYLIST_SIZE    6   /* segments to list in the .m3u8 playlist */
+#define SEGMENT_DURATION_SEC 2   /* target MPEG-TS segment duration */
+
+/* ------------------------------------------------------------------ */
+/* Quality preset parameters                                           */
+/* ------------------------------------------------------------------ */
+
+typedef struct {
+    int max_width;   /* maximum width (0 = native) */
+    int max_height;  /* maximum height (0 = native) */
+    int bitrate;     /* target bitrate in bits per second */
+    int fps;         /* target frame rate */
+} quality_params_t;
+
+static const quality_params_t quality_presets[] = {
+    [StreamQualityLow]    = { 1280, 720,  1500000, 24 },
+    [StreamQualityMedium] = { 1920, 1080, 3000000, 30 },
+    [StreamQualityHigh]   = { 0,    0,    6000000, 30 },
+};
+
+/* ------------------------------------------------------------------ */
+/* Pipeline context (passed as void *ctx to C callbacks)               */
+/* ------------------------------------------------------------------ */
+
+typedef struct {
+    video_encoder_t *encoder;
+    ts_muxer_t      *muxer;
+    hls_writer_t    *writer;
+    int              sps_pps_sent;    /* whether SPS/PPS has been set on the muxer */
+    int              expected_width;  /* encoder's configured frame width */
+    int              expected_height; /* encoder's configured frame height */
+} pipeline_ctx_t;
+
+/* ------------------------------------------------------------------ */
+/* Forward declarations for C callbacks                                */
+/* ------------------------------------------------------------------ */
+
+static void frame_capture_cb(uint8_t *rgba_data, int width, int height,
+                              int stride, uint64_t pts, void *ctx);
+static void encoded_frame_cb(uint8_t *data, int len, bool is_keyframe,
+                              uint8_t *sps, int sps_len,
+                              uint8_t *pps, int pps_len,
+                              uint64_t pts, void *ctx);
+static void segment_cb(void *context, uint8_t *segment_data,
+                        size_t segment_size, double duration,
+                        uint64_t segment_index);
+
+/* ------------------------------------------------------------------ */
+/* Private helper declarations                                         */
+/* ------------------------------------------------------------------ */
+
+static NSString *get_local_ip_address(void);
+static void compute_output_resolution(StreamQuality quality,
+                                       int source_width, int source_height,
+                                       int *out_width, int *out_height);
+
+/* ------------------------------------------------------------------ */
+/* StreamManager class extension (private ivars)                       */
+/* ------------------------------------------------------------------ */
+
+@interface StreamManager () {
+    ImageView        *_imageView;
+
+    /* Pipeline components */
+    frame_capture_t  *_capture;
+    video_encoder_t  *_encoder;
+    ts_muxer_t       *_muxer;
+    hls_writer_t     *_writer;
+    stream_server_t  *_server;
+
+    /* Pipeline callback context (heap-allocated, shared with C callbacks) */
+    pipeline_ctx_t   *_pipeline_ctx;
+
+    /* State */
+    BOOL              _streaming;
+    int               _port;
+    StreamQuality     _quality;
+}
+@end
+
+/* ------------------------------------------------------------------ */
+/* StreamManager implementation                                        */
+/* ------------------------------------------------------------------ */
+
+@implementation StreamManager
+
+/**
+ * Initialize the stream manager with an ImageView to capture from.
+ * @param imageView The source ImageView
+ * @return A new StreamManager instance
+ */
+- (instancetype)initWithImageView:(ImageView *)imageView
+{
+    self = [super init];
+    if (self) {
+        _imageView = imageView;
+        _capture = NULL;
+        _encoder = NULL;
+        _muxer = NULL;
+        _writer = NULL;
+        _server = NULL;
+        _pipeline_ctx = NULL;
+        _streaming = NO;
+        _port = 0;
+        _quality = StreamQualityMedium;
+    }
+    return self;
+} // end of function initWithImageView:
+
+/**
+ * Start the full streaming pipeline on the given port with the specified quality.
+ * @param port    TCP port for the HTTP server
+ * @param quality Quality preset
+ * @return YES on success, NO on failure
+ */
+- (BOOL)startStreamingOnPort:(int)port withQuality:(StreamQuality)quality
+{
+    if (_streaming) {
+        NSLog(@"stream_manager: already streaming, stop first");
+        return NO;
+    }
+
+    if (!_imageView) {
+        NSLog(@"stream_manager: no ImageView configured");
+        return NO;
+    }
+
+    _quality = quality;
+    _port = port;
+
+    /* Determine source dimensions, preferring the renderer's current frame size.
+       View bounds can be larger than the actual rendered CIImage (HiDPI/crop cases),
+       which would otherwise configure an encoder too large and drop every frame. */
+    int source_width = 0;
+    int source_height = 0;
+
+    CIImage *current_image = [_imageView.renderer currentImage];
+    if (current_image) {
+        CGRect extent = current_image.extent;
+        source_width = (int)extent.size.width;
+        source_height = (int)extent.size.height;
+    }
+
+    if (source_width <= 0 || source_height <= 0) {
+        NSSize view_size = _imageView.bounds.size;
+        source_width = (int)view_size.width;
+        source_height = (int)view_size.height;
+    }
+
+    if (source_width <= 0 || source_height <= 0) {
+        NSLog(@"stream_manager: invalid source dimensions %dx%d", source_width, source_height);
+        return NO;
+    }
+
+    /* Compute output resolution based on quality preset */
+    int output_width = 0;
+    int output_height = 0;
+    compute_output_resolution(quality, source_width, source_height,
+                              &output_width, &output_height);
+
+    const quality_params_t *params = &quality_presets[quality];
+
+    NSLog(@"stream_manager: starting pipeline %dx%d @ %d fps, %d kbps, port %d",
+          output_width, output_height, params->fps, params->bitrate / 1000, port);
+
+    /* --- 1. Create the HLS writer (ring buffer of segments) --- */
+    _writer = hls_writer_create(HLS_MAX_SEGMENTS, HLS_PLAYLIST_SIZE, SEGMENT_DURATION_SEC);
+    if (!_writer) {
+        NSLog(@"stream_manager: failed to create HLS writer");
+        [self stopStreaming];
+        return NO;
+    }
+
+    /* --- 2. Create the TS muxer (feeds segments into the HLS writer) --- */
+    _muxer = ts_muxer_create(SEGMENT_DURATION_SEC, segment_cb, _writer);
+    if (!_muxer) {
+        NSLog(@"stream_manager: failed to create TS muxer");
+        [self stopStreaming];
+        return NO;
+    }
+
+    /* --- 3. Create the video encoder --- */
+    _encoder = video_encoder_init(output_width, output_height,
+                                  params->fps, params->bitrate);
+    if (!_encoder) {
+        NSLog(@"stream_manager: failed to create video encoder");
+        [self stopStreaming];
+        return NO;
+    }
+
+    /* --- 4. Allocate and populate the pipeline context --- */
+    _pipeline_ctx = calloc(1, sizeof(pipeline_ctx_t));
+    if (!_pipeline_ctx) {
+        NSLog(@"stream_manager: failed to allocate pipeline context");
+        [self stopStreaming];
+        return NO;
+    }
+    _pipeline_ctx->encoder = _encoder;
+    _pipeline_ctx->muxer = _muxer;
+    _pipeline_ctx->writer = _writer;
+    _pipeline_ctx->sps_pps_sent = 0;
+    _pipeline_ctx->expected_width = output_width;
+    _pipeline_ctx->expected_height = output_height;
+
+    /* Wire the encoder callback -> TS muxer */
+    video_encoder_set_callback(_encoder, encoded_frame_cb, _pipeline_ctx);
+
+    /* --- 5. Create the frame capture (source is the ImageView) --- */
+    _capture = frame_capture_init((__bridge void *)_imageView);
+    if (!_capture) {
+        NSLog(@"stream_manager: failed to create frame capture");
+        [self stopStreaming];
+        return NO;
+    }
+
+    /* Wire the capture callback -> encoder */
+    frame_capture_set_callback(_capture, frame_capture_cb, _pipeline_ctx);
+
+    /* --- 6. Create and configure the HTTP server --- */
+    _server = stream_server_create(port);
+    if (!_server) {
+        NSLog(@"stream_manager: failed to create stream server");
+        [self stopStreaming];
+        return NO;
+    }
+
+    stream_server_set_hls_writer(_server, _writer);
+    stream_server_set_viewer_data(_server, gviewer_htmlData, gviewer_htmlSize);
+    stream_server_set_hlsjs_data(_server, ghls_jsData, ghls_jsSize);
+
+    if (stream_server_start(_server) != 0) {
+        NSLog(@"stream_manager: failed to start stream server on port %d", port);
+        [self stopStreaming];
+        return NO;
+    }
+
+    /* --- 7. Start capturing frames (this kicks off the whole pipeline) --- */
+    if (frame_capture_start(_capture, params->fps) != 0) {
+        NSLog(@"stream_manager: failed to start frame capture");
+        [self stopStreaming];
+        return NO;
+    }
+
+    _streaming = YES;
+
+    NSString *url = [self streamURL];
+    NSLog(@"stream_manager: pipeline started, stream available at %@", url ?: @"(unknown)");
+
+    return YES;
+} // end of function startStreamingOnPort:withQuality:
+
+/**
+ * Stop the streaming pipeline and destroy all components in reverse order.
+ */
+- (void)stopStreaming
+{
+    if (!_streaming && !_capture && !_encoder && !_muxer && !_writer && !_server) {
+        return;
+    }
+
+    NSLog(@"stream_manager: stopping pipeline");
+
+    /* Stop in reverse order: capture -> encoder -> muxer -> writer -> server */
+
+    /* 1. Stop and destroy frame capture (stops producing frames) */
+    if (_capture) {
+        frame_capture_stop(_capture);
+        frame_capture_destroy(_capture);
+        _capture = NULL;
+    }
+
+    /* 2. Destroy the video encoder (no more encoded frames after this) */
+    if (_encoder) {
+        video_encoder_destroy(_encoder);
+        _encoder = NULL;
+    }
+
+    /* 3. Flush and destroy the TS muxer (emit any partial segment) */
+    if (_muxer) {
+        ts_muxer_flush(_muxer);
+        ts_muxer_destroy(_muxer);
+        _muxer = NULL;
+    }
+
+    /* 4. Stop and destroy the HTTP server */
+    if (_server) {
+        stream_server_stop(_server);
+        stream_server_destroy(_server);
+        _server = NULL;
+    }
+
+    /* 5. Destroy the HLS writer (frees segment ring buffer) */
+    if (_writer) {
+        hls_writer_destroy(_writer);
+        _writer = NULL;
+    }
+
+    /* 6. Free the pipeline context */
+    if (_pipeline_ctx) {
+        free(_pipeline_ctx);
+        _pipeline_ctx = NULL;
+    }
+
+    _streaming = NO;
+
+    NSLog(@"stream_manager: pipeline stopped");
+} // end of function stopStreaming
+
+/**
+ * Check whether the pipeline is currently active.
+ * @return YES if streaming
+ */
+- (BOOL)isStreaming
+{
+    return _streaming;
+} // end of function isStreaming
+
+/**
+ * Get the port the HTTP server is listening on.
+ * @return Port number, or 0 if not streaming
+ */
+- (int)port
+{
+    if (_server) {
+        return stream_server_get_port(_server);
+    }
+    return 0;
+} // end of function port
+
+/**
+ * Get the number of currently connected viewers.
+ * @return Active connection count
+ */
+- (int)viewerCount
+{
+    if (_server) {
+        return stream_server_get_connection_count(_server);
+    }
+    return 0;
+} // end of function viewerCount
+
+/**
+ * Get the full stream URL including the local IP address and port.
+ * @return URL string, or nil if not streaming
+ */
+- (NSString *)streamURL
+{
+    if (!_streaming || !_server) {
+        return nil;
+    }
+
+    NSString *ip = get_local_ip_address();
+    if (!ip) {
+        ip = @"127.0.0.1";
+    }
+
+    int active_port = stream_server_get_port(_server);
+    return [NSString stringWithFormat:@"http://%@:%d", ip, active_port];
+} // end of function streamURL
+
+/**
+ * Change the streaming quality. Restarts the pipeline if currently streaming.
+ * @param quality The new quality preset
+ */
+- (void)setQuality:(StreamQuality)quality
+{
+    if (_quality == quality) {
+        return;
+    }
+
+    _quality = quality;
+
+    /* If currently streaming, restart with the new quality */
+    if (_streaming) {
+        int current_port = _port;
+        [self stopStreaming];
+        [self startStreamingOnPort:current_port withQuality:quality];
+    }
+} // end of function setQuality:
+
+/**
+ * Get the current quality preset.
+ * @return The active StreamQuality value
+ */
+- (StreamQuality)currentQuality
+{
+    return _quality;
+} // end of function currentQuality
+
+/**
+ * Generate a QR code NSImage from a string.
+ * Uses CIQRCodeGenerator filter from CoreImage.
+ * @param string The string to encode as a QR code
+ * @param size   The desired image size in points
+ * @return An NSImage of the QR code, or nil on failure
+ */
+static NSImage *generateQRCode(NSString *string, CGFloat size)
+{
+    CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
+    [filter setValue:[string dataUsingEncoding:NSUTF8StringEncoding] forKey:@"inputMessage"];
+    [filter setValue:@"M" forKey:@"inputCorrectionLevel"];
+
+    CIImage *ciImage = filter.outputImage;
+    if (!ciImage) return nil;
+
+    /* Scale up from the tiny QR to the desired size */
+    CGFloat scale = size / ciImage.extent.size.width;
+    CIImage *scaled = [ciImage imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
+
+    NSCIImageRep *rep = [NSCIImageRep imageRepWithCIImage:scaled];
+    NSImage *image = [[NSImage alloc] initWithSize:rep.size];
+    [image addRepresentation:rep];
+    return image;
+} // end of function generateQRCode()
+
+/**
+ * Show a floating window with the QR code for the stream URL.
+ * The window contains the QR code image and the URL text below it.
+ * Does nothing if the pipeline is not streaming.
+ */
+- (void)showQRCode
+{
+    if (!_streaming) return;
+
+    NSString *url = [self streamURL];
+    if (!url) return;
+
+    NSImage *qrImage = generateQRCode(url, 200);
+    if (!qrImage) return;
+
+    /* Create a floating panel */
+    NSPanel *panel = [[NSPanel alloc]
+        initWithContentRect:NSMakeRect(0, 0, 250, 280)
+        styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskNonactivatingPanel
+        backing:NSBackingStoreBuffered defer:YES];
+    [panel setTitle:@"Código QR"];
+    [panel setLevel:NSFloatingWindowLevel];
+    [panel center];
+
+    NSView *contentView = panel.contentView;
+
+    /* QR code image view */
+    NSImageView *imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(25, 50, 200, 200)];
+    [imageView setImage:qrImage];
+    [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
+    [contentView addSubview:imageView];
+
+    /* URL label */
+    NSTextField *label = [NSTextField wrappingLabelWithString:url];
+    [label setFrame:NSMakeRect(10, 10, 230, 30)];
+    [label setAlignment:NSTextAlignmentCenter];
+    [label setFont:[NSFont systemFontOfSize:11]];
+    [label setSelectable:YES];
+    [contentView addSubview:label];
+
+    [panel makeKeyAndOrderFront:nil];
+} // end of function showQRCode
+
+/**
+ * Get all local non-loopback IPv4 addresses from active network interfaces.
+ * @return Array of IP address strings
+ */
+- (NSArray *)localIPAddresses
+{
+    NSMutableArray *addresses = [[NSMutableArray alloc] init];
+    struct ifaddrs *interfaces = NULL;
+
+    if (getifaddrs(&interfaces) != 0) {
+        return addresses;
+    }
+
+    struct ifaddrs *cursor = interfaces;
+    while (cursor != NULL) {
+        if (cursor->ifa_addr->sa_family == AF_INET) {
+            unsigned int flags = cursor->ifa_flags;
+            if ((flags & IFF_UP) && !(flags & IFF_LOOPBACK)) {
+                struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr;
+                char addr_buf[INET_ADDRSTRLEN];
+                inet_ntop(AF_INET, &addr->sin_addr, addr_buf, sizeof(addr_buf));
+                [addresses addObject:[NSString stringWithUTF8String:addr_buf]];
+            }
+        } // end of block handling AF_INET addresses
+        cursor = cursor->ifa_next;
+    } // end of loop iterating over network interfaces
+
+    freeifaddrs(interfaces);
+    return addresses;
+} // end of function localIPAddresses
+
+/**
+ * Clean up all resources when the manager is deallocated.
+ */
+- (void)dealloc
+{
+    [self stopStreaming];
+} // end of function dealloc
+
+@end
+
+/* ================================================================== */
+/* C callback functions (static, pipeline glue)                        */
+/* ================================================================== */
+
+/**
+ * Frame capture callback. Called for each captured RGBA frame.
+ * Feeds the frame data into the video encoder.
+ * @param rgba_data Raw RGBA32 pixel data (freed after callback returns)
+ * @param width     Frame width in pixels
+ * @param height    Frame height in pixels
+ * @param stride    Bytes per row
+ * @param pts       Presentation timestamp in microseconds
+ * @param ctx       Pipeline context (pipeline_ctx_t *)
+ */
+static void
+frame_capture_cb(uint8_t *rgba_data, int width, int height,
+                 int stride, uint64_t pts, void *ctx)
+{
+    pipeline_ctx_t *pipeline = (pipeline_ctx_t *)ctx;
+    if (!pipeline || !pipeline->encoder) {
+        return;
+    }
+
+    /* Skip frames that are smaller than the encoder expects.
+       The encoder reads enc->width * enc->height pixels from the buffer,
+       so a smaller source would cause out-of-bounds reads. Larger sources
+       are safe (the encoder simply crops to its configured dimensions). */
+    if (width < pipeline->expected_width || height < pipeline->expected_height) {
+        return;
+    }
+
+    video_encoder_encode_frame(pipeline->encoder, rgba_data, stride, pts);
+} // end of function frame_capture_cb()
+
+/**
+ * Encoded frame callback. Called for each encoded H.264 access unit.
+ * Sets SPS/PPS on the muxer when available, then pushes the NAL data.
+ * @param data        Encoded H.264 data in AVCC format (freed after callback returns)
+ * @param len         Length of encoded data in bytes
+ * @param is_keyframe True if this is a keyframe (IDR)
+ * @param sps         SPS NAL unit data (may be NULL)
+ * @param sps_len     Length of SPS data
+ * @param pps         PPS NAL unit data (may be NULL)
+ * @param pps_len     Length of PPS data
+ * @param pts         Presentation timestamp in microseconds
+ * @param ctx         Pipeline context (pipeline_ctx_t *)
+ */
+static void
+encoded_frame_cb(uint8_t *data, int len, bool is_keyframe,
+                 uint8_t *sps, int sps_len,
+                 uint8_t *pps, int pps_len,
+                 uint64_t pts, void *ctx)
+{
+    pipeline_ctx_t *pipeline = (pipeline_ctx_t *)ctx;
+    if (!pipeline || !pipeline->muxer) {
+        return;
+    }
+
+    /* Update SPS/PPS on the muxer whenever new parameter sets arrive */
+    if (sps && sps_len > 0 && pps && pps_len > 0) {
+        ts_muxer_set_sps_pps(pipeline->muxer,
+                              sps, (size_t)sps_len,
+                              pps, (size_t)pps_len);
+        pipeline->sps_pps_sent = 1;
+    }
+
+    /* Only push data if we have SPS/PPS configured */
+    if (!pipeline->sps_pps_sent) {
+        return;
+    }
+
+    ts_muxer_push_h264(pipeline->muxer,
+                        data, (size_t)len,
+                        pts, is_keyframe ? 1 : 0);
+} // end of function encoded_frame_cb()
+
+/**
+ * TS segment callback. Called when a complete MPEG-TS segment is ready.
+ * Stores the segment in the HLS writer ring buffer.
+ * @param context       HLS writer instance (hls_writer_t *)
+ * @param segment_data  Segment data (copied by hls_writer_add_segment)
+ * @param segment_size  Size of segment data in bytes
+ * @param duration      Segment duration in seconds
+ * @param segment_index Zero-based segment index
+ */
+static void
+segment_cb(void *context, uint8_t *segment_data,
+           size_t segment_size, double duration,
+           uint64_t segment_index)
+{
+    hls_writer_t *writer = (hls_writer_t *)context;
+    if (!writer) {
+        return;
+    }
+
+    hls_writer_add_segment(writer, segment_data, segment_size,
+                           duration, segment_index);
+} // end of function segment_cb()
+
+/* ================================================================== */
+/* Private helper functions                                            */
+/* ================================================================== */
+
+/**
+ * Get the local IP address of the first active non-loopback IPv4 interface.
+ * Prefers en0/en1 (Wi-Fi/Ethernet) interfaces.
+ * @return IP address string, or nil if no suitable interface is found
+ */
+static NSString *
+get_local_ip_address(void)
+{
+    NSString *result = nil;
+    struct ifaddrs *interfaces = NULL;
+
+    if (getifaddrs(&interfaces) != 0) {
+        NSLog(@"stream_manager: getifaddrs() failed");
+        return nil;
+    }
+
+    struct ifaddrs *cursor = interfaces;
+    while (cursor != NULL) {
+        /* Only consider IPv4 addresses */
+        if (cursor->ifa_addr->sa_family != AF_INET) {
+            cursor = cursor->ifa_next;
+            continue;
+        }
+
+        /* Skip interfaces that are not up or are loopback */
+        unsigned int flags = cursor->ifa_flags;
+        if (!(flags & IFF_UP) || (flags & IFF_LOOPBACK)) {
+            cursor = cursor->ifa_next;
+            continue;
+        }
+
+        /* Prefer en0, en1, etc. (Wi-Fi and Ethernet) */
+        NSString *if_name = [NSString stringWithUTF8String:cursor->ifa_name];
+        if ([if_name hasPrefix:@"en"]) {
+            struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr;
+            char addr_buf[INET_ADDRSTRLEN];
+            inet_ntop(AF_INET, &addr->sin_addr, addr_buf, sizeof(addr_buf));
+            result = [NSString stringWithUTF8String:addr_buf];
+            break;
+        }
+
+        cursor = cursor->ifa_next;
+    } // end of loop iterating over network interfaces
+
+    freeifaddrs(interfaces);
+    return result;
+} // end of function get_local_ip_address()
+
+/**
+ * Compute the output resolution based on the quality preset and source dimensions.
+ * Clamps to the preset maximum while maintaining the source aspect ratio.
+ * For StreamQualityHigh, uses the native source resolution.
+ * @param quality       Quality preset
+ * @param source_width  Source width in pixels
+ * @param source_height Source height in pixels
+ * @param out_width     Output: computed width (always even for encoder compatibility)
+ * @param out_height    Output: computed height (always even for encoder compatibility)
+ */
+static void
+compute_output_resolution(StreamQuality quality,
+                          int source_width, int source_height,
+                          int *out_width, int *out_height)
+{
+    const quality_params_t *params = &quality_presets[quality];
+
+    int w = source_width;
+    int h = source_height;
+
+    /* If the preset has a maximum, clamp to it while maintaining aspect ratio */
+    if (params->max_width > 0 && params->max_height > 0) {
+        if (w > params->max_width || h > params->max_height) {
+            double scale_w = (double)params->max_width / (double)w;
+            double scale_h = (double)params->max_height / (double)h;
+            double scale = (scale_w < scale_h) ? scale_w : scale_h;
+            w = (int)(w * scale);
+            h = (int)(h * scale);
+        }
+    } // end of block clamping to preset maximum
+
+    /* Ensure dimensions are even (required by most video encoders) */
+    w = (w + 1) & ~1;
+    h = (h + 1) & ~1;
+
+    /* Ensure minimum dimensions */
+    if (w < 2) w = 2;
+    if (h < 2) h = 2;
+
+    *out_width = w;
+    *out_height = h;
+} // end of function compute_output_resolution()
diff --git a/pip/stream_server.h b/pip/stream_server.h
new file mode 100644
index 0000000..8df7814
--- /dev/null
+++ b/pip/stream_server.h
@@ -0,0 +1,105 @@
+/**
+ *  stream_server.h
+ *  PiP
+ *
+ *  Minimal GCD-based HTTP server for HLS streaming. Serves the live HLS
+ *  playlist and MPEG-TS segments from an hls_writer instance, along with
+ *  embedded viewer HTML and hls.js library. Uses dispatch sources for
+ *  non-blocking socket I/O on a dedicated dispatch queue.
+ */
+
+#ifndef STREAM_SERVER_H
+#define STREAM_SERVER_H
+
+#import 
+#include "hls_writer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct stream_server_s stream_server_t;
+
+/**
+ * Create a stream server instance bound to the given port.
+ * The server is not started until stream_server_start() is called.
+ * @param port Port number to listen on (use 8080 as default)
+ * @return New server instance, or NULL on allocation failure
+ */
+stream_server_t *stream_server_create(int port);
+
+/**
+ * Destroy a server instance and release all resources.
+ * Stops the server if it is still running.
+ * @param server The server instance (may be NULL)
+ */
+void stream_server_destroy(stream_server_t *server);
+
+/**
+ * Set the HLS writer to serve content from.
+ * The writer must remain valid for the lifetime of the server.
+ * @param server The server instance
+ * @param writer The HLS writer providing playlist and segment data
+ */
+void stream_server_set_hls_writer(stream_server_t *server, hls_writer_t *writer);
+
+/**
+ * Set the embedded viewer HTML data to serve at the root route.
+ * The data pointer must remain valid for the lifetime of the server.
+ * @param server The server instance
+ * @param data   Pointer to the HTML data (not copied, must stay valid)
+ * @param size   Size of the HTML data in bytes
+ */
+void stream_server_set_viewer_data(stream_server_t *server, const uint8_t *data, size_t size);
+
+/**
+ * Set the embedded hls.js library data to serve.
+ * The data pointer must remain valid for the lifetime of the server.
+ * @param server The server instance
+ * @param data   Pointer to the JavaScript data (not copied, must stay valid)
+ * @param size   Size of the JavaScript data in bytes
+ */
+void stream_server_set_hlsjs_data(stream_server_t *server, const uint8_t *data, size_t size);
+
+/**
+ * Start listening for incoming HTTP connections.
+ * Creates a TCP socket, binds to the configured port, and begins
+ * accepting connections on a dedicated GCD queue.
+ * @param server The server instance
+ * @return 0 on success, -1 on failure
+ */
+int stream_server_start(stream_server_t *server);
+
+/**
+ * Stop the server and close all active connections.
+ * The server can be restarted with stream_server_start().
+ * @param server The server instance
+ */
+void stream_server_stop(stream_server_t *server);
+
+/**
+ * Get the port the server is configured to listen on.
+ * @param server The server instance
+ * @return Port number
+ */
+int stream_server_get_port(stream_server_t *server);
+
+/**
+ * Get the number of currently active client connections.
+ * @param server The server instance
+ * @return Number of active connections
+ */
+int stream_server_get_connection_count(stream_server_t *server);
+
+/**
+ * Check whether the server is currently running and accepting connections.
+ * @param server The server instance
+ * @return Non-zero if running, 0 if stopped
+ */
+int stream_server_is_running(stream_server_t *server);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* STREAM_SERVER_H */
diff --git a/pip/stream_server.m b/pip/stream_server.m
new file mode 100644
index 0000000..7102c1e
--- /dev/null
+++ b/pip/stream_server.m
@@ -0,0 +1,752 @@
+/**
+ *  stream_server.m
+ *  PiP
+ *
+ *  Minimal GCD-based HTTP server for HLS streaming.
+ *  Uses dispatch sources for non-blocking socket I/O on a dedicated queue.
+ *  Serves the live HLS playlist, MPEG-TS segments, embedded viewer HTML,
+ *  embedded hls.js library, and a JSON status endpoint.
+ */
+
+#import "stream_server.h"
+#import 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+/* ------------------------------------------------------------------ */
+/* Constants                                                           */
+/* ------------------------------------------------------------------ */
+
+#define MAX_CONNECTIONS     20
+#define READ_BUFFER_SIZE    4096
+#define MAX_REQUEST_SIZE    8192
+
+/* ------------------------------------------------------------------ */
+/* Connection state structure                                          */
+/* ------------------------------------------------------------------ */
+
+typedef struct {
+    int                  fd;          /* client socket file descriptor */
+    dispatch_source_t    read_source; /* GCD read source for this connection */
+    char                 buffer[MAX_REQUEST_SIZE]; /* accumulated request data */
+    size_t               buffer_len;  /* bytes accumulated so far */
+    int                  active;      /* non-zero if this slot is in use */
+} connection_t;
+
+/* ------------------------------------------------------------------ */
+/* Server state structure                                              */
+/* ------------------------------------------------------------------ */
+
+struct stream_server_s {
+    int                  port;             /* configured port number */
+    int                  listen_fd;        /* listening socket fd (-1 if not listening) */
+    int                  running;          /* non-zero if server is running */
+
+    dispatch_queue_t     queue;            /* dedicated serial queue for server I/O */
+    dispatch_source_t    accept_source;    /* GCD source for accept events */
+
+    /* HLS content provider */
+    hls_writer_t        *hls_writer;
+
+    /* Embedded static content (not owned, caller must keep alive) */
+    const uint8_t       *viewer_data;
+    size_t               viewer_size;
+    const uint8_t       *hlsjs_data;
+    size_t               hlsjs_size;
+
+    /* Connection pool */
+    connection_t         connections[MAX_CONNECTIONS];
+    int                  connection_count;
+};
+
+/* ------------------------------------------------------------------ */
+/* Forward declarations (private helpers)                              */
+/* ------------------------------------------------------------------ */
+
+static void accept_connection(stream_server_t *server);
+static void handle_request_data(stream_server_t *server, connection_t *conn);
+static void process_http_request(stream_server_t *server, connection_t *conn, const char *method, const char *path);
+static void send_response(int fd, int status_code, const char *status_text,
+                          const char *content_type, const char *extra_headers,
+                          const uint8_t *body, size_t body_len,
+                          int head_only);
+static void send_text_response(int fd, int status_code, const char *status_text,
+                               const char *content_type, const char *extra_headers,
+                               const char *body, int head_only);
+static int write_all(int fd, const uint8_t *data, size_t len);
+static void close_connection(stream_server_t *server, connection_t *conn);
+static connection_t *find_free_slot(stream_server_t *server);
+
+/* ------------------------------------------------------------------ */
+/* Public API                                                          */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Create a stream server instance bound to the given port.
+ * @param port Port number to listen on (use 8080 as default)
+ * @return New server instance, or NULL on allocation failure
+ */
+stream_server_t *
+stream_server_create(int port)
+{
+    stream_server_t *server = calloc(1, sizeof(stream_server_t));
+    if (!server) {
+        return NULL;
+    }
+
+    server->port = port > 0 ? port : 8080;
+    server->listen_fd = -1;
+    server->running = 0;
+    server->hls_writer = NULL;
+    server->viewer_data = NULL;
+    server->viewer_size = 0;
+    server->hlsjs_data = NULL;
+    server->hlsjs_size = 0;
+    server->connection_count = 0;
+
+    /* Initialize connection pool */
+    for (int i = 0; i < MAX_CONNECTIONS; i++) {
+        server->connections[i].fd = -1;
+        server->connections[i].read_source = NULL;
+        server->connections[i].buffer_len = 0;
+        server->connections[i].active = 0;
+    } // end of loop initializing connection pool
+
+    server->queue = dispatch_queue_create("com.pip.stream_server", DISPATCH_QUEUE_SERIAL);
+
+    return server;
+} // end of function stream_server_create()
+
+/**
+ * Destroy a server instance and release all resources.
+ * @param server The server instance (may be NULL)
+ */
+void
+stream_server_destroy(stream_server_t *server)
+{
+    if (!server) {
+        return;
+    }
+
+    stream_server_stop(server);
+
+    /* The queue is ARC-managed in ObjC, no explicit release needed */
+    server->queue = nil;
+
+    free(server);
+} // end of function stream_server_destroy()
+
+/**
+ * Set the HLS writer to serve content from.
+ * @param server The server instance
+ * @param writer The HLS writer providing playlist and segment data
+ */
+void
+stream_server_set_hls_writer(stream_server_t *server, hls_writer_t *writer)
+{
+    if (server) {
+        server->hls_writer = writer;
+    }
+} // end of function stream_server_set_hls_writer()
+
+/**
+ * Set the embedded viewer HTML data to serve at the root route.
+ * @param server The server instance
+ * @param data   Pointer to the HTML data
+ * @param size   Size of the HTML data in bytes
+ */
+void
+stream_server_set_viewer_data(stream_server_t *server, const uint8_t *data, size_t size)
+{
+    if (server) {
+        server->viewer_data = data;
+        server->viewer_size = size;
+    }
+} // end of function stream_server_set_viewer_data()
+
+/**
+ * Set the embedded hls.js library data to serve.
+ * @param server The server instance
+ * @param data   Pointer to the JavaScript data
+ * @param size   Size of the JavaScript data in bytes
+ */
+void
+stream_server_set_hlsjs_data(stream_server_t *server, const uint8_t *data, size_t size)
+{
+    if (server) {
+        server->hlsjs_data = data;
+        server->hlsjs_size = size;
+    }
+} // end of function stream_server_set_hlsjs_data()
+
+/**
+ * Start listening for incoming HTTP connections.
+ * @param server The server instance
+ * @return 0 on success, -1 on failure
+ */
+int
+stream_server_start(stream_server_t *server)
+{
+    if (!server || server->running) {
+        return -1;
+    }
+
+    /* Create the listening socket */
+    int fd = socket(AF_INET, SOCK_STREAM, 0);
+    if (fd < 0) {
+        NSLog(@"stream_server: failed to create socket");
+        return -1;
+    }
+
+    /* Allow address reuse to avoid "address already in use" on restart */
+    int reuse = 1;
+    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
+
+    /* Bind to all interfaces on the configured port */
+    struct sockaddr_in addr;
+    memset(&addr, 0, sizeof(addr));
+    addr.sin_family = AF_INET;
+    addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    addr.sin_port = htons((uint16_t)server->port);
+
+    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+        NSLog(@"stream_server: failed to bind to port %d", server->port);
+        close(fd);
+        return -1;
+    }
+
+    /* Start listening with a backlog matching max connections */
+    if (listen(fd, MAX_CONNECTIONS) < 0) {
+        NSLog(@"stream_server: failed to listen on port %d", server->port);
+        close(fd);
+        return -1;
+    }
+
+    /* Make listening socket non-blocking so accept() can be drained safely. */
+    int listen_flags = fcntl(fd, F_GETFL, 0);
+    if (listen_flags < 0 || fcntl(fd, F_SETFL, listen_flags | O_NONBLOCK) < 0) {
+        NSLog(@"stream_server: failed to set listen socket non-blocking");
+        close(fd);
+        return -1;
+    }
+
+    server->listen_fd = fd;
+    server->running = 1;
+
+    /* Create a GCD dispatch source to handle incoming connections */
+    server->accept_source = dispatch_source_create(
+        DISPATCH_SOURCE_TYPE_READ,
+        (uintptr_t)fd,
+        0,
+        server->queue
+    );
+
+    if (!server->accept_source) {
+        NSLog(@"stream_server: failed to create accept dispatch source");
+        close(fd);
+        server->listen_fd = -1;
+        server->running = 0;
+        return -1;
+    }
+
+    /* Capture server pointer for the event handler block */
+    stream_server_t *srv = server;
+
+    dispatch_source_set_event_handler(server->accept_source, ^{
+        accept_connection(srv);
+    });
+
+    dispatch_source_set_cancel_handler(server->accept_source, ^{
+        close(fd);
+    });
+
+    dispatch_resume(server->accept_source);
+
+    NSLog(@"stream_server: listening on port %d", server->port);
+    return 0;
+} // end of function stream_server_start()
+
+/**
+ * Stop the server and close all active connections.
+ * @param server The server instance
+ */
+void
+stream_server_stop(stream_server_t *server)
+{
+    if (!server || !server->running) {
+        return;
+    }
+
+    server->running = 0;
+
+    /* Cancel the accept source (its cancel handler will close listen_fd) */
+    if (server->accept_source) {
+        dispatch_source_cancel(server->accept_source);
+        server->accept_source = nil;
+    }
+
+    server->listen_fd = -1;
+
+    /* Close all active connections */
+    for (int i = 0; i < MAX_CONNECTIONS; i++) {
+        if (server->connections[i].active) {
+            close_connection(server, &server->connections[i]);
+        }
+    } // end of loop closing all active connections
+
+    NSLog(@"stream_server: stopped");
+} // end of function stream_server_stop()
+
+/**
+ * Get the port the server is configured to listen on.
+ * @param server The server instance
+ * @return Port number
+ */
+int
+stream_server_get_port(stream_server_t *server)
+{
+    return server ? server->port : 0;
+} // end of function stream_server_get_port()
+
+/**
+ * Get the number of currently active client connections.
+ * @param server The server instance
+ * @return Number of active connections
+ */
+int
+stream_server_get_connection_count(stream_server_t *server)
+{
+    return server ? server->connection_count : 0;
+} // end of function stream_server_get_connection_count()
+
+/**
+ * Check whether the server is currently running.
+ * @param server The server instance
+ * @return Non-zero if running, 0 if stopped
+ */
+int
+stream_server_is_running(stream_server_t *server)
+{
+    return server ? server->running : 0;
+} // end of function stream_server_is_running()
+
+/* ------------------------------------------------------------------ */
+/* Private helpers                                                     */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Accept a new incoming connection and set up a GCD read source for it.
+ * Called on the server's dispatch queue when the listen socket is readable.
+ * @param server The server instance
+ */
+static void
+accept_connection(stream_server_t *server)
+{
+    for (;;) {
+        struct sockaddr_in client_addr;
+        socklen_t addr_len = sizeof(client_addr);
+
+        int client_fd = accept(server->listen_fd, (struct sockaddr *)&client_addr, &addr_len);
+        if (client_fd < 0) {
+            if (errno == EAGAIN || errno == EWOULDBLOCK) {
+                /* Backlog drained for this accept event. */
+                break;
+            }
+            if (errno == EINTR) {
+                continue;
+            }
+            NSLog(@"stream_server: accept() failed: %d", errno);
+            break;
+        }
+
+        /* Keep accepted sockets in blocking mode for simple write-all semantics.
+           listen_fd is non-blocking so it can be drained safely in this loop. */
+        int client_flags = fcntl(client_fd, F_GETFL, 0);
+        if (client_flags >= 0 && (client_flags & O_NONBLOCK)) {
+            if (fcntl(client_fd, F_SETFL, client_flags & ~O_NONBLOCK) < 0) {
+                NSLog(@"stream_server: failed to clear O_NONBLOCK on fd=%d", client_fd);
+                close(client_fd);
+                continue;
+            }
+        }
+
+        /* Find a free connection slot */
+        connection_t *conn = find_free_slot(server);
+        if (!conn) {
+            NSLog(@"stream_server: max connections reached, rejecting client");
+            close(client_fd);
+            continue;
+        }
+
+        /* Initialize the connection */
+        conn->fd = client_fd;
+        conn->buffer_len = 0;
+        conn->active = 1;
+        server->connection_count++;
+
+        char *client_ip = inet_ntoa(client_addr.sin_addr);
+        int client_port = ntohs(client_addr.sin_port);
+        NSLog(@"stream_server: accepted connection from %s:%d (fd=%d, active=%d)",
+              client_ip, client_port, client_fd, server->connection_count);
+
+        /* Create a GCD read source for this connection */
+        conn->read_source = dispatch_source_create(
+            DISPATCH_SOURCE_TYPE_READ,
+            (uintptr_t)client_fd,
+            0,
+            server->queue
+        );
+
+        if (!conn->read_source) {
+            NSLog(@"stream_server: failed to create read source for fd=%d", client_fd);
+            close(client_fd);
+            conn->fd = -1;
+            conn->active = 0;
+            server->connection_count--;
+            continue;
+        }
+
+        /* Capture pointers for the block.
+           IMPORTANT: capture client_fd by VALUE for the cancel handler, because
+           the connection slot may be reused before the cancel handler runs.
+           If we used c->fd, the cancel handler could close the WRONG fd. */
+        stream_server_t *srv = server;
+        connection_t *c = conn;
+        int fd_to_close = client_fd;
+
+        dispatch_source_set_event_handler(conn->read_source, ^{
+            handle_request_data(srv, c);
+        });
+
+        dispatch_source_set_cancel_handler(conn->read_source, ^{
+            close(fd_to_close);
+        });
+
+        dispatch_resume(conn->read_source);
+    }
+} // end of function accept_connection()
+
+/**
+ * Handle incoming data on a client connection.
+ * Accumulates data in the connection buffer until a complete HTTP request
+ * is detected (double CRLF), then dispatches to process_http_request().
+ * @param server The server instance
+ * @param conn   The connection that has data available
+ */
+static void
+handle_request_data(stream_server_t *server, connection_t *conn)
+{
+    if (!conn->active) {
+        return;
+    }
+
+    char temp_buf[READ_BUFFER_SIZE];
+    ssize_t bytes_read = read(conn->fd, temp_buf, sizeof(temp_buf));
+
+    if (bytes_read <= 0) {
+        /* Connection closed or error */
+        close_connection(server, conn);
+        return;
+    }
+
+    /* Append to connection buffer, respecting max size */
+    size_t space = MAX_REQUEST_SIZE - conn->buffer_len - 1;
+    size_t to_copy = (size_t)bytes_read < space ? (size_t)bytes_read : space;
+    memcpy(conn->buffer + conn->buffer_len, temp_buf, to_copy);
+    conn->buffer_len += to_copy;
+    conn->buffer[conn->buffer_len] = '\0';
+
+    /* Check for end of HTTP headers (double CRLF) */
+    if (strstr(conn->buffer, "\r\n\r\n") || strstr(conn->buffer, "\n\n")) {
+        /* Parse the request line: "METHOD /path HTTP/1.x" */
+        char method[16] = {0};
+        char path[1024] = {0};
+
+        if (sscanf(conn->buffer, "%15s %1023s", method, path) == 2) {
+            process_http_request(server, conn, method, path);
+        } else {
+            send_text_response(conn->fd, 400, "Bad Request",
+                               "text/plain", NULL, "Bad Request\n", 0);
+        }
+
+        /* Close connection after response (HTTP/1.0 style) */
+        close_connection(server, conn);
+    }
+} // end of function handle_request_data()
+
+/**
+ * Process a parsed HTTP request and send the appropriate response.
+ * Routes: / (viewer), /stream.m3u8 (playlist), /segment_N.ts (segments),
+ *         /hls.min.js (library), /status (JSON status).
+ * @param server The server instance
+ * @param conn   The client connection
+ * @param method The HTTP method (e.g. "GET")
+ * @param path   The requested URL path
+ */
+static void
+process_http_request(stream_server_t *server, connection_t *conn, const char *method, const char *path)
+{
+    /* Cache-control headers for live HLS: prevent browsers from caching the
+       playlist or error responses, which would cause playback to stall. */
+    static const char *nocache = "Cache-Control: no-cache, no-store, must-revalidate\r\nPragma: no-cache\r\n";
+
+    /* Only support GET and HEAD requests */
+    int is_head = (strcmp(method, "HEAD") == 0);
+    if (strcmp(method, "GET") != 0 && !is_head) {
+        send_text_response(conn->fd, 405, "Method Not Allowed",
+                           "text/plain", NULL, "Method Not Allowed\n", 0);
+        return;
+    }
+
+    /* Route: GET / - Serve embedded viewer HTML */
+    if (strcmp(path, "/") == 0) {
+        if (server->viewer_data && server->viewer_size > 0) {
+            send_response(conn->fd, 200, "OK",
+                          "text/html; charset=utf-8", NULL,
+                          server->viewer_data, server->viewer_size, is_head);
+        } else {
+            send_text_response(conn->fd, 503, "Service Unavailable",
+                               "text/plain", NULL, "Viewer not configured\n", is_head);
+        }
+        return;
+    }
+
+    /* Route: GET /stream.m3u8 - Serve live HLS playlist (must NOT be cached) */
+    if (strcmp(path, "/stream.m3u8") == 0) {
+        if (!server->hls_writer) {
+            send_text_response(conn->fd, 503, "Service Unavailable",
+                               "text/plain", nocache, "No HLS writer configured\n", is_head);
+            return;
+        }
+
+        char *playlist = hls_writer_get_playlist(server->hls_writer);
+        if (!playlist) {
+            send_text_response(conn->fd, 503, "Service Unavailable",
+                               "text/plain", nocache, "No segments available yet\n", is_head);
+            return;
+        }
+
+        send_text_response(conn->fd, 200, "OK",
+                           "application/vnd.apple.mpegurl", nocache, playlist, is_head);
+        free(playlist);
+        return;
+    } // end of route /stream.m3u8
+
+    /* Route: GET /segment_N.ts - Serve individual MPEG-TS segments */
+    uint64_t segment_index = 0;
+    if (sscanf(path, "/segment_%llu.ts", &segment_index) == 1) {
+        if (!server->hls_writer) {
+            send_text_response(conn->fd, 503, "Service Unavailable",
+                               "text/plain", nocache, "No HLS writer configured\n", is_head);
+            return;
+        }
+
+        uint8_t *seg_data = NULL;
+        size_t seg_size = 0;
+
+        if (hls_writer_get_segment(server->hls_writer, segment_index, &seg_data, &seg_size) == 0) {
+            /* hls_writer_get_segment now returns a malloc'd copy of the data,
+               safe to use without worrying about ring buffer eviction. */
+            send_response(conn->fd, 200, "OK",
+                          "video/mp2t", NULL, seg_data, seg_size, is_head);
+            free(seg_data);
+        } else {
+            send_text_response(conn->fd, 404, "Not Found",
+                               "text/plain", nocache, "Segment not found\n", is_head);
+        }
+        return;
+    } // end of route /segment_N.ts
+
+    /* Route: GET /hls.min.js - Serve embedded hls.js library */
+    if (strcmp(path, "/hls.min.js") == 0) {
+        if (server->hlsjs_data && server->hlsjs_size > 0) {
+            send_response(conn->fd, 200, "OK",
+                          "application/javascript", NULL,
+                          server->hlsjs_data, server->hlsjs_size, is_head);
+        } else {
+            send_text_response(conn->fd, 503, "Service Unavailable",
+                               "text/plain", NULL, "hls.js not configured\n", is_head);
+        }
+        return;
+    }
+
+    /* Route: GET /status - Stream status JSON */
+    if (strcmp(path, "/status") == 0) {
+        int is_streaming = (server->hls_writer != NULL) ? 1 : 0;
+        int seg_count = 0;
+        if (server->hls_writer) {
+            seg_count = hls_writer_segment_count(server->hls_writer);
+            if (seg_count > 0) {
+                is_streaming = 1;
+            } else {
+                is_streaming = 0;
+            }
+        }
+
+        char json_buf[256];
+        snprintf(json_buf, sizeof(json_buf),
+                 "{\"streaming\": %s, \"segments\": %d, \"port\": %d}",
+                 is_streaming ? "true" : "false",
+                 seg_count,
+                 server->port);
+
+        send_text_response(conn->fd, 200, "OK",
+                           "application/json", nocache, json_buf, is_head);
+        return;
+    } // end of route /status
+
+    /* No route matched */
+    send_text_response(conn->fd, 404, "Not Found",
+                       "text/plain", NULL, "Not Found\n", is_head);
+} // end of function process_http_request()
+
+/**
+ * Send an HTTP response with binary body data.
+ * Includes Content-Type, Content-Length, Connection: close, and CORS headers.
+ * @param fd            Client socket file descriptor
+ * @param status_code   HTTP status code (e.g. 200)
+ * @param status_text   HTTP status text (e.g. "OK")
+ * @param content_type  Content-Type header value
+ * @param extra_headers Additional HTTP headers (NULL for none, must end with \r\n)
+ * @param body          Response body data
+ * @param body_len      Length of response body in bytes
+ * @param head_only     If non-zero, send only headers (for HEAD requests)
+ */
+static void
+send_response(int fd, int status_code, const char *status_text,
+              const char *content_type, const char *extra_headers,
+              const uint8_t *body, size_t body_len,
+              int head_only)
+{
+    /* Build the HTTP response header */
+    char header[1024];
+    int header_len = snprintf(header, sizeof(header),
+        "HTTP/1.1 %d %s\r\n"
+        "Content-Type: %s\r\n"
+        "Content-Length: %zu\r\n"
+        "Connection: close\r\n"
+        "Access-Control-Allow-Origin: *\r\n"
+        "%s"
+        "\r\n",
+        status_code, status_text,
+        content_type,
+        body_len,
+        extra_headers ? extra_headers : "");
+
+    /* Send header, then body (skip body for HEAD requests). */
+    if (write_all(fd, (const uint8_t *)header, (size_t)header_len) != 0) {
+        return;
+    }
+
+    if (!head_only && body && body_len > 0) {
+        (void)write_all(fd, body, body_len);
+    }
+} // end of function send_response()
+
+/**
+ * Write the full buffer to a socket.
+ * Retries short writes and transient EINTR/EAGAIN failures.
+ * @return 0 on success, -1 on write failure
+ */
+static int
+write_all(int fd, const uint8_t *data, size_t len)
+{
+    size_t total_written = 0;
+    while (total_written < len) {
+        ssize_t written = write(fd, data + total_written, len - total_written);
+        if (written > 0) {
+            total_written += (size_t)written;
+            continue;
+        }
+        if (written < 0 && errno == EINTR) {
+            continue;
+        }
+        if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+            usleep(1000);
+            continue;
+        }
+        return -1;
+    }
+    return 0;
+}
+
+/**
+ * Send an HTTP response with a C string body (convenience wrapper).
+ * @param fd            Client socket file descriptor
+ * @param status_code   HTTP status code
+ * @param status_text   HTTP status text
+ * @param content_type  Content-Type header value
+ * @param extra_headers Additional HTTP headers (NULL for none)
+ * @param body          Null-terminated response body string
+ * @param head_only     If non-zero, send only headers (for HEAD requests)
+ */
+static void
+send_text_response(int fd, int status_code, const char *status_text,
+                   const char *content_type, const char *extra_headers,
+                   const char *body, int head_only)
+{
+    send_response(fd, status_code, status_text,
+                  content_type, extra_headers,
+                  (const uint8_t *)body,
+                  body ? strlen(body) : 0,
+                  head_only);
+} // end of function send_text_response()
+
+/**
+ * Close a client connection and free its resources.
+ * Cancels the GCD read source and marks the connection slot as available.
+ * @param server The server instance
+ * @param conn   The connection to close
+ */
+static void
+close_connection(stream_server_t *server, connection_t *conn)
+{
+    if (!conn || !conn->active) {
+        return;
+    }
+
+    conn->active = 0;
+
+    /* Save and invalidate the fd BEFORE cancelling, so that if the slot
+       is reused before the cancel handler runs, the new fd won't be corrupted.
+       The cancel handler captures the fd by VALUE and will close the right one. */
+    int saved_fd = conn->fd;
+    conn->fd = -1;
+
+    if (conn->read_source) {
+        dispatch_source_cancel(conn->read_source);
+        conn->read_source = nil;
+        /* fd will be closed by the cancel handler (which captured it by value) */
+    } else if (saved_fd >= 0) {
+        /* No read_source (error path): close fd directly */
+        close(saved_fd);
+    }
+
+    conn->buffer_len = 0;
+
+    if (server->connection_count > 0) {
+        server->connection_count--;
+    }
+} // end of function close_connection()
+
+/**
+ * Find a free connection slot in the server's connection pool.
+ * @param server The server instance
+ * @return Pointer to a free connection_t slot, or NULL if all slots are in use
+ */
+static connection_t *
+find_free_slot(stream_server_t *server)
+{
+    for (int i = 0; i < MAX_CONNECTIONS; i++) {
+        if (!server->connections[i].active) {
+            return &server->connections[i];
+        }
+    } // end of loop searching for free connection slot
+    return NULL;
+} // end of function find_free_slot()
diff --git a/pip/ts_muxer.c b/pip/ts_muxer.c
new file mode 100644
index 0000000..d0619de
--- /dev/null
+++ b/pip/ts_muxer.c
@@ -0,0 +1,918 @@
+/**
+ *  ts_muxer.c
+ *  PiP
+ *
+ *  MPEG-TS muxer implementation. Converts H.264 NAL units (AVCC format)
+ *  into MPEG-TS segments for HLS streaming.
+ *
+ *  MPEG-TS overview:
+ *  - Fixed 188-byte packets, each starting with sync byte 0x47
+ *  - PAT (PID 0x0000) maps programs to PMT PIDs
+ *  - PMT (PID 0x1000) describes elementary streams in a program
+ *  - PES packets on PID 0x0100 carry H.264 video data
+ *  - Each PID has an independent 4-bit continuity counter (wraps at 16)
+ */
+
+#include "ts_muxer.h"
+#include 
+#include 
+#include 
+#include 
+
+/* ------------------------------------------------------------------ */
+/* Constants                                                           */
+/* ------------------------------------------------------------------ */
+
+#define TS_PACKET_SIZE      188
+#define TS_SYNC_BYTE        0x47
+
+#define PID_PAT             0x0000
+#define PID_PMT             0x1000
+#define PID_VIDEO           0x0100
+
+#define STREAM_TYPE_H264    0x1B
+#define STREAM_ID_VIDEO     0xE0
+
+#define INITIAL_BUFFER_SIZE (256 * 1024)  /* 256 KB */
+#define PCR_INTERVAL_90KHZ 3600          /* 40ms in 90kHz ticks */
+
+/* ------------------------------------------------------------------ */
+/* CRC32/MPEG2 lookup table                                            */
+/* ------------------------------------------------------------------ */
+
+static uint32_t crc32_table[256];
+static pthread_once_t crc32_once = PTHREAD_ONCE_INIT;
+
+/**
+ * Initialize the CRC32/MPEG2 lookup table.
+ * MPEG-2 CRC uses polynomial 0x04C11DB7 with no final inversion.
+ * Thread-safe via pthread_once.
+ */
+static void
+crc32_init_table(void)
+{
+    for (int i = 0; i < 256; i++) {
+        uint32_t crc = (uint32_t)i << 24;
+        for (int j = 0; j < 8; j++) {
+            if (crc & 0x80000000) {
+                crc = (crc << 1) ^ 0x04C11DB7;
+            } else {
+                crc = crc << 1;
+            }
+        } // end of bit loop (j)
+        crc32_table[i] = crc;
+    } // end of byte loop (i)
+} // end of function crc32_init_table()
+
+/**
+ * Compute CRC32/MPEG2 over a block of data.
+ * @param data   Pointer to the data
+ * @param length Number of bytes
+ * @return CRC32 value
+ */
+static uint32_t
+crc32_mpeg2(const uint8_t *data, size_t length)
+{
+    pthread_once(&crc32_once, crc32_init_table);
+
+    uint32_t crc = 0xFFFFFFFF;
+    for (size_t i = 0; i < length; i++) {
+        crc = (crc << 8) ^ crc32_table[((crc >> 24) ^ data[i]) & 0xFF];
+    }
+    return crc;
+} // end of function crc32_mpeg2()
+
+/* ------------------------------------------------------------------ */
+/* Muxer state structure                                               */
+/* ------------------------------------------------------------------ */
+
+struct ts_muxer_s {
+    /* Segment configuration */
+    int segment_duration_seconds;
+    ts_segment_callback_t callback;
+    void *callback_ctx;
+
+    /* SPS/PPS parameter sets (stored copies, without start codes) */
+    uint8_t *sps;
+    size_t sps_size;
+    uint8_t *pps;
+    size_t pps_size;
+
+    /* Segment buffer (accumulates TS packets) */
+    uint8_t *segment_buf;
+    size_t segment_buf_size;     /* allocated size */
+    size_t segment_buf_used;     /* bytes written */
+
+    /* Timing */
+    uint64_t segment_start_pts;  /* PTS of first frame in current segment (us) */
+    uint64_t segment_last_pts;   /* PTS of last frame pushed (us) */
+    int segment_has_data;        /* non-zero if segment_buf has any video data */
+
+    /* Segment index counter */
+    uint64_t segment_index;
+
+    /* Continuity counters (4-bit, per PID) */
+    uint8_t cc_pat;
+    uint8_t cc_pmt;
+    uint8_t cc_video;
+
+    /* Startup/resync state: drop frames until first IDR */
+    int waiting_for_keyframe;
+
+    /* PCR cadence: next deadline in 90kHz ticks for PCR insertion */
+    uint64_t next_pcr_90khz;
+};
+
+/* ------------------------------------------------------------------ */
+/* Internal helpers: buffer management                                 */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Ensure the segment buffer has room for at least `needed` more bytes.
+ * Grows the buffer by 2x when capacity is insufficient.
+ * @param muxer  The muxer instance
+ * @param needed Number of additional bytes required
+ * @return 0 on success, -1 on allocation failure
+ */
+static int
+ensure_buffer_space(ts_muxer_t *muxer, size_t needed)
+{
+    if (muxer->segment_buf_used + needed <= muxer->segment_buf_size) {
+        return 0;
+    }
+
+    size_t new_size = muxer->segment_buf_size;
+    while (new_size < muxer->segment_buf_used + needed) {
+        new_size *= 2;
+    }
+
+    uint8_t *new_buf = realloc(muxer->segment_buf, new_size);
+    if (!new_buf) {
+        return -1;
+    }
+
+    muxer->segment_buf = new_buf;
+    muxer->segment_buf_size = new_size;
+    return 0;
+} // end of function ensure_buffer_space()
+
+/**
+ * Append a complete 188-byte TS packet to the segment buffer.
+ * @param muxer  The muxer instance
+ * @param packet Pointer to a 188-byte TS packet
+ * @return 0 on success, -1 on allocation failure
+ */
+static int
+append_ts_packet(ts_muxer_t *muxer, const uint8_t *packet)
+{
+    if (ensure_buffer_space(muxer, TS_PACKET_SIZE) != 0) {
+        return -1;
+    }
+    memcpy(muxer->segment_buf + muxer->segment_buf_used, packet, TS_PACKET_SIZE);
+    muxer->segment_buf_used += TS_PACKET_SIZE;
+    return 0;
+} // end of function append_ts_packet()
+
+/* ------------------------------------------------------------------ */
+/* Internal helpers: TS packet construction                            */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Write a PAT (Program Association Table) as a single TS packet.
+ *
+ * PAT structure (after TS header + pointer_field):
+ *   table_id (8)         = 0x00
+ *   section_syntax (1)   = 1
+ *   '0' (1)              = 0
+ *   reserved (2)         = 0x3
+ *   section_length (12)  = 13 (5 header + 4 program + 4 CRC)
+ *   transport_stream_id (16) = 0x0001
+ *   reserved (2)         = 0x3
+ *   version (5)          = 0
+ *   current_next (1)     = 1
+ *   section_number (8)   = 0
+ *   last_section (8)     = 0
+ *   program_number (16)  = 0x0001
+ *   reserved (3)         = 0x7
+ *   program_map_PID (13) = 0x1000
+ *   CRC32 (32)
+ *
+ * @param muxer The muxer instance
+ * @return 0 on success, -1 on failure
+ */
+static int
+write_pat(ts_muxer_t *muxer)
+{
+    uint8_t packet[TS_PACKET_SIZE];
+    memset(packet, 0xFF, TS_PACKET_SIZE);
+
+    /* TS header (4 bytes) */
+    packet[0] = TS_SYNC_BYTE;
+    packet[1] = 0x40 | ((PID_PAT >> 8) & 0x1F);  /* PUSI=1, PID high */
+    packet[2] = PID_PAT & 0xFF;                    /* PID low */
+    packet[3] = 0x10 | (muxer->cc_pat & 0x0F);    /* no adaptation, payload only */
+    muxer->cc_pat = (muxer->cc_pat + 1) & 0x0F;
+
+    /* Pointer field (1 byte) */
+    packet[4] = 0x00;
+
+    /* PAT section data */
+    int section_start = 5;
+    uint8_t *p = &packet[section_start];
+
+    p[0] = 0x00;  /* table_id */
+    /* section_syntax_indicator=1, '0'=0, reserved=11, section_length=13 */
+    p[1] = 0xB0;
+    p[2] = 13;    /* section_length: 5 (after length) + 4 (program) + 4 (CRC) */
+    p[3] = 0x00;  /* transport_stream_id high */
+    p[4] = 0x01;  /* transport_stream_id low */
+    /* reserved=11, version=00000, current_next=1 */
+    p[5] = 0xC1;
+    p[6] = 0x00;  /* section_number */
+    p[7] = 0x00;  /* last_section_number */
+    /* program_number = 1 */
+    p[8] = 0x00;
+    p[9] = 0x01;
+    /* reserved=111, program_map_PID = 0x1000 */
+    p[10] = 0xE0 | ((PID_PMT >> 8) & 0x1F);
+    p[11] = PID_PMT & 0xFF;
+
+    /* CRC32 over section data (from table_id to just before CRC) */
+    uint32_t crc = crc32_mpeg2(p, 12);
+    p[12] = (crc >> 24) & 0xFF;
+    p[13] = (crc >> 16) & 0xFF;
+    p[14] = (crc >> 8) & 0xFF;
+    p[15] = crc & 0xFF;
+
+    return append_ts_packet(muxer, packet);
+} // end of function write_pat()
+
+/**
+ * Write a PMT (Program Map Table) as a single TS packet.
+ *
+ * PMT structure (after TS header + pointer_field):
+ *   table_id (8)         = 0x02
+ *   section_syntax (1)   = 1
+ *   '0' (1)              = 0
+ *   reserved (2)         = 0x3
+ *   section_length (12)  = 18 (5 header + 4 program_info + 5 stream_entry + 4 CRC)
+ *   program_number (16)  = 0x0001
+ *   reserved (2)         = 0x3
+ *   version (5)          = 0
+ *   current_next (1)     = 1
+ *   section_number (8)   = 0
+ *   last_section (8)     = 0
+ *   reserved (3)         = 0x7
+ *   PCR_PID (13)         = 0x0100
+ *   reserved (4)         = 0xF
+ *   program_info_length (12) = 0
+ *   -- stream entry --
+ *   stream_type (8)      = 0x1B (H.264)
+ *   reserved (3)         = 0x7
+ *   elementary_PID (13)  = 0x0100
+ *   reserved (4)         = 0xF
+ *   ES_info_length (12)  = 0
+ *   CRC32 (32)
+ *
+ * @param muxer The muxer instance
+ * @return 0 on success, -1 on failure
+ */
+static int
+write_pmt(ts_muxer_t *muxer)
+{
+    uint8_t packet[TS_PACKET_SIZE];
+    memset(packet, 0xFF, TS_PACKET_SIZE);
+
+    /* TS header (4 bytes) */
+    packet[0] = TS_SYNC_BYTE;
+    packet[1] = 0x40 | ((PID_PMT >> 8) & 0x1F);  /* PUSI=1, PID high */
+    packet[2] = PID_PMT & 0xFF;                    /* PID low */
+    packet[3] = 0x10 | (muxer->cc_pmt & 0x0F);    /* no adaptation, payload only */
+    muxer->cc_pmt = (muxer->cc_pmt + 1) & 0x0F;
+
+    /* Pointer field */
+    packet[4] = 0x00;
+
+    /* PMT section data */
+    int section_start = 5;
+    uint8_t *p = &packet[section_start];
+
+    p[0] = 0x02;  /* table_id */
+    /* section_syntax_indicator=1, '0'=0, reserved=11, section_length=18 */
+    p[1] = 0xB0;
+    p[2] = 18;    /* section_length: 5 header + 4 program_info + 5 stream + 4 CRC */
+    p[3] = 0x00;  /* program_number high */
+    p[4] = 0x01;  /* program_number low */
+    /* reserved=11, version=00000, current_next=1 */
+    p[5] = 0xC1;
+    p[6] = 0x00;  /* section_number */
+    p[7] = 0x00;  /* last_section_number */
+    /* reserved=111, PCR_PID = 0x0100 */
+    p[8] = 0xE0 | ((PID_VIDEO >> 8) & 0x1F);
+    p[9] = PID_VIDEO & 0xFF;
+    /* reserved=1111, program_info_length = 0 */
+    p[10] = 0xF0;
+    p[11] = 0x00;
+
+    /* Stream entry: H.264 video on PID 0x0100 */
+    p[12] = STREAM_TYPE_H264;   /* stream_type */
+    /* reserved=111, elementary_PID = 0x0100 */
+    p[13] = 0xE0 | ((PID_VIDEO >> 8) & 0x1F);
+    p[14] = PID_VIDEO & 0xFF;
+    /* reserved=1111, ES_info_length = 0 */
+    p[15] = 0xF0;
+    p[16] = 0x00;
+
+    /* CRC32 over section data (from table_id through stream entry) */
+    uint32_t crc = crc32_mpeg2(p, 17);
+    p[17] = (crc >> 24) & 0xFF;
+    p[18] = (crc >> 16) & 0xFF;
+    p[19] = (crc >> 8) & 0xFF;
+    p[20] = crc & 0xFF;
+
+    return append_ts_packet(muxer, packet);
+} // end of function write_pmt()
+
+/**
+ * Write 5-byte PTS (or DTS) field in PES header format.
+ * The 33-bit timestamp is encoded across 5 bytes with marker bits.
+ *
+ * Format: '00xx' (4 bits marker) | PTS[32..30] | '1' | PTS[29..15] | '1' | PTS[14..0] | '1'
+ *
+ * @param buf    Destination buffer (at least 5 bytes)
+ * @param marker Upper 4-bit marker value (0x20 for PTS-only, 0x30 for PTS in PTS+DTS, 0x10 for DTS)
+ * @param ts     The 33-bit timestamp in 90kHz units
+ */
+static void
+write_pts_dts(uint8_t *buf, uint8_t marker, uint64_t ts)
+{
+    buf[0] = (uint8_t)(marker | (((ts >> 30) & 0x07) << 1) | 0x01);
+    buf[1] = (uint8_t)((ts >> 22) & 0xFF);
+    buf[2] = (uint8_t)((((ts >> 15) & 0x7F) << 1) | 0x01);
+    buf[3] = (uint8_t)((ts >> 7) & 0xFF);
+    buf[4] = (uint8_t)(((ts & 0x7F) << 1) | 0x01);
+} // end of function write_pts_dts()
+
+/**
+ * Write PCR (Program Clock Reference) into a 6-byte adaptation field area.
+ * PCR = base (33 bits, 90 kHz) + extension (9 bits, 27 MHz).
+ * We set extension to 0 for simplicity (sufficient precision for HLS).
+ *
+ * Format: base[32..25] | base[24..17] | base[16..9] | base[8..1] |
+ *         base[0] | reserved(6) | ext[8] | ext[7..0]
+ *
+ * @param buf      Destination buffer (at least 6 bytes)
+ * @param pcr_base The 33-bit PCR base value in 90kHz units
+ */
+static void
+write_pcr(uint8_t *buf, uint64_t pcr_base)
+{
+    uint64_t pcr_ext = 0;
+    buf[0] = (uint8_t)((pcr_base >> 25) & 0xFF);
+    buf[1] = (uint8_t)((pcr_base >> 17) & 0xFF);
+    buf[2] = (uint8_t)((pcr_base >> 9) & 0xFF);
+    buf[3] = (uint8_t)((pcr_base >> 1) & 0xFF);
+    buf[4] = (uint8_t)(((pcr_base & 0x01) << 7) | 0x7E | ((pcr_ext >> 8) & 0x01));
+    buf[5] = (uint8_t)(pcr_ext & 0xFF);
+} // end of function write_pcr()
+
+/**
+ * Build an Annex B formatted access unit from AVCC-format NAL data.
+ * On keyframes, SPS and PPS are prepended with start codes.
+ *
+ * AVCC format: [4-byte big-endian length][NAL unit] repeated
+ * Annex B format: [00 00 00 01][NAL unit] repeated
+ *
+ * @param muxer       The muxer instance (for SPS/PPS)
+ * @param avcc_data   Input AVCC-format NAL data
+ * @param avcc_size   Size of avcc_data in bytes
+ * @param is_keyframe Non-zero to prepend SPS/PPS
+ * @param out_data    Output pointer to allocated Annex B data (caller must free)
+ * @param out_size    Output size of the Annex B data
+ * @return 0 on success, -1 on failure
+ */
+static int
+build_annex_b_au(ts_muxer_t *muxer, uint8_t *avcc_data, size_t avcc_size,
+                 int is_keyframe, uint8_t **out_data, size_t *out_size)
+{
+    /* Calculate output size: for each NAL, replace 4-byte length with 4-byte start code.
+       Add 6 bytes for AUD NAL unit (start code + 2-byte NAL).
+       For keyframes, also add SPS (4 + sps_size) and PPS (4 + pps_size). */
+    size_t estimated_size = avcc_size + 6;  /* +6 for AUD NAL with start code */
+    if (is_keyframe && muxer->sps && muxer->pps) {
+        estimated_size += 4 + muxer->sps_size + 4 + muxer->pps_size;
+    }
+
+    uint8_t *buf = malloc(estimated_size);
+    if (!buf) {
+        return -1;
+    }
+
+    size_t offset = 0;
+
+    /* Prepend AUD (Access Unit Delimiter) NAL unit.
+       Required by HLS spec for proper access unit boundary detection in browsers.
+       AUD NAL: nal_ref_idc=0, nal_unit_type=9, primary_pic_type=7 (any). */
+    buf[offset++] = 0x00;
+    buf[offset++] = 0x00;
+    buf[offset++] = 0x00;
+    buf[offset++] = 0x01;
+    buf[offset++] = 0x09;  /* NAL header: type 9 (AUD) */
+    buf[offset++] = 0xF0;  /* primary_pic_type=7 (111), rbsp_stop=1, align=0000 */
+
+    /* Prepend SPS and PPS with Annex B start codes on keyframes */
+    if (is_keyframe && muxer->sps && muxer->pps) {
+        /* SPS */
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x01;
+        memcpy(buf + offset, muxer->sps, muxer->sps_size);
+        offset += muxer->sps_size;
+
+        /* PPS */
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x01;
+        memcpy(buf + offset, muxer->pps, muxer->pps_size);
+        offset += muxer->pps_size;
+    } // end of keyframe SPS/PPS prepend
+
+    /* Convert each NAL unit from AVCC (length-prefixed) to Annex B (start code prefixed) */
+    int parse_error = 0;
+    size_t pos = 0;
+    while (pos + 4 <= avcc_size) {
+        /* Read 4-byte big-endian NAL unit length */
+        uint32_t nal_len = ((uint32_t)avcc_data[pos] << 24) |
+                           ((uint32_t)avcc_data[pos + 1] << 16) |
+                           ((uint32_t)avcc_data[pos + 2] << 8) |
+                           ((uint32_t)avcc_data[pos + 3]);
+        pos += 4;
+
+        if (nal_len == 0 || pos + nal_len > avcc_size) {
+            parse_error = 1;
+            break;
+        }
+
+        /* Write Annex B start code */
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x00;
+        buf[offset++] = 0x01;
+
+        /* Copy NAL unit data */
+        memcpy(buf + offset, avcc_data + pos, nal_len);
+        offset += nal_len;
+        pos += nal_len;
+    } // end of AVCC-to-Annex-B NAL conversion loop
+
+    /* Treat malformed AVCC data as a hard error: don't emit partial AUs.
+       Also reject if trailing bytes remain (pos != avcc_size). */
+    if (parse_error || offset == 0 || pos != avcc_size) {
+        free(buf);
+        return -1;
+    }
+
+    *out_data = buf;
+    *out_size = offset;
+    return 0;
+} // end of function build_annex_b_au()
+
+/**
+ * Write a PES-wrapped H.264 access unit as a series of TS packets.
+ * The first TS packet includes an adaptation field with PCR (for keyframes)
+ * and the PES header with PTS. Subsequent packets are continuation packets.
+ *
+ * @param muxer       The muxer instance
+ * @param au_data     Annex B formatted access unit data
+ * @param au_size     Size of au_data in bytes
+ * @param pts_us      Presentation timestamp in microseconds
+ * @param is_keyframe Non-zero if this is a keyframe
+ * @return 0 on success, -1 on failure
+ */
+static int
+write_pes_packets(ts_muxer_t *muxer, uint8_t *au_data, size_t au_size,
+                  uint64_t pts_us, int is_keyframe)
+{
+    /* Convert PTS from microseconds to 90kHz MPEG-TS clock */
+    uint64_t pts_90khz = pts_us * 90 / 1000;
+
+    /* Build PES header */
+    /* PES header: start_code(3) + stream_id(1) + pes_length(2) + flags(2) + header_data_length(1) + PTS(5) = 14 bytes */
+    uint8_t pes_header[14];
+    pes_header[0] = 0x00;  /* packet_start_code_prefix */
+    pes_header[1] = 0x00;
+    pes_header[2] = 0x01;
+    pes_header[3] = STREAM_ID_VIDEO;  /* stream_id */
+
+    /* PES packet length: 0 means unbounded (allowed for video in TS) */
+    pes_header[4] = 0x00;
+    pes_header[5] = 0x00;
+
+    /* Flags: '10' (2), PES_scrambling_control=00, PES_priority=0,
+       data_alignment_indicator=1, copyright=0, original_or_copy=0 */
+    pes_header[6] = 0x84;  /* 10 00 0 1 0 0 = 0x84 (data alignment set) */
+    /* PTS_DTS_flags=10 (PTS only), rest=0 */
+    pes_header[7] = 0x80;
+    /* PES_header_data_length = 5 (PTS only) */
+    pes_header[8] = 0x05;
+
+    /* Write PTS (marker 0x20 for PTS-only) */
+    write_pts_dts(&pes_header[9], 0x20, pts_90khz);
+
+    size_t pes_header_len = 14;
+
+    /* Total PES payload = PES header + AU data */
+    size_t total_payload = pes_header_len + au_size;
+    size_t payload_pos = 0;  /* how much of total_payload we've written */
+
+    int first_packet = 1;
+
+    while (payload_pos < total_payload) {
+        uint8_t packet[TS_PACKET_SIZE];
+        memset(packet, 0xFF, TS_PACKET_SIZE);
+
+        /* TS header (4 bytes) */
+        packet[0] = TS_SYNC_BYTE;
+
+        uint8_t pusi = first_packet ? 0x40 : 0x00;
+        packet[1] = pusi | ((PID_VIDEO >> 8) & 0x1F);
+        packet[2] = PID_VIDEO & 0xFF;
+
+        size_t header_size = 4;   /* TS header so far */
+        size_t adapt_size = 0;    /* adaptation field size (including length byte) */
+        int include_adapt = 0;
+
+        /* Calculate available payload space to determine if we need adaptation field stuffing */
+        size_t remaining_payload = total_payload - payload_pos;
+
+        /* Determine if this packet needs PCR:
+           - Always on keyframe first packets (with random_access_indicator)
+           - On first packet when PCR cadence deadline is reached (~40ms) */
+        int need_pcr = 0;
+        if (first_packet) {
+            if (is_keyframe || pts_90khz >= muxer->next_pcr_90khz) {
+                need_pcr = 1;
+            }
+        }
+
+        if (need_pcr) {
+            /* Include adaptation field with PCR.
+               Adaptation field: length(1) + flags(1) + PCR(6) = 8 bytes */
+            include_adapt = 1;
+            adapt_size = 8;
+
+            size_t available = TS_PACKET_SIZE - header_size - adapt_size;
+            if (remaining_payload < available) {
+                /* Need stuffing bytes to fill the packet */
+                size_t stuff = available - remaining_payload;
+                adapt_size += stuff;
+            }
+
+            packet[3] = 0x30 | (muxer->cc_video & 0x0F);  /* adaptation + payload */
+            muxer->cc_video = (muxer->cc_video + 1) & 0x0F;
+
+            /* Adaptation field */
+            size_t af_start = header_size;
+            packet[af_start] = (uint8_t)(adapt_size - 1);  /* adaptation_field_length (excludes itself) */
+            /* Flags: PCR_flag=1, random_access_indicator=1 only on keyframes */
+            packet[af_start + 1] = is_keyframe ? 0x50 : 0x10;  /* PCR + optional random_access */
+
+            /* PCR (6 bytes) */
+            write_pcr(&packet[af_start + 2], pts_90khz);
+            muxer->next_pcr_90khz = pts_90khz + PCR_INTERVAL_90KHZ;
+
+            /* Fill remaining adaptation field with stuffing bytes (0xFF) */
+            for (size_t s = 8; s < adapt_size; s++) {
+                packet[af_start + s] = 0xFF;
+            }
+        } else {
+            /* No PCR needed: may still need adaptation field for stuffing
+               if payload doesn't fill the packet */
+            size_t available = TS_PACKET_SIZE - header_size;
+            if (remaining_payload < available) {
+                /* Need adaptation field for stuffing */
+                include_adapt = 1;
+                adapt_size = available - remaining_payload;
+
+                /* Minimum adaptation field is 1 byte (length=0, no flags).
+                   If >= 2 bytes, we have length byte + flags byte + optional stuffing. */
+                if (adapt_size == 1) {
+                    /* adaptation_field_length = 0: just the length byte, no flags */
+                    adapt_size = 1;
+                } else if (adapt_size < 2) {
+                    adapt_size = 2;
+                }
+
+                packet[3] = 0x30 | (muxer->cc_video & 0x0F);  /* adaptation + payload */
+                muxer->cc_video = (muxer->cc_video + 1) & 0x0F;
+
+                size_t af_start = header_size;
+                packet[af_start] = (uint8_t)(adapt_size - 1);  /* adaptation_field_length */
+                if (adapt_size >= 2) {
+                    packet[af_start + 1] = 0x00;  /* no flags set */
+                }
+                /* Stuff remaining with 0xFF */
+                for (size_t s = 2; s < adapt_size; s++) {
+                    packet[af_start + s] = 0xFF;
+                }
+            } else {
+                /* No adaptation field needed, payload fills entire packet */
+                packet[3] = 0x10 | (muxer->cc_video & 0x0F);  /* payload only */
+                muxer->cc_video = (muxer->cc_video + 1) & 0x0F;
+            }
+        } // end of TS header + adaptation field construction
+
+        /* Calculate payload offset within the packet */
+        size_t payload_start = header_size + (include_adapt ? adapt_size : 0);
+        size_t payload_space = TS_PACKET_SIZE - payload_start;
+
+        /* Copy payload data (PES header first, then AU data) */
+        size_t written = 0;
+        while (written < payload_space && payload_pos < total_payload) {
+            if (payload_pos < pes_header_len) {
+                /* Still writing PES header bytes */
+                size_t pes_remaining = pes_header_len - payload_pos;
+                size_t space_remaining = payload_space - written;
+                size_t chunk = (pes_remaining < space_remaining) ? pes_remaining : space_remaining;
+                memcpy(&packet[payload_start + written], &pes_header[payload_pos], chunk);
+                payload_pos += chunk;
+                written += chunk;
+            } else {
+                /* Writing AU data */
+                size_t au_offset = payload_pos - pes_header_len;
+                size_t au_remaining = au_size - au_offset;
+                size_t space_remaining = payload_space - written;
+                size_t chunk = (au_remaining < space_remaining) ? au_remaining : space_remaining;
+                memcpy(&packet[payload_start + written], &au_data[au_offset], chunk);
+                payload_pos += chunk;
+                written += chunk;
+            }
+        } // end of payload copy loop
+
+        if (append_ts_packet(muxer, packet) != 0) {
+            return -1;
+        }
+
+        first_packet = 0;
+    } // end of TS packet generation loop for this PES
+
+    return 0;
+} // end of function write_pes_packets()
+
+/**
+ * Drop the current segment due to an error, resetting buffer state
+ * and forcing re-sync on the next keyframe.
+ * @param muxer The muxer instance
+ */
+static void
+drop_current_segment(ts_muxer_t *muxer)
+{
+    muxer->segment_buf_used = 0;
+    muxer->segment_has_data = 0;
+    muxer->segment_start_pts = 0;
+    muxer->segment_last_pts = 0;
+    muxer->waiting_for_keyframe = 1;
+} // end of function drop_current_segment()
+
+/**
+ * Emit the current segment via the callback and reset the buffer.
+ * Calculates segment duration from the PTS of the first and last frames.
+ * @param muxer The muxer instance
+ */
+static void
+emit_segment(ts_muxer_t *muxer)
+{
+    if (!muxer->segment_has_data || muxer->segment_buf_used == 0) {
+        return;
+    }
+
+    double duration = 0.0;
+    if (muxer->segment_last_pts > muxer->segment_start_pts) {
+        duration = (double)(muxer->segment_last_pts - muxer->segment_start_pts) / 1000000.0;
+    }
+
+    if (muxer->callback) {
+        muxer->callback(muxer->callback_ctx, muxer->segment_buf,
+                        muxer->segment_buf_used, duration, muxer->segment_index);
+    }
+
+    muxer->segment_index++;
+    muxer->segment_buf_used = 0;
+    muxer->segment_has_data = 0;
+} // end of function emit_segment()
+
+/* ------------------------------------------------------------------ */
+/* Public API                                                          */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Create a new MPEG-TS muxer instance.
+ * @param segment_duration_seconds Target duration for each segment in seconds
+ * @param callback                 Callback to invoke when a segment is complete
+ * @param context                  User context passed to the callback
+ * @return New muxer instance, or NULL on allocation failure
+ */
+ts_muxer_t *
+ts_muxer_create(int segment_duration_seconds, ts_segment_callback_t callback, void *context)
+{
+    ts_muxer_t *muxer = calloc(1, sizeof(ts_muxer_t));
+    if (!muxer) {
+        return NULL;
+    }
+
+    muxer->segment_duration_seconds = segment_duration_seconds;
+    muxer->callback = callback;
+    muxer->callback_ctx = context;
+
+    muxer->sps = NULL;
+    muxer->sps_size = 0;
+    muxer->pps = NULL;
+    muxer->pps_size = 0;
+
+    muxer->segment_buf = malloc(INITIAL_BUFFER_SIZE);
+    if (!muxer->segment_buf) {
+        free(muxer);
+        return NULL;
+    }
+    muxer->segment_buf_size = INITIAL_BUFFER_SIZE;
+    muxer->segment_buf_used = 0;
+
+    muxer->segment_start_pts = 0;
+    muxer->segment_last_pts = 0;
+    muxer->segment_has_data = 0;
+    muxer->segment_index = 0;
+
+    muxer->cc_pat = 0;
+    muxer->cc_pmt = 0;
+    muxer->cc_video = 0;
+
+    muxer->waiting_for_keyframe = 1;  /* wait for first IDR before emitting data */
+    muxer->next_pcr_90khz = 0;       /* force PCR on first AU */
+
+    return muxer;
+} // end of function ts_muxer_create()
+
+/**
+ * Destroy a muxer instance and free all associated resources.
+ * @param muxer The muxer instance to destroy (may be NULL)
+ */
+void
+ts_muxer_destroy(ts_muxer_t *muxer)
+{
+    if (!muxer) {
+        return;
+    }
+
+    if (muxer->segment_buf) {
+        free(muxer->segment_buf);
+        muxer->segment_buf = NULL;
+    }
+
+    if (muxer->sps) {
+        free(muxer->sps);
+        muxer->sps = NULL;
+    }
+
+    if (muxer->pps) {
+        free(muxer->pps);
+        muxer->pps = NULL;
+    }
+
+    free(muxer);
+} // end of function ts_muxer_destroy()
+
+/**
+ * Push an H.264 access unit (one or more NAL units) to the muxer.
+ * The data must be in AVCC format (4-byte big-endian length prefix per NAL).
+ * The muxer converts to Annex B format, wraps in PES/TS packets, and
+ * accumulates into the current segment.
+ *
+ * Segment boundary logic: when a keyframe arrives and the current segment
+ * duration >= target duration, the current segment is emitted first.
+ *
+ * @param muxer       The muxer instance
+ * @param nal_data    H.264 NAL data in AVCC format
+ * @param nal_size    Size of nal_data in bytes
+ * @param pts         Presentation timestamp in microseconds
+ * @param is_keyframe Non-zero if this is a keyframe (IDR)
+ */
+void
+ts_muxer_push_h264(ts_muxer_t *muxer, uint8_t *nal_data, size_t nal_size,
+                   uint64_t pts, int is_keyframe)
+{
+    if (!muxer || !nal_data || nal_size == 0) {
+        return;
+    }
+
+    /* Drop frames until first IDR after start or error recovery */
+    if (muxer->waiting_for_keyframe) {
+        if (!is_keyframe) {
+            return;
+        }
+        muxer->waiting_for_keyframe = 0;
+    }
+
+    /* Check if we should start a new segment:
+       a keyframe has arrived and the current segment has enough duration.
+       Use a tolerance of 100ms to account for keyframes arriving slightly
+       before the exact boundary (e.g. 1.967s when target is 2.0s). */
+    if (is_keyframe && muxer->segment_has_data) {
+        double elapsed = (double)(pts - muxer->segment_start_pts) / 1000000.0;
+        double threshold = (double)muxer->segment_duration_seconds - 0.1;
+        if (elapsed >= threshold) {
+            emit_segment(muxer);
+        }
+    } // end of segment boundary check
+
+    /* If starting a new segment (either first frame or after emit), record start PTS */
+    if (!muxer->segment_has_data) {
+        muxer->segment_start_pts = pts;
+
+        /* Write PAT and PMT at the start of each segment */
+        if (write_pat(muxer) != 0 || write_pmt(muxer) != 0) {
+            drop_current_segment(muxer);
+            return;
+        }
+    }
+
+    /* Convert AVCC to Annex B format */
+    uint8_t *au_data = NULL;
+    size_t au_size = 0;
+    if (build_annex_b_au(muxer, nal_data, nal_size, is_keyframe, &au_data, &au_size) != 0) {
+        drop_current_segment(muxer);
+        return;
+    }
+
+    /* Write PES-wrapped TS packets */
+    if (write_pes_packets(muxer, au_data, au_size, pts, is_keyframe) != 0) {
+        free(au_data);
+        drop_current_segment(muxer);
+        return;
+    }
+
+    free(au_data);
+
+    muxer->segment_last_pts = pts;
+    muxer->segment_has_data = 1;
+} // end of function ts_muxer_push_h264()
+
+/**
+ * Set the SPS and PPS parameter sets for the H.264 stream.
+ * These are stored internally and prepended to keyframes in the TS output.
+ * @param muxer    The muxer instance
+ * @param sps      SPS NAL unit data (without start code or length prefix)
+ * @param sps_size Size of the SPS data in bytes
+ * @param pps      PPS NAL unit data (without start code or length prefix)
+ * @param pps_size Size of the PPS data in bytes
+ */
+void
+ts_muxer_set_sps_pps(ts_muxer_t *muxer, uint8_t *sps, size_t sps_size,
+                      uint8_t *pps, size_t pps_size)
+{
+    if (!muxer) {
+        return;
+    }
+
+    /* Update SPS */
+    if (sps && sps_size > 0) {
+        uint8_t *new_sps = malloc(sps_size);
+        if (new_sps) {
+            if (muxer->sps) {
+                free(muxer->sps);
+            }
+            memcpy(new_sps, sps, sps_size);
+            muxer->sps = new_sps;
+            muxer->sps_size = sps_size;
+        }
+    }
+
+    /* Update PPS */
+    if (pps && pps_size > 0) {
+        uint8_t *new_pps = malloc(pps_size);
+        if (new_pps) {
+            if (muxer->pps) {
+                free(muxer->pps);
+            }
+            memcpy(new_pps, pps, pps_size);
+            muxer->pps = new_pps;
+            muxer->pps_size = pps_size;
+        }
+    }
+} // end of function ts_muxer_set_sps_pps()
+
+/**
+ * Flush the current segment, invoking the callback with whatever data
+ * has been accumulated so far. Used when stopping the stream.
+ * @param muxer The muxer instance
+ */
+void
+ts_muxer_flush(ts_muxer_t *muxer)
+{
+    if (!muxer) {
+        return;
+    }
+
+    emit_segment(muxer);
+} // end of function ts_muxer_flush()
diff --git a/pip/ts_muxer.h b/pip/ts_muxer.h
new file mode 100644
index 0000000..965ff1d
--- /dev/null
+++ b/pip/ts_muxer.h
@@ -0,0 +1,92 @@
+/**
+ *  ts_muxer.h
+ *  PiP
+ *
+ *  MPEG-TS muxer for converting H.264 NAL units into MPEG-TS segments
+ *  suitable for HLS streaming. Accepts AVCC-format H.264 data and produces
+ *  188-byte MPEG-TS packet streams organized into timed segments.
+ *
+ *  Thread safety: All calls to a single ts_muxer_t instance must be
+ *  serialized (e.g. on a single dispatch queue). The segment callback
+ *  must NOT re-enter the muxer.
+ */
+
+#ifndef TS_MUXER_H
+#define TS_MUXER_H
+
+#include 
+#include 
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct ts_muxer_s ts_muxer_t;
+
+/**
+ * Callback invoked when a complete MPEG-TS segment is ready.
+ * @param context      User-provided context pointer
+ * @param segment_data Pointer to the segment data (caller must copy if needed)
+ * @param segment_size Size of the segment data in bytes
+ * @param duration     Duration of the segment in seconds
+ * @param segment_index Zero-based index of this segment
+ */
+typedef void (*ts_segment_callback_t)(
+    void *context,
+    uint8_t *segment_data,
+    size_t segment_size,
+    double duration,
+    uint64_t segment_index
+);
+
+/**
+ * Create a new MPEG-TS muxer instance.
+ * @param segment_duration_seconds Target duration for each segment in seconds
+ * @param callback                 Callback to invoke when a segment is complete
+ * @param context                  User context passed to the callback
+ * @return New muxer instance, or NULL on allocation failure
+ */
+ts_muxer_t *ts_muxer_create(int segment_duration_seconds, ts_segment_callback_t callback, void *context);
+
+/**
+ * Destroy a muxer instance and free all associated resources.
+ * @param muxer The muxer instance to destroy (may be NULL)
+ */
+void ts_muxer_destroy(ts_muxer_t *muxer);
+
+/**
+ * Push an H.264 access unit (one or more NAL units) to the muxer.
+ * The data must be in AVCC format (4-byte big-endian length prefix per NAL).
+ * The muxer converts to Annex B format, wraps in PES/TS packets, and
+ * accumulates into the current segment.
+ * @param muxer       The muxer instance
+ * @param nal_data    H.264 NAL data in AVCC format
+ * @param nal_size    Size of nal_data in bytes
+ * @param pts         Presentation timestamp in microseconds
+ * @param is_keyframe Non-zero if this is a keyframe (IDR)
+ */
+void ts_muxer_push_h264(ts_muxer_t *muxer, uint8_t *nal_data, size_t nal_size, uint64_t pts, int is_keyframe);
+
+/**
+ * Set the SPS and PPS parameter sets for the H.264 stream.
+ * These are stored internally and prepended to keyframes in the TS output.
+ * @param muxer    The muxer instance
+ * @param sps      SPS NAL unit data (without start code or length prefix)
+ * @param sps_size Size of the SPS data in bytes
+ * @param pps      PPS NAL unit data (without start code or length prefix)
+ * @param pps_size Size of the PPS data in bytes
+ */
+void ts_muxer_set_sps_pps(ts_muxer_t *muxer, uint8_t *sps, size_t sps_size, uint8_t *pps, size_t pps_size);
+
+/**
+ * Flush the current segment, invoking the callback with whatever data
+ * has been accumulated so far. Used when stopping the stream.
+ * @param muxer The muxer instance
+ */
+void ts_muxer_flush(ts_muxer_t *muxer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TS_MUXER_H */
diff --git a/pip/viewer.html b/pip/viewer.html
new file mode 100644
index 0000000..2782b4b
--- /dev/null
+++ b/pip/viewer.html
@@ -0,0 +1,195 @@
+
+
+
+    
+    
+    PiP Stream
+    
+
+
+    
+ +
+

Transmisión en vivo

+

Cargando...

+
+
+ + Cargando... +
+
+ + + + + diff --git a/pip/window.h b/pip/window.h index 346ebeb..39ee5ed 100644 --- a/pip/window.h +++ b/pip/window.h @@ -15,6 +15,7 @@ #import "preferences.h" #import "selectionView.h" #import "HLSPlayer.h" +#import "stream_manager.h" #ifndef NO_AIRPLAY #import "airplaySender.h" #endif @@ -74,5 +75,6 @@ - (BOOL) cloneSourceToWindow:(Window*)target; - (NSString*) sourceType; - (NSString*) sourceStatus; +- (void) startStreamAction:(id)sender; @end #endif /* Window_h */ diff --git a/pip/window.m b/pip/window.m index 0c02b70..09ed896 100644 --- a/pip/window.m +++ b/pip/window.m @@ -12,6 +12,7 @@ #import "audioPlayer.h" #import "H264Decoder.h" #import "HLSPlayer.h" +#import "stream_manager.h" #import #import #ifndef NO_AIRPLAY @@ -854,6 +855,7 @@ @implementation Window{ bool is_airplay_sending; dispatch_queue_t senderQueue; // Serial queue for sender operations #endif + StreamManager* streamManager; } - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ @@ -2001,6 +2003,57 @@ - (void)rightMouseDown:(NSEvent *)theEvent { } } #endif + + // Streaming submenu + if ([self is_capturing] || is_hls_session || camera_id) { + NSMenu *streamMenu = [[NSMenu alloc] init]; + + if (streamManager && [streamManager isStreaming]) { + ADD_MENU_ITEM(streamMenu, @"Detener transmisión", @selector(stopStreamAction:), NULL) + [streamMenu addItem:[NSMenuItem separatorItem]]; + ADD_MENU_ITEM(streamMenu, @"Copiar URL", @selector(copyStreamURL:), NULL) + ADD_MENU_ITEM(streamMenu, @"Abrir en navegador", @selector(openStreamInBrowser:), NULL) + } else { + ADD_MENU_ITEM(streamMenu, @"Iniciar transmisión", @selector(startStreamAction:), NULL) + } + + [streamMenu addItem:[NSMenuItem separatorItem]]; + + // Quality submenu + NSMenu *qualitySubmenu = [[NSMenu alloc] init]; + StreamQuality currentQ = streamManager ? [streamManager currentQuality] : StreamQualityMedium; + + NSArray *qualityNames = @[@"Baja (720p)", @"Media (1080p)", @"Alta (nativa)"]; + NSArray *qualityValues = @[@(StreamQualityLow), @(StreamQualityMedium), @(StreamQualityHigh)]; + + for (int i = 0; i < 3; i++) { + NSMenuItem *qItem = [qualitySubmenu addItemWithTitle:qualityNames[i] action:@selector(setStreamQuality:) keyEquivalent:@""]; + [qItem setTarget:self]; + [qItem setTag:[qualityValues[i] intValue]]; + if ([qualityValues[i] intValue] == (int)currentQ) { + [qItem setState:NSControlStateValueOn]; + } + } // End of loop through quality options + + ADD_MENU_ITEM(streamMenu, @"Calidad", nil, NULL, { + [item setSubmenu:qualitySubmenu]; + }) + + // Show URL as info if streaming + if (streamManager && [streamManager isStreaming]) { + [streamMenu addItem:[NSMenuItem separatorItem]]; + NSString *url = [streamManager streamURL]; + if (url) { + NSMenuItem *urlItem = [streamMenu addItemWithTitle:url action:nil keyEquivalent:@""]; + [urlItem setEnabled:NO]; + } + } + + ADD_MENU_ITEM(theMenu, @"Transmitir", nil, NULL, { + [item setSubmenu:streamMenu]; + }) + } + end: if(is_hls_session && !pvc){ // Add quality/resolution selection menu for HLS @@ -2130,6 +2183,89 @@ - (void)adjustOpacity:(id)sender{ [self setAlphaValue:slider.doubleValue]; } +#pragma mark - Streaming Methods + +/** + * Start streaming the current window content. + * Creates a StreamManager if needed, reads port/quality from preferences, + * and copies the stream URL to the clipboard on success. + */ +- (void)startStreamAction:(id)sender { + if (!imageView) return; + + if (!streamManager) { + streamManager = [[StreamManager alloc] initWithImageView:imageView]; + } + + // stream_port is stored as NSString by TextInput preferences + NSObject *portPref = getPref(@"stream_port"); + int port = 8080; + if ([portPref isKindOfClass:[NSString class]]) { + port = [(NSString*)portPref intValue]; + } else if ([portPref isKindOfClass:[NSNumber class]]) { + port = [(NSNumber*)portPref intValue]; + } + if (port <= 0 || port > 65535) port = 8080; + + StreamQuality quality = (StreamQuality)[(NSNumber*)getPref(@"stream_quality") intValue]; + + BOOL success = [streamManager startStreamingOnPort:port withQuality:quality]; + if (success) { + NSString *url = [streamManager streamURL]; + NSLog(@"Streaming started at %@", url); + // Copy URL to clipboard automatically + [[NSPasteboard generalPasteboard] clearContents]; + [[NSPasteboard generalPasteboard] setString:url forType:NSPasteboardTypeString]; + } +} // End of startStreamAction: + +/** + * Stop the current streaming session. + */ +- (void)stopStreamAction:(id)sender { + if (streamManager) { + [streamManager stopStreaming]; + } +} // End of stopStreamAction: + +/** + * Copy the stream URL to the system clipboard. + */ +- (void)copyStreamURL:(id)sender { + if (streamManager && [streamManager isStreaming]) { + NSString *url = [streamManager streamURL]; + if (url) { + [[NSPasteboard generalPasteboard] clearContents]; + [[NSPasteboard generalPasteboard] setString:url forType:NSPasteboardTypeString]; + } + } +} // End of copyStreamURL: + +/** + * Open the stream URL in the default web browser. + */ +- (void)openStreamInBrowser:(id)sender { + if (streamManager && [streamManager isStreaming]) { + NSString *url = [streamManager streamURL]; + if (url) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; + } + } +} // End of openStreamInBrowser: + +/** + * Set the streaming quality from a menu item tag. + * Saves the preference and updates the active stream if running. + */ +- (void)setStreamQuality:(id)sender { + NSMenuItem *menuItem = (NSMenuItem *)sender; + StreamQuality quality = (StreamQuality)[menuItem tag]; + setPref(@"stream_quality", [NSNumber numberWithInt:(int)quality]); + if (streamManager) { + [streamManager setQuality:quality]; + } +} // End of setStreamQuality: + -(void)stopDisplayStream{ if(!display_stream) return; CGDisplayStreamStop(display_stream); @@ -3589,6 +3725,12 @@ - (void)windowWillClose:(NSNotification *)notification{ [self stopDisplayStream]; [self stopWindowStream]; [self stopCameraCapture]; + + // Stop streaming if active + if (streamManager) { + [streamManager stopStreaming]; + streamManager = nil; + } CGDisplayRemoveReconfigurationCallback(displayReconfigurationCallback, (__bridge void*)self); // If this is the last PiP window, terminate the app From 58f2b6b364bd01dbf373b708bfd698cbfc8acc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 16:32:46 +0000 Subject: [PATCH 12/19] Avoid shutdown deadlock in frame capture --- pip/frame_capture.m | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pip/frame_capture.m b/pip/frame_capture.m index 22902dc..ab678d9 100644 --- a/pip/frame_capture.m +++ b/pip/frame_capture.m @@ -97,13 +97,27 @@ static void capture_frame(frame_capture_t *cap) { } // Get the current image from the renderer on main thread. + // Use a bounded wait when called from the capture queue to avoid deadlock + // during shutdown (main thread stopping capture while capture queue requests main). __block CIImage *currentImage = nil; if ([NSThread isMainThread]) { currentImage = [imageView.renderer currentImage]; } else { - dispatch_sync(dispatch_get_main_queue(), ^{ + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_main_queue(), ^{ currentImage = [imageView.renderer currentImage]; + dispatch_semaphore_signal(sem); }); + long wait_result = dispatch_semaphore_wait( + sem, + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)) + ); + if (wait_result != 0) { + if (cap->frame_count == 0 || cap->frame_count % 30 == 0) { + NSLog(@"frame_capture: timed out waiting for main-thread image (frame_count: %llu)", cap->frame_count); + } + return; + } } if (!currentImage) { From 2f161f460396dfafceb2fa70eeea41d5e13249f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 17:18:58 +0000 Subject: [PATCH 13/19] Fix HLS stability and add outgoing camera audio --- pip/stream_manager.h | 8 + pip/stream_manager.m | 638 ++++++++++++++++++++++++++++++++++++++++--- pip/stream_server.m | 30 +- pip/ts_muxer.c | 249 ++++++++++++++++- pip/ts_muxer.h | 13 + pip/window.h | 2 +- pip/window.m | 52 +++- 7 files changed, 934 insertions(+), 58 deletions(-) diff --git a/pip/stream_manager.h b/pip/stream_manager.h index fae2513..6c74d98 100644 --- a/pip/stream_manager.h +++ b/pip/stream_manager.h @@ -13,6 +13,7 @@ #define stream_manager_h #import +#import @class ImageView; @@ -100,6 +101,13 @@ typedef enum { */ - (NSArray *)localIPAddresses; +/** + * Push a captured audio CMSampleBuffer into the live stream pipeline. + * Intended for audio buffers received from ScreenCaptureKit. + * @param sampleBuffer Audio sample buffer + */ +- (void)pushAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; + @end #endif /* stream_manager_h */ diff --git a/pip/stream_manager.m b/pip/stream_manager.m index 94cd76f..b7fc8e4 100644 --- a/pip/stream_manager.m +++ b/pip/stream_manager.m @@ -25,10 +25,14 @@ #import #import +#import +#import #include #include #include +#include +#include /* ------------------------------------------------------------------ */ /* Embedded viewer resources via incbin */ @@ -76,6 +80,9 @@ int sps_pps_sent; /* whether SPS/PPS has been set on the muxer */ int expected_width; /* encoder's configured frame width */ int expected_height; /* encoder's configured frame height */ + int audio_sample_rate; + int audio_channels; + pthread_mutex_t muxer_lock; } pipeline_ctx_t; /* ------------------------------------------------------------------ */ @@ -88,6 +95,7 @@ static void encoded_frame_cb(uint8_t *data, int len, bool is_keyframe, uint8_t *sps, int sps_len, uint8_t *pps, int pps_len, uint64_t pts, void *ctx); +static void encoded_audio_cb(uint8_t *data, int len, uint64_t pts, void *ctx); static void segment_cb(void *context, uint8_t *segment_data, size_t segment_size, double duration, uint64_t segment_index); @@ -100,6 +108,34 @@ static void segment_cb(void *context, uint8_t *segment_data, static void compute_output_resolution(StreamQuality quality, int source_width, int source_height, int *out_width, int *out_height); +static float *resample_interleaved_linear(const float *input, int input_frames, int channels, + int input_rate, int output_rate, int *output_frames); +static float *expand_mono_to_stereo(const float *input, int frames); + +/* ------------------------------------------------------------------ */ +/* Internal AAC encoder state */ +/* ------------------------------------------------------------------ */ + +typedef struct { + AudioConverterRef converter; + AudioStreamBasicDescription in_asbd; + AudioStreamBasicDescription out_asbd; + float *pcm_buffer; + int pcm_capacity_frames; + int pcm_frames; + int sample_rate; + int channels; + int bitrate; + int pts_initialized; + uint64_t next_pts_us; +} aac_encoder_t; + +static aac_encoder_t *aac_encoder_create(int sample_rate, int channels, int bitrate); +static void aac_encoder_destroy(aac_encoder_t *enc); +static int aac_encoder_encode_pcm(aac_encoder_t *enc, const float *samples, int num_frames, + uint64_t pts, + void (*callback)(uint8_t *data, int len, uint64_t pts, void *ctx), + void *callback_ctx); /* ------------------------------------------------------------------ */ /* StreamManager class extension (private ivars) */ @@ -111,6 +147,7 @@ @interface StreamManager () { /* Pipeline components */ frame_capture_t *_capture; video_encoder_t *_encoder; + aac_encoder_t *_audio_encoder; ts_muxer_t *_muxer; hls_writer_t *_writer; stream_server_t *_server; @@ -122,6 +159,10 @@ @interface StreamManager () { BOOL _streaming; int _port; StreamQuality _quality; + int _audio_sample_rate; + int _audio_channels; + uint64_t _audio_next_pts_us; + BOOL _audio_pts_initialized; } @end @@ -143,6 +184,7 @@ - (instancetype)initWithImageView:(ImageView *)imageView _imageView = imageView; _capture = NULL; _encoder = NULL; + _audio_encoder = NULL; _muxer = NULL; _writer = NULL; _server = NULL; @@ -150,6 +192,10 @@ - (instancetype)initWithImageView:(ImageView *)imageView _streaming = NO; _port = 0; _quality = StreamQualityMedium; + _audio_sample_rate = 0; + _audio_channels = 0; + _audio_next_pts_us = 0; + _audio_pts_initialized = NO; } return self; } // end of function initWithImageView: @@ -248,6 +294,9 @@ View bounds can be larger than the actual rendered CIImage (HiDPI/crop cases), _pipeline_ctx->sps_pps_sent = 0; _pipeline_ctx->expected_width = output_width; _pipeline_ctx->expected_height = output_height; + _pipeline_ctx->audio_sample_rate = 48000; + _pipeline_ctx->audio_channels = 2; + pthread_mutex_init(&_pipeline_ctx->muxer_lock, NULL); /* Wire the encoder callback -> TS muxer */ video_encoder_set_callback(_encoder, encoded_frame_cb, _pipeline_ctx); @@ -289,6 +338,10 @@ View bounds can be larger than the actual rendered CIImage (HiDPI/crop cases), } _streaming = YES; + _audio_sample_rate = 0; + _audio_channels = 0; + _audio_next_pts_us = 0; + _audio_pts_initialized = NO; NSString *url = [self streamURL]; NSLog(@"stream_manager: pipeline started, stream available at %@", url ?: @"(unknown)"); @@ -301,56 +354,68 @@ View bounds can be larger than the actual rendered CIImage (HiDPI/crop cases), */ - (void)stopStreaming { - if (!_streaming && !_capture && !_encoder && !_muxer && !_writer && !_server) { - return; - } + @synchronized (self) { + if (!_streaming && !_capture && !_encoder && !_muxer && !_writer && !_server) { + return; + } - NSLog(@"stream_manager: stopping pipeline"); + NSLog(@"stream_manager: stopping pipeline"); + _streaming = NO; - /* Stop in reverse order: capture -> encoder -> muxer -> writer -> server */ + /* Stop in reverse order: capture -> encoder -> muxer -> writer -> server */ /* 1. Stop and destroy frame capture (stops producing frames) */ - if (_capture) { - frame_capture_stop(_capture); - frame_capture_destroy(_capture); - _capture = NULL; - } + if (_capture) { + frame_capture_stop(_capture); + frame_capture_destroy(_capture); + _capture = NULL; + } /* 2. Destroy the video encoder (no more encoded frames after this) */ - if (_encoder) { - video_encoder_destroy(_encoder); - _encoder = NULL; - } + if (_encoder) { + video_encoder_destroy(_encoder); + _encoder = NULL; + } - /* 3. Flush and destroy the TS muxer (emit any partial segment) */ - if (_muxer) { - ts_muxer_flush(_muxer); - ts_muxer_destroy(_muxer); - _muxer = NULL; - } + /* 3. Destroy the audio encoder */ + if (_audio_encoder) { + aac_encoder_destroy(_audio_encoder); + _audio_encoder = NULL; + } - /* 4. Stop and destroy the HTTP server */ - if (_server) { - stream_server_stop(_server); - stream_server_destroy(_server); - _server = NULL; - } + /* 4. Flush and destroy the TS muxer (emit any partial segment) */ + if (_muxer) { + ts_muxer_flush(_muxer); + ts_muxer_destroy(_muxer); + _muxer = NULL; + } - /* 5. Destroy the HLS writer (frees segment ring buffer) */ - if (_writer) { - hls_writer_destroy(_writer); - _writer = NULL; - } + /* 5. Stop and destroy the HTTP server */ + if (_server) { + stream_server_stop(_server); + stream_server_destroy(_server); + _server = NULL; + } - /* 6. Free the pipeline context */ - if (_pipeline_ctx) { - free(_pipeline_ctx); - _pipeline_ctx = NULL; - } + /* 6. Destroy the HLS writer (frees segment ring buffer) */ + if (_writer) { + hls_writer_destroy(_writer); + _writer = NULL; + } - _streaming = NO; + /* 7. Free the pipeline context */ + if (_pipeline_ctx) { + pthread_mutex_destroy(&_pipeline_ctx->muxer_lock); + free(_pipeline_ctx); + _pipeline_ctx = NULL; + } + _audio_sample_rate = 0; + _audio_channels = 0; + _audio_next_pts_us = 0; + _audio_pts_initialized = NO; - NSLog(@"stream_manager: pipeline stopped"); + NSLog(@"stream_manager: pipeline stopped"); + } } // end of function stopStreaming /** @@ -534,6 +599,261 @@ - (void)showQRCode return addresses; } // end of function localIPAddresses +static float * +resample_interleaved_linear(const float *input, int input_frames, int channels, + int input_rate, int output_rate, int *output_frames) +{ + if (!input || input_frames <= 0 || channels <= 0 || input_rate <= 0 || output_rate <= 0 || !output_frames) { + return NULL; + } + + if (input_rate == output_rate) { + size_t sample_count = (size_t)input_frames * (size_t)channels; + float *copy = malloc(sample_count * sizeof(float)); + if (!copy) { + return NULL; + } + memcpy(copy, input, sample_count * sizeof(float)); + *output_frames = input_frames; + return copy; + } + + double ratio = (double)output_rate / (double)input_rate; + int out_frames = (int)floor((double)input_frames * ratio); + if (out_frames <= 0) { + return NULL; + } + + float *out = calloc((size_t)out_frames * (size_t)channels, sizeof(float)); + if (!out) { + return NULL; + } + + double step = (double)input_rate / (double)output_rate; + for (int i = 0; i < out_frames; i++) { + double src_pos = (double)i * step; + int idx0 = (int)src_pos; + if (idx0 < 0) { + idx0 = 0; + } + if (idx0 >= input_frames) { + idx0 = input_frames - 1; + } + int idx1 = idx0 < (input_frames - 1) ? idx0 + 1 : idx0; + float frac = (float)(src_pos - (double)idx0); + for (int ch = 0; ch < channels; ch++) { + float a = input[(size_t)idx0 * (size_t)channels + (size_t)ch]; + float b = input[(size_t)idx1 * (size_t)channels + (size_t)ch]; + out[(size_t)i * (size_t)channels + (size_t)ch] = a + (b - a) * frac; + } + } + + *output_frames = out_frames; + return out; +} + +static float * +expand_mono_to_stereo(const float *input, int frames) +{ + if (!input || frames <= 0) { + return NULL; + } + + float *out = calloc((size_t)frames * 2, sizeof(float)); + if (!out) { + return NULL; + } + + for (int i = 0; i < frames; i++) { + float v = input[i]; + out[(size_t)i * 2] = v; + out[(size_t)i * 2 + 1] = v; + } + return out; +} + +- (void)pushAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer +{ + @synchronized (self) { + if (!sampleBuffer || !_streaming || !_pipeline_ctx || !_pipeline_ctx->muxer) { + return; + } + + CMAudioFormatDescriptionRef format_desc = CMSampleBufferGetFormatDescription(sampleBuffer); + if (!format_desc) { + return; + } + + const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format_desc); + if (!asbd) { + return; + } + + int channels = (int)asbd->mChannelsPerFrame; + int sample_rate = (int)llround(asbd->mSampleRate); + int num_frames = (int)CMSampleBufferGetNumSamples(sampleBuffer); + if (channels <= 0 || sample_rate <= 0 || num_frames <= 0) { + return; + } + + size_t abl_size = sizeof(AudioBufferList) + (size_t)(channels > 1 ? (channels - 1) : 0) * sizeof(AudioBuffer); + AudioBufferList *audio_list = calloc(1, abl_size); + if (!audio_list) { + return; + } + CMBlockBufferRef block_buffer = NULL; + OSStatus list_status = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer( + sampleBuffer, + NULL, + audio_list, + abl_size, + NULL, + NULL, + kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, + &block_buffer + ); + if (list_status != noErr || audio_list->mNumberBuffers == 0) { + free(audio_list); + if (block_buffer) { + CFRelease(block_buffer); + } + return; + } + + size_t sample_count = (size_t)num_frames * (size_t)channels; + float *working = calloc(sample_count, sizeof(float)); + if (!working) { + free(audio_list); + if (block_buffer) { + CFRelease(block_buffer); + } + return; + } + + int non_interleaved = (asbd->mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? 1 : 0; + int is_float = (asbd->mFormatFlags & kAudioFormatFlagIsFloat) ? 1 : 0; + int is_signed_int = (asbd->mFormatFlags & kAudioFormatFlagIsSignedInteger) ? 1 : 0; + int bits_per_channel = (int)asbd->mBitsPerChannel; + + if (non_interleaved) { + for (int ch = 0; ch < channels; ch++) { + uint32_t buf_index = (uint32_t)((ch < (int)audio_list->mNumberBuffers) ? ch : 0); + AudioBuffer ab = audio_list->mBuffers[buf_index]; + if (!ab.mData) { + continue; + } + + if (is_float && bits_per_channel == 32) { + const float *src = (const float *)ab.mData; + for (int i = 0; i < num_frames; i++) { + working[i * channels + ch] = src[i]; + } + } else if (is_signed_int && bits_per_channel == 16) { + const int16_t *src = (const int16_t *)ab.mData; + for (int i = 0; i < num_frames; i++) { + working[i * channels + ch] = (float)src[i] / 32768.0f; + } + } + } + } else { + AudioBuffer ab = audio_list->mBuffers[0]; + if (ab.mData) { + if (is_float && bits_per_channel == 32) { + size_t copy_count = sample_count; + size_t available = ab.mDataByteSize / sizeof(float); + if (available < copy_count) { + copy_count = available; + } + memcpy(working, ab.mData, copy_count * sizeof(float)); + } else if (is_signed_int && bits_per_channel == 16) { + const int16_t *src = (const int16_t *)ab.mData; + size_t available = ab.mDataByteSize / sizeof(int16_t); + size_t count = sample_count < available ? sample_count : available; + for (size_t i = 0; i < count; i++) { + working[i] = (float)src[i] / 32768.0f; + } + } + } + } + + int encode_sample_rate = sample_rate; + int encode_channels = channels; + int encode_frames = num_frames; + + if (encode_sample_rate > 48000) { + int resampled_frames = 0; + float *resampled = resample_interleaved_linear(working, encode_frames, encode_channels, + encode_sample_rate, 48000, &resampled_frames); + if (resampled && resampled_frames > 0) { + free(working); + working = resampled; + encode_frames = resampled_frames; + encode_sample_rate = 48000; + } + } + + if (encode_channels == 1) { + float *stereo = expand_mono_to_stereo(working, encode_frames); + if (stereo) { + free(working); + working = stereo; + encode_channels = 2; + } + } + + if (encode_frames <= 0) { + free(working); + free(audio_list); + if (block_buffer) { + CFRelease(block_buffer); + } + return; + } + + if (!_audio_encoder || _audio_sample_rate != encode_sample_rate || _audio_channels != encode_channels) { + if (_audio_encoder) { + aac_encoder_destroy(_audio_encoder); + _audio_encoder = NULL; + } + + _audio_encoder = aac_encoder_create(encode_sample_rate, encode_channels, 128000); + if (!_audio_encoder) { + free(working); + free(audio_list); + if (block_buffer) { + CFRelease(block_buffer); + } + return; + } + + _audio_sample_rate = _audio_encoder->sample_rate; + _audio_channels = _audio_encoder->channels; + _pipeline_ctx->audio_sample_rate = _audio_encoder->sample_rate; + _pipeline_ctx->audio_channels = _audio_encoder->channels; + _audio_next_pts_us = 0; + _audio_pts_initialized = NO; + NSLog(@"stream_manager: audio encoder configured %d Hz, %d ch (input %d Hz, %d ch)", + _audio_encoder->sample_rate, _audio_encoder->channels, sample_rate, channels); + } + + uint64_t pts_us = 0; + if (!_audio_pts_initialized) { + _audio_next_pts_us = 0; + _audio_pts_initialized = YES; + } + pts_us = _audio_next_pts_us; + _audio_next_pts_us += (uint64_t)((double)encode_frames * 1000000.0 / (double)_audio_sample_rate); + + aac_encoder_encode_pcm(_audio_encoder, working, encode_frames, pts_us, encoded_audio_cb, _pipeline_ctx); + + free(working); + free(audio_list); + if (block_buffer) { + CFRelease(block_buffer); + } + } +} + /** * Clean up all resources when the manager is deallocated. */ @@ -544,6 +864,229 @@ - (void)dealloc @end +/* ================================================================== */ +/* Internal AAC encoder (PCM float -> AAC LC raw frames) */ +/* ================================================================== */ + +typedef struct { + const float *pcm; + UInt32 frames; + UInt32 channels; +} aac_input_ctx_t; + +static OSStatus +aac_input_data_proc(AudioConverterRef inAudioConverter, + UInt32 *ioNumberDataPackets, + AudioBufferList *ioData, + AudioStreamPacketDescription **outDataPacketDescription, + void *inUserData) +{ + (void)inAudioConverter; + (void)outDataPacketDescription; + + aac_input_ctx_t *ctx = (aac_input_ctx_t *)inUserData; + if (!ctx || !ctx->pcm || !ioNumberDataPackets || *ioNumberDataPackets == 0 || ctx->frames == 0) { + *ioNumberDataPackets = 0; + return -1; + } + + ioData->mNumberBuffers = 1; + ioData->mBuffers[0].mNumberChannels = ctx->channels; + ioData->mBuffers[0].mData = (void *)ctx->pcm; + ioData->mBuffers[0].mDataByteSize = ctx->frames * ctx->channels * sizeof(float); + *ioNumberDataPackets = ctx->frames; + return noErr; +} + +static aac_encoder_t * +aac_encoder_create(int sample_rate, int channels, int bitrate) +{ + if (sample_rate <= 0 || channels <= 0) { + return NULL; + } + + aac_encoder_t *enc = calloc(1, sizeof(aac_encoder_t)); + if (!enc) { + return NULL; + } + + enc->sample_rate = sample_rate; + enc->channels = channels; + enc->bitrate = bitrate > 0 ? bitrate : 128000; + enc->pcm_capacity_frames = 8192; + enc->pcm_frames = 0; + enc->pts_initialized = 0; + enc->next_pts_us = 0; + enc->pcm_buffer = calloc((size_t)enc->pcm_capacity_frames * (size_t)channels, sizeof(float)); + if (!enc->pcm_buffer) { + free(enc); + return NULL; + } + + memset(&enc->in_asbd, 0, sizeof(enc->in_asbd)); + enc->in_asbd.mSampleRate = (Float64)sample_rate; + enc->in_asbd.mFormatID = kAudioFormatLinearPCM; + enc->in_asbd.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + enc->in_asbd.mBitsPerChannel = 32; + enc->in_asbd.mChannelsPerFrame = (UInt32)channels; + enc->in_asbd.mFramesPerPacket = 1; + enc->in_asbd.mBytesPerFrame = (UInt32)(channels * (int)sizeof(float)); + enc->in_asbd.mBytesPerPacket = enc->in_asbd.mBytesPerFrame; + + memset(&enc->out_asbd, 0, sizeof(enc->out_asbd)); + enc->out_asbd.mSampleRate = (Float64)sample_rate; + enc->out_asbd.mFormatID = kAudioFormatMPEG4AAC; + enc->out_asbd.mFormatFlags = kMPEG4Object_AAC_LC; + enc->out_asbd.mChannelsPerFrame = (UInt32)channels; + + UInt32 out_asbd_size = sizeof(enc->out_asbd); + OSStatus format_status = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, + 0, NULL, + &out_asbd_size, &enc->out_asbd); + if (format_status != noErr) { + free(enc->pcm_buffer); + free(enc); + return NULL; + } + + OSStatus create_status = AudioConverterNew(&enc->in_asbd, &enc->out_asbd, &enc->converter); + if (create_status != noErr || !enc->converter) { + free(enc->pcm_buffer); + free(enc); + return NULL; + } + + UInt32 br = (UInt32)enc->bitrate; + AudioConverterSetProperty(enc->converter, kAudioConverterEncodeBitRate, sizeof(br), &br); + + return enc; +} + +static void +aac_encoder_destroy(aac_encoder_t *enc) +{ + if (!enc) { + return; + } + + if (enc->converter) { + AudioConverterDispose(enc->converter); + enc->converter = NULL; + } + if (enc->pcm_buffer) { + free(enc->pcm_buffer); + enc->pcm_buffer = NULL; + } + free(enc); +} + +static int +aac_encoder_ensure_pcm_capacity(aac_encoder_t *enc, int needed_frames) +{ + if (!enc) { + return -1; + } + if (needed_frames <= enc->pcm_capacity_frames) { + return 0; + } + + int new_capacity = enc->pcm_capacity_frames; + while (new_capacity < needed_frames) { + new_capacity *= 2; + } + + size_t sample_count = (size_t)new_capacity * (size_t)enc->channels; + float *new_buf = realloc(enc->pcm_buffer, sample_count * sizeof(float)); + if (!new_buf) { + return -1; + } + + enc->pcm_buffer = new_buf; + enc->pcm_capacity_frames = new_capacity; + return 0; +} + +static int +aac_encoder_encode_pcm(aac_encoder_t *enc, const float *samples, int num_frames, + uint64_t pts, + void (*callback)(uint8_t *data, int len, uint64_t pts, void *ctx), + void *callback_ctx) +{ + if (!enc || !samples || num_frames <= 0 || !enc->converter) { + return -1; + } + + if (!enc->pts_initialized) { + enc->next_pts_us = pts; + enc->pts_initialized = 1; + } else { + /* Re-anchor if external clock drifts significantly. */ + uint64_t delta = enc->next_pts_us > pts ? enc->next_pts_us - pts : pts - enc->next_pts_us; + if (delta > 2000000ULL) { + enc->next_pts_us = pts; + } + } + + int needed_frames = enc->pcm_frames + num_frames; + if (aac_encoder_ensure_pcm_capacity(enc, needed_frames) != 0) { + return -1; + } + + size_t dst_offset = (size_t)enc->pcm_frames * (size_t)enc->channels; + size_t copy_samples = (size_t)num_frames * (size_t)enc->channels; + memcpy(enc->pcm_buffer + dst_offset, samples, copy_samples * sizeof(float)); + enc->pcm_frames += num_frames; + + const int aac_frame_samples = 1024; + + while (enc->pcm_frames >= aac_frame_samples) { + aac_input_ctx_t input_ctx; + input_ctx.pcm = enc->pcm_buffer; + input_ctx.frames = (UInt32)aac_frame_samples; + input_ctx.channels = (UInt32)enc->channels; + + uint8_t out_buf[8192]; + AudioBufferList out_list; + out_list.mNumberBuffers = 1; + out_list.mBuffers[0].mNumberChannels = (UInt32)enc->channels; + out_list.mBuffers[0].mData = out_buf; + out_list.mBuffers[0].mDataByteSize = sizeof(out_buf); + + UInt32 out_packets = 1; + AudioStreamPacketDescription out_desc = {0}; + OSStatus enc_status = AudioConverterFillComplexBuffer( + enc->converter, + aac_input_data_proc, + &input_ctx, + &out_packets, + &out_list, + &out_desc + ); + + if (enc_status == noErr && out_packets > 0 && out_list.mBuffers[0].mDataByteSize > 0 && callback) { + uint8_t *copy = malloc(out_list.mBuffers[0].mDataByteSize); + if (copy) { + memcpy(copy, out_list.mBuffers[0].mData, out_list.mBuffers[0].mDataByteSize); + callback(copy, (int)out_list.mBuffers[0].mDataByteSize, enc->next_pts_us, callback_ctx); + free(copy); + } + } + + enc->next_pts_us += (uint64_t)((double)aac_frame_samples * 1000000.0 / (double)enc->sample_rate); + + int remaining_frames = enc->pcm_frames - aac_frame_samples; + if (remaining_frames > 0) { + size_t remaining_samples = (size_t)remaining_frames * (size_t)enc->channels; + memmove(enc->pcm_buffer, + enc->pcm_buffer + ((size_t)aac_frame_samples * (size_t)enc->channels), + remaining_samples * sizeof(float)); + } + enc->pcm_frames = remaining_frames; + } + + return 0; +} + /* ================================================================== */ /* C callback functions (static, pipeline glue) */ /* ================================================================== */ @@ -602,6 +1145,8 @@ are safe (the encoder simply crops to its configured dimensions). */ return; } + pthread_mutex_lock(&pipeline->muxer_lock); + /* Update SPS/PPS on the muxer whenever new parameter sets arrive */ if (sps && sps_len > 0 && pps && pps_len > 0) { ts_muxer_set_sps_pps(pipeline->muxer, @@ -612,14 +1157,31 @@ are safe (the encoder simply crops to its configured dimensions). */ /* Only push data if we have SPS/PPS configured */ if (!pipeline->sps_pps_sent) { + pthread_mutex_unlock(&pipeline->muxer_lock); return; } ts_muxer_push_h264(pipeline->muxer, data, (size_t)len, pts, is_keyframe ? 1 : 0); + + pthread_mutex_unlock(&pipeline->muxer_lock); } // end of function encoded_frame_cb() +static void +encoded_audio_cb(uint8_t *data, int len, uint64_t pts, void *ctx) +{ + pipeline_ctx_t *pipeline = (pipeline_ctx_t *)ctx; + if (!pipeline || !pipeline->muxer || !data || len <= 0) { + return; + } + + pthread_mutex_lock(&pipeline->muxer_lock); + ts_muxer_push_aac(pipeline->muxer, data, (size_t)len, pts, + pipeline->audio_sample_rate, pipeline->audio_channels); + pthread_mutex_unlock(&pipeline->muxer_lock); +} + /** * TS segment callback. Called when a complete MPEG-TS segment is ready. * Stores the segment in the HLS writer ring buffer. diff --git a/pip/stream_server.m b/pip/stream_server.m index 7102c1e..1a934eb 100644 --- a/pip/stream_server.m +++ b/pip/stream_server.m @@ -365,15 +365,22 @@ static void send_text_response(int fd, int status_code, const char *status_text, break; } - /* Keep accepted sockets in blocking mode for simple write-all semantics. - listen_fd is non-blocking so it can be drained safely in this loop. */ + /* Keep accepted sockets non-blocking so a slow/stalled client cannot + block the entire serial server queue while we stream TS data. */ int client_flags = fcntl(client_fd, F_GETFL, 0); - if (client_flags >= 0 && (client_flags & O_NONBLOCK)) { - if (fcntl(client_fd, F_SETFL, client_flags & ~O_NONBLOCK) < 0) { - NSLog(@"stream_server: failed to clear O_NONBLOCK on fd=%d", client_fd); - close(client_fd); - continue; - } + if (client_flags < 0 || fcntl(client_fd, F_SETFL, client_flags | O_NONBLOCK) < 0) { + NSLog(@"stream_server: failed to set O_NONBLOCK on fd=%d", client_fd); + close(client_fd); + continue; + } + + /* Prevent process-wide SIGPIPE termination if a client disconnects + while we are writing a response body. */ + int no_sigpipe = 1; + if (setsockopt(client_fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)) < 0) { + NSLog(@"stream_server: failed to set SO_NOSIGPIPE on fd=%d", client_fd); + close(client_fd); + continue; } /* Find a free connection slot */ @@ -657,17 +664,24 @@ static void send_text_response(int fd, int status_code, const char *status_text, static int write_all(int fd, const uint8_t *data, size_t len) { + const int max_eagain_retries = 250; /* ~250ms with 1ms sleep */ + int eagain_retries = 0; size_t total_written = 0; while (total_written < len) { ssize_t written = write(fd, data + total_written, len - total_written); if (written > 0) { total_written += (size_t)written; + eagain_retries = 0; continue; } if (written < 0 && errno == EINTR) { continue; } if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + eagain_retries++; + if (eagain_retries >= max_eagain_retries) { + return -1; + } usleep(1000); continue; } diff --git a/pip/ts_muxer.c b/pip/ts_muxer.c index d0619de..ab9c235 100644 --- a/pip/ts_muxer.c +++ b/pip/ts_muxer.c @@ -29,9 +29,12 @@ #define PID_PAT 0x0000 #define PID_PMT 0x1000 #define PID_VIDEO 0x0100 +#define PID_AUDIO 0x0101 #define STREAM_TYPE_H264 0x1B +#define STREAM_TYPE_AAC 0x0F #define STREAM_ID_VIDEO 0xE0 +#define STREAM_ID_AUDIO 0xC0 #define INITIAL_BUFFER_SIZE (256 * 1024) /* 256 KB */ #define PCR_INTERVAL_90KHZ 3600 /* 40ms in 90kHz ticks */ @@ -115,6 +118,13 @@ struct ts_muxer_s { uint8_t cc_pat; uint8_t cc_pmt; uint8_t cc_video; + uint8_t cc_audio; + + /* AAC configuration used to build ADTS headers */ + int audio_sample_rate; + int audio_channels; + int has_audio; + int segment_audio_enabled; /* Startup/resync state: drop frames until first IDR */ int waiting_for_keyframe; @@ -255,7 +265,7 @@ write_pat(ts_muxer_t *muxer) * section_syntax (1) = 1 * '0' (1) = 0 * reserved (2) = 0x3 - * section_length (12) = 18 (5 header + 4 program_info + 5 stream_entry + 4 CRC) + * section_length (12) = 18 (video only) or 23 (video + audio) * program_number (16) = 0x0001 * reserved (2) = 0x3 * version (5) = 0 @@ -266,12 +276,18 @@ write_pat(ts_muxer_t *muxer) * PCR_PID (13) = 0x0100 * reserved (4) = 0xF * program_info_length (12) = 0 - * -- stream entry -- + * -- stream entry (video) -- * stream_type (8) = 0x1B (H.264) * reserved (3) = 0x7 * elementary_PID (13) = 0x0100 * reserved (4) = 0xF * ES_info_length (12) = 0 + * -- optional stream entry (audio) -- + * stream_type (8) = 0x0F (AAC/ADTS) + * reserved (3) = 0x7 + * elementary_PID (13) = 0x0101 + * reserved (4) = 0xF + * ES_info_length (12) = 0 * CRC32 (32) * * @param muxer The muxer instance @@ -298,9 +314,10 @@ write_pmt(ts_muxer_t *muxer) uint8_t *p = &packet[section_start]; p[0] = 0x02; /* table_id */ - /* section_syntax_indicator=1, '0'=0, reserved=11, section_length=18 */ + int section_length = muxer->has_audio ? 23 : 18; + /* section_syntax_indicator=1, '0'=0, reserved=11 */ p[1] = 0xB0; - p[2] = 18; /* section_length: 5 header + 4 program_info + 5 stream + 4 CRC */ + p[2] = (uint8_t)section_length; p[3] = 0x00; /* program_number high */ p[4] = 0x01; /* program_number low */ /* reserved=11, version=00000, current_next=1 */ @@ -323,12 +340,24 @@ write_pmt(ts_muxer_t *muxer) p[15] = 0xF0; p[16] = 0x00; - /* CRC32 over section data (from table_id through stream entry) */ - uint32_t crc = crc32_mpeg2(p, 17); - p[17] = (crc >> 24) & 0xFF; - p[18] = (crc >> 16) & 0xFF; - p[19] = (crc >> 8) & 0xFF; - p[20] = crc & 0xFF; + int section_data_len = 17; /* through video entry */ + + if (muxer->has_audio) { + /* Optional stream entry: AAC audio on PID 0x0101 */ + p[17] = STREAM_TYPE_AAC; + p[18] = 0xE0 | ((PID_AUDIO >> 8) & 0x1F); + p[19] = PID_AUDIO & 0xFF; + p[20] = 0xF0; + p[21] = 0x00; + section_data_len = 22; + } + + /* CRC32 over section data (from table_id through stream entries) */ + uint32_t crc = crc32_mpeg2(p, (size_t)section_data_len); + p[section_data_len + 0] = (crc >> 24) & 0xFF; + p[section_data_len + 1] = (crc >> 16) & 0xFF; + p[section_data_len + 2] = (crc >> 8) & 0xFF; + p[section_data_len + 3] = crc & 0xFF; return append_ts_packet(muxer, packet); } // end of function write_pmt() @@ -661,6 +690,148 @@ write_pes_packets(ts_muxer_t *muxer, uint8_t *au_data, size_t au_size, return 0; } // end of function write_pes_packets() +/** + * Map AAC sample rate (Hz) to ADTS sampling_frequency_index. + * Falls back to 48 kHz index when unknown. + */ +static int +aac_sample_rate_index(int sample_rate) +{ + static const int rates[] = { + 96000, 88200, 64000, 48000, 44100, 32000, + 24000, 22050, 16000, 12000, 11025, 8000, 7350 + }; + + for (int i = 0; i < (int)(sizeof(rates) / sizeof(rates[0])); i++) { + if (rates[i] == sample_rate) { + return i; + } + } + return 3; /* 48kHz */ +} + +/** + * Build a 7-byte ADTS header for one AAC-LC frame. + */ +static void +build_adts_header(uint8_t *hdr, size_t aac_payload_size, int sample_rate, int channels) +{ + int sr_idx = aac_sample_rate_index(sample_rate); + int chan = channels < 1 ? 2 : channels; + if (chan > 7) { + chan = 2; + } + + size_t frame_len = aac_payload_size + 7; + + /* MPEG-4 AAC LC, no CRC */ + hdr[0] = 0xFF; + hdr[1] = 0xF1; /* 1111 1 00 1: sync + MPEG-4 + layer + protection_absent */ + hdr[2] = (uint8_t)(((2 - 1) << 6) | ((sr_idx & 0x0F) << 2) | ((chan >> 2) & 0x01)); + hdr[3] = (uint8_t)(((chan & 0x03) << 6) | ((frame_len >> 11) & 0x03)); + hdr[4] = (uint8_t)((frame_len >> 3) & 0xFF); + hdr[5] = (uint8_t)(((frame_len & 0x07) << 5) | 0x1F); + hdr[6] = 0xFC; +} + +/** + * Write a PES-wrapped AAC frame as TS packets on the audio PID. + */ +static int +write_pes_audio_packets(ts_muxer_t *muxer, uint8_t *adts_frame, size_t adts_size, uint64_t pts_us) +{ + uint64_t pts_90khz = pts_us * 90 / 1000; + + uint8_t pes_header[14]; + pes_header[0] = 0x00; + pes_header[1] = 0x00; + pes_header[2] = 0x01; + pes_header[3] = STREAM_ID_AUDIO; + + /* PES length includes bytes after this field: 3 + 5 + payload */ + uint32_t pes_len = (uint32_t)(adts_size + 8); + if (pes_len > 0xFFFF) { + pes_len = 0; + } + pes_header[4] = (uint8_t)((pes_len >> 8) & 0xFF); + pes_header[5] = (uint8_t)(pes_len & 0xFF); + pes_header[6] = 0x80; + pes_header[7] = 0x80; /* PTS only */ + pes_header[8] = 0x05; + write_pts_dts(&pes_header[9], 0x20, pts_90khz); + + size_t pes_header_len = 14; + size_t total_payload = pes_header_len + adts_size; + size_t payload_pos = 0; + int first_packet = 1; + + while (payload_pos < total_payload) { + uint8_t packet[TS_PACKET_SIZE]; + memset(packet, 0xFF, TS_PACKET_SIZE); + + packet[0] = TS_SYNC_BYTE; + packet[1] = (first_packet ? 0x40 : 0x00) | ((PID_AUDIO >> 8) & 0x1F); + packet[2] = PID_AUDIO & 0xFF; + + size_t header_size = 4; + size_t remaining_payload = total_payload - payload_pos; + size_t payload_start = header_size; + size_t payload_space = TS_PACKET_SIZE - payload_start; + + if (remaining_payload < payload_space) { + /* Add adaptation field stuffing on final short packet. */ + size_t adapt_size = payload_space - remaining_payload; + if (adapt_size < 2) { + adapt_size = 2; + } + + packet[3] = 0x30 | (muxer->cc_audio & 0x0F); + muxer->cc_audio = (muxer->cc_audio + 1) & 0x0F; + + size_t af_start = header_size; + packet[af_start] = (uint8_t)(adapt_size - 1); + packet[af_start + 1] = 0x00; + for (size_t s = 2; s < adapt_size; s++) { + packet[af_start + s] = 0xFF; + } + + payload_start = header_size + adapt_size; + payload_space = TS_PACKET_SIZE - payload_start; + } else { + packet[3] = 0x10 | (muxer->cc_audio & 0x0F); + muxer->cc_audio = (muxer->cc_audio + 1) & 0x0F; + } + + size_t written = 0; + while (written < payload_space && payload_pos < total_payload) { + if (payload_pos < pes_header_len) { + size_t pes_remaining = pes_header_len - payload_pos; + size_t space_remaining = payload_space - written; + size_t chunk = (pes_remaining < space_remaining) ? pes_remaining : space_remaining; + memcpy(&packet[payload_start + written], &pes_header[payload_pos], chunk); + payload_pos += chunk; + written += chunk; + } else { + size_t adts_offset = payload_pos - pes_header_len; + size_t adts_remaining = adts_size - adts_offset; + size_t space_remaining = payload_space - written; + size_t chunk = (adts_remaining < space_remaining) ? adts_remaining : space_remaining; + memcpy(&packet[payload_start + written], &adts_frame[adts_offset], chunk); + payload_pos += chunk; + written += chunk; + } + } + + if (append_ts_packet(muxer, packet) != 0) { + return -1; + } + + first_packet = 0; + } + + return 0; +} + /** * Drop the current segment due to an error, resetting buffer state * and forcing re-sync on the next keyframe. @@ -674,6 +845,7 @@ drop_current_segment(ts_muxer_t *muxer) muxer->segment_start_pts = 0; muxer->segment_last_pts = 0; muxer->waiting_for_keyframe = 1; + muxer->segment_audio_enabled = 0; } // end of function drop_current_segment() /** @@ -701,6 +873,7 @@ emit_segment(ts_muxer_t *muxer) muxer->segment_index++; muxer->segment_buf_used = 0; muxer->segment_has_data = 0; + muxer->segment_audio_enabled = 0; } // end of function emit_segment() /* ------------------------------------------------------------------ */ @@ -747,6 +920,11 @@ ts_muxer_create(int segment_duration_seconds, ts_segment_callback_t callback, vo muxer->cc_pat = 0; muxer->cc_pmt = 0; muxer->cc_video = 0; + muxer->cc_audio = 0; + muxer->audio_sample_rate = 48000; + muxer->audio_channels = 2; + muxer->has_audio = 0; + muxer->segment_audio_enabled = 0; muxer->waiting_for_keyframe = 1; /* wait for first IDR before emitting data */ muxer->next_pcr_90khz = 0; /* force PCR on first AU */ @@ -835,6 +1013,7 @@ ts_muxer_push_h264(ts_muxer_t *muxer, uint8_t *nal_data, size_t nal_size, drop_current_segment(muxer); return; } + muxer->segment_audio_enabled = muxer->has_audio; } /* Convert AVCC to Annex B format */ @@ -858,6 +1037,56 @@ ts_muxer_push_h264(ts_muxer_t *muxer, uint8_t *nal_data, size_t nal_size, muxer->segment_has_data = 1; } // end of function ts_muxer_push_h264() +void +ts_muxer_push_aac(ts_muxer_t *muxer, uint8_t *aac_data, size_t aac_size, + uint64_t pts, int sample_rate, int channels) +{ + if (!muxer || !aac_data || aac_size == 0) { + return; + } + + /* Keep muxer audio config up to date for ADTS headers. */ + if (sample_rate > 0) { + muxer->audio_sample_rate = sample_rate; + } + if (channels > 0) { + muxer->audio_channels = channels; + } + + if (!muxer->has_audio) { + /* Enable audio from the next segment boundary to keep PMT and packets aligned. */ + muxer->has_audio = 1; + return; + } + + /* Do not start segments from audio before first video keyframe, and only + write audio when this segment's PMT advertises an audio stream. */ + if (muxer->waiting_for_keyframe || !muxer->segment_has_data || !muxer->segment_audio_enabled) { + return; + } + + uint8_t *adts_frame = malloc(aac_size + 7); + if (!adts_frame) { + return; + } + + build_adts_header(adts_frame, aac_size, muxer->audio_sample_rate, muxer->audio_channels); + memcpy(adts_frame + 7, aac_data, aac_size); + + if (write_pes_audio_packets(muxer, adts_frame, aac_size + 7, pts) != 0) { + free(adts_frame); + drop_current_segment(muxer); + return; + } + + free(adts_frame); + + if (pts > muxer->segment_last_pts) { + muxer->segment_last_pts = pts; + } + muxer->segment_has_data = 1; +} + /** * Set the SPS and PPS parameter sets for the H.264 stream. * These are stored internally and prepended to keyframes in the TS output. diff --git a/pip/ts_muxer.h b/pip/ts_muxer.h index 965ff1d..c823ccf 100644 --- a/pip/ts_muxer.h +++ b/pip/ts_muxer.h @@ -67,6 +67,19 @@ void ts_muxer_destroy(ts_muxer_t *muxer); */ void ts_muxer_push_h264(ts_muxer_t *muxer, uint8_t *nal_data, size_t nal_size, uint64_t pts, int is_keyframe); +/** + * Push one raw AAC frame (no ADTS header) to the muxer. + * The muxer wraps it in ADTS + PES + TS packets on the audio PID. + * @param muxer The muxer instance + * @param aac_data Raw AAC frame bytes + * @param aac_size Size of aac_data in bytes + * @param pts Presentation timestamp in microseconds + * @param sample_rate AAC sample rate in Hz + * @param channels AAC channel count + */ +void ts_muxer_push_aac(ts_muxer_t *muxer, uint8_t *aac_data, size_t aac_size, + uint64_t pts, int sample_rate, int channels); + /** * Set the SPS and PPS parameter sets for the H.264 stream. * These are stored internally and prepended to keyframes in the TS output. diff --git a/pip/window.h b/pip/window.h index 39ee5ed..09a8b6f 100644 --- a/pip/window.h +++ b/pip/window.h @@ -56,7 +56,7 @@ - (void) setEnable:(bool) en; @end -@interface Window : NSPanel *)getAvailableCameraResolutions:(NSString*)deviceId { @@ -2483,6 +2489,8 @@ -(void)startCameraCapture:(NSString*)deviceId{ // Set up audio preview if enabled - uses AVCaptureAudioPreviewOutput for automatic format handling camera_has_microphone = false; camera_audio_preview = nil; + camera_audio_output = nil; + camera_audio_sample_count = 0; if(camera_audio_enabled) { // Check microphone permission @@ -2531,6 +2539,18 @@ -(void)startCameraCapture:(NSString*)deviceId{ } else { [session addInput:audioInput]; + AVCaptureAudioDataOutput *audioDataOutput = [[AVCaptureAudioDataOutput alloc] init]; + dispatch_queue_t audioQueue = dispatch_queue_create("com.pip.camera.audio", DISPATCH_QUEUE_SERIAL); + [audioDataOutput setSampleBufferDelegate:self queue:audioQueue]; + if([session canAddOutput:audioDataOutput]) { + [session addOutput:audioDataOutput]; + camera_audio_output = audioDataOutput; + camera_has_microphone = true; + NSLog(@"Camera audio stream output configured for outgoing HLS audio"); + } else { + NSLog(@"Cannot add audio data output to camera session"); + } + // Use AVCaptureAudioPreviewOutput for automatic audio playback // This handles all format conversion automatically AVCaptureAudioPreviewOutput *audioPreview = [[AVCaptureAudioPreviewOutput alloc] init]; @@ -2578,7 +2598,17 @@ -(void)startCameraCapture:(NSString*)deviceId{ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if(!is_playing || isWinClosing || !camera_session) return; - // Only handle video output - audio is handled automatically by AVCaptureAudioPreviewOutput + if([output isKindOfClass:[AVCaptureAudioDataOutput class]]) { + camera_audio_sample_count++; + if(camera_audio_sample_count == 1 || (camera_audio_sample_count % 500 == 0)) { + NSLog(@"Camera audio callback samples=%llu", (unsigned long long)camera_audio_sample_count); + } + if(streamManager && [streamManager isStreaming]) { + [streamManager pushAudioSampleBuffer:sampleBuffer]; + } + return; + } + if(![output isKindOfClass:[AVCaptureVideoDataOutput class]]) { return; } @@ -2814,6 +2844,15 @@ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error API_AVAILABL - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type API_AVAILABLE(macos(12.3)){ if(!is_playing || isWinClosing) return; + if (@available(macOS 13.0, *)) { + if (type == SCStreamOutputTypeAudio) { + if (streamManager && [streamManager isStreaming]) { + [streamManager pushAudioSampleBuffer:sampleBuffer]; + } + return; + } + } + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); if (!imageBuffer) { return; @@ -2935,6 +2974,9 @@ - (void)startWindowStream { streamConfig.minimumFrameInterval = CMTimeMake(1, refreshRate); streamConfig.scalesToFit = NO; streamConfig.queueDepth = 2; + streamConfig.capturesAudio = YES; + streamConfig.sampleRate = 48000; + streamConfig.channelCount = 2; NSLog(@"startWindowStream: window_id=%d, windowFrame={%.1f,%.1f,%.1f,%.1f}, is_hidpi=%d, config=%lux%lu", window_id, windowFrame.origin.x, windowFrame.origin.y, windowFrame.size.width, windowFrame.size.height, @@ -2959,6 +3001,14 @@ - (void)startWindowStream { return; } + if (@available(macOS 13.0, *)) { + NSError *audioOutputError = nil; + BOOL audioOutputAdded = [self->window_stream addStreamOutput:self type:SCStreamOutputTypeAudio sampleHandlerQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) error:&audioOutputError]; + if (!audioOutputAdded || audioOutputError) { + NSLog(@"ScreenCaptureKit audio output unavailable: %@", audioOutputError); + } + } + // Start stream [self->window_stream startCaptureWithCompletionHandler:^(NSError * _Nullable startError) { if (startError) { From 7cf0f755dcfde26c8e9a2d09d2ff0f1e8ac24220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 17:22:20 +0000 Subject: [PATCH 14/19] Translated streaming menu to English --- .claude/settings.json | 9 ++ MULTI-WINDOW-PLAN.md | 322 ------------------------------------------ pip/window.m | 14 +- 3 files changed, 16 insertions(+), 329 deletions(-) create mode 100644 .claude/settings.json delete mode 100644 MULTI-WINDOW-PLAN.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e8c7958 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(lsof:*)", + "Bash(log show:*)", + "Read(//Users/ccarpio/Library/Developer/Xcode/DerivedData/**)" + ] + } +} diff --git a/MULTI-WINDOW-PLAN.md b/MULTI-WINDOW-PLAN.md deleted file mode 100644 index 4253db6..0000000 --- a/MULTI-WINDOW-PLAN.md +++ /dev/null @@ -1,322 +0,0 @@ -# Multi-Window PiP: Implementation Plan - -## Overview - -This document outlines the plan to enhance the PiP application's multi-window experience, allowing users to have multiple floating windows with different input sources (monitors, windows, cameras, HLS streams) simultaneously. - -**Current state:** The app already supports creating multiple windows via Cmd+N. Each `Window` (NSPanel subclass) independently manages its own capture session, renderer, and source selection. However, there is no window management UI, no guidance for new windows, no layout tools, and GPU resources are duplicated per window. - -**Goal:** Make multi-window a first-class, polished experience rather than just a side effect of "you can press Cmd+N again." - ---- - -## Technical Feasibility Analysis - -### Concurrent Capture Sessions - -| Source Type | API | Concurrent Limit | Notes | -|---|---|---|---| -| Display | CGDisplayStream | No hard limit | Bound by GPU bandwidth; practical limit ~4-6 | -| Window | SCStream (ScreenCaptureKit) | No hard limit | Queue depth should stay ≤ 8 per stream to avoid memory pressure | -| Camera | AVCaptureSession | **One session per device** | Same camera cannot be opened by two sessions; must fan out frames | -| HLS | AVPlayer | No hard limit | Network bandwidth is the bottleneck | -| AirPlay | Custom RAOP server | 10 sessions (current soft limit) | Already supports multi-session | - -**Key constraint:** A camera device is exclusive to one `AVCaptureSession`. If two windows want the same camera, a source-sharing layer is needed (deferred to Phase 3). - -### GPU Resources - -- `MTLDevice` is thread-safe and should be created once and shared across all windows. -- Each window can safely create its own `MTLCommandQueue` from the shared device. -- Pipeline states and shader libraries should be cached and reused. -- This reduces VRAM usage and avoids redundant device initialization. - -### Realistic Performance Bounds - -- **Target use case:** 2-4 simultaneous windows (typical user). -- **Soft limit:** 6 windows (show performance warning). -- **Hard limit:** 8 windows (configurable in preferences). -- This is a lightweight PiP tool, not OBS. Optimize for the common case. - ---- - -## UX Design Decisions - -### Window Creation Flow - -**Decision:** New windows open blank with an overlay hint. No modal picker. - -- Cmd+N creates a new window. -- If preference is "Clone current": new window copies the frontmost window's source. -- If preference is "Blank with hint" (default): new window shows a centered overlay message: _"Clic derecho para seleccionar fuente"_ (Right-click to select source). -- The overlay is a simple `NSTextField` on a semi-transparent `NSVisualEffectView`, centered on the window. -- The overlay disappears when a source is selected. - -### Window Identification - -- Each window's `title` is already set to the source name (e.g., "Display 1", "Safari - Google", "FaceTime HD Camera"). -- AppKit will use these titles in the Window menu automatically. -- When hovering over a borderless window, the title bar appears briefly (existing behavior), showing the source name. -- No color-coded borders for Phase 1 (adds visual noise). - -### Source Selection - -- **Keep right-click context menu per window.** It's simple, already works, and aligns with user mental model. -- No changes to the source selection mechanism in Phase 1. - -### Closing Behavior - -- Closing a window closes only that window (existing behavior). -- "Close All Windows" quits the app (matches Preview.app behavior). -- No confirmation dialog (not a destructive app). -- Menu bar persistence is a future consideration. - -### Layout Management - -- **Arrange in Grid:** Distributes all open PiP windows in a grid on the current screen. -- **Cascade:** Offsets windows diagonally from top-left. -- Both use `screen.visibleFrame` to respect menu bar and dock. -- Grid preserves each window's aspect ratio (scale to fit within cell, center in cell). - -### Preferences - -Two new preferences for Phase 1: -1. **New window behavior:** "Blank with hint" (default) / "Clone current window" -2. **Maximum simultaneous windows:** Numeric, default 8 - ---- - -## Implementation Phases - -### Phase 1: Foundation - -Minimum viable multi-window enhancement. Low risk, high impact. - -#### 1.1 Automatic Window List in Window Menu - -**Files:** `main.m` - -- Call `[NSApp setWindowsMenu:windowMenu]` after creating the Window menu. -- AppKit will automatically list all open `Window` instances by their `title`. -- Ensure each window has a meaningful title (already the case: source name is set in `-changeWindow:`). -- Add custom items above the auto-managed list: "Close All Windows", "Arrange in Grid", "Cascade". - -#### 1.2 Close All Windows - -**Files:** `main.m` - -- Add menu item "Cerrar todas las ventanas" (Close All Windows) with shortcut Opt+Cmd+W. -- Action: iterate `[NSApp windows]`, close each `Window` instance. -- App terminates when last window closes (existing behavior via `applicationShouldTerminateAfterLastWindowClosed:`). - -#### 1.3 Overlay Hint for Blank Windows - -**Files:** `window.m` - -- When a new window is created (non-AirPlay) and no source is selected: - - Add a `NSVisualEffectView` overlay (blending mode: behind window, material: dark) centered on the window. - - Inside it, a centered `NSTextField` with text: _"Clic derecho para seleccionar fuente"_. - - Style: white text, medium system font, non-editable, non-selectable. -- When `-changeWindow:` is called and a source is selected, remove (hide) the overlay. -- The overlay should resize with the window (auto-resizing mask or constraints). - -#### 1.4 Arrange in Grid - -**Files:** `main.m` or a new utility function in `window.m` - -Algorithm: -1. Collect all open `Window` instances from `[NSApp windows]`. -2. Get `visibleFrame` of the screen where the frontmost window resides. -3. Calculate grid: `cols = ceil(sqrt(N))`, `rows = ceil(N / cols)`. -4. Cell size: `cellW = visibleFrame.width / cols`, `cellH = visibleFrame.height / rows`. -5. For each window: - - Calculate target size: scale to fit within cell while preserving aspect ratio. - - Clamp to window's `minSize`. - - Center within the cell. - - Animate with `setFrame:display:animate:`. - -Edge cases: -- Single window: center it on screen, don't resize. -- Respect each window's minimum size. - -#### 1.5 Cascade - -**Files:** `main.m` or utility function - -Algorithm: -1. Start at top-left of `visibleFrame`. -2. Each window offset by (25, -25) from the previous. -3. Don't resize windows. -4. Wrap back to top-left if cascade goes off-screen. - -#### 1.6 Shared MTLDevice - -**Files:** `main.m` (or app delegate), `window.m`, `metalRenderer.m` - -- Create `MTLCreateSystemDefaultDevice()` once in the app delegate and store it. -- Pass the shared device to each `Window` at creation time. -- `Window` passes it to `MetalRenderer` init. -- `MetalRenderer` uses the shared device but creates its own `MTLCommandQueue`. -- The device is retained by the app delegate for the app's lifetime. - -#### 1.7 New Preferences - -**Files:** `preferences.m`, `preferences.h` - -Add two new rows to the preferences table: -1. **"Comportamiento de nueva ventana"** (New window behavior): Popup with options "En blanco con pista" / "Clonar ventana actual". -2. **"Máximo de ventanas simultáneas"** (Max simultaneous windows): Popup with options 2, 4, 6, 8, 10. - -#### 1.8 Max Windows Enforcement - -**Files:** `main.m` (in the Cmd+N handler) - -- Before creating a new window, check count of open `Window` instances. -- If at limit: - - First time: show `NSAlert` with message _"Se alcanzó el máximo de ventanas. Puedes cambiar el límite en Preferencias."_ and "OK" + "Preferencias..." buttons. - - Subsequent times: just `NSBeep()`. -- Track "has shown alert" with a simple static boolean. - -#### 1.9 Keyboard Shortcut to Cycle Windows - -**Files:** `main.m` - -- Add Cmd+` (backtick) to cycle through open PiP windows (this is standard macOS behavior and may already work via AppKit if the Window menu is properly configured). -- Verify it works; if not, add a manual implementation that calls `[NSApp windows]` and `makeKeyAndOrderFront:` on the next window. - ---- - -### Phase 2: Management - -Better visibility and control over multiple windows. - -#### 2.1 Window Manager Panel - -- New `NSPanel` with an `NSTableView`. -- Columns: source icon + name, type (Display/Window/Camera/HLS), status (Active/Paused/Disconnected). -- Click a row to focus that window. -- Context menu on row: Close, Reassign Source, Clone. -- Opened via Window menu item "Administrador de ventanas" or keyboard shortcut. - -#### 2.2 Clone Current Window - -- Menu item "Clonar ventana" (Clone Window) with shortcut Shift+Cmd+N. -- Creates a new window and copies the frontmost window's source (`WindowSel`). -- Calls `-changeWindow:` on the new window with the same selection. -- For cameras: warn that the same camera can't be opened twice; offer to pick a different source. - -#### 2.3 Disconnect / Error Handling - -Uniform handling across all windows: -- **Display disconnected:** Stop stream, show overlay _"Pantalla desconectada"_ with option to pick a new source. -- **Camera unplugged:** Stop session, show overlay _"Cámara no disponible"_. -- **Captured window closed:** Stop stream, show overlay _"Ventana cerrada"_. -- All overlays include a "Select new source" button. - -#### 2.4 Performance Warnings - -- When opening the 6th+ window, show a brief non-modal notification: _"Muchas ventanas abiertas. El rendimiento puede verse afectado."_ -- Optional: show FPS indicator per window (toggle in preferences). - ---- - -### Phase 3: Power Features - -Architectural changes for advanced users. - -#### 3.1 CaptureSourceController Abstraction - -- Create per-source-type controllers: `DisplaySourceController`, `WindowSourceController`, `CameraSourceController`, `HLSSourceController`. -- Each has `-start`, `-stop`, and a delegate callback delivering native frame data. -- Window becomes a consumer that receives frames from a controller. -- This decouples capture logic from window management. - -#### 3.2 Source Sharing - -- When multiple windows select the same source, they attach as consumers to one `CaptureSourceController`. -- Especially important for cameras (AVCaptureSession exclusivity). -- One capture session fans out frames to multiple renderers. -- Source-specific controls (resolution, audio) live on the controller; any window can present them but changes affect all attached windows. - -#### 3.3 Adaptive Throttling - -- Monitor total capture load (aggregate FPS, frame times). -- When load exceeds threshold, automatically reduce FPS or resolution on lower-priority windows. -- Priority can be based on: frontmost > visible > minimized. -- ScreenCaptureKit `queueDepth` kept ≤ 8 per stream. - -#### 3.4 Per-Window Quality Settings - -- Move renderer type, FPS cap, resolution, and crop settings to per-window preferences. -- Access via right-click menu > "Window Settings" submenu. -- Global preferences become defaults for new windows. - -#### 3.5 Session Persistence / Restore - -- On quit, save the list of open windows with their source identifiers, positions, and sizes to `NSUserDefaults`. -- On launch, attempt to restore the session: - - For displays: match by EDID identifier (already used for custom names). - - For windows: match by app name + window title (best effort). - - For cameras: match by `uniqueID`. - - For HLS: match by URL. -- If a source is unavailable, open the window blank with a "Source unavailable" overlay. - ---- - -## Summary - -| Phase | Effort | Impact | Risk | -|---|---|---|---| -| Phase 1 | Low-Medium | High (usable multi-window) | Low | -| Phase 2 | Medium | Medium (better management) | Low | -| Phase 3 | High | Medium (power users) | Medium (architectural changes) | - -Phase 1 can be implemented without major refactoring — it builds on the existing architecture. Phases 2 and 3 introduce new abstractions but can be done incrementally. - -The key insight is that the app already supports multiple independent windows. The work is primarily about **UX polish** (guiding users, managing windows, arranging layouts) and **resource optimization** (shared GPU device, performance limits), not about fundamental architectural changes. - ---- - -## Phase 1 Implementation Status - -**All Phase 1 items implemented.** Files modified: - -| File | Changes | -|---|---| -| `main.m` | Shared MTLDevice (dispatch_once), allPipWindows/visiblePipWindows helpers, closeAllWindows (closes prefs first), arrangeInGrid, arrangeInCascade, max windows enforcement with alert, clone window behavior via preference, Window menu auto-population via setWindowsMenu, applicationShouldTerminateAfterLastWindowClosed→YES | -| `window.m` | PassthroughView class (hitTest→nil), sourceHintOverlay ivar + setup in init (PassthroughView with centered label), hide/show in changeWindow, cleanup in close, cloneSourceToWindow: method | -| `window.h` | Added cloneSourceToWindow: declaration | -| `metalRenderer.m` | Uses getSharedMTLDevice() instead of MTLCreateSystemDefaultDevice() | -| `imageRenderer.h` | Added Metal import and getSharedMTLDevice() declaration | -| `preferences.m` | Two new prefs (new_window_behavior, max_windows), panel height 230→290, auto-quit when prefs close and no PiP windows remain (dispatch_async for safety) | - -### Issues found during Codex review and fixed: -1. Overlay blocked right-click events → used PassthroughView with hitTest:→nil -2. pipWindows excluded minimized windows → split into allPipWindows/visiblePipWindows -3. Grid/cascade used keyWindow's screen (could be Preferences) → use first PiP window's screen -4. Overlay was destroyed instead of hidden → changed to hide/show so it reappears -5. getSharedMTLDevice wasn't thread-safe → added dispatch_once -6. closeAllWindows didn't close preferences panel → added explicit close -7. Prefs close could leave app running with no windows → added auto-quit check -8. new_window_behavior pref was unused → implemented clone via cloneSourceToWindow: - -### Note on 1.9 (Cmd+` cycling): -Standard macOS Cmd+` window cycling should work automatically now that the Window menu is properly configured with `setWindowsMenu:`. AppKit handles this natively for all windows registered in the windows menu. - ---- - -## Phase 2 Implementation Status - -**All Phase 2 items implemented.** Files modified: - -| File | Changes | -|---|---| -| `main.m` | "Clonar ventana" menu item (Shift+Cmd+N) with cloneCurrentWindow method, performance warning on 6th+ window (shown once), WindowManagerPanel class (NSPanel with 3-column NSTableView: source name, type, status), "Administrador de ventanas" menu item (Opt+Cmd+M), auto-refresh timer with weakSelf pattern | -| `window.m` | Promoted hintLabel to ivar for dynamic text, showDisconnectOverlay: method (stops captures, shows overlay with disconnect message), handleDisplayDisconnected: for display removal, cameraSessionError: for camera unplug/error, CGDisplayReconfigurationCallback registration/unregistration, AVCaptureSession/Device notification observers in startCameraCapture/stopCameraCapture, SCStream didStopWithError: now shows disconnect overlay, CGDisplayStream callback handles kCGDisplayStreamFrameStatusStopped, sourceType/sourceStatus public methods, cloneSourceToWindow: returns BOOL, forward declaration category for C callback | -| `window.h` | Added sourceType, sourceStatus declarations, changed cloneSourceToWindow: return type to BOOL | - -### Issues found during Codex review and fixed: -1. WindowManagerPanel refresh timer created retain cycle → used weakSelf pattern in block -2. CGDisplayReconfigurationCallback uses __bridge (no retain) → safe because close always unregisters before dealloc (NSWindow lifecycle guarantees close before dealloc) -3. Camera notification handler reads camera_session on arbitrary thread → benign pointer check, all state mutation dispatched to main queue -4. showDisconnectOverlay: verified as idempotent — safe to call from multiple error paths simultaneously diff --git a/pip/window.m b/pip/window.m index a357609..ad38777 100644 --- a/pip/window.m +++ b/pip/window.m @@ -2013,12 +2013,12 @@ - (void)rightMouseDown:(NSEvent *)theEvent { NSMenu *streamMenu = [[NSMenu alloc] init]; if (streamManager && [streamManager isStreaming]) { - ADD_MENU_ITEM(streamMenu, @"Detener transmisión", @selector(stopStreamAction:), NULL) + ADD_MENU_ITEM(streamMenu, @"Stop Streaming", @selector(stopStreamAction:), NULL) [streamMenu addItem:[NSMenuItem separatorItem]]; - ADD_MENU_ITEM(streamMenu, @"Copiar URL", @selector(copyStreamURL:), NULL) - ADD_MENU_ITEM(streamMenu, @"Abrir en navegador", @selector(openStreamInBrowser:), NULL) + ADD_MENU_ITEM(streamMenu, @"Copy URL", @selector(copyStreamURL:), NULL) + ADD_MENU_ITEM(streamMenu, @"Open in Browser", @selector(openStreamInBrowser:), NULL) } else { - ADD_MENU_ITEM(streamMenu, @"Iniciar transmisión", @selector(startStreamAction:), NULL) + ADD_MENU_ITEM(streamMenu, @"Start Streaming", @selector(startStreamAction:), NULL) } [streamMenu addItem:[NSMenuItem separatorItem]]; @@ -2027,7 +2027,7 @@ - (void)rightMouseDown:(NSEvent *)theEvent { NSMenu *qualitySubmenu = [[NSMenu alloc] init]; StreamQuality currentQ = streamManager ? [streamManager currentQuality] : StreamQualityMedium; - NSArray *qualityNames = @[@"Baja (720p)", @"Media (1080p)", @"Alta (nativa)"]; + NSArray *qualityNames = @[@"Low (720p)", @"Medium (1080p)", @"High (native)"]; NSArray *qualityValues = @[@(StreamQualityLow), @(StreamQualityMedium), @(StreamQualityHigh)]; for (int i = 0; i < 3; i++) { @@ -2039,7 +2039,7 @@ - (void)rightMouseDown:(NSEvent *)theEvent { } } // End of loop through quality options - ADD_MENU_ITEM(streamMenu, @"Calidad", nil, NULL, { + ADD_MENU_ITEM(streamMenu, @"Quality", nil, NULL, { [item setSubmenu:qualitySubmenu]; }) @@ -2053,7 +2053,7 @@ - (void)rightMouseDown:(NSEvent *)theEvent { } } - ADD_MENU_ITEM(theMenu, @"Transmitir", nil, NULL, { + ADD_MENU_ITEM(theMenu, @"Stream", nil, NULL, { [item setSubmenu:streamMenu]; }) } From e23797c941e1fddd580b8c1236bd1c67ac7b63b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 17:27:45 +0000 Subject: [PATCH 15/19] Update README with live streaming documentation --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f311be..024f244 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Always on top window preview with AirPlay receiver support (if on macOS 12+, tur ## Features * Clone any visibile window * Clone multiple active display -* Camera preview -* HLS streaming +* Camera preview with audio monitoring +* HLS live streaming of any preview (window, display, or camera) * Airplay sender (unstable) * Crop the preview * Auto and manual resize preserving the aspect ratio @@ -30,6 +30,40 @@ Always on top window preview with AirPlay receiver support (if on macOS 12+, tur * Minimal modern UI * Upto 10 parallel airplay sessions (soft limit) +## Live Streaming + +PiP can stream any preview window over your local network using HLS (HTTP Live Streaming). Any device with a web browser on the same network can watch the live feed. + +### How to use +1. Right-click any active preview window and select **Stream > Start Streaming** +2. The stream URL (e.g. `http://192.168.1.42:8080`) appears at the bottom of the Stream menu +3. Open that URL on any device's browser to watch the live stream +4. Use **Copy URL** or **Open in Browser** for quick access + +### Quality presets +| Preset | Resolution | Bitrate | FPS | +|--------|-----------|---------|-----| +| Low | 720p | 1.5 Mbps | 24 | +| Medium | 1080p | 3 Mbps | 30 | +| High | Native | 6 Mbps | 30 | + +Change quality on the fly via **Stream > Quality**. + +### Architecture +The streaming pipeline is fully self-contained with no third-party dependencies: + +``` +Frame Capture → H.264 Video Encoder → MPEG-TS Muxer → HLS Writer → HTTP Server + ↑ + Audio Capture +``` + +* **Frame Capture** – grabs RGBA frames from the preview's OpenGL/Metal renderer +* **Video Encoder** – hardware-accelerated H.264 encoding via VideoToolbox +* **TS Muxer** – multiplexes video (and optional audio) into MPEG-TS segments +* **HLS Writer** – manages a ring buffer of `.ts` segments and generates the `.m3u8` playlist +* **HTTP Server** – lightweight embedded server that serves the playlist, segments, and a built-in web viewer with hls.js + ## Installation ### Manual download From 483e4ce7939ff2811a997ebfb8deedb3f4af0629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 19:42:22 +0000 Subject: [PATCH 16/19] Add unified default source with camera naming and sorting --- pip/preferences.h | 5 + pip/preferences.m | 343 +++++++++++++++++++++++++++++++++------------- pip/window.m | 39 ++++-- 3 files changed, 281 insertions(+), 106 deletions(-) diff --git a/pip/preferences.h b/pip/preferences.h index d415ae3..d2d20ae 100644 --- a/pip/preferences.h +++ b/pip/preferences.h @@ -19,8 +19,13 @@ NSObject* getPref(NSString* key); NSObject* getPrefOption(NSString* key); void setPref(NSString* key, NSObject* val); NSArray* getDisplayList(void); +NSArray* getSourceList(void); +NSDictionary* getDefaultSourcePreference(void); NSString* getDisplayNameForId(CGDirectDisplayID displayId); +NSString* getCameraNameForId(NSString* cameraId); void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name); +void setCustomCameraName(NSString* cameraId, NSString* name); +void showSourceNamesPanel(void); void showDisplayNamesPanel(void); @interface Preferences : NSPanel diff --git a/pip/preferences.m b/pip/preferences.m index 497c27c..bd3faa3 100644 --- a/pip/preferences.m +++ b/pip/preferences.m @@ -9,12 +9,49 @@ #import "preferences.h" #import #import +#import #if __has_include() #import #endif Preferences* global_pref = nil; -static NSPanel* displayNamesPanel = nil; +static NSPanel* sourceNamesPanel = nil; + +static NSDictionary* sourceNone(void){ + return @{@"type": @"none"}; +} + +static NSDictionary* sourceFromDisplayId(NSNumber* displayId){ + return @{@"type": @"display", @"id": displayId}; +} + +static NSDictionary* sourceFromCameraId(NSString* cameraId){ + return @{@"type": @"camera", @"id": cameraId}; +} + +static NSDictionary* normalizeSourcePreference(NSObject* source){ + if([source isKindOfClass:[NSDictionary class]]){ + NSDictionary* sourceDict = (NSDictionary*)source; + NSString* type = sourceDict[@"type"]; + NSObject* sourceId = sourceDict[@"id"]; + if([type isEqualToString:@"display"] && [sourceId isKindOfClass:[NSNumber class]] && [(NSNumber*)sourceId intValue] > 0){ + return sourceFromDisplayId((NSNumber*)sourceId); + } + if([type isEqualToString:@"camera"] && [sourceId isKindOfClass:[NSString class]] && ((NSString*)sourceId).length > 0){ + return sourceFromCameraId((NSString*)sourceId); + } + if([type isEqualToString:@"none"]){ + return sourceNone(); + } + } + if([source isKindOfClass:[NSNumber class]] && [(NSNumber*)source intValue] > 0){ + return sourceFromDisplayId((NSNumber*)source); + } + if([source isKindOfClass:[NSString class]] && ((NSString*)source).length > 0){ + return sourceFromCameraId((NSString*)source); + } + return sourceNone(); +} /** * Gets a stable identifier for a display using EDID data (vendor, model, serial). @@ -55,6 +92,18 @@ return customNames[legacyKey]; } // End of getCustomDisplayNameForId() +/** + * Gets the custom camera name for a given camera unique ID. + * @param cameraId The AVCaptureDevice unique ID + * @return The custom name if set, otherwise nil + */ +NSString* getCustomCameraNameForId(NSString* cameraId){ + if(!cameraId || cameraId.length == 0) return nil; + NSDictionary* customNames = (NSDictionary*)getPref(@"camera_custom_names"); + if(!customNames) return nil; + return customNames[cameraId]; +} // End of getCustomCameraNameForId() + /** * Gets the display name for a given display ID, using custom name if available. * @param displayId The CGDirectDisplayID of the display @@ -76,6 +125,23 @@ return [NSString stringWithFormat:@"Display %u", displayId]; } // End of getDisplayNameForId() +/** + * Gets the camera name for a given camera ID, using custom name if available. + * @param cameraId The AVCaptureDevice unique ID + * @return The custom name if set, otherwise the system camera name + */ +NSString* getCameraNameForId(NSString* cameraId){ + NSString* customName = getCustomCameraNameForId(cameraId); + if(customName && customName.length > 0) return customName; + + AVCaptureDevice* camera = [AVCaptureDevice deviceWithUniqueID:cameraId]; + if(camera){ + NSString* localizedName = [camera localizedName]; + if(localizedName && localizedName.length > 0) return localizedName; + } + return cameraId && cameraId.length > 0 ? cameraId : @"Camera"; +} // End of getCameraNameForId() + /** * Sets a custom display name for a given display ID. * Uses stable identifier (EDID-based) to persist names across reboots. @@ -107,6 +173,26 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ [[NSUserDefaults standardUserDefaults] setObject:customNames forKey:@"display_custom_names"]; } // End of setCustomDisplayName() +/** + * Sets a custom camera name for a given camera unique ID. + * @param cameraId The AVCaptureDevice unique ID + * @param name The custom name to set (empty string to clear) + */ +void setCustomCameraName(NSString* cameraId, NSString* name){ + if(!cameraId || cameraId.length == 0) return; + + NSDictionary* existingNames = (NSDictionary*)[[NSUserDefaults standardUserDefaults] objectForKey:@"camera_custom_names"]; + NSMutableDictionary* customNames = existingNames ? [existingNames mutableCopy] : [[NSMutableDictionary alloc] init]; + + if(name && name.length > 0){ + customNames[cameraId] = name; + } else { + [customNames removeObjectForKey:cameraId]; + } + + [[NSUserDefaults standardUserDefaults] setObject:customNames forKey:@"camera_custom_names"]; +} // End of setCustomCameraName() + /** * Gets the list of available displays with their names (using custom names if set). * @return An array of dictionaries with "name" and "id" keys @@ -123,12 +209,75 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ return displays; } // End of getDisplayList() +/** + * Returns the default source preference with migration from legacy default_display. + * @return A dictionary in the form {type: "none|display|camera", id: ...} + */ +NSDictionary* getDefaultSourcePreference(void){ + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSObject* sourcePref = [defaults objectForKey:@"default_source"]; + if(sourcePref){ + NSDictionary* normalized = normalizeSourcePreference(sourcePref); + if(![normalized isEqual:sourcePref]){ + [defaults setObject:normalized forKey:@"default_source"]; + } + return normalized; + } + + NSObject* legacyDefaultDisplay = [defaults objectForKey:@"default_display"]; + NSDictionary* migrated = normalizeSourcePreference(legacyDefaultDisplay); + [defaults setObject:migrated forKey:@"default_source"]; + return migrated; +} // End of getDefaultSourcePreference() + +/** + * Gets the list of available capture sources (displays + cameras). + * @return An array of dictionaries with "name" and "value" keys + */ +NSArray* getSourceList(void){ + NSMutableArray* sources = [[NSMutableArray alloc] init]; + [sources addObject:@{@"name": @"None", @"value": sourceNone()}]; + + NSMutableArray* displaySources = [[NSMutableArray alloc] init]; + for(NSScreen* screen in [NSScreen screens]){ + NSDictionary* dict = [screen deviceDescription]; + CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; + NSString* name = [NSString stringWithFormat:@"Display - %@", getDisplayNameForId(did)]; + [displaySources addObject:@{ + @"name": name, + @"value": sourceFromDisplayId([NSNumber numberWithUnsignedInt:did]), + }]; + } + [displaySources sortUsingComparator:^NSComparisonResult(NSDictionary* a, NSDictionary* b) { + return [a[@"name"] localizedCaseInsensitiveCompare:b[@"name"]]; + }]; + [sources addObjectsFromArray:displaySources]; + + NSMutableArray* cameraSources = [[NSMutableArray alloc] init]; + NSArray* cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + for(AVCaptureDevice* camera in cameras){ + NSString* cameraId = [camera uniqueID]; + if(!cameraId || cameraId.length == 0) continue; + NSString* name = [NSString stringWithFormat:@"Camera - %@", getCameraNameForId(cameraId)]; + [cameraSources addObject:@{ + @"name": name, + @"value": sourceFromCameraId(cameraId), + }]; + } + [cameraSources sortUsingComparator:^NSComparisonResult(NSDictionary* a, NSDictionary* b) { + return [a[@"name"] localizedCaseInsensitiveCompare:b[@"name"]]; + }]; + [sources addObjectsFromArray:cameraSources]; + + return sources; +} // End of getSourceList() + typedef enum{ OptionTypeNumber, OptionTypeSelect, OptionTypeCheckBox, OptionTypeTextInput, - OptionTypeDisplaySelect, + OptionTypeSourceSelect, OptionTypeButton, } OptionType; @@ -139,8 +288,8 @@ void setCustomDisplayName(CGDirectDisplayID displayId, NSString* name){ NSMutableArray* prefs = [NSMutableArray arrayWithArray:@[ OPTION(hidpi, "Use HiDPI mode", CheckBox, [NSNull null], @1, @"on supported displays"), OPTION(renderer, "Display Renderer", Select, (@[@"Metal", @"Opengl"]), [NSNumber numberWithInt:DisplayRendererTypeOpenGL], [NSNull null]), - OPTION(default_display, "Default Display", DisplaySelect, [NSNull null], @-1, [NSNull null]), - OPTION(display_names, "Display Names", Button, [NSNull null], [NSNull null], @"Configure..."), + OPTION(default_source, "Default Source", SourceSelect, [NSNull null], sourceNone(), [NSNull null]), + OPTION(source_names, "Source Names", Button, [NSNull null], [NSNull null], @"Configure..."), #ifndef NO_AIRPLAY OPTION(airplay, "AirPlay Receiver", CheckBox, [NSNull null], @0, @"Use PiP as Airplay receiver"), OPTION(airplay_scale_factor, "AirPlay Scale factor", Select, (@[@"1.00", @"2.00", @"3.00", @"Default"]), @3, [NSNull null]), @@ -288,15 +437,15 @@ - (void)onSelect:(NSMenuItem*)sender{ setPref(sender.identifier, [NSNumber numberWithLong:index]); } -- (void)onDisplaySelect:(NSMenuItem*)sender{ - NSNumber* displayId = [sender representedObject]; -// NSLog(@"onDisplaySelect: %@ -> %@", sender.identifier, displayId); - setPref(sender.identifier, displayId); +- (void)onSourceSelect:(NSMenuItem*)sender{ + NSDictionary* source = normalizeSourcePreference([sender representedObject]); +// NSLog(@"onSourceSelect: %@ -> %@", sender.identifier, source); + setPref(sender.identifier, source); } - (void)onButtonClick:(NSButton*)sender{ - if([sender.identifier isEqual:@"display_names"]){ - showDisplayNamesPanel(); + if([sender.identifier isEqual:@"source_names"]){ + showSourceNamesPanel(); } } // End of onButtonClick() @@ -384,23 +533,24 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null view = textField; break; } - case OptionTypeDisplaySelect:{ + case OptionTypeSourceSelect:{ NSPopUpButton* button = [[NSPopUpButton alloc] init]; button.translatesAutoresizingMaskIntoConstraints = false; button.menu = [[NSMenu alloc] init]; - NSArray* displays = getDisplayList(); - int savedDisplayId = [(NSNumber*)value intValue]; + NSDictionary* savedSource = getDefaultSourcePreference(); + NSArray* sources = getSourceList(); int selectedIndex = 0; - for(int i = 0; i < displays.count; i++){ - NSDictionary* display = displays[i]; - NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:display[@"name"] action:@selector(onDisplaySelect:) keyEquivalent:@""]; + for(int i = 0; i < sources.count; i++){ + NSDictionary* source = sources[i]; + NSDictionary* sourceValue = normalizeSourcePreference(source[@"value"]); + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:source[@"name"] action:@selector(onSourceSelect:) keyEquivalent:@""]; item.target = self; item.identifier = key; - item.representedObject = display[@"id"]; + item.representedObject = sourceValue; [button.menu addItem:item]; - if([display[@"id"] intValue] == savedDisplayId) selectedIndex = i; - } // End of loop through displays + if([sourceValue isEqual:savedSource]) selectedIndex = i; + } // End of loop through sources [button selectItem:[button.menu itemArray][selectedIndex]]; view = button; break; @@ -445,7 +595,7 @@ - (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex{ */ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row{ CGFloat baseHeight = 26; - // Add 12pt padding below Display Renderer, Default Display, and Display Names rows + // Add 12pt padding below Display Renderer, Default Source, and Source Names rows if(row == 1 || row == 2 || row == 3){ return baseHeight + 12; } @@ -480,26 +630,27 @@ - (void)windowWillClose:(NSNotification *)notification{ @end -#pragma mark - Display Names Panel +#pragma mark - Source Names Panel -@interface DisplayNamesPanel : NSPanel +@interface SourceNamesPanel : NSPanel @property (nonatomic, strong) NSTableView* tableView; -@property (nonatomic, strong) NSMutableArray* displayData; +@property (nonatomic, strong) NSMutableArray* sourceData; @property (nonatomic, strong) NSMutableArray* textFields; @property (nonatomic, strong) NSButton* okButton; - (void)refreshPreferencesWindow; +- (void)loadSourceData; - (void)completeTabOrder; @end -@implementation DisplayNamesPanel +@implementation SourceNamesPanel /** - * Initializes the display names panel. + * Initializes the source names panel. * @return The initialized panel */ -(id)init{ self = [super - initWithContentRect:NSMakeRect(0, 0, 450, 200) + initWithContentRect:NSMakeRect(0, 0, 480, 240) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:YES ]; @@ -507,29 +658,26 @@ -(id)init{ self.level = NSFloatingWindowLevel; self.collectionBehavior = NSWindowCollectionBehaviorManaged | NSWindowCollectionBehaviorParticipatesInCycle; self.becomesKeyOnlyIfNeeded = NO; - [self setTitle:@"Display Names"]; + [self setTitle:@"Source Names"]; - [self loadDisplayData]; + [self loadSourceData]; _textFields = [[NSMutableArray alloc] init]; NSView* rootView = [[NSView alloc] init]; rootView.translatesAutoresizingMaskIntoConstraints = false; - // Create scroll view for table NSScrollView* scrollView = [[NSScrollView alloc] init]; scrollView.hasHorizontalScroller = false; scrollView.hasVerticalScroller = true; scrollView.translatesAutoresizingMaskIntoConstraints = false; [rootView addSubview:scrollView]; - // Create OK button _okButton = [NSButton buttonWithTitle:@"OK" target:self action:@selector(onOKClick:)]; _okButton.translatesAutoresizingMaskIntoConstraints = false; _okButton.bezelStyle = NSBezelStyleRounded; - _okButton.keyEquivalent = @"\r"; // Enter key + _okButton.keyEquivalent = @"\r"; [rootView addSubview:_okButton]; - // Layout constraints [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]]; [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeRight multiplier:1 constant:0]]; [rootView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:rootView attribute:NSLayoutAttributeTop multiplier:1 constant:0]]; @@ -549,13 +697,13 @@ -(id)init{ _tableView.rowHeight = 28; NSTableColumn* systemNameCol = [[NSTableColumn alloc] initWithIdentifier:@"systemName"]; - systemNameCol.title = @"System Name"; - systemNameCol.width = 150; + systemNameCol.title = @"Source"; + systemNameCol.width = 220; [_tableView addTableColumn:systemNameCol]; NSTableColumn* customNameCol = [[NSTableColumn alloc] initWithIdentifier:@"customName"]; customNameCol.title = @"Custom Name"; - customNameCol.width = 250; + customNameCol.width = 240; [_tableView addTableColumn:customNameCol]; scrollView.documentView = _tableView; @@ -566,33 +714,20 @@ -(id)init{ NSPoint point = NSMakePoint(screenSize.width/2 - windowSize.width/2, screenSize.height/2 - windowSize.height/2); [self setFrameOrigin:point]; - // Complete tab order after table is set up [_tableView reloadData]; [self completeTabOrder]; return self; } // End of init() -/** - * Called when OK button is clicked. Commits pending edits and closes the panel. - * @param sender The button that was clicked - */ - (void)onOKClick:(NSButton*)sender{ - // Commit any pending text field edits by resigning first responder [self makeFirstResponder:nil]; - - // Update preferences window immediately [self refreshPreferencesWindow]; - [self close]; } // End of onOKClick() -/** - * Refreshes the preferences window to show updated display names. - */ - (void)refreshPreferencesWindow{ if(global_pref){ - // Find the table view in the preferences window and reload it NSView* contentView = [global_pref contentView]; for(NSView* subview in contentView.subviews){ if([subview isKindOfClass:[NSScrollView class]]){ @@ -608,10 +743,12 @@ - (void)refreshPreferencesWindow{ } // End of refreshPreferencesWindow() /** - * Loads display data from connected screens. + * Loads source data from connected displays and cameras. */ -- (void)loadDisplayData{ - _displayData = [[NSMutableArray alloc] init]; +- (void)loadSourceData{ + _sourceData = [[NSMutableArray alloc] init]; + + NSMutableArray* displayData = [[NSMutableArray alloc] init]; for(NSScreen* screen in [NSScreen screens]){ NSDictionary* dict = [screen deviceDescription]; CGDirectDisplayID did = [dict[@"NSScreenNumber"] unsignedIntValue]; @@ -620,22 +757,44 @@ - (void)loadDisplayData{ NSString* customName = getCustomDisplayNameForId(did); if(!customName) customName = @""; - [_displayData addObject:[@{ + [displayData addObject:[@{ + @"type": @"display", @"id": [NSNumber numberWithUnsignedInt:did], - @"systemName": systemName, - @"customName": customName + @"systemName": [NSString stringWithFormat:@"Display - %@", systemName], + @"customName": customName, } mutableCopy]]; } // End of loop through screens -} // End of loadDisplayData() + [displayData sortUsingComparator:^NSComparisonResult(NSDictionary* a, NSDictionary* b) { + return [a[@"systemName"] localizedCaseInsensitiveCompare:b[@"systemName"]]; + }]; + [_sourceData addObjectsFromArray:displayData]; + + NSMutableArray* cameraData = [[NSMutableArray alloc] init]; + NSArray* cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + for(AVCaptureDevice* camera in cameras){ + NSString* cameraId = [camera uniqueID]; + if(!cameraId || cameraId.length == 0) continue; + NSString* systemName = [camera localizedName]; + if(!systemName || systemName.length == 0) systemName = @"Camera"; + NSString* customName = getCustomCameraNameForId(cameraId); + if(!customName) customName = @""; + + [cameraData addObject:[@{ + @"type": @"camera", + @"id": cameraId, + @"systemName": [NSString stringWithFormat:@"Camera - %@", systemName], + @"customName": customName, + } mutableCopy]]; + } // End of loop through cameras + [cameraData sortUsingComparator:^NSComparisonResult(NSDictionary* a, NSDictionary* b) { + return [a[@"systemName"] localizedCaseInsensitiveCompare:b[@"systemName"]]; + }]; + [_sourceData addObjectsFromArray:cameraData]; +} // End of loadSourceData() -/** - * Completes the circular tab order for keyboard navigation. - * Chain: first text field -> ... -> last text field -> OK button -> first text field - */ - (void)completeTabOrder{ if(_textFields.count == 0) return; - // Find first and last valid text fields NSTextField* firstField = nil; NSTextField* lastField = nil; @@ -653,28 +812,27 @@ - (void)completeTabOrder{ } // End of completeTabOrder() - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{ - return _displayData.count; + return _sourceData.count; } - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row{ NSTableCellView* cell = [[NSTableCellView alloc] init]; - NSMutableDictionary* display = _displayData[row]; + NSMutableDictionary* source = _sourceData[row]; if([tableColumn.identifier isEqual:@"systemName"]){ NSTextField* text = [[NSTextField alloc] init]; - NSString* systemName = display[@"systemName"]; + NSString* systemName = source[@"systemName"]; if(systemName && systemName.length > 0){ text.stringValue = systemName; text.textColor = [NSColor secondaryLabelColor]; } else { - text.stringValue = @"(Unknown display)"; - // Use italic font for placeholder + text.stringValue = @"(Unknown source)"; NSFont* currentFont = [NSFont systemFontOfSize:[NSFont systemFontSize]]; NSFontDescriptor* italicDescriptor = [currentFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitItalic]; if(italicDescriptor){ text.font = [NSFont fontWithDescriptor:italicDescriptor size:currentFont.pointSize]; } - text.textColor = [NSColor tertiaryLabelColor]; // More muted than secondaryLabelColor + text.textColor = [NSColor tertiaryLabelColor]; } text.editable = false; text.drawsBackground = false; @@ -686,9 +844,9 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null [cell addConstraint:[NSLayoutConstraint constraintWithItem:text attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1 constant:-8]]; } else if([tableColumn.identifier isEqual:@"customName"]){ NSTextField* textField = [[NSTextField alloc] init]; - textField.stringValue = display[@"customName"]; - NSString* systemName = display[@"systemName"]; - textField.placeholderString = (systemName && systemName.length > 0) ? systemName : @"Enter display name"; + textField.stringValue = source[@"customName"]; + NSString* systemName = source[@"systemName"]; + textField.placeholderString = (systemName && systemName.length > 0) ? systemName : @"Enter source name"; textField.editable = YES; textField.selectable = YES; textField.bezeled = YES; @@ -700,13 +858,11 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null textField.tag = row; textField.cell.scrollable = YES; - // Add to text fields array for tab navigation while(_textFields.count <= (NSUInteger)row){ [_textFields addObject:[NSNull null]]; } _textFields[row] = textField; - // Set up tab order if(row > 0 && _textFields.count > 1 && _textFields[row-1] != [NSNull null]){ NSTextField* prevField = _textFields[row-1]; [prevField setNextKeyView:textField]; @@ -727,29 +883,25 @@ - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(null - (void)controlTextDidEndEditing:(NSNotification *)notification{ NSTextField* textField = notification.object; NSInteger row = textField.tag; - if(row >= 0 && row < (NSInteger)_displayData.count){ - NSMutableDictionary* display = _displayData[row]; + if(row >= 0 && row < (NSInteger)_sourceData.count){ + NSMutableDictionary* source = _sourceData[row]; NSString* newName = textField.stringValue; - display[@"customName"] = newName; - CGDirectDisplayID displayId = [display[@"id"] unsignedIntValue]; - setCustomDisplayName(displayId, newName); + source[@"customName"] = newName; + + NSString* type = source[@"type"]; + if([type isEqualToString:@"display"]){ + CGDirectDisplayID displayId = [source[@"id"] unsignedIntValue]; + setCustomDisplayName(displayId, newName); + } else if([type isEqualToString:@"camera"]){ + setCustomCameraName(source[@"id"], newName); + } } } // End of controlTextDidEndEditing() -/** - * Handles special key commands in text fields, including Shift+Tab for backward navigation. - * @param control The control sending the command - * @param textView The field editor - * @param commandSelector The command selector - * @return YES if the command was handled, NO otherwise - */ - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector{ if(commandSelector == @selector(insertBacktab:)){ - // Shift+Tab pressed - move to previous field NSTextField* currentField = (NSTextField*)control; NSInteger currentRow = currentField.tag; - - // Find previous text field for(NSInteger i = currentRow - 1; i >= 0; i--){ if(i < (NSInteger)_textFields.count && _textFields[i] != [NSNull null]){ NSTextField* prevField = _textFields[i]; @@ -757,8 +909,6 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBy return YES; } } // End of loop searching for previous field - - // If at first field, wrap to OK button if(_okButton){ [self makeFirstResponder:_okButton]; return YES; @@ -784,17 +934,18 @@ - (void)windowDidBecomeKey:(NSNotification *)notification{ - (void)windowWillClose:(NSNotification *)notification{ [self refreshPreferencesWindow]; - displayNamesPanel = nil; + sourceNamesPanel = nil; } @end -/** - * Shows the display names configuration panel. - */ -void showDisplayNamesPanel(void){ - if(!displayNamesPanel){ - displayNamesPanel = [[DisplayNamesPanel alloc] init]; +void showSourceNamesPanel(void){ + if(!sourceNamesPanel){ + sourceNamesPanel = [[SourceNamesPanel alloc] init]; } - [displayNamesPanel makeKeyAndOrderFront:nil]; + [sourceNamesPanel makeKeyAndOrderFront:nil]; +} // End of showSourceNamesPanel() + +void showDisplayNamesPanel(void){ + showSourceNamesPanel(); } // End of showDisplayNamesPanel() diff --git a/pip/window.m b/pip/window.m index ad38777..0d23459 100644 --- a/pip/window.m +++ b/pip/window.m @@ -1140,20 +1140,38 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ [self setupNonHLSControls]; if(!is_airplay_session){ - int defaultDisplayId = [(NSNumber*)getPref(@"default_display") intValue]; - if(defaultDisplayId > 0){ - NSArray* displays = getDisplayList(); - for(NSDictionary* display in displays){ - if([display[@"id"] intValue] == defaultDisplayId){ + NSDictionary* defaultSource = getDefaultSourcePreference(); + NSString* sourceType = defaultSource[@"type"]; + if([sourceType isEqualToString:@"display"]){ + int defaultDisplayId = [defaultSource[@"id"] intValue]; + if(defaultDisplayId > 0){ + BOOL hasDisplay = NO; + NSArray* displays = getDisplayList(); + for(NSDictionary* display in displays){ + if([display[@"id"] intValue] == defaultDisplayId){ + hasDisplay = YES; + break; + } + } // End of loop through displays + if(hasDisplay){ WindowSel* sel = [WindowSel getDefault]; - sel.title = display[@"name"]; + sel.title = getDisplayNameForId(defaultDisplayId); sel.dspId = defaultDisplayId; NSMenuItem* item = [[NSMenuItem alloc] init]; [item setRepresentedObject:sel]; [self changeWindow:item]; - break; } - } // End of loop through displays + } + } else if([sourceType isEqualToString:@"camera"]){ + NSString* defaultCameraId = defaultSource[@"id"]; + if(defaultCameraId && defaultCameraId.length > 0 && [AVCaptureDevice deviceWithUniqueID:defaultCameraId]){ + WindowSel* sel = [WindowSel getDefault]; + sel.title = getCameraNameForId(defaultCameraId); + sel.cameraId = defaultCameraId; + NSMenuItem* item = [[NSMenuItem alloc] init]; + [item setRepresentedObject:sel]; + [self changeWindow:item]; + } } } // End of if not airplay session @@ -1955,10 +1973,11 @@ - (void)rightMouseDown:(NSEvent *)theEvent { // Add camera menu cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for(AVCaptureDevice *camera in cameras){ + NSString* cameraName = getCameraNameForId([camera uniqueID]); WindowSel* sel = [WindowSel getDefault]; - sel.title = [camera localizedName]; + sel.title = cameraName; sel.cameraId = [camera uniqueID]; - ADD_MENU_ITEM(camera_menu, [camera localizedName], @selector(changeWindow:), NULL, { + ADD_MENU_ITEM(camera_menu, cameraName, @selector(changeWindow:), NULL, { [item setRepresentedObject:sel]; }) } From 6bfe00e280d15d288b3f92b2f71e2480ae77b37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Wed, 18 Feb 2026 19:58:01 +0000 Subject: [PATCH 17/19] Reduce HLS streaming latency to ~2-3 seconds --- airplay_sender/video_encoder.h | 9 +++++- airplay_sender/video_encoder_stub.c | 8 ++++++ pip/HLSPlayer.m | 2 ++ pip/stream_manager.m | 8 ++++-- pip/video_encoder.m | 44 +++++++++++++++++++++++++---- pip/viewer.html | 8 ++++-- 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/airplay_sender/video_encoder.h b/airplay_sender/video_encoder.h index 7486075..7e6ee39 100644 --- a/airplay_sender/video_encoder.h +++ b/airplay_sender/video_encoder.h @@ -60,6 +60,13 @@ video_encoder_t *video_encoder_init(int width, int height, int fps, int bitrate) */ void video_encoder_set_callback(video_encoder_t *enc, encoded_frame_callback_t cb, void *ctx); +/** + * Set maximum keyframe interval in frames. + * Example: at 30fps, 30 => ~1 second keyframe cadence. + * Returns 0 on success, -1 on error. + */ +int video_encoder_set_keyframe_interval(video_encoder_t *enc, int keyframe_interval_frames); + /** * Encode a frame * rgba_data: RGBA32 format frame data (row-major) @@ -78,4 +85,4 @@ void video_encoder_destroy(video_encoder_t *enc); } #endif -#endif \ No newline at end of file +#endif diff --git a/airplay_sender/video_encoder_stub.c b/airplay_sender/video_encoder_stub.c index b2b0866..ef1865f 100644 --- a/airplay_sender/video_encoder_stub.c +++ b/airplay_sender/video_encoder_stub.c @@ -44,6 +44,14 @@ video_encoder_set_callback(video_encoder_t *enc, encoded_frame_callback_t cb, vo } } +int +video_encoder_set_keyframe_interval(video_encoder_t *enc, int keyframe_interval_frames) +{ + (void)enc; + (void)keyframe_interval_frames; + return 0; +} + int video_encoder_encode_frame(video_encoder_t *enc, uint8_t *rgba_data, int stride, uint64_t pts) { diff --git a/pip/HLSPlayer.m b/pip/HLSPlayer.m index 76388d3..e69338c 100644 --- a/pip/HLSPlayer.m +++ b/pip/HLSPlayer.m @@ -33,6 +33,8 @@ - (instancetype)initWithURL:(NSURL *)url headers:(NSDictionaryfps); /* --- 4. Allocate and populate the pipeline context --- */ _pipeline_ctx = calloc(1, sizeof(pipeline_ctx_t)); diff --git a/pip/video_encoder.m b/pip/video_encoder.m index e698732..cac7e90 100644 --- a/pip/video_encoder.m +++ b/pip/video_encoder.m @@ -186,6 +186,41 @@ static void compression_output_callback(void *outputCallbackRefCon, enc->frame_count++; } +int +video_encoder_set_keyframe_interval(video_encoder_t *enc, int keyframe_interval_frames) +{ + if (!enc || !enc->compression_session || keyframe_interval_frames <= 0) { + return -1; + } + + CFNumberRef keyframe_interval_num = CFNumberCreate(NULL, kCFNumberIntType, &keyframe_interval_frames); + if (!keyframe_interval_num) { + return -1; + } + + OSStatus status = VTSessionSetProperty(enc->compression_session, + kVTCompressionPropertyKey_MaxKeyFrameInterval, + keyframe_interval_num); + CFRelease(keyframe_interval_num); + if (status != noErr) { + NSLog(@"video_encoder: failed to set MaxKeyFrameInterval: %d", (int)status); + return -1; + } + + if (enc->fps > 0) { + double interval_seconds = (double)keyframe_interval_frames / (double)enc->fps; + CFNumberRef keyframe_duration_num = CFNumberCreate(NULL, kCFNumberDoubleType, &interval_seconds); + if (keyframe_duration_num) { + VTSessionSetProperty(enc->compression_session, + kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, + keyframe_duration_num); + CFRelease(keyframe_duration_num); + } + } + + return 0; +} + video_encoder_t * video_encoder_init(int width, int height, int fps, int bitrate) { @@ -259,12 +294,11 @@ static void compression_output_callback(void *outputCallbackRefCon, CFRelease(rate_limit_values[1]); CFRelease(data_rate_limits); - // Set keyframe interval (every 2 seconds at target fps) - // AirPlay typically uses 2-second keyframe intervals for better error recovery + // Default keyframe interval: every ~2 seconds. + // StreamManager may override this to 1 second for lower HLS latency. int keyframe_interval = fps * 2; - CFNumberRef keyframe_interval_num = CFNumberCreate(NULL, kCFNumberIntType, &keyframe_interval); - VTSessionSetProperty(enc->compression_session, kVTCompressionPropertyKey_MaxKeyFrameInterval, keyframe_interval_num); - CFRelease(keyframe_interval_num); + if (keyframe_interval < 1) keyframe_interval = 1; + video_encoder_set_keyframe_interval(enc, keyframe_interval); // Set expected frame rate CFNumberRef fps_num = CFNumberCreate(NULL, kCFNumberIntType, &fps); diff --git a/pip/viewer.html b/pip/viewer.html index 2782b4b..3d31e9c 100644 --- a/pip/viewer.html +++ b/pip/viewer.html @@ -155,8 +155,12 @@

Transmisión en vivo

if (typeof Hls !== "undefined" && Hls.isSupported()) { console.log("[PiP] Using hls.js"); var hls = new Hls({ - liveSyncDuration: 3, - liveMaxLatencyDuration: 6, + lowLatencyMode: true, + liveSyncDuration: 2, + liveMaxLatencyDuration: 3.5, + maxBufferLength: 4, + backBufferLength: 8, + maxLiveSyncPlaybackRate: 1.25, enableWorker: true, debug: false }); From 573cc7909c6fc9e1ed84d63af648d4133cee2c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Sat, 14 Mar 2026 16:34:39 +0000 Subject: [PATCH 18/19] Separate local audio monitoring from HLS audio streaming The audio monitoring toggle now only mutes/unmutes local playback without affecting audio capture or HLS streaming to other devices. --- pip/window.m | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/pip/window.m b/pip/window.m index 0d23459..53b3a37 100644 --- a/pip/window.m +++ b/pip/window.m @@ -842,6 +842,7 @@ @implementation Window{ // Camera audio capture and playback using AVCaptureAudioPreviewOutput AVCaptureAudioPreviewOutput* camera_audio_preview; bool camera_audio_enabled; + bool camera_audio_monitoring; // Local audio monitoring (does not affect HLS streaming) bool camera_has_microphone; #if __has_include() SCStream *window_stream API_AVAILABLE(macos(12.3)); @@ -884,6 +885,7 @@ - (id) initWithAirplay:(bool)enable andTitle:(NSString*)title{ // Initialize camera audio variables camera_audio_preview = nil; camera_audio_enabled = true; // Enabled by default + camera_audio_monitoring = true; // Local monitoring enabled by default camera_has_microphone = false; #if __has_include() if (@available(macOS 12.3, *)) { @@ -2142,10 +2144,10 @@ - (void)rightMouseDown:(NSEvent *)theEvent { }) } - // Audio monitoring toggle + // Local audio monitoring toggle (does not affect HLS streaming) if(camera_has_microphone) { - ADD_MENU_ITEM(theMenu, @"Audio Monitoring", @selector(toggleCameraAudio:), NULL, { - if(camera_audio_enabled) { + ADD_MENU_ITEM(theMenu, @"Local Audio Monitoring", @selector(toggleCameraAudio:), NULL, { + if(camera_audio_monitoring) { [item setState:NSControlStateValueOn]; } }) @@ -2573,7 +2575,7 @@ -(void)startCameraCapture:(NSString*)deviceId{ // Use AVCaptureAudioPreviewOutput for automatic audio playback // This handles all format conversion automatically AVCaptureAudioPreviewOutput *audioPreview = [[AVCaptureAudioPreviewOutput alloc] init]; - audioPreview.volume = 1.0; // Full volume + audioPreview.volume = camera_audio_monitoring ? 1.0 : 0.0; audioPreview.outputDeviceUniqueID = nil; // Use default output device if(![session canAddOutput:audioPreview]) { @@ -3508,29 +3510,18 @@ - (void)selectCameraResolution:(id)sender { } // End of selectCameraResolution: /** - * Toggles camera audio monitoring on/off. + * Toggles local camera audio monitoring on/off. + * Only affects the local audio preview volume; audio capture and HLS streaming are not affected. * @param sender The menu item that triggered this action */ -(void)toggleCameraAudio:(id)sender { - camera_audio_enabled = !camera_audio_enabled; - - if(camera_audio_enabled && camera_id && camera_has_microphone) { - // Restart camera capture to enable audio - NSString *currentCameraId = [camera_id copy]; - [self startCameraCapture:currentCameraId]; - } else if(!camera_audio_enabled) { - // Mute audio by setting volume to 0, or remove output - if(camera_audio_preview) { - camera_audio_preview.volume = 0.0; - } - } else { - // Unmute audio - if(camera_audio_preview) { - camera_audio_preview.volume = 1.0; - } + camera_audio_monitoring = !camera_audio_monitoring; + + if(camera_audio_preview) { + camera_audio_preview.volume = camera_audio_monitoring ? 1.0 : 0.0; } - NSLog(@"Camera audio monitoring %@", camera_audio_enabled ? @"enabled" : @"disabled"); + NSLog(@"Local camera audio monitoring %@", camera_audio_monitoring ? @"enabled" : @"disabled"); } // End of toggleCameraAudio: - (void)updateHLSInputViewLayout { From 35696f41e36817cfb5824ac9aaea1805d8e0228d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Carpio=20Garc=C3=ADa?= Date: Sun, 15 Mar 2026 16:43:27 +0000 Subject: [PATCH 19/19] Copy URL now points directly to the HLS stream The Copy URL action and auto-copy on stream start now copy the direct stream URL (with /stream.m3u8) instead of the base viewer URL. Open in Browser still opens the web viewer. --- README.md | 2 +- pip/window.m | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 024f244..1983043 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ PiP can stream any preview window over your local network using HLS (HTTP Live S 1. Right-click any active preview window and select **Stream > Start Streaming** 2. The stream URL (e.g. `http://192.168.1.42:8080`) appears at the bottom of the Stream menu 3. Open that URL on any device's browser to watch the live stream -4. Use **Copy URL** or **Open in Browser** for quick access +4. Use **Copy URL** to copy the direct HLS stream URL (`/stream.m3u8`) or **Open in Browser** to open the web viewer ### Quality presets | Preset | Resolution | Bitrate | FPS | diff --git a/pip/window.m b/pip/window.m index 53b3a37..4f3f1b9 100644 --- a/pip/window.m +++ b/pip/window.m @@ -2238,9 +2238,10 @@ - (void)startStreamAction:(id)sender { if (success) { NSString *url = [streamManager streamURL]; NSLog(@"Streaming started at %@", url); - // Copy URL to clipboard automatically + // Copy direct stream URL to clipboard automatically + NSString *directURL = [url stringByAppendingString:@"/stream.m3u8"]; [[NSPasteboard generalPasteboard] clearContents]; - [[NSPasteboard generalPasteboard] setString:url forType:NSPasteboardTypeString]; + [[NSPasteboard generalPasteboard] setString:directURL forType:NSPasteboardTypeString]; } } // End of startStreamAction: @@ -2260,8 +2261,9 @@ - (void)copyStreamURL:(id)sender { if (streamManager && [streamManager isStreaming]) { NSString *url = [streamManager streamURL]; if (url) { + NSString *directURL = [url stringByAppendingString:@"/stream.m3u8"]; [[NSPasteboard generalPasteboard] clearContents]; - [[NSPasteboard generalPasteboard] setString:url forType:NSPasteboardTypeString]; + [[NSPasteboard generalPasteboard] setString:directURL forType:NSPasteboardTypeString]; } } } // End of copyStreamURL: