diff --git a/.gitignore b/.gitignore index a0d74e1226..3603100d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,7 @@ xcodeproj.bazelrc build-input/* **/*.pyc *.pyc +submodules/OpusBinding/SharedHeaders/* +submodules/FFMpegBinding/SharedHeaders/* +submodules/OpenSSLEncryptionProvider/SharedHeaders/* + diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 68fc11e9fb..39da024bab 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12950,7 +12950,7 @@ Sorry for the inconvenience."; "SharedMedia.GiftCount_any" = "%@ gifts"; "Stars.Info.Title" = "What are Stars?"; -"Stars.Info.Description" = "Buy packages of Stars on Telegram that let you do following:"; +"Stars.Info.Description" = "Telegram Stars allow users to:"; "Stars.Info.Gift.Title" = "Send Gifts to Friends"; "Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars."; "Stars.Info.Miniapp.Title" = "Use Stars in Miniapps"; @@ -12976,6 +12976,7 @@ Sorry for the inconvenience."; "Gift.View.HiddenName" = "Hidden Name"; "Gift.View.Date" = "Date"; "Gift.View.Availability" = "Availability"; +"Gift.View.Availability.Of" = "%1$@ of %2$@"; "Gift.View.Hide" = "Hide from My Page"; "Gift.View.Display" = "Display on My Page"; "Gift.View.Convert" = "Convert to %@"; @@ -12986,7 +12987,7 @@ Sorry for the inconvenience."; "Gift.Hidden.Title" = "Gift Removed from Profile"; "Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]()."; "Gift.Convert.Title" = "Convert Gift to Stars"; -"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis action cannot be undone."; +"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift."; "Gift.Convert.Stars_1" = "%@ Star"; "Gift.Convert.Stars_any" = "%@ Stars"; "Gift.Convert.Convert" = "Convert"; @@ -13032,10 +13033,12 @@ Sorry for the inconvenience."; "Gift.Send.Title" = "Send a Gift"; "Gift.Send.Customize.Title" = "CUSTOMIZE YOUR GIFT"; -"Gift.Send.Customize.MessagePlaceholder" = "Enter Message"; +"Gift.Send.Customize.MessagePlaceholder" = "Enter Message (Optional)"; +"Gift.Send.Customize.Info" = "Only %@ will see your message."; "Gift.Send.HideMyName" = "Hide My Name"; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; +"Gift.Send.Limited" = "Limited"; "Profile.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift"; @@ -13047,7 +13050,22 @@ Sorry for the inconvenience."; "Report.Title.User" = "Report User"; "Report.Title.Group" = "Report Group"; "Report.Title.Channel" = "Report Channel"; +"Report.Title.Bot" = "Report Bot"; "Report.Comment.Placeholder" = "Add Comment"; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; "Report.Send" = "Send Report"; + +"Notification.PremiumGift.MonthsTitle_1" = "%@ Month Premium"; +"Notification.PremiumGift.MonthsTitle_any" = "%@ Months Premium"; + +"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; +"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; + +"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; + +"Notification.StarsGift.Stars_1" = "%@ Star"; +"Notification.StarsGift.Stars_any" = "%@ Stars"; + +"WebBrowser.AuthChallenge.Title" = "Sign in to %@"; +"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5e32fda584..95b279d4b1 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1041,7 +1041,7 @@ public protocol SharedAccountContext: AnyObject { func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController - func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 092fc4c52b..a0e4ecc34a 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -202,6 +202,7 @@ public protocol UniversalVideoManager: AnyObject { func removePlaybackCompleted(id: AnyHashable, index: Int) func statusSignal(content: UniversalVideoContent) -> Signal func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(RangeSet, Int64)?, NoError> + func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal } public enum AudioRecordingState: Equatable { diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 0edf8c349c..9134f6d26b 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -143,6 +143,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, starsGiftsPurchaseAvailable: false, + starGiftsPurchaseBlocked: true, boostsPerGiftCount: 3, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, @@ -170,6 +171,7 @@ public struct PremiumConfiguration { public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool public let starsGiftsPurchaseAvailable: Bool + public let starGiftsPurchaseBlocked: Bool public let boostsPerGiftCount: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 @@ -196,6 +198,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, starsGiftsPurchaseAvailable: Bool, + starGiftsPurchaseBlocked: Bool, boostsPerGiftCount: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, @@ -221,6 +224,7 @@ public struct PremiumConfiguration { self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable + self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked self.boostsPerGiftCount = boostsPerGiftCount self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount @@ -254,6 +258,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, + starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 788431c2be..8ff0bf498f 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -19,8 +19,9 @@ public protocol UniversalVideoContentNode: AnyObject { var ready: Signal { get } var status: Signal { get } var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { get } + var isNativePictureInPictureActive: Signal { get } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) func play() func pause() @@ -41,6 +42,8 @@ public protocol UniversalVideoContentNode: AnyObject { func fetchControl(_ control: UniversalVideoNodeFetchControl) func notifyPlaybackControlsHidden(_ hidden: Bool) func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) + func enterNativePictureInPicture() -> Bool + func exitNativePictureInPicture() } public protocol UniversalVideoContent { @@ -68,7 +71,7 @@ public protocol UniversalVideoDecoration: AnyObject { func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) func updateContentNodeSnapshot(_ snapshot: UIView?) - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) func tap() } @@ -100,7 +103,7 @@ public final class UniversalVideoNode: ASDisplayNode { private let autoplay: Bool private let snapshotContentWhenGone: Bool - private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? + private(set) var contentNode: (UniversalVideoContentNode & ASDisplayNode)? private var contentNodeId: Int32? private var playbackCompletedIndex: Int? @@ -125,6 +128,11 @@ public final class UniversalVideoNode: ASDisplayNode { return self._bufferingStatus.get() } + private let _isNativePictureInPictureActive = Promise() + public var isNativePictureInPictureActive: Signal { + return self._isNativePictureInPictureActive.get() + } + private let _ready = Promise() public var ready: Signal { return self._ready.get() @@ -181,6 +189,7 @@ public final class UniversalVideoNode: ASDisplayNode { self._status.set(self.manager.statusSignal(content: self.content)) self._bufferingStatus.set(self.manager.bufferingStatusSignal(content: self.content)) + self._isNativePictureInPictureActive.set(self.manager.isNativePictureInPictureActiveSignal(content: self.content)) self.decoration.setStatus(self.status) @@ -247,8 +256,8 @@ public final class UniversalVideoNode: ASDisplayNode { } } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.decoration.updateLayout(size: size, transition: transition) + public func updateLayout(size: CGSize, actualSize: CGSize? = nil, transition: ContainedViewLayoutTransition) { + self.decoration.updateLayout(size: size, actualSize: actualSize ?? size, transition: transition) } public func play() { @@ -418,4 +427,22 @@ public final class UniversalVideoNode: ASDisplayNode { } }) } + + public func enterNativePictureInPicture() -> Bool { + var result = false + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + result = contentNode.enterNativePictureInPicture() + } + }) + return result + } + + public func exitNativePictureInPicture() { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.exitNativePictureInPicture() + } + }) + } } diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index a8a29c486a..3fcfba0dd7 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -67,7 +67,7 @@ private class AvatarNodeParameters: NSObject { } } -private func calculateColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { +public func calculateAvatarColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { let colorIndex: Int if let explicitColorIndex = explicitColorIndex { colorIndex = explicitColorIndex @@ -183,7 +183,7 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { } } -private enum AvatarNodeIcon: Equatable { +public enum AvatarNodeIcon: Equatable { case none case savedMessagesIcon case repliesIcon @@ -577,7 +577,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -586,7 +586,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) + let colors = calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -754,7 +754,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -763,7 +763,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) + let colors = calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -800,9 +800,9 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters if let icon = icon, case .phone = icon { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } else { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } self.displaySuspended = true diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 85c701d480..ad45b10348 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -325,7 +325,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -345,9 +345,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -405,8 +405,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -421,7 +421,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index ca84dca958..f0a4d76653 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -102,7 +102,7 @@ final class CameraDeviceContext { return 30.0 } switch DeviceModel.current { - case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax: + case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax: return 60.0 default: return 30.0 diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 4d5c684da8..01d68996e1 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -34,6 +34,10 @@ public extension Camera { self = .iPhone15Pro case .iPhone15ProMax: self = .iPhone15ProMax + case .iPhone16Pro: + self = .iPhone15Pro + case .iPhone16ProMax: + self = .iPhone15ProMax case .unknown: self = .unknown default: diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3620d86c21..4f211c7658 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool) case addContact(String, PresentationTheme, PresentationStrings) @@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .localPeerId(peer.id) case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _): return .messageId(message.id, section) @@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery { return true } else { return false @@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case .globalPeer, .message, .addContact: return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer, .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true @@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging): + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { if peer.peer is TelegramUser { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))" } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" } else { @@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { isSavedMessages = true } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil) }, disabledAction: { _ in interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery)) index += 1 numberOfGlobalPeers += 1 } @@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case let .user(user) = peer, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(peer.id) } - case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _): storyStatsIds.append(foundPeer.peer.id) if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) @@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) - case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): - mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false) + case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery): + mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery) case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _): mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false) default: @@ -3672,7 +3672,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9d7d970285..fc2bb3c91e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1714,9 +1714,20 @@ public final class ChatListNode: ListView { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) - controller.navigationPresentation = .modal - self.push?(controller) + if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first { + let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions) + controller.navigationPresentation = .modal + self.push?(controller) + }) + } else { + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.push?(controller) + } }, openPremiumManagement: { [weak self] in guard let self else { return diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index d6165246af..b561f12bee 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -8,11 +8,15 @@ public final class BundleIconComponent: Component { public let name: String public let tintColor: UIColor? public let maxSize: CGSize? + public let shadowColor: UIColor? + public let shadowBlur: CGFloat - public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) { self.name = name self.tintColor = tintColor self.maxSize = maxSize + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool { @@ -25,6 +29,12 @@ public final class BundleIconComponent: Component { if lhs.maxSize != rhs.maxSize { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } @@ -40,12 +50,24 @@ public final class BundleIconComponent: Component { } func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - if self.component?.name != component.name || self.component?.tintColor != component.tintColor { + if self.component?.name != component.name || self.component?.tintColor != component.tintColor || self.component?.shadowColor != component.shadowColor || self.component?.shadowBlur != component.shadowBlur { + var image: UIImage? if let tintColor = component.tintColor { - self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) + image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) } else { - self.image = UIImage(bundleImageName: component.name) + image = UIImage(bundleImageName: component.name) } + if let imageValue = image, let shadowColor = component.shadowColor, component.shadowBlur != 0.0 { + image = generateImage(CGSize(width: imageValue.size.width + component.shadowBlur * 2.0, height: imageValue.size.height + component.shadowBlur * 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(), blur: component.shadowBlur, color: shadowColor.cgColor) + + if let cgImage = imageValue.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: component.shadowBlur, y: component.shadowBlur), size: imageValue.size)) + } + }) + } + self.image = image } self.component = component diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 966d2d5a2e..12346ecdb7 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] let actionIcon: ContactsPeerItemActionIcon + let searchQuery: String? let action: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? @@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { actionIcon: ContactsPeerItemActionIcon = .none, index: SortIndex?, header: ListViewItemHeader?, + searchQuery: String? = nil, action: ((ContactsPeerItemPeer) -> Void)?, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, @@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.options = options self.additionalActions = additionalActions self.actionIcon = actionIcon + self.searchQuery = searchQuery self.action = action self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) } case let .addressName(suffix): - if let addressName = peer.addressName { + var addressName = peer.addressName + if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) { + for username in peer.usernames { + if username.username.lowercased().contains(searchQuery) { + addressName = username.username + break + } + } + } + if let addressName { let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor) if !suffix.isEmpty { let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index b01c2a487d..64801b608e 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone14ProZoomed case iPhone14ProMax case iPhone14ProMaxZoomed + case iPhone16Pro + case iPhone16ProMax case iPad case iPadMini case iPad102Inch @@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, + .iPhone16Pro, + .iPhone16ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 430.0, height: 932.0) case .iPhone14ProMaxZoomed: return CGSize(width: 375.0, height: 812.0) + case .iPhone16Pro: + return CGSize(width: 402.0, height: 874.0) + case .iPhone16ProMax: + return CGSize(width: 440.0, height: 956.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 + case .iPhone16Pro, .iPhone16ProMax: + return 55.0 case let .unknown(_, _, _, screenCornerRadius): return screenCornerRadius default: @@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? 21.0 : 34.0 case .iPhone14ProZoomed: return inLandscape ? 21.0 : 28.0 @@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 54.0 case .iPhone14ProMaxZoomed: return 47.0 + case .iPhone16Pro, .iPhone16ProMax: + return 54.0 case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: @@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: return 292.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 44.0 case .iPhone6Plus: return 45.0 @@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public var hasDynamicIsland: Bool { switch self { - case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return true default: return false diff --git a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift b/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift deleted file mode 100644 index b8c2ed10c5..0000000000 --- a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import UIKit - -final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.cancelsTouchesInView = false - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - self.delegate = self - - self.direction = [.left, .right] - self.numberOfTouchesRequired = 2 - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } -} diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index c1c8f5d686..e73f00da42 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -1141,7 +1141,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -1161,9 +1161,9 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -1221,8 +1221,8 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -1237,7 +1237,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/FFMpegBinding/Package.swift b/submodules/FFMpegBinding/Package.swift index da926bdafc..6d794a2920 100644 --- a/submodules/FFMpegBinding/Package.swift +++ b/submodules/FFMpegBinding/Package.swift @@ -27,7 +27,7 @@ let package = Package( publicHeadersPath: "Public", cSettings: [ .headerSearchPath("Public"), - .unsafeFlags(["-I../../../../core-xprojects/ffmpeg/build/ffmpeg/include"]) + .headerSearchPath("SharedHeaders/ffmpeg/include"), ]), ] ) diff --git a/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift b/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift index ac93529926..65fede2921 100644 --- a/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift +++ b/submodules/GalleryUI/Sources/GalleryVideoDecoration.swift @@ -14,7 +14,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -34,9 +34,9 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -94,8 +94,8 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -110,7 +110,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index ac455d91b6..9f95f5e933 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -604,6 +604,8 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte private var hiddenMediaManagerIndex: Int? private var messageRemovedDisposable: Disposable? + + private var isNativePictureInPictureActiveDisposable: Disposable? init(context: AccountContext, overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { self.overlayController = overlayController @@ -617,30 +619,84 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte super.init() - let contentDelegate = PlaybackDelegate(node: self.node) - self.contentDelegate = contentDelegate + if let videoLayer = videoNode.getVideoLayer() { + let contentDelegate = PlaybackDelegate(node: self.node) + self.contentDelegate = contentDelegate + + let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate)) + self.pictureInPictureController = pictureInPictureController + contentDelegate.pictureInPictureController = pictureInPictureController + + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + pictureInPictureController.requiresLinearPlayback = !canSkip + pictureInPictureController.delegate = self + self.pictureInPictureController = pictureInPictureController + let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in + guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else { + return + } + if pictureInPictureController.isPictureInPicturePossible { + strongSelf.pictureInPictureTimer?.invalidate() + strongSelf.pictureInPictureTimer = nil + + pictureInPictureController.startPictureInPicture() + } + }, queue: .mainQueue()) + self.pictureInPictureTimer = timer + timer.start() + } else { + var currentIsNativePictureInPictureActive = false + self.isNativePictureInPictureActiveDisposable = (videoNode.isNativePictureInPictureActive + |> deliverOnMainQueue).startStrict(next: { [weak self] isNativePictureInPictureActive in + guard let self else { + return + } + + if currentIsNativePictureInPictureActive == isNativePictureInPictureActive { + return + } + currentIsNativePictureInPictureActive = isNativePictureInPictureActive + + if isNativePictureInPictureActive { + Queue.mainQueue().after(0.0, { [weak self] in + guard let self else { + return + } + self.willBegin(self) + + if let overlayController = self.overlayController { + overlayController.setPictureInPictureContentHidden(content: self, isHidden: true) + } + + self.didEnd(self) + }) + } else { + self.expand { [weak self] in + guard let self else { + return + } - let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoNode.getVideoLayer()!, playbackDelegate: contentDelegate)) - self.pictureInPictureController = pictureInPictureController - contentDelegate.pictureInPictureController = pictureInPictureController - - pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false - pictureInPictureController.requiresLinearPlayback = !canSkip - pictureInPictureController.delegate = self - self.pictureInPictureController = pictureInPictureController - let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in - guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else { - return - } - if pictureInPictureController.isPictureInPicturePossible { - strongSelf.pictureInPictureTimer?.invalidate() - strongSelf.pictureInPictureTimer = nil + self.didExpand = true - pictureInPictureController.startPictureInPicture() - } - }, queue: .mainQueue()) - self.pictureInPictureTimer = timer - timer.start() + if let overlayController = self.overlayController { + overlayController.setPictureInPictureContentHidden(content: self, isHidden: false) + self.node.alpha = 0.02 + } + + guard let overlayController = self.overlayController else { + return + } + overlayController.removePictureInPictureContent(content: self) + self.node.canAttachContent = false + if self.didExpand { + return + } + self.node.continuePlayingWithoutSound() + } + } + }) + let _ = videoNode.enterNativePictureInPicture() + } if let hiddenMedia = hiddenMedia { self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia) @@ -676,6 +732,7 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte deinit { self.messageRemovedDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() self.pictureInPictureTimer?.invalidate() self.node.setCanPlaybackWithoutHierarchy(false) @@ -743,10 +800,6 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte } completionHandler(true) - - /*Queue.mainQueue().after(0.2, { - self?.node.canAttachContent = false - })*/ } } } @@ -1186,7 +1239,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoScale = 2.0 } let videoSize = CGSize(width: item.content.dimensions.width * videoScale, height: item.content.dimensions.height * videoScale) - videoNode.updateLayout(size: videoSize, transition: .immediate) + let actualVideoSize = CGSize(width: item.content.dimensions.width, height: item.content.dimensions.height) + videoNode.updateLayout(size: videoSize, actualSize: actualVideoSize, transition: .immediate) videoNode.ownsContentNodeUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateDisplayPlaceholder(!value) @@ -1294,7 +1348,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(), + /*self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(), self.playbackRatePromise.get(), self.isShowingContextMenuPromise.get() ).start(next: { [weak self] playbackRate, isShowingContextMenu in @@ -1333,7 +1387,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() } } - })) + }))*/ self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in @@ -2305,6 +2359,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let playbackRate = self.playbackRate if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported { + self.disablePictureInPicturePlaceholder = true let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) @@ -2837,7 +2892,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { + if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil && !(item.content is HLSVideoContent) { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index b607216341..0c955d674c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -623,6 +623,8 @@ private final class PendingInAppPurchaseState: Codable { case untilDate case stars case users + case text + case entities } enum PurposeType: Int32 { @@ -641,7 +643,7 @@ private final class PendingInAppPurchaseState: Codable { case upgrade case restore case gift(peerId: EnginePeer.Id) - case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?) + case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) case stars(count: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64) @@ -665,7 +667,9 @@ private final class PendingInAppPurchaseState: Codable { case .giftCode: self = .giftCode( peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) }, - boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }) + boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }), + text: try container.decodeIfPresent(String.self, forKey: .text), + entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities) ) case .giveaway: self = .giveaway( @@ -718,10 +722,12 @@ private final class PendingInAppPurchaseState: Codable { case let .gift(peerId): try container.encode(PurposeType.gift.rawValue, forKey: .type) try container.encode(peerId.toInt64(), forKey: .peer) - case let .giftCode(peerIds, boostPeer): + case let .giftCode(peerIds, boostPeer, text, entities): try container.encode(PurposeType.giftCode.rawValue, forKey: .type) try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers) try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer) + try container.encodeIfPresent(text, forKey: .text) + try container.encodeIfPresent(entities, forKey: .entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): try container.encode(PurposeType.giveaway.rawValue, forKey: .type) try container.encode(boostPeer.toInt64(), forKey: .boostPeer) @@ -764,8 +770,8 @@ private final class PendingInAppPurchaseState: Codable { self = .restore case let .gift(peerId, _, _): self = .gift(peerId: peerId) - case let .giftCode(peerIds, boostPeer, _, _): - self = .giftCode(peerIds: peerIds, boostPeer: boostPeer) + case let .giftCode(peerIds, boostPeer, _, _, text, entities): + self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _): self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) case let .stars(count, _, _): @@ -788,8 +794,8 @@ private final class PendingInAppPurchaseState: Codable { return .restore case let .gift(peerId): return .gift(peerId: peerId, currency: currency, amount: amount) - case let .giftCode(peerIds, boostPeer): - return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount) + case let .giftCode(peerIds, boostPeer, text, entities): + return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) case let .stars(count): diff --git a/submodules/OpenSSLEncryptionProvider/Package.swift b/submodules/OpenSSLEncryptionProvider/Package.swift index 1fd1668319..a8c920d7c8 100644 --- a/submodules/OpenSSLEncryptionProvider/Package.swift +++ b/submodules/OpenSSLEncryptionProvider/Package.swift @@ -23,10 +23,8 @@ let package = Package( publicHeadersPath: "PublicHeaders", cSettings: [ .headerSearchPath("PublicHeaders"), - .unsafeFlags([ - "-I../../../../core-xprojects/openssl/build/openssl/include", - "-I../EncryptionProvider/PublicHeaders" - ]) + .headerSearchPath("SharedHeaders/openssl/include"), + .headerSearchPath("SharedHeaders/EncryptionProvider"), ]), ] ) diff --git a/submodules/OpusBinding/Package.swift b/submodules/OpusBinding/Package.swift index 12028015a3..045c0e30c3 100644 --- a/submodules/OpusBinding/Package.swift +++ b/submodules/OpusBinding/Package.swift @@ -28,8 +28,8 @@ let package = Package( cSettings: [ .headerSearchPath("PublicHeaders"), .headerSearchPath("PublicHeaders/OpusBinding"), + .headerSearchPath("SharedHeaders/libopus/include"), .headerSearchPath("Sources"), - .unsafeFlags(["-I../../../../core-xprojects/libopus/build/libopus/include"]) ]), ] ) diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 5819862754..c5032937c6 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index ac98ad54e3..854a6f29c6 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -674,6 +674,28 @@ public final class MediaBox { return self.resourceData(id: resource.id, size: size, in: range, mode: mode, notifyAboutIncomplete: notifyAboutIncomplete, attemptSynchronously: attemptSynchronously) } + public func internal_resourceData(id: MediaResourceId, size: Int64, in range: Range) -> (file: ManagedFile, length: Int)? { + let paths = self.storePathsForId(id) + + self.timeBasedCleanup.touch(paths: [ + paths.complete + ]) + + if let file = ManagedFile(queue: nil, path: paths.complete, mode: .read), let completeSize = file.getSize() { + let clippedLowerBound = min(completeSize, max(0, range.lowerBound)) + let clippedUpperBound = min(completeSize, max(0, range.upperBound)) + if clippedLowerBound < clippedUpperBound && (clippedUpperBound - clippedLowerBound) <= 64 * 1024 * 1024 { + let _ = file.seek(position: clippedLowerBound) + return (file, Int(clippedUpperBound - clippedLowerBound)) + } else { + return nil + } + } else { + let tempManager = MediaBoxFileManager(queue: nil) + return MediaBoxPartialFile.internal_extractPartialData(manager: tempManager, path: paths.partial, metaPath: paths.partial + ".meta", range: range) + } + } + public func resourceData(id: MediaResourceId, size: Int64, in range: Range, mode: ResourceDataRangeMode = .complete, notifyAboutIncomplete: Bool = false, attemptSynchronously: Bool = false) -> Signal<(Data, Bool), NoError> { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 44472bd1ce..8e1d9d6d0c 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -92,6 +92,20 @@ final class MediaBoxPartialFile { return fd.readData(count: Int(clippedRange.upperBound - clippedRange.lowerBound)) } + static func internal_extractPartialData(manager: MediaBoxFileManager, path: String, metaPath: String, range: Range) -> (file: ManagedFile, length: Int)? { + guard let fd = ManagedFile(queue: nil, path: path, mode: .read) else { + return nil + } + guard let fileMap = try? MediaBoxFileMap.read(manager: manager, path: metaPath) else { + return nil + } + guard let clippedRange = fileMap.contains(range) else { + return nil + } + let _ = fd.seek(position: Int64(clippedRange.lowerBound)) + return (fd, Int(clippedRange.upperBound - clippedRange.lowerBound)) + } + var storedSize: Int64 { assert(self.queue.isCurrent()) return self.fileMap.sum diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 3e6c32d5b0..b878cc81f9 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -1356,7 +1356,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio return } let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount - purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(state.peers.count) storeProduct = selectedProduct.storeProduct case .starsGiveaway: diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index c3bb78f7ee..8b963e3336 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -626,7 +626,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -646,9 +646,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -706,8 +706,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -722,7 +722,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index bc802fe14c..af760b9b64 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -910,7 +910,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first { purpose = .gift(peerId: peerId, currency: currency, amount: amount) } else { - purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) + purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(self.peerIds.count) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index aa22fb967b..14becffa38 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -467,7 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } - dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } + dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) } @@ -551,8 +551,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } - dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) } - dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) } + dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) } dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[-2015170219] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index d626af8099..5a3bc02c8b 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -711,7 +711,7 @@ public extension Api { public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) - case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64) + case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case inputStorePaymentPremiumSubscription(flags: Int32) case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64) @@ -728,9 +728,9 @@ public extension Api { serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) break - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): if boxed { - buffer.appendInt32(-1551868097) + buffer.appendInt32(-75955309) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -741,6 +741,7 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)} serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): if boxed { @@ -818,8 +819,8 @@ public extension Api { switch self { case .inputStorePaymentGiftPremium(let userId, let currency, let amount): return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): - return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): + return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any), ("message", message as Any)]) case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): @@ -867,13 +868,18 @@ public extension Api { _4 = parseString(reader) var _5: Int64? _5 = reader.readInt64() + var _6: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!) + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 7f7ead710e..ef5c9801fb 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -986,8 +986,8 @@ public extension Api { case messageActionEmpty case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) - case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) - case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) + case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case messageActionGiveawayLaunch(flags: Int32, stars: Int64?) case messageActionGiveawayResults(flags: Int32, winnersCount: Int32, unclaimedCount: Int32) @@ -1141,9 +1141,9 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(1737240073) + buffer.appendInt32(1456486804) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)} @@ -1153,10 +1153,11 @@ public extension Api { if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)} break - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(-935499028) + buffer.appendInt32(1818391802) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -1164,6 +1165,7 @@ public extension Api { serializeInt32(months, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): if boxed { @@ -1439,10 +1441,10 @@ public extension Api { return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)]) case .messageActionGiveawayLaunch(let flags, let stars): @@ -1718,6 +1720,10 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) } var _8: Int64? if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } + var _9: Api.TextWithEntities? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil @@ -1726,8 +1732,9 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8) + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9) } else { return nil @@ -1746,14 +1753,19 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } var _6: Int64? if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } + var _7: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6) + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 8d6b3f69bd..656494c344 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -36,13 +36,13 @@ final class VideoChatActionButtonComponent: Component { case leave } - case audio(audio: Audio) + case audio(audio: Audio, isEnabled: Bool) case video(isActive: Bool) case leave fileprivate var iconType: IconType { switch self { - case let .audio(audio): + case let .audio(audio, _): let mappedAudio: IconType.Audio switch audio { case .none, .builtin, .speaker: @@ -136,14 +136,16 @@ final class VideoChatActionButtonComponent: Component { let titleText: String let backgroundColor: UIColor let iconDiameter: CGFloat + var isEnabled: Bool = true switch component.content { - case let .audio(audio): + case let .audio(audio, isEnabledValue): var isActive = false switch audio { case .none, .builtin: titleText = component.strings.Call_Speaker case .speaker: - isActive = true + isEnabled = isEnabledValue + isActive = isEnabledValue titleText = component.strings.Call_Speaker case .headphones: titleText = component.strings.Call_Audio @@ -276,8 +278,11 @@ final class VideoChatActionButtonComponent: Component { self.addSubview(iconView) } transition.setFrame(view: iconView, frame: iconFrame) + transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6) } + self.isEnabled = isEnabled + return size } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index 355c75d06f..ff93749b66 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -11,6 +11,7 @@ import SwiftSignalKit import MetalEngine import CallScreen import AvatarNode +import ContextUI final class VideoChatParticipantThumbnailComponent: Component { let call: PresentationGroupCall @@ -21,6 +22,7 @@ final class VideoChatParticipantThumbnailComponent: Component { let isSpeaking: Bool let interfaceOrientation: UIInterfaceOrientation let action: (() -> Void)? + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: PresentationGroupCall, @@ -30,7 +32,8 @@ final class VideoChatParticipantThumbnailComponent: Component { isSelected: Bool, isSpeaking: Bool, interfaceOrientation: UIInterfaceOrientation, - action: (() -> Void)? + action: (() -> Void)?, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme @@ -40,6 +43,7 @@ final class VideoChatParticipantThumbnailComponent: Component { self.isSpeaking = isSpeaking self.interfaceOrientation = interfaceOrientation self.action = action + self.contextAction = contextAction } static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool { @@ -64,6 +68,12 @@ final class VideoChatParticipantThumbnailComponent: Component { if lhs.interfaceOrientation != rhs.interfaceOrientation { return false } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } return true } @@ -79,7 +89,7 @@ final class VideoChatParticipantThumbnailComponent: Component { } } - final class View: HighlightTrackingButton { + final class View: ContextControllerSourceView { private static let selectedBorderImage: UIImage? = { return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate) }() @@ -88,6 +98,10 @@ final class VideoChatParticipantThumbnailComponent: Component { private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false + private let extractedContainerView: ContextExtractedContentContainingView + + private let backgroundLayer: SimpleLayer + private var avatarNode: AvatarNode? private let title = ComponentView() private let muteStatus = ComponentView() @@ -101,13 +115,30 @@ final class VideoChatParticipantThumbnailComponent: Component { private var videoSpec: VideoSpec? override init(frame: CGRect) { + self.extractedContainerView = ContextExtractedContentContainingView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor + super.init(frame: frame) - //TODO:release optimize - self.clipsToBounds = true - self.layer.cornerRadius = 10.0 + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer) + + self.extractedContainerView.contentView.clipsToBounds = true + self.extractedContainerView.contentView.layer.cornerRadius = 10.0 + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + } } required init?(coder: NSCoder) { @@ -118,11 +149,13 @@ final class VideoChatParticipantThumbnailComponent: Component { self.videoDisposable?.dispose() } - @objc private func pressed() { - guard let component = self.component, let action = component.action else { - return + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let component = self.component, let action = component.action else { + return + } + action() } - action() } func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -131,10 +164,6 @@ final class VideoChatParticipantThumbnailComponent: Component { self.isUpdating = false } - if self.component == nil { - self.backgroundColor = UIColor(rgb: 0x1C1C1E) - } - let previousComponent = self.component let wasSpeaking = previousComponent?.isSpeaking ?? false @@ -156,6 +185,14 @@ final class VideoChatParticipantThumbnailComponent: Component { self.component = component self.componentState = state + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) + + transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) + let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current @@ -163,7 +200,7 @@ final class VideoChatParticipantThumbnailComponent: Component { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) avatarNode.isUserInteractionEnabled = false self.avatarNode = avatarNode - self.addSubview(avatarNode.view) + self.extractedContainerView.contentView.addSubview(avatarNode.view) } let avatarSize = CGSize(width: 50.0, height: 50.0) @@ -188,7 +225,7 @@ final class VideoChatParticipantThumbnailComponent: Component { let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize) if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View { if muteStatusView.superview == nil { - self.addSubview(muteStatusView) + self.extractedContainerView.contentView.addSubview(muteStatusView) } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) @@ -208,7 +245,7 @@ final class VideoChatParticipantThumbnailComponent: Component { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() titleView.isUserInteractionEnabled = false - self.addSubview(titleView) + self.extractedContainerView.contentView.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) @@ -222,7 +259,7 @@ final class VideoChatParticipantThumbnailComponent: Component { videoBackgroundLayer = SimpleLayer() videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor self.videoBackgroundLayer = videoBackgroundLayer - self.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) + self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) videoBackgroundLayer.isHidden = true } @@ -232,8 +269,8 @@ final class VideoChatParticipantThumbnailComponent: Component { } else { videoLayer = PrivateCallVideoLayer() self.videoLayer = videoLayer - self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) - self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) + self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) + self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) videoLayer.blurredLayer.opacity = 0.25 @@ -346,7 +383,7 @@ final class VideoChatParticipantThumbnailComponent: Component { selectedBorderView = UIImageView() self.selectedBorderView = selectedBorderView selectedBorderView.alpha = 0.0 - self.addSubview(selectedBorderView) + self.extractedContainerView.contentView.addSubview(selectedBorderView) selectedBorderView.image = View.selectedBorderImage selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize) @@ -438,6 +475,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { let speakingParticipants: Set let interfaceOrientation: UIInterfaceOrientation let updateSelectedParticipant: (Participant.Key) -> Void + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: PresentationGroupCall, @@ -446,7 +484,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { selectedParticipant: Participant.Key?, speakingParticipants: Set, interfaceOrientation: UIInterfaceOrientation, - updateSelectedParticipant: @escaping (Participant.Key) -> Void + updateSelectedParticipant: @escaping (Participant.Key) -> Void, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme @@ -455,6 +494,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { self.speakingParticipants = speakingParticipants self.interfaceOrientation = interfaceOrientation self.updateSelectedParticipant = updateSelectedParticipant + self.contextAction = contextAction } static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool { @@ -476,6 +516,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { if lhs.interfaceOrientation != rhs.interfaceOrientation { return false } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } return true } @@ -617,7 +660,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { return } component.updateSelectedParticipant(participantKey) - } + }, + contextAction: component.contextAction )), environment: {}, containerSize: itemFrame.size diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift index 3bc92c4bcf..748a319b13 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift @@ -16,13 +16,19 @@ final class VideoChatMuteIconComponent: Component { let color: UIColor let content: Content + let shadowColor: UIColor? + let shadowBlur: CGFloat init( color: UIColor, - content: Content + content: Content, + shadowColor: UIColor? = nil, + shadowBlur: CGFloat = 0.0 ) { self.color = color self.content = content + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool { @@ -32,6 +38,12 @@ final class VideoChatMuteIconComponent: Component { if lhs.content != rhs.content { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } @@ -75,9 +87,9 @@ final class VideoChatMuteIconComponent: Component { } let animationSize = availableSize - let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) + let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)).insetBy(dx: -component.shadowBlur, dy: -component.shadowBlur) transition.setFrame(view: icon.view, frame: animationFrame) - icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate) + icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color, shadowColor: component.shadowColor, shadowBlur: component.shadowBlur), animated: !transition.animation.isImmediate) } else { if let icon = self.icon { self.icon = nil @@ -97,7 +109,9 @@ final class VideoChatMuteIconComponent: Component { transition: transition, component: AnyComponent(BundleIconComponent( name: "Call/StatusScreen", - tintColor: component.color + tintColor: component.color, + shadowColor: component.shadowColor, + shadowBlur: component.shadowBlur )), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index d295c40a7f..fa08eaca1a 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -12,6 +12,9 @@ import AccountContext import SwiftSignalKit import DirectMediaImageCache import FastBlur +import ContextUI +import ComponentDisplayAdapters +import AvatarNode private func blurredAvatarImage(_ dataImage: UIImage) -> UIImage? { let imageContextSize = CGSize(width: 64.0, height: 64.0) @@ -35,6 +38,7 @@ private let activityBorderImage: UIImage = { }() final class VideoChatParticipantVideoComponent: Component { + let theme: PresentationTheme let strings: PresentationStrings let call: PresentationGroupCall let participant: GroupCallParticipantsContext.Participant @@ -47,8 +51,12 @@ final class VideoChatParticipantVideoComponent: Component { let controlInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation let action: (() -> Void)? + let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? + let activatePinch: ((PinchSourceContainerNode) -> Void)? + let deactivatedPinch: (() -> Void)? init( + theme: PresentationTheme, strings: PresentationStrings, call: PresentationGroupCall, participant: GroupCallParticipantsContext.Participant, @@ -60,8 +68,12 @@ final class VideoChatParticipantVideoComponent: Component { contentInsets: UIEdgeInsets, controlInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, - action: (() -> Void)? + action: (() -> Void)?, + contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?, + activatePinch: ((PinchSourceContainerNode) -> Void)?, + deactivatedPinch: (() -> Void)? ) { + self.theme = theme self.strings = strings self.call = call self.participant = participant @@ -74,6 +86,9 @@ final class VideoChatParticipantVideoComponent: Component { self.controlInsets = controlInsets self.interfaceOrientation = interfaceOrientation self.action = action + self.contextAction = contextAction + self.activatePinch = activatePinch + self.deactivatedPinch = deactivatedPinch } static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool { @@ -107,6 +122,15 @@ final class VideoChatParticipantVideoComponent: Component { if (lhs.action == nil) != (rhs.action == nil) { return false } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } + if (lhs.activatePinch == nil) != (rhs.activatePinch == nil) { + return false + } + if (lhs.deactivatedPinch == nil) != (rhs.deactivatedPinch == nil) { + return false + } return true } @@ -144,7 +168,7 @@ final class VideoChatParticipantVideoComponent: Component { } } - final class View: HighlightTrackingButton { + final class View: ContextControllerSourceView { private var component: VideoChatParticipantVideoComponent? private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false @@ -158,6 +182,8 @@ final class VideoChatParticipantVideoComponent: Component { private var blurredAvatarDisposable: Disposable? private var blurredAvatarView: UIImageView? + private let pinchContainerNode: PinchSourceContainerNode + private let extractedContainerView: ContextExtractedContentContainingView private var videoSource: AdaptedCallVideoSource? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? @@ -173,16 +199,44 @@ final class VideoChatParticipantVideoComponent: Component { override init(frame: CGRect) { self.backgroundGradientView = UIImageView() + self.pinchContainerNode = PinchSourceContainerNode() + self.extractedContainerView = ContextExtractedContentContainingView() super.init(frame: frame) - self.addSubview(self.backgroundGradientView) + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view) + self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView) //TODO:release optimize - self.clipsToBounds = true - self.layer.cornerRadius = 10.0 + self.pinchContainerNode.contentNode.view.layer.cornerRadius = 10.0 + self.pinchContainerNode.contentNode.view.clipsToBounds = true - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let self, let component = self.component else { + return + } + component.activatePinch?(sourceNode) + } + self.pinchContainerNode.animatedOut = { [weak self] in + guard let self, let component = self.component else { + return + } + + component.deactivatedPinch?() + } + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + } } required init?(coder: NSCoder) { @@ -194,11 +248,13 @@ final class VideoChatParticipantVideoComponent: Component { self.blurredAvatarDisposable?.dispose() } - @objc private func pressed() { - guard let component = self.component, let action = component.action else { - return + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let component = self.component, let action = component.action else { + return + } + action() } - action() } func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -211,6 +267,21 @@ final class VideoChatParticipantVideoComponent: Component { self.component = component self.componentState = state + self.isGestureEnabled = !component.isExpanded + + self.pinchContainerNode.isPinchGestureEnabled = component.activatePinch != nil + transition.setPosition(view: self.pinchContainerNode.view, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.pinchContainerNode.view, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.pinchContainerNode.update(size: availableSize, transition: transition.containedViewLayoutTransition) + + transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) + + transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize)) let alphaTransition: ComponentTransition @@ -229,14 +300,10 @@ final class VideoChatParticipantVideoComponent: Component { let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0 - let nameColor = component.participant.peer.nameColor ?? .blue - let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) - if previousComponent == nil { - self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: [ - nameColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 1.3), - nameColors.main.withMultiplied(hue: 1.0, saturation: 1.2, brightness: 1.0) - ], locations: [0.0, 1.0], direction: .vertical) + let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer.id, nameColor: component.participant.peer.nameColor, icon: .none, theme: component.theme) + + self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical) } if let smallProfileImage = component.participant.peer.smallProfileImage { @@ -249,7 +316,7 @@ final class VideoChatParticipantVideoComponent: Component { blurredAvatarView = UIImageView() blurredAvatarView.contentMode = .scaleAspectFill self.blurredAvatarView = blurredAvatarView - self.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView) + self.pinchContainerNode.contentNode.view.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView) blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize) } @@ -292,7 +359,9 @@ final class VideoChatParticipantVideoComponent: Component { transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, - content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking) + content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking), + shadowColor: UIColor(white: 0.0, alpha: 0.7), + shadowBlur: 8.0 )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) @@ -305,14 +374,8 @@ final class VideoChatParticipantVideoComponent: Component { } if let muteStatusView = self.muteStatus.view { if muteStatusView.superview == nil { - self.addSubview(muteStatusView) + self.pinchContainerNode.contentNode.view.addSubview(muteStatusView) muteStatusView.alpha = controlsAlpha - - //TODO:release - muteStatusView.layer.shadowOpacity = 0.7 - muteStatusView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor - muteStatusView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) - muteStatusView.layer.shadowRadius = 8.0 } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) @@ -320,31 +383,29 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha) } + let titleInnerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)) + text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)), + insets: titleInnerInsets, + textShadowColor: UIColor(white: 0.0, alpha: 0.7), + textShadowBlur: 8.0 )), environment: {}, containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0) ) let titleFrame: CGRect if component.isExpanded { - titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 36.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height + titleInnerInsets.top), size: titleSize) } else { - titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 29.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height + titleInnerInsets.top + 1.0), size: titleSize) } if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() - self.addSubview(titleView) + self.pinchContainerNode.contentNode.view.addSubview(titleView) titleView.alpha = controlsAlpha - - //TODO:release - titleView.layer.shadowOpacity = 0.7 - titleView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor - titleView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) - titleView.layer.shadowRadius = 8.0 } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) @@ -377,9 +438,9 @@ final class VideoChatParticipantVideoComponent: Component { videoBackgroundLayer.opacity = 0.0 self.videoBackgroundLayer = videoBackgroundLayer if let blurredAvatarView = self.blurredAvatarView { - self.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) } else { - self.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer) } videoBackgroundLayer.isHidden = true } @@ -391,8 +452,8 @@ final class VideoChatParticipantVideoComponent: Component { videoLayer = PrivateCallVideoLayer() self.videoLayer = videoLayer videoLayer.opacity = 0.0 - self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) - self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) + self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) videoLayer.blurredLayer.opacity = 0.0 @@ -537,7 +598,7 @@ final class VideoChatParticipantVideoComponent: Component { if videoStatusView.superview == nil { videoStatusView.isUserInteractionEnabled = false videoStatusView.alpha = 0.0 - self.addSubview(videoStatusView) + self.pinchContainerNode.contentNode.view.addSubview(videoStatusView) } videoStatusTransition.setFrame(view: videoStatusView, frame: CGRect(origin: CGPoint(), size: availableSize)) videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 1.0) @@ -557,7 +618,7 @@ final class VideoChatParticipantVideoComponent: Component { self.loadingEffectView = loadingEffectView loadingEffectView.alpha = 0.0 loadingEffectView.isUserInteractionEnabled = false - self.addSubview(loadingEffectView) + self.pinchContainerNode.contentNode.view.addSubview(loadingEffectView) if let referenceLocation = self.referenceLocation { self.updateHorizontalReferenceLocation(containerWidth: referenceLocation.containerWidth, positionX: referenceLocation.positionX, transition: .immediate) } @@ -578,7 +639,7 @@ final class VideoChatParticipantVideoComponent: Component { } else { activityBorderView = UIImageView() self.activityBorderView = activityBorderView - self.addSubview(activityBorderView) + self.pinchContainerNode.contentNode.view.addSubview(activityBorderView) activityBorderView.image = activityBorderImage activityBorderView.tintColor = UIColor(rgb: 0x33C758) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index dda1b187c8..7b98a0b136 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -10,6 +10,7 @@ import SwiftSignalKit import MultilineTextComponent import TelegramPresentationData import PeerListItemComponent +import ContextUI final class VideoChatParticipantsComponent: Component { struct Layout: Equatable { @@ -645,6 +646,10 @@ final class VideoChatParticipantsComponent: Component { private var appliedGridIsEmpty: Bool = true + private var isPinchToZoomActive: Bool = false + + private var stopRequestingNonCentralVideo: Bool = false + private var stopRequestingNonCentralVideoTimer: Foundation.Timer? private var currentLoadMoreToken: String? private var mainScrollViewEventCycleState: EventCycleState? @@ -719,6 +724,10 @@ final class VideoChatParticipantsComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.stopRequestingNonCentralVideoTimer?.invalidate() + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let component = self.component else { return nil @@ -986,7 +995,10 @@ final class VideoChatParticipantsComponent: Component { var itemControlInsets: UIEdgeInsets if isItemExpanded { itemControlInsets = itemContentInsets - itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0) + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden { + } else { + itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0) + } } else { itemControlInsets = itemContentInsets } @@ -1003,6 +1015,7 @@ final class VideoChatParticipantsComponent: Component { let _ = itemView.view.update( transition: itemTransition, component: AnyComponent(VideoChatParticipantVideoComponent( + theme: component.theme, strings: component.strings, call: component.call, participant: videoParticipant.participant, @@ -1010,7 +1023,7 @@ final class VideoChatParticipantsComponent: Component { isPresentation: videoParticipant.isPresentation, isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isExpanded: isItemExpanded, - isUIHidden: isItemUIHidden, + isUIHidden: isItemUIHidden || self.isPinchToZoomActive, contentInsets: itemContentInsets, controlInsets: itemControlInsets, interfaceOrientation: component.interfaceOrientation, @@ -1032,7 +1045,31 @@ final class VideoChatParticipantsComponent: Component { component.updateMainParticipant(videoParticipantKey, nil) } } - } + }, + contextAction: !isItemExpanded ? { [weak self] peer, sourceView, gesture in + guard let self, let component = self.component else { + return + } + component.openParticipantContextMenu(peer.id, sourceView, gesture) + } : nil, + activatePinch: isItemExpanded ? { [weak self] sourceNode in + guard let self, let component = self.component else { + return + } + self.isPinchToZoomActive = true + self.state?.updated(transition: .immediate, isLocal: true) + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + component.call.accountContext.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } : nil, + deactivatedPinch: isItemExpanded ? { [weak self] in + guard let self else { + return + } + self.isPinchToZoomActive = false + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } : nil )), environment: {}, containerSize: itemFrame.size @@ -1158,7 +1195,7 @@ final class VideoChatParticipantsComponent: Component { if participant.peer.id == component.call.accountContext.account.peerId { subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) } else if component.speakingParticipants.contains(participant.peer.id) { - if let volume = participant.volume, volume != 10000 { + if let volume = participant.volume, volume / 100 != 100 { subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive) } else { subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive) @@ -1322,17 +1359,8 @@ final class VideoChatParticipantsComponent: Component { )) }*/ - let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0 + let expandedControlsAlpha: CGFloat = (expandedVideoState.isUIHidden || self.isPinchToZoomActive) ? 0.0 : 1.0 let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha - /*if itemLayout.layout.videoColumn == nil { - if expandedVideoState.isUIHidden { - expandedThumbnailsAlpha = 0.0 - } else { - expandedThumbnailsAlpha = 1.0 - } - } else { - expandedThumbnailsAlpha = 0.0 - }*/ var expandedThumbnailsTransition = transition let expandedThumbnailsView: ComponentView @@ -1359,12 +1387,21 @@ final class VideoChatParticipantsComponent: Component { return } component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil) + }, + contextAction: { [weak self] peer, sourceView, gesture in + guard let self, let component = self.component else { + return + } + component.openParticipantContextMenu(peer.id, sourceView, gesture) } )), environment: {}, containerSize: itemLayout.expandedGrid.itemContainerFrame().size ) - let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) + var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) + if expandedVideoState.isUIHidden { + expandedThumbnailsFrame.origin.y += expandedThumbnailsSize.height + } if let expandedThumbnailsComponentView = expandedThumbnailsView.view { if expandedThumbnailsComponentView.superview == nil { self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView) @@ -1577,9 +1614,33 @@ final class VideoChatParticipantsComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden { + if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState { + self.stopRequestingNonCentralVideoTimer?.invalidate() + + self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.stopRequestingNonCentralVideo = true + self.stopRequestingNonCentralVideoTimer = nil + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + }) + } + } else { + self.stopRequestingNonCentralVideo = false + if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer { + self.stopRequestingNonCentralVideoTimer = nil + stopRequestingNonCentralVideoTimer.invalidate() + } + } + let measureListItemSize = self.measureListItemView.update( transition: .immediate, component: AnyComponent(PeerListItemComponent( @@ -1727,13 +1788,19 @@ final class VideoChatParticipantsComponent: Component { } if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) { - if !requestedVideo.contains(videoChannel) { - requestedVideo.append(videoChannel) + if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full { + } else { + if !requestedVideo.contains(videoChannel) { + requestedVideo.append(videoChannel) + } } } if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) { - if !requestedVideo.contains(videoChannel) { - requestedVideo.append(videoChannel) + if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full { + } else { + if !requestedVideo.contains(videoChannel) { + requestedVideo.append(videoChannel) + } } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index d013e312fb..35c8beec52 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -22,6 +22,7 @@ import ShareController import AvatarNode import TelegramAudio import LegacyComponents +import TooltipUI final class VideoChatScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -83,6 +84,7 @@ final class VideoChatScreenComponent: Component { var scheduleInfo: ComponentView? var reconnectedAsEventsDisposable: Disposable? + var memberEventsDisposable: Disposable? var peer: EnginePeer? var callState: PresentationGroupCallState? @@ -144,6 +146,7 @@ final class VideoChatScreenComponent: Component { self.membersDisposable?.dispose() self.applicationStateDisposable?.dispose() self.reconnectedAsEventsDisposable?.dispose() + self.memberEventsDisposable?.dispose() self.displayAsPeersDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.inviteLinksDisposable?.dispose() @@ -819,7 +822,7 @@ final class VideoChatScreenComponent: Component { self.members = members - if let members, let _ = self.expandedParticipantsVideoState { + if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden { var videoCount = 0 for participant in members.participants { if participant.presentationDescription != nil { @@ -1008,6 +1011,31 @@ final class VideoChatScreenComponent: Component { } self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) }) + + self.memberEventsDisposable = (component.call.memberEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let self, let members = self.members, let component = self.component, let environment = self.environment else { + return + } + if event.joined { + var displayEvent = false + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + displayEvent = false + } + if members.totalCount < 250 { + displayEvent = true + } else if event.peer.isVerified { + displayEvent = true + } else if event.isContact || event.isInChatList { + displayEvent = true + } + + if displayEvent { + let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + } + }) } self.isPresentedValue.set(environment.isVisible) @@ -1072,7 +1100,7 @@ final class VideoChatScreenComponent: Component { } else { containerOffset = verticalPanState.fraction * availableSize.height } - self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius + self.containerView.layer.cornerRadius = containerOffset.isZero ? 0.0 : environment.deviceMetrics.screenCornerRadius } transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: availableSize), completion: { [weak self] completed in @@ -1249,21 +1277,61 @@ final class VideoChatScreenComponent: Component { } else { idleTitleStatusText = " " } + + let canManageCall = self.callState?.canManageCall ?? false + + var maxTitleWidth: CGFloat = availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0 + if isTwoColumnLayout { + maxTitleWidth -= 110.0 + } + let titleSize = self.title.update( transition: transition, component: AnyComponent(VideoChatTitleComponent( title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ", status: idleTitleStatusText, isRecording: self.callState?.recordingStartTimestamp != nil, - strings: environment.strings + strings: environment.strings, + tapAction: self.callState?.recordingStartTimestamp != nil ? { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let titleView = self.title.view as? VideoChatTitleComponent.View, let recordingIndicatorView = titleView.recordingIndicatorView else { + return + } + var hasTooltipAlready = false + environment.controller()?.forEachController { controller -> Bool in + if controller is TooltipScreen { + hasTooltipAlready = true + } + return true + } + if !hasTooltipAlready { + let location = recordingIndicatorView.convert(recordingIndicatorView.bounds, to: self) + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingInProgress + } else { + text = environment.strings.VoiceChat_RecordingInProgress + } + environment.controller()?.present(TooltipScreen(account: component.call.accountContext.account, sharedContext: component.call.accountContext.sharedContext, text: .plain(text: text), icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _, _ in + return .dismiss(consume: true) + }), in: .current) + } + } : nil, + longTapAction: canManageCall ? { [weak self] in + guard let self else { + return + } + self.openTitleEditing() + } : nil )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0, height: 100.0) + containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { - titleView.isUserInteractionEnabled = false self.containerView.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) @@ -1436,7 +1504,7 @@ final class VideoChatScreenComponent: Component { component: AnyComponent(VideoChatParticipantsComponent( call: component.call, participants: mappedParticipants, - speakingParticipants: members?.speakingParticipants ?? Set(), + speakingParticipants: self.members?.speakingParticipants ?? Set(), expandedVideoState: self.expandedParticipantsVideoState, theme: environment.theme, strings: environment.strings, @@ -1699,7 +1767,9 @@ final class VideoChatScreenComponent: Component { let videoButtonContent: VideoChatActionButtonComponent.Content if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute { var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker + var buttonIsEnabled = false if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput { + buttonIsEnabled = availableOutputs.count > 1 switch currentOutput { case .builtin: buttonAudio = .builtin @@ -1723,7 +1793,7 @@ final class VideoChatScreenComponent: Component { buttonAudio = .none } } - videoButtonContent = .audio(audio: buttonAudio) + videoButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled) } else { //TODO:release videoButtonContent = .video(isActive: false) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index 6b36289463..b1818a7e93 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -12,17 +12,23 @@ final class VideoChatTitleComponent: Component { let status: String let isRecording: Bool let strings: PresentationStrings + let tapAction: (() -> Void)? + let longTapAction: (() -> Void)? init( title: String, status: String, isRecording: Bool, - strings: PresentationStrings + strings: PresentationStrings, + tapAction: (() -> Void)?, + longTapAction: (() -> Void)? ) { self.title = title self.status = status self.isRecording = isRecording self.strings = strings + self.tapAction = tapAction + self.longTapAction = longTapAction } static func ==(lhs: VideoChatTitleComponent, rhs: VideoChatTitleComponent) -> Bool { @@ -38,6 +44,12 @@ final class VideoChatTitleComponent: Component { if lhs.strings !== rhs.strings { return false } + if (lhs.tapAction == nil) != (rhs.tapAction == nil) { + return false + } + if (lhs.longTapAction == nil) != (rhs.longTapAction == nil) { + return false + } return true } @@ -55,6 +67,12 @@ final class VideoChatTitleComponent: Component { private var currentActivityStatus: String? private var currentSize: CGSize? + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + + public var recordingIndicatorView: UIView? { + return self.recordingImageView + } + override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() @@ -67,12 +85,33 @@ final class VideoChatTitleComponent: Component { } self.updateAnimations() } + + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + tapRecognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.addGestureRecognizer(tapRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if case .tap = gesture { + component.tapAction?() + } else if case .longTap = gesture { + component.longTapAction?() + } + } + } + } + private func updateAnimations() { if let recordingImageView = self.recordingImageView { if recordingImageView.layer.animation(forKey: "blink") == nil { @@ -153,15 +192,22 @@ final class VideoChatTitleComponent: Component { self.component = component + self.tapRecognizer?.isEnabled = component.longTapAction != nil || component.tapAction != nil + let spacing: CGFloat = 1.0 + var maxTitleWidth = availableSize.width + if component.isRecording { + maxTitleWidth -= 10.0 + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) + containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) let statusComponent: AnyComponent @@ -181,15 +227,18 @@ final class VideoChatTitleComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + titleView.isUserInteractionEnabled = false self.addSubview(titleView) } - transition.setPosition(view: titleView, position: titleFrame.center) + transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize) if let statusView = self.status.view { if statusView.superview == nil { + statusView.isUserInteractionEnabled = false self.addSubview(statusView) } transition.setPosition(view: statusView, position: statusFrame.center) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift index 0edd0f7c02..c644942dc0 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift @@ -5,12 +5,16 @@ import Display private final class VoiceChatMicrophoneNodeDrawingState: NSObject { let color: UIColor + let shadowColor: UIColor? + let shadowBlur: CGFloat let filled: Bool let transition: CGFloat let reverse: Bool - init(color: UIColor, filled: Bool, transition: CGFloat, reverse: Bool) { + init(color: UIColor, shadowColor: UIColor?, shadowBlur: CGFloat, filled: Bool, transition: CGFloat, reverse: Bool) { self.color = color + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur self.filled = filled self.transition = transition self.reverse = reverse @@ -24,11 +28,15 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { let muted: Bool let color: UIColor let filled: Bool + let shadowColor: UIColor? + let shadowBlur: CGFloat - init(muted: Bool, filled: Bool, color: UIColor) { + init(muted: Bool, filled: Bool, color: UIColor, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) { self.muted = muted self.filled = filled self.color = color + self.shadowColor = shadowColor + self.shadowBlur = shadowBlur } static func ==(lhs: State, rhs: State) -> Bool { @@ -41,6 +49,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { if lhs.filled != rhs.filled { return false } + if lhs.shadowColor != rhs.shadowColor { + return false + } + if lhs.shadowBlur != rhs.shadowBlur { + return false + } return true } } @@ -122,6 +136,8 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0 var color = self.state.color + var shadowColor = self.state.shadowColor + var shadowBlur = self.state.shadowBlur var reverse = false if let transitionContext = self.transitionContext { @@ -138,9 +154,17 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { if transitionContext.previousState.color.rgb != color.rgb { color = transitionContext.previousState.color.interpolateTo(color, fraction: t)! } + + if let previousShadowColor = transitionContext.previousState.shadowColor, let shadowColorValue = shadowColor, previousShadowColor.rgb != shadowColorValue.rgb { + shadowColor = previousShadowColor.interpolateTo(shadowColorValue, fraction: t)! + } + + if transitionContext.previousState.shadowBlur != shadowBlur { + shadowBlur = transitionContext.previousState.shadowBlur * (1.0 - t) + shadowBlur * t + } } - return VoiceChatMicrophoneNodeDrawingState(color: color, filled: self.state.filled, transition: transitionFraction, reverse: reverse) + return VoiceChatMicrophoneNodeDrawingState(color: color, shadowColor: shadowColor, shadowBlur: shadowBlur, filled: self.state.filled, transition: transitionFraction, reverse: reverse) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -155,9 +179,18 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { guard let parameters = parameters as? VoiceChatMicrophoneNodeDrawingState else { return } + + var bounds = bounds + bounds = bounds.insetBy(dx: parameters.shadowBlur, dy: parameters.shadowBlur) + + context.translateBy(x: bounds.minX, y: bounds.minY) context.setFillColor(parameters.color.cgColor) + if let shadowColor = parameters.shadowColor, parameters.shadowBlur != 0.0 { + context.setShadow(offset: CGSize(), blur: parameters.shadowBlur, color: shadowColor.cgColor) + } + var clearLineWidth: CGFloat = 2.0 var lineWidth: CGFloat = 1.0 + UIScreenPixel if bounds.size.width > 36.0 { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 302ec5da6b..3fa705177b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -254,7 +254,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } case let .messageActionRequestedPeer(_, peers): result.append(contentsOf: peers.map(\.peerId)) - case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _): + case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _, _): if let boostPeer = boostPeer { result.append(boostPeer.peerId) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 2557eb591c..de186a7a37 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -100,8 +100,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .joinedByRequest) case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text): return TelegramMediaAction(action: .webViewData(text)) - case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId): return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId)) case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): @@ -133,8 +143,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe } else { return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0)) } - case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiveawayLaunch(_, stars): return TelegramMediaAction(action: .giveawayLaunched(stars: stars)) case let .messageActionGiveawayResults(flags, winners, unclaimed): diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 12c50ddd30..901664c8fd 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 189 + return 190 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 59239b2aba..8fb86d7327 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -114,7 +114,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case setChatTheme(emoji: String) case joinedByRequest case webViewData(String) - case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?) case topicEdited(components: [ForumTopicEditComponent]) case suggestedProfilePhoto(image: TelegramMediaImage?) @@ -122,7 +122,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case requestedPeer(buttonId: Int32, peerIds: [PeerId]) case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool) case setSameChatWallpaper(wallpaper: TelegramWallpaper) - case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case giveawayLaunched(stars: Int64?) case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32, stars: Bool) @@ -200,7 +200,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 26: self = .webViewData(decoder.decodeStringForKey("t", orElse: "")) case 27: - self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 28: self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId")) case 29: @@ -230,7 +230,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 35: self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) case 36: - self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 37: self = .giveawayLaunched(stars: decoder.decodeOptionalInt64ForKey("stars")) case 38: @@ -382,7 +382,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .webViewData(text): encoder.encodeInt32(26, forKey: "_rawValue") encoder.encodeString(text, forKey: "t") - case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount): + case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(27, forKey: "_rawValue") encoder.encodeString(currency, forKey: "currency") encoder.encodeInt64(amount, forKey: "amount") @@ -390,6 +390,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount { encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency") encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount") + } else { + encoder.encodeNil(forKey: "cryptoCurrency") + encoder.encodeNil(forKey: "cryptoAmount") + } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") } case let .topicCreated(title, iconColor, iconFileId): encoder.encodeInt32(28, forKey: "_rawValue") @@ -433,7 +443,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "atp") } - case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount): + case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(36, forKey: "_rawValue") encoder.encodeString(slug, forKey: "slug") encoder.encodeBool(fromGiveaway, forKey: "give") @@ -464,6 +474,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "cryptoAmount") } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") + } case let .giveawayLaunched(stars): encoder.encodeInt32(37, forKey: "_rawValue") if let stars = stars { @@ -563,7 +580,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return peerIds case let .requestedPeer(_, peerIds): return peerIds - case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _): + case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _): return boostPeerId.flatMap { [$0] } ?? [] case let .paymentRefunded(peerId, _, _, _, _): return [peerId] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 95a4ae2dfc..f866267056 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -15,7 +15,7 @@ public enum AppStoreTransactionPurpose { case upgrade case restore case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) - case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64) + case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case stars(count: Int64, currency: String, amount: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) @@ -43,7 +43,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } - case let .giftCode(peerIds, boostPeerId, currency, amount): + case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities): return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in var flags: Int32 = 0 var apiBoostPeer: Api.InputPeer? @@ -59,8 +59,13 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran apiBoostPeer = apiPeer flags |= (1 << 0) } - - return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount) + + var message: Api.TextWithEntities? + if let text { + flags |= (1 << 1) + message = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities ?? [], associatedPeers: SimpleDictionary())) + } + return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message) } case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): return account.postbox.transaction { transaction -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 85b69769eb..1296173518 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -293,7 +293,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv } } - let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount) + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil) var flags: Int32 = 0 if let _ = option.storeProductId { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 97c3c609a0..5ee8026860 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -462,6 +462,10 @@ public extension EnginePeer { var addressName: String? { return self._asPeer().addressName } + + var usernames: [TelegramPeerUsername] { + return self._asPeer().usernames + } var indexName: EnginePeer.IndexName { return EnginePeer.IndexName(self._asPeer().indexName) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index f6099c2e55..aa07479d98 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } } } - + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var renderedMyPeers: [FoundPeer] = [] @@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } @@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 134e73fddc..5863b5a9e2 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -735,7 +735,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } case let .webViewData(text): attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor) - case let .giftPremium(currency, amount, _, _, _): + case let .giftPremium(currency, amount, _, _, _, _, _): let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) @@ -952,7 +952,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } - case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _): + case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, _, _): if boostPeerId == nil, let currency, let amount { let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { @@ -1058,7 +1058,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let _ = text let _ = entities - let starsPrice = "\(gift.price) Stars" + let starsPrice = strings.Notification_StarsGift_Stars(Int32(gift.price)) var authorName = compactAuthorName var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index 2bff980564..1769e8e2d3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -511,7 +511,7 @@ private final class VideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -531,9 +531,9 @@ private final class VideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -591,8 +591,8 @@ private final class VideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -607,7 +607,7 @@ private final class VideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index acf4bc4dbd..b286626bdd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -343,15 +343,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if oldValue != self.visibility { - switch self.visibility { - case .none: - self.textNode.visibilityRect = nil - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - self.textNode.visibilityRect = subRect - } + self.updateVisibility() } } } @@ -594,6 +586,21 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + var effectiveVisibility = self.visibility + if !isPlaying { + effectiveVisibility = .none + } + + switch effectiveVisibility { + case .none: + self.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect + } + var canPlayEffects = isPlaying if !item.controllerInteraction.canReadHistory { canPlayEffects = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 59ac95207b..8a36726273 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -680,22 +680,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI override public var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { - for contentNode in self.contentNodes { - contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) - } - - if let threadInfoNode = self.threadInfoNode { - threadInfoNode.visibility = self.visibility != .none - } - - if let replyInfoNode = self.replyInfoNode { - replyInfoNode.visibility = self.visibility != .none - } - - if let unlockButtonNode = self.unlockButtonNode { - unlockButtonNode.visibility = self.visibility != .none - } - self.visibilityStatus = self.visibility != .none self.updateVisibility() @@ -718,6 +702,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private var forceStopAnimations: Bool = false + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -6207,6 +6193,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return false } + override public func updateStickerSettings(forceStopAnimations: Bool) { + self.forceStopAnimations = forceStopAnimations + self.updateVisibility() + } + private func updateVisibility() { guard let item = self.item else { return @@ -6223,11 +6214,35 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !item.controllerInteraction.canReadHistory { isPlaying = false } + if self.forceStopAnimations { + isPlaying = false + } if !isPlaying { self.removeEffectAnimations() } + var effectiveVisibility = self.visibility + if !isPlaying { + effectiveVisibility = .none + } + + for contentNode in self.contentNodes { + contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + } + + if let threadInfoNode = self.threadInfoNode { + threadInfoNode.visibility = effectiveVisibility != .none + } + + if let replyInfoNode = self.replyInfoNode { + replyInfoNode.visibility = effectiveVisibility != .none + } + + if let unlockButtonNode = self.unlockButtonNode { + unlockButtonNode.visibility = effectiveVisibility != .none + } + if isPlaying { var alreadySeen = true if item.message.flags.contains(.Incoming) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index d683820320..eecab9955f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode private let subtitleNode: TextNodeWithEntities + private let textClippingNode: ASDisplayNode private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode @@ -49,11 +50,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode + private var maskView: UIImageView? + private var maskOverlayView: UIView? + private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var absoluteRect: (CGRect, CGSize)? private var isPlaying: Bool = false + private var isExpanded: Bool = false + private var appliedIsExpanded: Bool = false + private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { @@ -105,6 +112,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.subtitleNode.textNode.isUserInteractionEnabled = false self.subtitleNode.textNode.displaysAsynchronously = false + self.textClippingNode = ASDisplayNode() + self.textClippingNode.clipsToBounds = true + self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true self.buttonNode.cornerRadius = 17.0 @@ -133,7 +143,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.labelNode) self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode.textNode) + self.addSubnode(self.textClippingNode) + self.textClippingNode.addSubnode(self.subtitleNode.textNode) self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) @@ -251,9 +262,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) - + let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage + let currentIsExpanded = self.isExpanded + return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -279,9 +293,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .giftPremium(_, _, monthsValue, _, _): + case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities): months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } case let .giftStars(_, _, count, _, _, _): if count <= 1000 { months = 3 @@ -310,10 +334,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } title = item.presentationData.strings.Notification_StarsGiveaway_Title text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): + case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities): if channelId == nil { months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } if item.message.author?.id != item.context.account.peerId { buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift } @@ -419,13 +453,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { ), textAlignment: .center) } - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + var canExpand = false + var clippedTextHeight: CGFloat = subtitleLayout.size.height + if subtitleLayout.numberOfLines > 4 { + let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + canExpand = true + if !currentIsExpanded { + clippedTextHeight = measuredTextLayout.size.height + } + } + let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 164.0 + giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0 if !buttonTitle.isEmpty { giftSize.height += 48.0 } @@ -472,8 +517,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } return (backgroundSize.width, { boundingWidth in - return (backgroundSize, { [weak self] animation, synchronousLoads, _ in + return (backgroundSize, { [weak self] animation, synchronousLoads, info in if let strongSelf = self { + let isFirstTime = strongSelf.item == nil + + var isExpandedUpdated = false + if strongSelf.appliedIsExpanded != currentIsExpanded { + strongSelf.appliedIsExpanded = currentIsExpanded + info?.setInvertOffsetDirection() + isExpandedUpdated = true + } + let _ = isExpandedUpdated + let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) @@ -551,8 +606,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame + let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight)) + let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) - strongSelf.subtitleNode.textNode.frame = subtitleFrame + strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) + + if isFirstTime { + strongSelf.textClippingNode.frame = clippingTextFrame + } else { + animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) + } + if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { + animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + } + if !subtitleLayout.spoilers.isEmpty { let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -573,11 +641,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = nil } - let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) + let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) - strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) if ribbonTextLayout.size.width > 0.0 { @@ -666,6 +734,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateAbsoluteRect(rect, within: size) } + if canExpand { + if strongSelf.maskView?.image == nil { + strongSelf.maskView?.image = generateMaskImage() + } + strongSelf.textClippingNode.view.mask = strongSelf.maskView + +// var expandIconFrame: CGRect = .zero +// if let icon = strongSelf.expandIcon.image { +// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size) +// if wasHidden || isFirstTime { +// strongSelf.expandIcon.position = expandIconFrame.center +// } else { +// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) +// } +// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) +// } + } else { + strongSelf.textClippingNode.view.mask = nil + } + switch strongSelf.visibility { case .none: strongSelf.subtitleNode.visibilityRect = nil @@ -853,3 +941,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } + +private func generateMaskImage() -> UIImage? { + return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.setBlendMode(.copy) + context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) + context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0)) +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 707944d151..6cfd0d6f48 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1425,6 +1425,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let strongSelf = self { if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).startStrict()) + } else if NativeVideoContent.isHLSVideo(file: file) { + strongSelf.fetchDisposable.set(nil) } else { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, storeToDownloadsPeerId: storeToDownloadsPeerId).startStrict()) } @@ -1659,16 +1661,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let loopVideo = updatedVideoFile.isAnimated let videoContent: UniversalVideoContent - if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile) { - videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo) - } else { - videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in - guard let context, let peerId else { - return - } - let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() - }) - } + videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in + guard let context, let peerId else { + return + } + let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() + }) let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in @@ -1850,7 +1848,32 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if case .full = automaticDownload { + if automaticDownload != .none, let file = media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) { + let postbox = context.account.postbox + let fetchSignal = HLSVideoContent.minimizedHLSQualityPreloadData(postbox: context.account.postbox, file: .message(message: MessageReference(message), media: file), userLocation: .peer(message.id.peerId), prefixSeconds: 10, autofetchPlaylist: true) + |> mapToSignal { fileAndRange -> Signal in + guard let fileAndRange else { + return .complete() + } + return freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: .peer(message.id.peerId), fileReference: fileAndRange.0, resource: fileAndRange.0.media.resource, range: (fileAndRange.1, .default)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } + + let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() + |> mapToSignal { visibility -> Signal in + if visibility { + return fetchSignal + |> mapToSignal { _ -> Signal in + } + } else { + return .complete() + } + } + strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.startStrict()) + } else if case .full = automaticDownload { if let _ = media as? TelegramMediaImage { updatedFetchControls.fetch(false) } else if let image = media as? TelegramMediaWebFile { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD index 5de32fcb35..e5008a8e93 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + "//submodules/TelegramUniversalVideoContent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index dbe1880c41..fbf52337fa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -16,6 +16,7 @@ import ChatMessageItemCommon import ChatMessageInteractiveMediaNode import ChatControllerInteraction import InvisibleInkDustNode +import TelegramUniversalVideoContent public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override public var supportsMosaic: Bool { @@ -163,7 +164,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { - if case .full = automaticDownload { + if NativeVideoContent.isHLSVideo(file: telegramFile) { + automaticPlayback = true + } else if case .full = automaticDownload { automaticPlayback = true } else { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil @@ -207,7 +210,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { - if case .full = automaticDownload { + if NativeVideoContent.isHLSVideo(file: telegramFile) { + automaticPlayback = true + } else if case .full = automaticDownload { automaticPlayback = true } else { automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c63976ce11..c6c2ccdcc1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -101,6 +101,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + private var forceStopAnimations: Bool = false + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() @@ -2160,6 +2162,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if !item.controllerInteraction.canReadHistory { isPlaying = false } + if self.forceStopAnimations { + isPlaying = false + } if !isPlaying { self.removeEffectAnimations() @@ -2191,6 +2196,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + override public func updateStickerSettings(forceStopAnimations: Bool) { + self.forceStopAnimations = forceStopAnimations + self.updateVisibility() + } + override public func messageEffectTargetView() -> UIView? { if let result = self.dateAndStatusNode.messageEffectTargetView() { return result diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index 1d258a7b4f..5585d5c5d7 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent { switch component.subject { case .peer: if let peer = state.peer { - if case .user = peer { - mainTitle = environment.strings.Report_Title_User + if case let .user(user) = peer { + if let _ = user.botInfo { + mainTitle = environment.strings.Report_Title_Bot + } else { + mainTitle = environment.strings.Report_Title_User + } } else if case let .channel(channel) = peer, case .broadcast = channel.info { mainTitle = environment.strings.Report_Title_Channel } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 4facf746b2..f7c9b5873b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -306,7 +306,7 @@ final class GiftOptionsScreenComponent: Component { let giftController = GiftSetupScreen( context: component.context, peerId: component.peerId, - gift: gift, + subject: .starGift(gift), completion: component.completion ) mainController.push(giftController) @@ -359,94 +359,6 @@ final class GiftOptionsScreenComponent: Component { } } - private func buyPremium(_ product: PremiumGiftProduct) { - guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else { - return - } - - self.inProgressPremiumGift = product.id - self.state?.updated() - - let (currency, amount) = product.storeProduct.priceCurrencyAndAmount - - addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") - - let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount) - let quantity: Int32 = 1 - - let completion = component.completion - - let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] available in - if let strongSelf = self { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - if available { - strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let completion { - completion() - } else { - guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { - return - } - - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftOptionsScreen) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break - } - } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) - } - }, error: { [weak self] error in - guard let self, let controller = self.environment?.controller() else { - return - } - self.inProgressPremiumGift = nil - self.state?.updated(transition: .immediate) - - var errorText: String? - switch error { - case .generic: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .network: - errorText = presentationData.strings.Premium_Purchase_ErrorNetwork - case .notAllowed: - errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed - case .cantMakePayments: - errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments - case .assignFailed: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .tryLater: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .cancelled: - break - } - - if let errorText { - addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") - - let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - controller.present(alertController, in: .window(.root)) - } - })) - } else { - self?.inProgressPremiumGift = nil - self?.state?.updated(transition: .immediate) - } - } - }) - } - func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -707,7 +619,7 @@ final class GiftOptionsScreenComponent: Component { var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize) for product in premiumProducts { - let itemId = AnyHashable(product.storeProduct.id) + let itemId = AnyHashable(product.id) validIds.append(itemId) var itemTransition = transition @@ -756,7 +668,23 @@ final class GiftOptionsScreenComponent: Component { ), effectAlignment: .center, action: { [weak self] in - self?.buyPremium(product) + if let self, let component = self.component { + if let controller = controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .premium(product), + completion: component.completion + ) + mainController.push(giftController) + } + } }, animateAlpha: false ) @@ -1019,23 +947,43 @@ final class GiftOptionsScreenComponent: Component { } self.peer = peer - let shortestOptionPrice: (Int64, NSDecimalNumber) - if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) { - shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) - } else { - shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) - } - - var premiumProducts: [PremiumGiftProduct] = [] - for option in premiumOptions { - if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { - let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) - let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) - premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + if availableProducts.isEmpty { + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + premiumProducts.append( + PremiumGiftProduct( + giftOption: CachedPremiumGiftOption( + months: option.months, + currency: option.currency, + amount: option.amount, + botUrl: "", + storeProductId: option.storeProductId + ), + storeProduct: nil, + discount: nil + ) + ) } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) + } else { + let shortestOptionPrice: (Int64, NSDecimalNumber) + if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) { + shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) + } else { + shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) + } + + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { + let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) + let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) + premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + } + } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) } - self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) - + self.starGifts = starGifts self.updated() @@ -1105,25 +1053,3 @@ open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScree super.containerLayoutUpdated(layout, transition: transition) } } - -private struct PremiumGiftProduct: Equatable { - let giftOption: CachedPremiumGiftOption - let storeProduct: InAppPurchaseManager.Product - let discount: Int? - - var id: String { - return self.storeProduct.id - } - - var months: Int32 { - return self.giftOption.months - } - - var price: String { - return self.storeProduct.price - } - - var pricePerMonth: String { - return self.storeProduct.pricePerMonth(Int(self.months)) - } -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index a64e80c41d..a5a8867320 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", + "//submodules/TelegramStringFormatting", "//submodules/AccountContext", "//submodules/PresentationDataUtils", "//submodules/Markdown", @@ -41,6 +42,7 @@ swift_library( "//submodules/BotPaymentsUI", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/InAppPurchaseManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index cd02508e46..12a43a1017 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -14,6 +14,10 @@ import WallpaperBackgroundNode import ListItemComponentAdaptor final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + enum Subject: Equatable { + case premium(months: Int32, amount: Int64, currency: String) + case starGift(gift: StarGift) + } let context: AccountContext let theme: PresentationTheme let componentTheme: PresentationTheme @@ -26,7 +30,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd let nameDisplayOrder: PresentationPersonNameOrder let accountPeer: EnginePeer? - let gift: StarGift + let subject: ChatGiftPreviewItem.Subject let text: String let entities: [MessageTextEntity] @@ -42,7 +46,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, accountPeer: EnginePeer?, - gift: StarGift, + subject: ChatGiftPreviewItem.Subject, text: String, entities: [MessageTextEntity] ) { @@ -57,7 +61,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.accountPeer = accountPeer - self.gift = gift + self.subject = subject self.text = text self.entities = entities } @@ -206,9 +210,22 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { peers[authorPeerId] = item.accountPeer?._asPeer() - let media: [Media] = [ - TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false)) - ] + let media: [Media] + switch item.subject { + case let .premium(months, amount, currency): + media = [ + TelegramMediaAction( + action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities) + ) + ] + case let .starGift(gift): + media = [ + TelegramMediaAction( + action: .starGift(gift: gift, convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false) + ) + ] + } + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 5f939cebbe..713deadceb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -7,6 +7,7 @@ import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences +import TelegramStringFormatting import PresentationDataUtils import AccountContext import ComponentFlow @@ -27,24 +28,25 @@ import EmojiSuggestionsComponent import ChatPresentationInterfaceState import AudioToolbox import TextFormat +import InAppPurchaseManager final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id - let gift: StarGift + let subject: GiftSetupScreen.Subject let completion: (() -> Void)? init( context: AccountContext, peerId: EnginePeer.Id, - gift: StarGift, + subject: GiftSetupScreen.Subject, completion: (() -> Void)? = nil ) { self.context = context self.peerId = peerId - self.gift = gift + self.subject = subject self.completion = completion } @@ -55,7 +57,7 @@ final class GiftSetupScreenComponent: Component { if lhs.peerId != rhs.peerId { return false } - if lhs.gift != rhs.gift { + if lhs.subject != rhs.subject { return false } return true @@ -72,6 +74,7 @@ final class GiftSetupScreenComponent: Component { private let scrollView: ScrollView private let navigationTitle = ComponentView() + private let remainingCount = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let hideSection = ComponentView() @@ -103,6 +106,7 @@ final class GiftSetupScreenComponent: Component { private var currentEmojiSuggestionView: ComponentHostView? private var hideName = false + private var inProgress = false private var previousHadInputHeight: Bool = false private var previousInputHeight: CGFloat? @@ -139,6 +143,8 @@ final class GiftSetupScreenComponent: Component { self.addSubview(self.scrollView) self.scrollView.layer.addSublayer(self.topOverscrollLayer) + + self.disablesInteractiveKeyboardGestureRecognizer = true } required init?(coder: NSCoder) { @@ -189,16 +195,122 @@ final class GiftSetupScreenComponent: Component { } func proceed() { - guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + guard let component = self.component else { + return + } + switch component.subject { + case .premium: + self.proceedWithPremiumGift() + case .starGift: + self.proceedWithStarGift() + } + } + + func proceedWithPremiumGift() { + guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else { return } + self.inProgress = true + self.state?.updated() + + let (currency, amount) = storeProduct.priceCurrencyAndAmount + + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") + + let entities = generateChatInputTextEntities(self.textInputState.text) + let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities) + let quantity: Int32 = 1 + + let completion = component.completion + + let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + guard let self else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + if available { + let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let completion { + completion() + } else { + guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + } + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") + + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } + }) + } else { + self.inProgress = false + self.state?.updated(transition: .immediate) + } + }) + } + + func proceedWithStarGift() { + guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + return + } + let proceed = { [weak self] in guard let self else { return } + + self.inProgress = true + self.state?.updated() + let entities = generateChatInputTextEntities(self.textInputState.text) - let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities) + let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) + let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -242,11 +354,13 @@ final class GiftSetupScreenComponent: Component { } navigationController.setViewControllers(controllers, animated: true) } + + starsContext.load(force: true) }) }) } - if starsState.balance < component.gift.price { + if starsState.balance < starGift.price { let _ = (self.optionsPromise.get() |> filter { $0 != nil } |> take(1) @@ -258,7 +372,7 @@ final class GiftSetupScreenComponent: Component { context: component.context, starsContext: starsContext, options: options ?? [], - purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price), + purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price), completion: { [weak starsContext] stars in starsContext?.add(balance: stars) Queue.mainQueue().after(0.1) { @@ -349,9 +463,7 @@ final class GiftSetupScreenComponent: Component { return } if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - if self.textInputState.isEditing { - textInputView.insertText(text: text) - } + textInputView.insertText(text: text) } }, backwardsDeleteText: { [weak self] in @@ -400,6 +512,16 @@ final class GiftSetupScreenComponent: Component { } } ) + + if case .starGift = component.subject { + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } } let environment = environment[EnvironmentType.self].value @@ -453,6 +575,42 @@ final class GiftSetupScreenComponent: Component { contentHeight += environment.navigationHeight contentHeight += 26.0 + if case let .starGift(starGift) = component.subject, let availability = starGift.availability { + let remains: Int32 = availability.remains + let position = CGFloat(remains) / CGFloat(availability.total) + let remainsString = "\(remains)" + let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) + let remainingCountSize = self.remainingCount.update( + transition: transition, + component: AnyComponent(RemainingCountComponent( + inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), + activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], + inactiveTitle: environment.strings.Gift_Send_Limited, + inactiveValue: "", + inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, + activeTitle: "", + activeValue: totalString, + activeTitleColor: .white, + badgeText: "\(remainsString)", + badgePosition: position, + badgeGraphPosition: position, + invertProgress: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight - 36.0), size: remainingCountSize) + if let remainingCountView = self.remainingCount.view { + if remainingCountView.superview == nil { + self.scrollView.addSubview(remainingCountView) + } + transition.setFrame(view: remainingCountView, frame: remainingCountFrame) + } + contentHeight += remainingCountSize.height + contentHeight -= 36.0 + contentHeight += sectionSpacing + } + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) var introSectionItems: [AnyComponentWithIdentity] = [] @@ -521,20 +679,30 @@ final class GiftSetupScreenComponent: Component { inputHeight = environment.inputHeight } } + + let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" + + let introFooter: AnyComponent? + switch component.subject { + case .premium: + introFooter = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_Customize_Info(peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )) + case .starGift: + introFooter = nil + } let introSectionSize = self.introSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_Customize_Title, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - footer: nil, + header: nil, + footer: introFooter, items: introSectionItems )), environment: {}, @@ -553,6 +721,15 @@ final class GiftSetupScreenComponent: Component { let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) if let accountPeer = self.peerMap[component.context.account.peerId] { + let subject: ChatGiftPreviewItem.Subject + switch component.subject { + case let .premium(product): + let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) + subject = .premium(months: product.months, amount: amount, currency: currency) + case let .starGift(gift): + subject = .starGift(gift: gift) + } + let introContentSize = self.introContent.update( transition: transition, component: AnyComponent( @@ -569,7 +746,7 @@ final class GiftSetupScreenComponent: Component { dateTimeFormat: environment.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeer: accountPeer, - gift: component.gift, + subject: subject, text: self.textInputState.text.string, entities: generateChatInputTextEntities(self.textInputState.text) ), @@ -589,55 +766,56 @@ final class GiftSetupScreenComponent: Component { } } - let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" - let hideSectionSize = self.hideSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + if case .starGift = component.subject { + let hideSectionSize = self.hideSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 )), - maximumNumberOfLines: 0 + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in + guard let self else { + return + } + self.hideName = !self.hideName + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_HideMyName, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in - guard let self else { - return - } - self.hideName = !self.hideName - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) - if let hideSectionView = self.hideSection.view { - if hideSectionView.superview == nil { - self.scrollView.addSubview(hideSectionView) + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) + if let hideSectionView = self.hideSection.view { + if hideSectionView.superview == nil { + self.scrollView.addSubview(hideSectionView) + } + transition.setFrame(view: hideSectionView, frame: hideSectionFrame) } - transition.setFrame(view: hideSectionView, frame: hideSectionFrame) + contentHeight += hideSectionSize.height } - contentHeight += hideSectionSize.height contentHeight += bottomContentInset @@ -647,8 +825,18 @@ final class GiftSetupScreenComponent: Component { if self.starImage == nil || self.starImage?.1 !== environment.theme { self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) } - let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator) - let buttonAttributedString = NSMutableAttributedString(string: "\(environment.strings.Gift_Send_Send) # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let buttonString: String + switch component.subject { + case let .premium(product): + let amountString = product.price + buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" + case let .starGift(starGift): + let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) + buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) @@ -669,7 +857,7 @@ final class GiftSetupScreenComponent: Component { component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, - displaysProgress: false, + displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } @@ -825,7 +1013,7 @@ final class GiftSetupScreenComponent: Component { } } } - if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) { + if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode { if self.textInputState.isEditing { self.recenterOnTag = self.textInputTag } @@ -1039,12 +1227,17 @@ final class GiftSetupScreenComponent: Component { } public final class GiftSetupScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case premium(PremiumGiftProduct) + case starGift(StarGift) + } + private let context: AccountContext public init( context: AccountContext, peerId: EnginePeer.Id, - gift: StarGift, + subject: Subject, completion: (() -> Void)? = nil ) { self.context = context @@ -1052,7 +1245,7 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { super.init(context: context, component: GiftSetupScreenComponent( context: context, peerId: peerId, - gift: gift, + subject: subject, completion: completion ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) @@ -1070,9 +1263,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { fatalError("init(coder:) has not been implemented") } - deinit { - } - @objc private func cancelPressed() { self.dismiss() } @@ -1105,3 +1295,31 @@ private struct GiftConfiguration { } } } + +public struct PremiumGiftProduct: Equatable { + public let giftOption: CachedPremiumGiftOption + public let storeProduct: InAppPurchaseManager.Product? + public let discount: Int? + + public var id: String { + return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "") + } + + public var months: Int32 { + return self.giftOption.months + } + + public var price: String { + return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency) + } + + public var pricePerMonth: String { + return self.storeProduct?.pricePerMonth(Int(self.months)) ?? "" + } + + public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) { + self.giftOption = giftOption + self.storeProduct = storeProduct + self.discount = discount + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift new file mode 100644 index 0000000000..267382cf3e --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -0,0 +1,827 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import MultilineTextComponent +import Markdown +import TextFormat +import RoundedRectWithTailPath + +public class RemainingCountComponent: Component { + private let inactiveColor: UIColor + private let activeColors: [UIColor] + private let inactiveTitle: String + private let inactiveValue: String + private let inactiveTitleColor: UIColor + private let activeTitle: String + private let activeValue: String + private let activeTitleColor: UIColor + private let badgeText: String? + private let badgePosition: CGFloat + private let badgeGraphPosition: CGFloat + private let invertProgress: Bool + + public init( + inactiveColor: UIColor, + activeColors: [UIColor], + inactiveTitle: String, + inactiveValue: String, + inactiveTitleColor: UIColor, + activeTitle: String, + activeValue: String, + activeTitleColor: UIColor, + badgeText: String?, + badgePosition: CGFloat, + badgeGraphPosition: CGFloat, + invertProgress: Bool = false + ) { + self.inactiveColor = inactiveColor + self.activeColors = activeColors + self.inactiveTitle = inactiveTitle + self.inactiveValue = inactiveValue + self.inactiveTitleColor = inactiveTitleColor + self.activeTitle = activeTitle + self.activeValue = activeValue + self.activeTitleColor = activeTitleColor + self.badgeText = badgeText + self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition + self.invertProgress = invertProgress + } + + public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.activeColors != rhs.activeColors { + return false + } + if lhs.inactiveTitle != rhs.inactiveTitle { + return false + } + if lhs.inactiveValue != rhs.inactiveValue { + return false + } + if lhs.inactiveTitleColor != rhs.inactiveTitleColor { + return false + } + if lhs.activeTitle != rhs.activeTitle { + return false + } + if lhs.activeValue != rhs.activeValue { + return false + } + if lhs.activeTitleColor != rhs.activeTitleColor { + return false + } + if lhs.badgeText != rhs.badgeText { + return false + } + if lhs.badgePosition != rhs.badgePosition { + return false + } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } + if lhs.invertProgress != rhs.invertProgress { + return false + } + return true + } + + public final class View: UIView { + private var component: RemainingCountComponent? + + private let container: UIView + private let inactiveBackground: SimpleLayer + + private let inactiveTitleLabel = ComponentView() + private let inactiveValueLabel = ComponentView() + + private let innerLeftTitleLabel = ComponentView() + private let innerRightTitleLabel = ComponentView() + + private let activeContainer: UIView + private let activeBackground: SimpleLayer + + private let activeTitleLabel = ComponentView() + private let activeValueLabel = ComponentView() + + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = CAShapeLayer() + + private let badgeForeground: SimpleLayer + private let badgeLabel: BadgeLabelView + private let badgeLabelMaskView = UIImageView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + override init(frame: CGRect) { + self.container = UIView() + self.container.clipsToBounds = true + self.container.layer.cornerRadius = 9.0 + + self.inactiveBackground = SimpleLayer() + + self.activeContainer = UIView() + self.activeContainer.clipsToBounds = true + + self.activeBackground = SimpleLayer() + self.activeBackground.anchorPoint = CGPoint() + + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + self.badgeLabel = BadgeLabelView() + let _ = self.badgeLabel.update(value: "0", transition: .immediate) + self.badgeLabel.mask = self.badgeLabelMaskView + + super.init(frame: frame) + + self.addSubview(self.container) + self.container.layer.addSublayer(self.inactiveBackground) + self.container.addSubview(self.activeContainer) + self.activeContainer.layer.addSublayer(self.activeBackground) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeLabel) + + self.badgeLabelMaskView.contentMode = .scaleToFill + self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.badgeShapeAnimator?.invalidate() + } + + private var didPlayAppearanceAnimation = false + func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) { + if from == nil { + self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + let rotationAngle: CGFloat + if badgeFullSize.width > 100.0 { + rotationAngle = 0.2 + } else { + rotationAngle = 0.26 + } + + let to: CGFloat = self.badgeView.center.x + + let positionAnimation = CABasicAnimation(keyPath: "position.x") + positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0)) + positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0)) + positionAnimation.duration = 0.5 + positionAnimation.fillMode = .forwards + positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.badgeView.layer.add(positionAnimation, forKey: "appearance1") + + if from != to { + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 as NSNumber + rotateAnimation.toValue = rotationAngle as NSNumber + rotateAnimation.duration = 0.15 + rotateAnimation.fillMode = .forwards + rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + rotateAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") + + Queue.mainQueue().after(0.5, { + let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + bounceAnimation.fromValue = rotationAngle as NSNumber + bounceAnimation.toValue = -0.04 as NSNumber + bounceAnimation.duration = 0.2 + bounceAnimation.fillMode = .forwards + bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + bounceAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(bounceAnimation, forKey: "appearance3") + self.badgeView.layer.removeAnimation(forKey: "appearance2") + + Queue.mainQueue().after(0.2) { + let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + returnAnimation.fromValue = -0.04 as NSNumber + returnAnimation.toValue = 0.0 as NSNumber + returnAnimation.duration = 0.15 + returnAnimation.fillMode = .forwards + returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + self.badgeView.layer.add(returnAnimation, forKey: "appearance4") + self.badgeView.layer.removeAnimation(forKey: "appearance3") + } + }) + } + + if from == nil { + self.badgeView.alpha = 1.0 + self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + if let badgeText = component.badgeText { + let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) + var frameTransition = transition + if from == nil { + frameTransition = frameTransition.withAnimation(.none) + } + let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition) + frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + } + } + + var previousAvailableSize: CGSize? + func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor + self.activeBackground.backgroundColor = component.activeColors.last?.cgColor + + let size = CGSize(width: availableSize.width, height: 90.0) + + self.badgeLabel.color = component.activeTitleColor + + let lineHeight: CGFloat = 30.0 + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight)) + self.container.frame = containerFrame + + let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition) + let activeWidth: CGFloat = containerFrame.width - activityPosition + + let leftTextColor: UIColor + let rightTextColor: UIColor + if component.invertProgress { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.inactiveTitleColor + } else { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.activeTitleColor + } + + if "".isEmpty { + if component.invertProgress { + let innerLeftTitleSize = self.innerLeftTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerLeftTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize) + } + + let innerRightTitleSize = self.innerRightTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerRightTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize) + } + } + + let inactiveTitleSize = self.inactiveTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) + } + + let inactiveValueSize = self.inactiveValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveValue, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) + } + + let activeTitleSize = self.activeTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeTitle, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) + } + + let activeValueSize = self.activeValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + + if component.invertProgress { + self.container.bringSubviewToFront(self.activeContainer) + } + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + } + } + + var progressTransition: ComponentTransition = .immediate + if !transition.animation.isImmediate { + progressTransition = .easeInOut(duration: 0.5) + } + if "".isEmpty { + if component.invertProgress { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } else { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))) + progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } + if self.activeBackground.animation(forKey: "movement") == nil { + self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) + } + } + + let countWidth: CGFloat + if let badgeText = component.badgeText { + countWidth = CGFloat(badgeText.count) * 10.0 + } else { + countWidth = 51.0 + } + let badgeWidth: CGFloat = countWidth + 20.0 + + let badgeSize = CGSize(width: badgeWidth, height: 30.0) + let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0) + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) + self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + + let currentBadgeX = self.badgeView.center.x + + let badgePosition = component.badgePosition + + if badgePosition > 1.0 - 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 1.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + } else { + self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0) + } + } else if badgePosition < 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0) + } + } else { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.5 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0) + } + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) + } + + self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) + + if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate { + self.didPlayAppearanceAnimation = true + if transition.animation.isImmediate { + if component.badgePosition < 0.1 { + self.badgeView.alpha = 1.0 + if let badgeText = component.badgeText { + let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate) + transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX) + } + } + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(component.activeColors.count - 1) + for i in 0 ..< component.activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.activeBackground.contentsGravity = .resizeAspectFill + self.activeBackground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return size + } + + private var badgeShapeAnimator: ConstantDisplayLinkAnimator? + private func animateBadgeTailPositionChange() { + if self.badgeShapeAnimator == nil { + self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBadgeTailPositionChange() + }) + self.badgeShapeAnimator?.isPaused = true + } + + if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments { + self.badgeShapeAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath + + if t >= 1.0 { + self.badgeShapeArguments = nil + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } else { + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } + + private func setupGradientAnimations() { + guard let _ = self.component else { + return + } + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let lineOffset = 0.0 + let linePreviousValue = self.activeBackground.position.x + var lineNewValue: CGFloat = lineOffset + if linePreviousValue < 0.0 { + lineNewValue = 0.0 + } else { + lineNewValue = -self.activeContainer.bounds.width * 0.35 + } + lineNewValue -= self.activeContainer.frame.minX + self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0) + + let badgeAnimation = CABasicAnimation(keyPath: "position.x") + badgeAnimation.duration = 4.5 + badgeAnimation.fromValue = badgePreviousValue + badgeAnimation.toValue = badgeNewValue + badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + self.badgeForeground.add(badgeAnimation, forKey: "movement") + + let lineAnimation = CABasicAnimation(keyPath: "position.x") + lineAnimation.duration = 4.5 + lineAnimation.fromValue = linePreviousValue + lineAnimation.toValue = lineNewValue + lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.activeBackground.add(lineAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + + +private let labelWidth: CGFloat = 10.0 +private let labelHeight: CGFloat = 30.0 +private let labelSize = CGSize(width: labelWidth, height: labelHeight) +private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: []) + +final class BadgeLabelView: UIView { + private class StackView: UIView { + var labels: [UILabel] = [] + + var currentValue: Int32 = 0 + + var color: UIColor = .white { + didSet { + for view in self.labels { + view.textColor = self.color + } + } + } + + init() { + super.init(frame: CGRect(origin: .zero, size: labelSize)) + + var height: CGFloat = -labelHeight + for i in -1 ..< 10 { + let label = UILabel() + if i == -1 { + label.text = "9" + } else { + label.text = "\(i)" + } + label.textColor = self.color + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) + self.addSubview(label) + self.labels.append(label) + + height += labelHeight + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + let previousValue = self.currentValue + self.currentValue = value + + self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 + + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } + } + + private var itemViews: [Int: StackView] = [:] + private var staticLabel = UILabel() + + init() { + super.init(frame: .zero) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var color: UIColor = .white { + didSet { + self.staticLabel.textColor = self.color + for (_, view) in self.itemViews { + view.color = self.color + } + } + } + + func update(value: String, transition: ComponentTransition) -> CGSize { + if value.contains(" ") { + for (_, view) in self.itemViews { + view.isHidden = true + } + + if self.staticLabel.superview == nil { + self.staticLabel.textColor = self.color + self.staticLabel.font = font + + self.addSubview(self.staticLabel) + } + + self.staticLabel.text = value + let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) + self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) + + return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) + } + + let string = value + let stringArray = Array(string.map { String($0) }.reversed()) + + let totalWidth = CGFloat(stringArray.count) * labelWidth + + var validIds: [Int] = [] + for i in 0 ..< stringArray.count { + validIds.append(i) + + let itemView: StackView + var itemTransition = transition + if let current = self.itemViews[i] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = StackView() + itemView.color = self.color + self.itemViews[i] = itemView + self.addSubview(itemView) + } + + let digit = Int32(stringArray[i]) ?? 0 + itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + + itemTransition.setFrame( + view: itemView, + frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight) + ) + } + + var removeIds: [Int] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + return CGSize(width: totalWidth, height: labelHeight) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ca18d07fb3..ff09e4d3a4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent { let text: String? let entities: [MessageTextEntity]? let limitTotal: Int32? + var outgoing = false var incoming = false var savedToProfile = false var converted = false var giftId: Int64 = 0 + var date: Int32 = 0 if let arguments = component.subject.arguments { animationFile = arguments.gift.file stars = arguments.gift.price @@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent { entities = arguments.entities limitTotal = arguments.gift.availability?.total convertStars = arguments.convertStars + if case .message = component.subject { + outgoing = !arguments.incoming + } else { + outgoing = false + } incoming = arguments.incoming || arguments.peerId == component.context.account.peerId savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = arguments.gift.id + date = arguments.date } else { animationFile = nil stars = 0 @@ -235,11 +243,13 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = modifiedString } - let formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) + var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) + if outgoing { + formattedAmount = "- \(formattedAmount)" + } let countFont: UIFont = Font.semibold(17.0) let amountText = formattedAmount - let countColor = theme.list.itemDisclosureActions.constructive.fillColor - + let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor let title = title.update( component: MultilineTextComponent( @@ -331,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "date", title: strings.Gift_View_Date, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) @@ -340,11 +350,13 @@ private final class GiftViewSheetContent: CombinedComponent { if let gift = state.starGiftsMap[giftId], let availability = gift.availability { remains = availability.remains } + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator) tableItems.append(.init( id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "\(remains) of \(limitTotal)", font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) )) } @@ -361,7 +373,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, - text: .plain(attributedText) + text: .plain(attributedText), + maximumNumberOfLines: 0 ) ) )) @@ -717,14 +730,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) - var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { - return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } @@ -1065,17 +1078,26 @@ private final class TableComponent: CombinedComponent { } else { insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) } - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append((valueChild, insets)) var titleHeight: CGFloat = 0.0 if let titleChild = updatedTitleChildren[i] { titleHeight = titleChild.size.height } + + let availableValueWidth: CGFloat + if titleHeight > 0.0 { + availableValueWidth = rightColumnWidth + } else { + availableValueWidth = context.availableSize.width + } + + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) rowHeights[i] = rowHeight totalHeight += rowHeight diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift index 75dfe458b3..20641f2428 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift @@ -5,7 +5,7 @@ import TelegramPresentationData import AnimationUI import Display -class DynamicIslandMaskNode: ASDisplayNode { +final class DynamicIslandMaskNode: ASDisplayNode { var animationNode: AnimationNode? var isForum = false { @@ -39,7 +39,7 @@ class DynamicIslandMaskNode: ASDisplayNode { } } -class DynamicIslandBlurNode: ASDisplayNode { +final class DynamicIslandBlurNode: ASDisplayNode { private var effectView: UIVisualEffectView? private let fadeNode = ASDisplayNode() let gradientNode = ASImageNode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 1caa1447d7..f499db410e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1382,7 +1382,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) }) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index dce8f22379..4becea5f6d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -138,8 +138,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize) + + var index: Int32 = 0 for product in starsProducts { - let itemId = AnyHashable(product.date) + let itemId = AnyHashable(index) validIds.append(itemId) var itemTransition = transition @@ -221,6 +223,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemFrame.origin.x = sideInset itemFrame.origin.y += starsOptionSize.height + optionSpacing } + index += 1 } var removeIds: [AnyHashable] = [] @@ -243,7 +246,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.starsItems.removeValue(forKey: id) } - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0 if self.peerId == self.context.account.peerId { let transition = ComponentTransition.immediate diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift index bed9c3d450..b740b464bb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryVideoDecoration.swift @@ -14,7 +14,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init() { self.contentContainerNode = ASDisplayNode() @@ -34,9 +34,9 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -94,8 +94,8 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -110,7 +110,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration { } if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html b/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html new file mode 100755 index 0000000000..26a5e4e76c --- /dev/null +++ b/submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html @@ -0,0 +1,197 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/WebEmbed/hls.js b/submodules/TelegramUI/Resources/WebEmbed/hls.js new file mode 100644 index 0000000000..b3ac6dd055 --- /dev/null +++ b/submodules/TelegramUI/Resources/WebEmbed/hls.js @@ -0,0 +1,2 @@ +!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function v(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(m);var p=m.exports,y=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},E=Number.isSafeInteger||function(t){return"number"==typeof t&&Math.abs(t)<=T},T=Number.MAX_SAFE_INTEGER||9007199254740991,S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.MAX_AUTO_LEVEL_UPDATED="hlsMaxAutoLevelUpdated",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t.STEERING_MANIFEST_LOADED="hlsSteeringManifestLoaded",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),A=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),R=function(){},b={trace:R,debug:R,log:R,warn:R,info:R,error:R},k=b;function D(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i"):R}(e)}))}function I(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){D(t,"debug","log","info","warn","error");try{k.log('Debug logs enabled for "'+e+'" in hls.js version 1.5.15')}catch(t){k=b}}else k=b}var w=k,C=/^(\d+)x(\d+)$/,_=/(.+?)=(".*?"|.*?)(?:,|$)/g,x=function(){function t(e){"string"==typeof e&&(e=t.parseAttrList(e)),o(this,e)}var e=t.prototype;return e.decimalInteger=function(t){var e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},s(t,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(t){return"X-"===t.substring(0,2)}))}}]),t}();function P(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new x({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);y(n.getTime())&&(this._endDate=n)}}return s(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(y(t))return t}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:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&y(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r,i=t.split("@",2);r=1===i.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(i[1]),this._byteRange=[r,parseInt(i[0])+r]},s(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=p.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.urlId=0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},s(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!y(this.programDateTime))return null;var t=y(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.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.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),s(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){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.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 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.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},s(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&y(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.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=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j="undefined"!=typeof self?self:void 0,q={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},X="org.w3.clearkey",z="com.apple.streamingkeydelivery",Q="com.microsoft.playready",J="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case z:return q.FAIRPLAY;case Q:return q.PLAYREADY;case J:return q.WIDEVINE;case X:return q.CLEARKEY}}var Z="1077efecc0b24d02ace33c1e52e2fb4b",tt="e2719d58a985b3c9781ab030af78d30e",et="9a04f07998404286ab92e65be0885f95",rt="edef8ba979d64acea3c827dcd51d21ed";function it(t){return t===rt?q.WIDEVINE:t===et?q.PLAYREADY:t===Z||t===tt?q.CLEARKEY:void 0}function nt(t){switch(t){case q.FAIRPLAY:return z;case q.PLAYREADY:return Q;case q.WIDEVINE:return J;case q.CLEARKEY:return X}}function at(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[q.FAIRPLAY,q.WIDEVINE,q.PLAYREADY,q.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[q.WIDEVINE]&&r&&i.push(q.WIDEVINE),i}var st,ot=null!=j&&null!=(st=j.navigator)&&st.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function lt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var ut,ht=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},dt=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},ct=function(t,e){for(var r=e,i=0;ht(t,e);)i+=10,i+=ft(t,e+6),dt(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ft=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},gt=function(t,e){return ht(t,e)&&ft(t,e+6)+10<=t.length-e},vt=function(t){for(var e=yt(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function bt(){if(!navigator.userAgent.includes("PlayStation 4"))return ut||void 0===self.TextDecoder||(ut=new self.TextDecoder("utf-8")),ut}var kt={hexDump:function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function Ot(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(Ct(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=Ot(t.subarray(n+8,s),e.slice(1));o.length&&It.apply(r,o)}n=s}return r}function Nt(t){var e=[],r=t[0],i=8,n=xt(t,i);i+=4;var a=0,s=0;0===r?(a=xt(t,i),s=xt(t,i+4),i+=8):(a=Pt(t,i),s=Pt(t,i+8),i+=16),i+=2;var o=t.length+s,l=_t(t,i);i+=2;for(var u=0;u>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var f=xt(t,h);h+=4,e.push({referenceSize:c,subsegmentDuration:f,info:{duration:f/n,start:o,end:o+c-1}}),o+=c,i=h+=4}return{earliestPresentationTime:a,timescale:n,version:r,referencesCount:l,references:e}}function Ut(t){for(var e=[],r=Ot(t,["moov","trak"]),n=0;n12){var h=4;if(3!==u[h++])break;h=Gt(u,h),h+=2;var d=u[h++];if(128&d&&(h+=2),64&d&&(h+=u[h++]),4!==u[h++])break;h=Gt(u,h);var c=u[h++];if(64!==c)break;if(n+="."+Kt(c),h+=12,5!==u[h++])break;h=Gt(u,h);var f=u[h++],g=(248&f)>>3;31===g&&(g+=1+((7&f)<<3)+((224&u[h])>>5)),n+="."+g}break;case"hvc1":case"hev1":var v=Ot(r,["hvcC"])[0],m=v[1],p=["","A","B","C"][m>>6],y=31&m,E=xt(v,2),T=(32&m)>>5?"H":"L",S=v[12],L=v.subarray(6,12);n+="."+p+y,n+="."+E.toString(16).toUpperCase(),n+="."+T+S;for(var A="",R=L.length;R--;){var b=L[R];(b||A)&&(A="."+b.toString(16).toUpperCase()+A)}n+=A;break;case"dvh1":case"dvhe":var k=Ot(r,["dvcC"])[0],D=k[2]>>1&127,I=k[2]<<5&32|k[3]>>3&31;n+="."+Ht(D)+"."+Ht(I);break;case"vp09":var w=Ot(r,["vpcC"])[0],C=w[4],_=w[5],x=w[6]>>4&15;n+="."+Ht(C)+"."+Ht(_)+"."+Ht(x);break;case"av01":var P=Ot(r,["av1C"])[0],F=P[1]>>>5,M=31&P[1],O=P[2]>>>7?"H":"M",N=(64&P[2])>>6,U=(32&P[2])>>5,B=2===F&&N?U?12:10:N?10:8,G=(16&P[2])>>4,K=(8&P[2])>>3,H=(4&P[2])>>2,V=3&P[2];n+="."+F+"."+Ht(M)+O+"."+Ht(B)+"."+G+"."+K+H+V+"."+Ht(1)+"."+Ht(1)+"."+Ht(1)+".0"}return{codec:n,encrypted:a}}function Gt(t,e){for(var r=e+5;128&t[e++]&&e>1&63;return 39===r||40===r}return 6==(31&e)}function Xt(t,e,r,i){var n=zt(t),a=0;a+=e;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,h=a;if(ou){w.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[h++]){var d=_t(n,h);if(h+=2,49===d){var c=xt(n,h);if(h+=4,1195456820===c){var f=n[h++];if(3===f){var g=n[h++],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[h++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var L=o-16,A=new Uint8Array(L),R=0;R0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>>24;if(0!==n&&1!==n)return{offset:r,size:e};var a=t.buffer,s=kt.hexDump(new Uint8Array(a,r+12,16)),o=t.getUint32(28),l=null,u=null;if(0===n){if(e-32>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case J:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case Q:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Qt(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=$t[this.uri];if(!f){var g=Object.keys($t).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),$t[this.uri]=f}this.keyId=f}return this},t}(),te=/\{\$([a-zA-Z0-9-_]+)\}/g;function ee(t){return te.test(t)}function re(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=ie(t,a))}}function ie(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(te,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function ne(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function ae(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function se(t){if(void 0===t&&(t=!0),"undefined"!=typeof self)return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}var oe={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function le(t,e,r){return void 0===r&&(r=!0),!t.split(",").some((function(t){return!ue(t,e,r)}))}function ue(t,e,r){var i;void 0===r&&(r=!0);var n=se(r);return null!=(i=null==n?void 0:n.isTypeSupported(he(t,e)))&&i}function he(t,e){return e+'/mp4;codecs="'+t+'"'}function de(t){if(t){var e=t.substring(0,4);return oe.video[e]}return 2}function ce(t){return t.split(",").reduce((function(t,e){var r=oe.video[e];return r?(2*r+t)/(t?3:2):(oe.audio[e]+t)/(t?2:1)}),0)}var fe={},ge=/flac|opus/i;function ve(t,e){return void 0===e&&(e=!0),t.replace(ge,(function(t){return function(t,e){if(void 0===e&&(e=!0),fe[t])return fe[t];for(var r={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t],i=0;i0&&a.length0&&X.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=X.optionalFloat("PART-HOLD-BACK",0),h.holdBack=X.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var z=new x(I);h.partTarget=z.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=h.partList;Q||(Q=h.partList=[]);var J=g>0?Q[Q.length-1]:void 0,$=g++,Z=new x(I);re(h,Z,["BYTERANGE","URI"]);var tt=new K(Z,E,e,$,J);Q.push(tt),E.duration+=tt.duration;break;case"PRELOAD-HINT":var et=new x(I);re(h,et,["URI"]),h.preloadHint=et;break;case"RENDITION-REPORT":var rt=new x(I);re(h,rt,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(rt);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(De(E,p),E.cc=m,h.fragmentHint=E,u&&we(E,u,h));var it=d.length,nt=d[0],at=d[it-1];if((v+=h.skippedSegments*h.targetduration)>0&&it&&at){h.averagetargetduration=v/it;var st=at.sn;h.endSN="initSegment"!==st?st:0,h.live||(at.endList=!0),nt&&(h.startCC=nt.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function Ae(t,e,r){var i,n,a=new x(t);re(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?Le.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new Zt(s,d,h,c,l)}function Re(t){var e=new x(t).decimalFloatingPoint("TIME-OFFSET");return y(e)?e:null}function be(t,e){var r=(t||"").split(/[ ,]+/).filter((function(t){return t}));["video","audio","text"].forEach((function(t){var i=r.filter((function(e){return function(t,e){var r=oe[e];return!!r&&!!r[t.slice(0,4)]}(e,t)}));i.length&&(e[t+"Codec"]=i.join(","),r=r.filter((function(t){return-1===i.indexOf(t)})))})),e.unknownCodecs=r}function ke(t,e,r){var i=e[r];i&&(t[r]=i)}function De(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),y(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Ie(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function we(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var Ce="manifest",_e="level",xe="audioTrack",Pe="subtitleTrack",Fe="main",Me="audio",Oe="subtitle";function Ne(t){switch(t.type){case xe:return Me;case Pe:return Oe;default:return Fe}}function Ue(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Be=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Ce,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.pathwayId,a=e.url,s=e.deliveryDirectives;this.load({id:r,level:i,pathwayId:n,responseType:"text",type:_e,url:a,deliveryDirectives:s})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:xe,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:Pe,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url&&l.level===t.level)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===Ce?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),y(null==(e=t.deliveryDirectives)?void 0:e.part)&&(t.type===_e&&null!==t.level?i=this.hls.levels[t.level].details:t.type===xe&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===Pe&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),Le.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=Ue(t,r),o=Le.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=Le.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new x({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=Ue(t,r),h=y(o)?o:y(s)?s:0,d=Ne(r),c=Le.parseLevelPlaylist(t.data,u,h,d,0,this.variableList);if(l===Ce){var f={attrs:new x({}),bitrate:0,details:c,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[f],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=c,this.handlePlaylistLoaded(c,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.MANIFEST_PARSING_ERROR,fatal:e.type===Ce,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===_e?s+=": "+t.level+" id: "+t.id:t.type!==xe&&t.type!==Pe||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=A.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case Ce:l=r?A.MANIFEST_LOAD_TIMEOUT:A.MANIFEST_LOAD_ERROR,u=!0;break;case _e:l=r?A.LEVEL_LOAD_TIMEOUT:A.LEVEL_LOAD_ERROR,u=!1;break;case xe:l=r?A.AUDIO_TRACK_LOAD_TIMEOUT:A.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case Pe:l=r?A.SUBTITLE_TRACK_LOAD_TIMEOUT:A.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=Ue(e,i),f=Ne(i),g="number"==typeof i.level&&f===Fe?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Ce:case _e:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case xe:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case Pe:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Ge(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Ke(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function He(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Ve(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sQe&&(d=Qe),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&(!t||e.startDate.05&&this.forwardBufferLength>1){var l=Math.min(2,Math.max(1,a)),u=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(l,Math.max(1,u))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},s(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-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 t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),tr=["NONE","TYPE-0","TYPE-1",null],er=["SDR","PQ","HLG"],rr="",ir="YES",nr="v2";function ar(t){var e=t.canSkipUntil,r=t.canSkipDateRanges,i=t.age;return e&&it.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function hr(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(y(l)){var h=Math.abs(l-r);y(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)ur(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||fr(e,i[r].start)}function fr(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(n)||!!r);return t.shouldRetry?t.shouldRetry(t,e,r,i,a):a}var Lr=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function Ar(t,e,r,i,n){void 0===r&&(r=0),void 0===i&&(i=0),void 0===n&&(n=.005);var a=null;if(t){a=e[t.sn-e[0].sn+1]||null;var s=t.endDTS-r;s>0&&s<15e-7&&(r+=15e-7)}else 0===r&&0===e[0].start&&(a=e[0]);if(a&&((!t||t.level===a.level)&&0===Rr(r,i,a)||function(t,e,r){if(e&&0===e.start&&e.level0){var i=e.tagList.reduce((function(t,e){return"INF"===e[0]&&(t+=parseFloat(e[1])),t}),r);return t.start<=i}return!1}(a,t,Math.min(n,i))))return a;var o=Lr(e,Rr.bind(null,r,i));return!o||o===t&&a?a:o}function Rr(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function br(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var kr=0,Dr=2,Ir=3,wr=5,Cr=0,_r=1,xr=2,Pr=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){},e.stopLoad=function(){this.playlistError=0},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===Fe?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case A.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:kr,flags:Cr});case A.FRAG_GAP:case A.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Dr);case A.LEVEL_EMPTY_ERROR:case A.LEVEL_PARSING_ERROR:var s,o,l=e.parent===Fe?e.level:n.loadLevel;return void(e.details===A.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case A.AUDIO_TRACK_LOAD_ERROR:case A.AUDIO_TRACK_LOAD_TIMEOUT:case A.SUBTITLE_LOAD_ERROR:case A.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===xe&&u.hasAudioGroup(a.groupId)||a.type===Pe&&u.hasSubtitleGroup(a.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=Dr,void(e.errorAction.flags=_r)}return;case A.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d?e.errorAction={action:Dr,flags:xr,hdcpLevel:d}:this.keySystemError(e));case A.BUFFER_ADD_CODEC_ERROR:case A.REMUX_ALLOC_ERROR:case A.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case A.INTERNAL_EXCEPTION:case A.BUFFER_APPENDING_ERROR:case A.BUFFER_FULL_ERROR:case A.LEVEL_SWITCH_ERROR:case A.BUFFER_STALLED_ERROR:case A.BUFFER_SEEK_OVER_HOLE:case A.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:kr,flags:Cr})}e.type===L.KEY_SYSTEM_ERROR&&this.keySystemError(e)}},e.keySystemError=function(t){var e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)},e.getPlaylistRetryOrSwitchAction=function(t,e){var r=yr(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(Sr(r,i,pr(t),t.response))return{action:wr,flags:Cr,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(t,e);return r&&(n.retryConfig=r,n.retryCount=i),n},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=yr(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i&&(t.details!==A.FRAG_GAP&&i.fragmentError++,Sr(o,l,pr(t),t.response)))return{action:wr,flags:Cr,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(t,r);return o&&(u.retryConfig=o,u.retryCount=l),u},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i){var n,a,s=t.details;i.loadError++,s===A.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,h=r.minAutoLevel,d=r.maxAutoLevel;r.autoLevelEnabled||(r.loadLevel=-1);for(var c,f=null==(n=t.frag)?void 0:n.type,g=(f===Me&&s===A.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR))&&l.some((function(t){var e=t.audioCodec;return i.audioCodec!==e})),v="video"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR)&&l.some((function(t){var e=t.codecSet,r=t.audioCodec;return i.codecSet!==e&&i.audioCodec===r})),m=null!=(a=t.context)?a:{},p=m.type,y=m.groupId,E=function(){var e=(T+u)%l.length;if(e!==u&&e>=h&&e<=d&&0===l[e].loadError){var r,n,a=l[e];if(s===A.FRAG_GAP&&f===Fe&&t.frag){var c=l[e].details;if(c){var m=Ar(t.frag,c.fragments,t.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===xe&&a.hasAudioGroup(y)||p===Pe&&a.hasSubtitleGroup(y))return 0;if(f===Me&&null!=(r=i.audioGroups)&&r.some((function(t){return a.hasAudioGroup(t)}))||f===Oe&&null!=(n=i.subtitleGroups)&&n.some((function(t){return a.hasSubtitleGroup(t)}))||g&&i.audioCodec===a.audioCodec||!g&&i.audioCodec!==a.audioCodec||v&&i.codecSet===a.codecSet)return 0}return o=e,1}},T=l.length;T--&&(0===(c=E())||1!==c););if(o>-1&&r.loadLevel!==o)return t.levelRetry=!0,this.playlistError=0,{action:Dr,flags:Cr,nextAutoLevel:o}}return{action:Dr,flags:_r}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case kr:break;case Dr:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===A.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn('MediaSource ended after "'+e.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case Cr:this.switchLevel(t,a);break;case xr:n&&(e.maxHdcpLevel=tr[tr.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},t}(),Fr=function(){function t(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=w.log.bind(w,e+":"),this.warn=w.warn.bind(w,e+":"),this.hls=t}var e=t.prototype;return e.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},e.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},e.startLoad=function(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()},e.stopLoad=function(){this.canLoad=!1,this.clearTimer()},e.switchParams=function(t,e,r){var i=null==e?void 0:e.renditionReports;if(i){for(var n=-1,a=0;a=0&&d>e.partTarget&&(h+=1)}var c=r&&ar(r);return new sr(u,h>=0?h:void 0,c)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&dr(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var E=Math.floor(y/n.targetduration);u+=E,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+E+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var T=this.hls.mainForwardBufferInfo,S=T?T.end-T.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=Er(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),Mr=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),Or=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Mr(t),this.fast_=new Mr(e),this.defaultTTFB_=i,this.ttfb_=new Mr(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new Mr(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Mr(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new Mr(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),Nr={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},Ur={};function Br(t,e,r){var n=t.videoCodec,a=t.audioCodec;if(!n||!a||!r)return Promise.resolve(Nr);var s={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},o=t.videoRange;"SDR"!==o&&(s.transferFunction=o.toLowerCase());var l=n.split(",").map((function(t){return{type:"media-source",video:i(i({},s),{},{contentType:he(t,"video")})}}));return a&&t.audioGroups&&t.audioGroups.forEach((function(t){var r;t&&(null==(r=e.groups[t])||r.tracks.forEach((function(e){if(e.groupId===t){var r=e.channels||"",i=parseFloat(r);y(i)&&i>2&&l.push.apply(l,a.split(",").map((function(t){return{type:"media-source",audio:{contentType:he(t,"audio"),channels:""+i}}})))}})))})),Promise.all(l.map((function(t){var e=function(t){var e=t.audio,r=t.video,i=r||e;if(i){var n=i.contentType.split('"')[1];if(r)return"r"+r.height+"x"+r.width+"f"+Math.ceil(r.framerate)+(r.transferFunction||"sd")+"_"+n+"_"+Math.ceil(r.bitrate/1e5);if(e)return"c"+e.channels+(e.spatialRendering?"s":"n")+"_"+n}return""}(t);return Ur[e]||(Ur[e]=r.decodingInfo(t))}))).then((function(t){return{supported:!t.some((function(t){return!t.supported})),configurations:l,decodingInfoResults:t}})).catch((function(t){return{supported:!1,configurations:l,decodingInfoResults:[],error:t}}))}function Gr(t,e){var r=!1,i=[];return t&&(r="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||er.slice(0),i=(r=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){var t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}())?i.filter((function(t){return"SDR"!==t})):["SDR"]),{preferHDR:r,allowedVideoRanges:i}}function Kr(t,e){w.log('[abr] start candidates with "'+t+'" ignored because '+e)}function Hr(t,e,r){if("attrs"in t){var i=e.indexOf(t);if(-1!==i)return i}for(var n=0;n-1,p=e.getBwEstimate(),E=i.levels,T=E[t.level],L=o.total||Math.max(o.loaded,Math.round(l*T.averageBitrate/8)),A=m?u-v:u;A<1&&m&&(A=Math.min(u,8*o.loaded/p));var R=m?1e3*o.loaded/A:0,b=R?(L-o.loaded)/R:8*L/p+c/1e3;if(!(b<=g)){var k,D=R?8*R:p,I=Number.POSITIVE_INFINITY;for(k=t.level-1;k>h;k--){var C=E[k].maxBitrate;if((I=e.getTimeToLoadFrag(c/1e3,D,l*C,!E[k].details))=b||I>10*l)){i.nextLoadLevel=i.nextAutoLevel=k,m?e.bwEstimator.sample(u-Math.min(c,v),o.loaded):e.bwEstimator.sampleTTFB(u);var _=E[k].maxBitrate;e.getBwEstimate()*e.hls.config.abrBandWidthUpFactor>_&&e.resetEstimator(_),e.clearTimer(),w.warn("[abr] Fragment "+t.sn+(r?" part "+r.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+g.toFixed(3)+" s\n Estimated load time for current fragment: "+b.toFixed(3)+" s\n Estimated load time for down switch fragment: "+I.toFixed(3)+" s\n TTFB estimate: "+(0|v)+" ms\n Current BW estimate: "+(y(p)?0|p:"Unknown")+" bps\n New BW estimate: "+(0|e.getBwEstimate())+" bps\n Switching to level "+k+" @ "+(0|_)+" bps"),i.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:r,stats:o})}}}}}}},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}var e=t.prototype;return e.resetEstimator=function(t){t&&(w.log("setting initial bwe to "+t),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},e.initEstimator=function(){var t=this.hls.config;return new Or(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(S.ERROR,this.onError,this))},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null},e.onManifestLoading=function(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},e.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},e.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.onError=function(t,e){if(!e.fatal)switch(e.details){case A.BUFFER_ADD_CODEC_ERROR:case A.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case A.FRAG_LOAD_TIMEOUT:var r=e.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)}}},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats.loading,n=i.end-i.start;y(n)&&(this.lastLevelLoadSec=n/1e3),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===Fe&&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(S.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.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}},e.ignoreFragment=function(t){return t.type!==Fe||"initSegment"===t.sn},e.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},e.getAutoLevelKey=function(){return this.getBwEstimate()+"_"+this.getStarvationDelay().toFixed(2)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=e?e.duration:t?t.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay(),u=n.abrBandWidthFactor,h=n.abrBandWidthUpFactor;if(l){var d=this.findBestLevel(o,a,i,l,0,u,h);if(d>=0)return d}var c=s?Math.min(s,n.maxStarvationDelay):n.maxStarvationDelay;if(!l){var f=this.bitrateTestDelay;f&&(c=(s?Math.min(s,n.maxLoadingDelay):n.maxLoadingDelay)-f,w.info("[abr] bitrate test took "+Math.round(1e3*f)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*c)+" ms"),u=h=1)}var g=this.findBestLevel(o,a,i,l,c,u,h);if(w.info("[abr] "+(l?"rebuffering expected":"buffer is empty")+", optimal quality level "+g),g>-1)return g;var v=r.levels[a],m=r.levels[r.loadLevel];return(null==v?void 0:v.bitrate)<(null==m?void 0:m.bitrate)?a:r.loadLevel},e.getStarvationDelay=function(){var t=this.hls,e=t.media;if(!e)return 1/0;var r=e&&0!==e.playbackRate?Math.abs(e.playbackRate):1,i=t.mainForwardBufferInfo;return(i?i.len:0)/r},e.getBwEstimate=function(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate},e.findBestLevel=function(t,e,r,i,n,a,s){var o,l=this,u=i+n,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,c=this.fragCurrent,f=this.partCurrent,g=this.hls,v=g.levels,m=g.allAudioTracks,p=g.loadLevel,E=g.config;if(1===v.length)return 0;var T,S=v[d],L=!(null==S||null==(o=S.details)||!o.live),A=-1===p||-1===h,R="SDR",b=(null==S?void 0:S.frameRate)||0,k=E.audioPreference,D=E.videoPreference,I=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce((function(t,e){var r=t.groups[e.groupId];r||(r=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),r.tracks.push(e);var i=e.channels||"2";return r.channels[i]=(r.channels[i]||0)+1,r.hasDefault=r.hasDefault||e.default,r.hasAutoSelect=r.hasAutoSelect||e.autoselect,r.hasDefault&&(t.hasDefaultAudio=!0),r.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(A){if(-1!==this.firstSelection)return this.firstSelection;var C=this.codecTiers||(this.codecTiers=function(t,e,r,i){return t.slice(r,i+1).reduce((function(t,r){if(!r.codecSet)return t;var i=r.audioGroups,n=t[r.codecSet];n||(t[r.codecSet]=n={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),n.minBitrate=Math.min(n.minBitrate,r.bitrate);var a=Math.min(r.height,r.width);return n.minHeight=Math.min(n.minHeight,a),n.minFramerate=Math.min(n.minFramerate,r.frameRate),n.maxScore=Math.max(n.maxScore,r.score),n.fragmentError+=r.fragmentError,n.videoRanges[r.videoRange]=(n.videoRanges[r.videoRange]||0)+1,i&&i.forEach((function(t){if(t){var r=e.groups[t];r&&(n.hasDefaultAudio=n.hasDefaultAudio||e.hasDefaultAudio?r.hasDefault:r.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(r.channels).forEach((function(t){n.channels[t]=(n.channels[t]||0)+r.channels[t]})))}})),t}),{})}(v,I,e,r)),_=function(t,e,r,i,n){for(var a=Object.keys(t),s=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=s&&2===parseInt(s),u=!0,h=!1,d=1/0,c=1/0,f=1/0,g=0,v=[],m=Gr(e,n),p=m.preferHDR,E=m.allowedVideoRanges,T=function(){var e=t[a[S]];u=e.channels[2]>0,d=Math.min(d,e.minHeight),c=Math.min(c,e.minFramerate),f=Math.min(f,e.minBitrate);var r=E.filter((function(t){return e.videoRanges[t]>0}));r.length>0&&(h=!0,v=r)},S=a.length;S--;)T();d=y(d)?d:0,c=y(c)?c:0;var L=Math.max(1080,d),A=Math.max(30,c);return f=y(f)?f:r,r=Math.max(f,r),h||(e=void 0,v=[]),{codecSet:a.reduce((function(e,i){var n=t[i];if(i===e)return e;if(n.minBitrate>r)return Kr(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),e;if(!n.hasDefaultAudio)return Kr(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Kr(i,'audio codec preference "'+o+'" not found'),e;if(s&&!l){if(!n.channels[s])return Kr(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),e}else if((!o||l)&&u&&0===n.channels[2])return Kr(i,"no renditions with stereo sound found"),e;return n.minHeight>L?(Kr(i,"min resolution of "+n.minHeight+" > maximum of "+L),e):n.minFramerate>A?(Kr(i,"min framerate of "+n.minFramerate+" > maximum of "+A),e):v.some((function(t){return n.videoRanges[t]>0}))?n.maxScore=ce(e)||n.fragmentError>t[e].fragmentError)?e:(g=n.maxScore,i):(Kr(i,"no variants with VIDEO-RANGE of "+JSON.stringify(v)+" found"),e)}),void 0),videoRanges:v,preferHDR:p,minFramerate:c,minBitrate:f}}(C,R,t,k,D),x=_.codecSet,P=_.videoRanges,F=_.minFramerate,M=_.minBitrate,O=_.preferHDR;T=x,R=O?P[P.length-1]:P[0],b=F,t=Math.max(t,M),w.log("[abr] picked start tier "+JSON.stringify(_))}else T=null==S?void 0:S.codecSet,R=null==S?void 0:S.videoRange;for(var N,U=f?f.duration:c?c.duration:0,B=this.bwEstimator.getEstimateTTFB()/1e3,G=[],K=function(){var e,o=v[H],c=H>d;if(!o)return 0;if(E.useMediaCapabilities&&!o.supportedResult&&!o.supportedPromise){var g=navigator.mediaCapabilities;"function"==typeof(null==g?void 0:g.decodingInfo)&&function(t,e,r,i,n,a){var s=t.audioCodec?t.audioGroups:null,o=null==a?void 0:a.audioCodec,l=null==a?void 0:a.channels,u=l?parseInt(l):o?1/0:2,h=null;if(null!=s&&s.length)try{h=1===s.length&&s[0]?e.groups[s[0]].channels:s.reduce((function(t,r){if(r){var i=e.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(e){t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==r||t.bitrate>Math.max(n,8e6))||!!h&&y(u)&&Object.keys(h).some((function(t){return parseInt(t)>u}))}(o,I,R,b,t,k)?(o.supportedPromise=Br(o,I,g),o.supportedPromise.then((function(t){if(l.hls){o.supportedResult=t;var e=l.hls.levels,r=e.indexOf(o);t.error?w.warn('[abr] MediaCapabilities decodingInfo error: "'+t.error+'" for level '+r+" "+JSON.stringify(t)):t.supported||(w.warn("[abr] Unsupported MediaCapabilities decodingInfo result for level "+r+" "+JSON.stringify(t)),r>-1&&e.length>1&&(w.log("[abr] Removing unsupported level "+r),l.hls.removeLevel(r)))}}))):o.supportedResult=Nr}if(T&&o.codecSet!==T||R&&o.videoRange!==R||c&&b>o.frameRate||!c&&b>0&&b=2*U&&0===n?v[H].averageBitrate:v[H].maxBitrate,x=l.getTimeToLoadFrag(B,m,_*C,void 0===D);if(m>=_&&(H===h||0===o.loadError&&0===o.fragmentError)&&(x<=B||!y(x)||L&&!l.bitrateTestDelay||x"+H+" adjustedbw("+Math.round(m)+")-bitrate="+Math.round(m-_)+" ttfb:"+B.toFixed(1)+" avgDuration:"+C.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+x.toFixed(1)+" firstSelection:"+A+" codecSet:"+T+" videoRange:"+R+" hls.loadLevel:"+p)),A&&(l.firstSelection=H),{v:H}}},H=r;H>=e;H--)if(0!==(N=K())&&N)return N.v;return-1},s(t,[{key:"firstAutoLevel",get:function(){var t=this.hls,e=t.maxAutoLevel,r=t.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,e,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),e);return w.warn("[abr] 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 t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;var i=e&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){var n=this.hls.levels;if(n.length>Math.max(t,i)&&n[t].loadError<=n[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(t){var e=this.hls,r=e.maxAutoLevel,i=e.minAutoLevel,n=Math.min(Math.max(t,i),r);this._nextAutoLevel!==n&&(this.nextAutoLevelKey="",this._nextAutoLevel=n)}}]),t}(),qr=function(){function t(){this._boundTick=void 0,this._tickTimer=null,this._tickInterval=null,this._tickCallCount=0,this._boundTick=this.tick.bind(this)}var e=t.prototype;return e.destroy=function(){this.onHandlerDestroying(),this.onHandlerDestroyed()},e.onHandlerDestroying=function(){this.clearNextTick(),this.clearInterval()},e.onHandlerDestroyed=function(){},e.hasInterval=function(){return!!this._tickInterval},e.hasNextTick=function(){return!!this._tickTimer},e.setInterval=function(t){return!this._tickInterval&&(this._tickCallCount=0,this._tickInterval=self.setInterval(this._boundTick,t),!0)},e.clearInterval=function(){return!!this._tickInterval&&(self.clearInterval(this._tickInterval),this._tickInterval=null,!0)},e.clearNextTick=function(){return!!this._tickTimer&&(self.clearTimeout(this._tickTimer),this._tickTimer=null,!0)},e.tick=function(){this._tickCallCount++,1===this._tickCallCount&&(this.doTick(),this._tickCallCount>1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Xr="NOT_LOADED",zr="APPENDING",Qr="PARTIAL",Jr="OK",$r=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=ti(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),Zr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=ti(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah){var c=Math.max(a,i.start(u)),f=Math.min(s,i.end(u));f>c&&(n.partial=!0,n.time.push({startPTS:c,endPTS:f}))}else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&Zr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||Zr(e))},e.getState=function(t){var e=ti(t),r=this.fragments[e];return r?r.buffered?Zr(r)?Qr:Jr:zr:Xr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=ti(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=ti(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,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.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=ti(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function Zr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function ti(t){return t.type+"_"+t.level+"_"+t.sn}var ei={length:0,start:function(){return 0},end:function(){return 0}},ri=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],E=c[y],T=257*c[m]^16843008*m;i[f]=T<<24|T>>>8,n[f]=T<<16|T>>>16,a[f]=T<<8|T>>>24,s[f]=T,T=16843009*E^65537*y^257*p^16843008*f,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,h[m]=T<<8|T>>>24,d[m]=T,f?(f=p^c[c[c[E^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&null!=a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(n){var a=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(e){r.state!==Ei&&r.state!==Ii&&(r.warn(e),r.resetFragmentLoading(t))}))},r.completeInitSegmentLoad=function(t){if(!this.levels)throw new Error("init load aborted, missing levels");var e=t.frag.stats;this.state=Ti,t.frag.data=new Uint8Array(t.payload),e.parsing.start=e.buffering.start=self.performance.now(),e.parsing.end=e.buffering.end=self.performance.now(),this.tick()},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.sn!==e.sn||t.level!==e.level},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===Fe?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?yi(ri.getBuffered(s)):"(detached)")+")"),"initSegment"!==t.sn){var o;if(t.type!==Oe){var l=t.elementaryStreams;if(!Object.keys(l).some((function(t){return!!l[t]})))return void(this.state=Ti)}var u=null==(o=this.levels)?void 0:o[t.level];null!=u&&u.fragmentError&&(this.log("Resetting level fragment error count of "+u.fragmentError+" on frag buffered"),u.fragmentError=0)}this.state=Ti,s&&(!this.loadedmetadata&&t.type==Fe&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new ii(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=Si,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===Si&&(a.state=Ti),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=Li,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),y(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=Li;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=gr(r,t.sn,o.index+1)||vr(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===A.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===bi){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===Ei||this.state===Ii||(this.state=Ti)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?gr(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return y(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=ri.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=i&&(r.maxMaxBufferLength=n,this.warn("Reduce max buffer length to "+n+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,Fe);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!y(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return Lr(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&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?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===Jr||f===Qr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(o?o.sn:"na")+" fragments: "+i),l}return n},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===A.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===wr&&d){this.resetStartWhenNotLoaded(this.levelLastLoaded);var c=Er(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+c+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+c,this.state=Ai}else if(d&&s){if(this.resetFragmentErrors(t),!(h.5;n&&this.reduceMaxBufferLength(i.len,(null==e?void 0:e.duration)||10);var a=!n;return a&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+r+" buffer"),e&&(this.fragmentTracker.removeFragment(e),this.nextLoadPosition=e.start),this.resetLoadingState(),a}return!1},r.resetFragmentErrors=function(t){t===Me&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==Ei&&(this.state=Ti)},r.afterBufferFlushed=function(t,e,r){if(t){var i=ri.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Di&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=Ti},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(!Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:hr(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1)&&null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=ki,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())},s(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(qr),xi=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:We,duration:Number.POSITIVE_INFINITY});n>>5}function Bi(t,e){return e+1=t.length)return!1;var i=Ui(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||Bi(t,n)}return!1}function Ki(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};var c=new Error("invalid ADTS sampling index:"+d);t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!0,error:c,reason:c.message})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function Hi(t){return 9216e4/t}function Vi(t,e,r,i,n){var a,s=i+n*Hi(t.samplerate),o=function(t,e){var r=Ni(t,e);if(e+r<=t.length){var i=Ui(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var Yi=null,Wi=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],ji=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],qi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Xi=[0,1,1,4];function zi(t,e,r,i,n){if(!(r+24>e.length)){var a=Qi(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Qi(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Wi[14*(3===r?3-i:3===i?3:4)+n-1],u=ji[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=qi[r][i],c=Xi[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===Yi){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Yi=v?parseInt(v[1]):0}return!!Yi&&Yi<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function Ji(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function $i(t,e){return e+18&&109===t[r+4]&&111===t[r+5]&&111===t[r+6]&&102===t[r+7])return!0;r=i>1?r+i:e}return!1}(t)},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Wt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=Ot(t,["moof"]);if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=lt(t,0,i.byteOffset-8),e.remainder=lt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=jt(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=jt(t,e),{videoTrack:e,audioTrack:Pi(),id3Track:i,textTrack:Pi()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=Ot(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==Ct(t.subarray(u,u+1));)r+=Ct(t.subarray(u,u+1)),u+=1;for(r+=Ct(t.subarray(u,u+1)),u+=1;"\0"!==Ct(t.subarray(u,u+1));)i+=Ct(t.subarray(u,u+1)),u+=1;i+=Ct(t.subarray(u,u+1)),u+=1,n=xt(t,12),a=xt(t,16),o=xt(t,20),l=xt(t,24),u=28}else if(1===e){n=xt(t,u+=4);var h=xt(t,u+=4),d=xt(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,E(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=xt(t,u),l=xt(t,u+=4),u+=4;"\0"!==Ct(t.subarray(u,u+1));)r+=Ct(t.subarray(u,u+1)),u+=1;for(r+=Ct(t.subarray(u,u+1)),u+=1;"\0"!==Ct(t.subarray(u,u+1));)i+=Ct(t.subarray(u,u+1)),u+=1;i+=Ct(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(en.test(i.schemeIdUri)){var n=y(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:qe,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),nn=function(t,e){var r=0,i=5;e+=i;for(var n=new Uint32Array(1),a=new Uint32Array(1),s=new Uint8Array(1);i>0;){s[0]=t[e];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<e.length)return-1;if(11!==e[r]||119!==e[r+1])return-1;var a=e[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&e[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>e.length)return-1;var u=e[r+6]>>5,h=0;2===u?h+=2:(1&u&&1!==u&&(h+=2),4&u&&(h+=2));var d=(e[r+6]<<8|e[r+7])>>12-h&1,c=[2,1,2,3,3,4,4,5][u]+d,f=e[r+5]>>3,g=7&e[r+5],v=new Uint8Array([a<<6|f<<1|g>>2,(3&g)<<6|u<<3|d<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=e.subarray(r,r+l);return t.config=v,t.channelCount=c,t.samplerate=s,t.samples.push({unit:p,pts:m}),l}var on=function(){function t(){this.VideoSample=null}var e=t.prototype;return e.createVideoSample=function(t,e,r,i){return{key:t,frame:!1,pts:e,dts:r,units:[],debug:i,length:0}},e.getLastNalUnit=function(t){var e,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.pushAccessUnit=function(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)},t}(),ln=function(){function t(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}var e=t.prototype;return e.loadWord=function(){var t=this.data,e=this.bytesAvailable,r=t.byteLength-e,i=new Uint8Array(4),n=Math.min(4,e);if(0===n)throw new Error("no bytes available");i.set(t.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},e.skipBits=function(t){var e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i4){var f=new ln(c).readSliceType();2!==f&&4!==f&&7!==f&&9!==f||(h=!0)}h&&null!=(d=l)&&d.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.frame=!0,l.key=h;break;case 5:a=!0,null!=(o=l)&&o.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.key=!0,l.frame=!0;break;case 6:a=!0,Xt(i.data,1,r.pts,e.samples);break;case 7:var g,v;a=!0,u=!0;var m=i.data,p=new ln(m).readSPS();if(!t.sps||t.width!==p.width||t.height!==p.height||(null==(g=t.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=t.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){t.width=p.width,t.height=p.height,t.pixelRatio=p.pixelRatio,t.sps=[m],t.duration=n;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}t.codec=E}break;case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,l&&s.pushAccessUnit(l,t),l=s.VideoSample=s.createVideoSample(!1,r.pts,r.dts,"");break;case 12:a=!0;break;default:a=!1,l&&(l.debug+="unknown NAL "+i.type+" ")}l&&a&&l.units.push(i)})),i&&l&&(this.pushAccessUnit(l,t),this.VideoSample=null)},r.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,i),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),i>0&&(c.data=Wt(c.data,e.subarray(0,i)),c.state=0))}l=0&&a>=0){var f={data:e.subarray(u,n),type:h,state:a};o.push(f)}if(0===o.length){var g=this.getLastNalUnit(t.samples);g&&(g.data=Wt(g.data,e))}return t.naluState=a,o},e}(on),hn=function(){function t(t,e,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new pi(e,{removePKCS7Padding:!1})}var e=t.prototype;return e.decryptBuffer=function(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)},e.decryptAacSample=function(t,e,r){var i=this,n=t[e].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(t,e+1,r)}))}},e.decryptAacSamples=function(t,e,r){for(;;e++){if(e>=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),dn=188,cn=function(){function t(t,e,r){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._duration=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=t,this.config=e,this.typeSupported=r,this.videoParser=new un}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,e-dn)+1,i=0;i1&&(0===a&&s>2||o+dn>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:wt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._videoTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},e.demux=function(e,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,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=e.length;if(this.remainderData&&(E=(e=Wt(this.remainderData,e)).length,this.remainderData=null),E>4>1){if((b=L+5+e[L+4])===L+dn)continue}else b=L+4;switch(R){case h:A&&(d&&(a=yn(d))&&this.videoParser.parseAVCPES(s,u,a,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(e.subarray(b,L+dn)),d.size+=L+dn-b);break;case c:if(A){if(g&&(a=yn(g)))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(e.subarray(b,L+dn)),g.size+=L+dn-b);break;case f:A&&(v&&(a=yn(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(b,L+dn)),v.size+=L+dn-b);break;case 0:A&&(b+=e[b]+1),y=this._pmtId=gn(e,b);break;case y:A&&(b+=e[b]+1);var k=vn(e,b,this.typeSupported,i,this.observer);(h=k.videoPid)>0&&(s.pid=h,s.segmentCodec=k.segmentVideoCodec),(c=k.audioPid)>0&&(o.pid=c,o.segmentCodec=k.segmentAudioCodec),(f=k.id3Pid)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+L+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,L=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=R}}else S++;S>0&&mn(this.observer,new Error("Found "+S+" TS packet/s that do not start with 0x47")),s.pesData=d,o.pesData=g,l.pesData=v;var D={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(D),D},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=yn(s))?(this.videoParser.parseAVCPES(i,a,e,!0,this._duration),i.pesData=null):i.pesData=s,o&&(e=yn(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e);break;case"ac3":this.parseAC3PES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=yn(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new hn(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Wt(s.sample.unit,o);else{var h=u-l;s.sample.unit.set(o.subarray(0,l),h),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else w.warn("[tsdemuxer]: AC3 PES unknown PTS")},e.parseID3PES=function(t,e){if(void 0!==e.pts){var r=o({},e,{type:this._videoTrack?qe:We,duration:Number.POSITIVE_INFINITY});t.samples.push(r)}else w.warn("[tsdemuxer]: ID3 PES unknown PTS")},t}();function fn(t,e){return((31&t[e+1])<<8)+t[e+2]}function gn(t,e){return(31&t[e+10])<<8|t[e+11]}function vn(t,e,r,i,n){var a={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},s=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0)for(var u=e+5,h=l;h>2;){106===t[u]&&(!0!==r.ac3?w.log("AC-3 audio found, not supported in this browser for now"):(a.audioPid=o,a.segmentAudioCodec="ac3"));var d=t[u+1]+2;u+=d,h-=d}break;case 194:case 135:return mn(n,new Error("Unsupported EC-3 in M2TS found")),a;case 36:return mn(n,new Error("Unsupported HEVC in M2TS found")),a}e+=l+5}return a}function mn(t,e,r){w.warn("parsing error: "+e.message),t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,levelRetry:r,error:e,reason:e.message})}function pn(t){w.log(t+" with AES-128-CBC encryption found in unencrypted stream")}function yn(t){var e,r,i,n,a,s=0,o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Wt(o[0],o[1]),o.splice(1,1);if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var l=e[7];192&l&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var u=(i=e[8])+9;if(t.size<=u)return null;t.size-=u;for(var h=new Uint8Array(t.size),d=0,c=o.length;df){u-=f;continue}e=e.subarray(u),f-=u,u=0}h.set(e,s),s+=f}return r&&(r-=i+3),{data:h,pts:n,dts:a,len:r}}return null}var En=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,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}},e.probe=function(t){if(!t)return!1;var e=ct(t,0),r=(null==e?void 0:e.length)||0;if(e&&11===t[r]&&119===t[r+1]&&void 0!==vt(e)&&nn(t,r)<=16)return!1;for(var i=t.length;r1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(Sn+1)),n=Math.floor(r%(Sn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,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 t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.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=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.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,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.audioStsd=function(t){var e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])},t.mp4a=function(e){return t.box(t.types.mp4a,t.audioStsd(e),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){return t.box(t.types[".mp3"],t.audioStsd(e))},t.ac3=function(e){return t.box(t.types["ac-3"],t.audioStsd(e),t.box(t.types.dac3,e.config))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):"ac3"===e.segmentCodec?t.box(t.types.stsd,t.STSD,t.ac3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(Sn+1)),o=Math.floor(i%(Sn+1));return t.box(t.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]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(Sn+1)),s=Math.floor(r%(Sn+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.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])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.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]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,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 t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e);return Wt(t.FTYP,r)},t}();Ln.types=void 0,Ln.HDLR_TYPES=void 0,Ln.STTS=void 0,Ln.STSC=void 0,Ln.STCO=void 0,Ln.STSZ=void 0,Ln.VMHD=void 0,Ln.SMHD=void 0,Ln.STSD=void 0,Ln.FTYP=void 0,Ln.DINF=void 0;var An=9e4;function Rn(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function bn(t,e){return void 0===e&&(e=!1),Rn(t,1e3,1/An,e)}var kn=null,Dn=null,In=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===kn){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);kn=n?parseInt(n[1]):0}if(null===Dn){var a=navigator.userAgent.match(/Safari\/(\d+)/i);Dn=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,wn(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,L,A,R,b=this.videoTrackConfig;!b||e.width===b.width&&e.height===b.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=b.pixelRatio)?void 0:L[0])&&(null==(A=e.pixelRatio)?void 0:A[1])===(null==(R=b.pixelRatio)?void 0:R[1])||this.resetInitSegment()}else h=this.generateIS(t,e,n,a);var k,D=this.isVideoContiguous,I=-1;if(T&&(I=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+I+" out of "+y+" video samples due to a missing keyframe");var C=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(I),e.dropped+=I,k=v+=(e.samples[0].pts-C)/e.inputTimeScale}else-1===I&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(E&&T){var _=this.getVideoStartPts(e.samples),x=(wn(t.samples[0].pts,_)-_)/e.inputTimeScale;g+=Math.max(0,x),v+=Math.max(0,-x)}if(E){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||T||o===Me?v:void 0),T){var P=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,D,P)}}else T&&(l=this.remuxVideo(e,v,D,0));l&&(l.firstKeyFrame=I,l.independent=-1!==I,l.firstKeyFramePTS=k)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Cn(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=_n(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):Ln.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))}if(e.sps&&e.pps&&l.length){if(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:Ln.initSegment([e]),metadata:{width:e.width,height:e.height}},c)if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,wn(l[0].dts,g)-v),n=Math.min(n,g-v)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;if(!r||null===c){var y=e*s,E=l[0].pts-wn(l[0].dts,l[0].pts);kn&&null!==c&&Math.abs(y-E-c)<15e3?r=!0:c=y-E}for(var T=d.baseTime*s/d.timescale,R=0;R0?R-1:R].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var k=(a=l[l.length-1].dts)-n,D=k?Math.round(k/(h-1)):g||t.inputTimeScale/30;if(r){var I=n-c,C=I>D,_=I<-1;if((C||_)&&(C?w.warn("AVC: "+bn(I,!0)+" ms ("+I+"dts) hole between fragments detected at "+e.toFixed(3)):w.warn("AVC: "+bn(-I,!0)+" ms ("+I+"dts) overlapping between fragments detected at "+e.toFixed(3)),!_||c>=l[0].pts||kn)){n=c;var x=l[0].pts-I;if(C)l[0].dts=n,l[0].pts=x;else for(var P=0;Px);P++)l[P].dts-=I,l[P].pts-=I;w.log("Video: Initial PTS/DTS adjusted: "+bn(x,!0)+"/"+bn(n,!0)+", delta: "+bn(I,!0)+" ms")}}for(var F=0,M=0,O=n=Math.max(0,n),N=0;N0?$.dts-l[J-1].dts:D;if(st=J>0?$.pts-l[J-1].pts:D,ot.stretchShortVideoTrack&&null!==this.nextAudioPts){var ut=Math.floor(ot.maxBufferHole*s),ht=(i?v+i*s:this.nextAudioPts)-$.pts;ht>ut?((g=ht-lt)<0?g=lt:j=!0,w.log("[mp4-remuxer]: It is approximately "+ht/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=lt}else g=lt}var dt=Math.round($.pts-$.dts);q=Math.min(q,g),z=Math.max(z,g),X=Math.min(X,st),Q=Math.max(Q,st),u.push(new Pn($.key,g,tt,dt))}if(u.length)if(kn){if(kn<70){var ct=u[0].flags;ct.dependsOn=2,ct.isNonSync=0}}else if(Dn&&Q-X0&&(i&&Math.abs(p-m)<9e3||Math.abs(wn(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=wn(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var E=this.config.maxAudioFramesDrift,T=0,R=m;T=E*u&&I<1e4&&f){var C=Math.round(D/u);(R=k-C*u)<0&&(C--,R+=u),0===T&&(this.nextAudioPts=m=R),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(R/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:A.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(Ln.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new Pn(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):Ln.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=Tn.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Cn(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=Ot(e,["encv"])),i.forEach((function(t){Ot(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=Vt(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+kt.hexDump(i)+" -> "+kt.hexDump(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Ut(t);i.audio&&(e=Mn(i.audio,O)),i.video&&(r=Mn(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};y(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=Ot(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,E=p+g;!function(t,e,r){Ot(e,["moof","traf"]).forEach((function(e){Ot(e,["tfhd"]).forEach((function(i){var n=xt(i,4),a=t[n];if(a){var s=a.timescale||9e4;Ot(e,["tfdt"]).forEach((function(t){var e=t[0],i=r*s;if(i){var n=xt(t,4);if(0===e)n-=i,Mt(t,4,n=Math.max(n,0));else{n*=Math.pow(2,32),n+=xt(t,8),n-=i,n=Math.max(n,0);var a=Math.floor(n/(Dt+1)),o=Math.floor(n%(Dt+1));Mt(t,4,a),Mt(t,8,o)}}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=E:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var A={data1:d,startPTS:p,startDTS:p,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===A.type?A:void 0,h.video="audio"!==A.type?A:void 0,h.initSegment=c,h.id3=Cn(r,n,l,l),i.samples.length&&(h.text=_n(i,n,l)),h},t}();function Mn(t,e){var r=null==t?void 0:t.codec;if(r&&r.length>4)return r;if(e===O){if("ec-3"===r||"ac-3"===r||"alac"===r)return r;if("fLaC"===r||"Opus"===r)return ve(r,!1);var i="mp4a.40.5";return w.info('Parsed audio codec "'+r+'" or audio object type not handled. Using "'+i+'"'),i}return w.warn('Unhandled video codec "'+r+'"'),"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1.42e01e"}try{xn=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),xn=null==j?void 0:j.Date.now}var On=[{demux:rn,remux:Fn},{demux:cn,remux:In},{demux:tn,remux:In},{demux:En,remux:In}];On.splice(2,0,{demux:an,remux:In});var Nn=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=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=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=xn();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,R=function(t,e){var r=null;return t.byteLength>0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(R&&"AES-128"===R.method){var b=this.getDecrypter();if(!b.isSync())return this.decryptionPromise=b.webCryptoDecrypt(s,R.key.buffer,R.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var k=b.softwareDecrypt(s,R.key.buffer,R.iv.buffer);if(r.part>-1&&(k=b.flush()),!k)return a.executeEnd=xn(),Un(r);s=new Uint8Array(k)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=xn(),Un(r)}(d||c||v||D)&&this.resetInitSegment(T,m,p,E,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,R,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=xn(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=xn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=xn(),[Un(t)];var d=u.flush(o);return Bn(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=xn()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!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,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=On.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===E||0===E&&(1===T||S&&T<=0)),A=self.performance.now();(y||E||0===n.stats.parsing.start)&&(n.stats.parsing.start=A),!a||!T&&L||(a.stats.parsing.start=A);var R=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),b=new Kn(p,L,o,y,g,R);if(!L||p||R){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+R);var k=new Gn(r,i,e,s,u);this.configureTransmuxer(k)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:b},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,b);Bn(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);Bn(i)||r.async?(Bn(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,chunkMeta:e,frag:this.frag||void 0,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data;if(null!=e&&e.event){var r=this.hls;if(this.hls)switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}}else w.warn("worker message received with no "+(e?"event name":"data"))},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}();function Xn(t,e){if(t.length!==e.length)return!1;for(var r=0;r0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=Ti):(this.loadedmetadata=!1,this.state=Ri),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case Ti:this.doTickIdle();break;case Ri:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=wi}break;case Ai:var a,s=performance.now(),o=this.retryDate;if(!o||s>=o||null!=(a=this.media)&&a.seeking){var l=this.levels,u=this.trackId;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==l?void 0:l[u])||null),this.state=Ti}break;case wi:var h=this.waitingData;if(h){var d=h.frag,c=h.part,f=h.cache,g=h.complete;if(void 0!==this.initPTS[d.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=Li;var v={frag:d,part:c,payload:f.flush(),networkDetails:null};this._handleFragmentLoadProgress(v),g&&t.prototype._handleFragmentLoadComplete.call(this,v)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+d.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var m=this.getLoadPosition(),p=ri.bufferInfo(this.mediaBuffer,m,this.config.maxBufferHole);Rr(p.end,this.config.maxFragLookUpTolerance,d)<0&&(this.log("Waiting fragment cc ("+d.cc+") @ "+d.start+" cancelled because another fragment at "+p.end+" is needed"),this.clearWaitingFragment())}}else this.state=Ti}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=Ti)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if((r||!this.startFragRequested&&n.startFragPrefetch)&&null!=e&&e[i]){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==a||this.waitForCdnTuneIn(s))this.state=Ri;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,Me));var l=this.getFwdBufferInfo(o,Me);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Di);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,Fe),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len),g=s.fragments,v=g[0].start,m=this.flushing?this.getLoadPosition():l.end;if(h&&r){var p=this.getLoadPosition();u&&!zn(h.attrs,u.attrs)&&(m=p),s.PTSKnown&&pv||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=v+.05)}if(!(c>=f&&!h&&md.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var L=this.getAppendedFrag(y.start,Fe);if(null===L)return;if(E||(E=!!L.gap||!!T&&0===d.len),T&&!E||E&&l.nextStart&&l.nextStart-1)n=a[o];else{var l=Hr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(S.AUDIO_TRACKS_UPDATED,h);var d=this.trackId;if(-1!==u&&-1===d)this.setAudioTrack(u);else if(a.length&&-1===d){var c,f=new Error("No audio track selected for current audio group-ID(s): "+(null==(c=this.groupIds)?void 0:c.join(","))+" track count: "+a.length);this.warn(f.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:f})}}else this.shouldReloadPlaylist(n)&&this.setAudioTrack(this.trackId)}},r.onError=function(t,e){!e.fatal&&e.context&&(e.context.type!==xe||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))},r.setAudioOption=function(t){var e=this.hls;if(e.config.audioPreference=t,t){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&&Vr(t,i,Yr))return i;var n=Hr(t,this.tracksInGroup,Yr);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=e.loadLevel;-1===s&&(s=e.firstAutoLevel);var o=function(t,e,r,i,n){var a=e[i],s=e.reduce((function(t,e,r){var i=e.uri;return(t[i]||(t[i]=[])).push(r),t}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),h=Wr(e,i,(function(e){if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==u)return!1;var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Hr(t,a,n)>-1}));return h>-1?h:Wr(e,i,(function(e){var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Hr(t,a,n)>-1}))}(t,e.levels,r,s,Yr);if(-1===o)return null;e.nextLoadLevel=o}if(t.channels||t.audioCodec){var l=Hr(t,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(t){var e=this.tracksInGroup;if(t<0||t>=e.length)this.warn("Invalid audio track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,n=e[t],a=n.details&&!n.details.live;if(!(t===this.trackId&&n===r&&a||(this.log("Switching to audio-track "+t+' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId+" channels:"+n.channels),this.trackId=t,this.currentTrack=n,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),a))){var s=this.switchParams(n.url,null==r?void 0:r.details,n.details);this.loadPlaylist(s)}}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=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)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=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){var u=this.mainDetails;if(a.deltaUpdateFailed||!u)return;var h,d=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details))&&d&&fr(a,l=d.start):a.hasProgramDateTime&&u.hasProgramDateTime?(li(a,u),l=a.fragments[0].start):d&&fr(a,l=d.start)}o.details=a,this.levelLastLoaded=o,s===i&&(this.startFragRequested||!this.mainDetails&&a.live||this.setStartPosition(this.mainDetails||a,l),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===Ti&&(Ar(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(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=Ti}))}},r.doTick=function(){if(this.media){if(this.state===Ti){var t=this.currentTrackId,e=this.levels,r=null==e?void 0:e[t];if(!r||!e.length||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=ri.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,Fe),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=Ar(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},ea=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupIds=null,r.tracksInGroup=[],r.trackId=-1,r.currentTrack=null,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.onTextTracksChanged=function(){if(r.useTextTrackPolling||self.clearInterval(r.subtitlePollingInterval),r.media&&r.hls.config.renderTextTracksNatively){for(var t=null,e=Ye(r.media.textTracks),i=0;i-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(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),Ye(this.media.textTracks).forEach((function(t){He(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.groupId,n=e.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=e.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,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(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(e){var r=e.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(t){return-1===(null==i?void 0:i.indexOf(t))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(t){return!r||-1!==r.indexOf(t.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(t){return t.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(t,e){t.id=e}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=Hr(s,a);if(o>-1)n=a[o];else{var l=Hr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,h),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}else this.shouldReloadPlaylist(n)&&this.setSubtitleTrack(this.trackId)}},r.findTrackId=function(t){for(var e=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=Hr(t,e);if(a>-1)return e[a]}}return null},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentTrack;if(this.shouldLoadPlaylist(r)&&r){var i=r.id,n=r.groupId,a=r.url;if(e)try{a=e.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Loading subtitle playlist for id "+i),this.hls.trigger(S.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:e||null})}},r.toggleTrackModes=function(){var t=this.media;if(t){var e,r=Ye(t.textTracks),i=this.currentTrack;if(i&&((e=r.filter((function(t){return Qn(i,t)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(t){"disabled"!==t.mode&&t!==e&&(t.mode="disabled")})),e){var n=this.subtitleDisplay?"showing":"hidden";e.mode!==n&&(e.mode=n)}}},r.setSubtitleTrack=function(t){var e=this.tracksInGroup;if(this.media)if(t<-1||t>=e.length||!y(t))this.warn("Invalid subtitle track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(t!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+t+(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,h=i.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(i.url,null==r?void 0:r.details,i.details);this.loadPlaylist(d)}}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}else this.queuedDefaultTrack=t},s(e,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(t){this._subtitleDisplay=t,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(t){this.selectDefaultTrack=!1,this.setSubtitleTrack(t)}}]),e}(Fr),ra=function(){function t(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}var e=t.prototype;return e.append=function(t,e,r){var i=this.queues[e];i.push(t),1!==i.length||r||this.executeNext(e)},e.insertAbort=function(t,e){this.queues[e].unshift(t),this.executeNext(e)},e.appendBlocker=function(t){var e,r=new Promise((function(t){e=t})),i={execute:e,onStart:function(){},onComplete:function(){},onError:function(){}};return this.append(i,t),r},e.executeNext=function(t){var e=this.queues[t];if(e.length){var r=e[0];try{r.execute()}catch(e){w.warn('[buffer-operation-queue]: Exception executing "'+t+'" SourceBuffer operation: '+e),r.onError(e);var i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}},e.shiftAndExecuteNext=function(t){this.queues[t].shift(),this.executeNext(t)},e.current=function(t){return this.queues[t][0]},t}(),ia=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,na=function(){function t(t){var e=this;this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=function(t){e.hls&&e.hls.pauseBuffering()},this._onStartStreaming=function(t){e.hls&&e.hls.resumeBuffering()},this._onMediaSourceOpen=function(){var t=e.media,r=e.mediaSource;e.log("Media source opened"),t&&(t.removeEventListener("emptied",e._onMediaEmptied),e.updateMediaElementDuration(),e.hls.trigger(S.MEDIA_ATTACHED,{media:t,mediaSource:r})),r&&r.removeEventListener("sourceopen",e._onMediaSourceOpen),e.checkPendingTracks()},this._onMediaSourceClose=function(){e.log("Media source closed")},this._onMediaSourceEnded=function(){e.log("Media source ended")},this._onMediaEmptied=function(){var t=e.mediaSrc,r=e._objectUrl;t!==r&&w.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},this.hls=t;var r,i="[buffer-controller]";this.appendSource=(r=se(t.config.preferManagedMediaSource),"undefined"!=typeof self&&r===self.ManagedMediaSource),this.log=w.log.bind(w,i),this.warn=w.warn.bind(w,i),this.error=w.error.bind(w,i),this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new ra(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media,i=se(this.appendSource);if(r&&i){var n,a=this.mediaSource=new i;this.log("created media source: "+(null==(n=a.constructor)?void 0:n.name)),a.addEventListener("sourceopen",this._onMediaSourceOpen),a.addEventListener("sourceended",this._onMediaSourceEnded),a.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(a.addEventListener("startstreaming",this._onStartStreaming),a.addEventListener("endstreaming",this._onEndStreaming));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,aa(r),function(t,e){var r=self.document.createElement("source");r.type="video/mp4",r.src=e,t.appendChild(r)}(r,s),r.load()}catch(t){r.src=s}else r.src=s;r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn("onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming)),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),this.mediaSrc===r?(t.removeAttribute("src"),this.appendSource&&aa(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){t.resetBuffer(e)})),this._initSourceBuffer()},e.resetBuffer=function(t){var e=this.sourceBuffer[t];try{var r;e&&(this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(r=this.mediaSource)&&r.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e))}catch(e){this.warn("onBufferReset "+t,e)}},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length,n=Object.keys(e);if(n.forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a,s=e[t],o=s.id,l=s.codec,u=s.levelCodec,h=s.container,d=s.metadata,c=me(n.codec,n.levelCodec),f=null==c?void 0:c.replace(ia,"$1"),g=me(l,u),v=null==(a=g)?void 0:a.replace(ia,"$1");if(g&&f!==v){"audio"===t.slice(0,5)&&(g=ve(g,r.appendSource));var m=h+";codecs="+g;r.appendChangeType(t,m),r.log("switching codec "+c+" to "+g),r.tracks[t]={buffer:n.buffer,codec:l,container:h,levelCodec:u,metadata:d,id:o}}}}else r.pendingTracks[t]=e[t]})),!i){var a=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==a&&(this.log(a+" bufferCodec event(s) expected "+n.join(",")),this.bufferCodecEventsExpected=a),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(r.log("changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t,!!this.pendingTracks[t])},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(r.log("Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=ri.getBuffered(e[n]);r.appendErrors[o]=0,"audio"===o||"video"===o?r.appendErrors.audiovideo=0:(r.appendErrors.audio=0,r.appendErrors.video=0),r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){var e={type:L.MEDIA_ERROR,parent:l.type,details:A.BUFFER_APPEND_ERROR,sourceBufferName:o,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=A.BUFFER_FULL_ERROR;else{var n=++r.appendErrors[o];e.details=A.BUFFER_APPEND_ERROR,r.warn("Failed "+n+"/"+i.config.appendErrorMaxRetry+' times to append segment in "'+o+'" sourceBuffer'),n>=i.config.appendErrorMaxRetry&&(e.fatal=!0)}i.trigger(S.ERROR,e)}};n.append(y,o,!!this.pendingTracks[o])},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){r.warn("Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&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 t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.trimBuffers()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,r.log(i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(r.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&r.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.trimBuffers=function(){var t=this.hls,e=this.details,r=this.media;if(r&&null!==e&&this.getSourceBufferTypes().length){var i=t.config,n=r.currentTime,a=e.levelTargetDuration,s=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(y(s)&&s>0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}if(y(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){var u=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),h=Math.max(u,a),d=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,d)}}},e.flushBackBuffer=function(t,e,r){var i=this,n=this.details,a=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(s){var o=a[s];if(o){var l=ri.getBuffered(o);if(l.length>0&&r>l.start(0)){if(i.hls.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:r}),null!=n&&n.live)i.hls.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(o.ended&&l.end(l.length-1)-t<2*e)return void i.log("Cannot flush "+s+" back buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:s})}}}))},e.flushFrontBuffer=function(t,e,r){var i=this,n=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(a){var s=n[a];if(s){var o=ri.getBuffered(s),l=o.length;if(l<2)return;var u=o.start(l-1),h=o.end(l-1);if(r>u||t>=u&&t<=h)return;if(s.ended&&t-h<2*e)return void i.log("Cannot flush "+a+" front buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:u,endOffset:1/0,type:a})}}))},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=y(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!y(a))&&(this.log("Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);this.log("Media Source duration is set to "+e.duration+". Setting seekable range to "+i+"-"+n+"."),e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&(!t||2===i||"audiovideo"in r)){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this,r=this.sourceBuffer,i=this.mediaSource;if(!i)throw Error("createSourceBuffers called when mediaSource was null");var n=function(n){if(!r[n]){var a,s=t[n];if(!s)throw Error("source buffer exists for track "+n+", however track does not");var o=-1===(null==(a=s.levelCodec)?void 0:a.indexOf(","))?s.levelCodec:s.codec;o&&"audio"===n.slice(0,5)&&(o=ve(o,e.appendSource));var l=s.container+";codecs="+o;e.log("creating sourceBuffer("+l+")");try{var u=r[n]=i.addSourceBuffer(l),h=n;e.addBufferListener(h,"updatestart",e._onSBUpdateStart),e.addBufferListener(h,"updateend",e._onSBUpdateEnd),e.addBufferListener(h,"error",e._onSBUpdateError),e.appendSource&&e.addBufferListener(h,"bufferedchange",(function(t,r){var i=r.removedRanges;null!=i&&i.length&&e.hls.trigger(S.BUFFER_FLUSHED,{type:n})})),e.tracks[n]={buffer:u,codec:o,container:s.container,levelCodec:s.levelCodec,metadata:s.metadata,id:s.id}}catch(t){e.error("error while trying to add sourceBuffer: "+t.message),e.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:n,mimeType:l})}}};for(var a in t)n(a)},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e;if("closed"!==(null==(e=this.mediaSource)?void 0:e.readyState)){var r=this.operationQueue;r.current(t).onComplete(),r.shiftAndExecuteNext(t)}else this.resetBuffer(t)},e._onSBUpdateError=function(t,e){var r,i=new Error(t+" SourceBuffer error. MediaSource readyState: "+(null==(r=this.mediaSource)?void 0:r.readyState));this.error(""+i,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});var n=this.operationQueue.current(t);n&&n.onError(i)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return this.warn("Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=y(i.duration)?i.duration:1/0,l=y(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&(!s.ending||s.ended)?(s.ended=!1,this.log("Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.sourceBuffer[e];if(r)r.ended=!1,r.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error("Attempting to append to the "+e+" SourceBuffer, but it does not exist")},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},s(t,[{key:"mediaSrc",get:function(){var t,e,r=(null==(t=this.media)||null==(e=t.querySelector)?void 0:e.call(t,"source"))||this.media;return null==r?void 0:r.src}}]),t}();function aa(t){var e=t.querySelectorAll("source");[].slice.call(e).forEach((function(e){t.removeChild(e)}))}var sa={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},oa=function(t){return String.fromCharCode(sa[t]||t)},la=15,ua=100,ha={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},da={17:2,18:4,21:6,22:8,23:10,19:13,20:15},ca={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},fa={25:2,26:4,29:6,30:8,31:10,27:13,28:15},ga=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],va=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),ma=function(t){for(var e=[],r=0;rua&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=ua)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=oa(t);this.pos>=ua?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),Sa=function(){function t(t,e,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=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new Ta(r),this.nonDisplayedMemory=new Ta(r),this.lastOutputScreen=new Ta(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.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},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"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=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),La=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new va;this.channels=[null,new Sa(t,e,i),new Sa(t+1,r,i)]}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r=this;this.logger.time=t;for(var i=function(t){var i=127&e[t],n=127&e[t+1],a=!1,s=null;if(0===i&&0===n)return 0;r.logger.log(3,(function(){return"["+ma([e[t],e[t+1]])+"] -> ("+ma([i,n])+")"}));var o=r.cmdHistory;if(i>=16&&i<=31){if(function(t,e,r){return r.a===t&&r.b===e}(i,n,o))return Aa(null,null,o),r.logger.log(3,(function(){return"Repeated command ("+ma([i,n])+") is dropped"})),0;Aa(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 Aa(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 "+ma([i,n])+" orig: "+ma([e[t],e[t+1]])}))},n=0;n=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;var r=20===t||21===t||23===t?1:2,i=this.channels[r];return 20===t||21===t||28===t||29===t?32===e?i.ccRCL():33===e?i.ccBS():34===e?i.ccAOF():35===e?i.ccAON():36===e?i.ccDER():37===e?i.ccRU(2):38===e?i.ccRU(3):39===e?i.ccRU(4):40===e?i.ccFON():41===e?i.ccRDC():42===e?i.ccTR():43===e?i.ccRTD():44===e?i.ccEDM():45===e?i.ccCR():46===e?i.ccENM():47===e&&i.ccEOC():i.ccTO(e-32),this.currentChannel=r,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,(function(){return"MIDROW ("+ma([t,e])+")"})),!0)}return!1},e.parsePAC=function(t,e){var r;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;var i=t<=23?1:2;r=e>=64&&e<=95?1===i?ha[t]:ca[t]:1===i?da[t]:fa[t];var n=this.channels[i];return!!n&&(n.setPAC(this.interpretPAC(r,e)),this.currentChannel=i,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-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},e.parseChars=function(t,e){var r,i,n=null,a=null;return t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,(function(){return"Special char '"+oa(i)+"' in channel "+r})),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n&&this.logger.log(3,(function(){return"Char codes = "+ma(n).join(",")})),n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=ga[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),ba=function(){if(null!=j&&j.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),ka=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function Da(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Ia=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function wa(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var Ca=new ba(0,0,""),_a="middle"===Ca.align?"middle":"center";function xa(t,e,r){var i=t;function n(){var e=Da(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Ia;wa(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",_a,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",_a,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",_a,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===Ca.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",_a);var a=i.get("position","auto");"auto"===a&&50===Ca.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function Pa(t){return t.replace(//gi,"\n")}var Fa=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new ka,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=Pa(t);r>>0).toString()};function Ua(t,e,r){return Na(t.toString())+Na(e.toString())+Na(r)}function Ba(t,e,r,i,n,a,s){var o,l,u,h=new Fa,d=Rt(new Uint8Array(t)).trim().replace(Ma,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),Rn(o,An,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=wn(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=Ua(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(Oa(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){Oa(t,"LOCAL:")?g=t.slice(6):Oa(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(y(e)&&y(r)&&y(i)&&y(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var Ga="stpp.ttml.im1t",Ka=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Ha=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Va={left:"start",center:"center",right:"end",start:"start",end:"end"};function Ya(t,e,r,i){var n=Ot(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return Rt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),Rn(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},a=Object.keys(n).reduce((function(t,e){return t[e]=i.getAttribute("ttp:"+e)||n[e],t}),{}),s="preserve"!==i.getAttribute("xml:space"),l=ja(Wa(i,"styling","style")),u=ja(Wa(i,"layout","region")),h=Wa(i,"body","[begin]");return[].map.call(h,(function(t){var r=qa(t,s);if(!r||!t.hasAttribute("begin"))return null;var i=Qa(t.getAttribute("begin"),a),n=Qa(t.getAttribute("dur"),a),h=Qa(t.getAttribute("end"),a);if(null===i)throw za(t);if(null===h){if(null===n)throw za(t);h=i+n}var d=new ba(i-e,h-e,r);d.id=Ua(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=Xa(e,i,a)||Xa(t,i,a)||Xa(n,i,a);return s&&(r[a]=s),r}),{})}(u[t.getAttribute("region")],l[t.getAttribute("style")],l),f=c.textAlign;if(f){var g=Va[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function Wa(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function ja(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function qa(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?qa(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function Xa(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function za(t){return new Error("Could not parse ttml timestamp "+t)}function Qa(t,e){if(!t)return null;var r=Da(t);return null===r&&(Ka.test(t)?r=function(t,e){var r=Ka.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):Ha.test(t)&&(r=function(t,e){var r=Ha.exec(t),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/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var Ja=function(){function t(t){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=t,this.config=t.config,this.Cues=t.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}},t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0},e.initCea608Parsers=function(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){var t=new Ra(this,"textTrack1"),e=new Ra(this,"textTrack2"),r=new Ra(this,"textTrack3"),i=new Ra(this,"textTrack4");this.cea608Parser1=new La(1,t,e),this.cea608Parser2=new La(3,r,i)}},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t,e){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||Ya(e,this.initPTS[t.cc],(function(){i.textCodec=Ga,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Ke(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===Oe&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){this.initCea608Parsers();var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==Fe||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;sthis.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=Math.max(e,r),o=0;o=s||l.height>=s)&&(i=l,!(n=t[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},s(t,[{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 t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),es=function(){function t(t){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=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.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)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),rs="[eme]",is=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,rs),this.log=w.log.bind(w,rs),this.warn=w.warn.bind(w,rs),this.error=w.error.bind(w,rs),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===q.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof ls?e:new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===ot&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case q.FAIRPLAY:n=["cenc","sinf"];break;case q.WIDEVINE:case q.PLAYREADY:n=["cenc"];break;case q.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+kt.hexDump(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return kt.hexDump(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+kt.hexDump((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=at(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=nt(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof ls?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:at(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=at(this.config)),0===t.length)throw new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData,n='"'+t.type+'" event: init data type: "'+r+'"';if(this.debug(n),null!==i){var a,s;if("sinf"===r&&this.config.drmSystems[q.FAIRPLAY]){var o=Ct(new Uint8Array(i));try{var l=V(JSON.parse(o).sinf),u=Vt(new Uint8Array(l));if(!u)throw new Error("'schm' box missing or not cbcs/cenc with schi > tenc");a=u.subarray(8,24),s=q.FAIRPLAY}catch(t){return void this.warn(n+" Failed to parse sinf: "+t)}}else{var h=function(t){var e=[];if(t instanceof ArrayBuffer)for(var r=t.byteLength,i=0;i+320)for(var a,s=0,o=n.length;s in key message");return W(atob(f))},e.setupLicenseXHR=function(t,e,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,t,e,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return t.open("POST",e,!0),a.call(n.hls,t,e,r,i)})).then((function(r){return t.readyState||t.open("POST",e,!0),{xhr:t,licenseChallenge:r||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))},e.requestLicense=function(t,e){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrl(t.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!t.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,t)}catch(t){r.error(t)}n(l)}else{var h=i.errorRetry,d=h?h.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>d||o.status>=400&&o.status<500)a(new ls({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_LICENSE_REQUEST_FAILED,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 c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(e){var i=e.xhr,n=e.licenseChallenge;t.keySystem==q.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},Zt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t)})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t)}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),t._onmessage&&(r.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();is.CDMCleanupPromise=void 0;var ns,as,ss,os,ls=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(c(Error));!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(ns||(ns={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}(as||(as={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}(ss||(ss={}));var us=((os={})[ss.OBJECT]=["br","d","ot","tb"],os[ss.REQUEST]=["bl","dl","mtp","nor","nrr","su"],os[ss.SESSION]=["cid","pr","sf","sid","st","v"],os[ss.STATUS]=["bs","rtp"],os),hs=function t(e,r){this.value=void 0,this.params=void 0,Array.isArray(e)&&(e=e.map((function(e){return e instanceof t?e:new t(e)}))),this.value=e,this.params=r},ds=function(t){this.description=void 0,this.description=t},cs="Dict";function fs(t,e,r,i){return new Error("failed to "+t+' "'+(n=e,(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}var gs="Bare Item",vs="Boolean",ms="Byte Sequence",ps="Decimal",ys="Integer",Es=/[\x00-\x1f\x7f]+/,Ts="Token",Ss="Key";function Ls(t,e,r){return fs("serialize",t,e,r)}function As(t){if(!1===ArrayBuffer.isView(t))throw Ls(t,ms);return":"+(e=t,btoa(String.fromCharCode.apply(String,e))+":");var e}function Rs(t){if(function(t){return t<-999999999999999||99999999999999912)throw Ls(t,ps);var r=e.toString();return r.includes(".")?r:r+".0"}var Ds="String";function Is(t){var e,r=(e=t).description||e.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw Ls(r,Ts);return r}function ws(t){switch(typeof t){case"number":if(!y(t))throw Ls(t,gs);return Number.isInteger(t)?Rs(t):ks(t);case"string":return function(t){if(Es.test(t))throw Ls(t,Ds);return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(t);case"symbol":return Is(t);case"boolean":return function(t){if("boolean"!=typeof t)throw Ls(t,vs);return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return"@"+Rs(t.getTime()/1e3)}(t);if(t instanceof Uint8Array)return As(t);if(t instanceof ds)return Is(t);default:throw Ls(t,gs)}}function Cs(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw Ls(t,Ss);return t}function _s(t){return null==t?"":Object.entries(t).map((function(t){var e=t[0],r=t[1];return!0===r?";"+Cs(e):";"+Cs(e)+"="+ws(r)})).join("")}function xs(t){return t instanceof hs?""+ws(t.value)+_s(t.params):ws(t)}function Ps(t,e){var r;if(void 0===e&&(e={whitespace:!0}),"object"!=typeof t)throw Ls(t,cs);var i=t instanceof Map?t.entries():Object.entries(t),n=null!=(r=e)&&r.whitespace?" ":"";return Array.from(i).map((function(t){var e=t[0],r=t[1];r instanceof hs==0&&(r=new hs(r));var i,n=Cs(e);return!0===r.value?n+=_s(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(xs).join(" ")+")"+_s(i.params):n+=xs(r)),n})).join(","+n)}var Fs=function(t){return"ot"===t||"sf"===t||"st"===t},Ms=function(t){return"number"==typeof t?y(t):null!=t&&""!==t&&!1!==t},Os=function(t){return Math.round(t)},Ns=function(t){return 100*Os(t/100)},Us={br:Os,d:Os,bl:Ns,dl:Ns,mtp:Ns,nor:function(t,e){return null!=e&&e.baseUrl&&(t=function(t,e){var r=new URL(t),i=new URL(e);if(r.origin!==i.origin)return t;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("/")}(t,e.baseUrl)),encodeURIComponent(t)},rtp:Ns,tb:Os};function Bs(t,e){return void 0===e&&(e={}),t?function(t,e){return Ps(t,e)}(function(t,e){var r={};if(null==t||"object"!=typeof t)return r;var i=Object.keys(t).sort(),n=o({},Us,null==e?void 0:e.formatters),a=null==e?void 0:e.filter;return i.forEach((function(i){if(null==a||!a(i)){var s=t[i],o=n[i];o&&(s=o(s,e)),"v"===i&&1===s||"pr"==i&&1===s||Ms(s)&&(Fs(i)&&"string"==typeof s&&(s=new ds(s)),r[i]=s)}})),r}(t,e),o({whitespace:!1},e)):""}function Gs(t,e,r){return o(t,function(t,e){var r;if(void 0===e&&(e={}),!t)return{};var i=Object.entries(t),n=Object.entries(us).concat(Object.entries((null==(r=e)?void 0:r.customHeaderMap)||{})),a=i.reduce((function(t,e){var r,i=e[0],a=e[1],s=(null==(r=n.find((function(t){return t[1].includes(i)})))?void 0:r[0])||ss.REQUEST;return null!=t[s]||(t[s]={}),t[s][i]=a,t}),{});return Object.entries(a).reduce((function(t,r){var i=r[0],n=r[1];return t[i]=Bs(n,e),t}),{})}(e,r))}var Ks="CMCD",Hs=/CMCD=[^&#]+/;function Vs(t,e,r){var i=function(t,e){if(void 0===e&&(e={}),!t)return"";var r=Bs(t,e);return Ks+"="+encodeURIComponent(r)}(e,r);if(!i)return t;if(Hs.test(t))return t.replace(Hs,i);var n=t.includes("?")?"&":"?";return""+t+n+i}var Ys=function(){function t(t){var e=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(){e.initialized&&(e.starved=!0),e.buffering=!0},this.onPlaying=function(){e.initialized||(e.initialized=!0),e.buffering=!1},this.applyPlaylistData=function(t){try{e.apply(t,{ot:ns.MANIFEST,su:!e.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var r=t.frag,i=e.hls.levels[r.level],n=e.getObjectType(r),a={d:1e3*r.duration,ot:n};n!==ns.VIDEO&&n!==ns.AUDIO&&n!=ns.MUXED||(a.br=i.bitrate/1e3,a.tb=e.getTopBandwidth(n)/1e3,a.bl=e.getBufferLength(n)),e.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=t;var r=this.config=t.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||function(){try{return crypto.randomUUID()}catch(i){try{var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var e=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==t?e:3&e|8).toString(16)}))}}}(),this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:as.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(t,e){void 0===e&&(e={}),o(e,this.createData());var r=e.ot===ns.INIT||e.ot===ns.VIDEO||e.ot===ns.MUXED;this.starved&&r&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);var i=this.includeKeys;i&&(e=Object.keys(e).reduce((function(t,r){return i.includes(r)&&(t[r]=e[r]),t}),{})),this.useHeaders?(t.headers||(t.headers={}),Gs(t.headers,e)):t.url=Vs(t.url,e)},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?ns.TIMED_TEXT:"initSegment"===t.sn?ns.INIT:"audio"===e?ns.AUDIO:"main"===e?this.hls.audioTracks.length?ns.VIDEO:ns.MUXED:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===ns.AUDIO)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=g(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===ns.AUDIO?this.audioBuffer:this.videoBuffer;return r&&e?1e3*ri.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t}(),Ws=function(){function t(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=w.log.bind(w,"[content-steering]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.ERROR,this.onError,this))},e.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}},e.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},e.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},e.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},e.removeLevel=function(t){var e=this.levels;e&&(this.levels=e.filter((function(e){return e!==t})))},e.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},e.onManifestLoaded=function(t,e){var r=e.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},e.onManifestParsed=function(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks},e.onError=function(t,e){var r=e.errorAction;if((null==r?void 0:r.action)===Dr&&r.flags===_r){var i=this.levels,n=this.pathwayPriority,a=this.pathwayId;if(e.context){var s=e.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=i.reduce((function(t,e){return-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t}),[])),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),r.resolved||w.warn("Could not resolve "+e.details+' ("'+e.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+JSON.stringify(n)+" penalized: "+JSON.stringify(this.penalizedPathways))}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,mr(e),this.hls.trigger(S.LEVELS_UPDATED,{levels:e});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}}}},e.getPathwayForGroupId=function(t,e,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n=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),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;Sr(d,r.retry,!1,{url:t.url,data:void 0,code:a})?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){if(this.config){var t=this.config.loadPolicy.timeoutRetry;if(Sr(t,this.stats.retry,!0))this.retry(t);else{var e;w.warn("timeout while loading "+(null==(e=this.context)?void 0:e.url));var r=this.callbacks;r&&(this.abortInternal(),r.onTimeout(this.stats,this.context,this.loader))}}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=Er(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==e?void 0:e.url)+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Xs.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Qs=/(\d+)-(\d+)\/(\d+)/,Js=function(){function t(t){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=t.fetchSetup||$s,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.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},e.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,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 a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&y(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new to(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Qs.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(y(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&y(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var 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 l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!y(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new xi,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function $s(t,e){return new self.Request(t.url,e)}var Zs,to=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(c(Error)),eo=/\s/,ro=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,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,loader:zs,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:jr,bufferController:na,capLevelController:ts,errorController:Pr,fpsController:es,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:ot,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,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}}},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:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=Pa(l.trim()),v=Ua(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Ke(t,e)}))),u}},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:Zn,subtitleTrackController:ea,timelineController:Ja,audioStreamController:Jn,audioTrackController:$n,emeController:is,cmcdController:Ys,contentSteeringController:Ws});function io(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(io):Object.keys(t).reduce((function(e,r){return e[r]=io(t[r]),e}),{}):t}function no(t){var e=t.loader;e!==Js&&e!==zs?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=Js,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var ao=function(t){function e(e,r){var i;return(i=t.call(this,e,"[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}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.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(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r=this.hls.config.preferManagedMediaSource,i=[],n={},a={},s=!1,o=!1,l=!1;e.levels.forEach((function(t){var e,u,h=t.attrs,d=t.audioCodec,c=t.videoCodec;-1!==(null==(e=d)?void 0:e.indexOf("mp4a.40.34"))&&(Zs||(Zs=/chrome|firefox/i.test(navigator.userAgent)),Zs&&(t.audioCodec=d=void 0)),d&&(t.audioCodec=d=ve(d,r)),0===(null==(u=c)?void 0:u.indexOf("avc1"))&&(c=t.videoCodec=function(t){for(var e=t.split(","),r=0;r2){var n=i.shift()+".";n+=parseInt(i.shift()).toString(16),n+=("000"+parseInt(i.shift()).toString(16)).slice(-4),e[r]=n}}return e.join(",")}(c));var f=t.width,g=t.height,v=t.unknownCodecs;if(s||(s=!(!f||!g)),o||(o=!!c),l||(l=!!d),!(null!=v&&v.length||d&&!le(d,"audio",r)||c&&!le(c,"video",r))){var m=h.CODECS,p=h["FRAME-RATE"],y=h["HDCP-LEVEL"],E=h["PATHWAY-ID"],T=h.RESOLUTION,S=h["VIDEO-RANGE"],L=(E||".")+"-"+t.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(n[L])if(n[L].uri===t.url||t.attrs["PATHWAY-ID"])n[L].addGroupId("audio",h.AUDIO),n[L].addGroupId("text",h.SUBTITLES);else{var A=a[L]+=1;t.attrs["PATHWAY-ID"]=new Array(A+1).join(".");var R=new or(t);n[L]=R,i.push(R)}else{var b=new or(t);n[L]=b,a[L]=1,i.push(b)}}})),this.filterAndSortMediaOptions(i,e,s,o,l)},r.filterAndSortMediaOptions=function(t,e,r,i,n){var a=this,s=[],o=[],l=t;if((r||i)&&n&&(l=l.filter((function(t){var e,r=t.videoCodec,i=t.videoRange,n=t.width,a=t.height;return(!!r||!(!n||!a))&&!!(e=i)&&er.indexOf(e)>-1}))),0!==l.length){if(e.audioTracks){var u=this.hls.config.preferManagedMediaSource;so(s=e.audioTracks.filter((function(t){return!t.audioCodec||le(t.audioCodec,"audio",u)})))}e.subtitles&&so(o=e.subtitles);var h=l.slice(0);l.sort((function(t,e){if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return er.indexOf(t.videoRange)-er.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){var i=de(t.videoCodec),n=de(e.videoCodec);if(i!==n)return n-i}if(t.uri===e.uri&&t.codecSet!==e.codecSet){var a=ce(t.codecSet),s=ce(e.codecSet);if(a!==s)return s-a}return t.averageBitrate!==e.averageBitrate?t.averageBitrate-e.averageBitrate:0}));var d=h[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==h.length)for(var c=0;cm&&m===ro.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=p)}break}var y=n&&!i,E={levels:l,audioTracks:s,subtitleTracks:o,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:n,video:i,altAudio:!y&&s.some((function(t){return!!t.url}))};this.hls.trigger(S.MANIFEST_PARSED,E),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}else Promise.resolve().then((function(){if(a.hls){e.levels.length&&a.warn("One or more CODECS in variant not supported: "+JSON.stringify(e.levels[0].attrs));var t=new Error("no level with compatible codecs found in manifest");a.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}))},r.onError=function(t,e){!e.fatal&&e.context&&e.context.type===_e&&e.context.level===this.level&&this.checkRetry(e)},r.onFragBuffered=function(t,e){var r=e.frag;if(void 0!==r&&r.type===Fe){var i=r.elementaryStreams;if(!Object.keys(i).some((function(t){return!!i[t]})))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(t,e){var r,i,n=e.level,a=e.details,s=this._levels[n];if(!s)return this.warn("Invalid level index "+n),void(null!=(i=e.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));n===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0),this.playlistLoaded(n,e,s.details)):null!=(r=e.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentLevelIndex,i=this.currentLevel;if(i&&this.shouldLoadPlaylist(i)){var n=i.uri;if(e)try{n=e.addDirectives(n)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}var a=i.attrs["PATHWAY-ID"];this.log("Loading level index "+r+(void 0!==(null==e?void 0:e.msn)?" at sn "+e.msn+" part "+e.part:"")+" with"+(a?" Pathway "+a:"")+" "+n),this.clearTimer(),this.hls.trigger(S.LEVEL_LOADING,{url:n,level:r,pathwayId:i.attrs["PATHWAY-ID"],id:0,deliveryDirectives:e||null})}},r.removeLevel=function(t){var e,r=this,i=this._levels.filter((function(e,i){return i!==t||(r.steering&&r.steering.removeLevel(e),e===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((function(t){return t.level=-1}))),!1)}));mr(i),this._levels=i,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(S.LEVELS_UPDATED,{levels:i})},r.onLevelsUpdated=function(t,e){var r=e.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var t=this.hls,e=t.autoLevelCapping,r=t.maxAutoLevel,i=t.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(S.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:e,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},s(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=e[t],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=o,n!==t||!o.details||!a||s!==l){this.log("Switching to level "+t+" ("+(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:t,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(S.LEVEL_SWITCHING,u);var h=o.details;if(!h||h.live){var d=this.switchParams(o.uri,null==a?void 0:a.details,h);this.loadPlaylist(d)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(Fr);function so(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var oo=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){var i;if(t&&t!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=A.KEY_LOAD_ERROR),new fi({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i2,c=!h||e&&e.start<=a||h-a>2&&!this.fragmentTracker.getPartialFragment(a);if(d||c)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var f;if(!(u.len>0||h))return;var g=Math.max(h,u.start||0)-a,v=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,m=(null==v||null==(f=v.details)?void 0:f.live)?2*v.details.targetduration:2,p=this.fragmentTracker.getPartialFragment(a);if(g>0&&(g<=m||p))return void(i.paused||this._trySkipBufferHole(p))}var y=self.performance.now();if(null!==n){var E=y-n;if(s||!(E>=250)||(this._reportStall(u),this.media)){var T=ri.bufferInfo(i,a,r.maxBufferHole);this._tryFixBufferStall(T,E)}}else this.stalled=y}else if(this.moved=!0,s||(this.nudgeRetry=0),null!==n){if(this.stallReported){var S=self.performance.now()-n;w.warn("playback not stuck anymore @"+a+", after "+Math.round(S)+"ms"),this.stallReported=!1}this.stalled=null}}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,n=this.media;if(null!==n){var a=n.currentTime,s=i.getPartialFragment(a);if(s&&(this._trySkipBufferHole(s)||!this.media))return;(t.len>r.maxBufferHole||t.nextStart&&t.nextStart-a1e3*r.highBufferWatchdogPeriod&&(w.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}},e._reportStall=function(t){var e=this.hls,r=this.media;if(!this.stallReported&&r){this.stallReported=!0;var i=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+JSON.stringify(t)+")");w.warn(i.message),e.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}},e._trySkipBufferHole=function(t){var e=this.config,r=this.hls,i=this.media;if(null===i)return 0;var n=i.currentTime,a=ri.bufferInfo(i,n,0),s=n0&&a.len<1&&i.readyState<3,u=s-n;if(u>0&&(o||l)){if(u>e.maxBufferHole){var h=this.fragmentTracker,d=!1;if(0===n){var c=h.getAppendedFrag(0,Fe);c&&s1?(i=0,this.bitrateTest=!0):i=r.firstAutoLevel),r.nextLoadLevel=i,this.level=r.loadLevel,this.loadedmetadata=!1}e>0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=Ti,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=Ei},r.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case Ci:var t=this.levels,e=this.level,r=null==t?void 0:t[e],i=null==r?void 0:r.details;if(i&&(!i.live||this.levelLastLoaded===r)){if(this.waitForCdnTuneIn(i))break;this.state=Ti;break}if(this.hls.nextLoadLevel!==this.level){this.state=Ti;break}break;case Ai:var n,a=self.performance.now(),s=this.retryDate;if(!s||a>=s||null!=(n=this.media)&&n.seeking){var o=this.levels,l=this.level,u=null==o?void 0:o[l];this.resetStartWhenNotLoaded(u||null),this.state=Ti}}this.state===Ti&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},r.doTickIdle=function(){var t=this.hls,e=this.levelLastLoaded,r=this.levels,i=this.media;if(null!==e&&(i||!this.startFragRequested&&t.config.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)){var n=t.nextLoadLevel;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 this.altAudio&&(l.type="video"),this.hls.trigger(S.BUFFER_EOS,l),void(this.state=Di)}t.loadLevel!==n&&-1===t.manualLevel&&this.log("Adapting to level "+n+" from level "+this.level),this.level=t.nextLoadLevel=n;var u=a.details;if(!u||this.state===Ci||u.live&&this.levelLastLoaded!==a)return this.level=n,void(this.state=Ci);var h=s.len,d=this.getMaxBufferLength(a.maxBitrate);if(!(h>=d)){this.backtrackFragment&&this.backtrackFragment.start>s.end&&(this.backtrackFragment=null);var c=this.backtrackFragment?this.backtrackFragment.start:s.end,f=this.getNextFragment(c,u);if(this.couldBacktrack&&!this.fragPrevious&&f&&"initSegment"!==f.sn&&this.fragmentTracker.getState(f)!==Jr){var g,v=(null!=(g=this.backtrackFragment)?g:f).sn-u.startSN,m=u.fragments[v-1];m&&f.cc===m.cc&&(f=m,this.fragmentTracker.removeFragment(m))}else this.backtrackFragment&&s.len&&(this.backtrackFragment=null);if(f&&this.isLoopLoading(f,c)){if(!f.gap){var p=this.audioOnly&&!this.altAudio?O:N,y=(p===N?this.videoBuffer:this.mediaBuffer)||this.media;y&&this.afterBufferFlushed(y,p,Fe)}f=this.getNextFragmentLoopLoading(f,u,s,Fe,d)}f&&(!f.initSegment||f.initSegment.data||this.bitrateTest||(f=f.initSegment),this.loadFragment(f,a,c))}}}}},r.loadFragment=function(e,r,i){var n=this.fragmentTracker.getState(e);this.fragCurrent=e,n===Xr||n===Qr?"initSegment"===e.sn?this._loadInitSegment(e,r):this.bitrateTest?(this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e,r)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):this.clearTrackerIfNeeded(e)},r.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,Fe)},r.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.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(!e.paused&&t){var s=t[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(e.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var h=u.maxStartPTS?u.maxStartPTS:u.start,d=u.duration,c=Math.max(l.end,h+Math.min(Math.max(d-this.config.maxFragLookUpTolerance,d*(this.couldBacktrack?.5:.125)),d*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(c,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case Si:case Li:case Ai:case bi:case ki:this.state=Ti}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},r.onMediaAttached=function(e,r){t.prototype.onMediaAttached.call(this,e,r);var i=r.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),i.addEventListener("playing",this.onvplaying),i.addEventListener("seeked",this.onvseeked),this.gapController=new ho(this.config,i,this.fragmentTracker,this.hls)},r.onMediaDetaching=function(){var e=this.media;e&&this.onvplaying&&this.onvseeked&&(e.removeEventListener("playing",this.onvplaying),e.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),t.prototype.onMediaDetaching.call(this)},r.onMediaPlaying=function(){this.tick()},r.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;y(e)&&this.log("Media seeked to "+e.toFixed(3));var r=this.getMainFwdBufferInfo();null!==r&&0!==r.len?this.tick():this.warn('Main forward buffer length on "seeked" event '+(r?r.len:"empty")+")")},r.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(S.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1},r.onManifestParsed=function(t,e){var r,i,n=!1,a=!1;e.levels.forEach((function(t){var e=t.audioCodec;e&&(n=n||-1!==e.indexOf("mp4a.40.2"),a=a||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=n&&a&&!("function"==typeof(null==(i=lo())||null==(r=i.prototype)?void 0:r.changeType)),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1},r.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===Ti){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=Ci)}},r.onLevelLoaded=function(t,e){var r,i=this.levels,n=e.level,a=e.details,s=a.totalduration;if(i){this.log("Level "+n+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+", cc ["+a.startCC+", "+a.endCC+"] duration:"+s);var o=i[n],l=this.fragCurrent;!l||this.state!==Li&&this.state!==Ai||l.level!==e.level&&l.loader&&this.abortCurrentFrag();var u=0;if(a.live||null!=(r=o.details)&&r.live){var h;if(this.checkLiveUpdate(a),a.deltaUpdateFailed)return;u=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details)}if(o.details=a,this.levelLastLoaded=o,this.hls.trigger(S.LEVEL_UPDATED,{details:a,level:n}),this.state===Ci){if(this.waitForCdnTuneIn(a))return;this.state=Ti}this.startFragRequested?a.live&&this.synchronizeToLiveEdge(a):this.setStartPosition(a,u),this.tick()}else this.warn("Levels were reset while loading level "+n)},r._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,n=t.payload,a=this.levels;if(a){var s=a[r.level],o=s.details;if(!o)return this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset"),void this.fragmentTracker.removeFragment(r);var l=s.videoCodec,u=o.PTSKnown||!o.live,h=null==(e=r.initSegment)?void 0:e.data,d=this._getAudioCodec(s),c=this.transmuxer=this.transmuxer||new qn(this.hls,Fe,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,v=new ii(r.level,r.sn,r.stats.chunkCount,n.byteLength,f,g),m=this.initPTS[r.cc];c.push(n,h,d,l,r,i,o.totalduration,u,v,m)}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r.onAudioTrackSwitching=function(t,e){var r=this.altAudio;if(!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var i=this.fragCurrent;i&&(this.log("Switching to main audio track, cancel main fragment load"),i.abortRequests(),this.fragmentTracker.removeFragment(i)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var n=this.hls;r&&(n.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),n.trigger(S.AUDIO_TRACK_SWITCHED,e)}},r.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var n=this.videoBuffer;n&&this.mediaBuffer!==n&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=n)}this.altAudio=i,this.tick()},r.onBufferCreated=function(t,e){var r,i,n=e.tracks,a=!1;for(var s in n){var o=n[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=n[s];l&&(this.videoBuffer=l.buffer)}}else a=!0}a&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},r.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===Fe){if(this.fragContextChanged(r))return this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state),void(this.state===ki&&(this.state=Ti));var n=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*n.total/(n.buffering.end-n.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},r.onError=function(t,e){var r;if(e.fatal)this.state=Ii;else switch(e.details){case A.FRAG_GAP:case A.FRAG_PARSING_ERROR:case A.FRAG_DECRYPT_ERROR:case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(Fe,e);break;case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:case A.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==Ci||(null==(r=e.context)?void 0:r.type)!==_e||(this.state=Ti);break;case A.BUFFER_APPEND_ERROR:case A.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===A.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case A.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}},r.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){if(this.loadedmetadata||!ri.getBuffered(t).length){var r=this.state!==Ti?this.fragCurrent:null;e.poll(this.lastCurrentTime,r)}this.lastCurrentTime=t.currentTime}},r.onFragLoadEmergencyAborted=function(){this.state=Ti,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},r.onBufferFlushed=function(t,e){var r=e.type;if(r!==O||this.audioOnly&&!this.altAudio){var i=(r===N?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,Fe),this.tick()}},r.onLevelsUpdated=function(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var t=this.media;if(t){var e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&(nT.cc;if(!1!==n.independent){var R=h.startPTS,b=h.endPTS,k=h.startDTS,D=h.endDTS;if(l)l.elementaryStreams[h.type]={startPTS:R,endPTS:b,startDTS:k,endDTS:D};else if(h.firstKeyFrame&&h.independent&&1===a.id&&!A&&(this.couldBacktrack=!0),h.dropped&&h.independent){var I=this.getMainFwdBufferInfo(),w=(I?I.end:this.getLoadPosition())+this.config.maxBufferHole,C=h.firstKeyFramePTS?h.firstKeyFramePTS:R;if(!L&&w2&&(o.gap=!0);o.setElementaryStreamInfo(h.type,R,b,k,D),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(h,o,l,a,L||A)}else{if(!L&&!A)return void this.backtrack(o);o.gap=!0}}if(v){var _=v.startPTS,x=v.endPTS,P=v.startDTS,F=v.endDTS;l&&(l.elementaryStreams[O]={startPTS:_,endPTS:x,startDTS:P,endDTS:F}),o.setElementaryStreamInfo(O,_,x,P,F),this.bufferFragmentData(v,o,l,a)}if(g&&null!=c&&null!=(e=c.samples)&&e.length){var M={id:r,frag:o,details:g,samples:c.samples};i.trigger(S.FRAG_PARSING_METADATA,M)}if(g&&d){var N={id:r,frag:o,details:g,samples:d.samples};i.trigger(S.FRAG_PARSING_USERDATA,N)}}}else this.resetWhenMissingContext(a)},r._bufferInitSegment=function(t,e,r,i){var n=this;if(this.state===bi){this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;var a=e.audio,s=e.video,o=e.audiovideo;if(a){var l=t.audioCodec,u=navigator.userAgent.toLowerCase();if(this.audioCodecSwitch){l&&(l=-1!==l.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5");var h=a.metadata;h&&"channelCount"in h&&1!==(h.channelCount||1)&&-1===u.indexOf("firefox")&&(l="mp4a.40.5")}l&&-1!==l.indexOf("mp4a.40.5")&&-1!==u.indexOf("android")&&"audio/mpeg"!==a.container&&(l="mp4a.40.2",this.log("Android: force audio codec to "+l)),t.audioCodec&&t.audioCodec!==l&&this.log('Swapping manifest audio codec "'+t.audioCodec+'" for "'+l+'"'),a.levelCodec=l,a.id="main",this.log("Init audio buffer, container:"+a.container+", codecs[selected/level/parsed]=["+(l||"")+"/"+(t.audioCodec||"")+"/"+a.codec+"]")}s&&(s.levelCodec=t.videoCodec,s.id="main",this.log("Init video buffer, container:"+s.container+", codecs[level/parsed]=["+(t.videoCodec||"")+"/"+s.codec+"]")),o&&this.log("Init audiovideo buffer, container:"+o.container+", codecs[level/parsed]=["+t.codecs+"/"+o.codec+"]"),this.hls.trigger(S.BUFFER_CODECS,e),Object.keys(e).forEach((function(t){var a=e[t].initSegment;null!=a&&a.byteLength&&n.hls.trigger(S.BUFFER_APPENDING,{type:t,data:a,frag:r,part:null,chunkMeta:i,parent:r.type})})),this.tickImmediate()}},r.getMainFwdBufferInfo=function(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,Fe)},r.backtrack=function(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=Ti},r.checkFragmentChanged=function(){var t=this.media,e=null;if(t&&t.readyState>1&&!1===t.seeking){var r=t.currentTime;if(ri.isBuffered(t,r)?e=this.getAppendedFrag(r):ri.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){this.backtrackFragment=null;var i=this.fragPlaying,n=e.level;i&&e.sn===i.sn&&i.level===n||(this.fragPlaying=e,this.hls.trigger(S.FRAG_CHANGED,{frag:e}),i&&i.level===n||this.hls.trigger(S.LEVEL_SWITCHED,{level:n}))}}},s(e,[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentFrag",get:function(){var t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}},{key:"currentProgramDateTime",get:function(){var t=this.media;if(t){var e=t.currentTime,r=this.currentFrag;if(r&&y(e)&&y(r.programDateTime)){var i=r.programDateTime+1e3*(e-r.start);return new Date(i)}}return null}},{key:"currentLevel",get:function(){var t=this.currentFrag;return t?t.level:-1}},{key:"nextBufferedFrag",get:function(){var t=this.currentFrag;return t?this.followingBufferedFrag(t):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}]),e}(_i),fo=function(){function t(e){void 0===e&&(e={}),this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new Vn,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.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,I(e.debug||!1,"Hls instance");var r=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var r=io(t),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(t){var i=("level"===t?"playlist":t)+"LoadPolicy",a=void 0===e[i],s=[];n.forEach((function(n){var o=t+"Loading"+n,l=e[o];if(void 0!==l&&a){s.push(o);var u=r[i].default;switch(e[i]={default:u},n){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}}})),s.length&&w.warn('hls.js config: "'+s.join('", "')+'" setting(s) are deprecated, use "'+i+'": '+JSON.stringify(e[i]))})),i(i({},r),e)}(t.DefaultConfig,e);this.userConfig=e,r.progressive&&no(r);var n=r.abrController,a=r.bufferController,s=r.capLevelController,o=r.errorController,l=r.fpsController,u=new o(this),h=this.abrController=new n(this),d=this.bufferController=new a(this),c=this.capLevelController=new s(this),f=new l(this),g=new Be(this),v=new $e(this),m=r.contentSteeringController,p=m?new m(this):null,y=this.levelController=new ao(this,p),E=new $r(this),T=new oo(this.config),L=this.streamController=new co(this,E,T);c.setStreamController(L),f.setStreamController(L);var A=[g,y,L];p&&A.splice(1,0,p),this.networkControllers=A;var R=[h,d,c,f,v,E];this.audioTrackController=this.createController(r.audioTrackController,A);var b=r.audioStreamController;b&&A.push(new b(this,E,T)),this.subtitleTrackController=this.createController(r.subtitleTrackController,A);var k=r.subtitleStreamController;k&&A.push(new k(this,E,T)),this.createController(r.timelineController,R),T.emeController=this.emeController=this.createController(r.emeController,R),this.cmcdController=this.createController(r.cmcdController,R),this.latencyController=this.createController(Ze,R),this.coreComponents=R,A.push(u);var D=u.onErrorOut;"function"==typeof D&&this.on(S.ERROR,D,u)}t.isMSESupported=function(){return uo()},t.isSupported=function(){return function(){if(!uo())return!1;var t=se();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((function(e){return t.isTypeSupported(he(e,"video"))}))||["mp4a.40.2","fLaC"].some((function(e){return t.isTypeSupported(he(e,"audio"))})))}()},t.getMediaSource=function(){return se()};var e=t.prototype;return e.createController=function(t,e){if(t){var r=new t(this);return e&&e.push(r),r}return null},e.on=function(t,e,r){void 0===r&&(r=this),this._emitter.on(t,e,r)},e.once=function(t,e,r){void 0===r&&(r=this),this._emitter.once(t,e,r)},e.removeAllListeners=function(t){this._emitter.removeAllListeners(t)},e.off=function(t,e,r,i){void 0===r&&(r=this),this._emitter.off(t,e,r,i)},e.listeners=function(t){return this._emitter.listeners(t)},e.emit=function(t,e,r){return this._emitter.emit(t,e,r)},e.trigger=function(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(w.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;var r=t===S.ERROR;this.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,fatal:r,event:t,error:e}),this.triggeringException=!1}}return!1},e.listenerCount=function(t){return this._emitter.listenerCount(t)},e.destroy=function(){w.log("destroy"),this.trigger(S.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((function(t){return t.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(t){return t.destroy()})),this.coreComponents.length=0;var t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null},e.attachMedia=function(t){w.log("attachMedia"),this._media=t,this.trigger(S.MEDIA_ATTACHING,{media:t})},e.detachMedia=function(){w.log("detachMedia"),this.trigger(S.MEDIA_DETACHING,void 0),this._media=null},e.loadSource=function(t){this.stopLoad();var e=this.media,r=this.url,i=this.url=p.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,w.log("loadSource:"+i),e&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(S.MANIFEST_LOADING,{url:t})},e.startLoad=function(t){void 0===t&&(t=-1),w.log("startLoad("+t+")"),this.started=!0,this.networkControllers.forEach((function(e){e.startLoad(t)}))},e.stopLoad=function(){w.log("stopLoad"),this.started=!1,this.networkControllers.forEach((function(t){t.stopLoad()}))},e.resumeBuffering=function(){this.started&&this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.startLoad(-1)}))},e.pauseBuffering=function(){this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.stopLoad()}))},e.swapAudioCodec=function(){w.log("swapAudioCodec"),this.streamController.swapAudioCodec()},e.recoverMediaError=function(){w.log("recoverMediaError");var t=this._media;this.detachMedia(),t&&this.attachMedia(t)},e.removeLevel=function(t){this.levelController.removeLevel(t)},e.setAudioOption=function(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)},e.setSubtitleOption=function(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null},s(t,[{key:"levels",get:function(){var t=this.levelController.levels;return t||[]}},{key:"currentLevel",get:function(){return this.streamController.currentLevel},set:function(t){w.log("set currentLevel:"+t),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}},{key:"nextLevel",get:function(){return this.streamController.nextLevel},set:function(t){w.log("set nextLevel:"+t),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}},{key:"loadLevel",get:function(){return this.levelController.level},set:function(t){w.log("set loadLevel:"+t),this.levelController.manualLevel=t}},{key:"nextLoadLevel",get:function(){return this.levelController.nextLoadLevel},set:function(t){this.levelController.nextLoadLevel=t}},{key:"firstLevel",get:function(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)},set:function(t){w.log("set firstLevel:"+t),this.levelController.firstLevel=t}},{key:"startLevel",get:function(){var t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t},set:function(t){w.log("set startLevel:"+t),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(t){var e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(t){this._autoLevelCapping!==t&&(w.log("set autoLevelCapping:"+t),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}},{key:"bandwidthEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimate():NaN},set:function(t){this.abrController.resetEstimator(t)}},{key:"ttfbEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(t){(function(t){return tr.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,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 t=this.levels,e=this.config.minAutoBitrate;if(!t)return 0;for(var r=t.length,i=0;i=e)return i;return 0}},{key:"maxAutoLevel",get:function(){var t,e=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(t=-1===r&&null!=e&&e.length?e.length-1:r,i)for(var n=t;n--;){var a=e[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return t}},{key:"firstAutoLevel",get:function(){return this.abrController.firstAutoLevel}},{key:"nextAutoLevel",get:function(){return this.abrController.nextAutoLevel},set:function(t){this.abrController.nextAutoLevel=t}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"allAudioTracks",get:function(){var t=this.audioTrackController;return t?t.allAudioTracks:[]}},{key:"audioTracks",get:function(){var t=this.audioTrackController;return t?t.audioTracks:[]}},{key:"audioTrack",get:function(){var t=this.audioTrackController;return t?t.audioTrack:-1},set:function(t){var e=this.audioTrackController;e&&(e.audioTrack=t)}},{key:"allSubtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}},{key:"subtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTrack:-1},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var t=this.subtitleTrackController;return!!t&&t.subtitleDisplay},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(t){this.config.lowLatencyMode=t}},{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}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}}],[{key:"version",get:function(){return"1.5.15"}},{key:"Events",get:function(){return S}},{key:"ErrorTypes",get:function(){return L}},{key:"ErrorDetails",get:function(){return A}},{key:"DefaultConfig",get:function(){return t.defaultConfig?t.defaultConfig:ro},set:function(e){t.defaultConfig=e}}]),t}();return fo.defaultConfig=void 0,fo},"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/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 89264f0f67..00ef0e1e1e 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -945,8 +945,8 @@ final class AuthorizedApplicationContext { chatLocation = .peer(peer) } - if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController { - self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true) + if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { + self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index c899f6db8e..e2d930eb90 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -14,7 +14,7 @@ import UndoUI import UrlHandling import TelegramPresentationData -func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { +func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { let presentationData: PresentationData if let parentController = parentController as? ChatControllerImpl { presentationData = parentController.presentationData @@ -135,22 +135,16 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } }, didDismiss: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl { -// let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false -// parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer) -// if isFocused { -// parentController.chatDisplayNode.textInputPanelNode?.ensureFocused() -// } - parentController.updateChatPresentationInterfaceState(interactive: false) { state in return state.updatedForceInputCommandsHidden(false) } } }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal parentController.push(controller) @@ -201,7 +195,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in presentImpl?(c, a) @@ -209,11 +203,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal if let parentController = parentController as? ChatControllerImpl { @@ -257,11 +251,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() } }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { - return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - } else { - return parentController?.navigationController as? NavigationController + navigationController = parentController.effectiveNavigationController } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) }) controller.navigationPresentation = .flatModal if let parentController = parentController as? ChatControllerImpl { @@ -316,7 +310,7 @@ public extension ChatControllerImpl { } self.chatDisplayNode.dismissInput() - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil) } static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { @@ -567,7 +561,7 @@ public extension ChatControllerImpl { } }) } else { - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload) } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b84d45e5ef..a4a0dff681 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1172,7 +1172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.push(wallpaperPreviewController) return true - case let .giftPremium(_, _, duration, _, _): + case let .giftPremium(_, _, duration, _, _, _, _): strongSelf.chatDisplayNode.dismissInput() let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId @@ -1187,7 +1187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message)) strongSelf.push(controller) return true - case let .giftCode(slug, _, _, _, _, _, _, _, _): + case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true case .prizeStars: @@ -6946,12 +6946,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), self.disableStickerAnimationsPromise.get()).startStrict(next: { [weak self] sharedData, disableStickerAnimations in + self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), + self.disableStickerAnimationsPromise.get(), + context.sharedContext.hasGroupCallOnScreen + ).startStrict(next: { [weak self] sharedData, disableStickerAnimations, hasGroupCallOnScreen in var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value } + var disableStickerAnimations = disableStickerAnimations + if hasGroupCallOnScreen { + disableStickerAnimations = true + } + let chatStickerSettings = ChatInterfaceStickerSettings(stickerSettings: stickerSettings) if let strongSelf = self, strongSelf.stickerSettings != chatStickerSettings || strongSelf.disableStickerAnimationsValue != disableStickerAnimations { strongSelf.stickerSettings = chatStickerSettings @@ -9712,7 +9721,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G commit() }) } else { - strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload) commit() } } diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift index 92ec328110..75f774190b 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift @@ -24,7 +24,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private var contentNodeSnapshot: UIView? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(tapped: @escaping () -> Void) { self.tapped = tapped @@ -58,9 +58,9 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { self.progressNode.status = contentNode.status if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -74,15 +74,15 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { if let snapshot = snapshot { self.contentContainerNode.view.addSubview(snapshot) - if let _ = self.validLayoutSize { + if let _ = self.validLayout { snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size) } } } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) self.contentContainerNode.cornerRadius = size.width / 2.0 @@ -96,7 +96,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -0.5, dy: -0.5)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } if let contentNodeSnapshot = self.contentNodeSnapshot { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 60de3e50ed..8213dafa0c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2827,8 +2827,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } - public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { - openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift index e80b23197a..ca6b841c48 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleInstantVideoDecoration.swift @@ -16,7 +16,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private let inset: CGFloat - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init(inset: CGFloat, backgroundImage: UIImage?, tapped: @escaping () -> Void) { self.inset = inset @@ -51,9 +51,9 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -63,8 +63,8 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let diameter = size.width + inset self.contentContainerNode.cornerRadius = (diameter - 3.0) / 2.0 @@ -80,7 +80,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration { self.contentContainerNode.subnodeTransform = CATransform3DMakeScale((contentFrame.width + 2.0) / contentFrame.width, (contentFrame.width + 2.0) / contentFrame.width, 1.0) if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift index 46b641a0e2..30bcc375aa 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift @@ -23,7 +23,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? public init(corners: ImageCorners, nativeSize: CGSize, contentMode: ChatBubbleVideoDecorationContentMode, backgroundColor: UIColor) { self.corners = corners @@ -82,23 +82,23 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let size = self.validLayoutSize { + if let validLayout = self.validLayout { var scaledSize: CGSize switch self.contentMode { case .aspectFit: - scaledSize = self.nativeSize.aspectFitted(size) + scaledSize = self.nativeSize.aspectFitted(validLayout.size) case .aspectFill: - scaledSize = self.nativeSize.aspectFilled(size) + scaledSize = self.nativeSize.aspectFilled(validLayout.size) } - if abs(scaledSize.width - size.width) < 2.0 { - scaledSize.width = size.width + if abs(scaledSize.width - validLayout.size.width) < 2.0 { + scaledSize.width = validLayout.size.width } - if abs(scaledSize.height - size.height) < 2.0 { - scaledSize.height = size.height + if abs(scaledSize.height - validLayout.size.height) < 2.0 { + scaledSize.height = validLayout.size.height } - contentNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize) - contentNode.updateLayout(size: scaledSize, transition: .immediate) + contentNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.size.width - scaledSize.width) / 2.0), y: floor((validLayout.size.height - scaledSize.height) / 2.0)), size: scaledSize) + contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: .immediate) } } } @@ -108,8 +108,8 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { public func updateContentNodeSnapshot(_ snapshot: UIView?) { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let bounds = CGRect(origin: CGPoint(), size: size) if let backgroundNode = self.backgroundNode { @@ -137,7 +137,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { scaledSize.height = size.height } transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) - contentNode.updateLayout(size: scaledSize, transition: transition) + contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: transition) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift new file mode 100644 index 0000000000..a47dc9becc --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift @@ -0,0 +1,549 @@ +import Foundation +import SwiftSignalKit +import UniversalMediaPlayer +import Postbox +import TelegramCore +import AsyncDisplayKit +import AccountContext +import TelegramAudio +import RangeSet +import AVFoundation +import Display +import PhotoResources +import TelegramVoip + +final class HLSVideoAVContentNode: ASDisplayNode, UniversalVideoContentNode { + private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let fileReference: FileMediaReference + private let approximateDuration: Double + private let intrinsicDimensions: CGSize + + private let audioSessionManager: ManagedAudioSession + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + private var baseRate: Double = 1.0 + private var isBuffering = false + private var seekId: Int = 0 + private let _status = ValuePromise() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(RangeSet, Int64)?>() + var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { + return self._bufferingStatus.get() + } + + var isNativePictureInPictureActive: Signal { + return .single(false) + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private var playerSource: HLSServerSource? + private var serverDisposable: Disposable? + + private let imageNode: TransformImageNode + + private var playerItem: AVPlayerItem? + private var player: AVPlayer? + private let playerNode: ASDisplayNode + + private var loadProgressDisposable: Disposable? + private var statusDisposable: Disposable? + + private var didPlayToEndTimeObserver: NSObjectProtocol? + private var didBecomeActiveObserver: NSObjectProtocol? + private var willResignActiveObserver: NSObjectProtocol? + private var failureObserverId: NSObjectProtocol? + private var errorObserverId: NSObjectProtocol? + private var playerItemFailedToPlayToEndTimeObserver: NSObjectProtocol? + + private let fetchDisposable = MetaDisposable() + + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: (size: CGSize, actualSize: CGSize)? + + private var statusTimer: Foundation.Timer? + + private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto + + init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + self.postbox = postbox + self.fileReference = fileReference + self.approximateDuration = fileReference.media.duration ?? 0.0 + self.audioSessionManager = audioSessionManager + self.userLocation = userLocation + self.baseRate = baseRate + + if var dimensions = fileReference.media.dimensions { + if let thumbnail = fileReference.media.previewRepresentations.first { + let dimensionsVertical = dimensions.width < dimensions.height + let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height + if dimensionsVertical != thumbnailVertical { + dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width) + } + } + self.dimensions = dimensions.cgSize + } else { + self.dimensions = CGSize(width: 128.0, height: 128.0) + } + + self.imageNode = TransformImageNode() + + var player: AVPlayer? + player = AVPlayer(playerItem: nil) + self.player = player + if #available(iOS 16.0, *) { + player?.defaultRate = Float(baseRate) + } + if !enableSound { + player?.volume = 0.0 + } + + self.playerNode = ASDisplayNode() + self.playerNode.setLayerBlock({ + return AVPlayerLayer(player: player) + }) + + self.intrinsicDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) + + self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + + if let qualitySet = HLSQualitySet(baseFile: fileReference) { + self.playerSource = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) + } + + super.init() + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) + } + } + } + } + return getData + }) + + self.addSubnode(self.imageNode) + self.addSubnode(self.playerNode) + self.player?.actionAtItemEnd = .pause + + self.imageNode.imageUpdated = { [weak self] _ in + self?._ready.set(.single(Void())) + } + + self.player?.addObserver(self, forKeyPath: "rate", options: [], context: nil) + + self._bufferingStatus.set(.single(nil)) + + if let playerSource = self.playerSource { + self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + + let playerItem: AVPlayerItem + let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8" + #if DEBUG + print("HLSVideoAVContentNode: playing \(assetUrl)") + #endif + playerItem = AVPlayerItem(url: URL(string: assetUrl)!) + + if #available(iOS 14.0, *) { + playerItem.startsOnFirstEligibleVariant = true + } + + self.setPlayerItem(playerItem) + } + }) + } + + self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in + guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { + return + } + layer.player = strongSelf.player + }) + self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in + guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { + return + } + layer.player = nil + }) + } + + deinit { + self.player?.removeObserver(self, forKeyPath: "rate") + + self.setPlayerItem(nil) + + self.audioSessionDisposable.dispose() + + self.loadProgressDisposable?.dispose() + self.statusDisposable?.dispose() + + if let didBecomeActiveObserver = self.didBecomeActiveObserver { + NotificationCenter.default.removeObserver(didBecomeActiveObserver) + } + if let willResignActiveObserver = self.willResignActiveObserver { + NotificationCenter.default.removeObserver(willResignActiveObserver) + } + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + if let failureObserverId = self.failureObserverId { + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + NotificationCenter.default.removeObserver(errorObserverId) + } + + self.serverDisposable?.dispose() + + self.statusTimer?.invalidate() + } + + private func setPlayerItem(_ item: AVPlayerItem?) { + if let playerItem = self.playerItem { + playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") + playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") + playerItem.removeObserver(self, forKeyPath: "status") + playerItem.removeObserver(self, forKeyPath: "presentationSize") + } + + if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver { + self.playerItemFailedToPlayToEndTimeObserver = nil + NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver) + } + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + self.didPlayToEndTimeObserver = nil + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + if let failureObserverId = self.failureObserverId { + self.failureObserverId = nil + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + self.errorObserverId = nil + NotificationCenter.default.removeObserver(errorObserverId) + } + + self.playerItem = item + + if let item { + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil, using: { [weak self] notification in + self?.performActionAtEnd() + }) + + self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: item, queue: .main, using: { notification in +#if DEBUG + print("Player Error: \(notification.description)") +#endif + }) + self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: item, queue: .main, using: { [weak item] notification in + if let item { + let event = item.errorLog()?.events.last + if let event { + let _ = event +#if DEBUG + print("Player Error: \(event.errorComment ?? "")") +#endif + } + } + }) + item.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil) + } + + if let playerItem = self.playerItem { + playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) + self.playerItemFailedToPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { [weak self] _ in + guard let self else { + return + } + let _ = self + }) + } + + self.player?.replaceCurrentItem(with: self.playerItem) + } + + private func updateStatus() { + guard let player = self.player else { + return + } + let isPlaying = !player.rate.isZero + let status: MediaPlayerPlaybackStatus + if self.isBuffering { + status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) + } else { + status = isPlaying ? .playing : .paused + } + var timestamp = player.currentTime().seconds + if timestamp.isFinite && !timestamp.isNaN { + } else { + timestamp = 0.0 + } + self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, seekId: self.seekId, status: status, soundEnabled: true) + self._status.set(self.statusValue) + + if case .playing = status { + if self.statusTimer == nil { + self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateStatus() + }) + } + } else if let statusTimer = self.statusTimer { + self.statusTimer = nil + statusTimer.invalidate() + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "rate" { + if let player = self.player { + let isPlaying = !player.rate.isZero + if isPlaying { + self.isBuffering = false + } + } + self.updateStatus() + } else if keyPath == "playbackBufferEmpty" { + self.isBuffering = true + self.updateStatus() + } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { + self.isBuffering = false + self.updateStatus() + } else if keyPath == "presentationSize" { + if let currentItem = self.player?.currentItem { + print("Presentation size: \(Int(currentItem.presentationSize.height))") + } + } + } + + private func performActionAtEnd() { + for listener in self.playbackCompletedListeners.copyItems() { + listener() + } + } + + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + + if let dimensions = self.dimensions { + let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + } + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) + } + if !self.hasAudioSession { + if self.player?.volume != 0.0 { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + guard let self else { + return + } + self.hasAudioSession = true + self.player?.play() + }, deactivate: { [weak self] _ in + guard let self else { + return .complete() + } + self.hasAudioSession = false + self.player?.pause() + + return .complete() + })) + } else { + self.player?.play() + } + } else { + self.player?.play() + } + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.player?.pause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + + guard let player = self.player else { + return + } + + if player.rate.isZero { + self.play() + } else { + self.pause() + } + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + if value { + if !self.hasAudioSession { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + self?.hasAudioSession = true + self?.player?.volume = 1.0 + }, deactivate: { [weak self] _ in + self?.hasAudioSession = false + self?.player?.pause() + return .complete() + })) + } + } else { + self.player?.volume = 0.0 + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + } + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 + self.player?.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) + } + + func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.player?.volume = 1.0 + self.play() + } + + func setSoundMuted(soundMuted: Bool) { + self.player?.volume = soundMuted ? 0.0 : 1.0 + } + + func continueWithOverridingAmbientMode(isAmbient: Bool) { + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + } + + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.player?.volume = 0.0 + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + } + + func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + } + + func setBaseRate(_ baseRate: Double) { + guard let player = self.player else { + return + } + self.baseRate = baseRate + if #available(iOS 16.0, *) { + player.defaultRate = Float(baseRate) + } + if player.rate != 0.0 { + player.rate = Float(baseRate) + } + self.updateStatus() + } + + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.preferredVideoQuality = videoQuality + + guard let currentItem = self.player?.currentItem else { + return + } + guard let playerSource = self.playerSource else { + return + } + + switch videoQuality { + case .auto: + currentItem.preferredPeakBitRate = 0.0 + case let .quality(qualityValue): + if let file = playerSource.qualityFiles[qualityValue] { + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + let bandwidth = Int(Double(size) / duration) * 8 + currentItem.preferredPeakBitRate = Double(bandwidth) + } + } + } + + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + guard let currentItem = self.player?.currentItem else { + return nil + } + guard let playerSource = self.playerSource else { + return nil + } + let current = Int(currentItem.presentationSize.height) + var available: [Int] = Array(playerSource.qualityFiles.keys) + available.sort(by: { $0 > $1 }) + return (current, self.preferredVideoQuality, available) + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } + + func notifyPlaybackControlsHidden(_ hidden: Bool) { + } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index 3cd8c3b4e3..77b46d4398 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -13,8 +13,167 @@ import PhotoResources import RangeSet import TelegramVoip import ManagedFile +import AppBundle + +public final class HLSQualitySet { + public let qualityFiles: [Int: FileMediaReference] + public let playlistFiles: [Int: FileMediaReference] + + public init?(baseFile: FileMediaReference) { + var qualityFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in baseFile.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + for attribute in alternativeFile.attributes { + if case let .Video(_, size, _, _, _, videoCodec) = attribute { + let _ = size + if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { + qualityFiles[Int(size.height)] = baseFile.withMedia(alternativeFile) + } + } + } + } + } + + var playlistFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in baseFile.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + if alternativeFile.mimeType == "application/x-mpegurl" { + if let fileName = alternativeFile.fileName { + if fileName.hasPrefix("mtproto:") { + let fileIdString = String(fileName[fileName.index(fileName.startIndex, offsetBy: "mtproto:".count)...]) + if let fileId = Int64(fileIdString) { + for (quality, file) in qualityFiles { + if file.media.fileId.id == fileId { + playlistFiles[quality] = baseFile.withMedia(alternativeFile) + break + } + } + } + } + } + } + } + } + if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys { + self.qualityFiles = qualityFiles + self.playlistFiles = playlistFiles + } else { + return nil + } + } +} public final class HLSVideoContent: UniversalVideoContent { + public static func minimizedHLSQuality(file: FileMediaReference) -> (playlist: FileMediaReference, file: FileMediaReference)? { + guard let qualitySet = HLSQualitySet(baseFile: file) else { + return nil + } + for (quality, qualityFile) in qualitySet.qualityFiles.sorted(by: { $0.key < $1.key }) { + if quality >= 400 { + guard let playlistFile = qualitySet.playlistFiles[quality] else { + return nil + } + return (playlistFile, qualityFile) + } + } + return nil + } + + public static func minimizedHLSQualityPreloadData(postbox: Postbox, file: FileMediaReference, userLocation: MediaResourceUserLocation, prefixSeconds: Int, autofetchPlaylist: Bool) -> Signal<(FileMediaReference, Range)?, NoError> { + guard let fileSet = minimizedHLSQuality(file: file) else { + return .single(nil) + } + + let playlistData: Signal?, NoError> = Signal { subscriber in + var fetchDisposable: Disposable? + if autofetchPlaylist { + fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: fileSet.playlist, resource: fileSet.playlist.media.resource).start() + } + let dataDisposable = postbox.mediaBox.resourceData(fileSet.playlist.media.resource).start(next: { data in + if !data.complete { + return + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + guard let playlistString = String(data: data, encoding: .utf8) else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + var durations: [Int] = [] + var byteRanges: [Range] = [] + + let extinfRegex = try! NSRegularExpression(pattern: "EXTINF:(\\d+)", options: []) + let byteRangeRegex = try! NSRegularExpression(pattern: "EXT-X-BYTERANGE:(\\d+)@(\\d+)", options: []) + + let extinfResults = extinfRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in extinfResults { + if let durationRange = Range(result.range(at: 1), in: playlistString) { + if let duration = Int(String(playlistString[durationRange])) { + durations.append(duration) + } + } + } + + let byteRangeResults = byteRangeRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in byteRangeResults { + if let lengthRange = Range(result.range(at: 1), in: playlistString), let upperBoundRange = Range(result.range(at: 2), in: playlistString) { + if let length = Int(String(playlistString[lengthRange])), let lowerBound = Int(String(playlistString[upperBoundRange])) { + byteRanges.append(lowerBound ..< (lowerBound + length)) + } + } + } + + if durations.count != byteRanges.count { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + var rangeUpperBound: Int64 = 0 + var remainingSeconds = prefixSeconds + + for i in 0 ..< durations.count { + if remainingSeconds <= 0 { + break + } + let duration = durations[i] + let byteRange = byteRanges[i] + + remainingSeconds -= duration + rangeUpperBound = max(rangeUpperBound, Int64(byteRange.upperBound)) + } + + if rangeUpperBound != 0 { + subscriber.putNext(0 ..< rangeUpperBound) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + + return + }) + + return ActionDisposable { + fetchDisposable?.dispose() + dataDisposable.dispose() + } + } + + return playlistData + |> map { range -> (FileMediaReference, Range)? in + guard let range else { + return nil + } + return (fileSet.file, range) + } + } + public let id: AnyHashable public let nativeId: PlatformVideoContentId let userLocation: MediaResourceUserLocation @@ -42,7 +201,11 @@ public final class HLSVideoContent: UniversalVideoContent { } public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return HLSVideoContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + if #available(iOS 17.1, *) { + return HLSVideoJSContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + } else { + return HLSVideoAVContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + } } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -59,740 +222,236 @@ public final class HLSVideoContent: UniversalVideoContent { } } -private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNode { - private final class HLSServerSource: SharedHLSServer.Source { - let id: String - let postbox: Postbox - let userLocation: MediaResourceUserLocation - let playlistFiles: [Int: FileMediaReference] - let qualityFiles: [Int: FileMediaReference] - - private var playlistFetchDisposables: [Int: Disposable] = [:] - - init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { - self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" - self.postbox = postbox - self.userLocation = userLocation - self.playlistFiles = playlistFiles - self.qualityFiles = qualityFiles - } - - deinit { - for (_, disposable) in self.playlistFetchDisposables { - disposable.dispose() - } - } - - func masterPlaylistData() -> Signal { - var playlistString: String = "" - playlistString.append("#EXTM3U\n") - - for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { - let width = file.media.dimensions?.width ?? 1280 - let height = file.media.dimensions?.height ?? 720 - - let bandwidth: Int - if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { - bandwidth = Int(Double(size) / duration) * 8 - } else { - bandwidth = 1000000 - } - - playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") - playlistString.append("hls_level_\(quality).m3u8\n") - } - return .single(playlistString) - } - - func playlistData(quality: Int) -> Signal { - guard let playlistFile = self.playlistFiles[quality] else { - return .never() - } - if self.playlistFetchDisposables[quality] == nil { - self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() - } - - return self.postbox.mediaBox.resourceData(playlistFile.media.resource) - |> filter { data in - return data.complete - } - |> map { data -> String in - guard data.complete else { - return "" - } - guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { - return "" - } - guard var playlistString = String(data: data, encoding: .utf8) else { - return "" - } - let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) - let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) - for result in results.reversed() { - if let range = Range(result.range, in: playlistString) { - if let fileIdRange = Range(result.range(at: 1), in: playlistString) { - let fileId = String(playlistString[fileIdRange]) - playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") - } - } - } - return playlistString - } - } - - func partData(index: Int, quality: Int) -> Signal { - return .never() - } - - func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { - guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { - return .single(nil) - } - let _ = quality - guard let size = file.media.size else { - return .single(nil) - } - - let postbox = self.postbox - let userLocation = self.userLocation - - let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) - - let queue = postbox.mediaBox.dataQueue - return Signal<(TempBoxFile, Range, Int)?, NoError> { subscriber in - guard let fetchResource = postbox.mediaBox.fetchResource else { - return EmptyDisposable - } - - let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) - let params = MediaResourceFetchParameters( - tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), - info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), - location: location, - contentType: .video, - isRandomAccessAllowed: true - ) - - let completeFile = TempBox.shared.tempFile(fileName: "data") - let partialFile = TempBox.shared.tempFile(fileName: "data") - let metaFile = TempBox.shared.tempFile(fileName: "data") - - guard let fileContext = MediaBoxFileContextV2Impl( - queue: queue, - manager: postbox.mediaBox.dataFileManager, - storageBox: nil, - resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, - path: completeFile.path, - partialPath: partialFile.path, - metaPath: metaFile.path - ) else { - return EmptyDisposable - } - - let fetchDisposable = fileContext.fetched( - range: mappedRange, - priority: .default, - fetch: { intervals in - return fetchResource(file.media.resource, intervals, params) - }, - error: { _ in - }, - completed: { - } - ) - - #if DEBUG - let startTime = CFAbsoluteTimeGetCurrent() - #endif - - let dataDisposable = fileContext.data( - range: mappedRange, - waitUntilAfterInitialFetch: true, - next: { result in - if result.complete { - #if DEBUG - let fetchTime = CFAbsoluteTimeGetCurrent() - startTime - print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") - #endif - subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) - subscriber.putCompletion() - } - } - ) - - return ActionDisposable { - queue.async { - fetchDisposable.dispose() - dataDisposable.dispose() - fileContext.cancelFullRangeFetches() - - TempBox.shared.dispose(completeFile) - TempBox.shared.dispose(metaFile) - } - } - } - |> runOn(queue) - } - } +final class HLSServerSource: SharedHLSServer.Source { + let id: String + let postbox: Postbox + let userLocation: MediaResourceUserLocation + let playlistFiles: [Int: FileMediaReference] + let qualityFiles: [Int: FileMediaReference] - private let postbox: Postbox - private let userLocation: MediaResourceUserLocation - private let fileReference: FileMediaReference - private let approximateDuration: Double - private let intrinsicDimensions: CGSize - - private let audioSessionManager: ManagedAudioSession - private let audioSessionDisposable = MetaDisposable() - private var hasAudioSession = false + private var playlistFetchDisposables: [Int: Disposable] = [:] - private let playbackCompletedListeners = Bag<() -> Void>() - - private var initializedStatus = false - private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) - private var baseRate: Double = 1.0 - private var isBuffering = false - private var seekId: Int = 0 - private let _status = ValuePromise() - var status: Signal { - return self._status.get() - } - - private let _bufferingStatus = Promise<(RangeSet, Int64)?>() - var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { - return self._bufferingStatus.get() - } - - private let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - private let _preloadCompleted = ValuePromise() - var preloadCompleted: Signal { - return self._preloadCompleted.get() - } - - private var playerSource: HLSServerSource? - private var serverDisposable: Disposable? - - private let imageNode: TransformImageNode - - private var playerItem: AVPlayerItem? - private var player: AVPlayer? - private let playerNode: ASDisplayNode - - private var loadProgressDisposable: Disposable? - private var statusDisposable: Disposable? - - private var didPlayToEndTimeObserver: NSObjectProtocol? - private var didBecomeActiveObserver: NSObjectProtocol? - private var willResignActiveObserver: NSObjectProtocol? - private var failureObserverId: NSObjectProtocol? - private var errorObserverId: NSObjectProtocol? - private var playerItemFailedToPlayToEndTimeObserver: NSObjectProtocol? - - private let fetchDisposable = MetaDisposable() - - private var dimensions: CGSize? - private let dimensionsPromise = ValuePromise(CGSize()) - - private var validLayout: CGSize? - - private var statusTimer: Foundation.Timer? - - private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto - - init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" self.postbox = postbox - self.fileReference = fileReference - self.approximateDuration = fileReference.media.duration ?? 0.0 - self.audioSessionManager = audioSessionManager self.userLocation = userLocation - self.baseRate = baseRate - - if var dimensions = fileReference.media.dimensions { - if let thumbnail = fileReference.media.previewRepresentations.first { - let dimensionsVertical = dimensions.width < dimensions.height - let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height - if dimensionsVertical != thumbnailVertical { - dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width) - } - } - self.dimensions = dimensions.cgSize - } else { - self.dimensions = CGSize(width: 128.0, height: 128.0) - } - - self.imageNode = TransformImageNode() - - var player: AVPlayer? - player = AVPlayer(playerItem: nil) - self.player = player - if #available(iOS 16.0, *) { - player?.defaultRate = Float(baseRate) - } - if !enableSound { - player?.volume = 0.0 - } - - self.playerNode = ASDisplayNode() - self.playerNode.setLayerBlock({ - return AVPlayerLayer(player: player) - }) - - self.intrinsicDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) - - self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) - - var qualityFiles: [Int: FileMediaReference] = [:] - for alternativeRepresentation in fileReference.media.alternativeRepresentations { - if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { - for attribute in alternativeFile.attributes { - if case let .Video(_, size, _, _, _, videoCodec) = attribute { - let _ = size - if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { - qualityFiles[Int(size.height)] = fileReference.withMedia(alternativeFile) - } - } - } - } - } - /*for key in Array(qualityFiles.keys) { - if key != 144 && key != 720 { - qualityFiles.removeValue(forKey: key) - } - }*/ - var playlistFiles: [Int: FileMediaReference] = [:] - for alternativeRepresentation in fileReference.media.alternativeRepresentations { - if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { - if alternativeFile.mimeType == "application/x-mpegurl" { - if let fileName = alternativeFile.fileName { - if fileName.hasPrefix("mtproto:") { - let fileIdString = String(fileName[fileName.index(fileName.startIndex, offsetBy: "mtproto:".count)...]) - if let fileId = Int64(fileIdString) { - for (quality, file) in qualityFiles { - if file.media.fileId.id == fileId { - playlistFiles[quality] = fileReference.withMedia(alternativeFile) - break - } - } - } - } - } - } - } - } - if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys { - self.playerSource = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: playlistFiles, qualityFiles: qualityFiles) - } - - super.init() - - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in - Queue.mainQueue().async { - if let strongSelf = self, strongSelf.dimensions == nil { - if let dimensions = getSize() { - strongSelf.dimensions = dimensions - strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) - } - } - } - } - return getData - }) - - self.addSubnode(self.imageNode) - self.addSubnode(self.playerNode) - self.player?.actionAtItemEnd = .pause - - self.imageNode.imageUpdated = { [weak self] _ in - self?._ready.set(.single(Void())) - } - - self.player?.addObserver(self, forKeyPath: "rate", options: [], context: nil) - - self._bufferingStatus.set(.single(nil)) - - if let playerSource = self.playerSource { - self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in - Queue.mainQueue().async { - guard let self else { - return - } - - let playerItem: AVPlayerItem - let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8" - #if DEBUG - print("HLSVideoContentNode: playing \(assetUrl)") - #endif - playerItem = AVPlayerItem(url: URL(string: assetUrl)!) - - if #available(iOS 14.0, *) { - playerItem.startsOnFirstEligibleVariant = true - } - - self.setPlayerItem(playerItem) - } - }) - } - - self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in - guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { - return - } - layer.player = strongSelf.player - }) - self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in - guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { - return - } - layer.player = nil - }) + self.playlistFiles = playlistFiles + self.qualityFiles = qualityFiles } deinit { - self.player?.removeObserver(self, forKeyPath: "rate") - - self.setPlayerItem(nil) - - self.audioSessionDisposable.dispose() - - self.loadProgressDisposable?.dispose() - self.statusDisposable?.dispose() - - if let didBecomeActiveObserver = self.didBecomeActiveObserver { - NotificationCenter.default.removeObserver(didBecomeActiveObserver) + for (_, disposable) in self.playlistFetchDisposables { + disposable.dispose() } - if let willResignActiveObserver = self.willResignActiveObserver { - NotificationCenter.default.removeObserver(willResignActiveObserver) - } - - if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { - NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) - } - if let failureObserverId = self.failureObserverId { - NotificationCenter.default.removeObserver(failureObserverId) - } - if let errorObserverId = self.errorObserverId { - NotificationCenter.default.removeObserver(errorObserverId) - } - - self.serverDisposable?.dispose() - - self.statusTimer?.invalidate() } - private func setPlayerItem(_ item: AVPlayerItem?) { - if let playerItem = self.playerItem { - playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") - playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") - playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") - playerItem.removeObserver(self, forKeyPath: "status") - playerItem.removeObserver(self, forKeyPath: "presentationSize") - } - - if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver { - self.playerItemFailedToPlayToEndTimeObserver = nil - NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver) - } - - if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { - self.didPlayToEndTimeObserver = nil - NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) - } - if let failureObserverId = self.failureObserverId { - self.failureObserverId = nil - NotificationCenter.default.removeObserver(failureObserverId) - } - if let errorObserverId = self.errorObserverId { - self.errorObserverId = nil - NotificationCenter.default.removeObserver(errorObserverId) - } - - self.playerItem = item - - if let item { - self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil, using: { [weak self] notification in - self?.performActionAtEnd() - }) - - self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: item, queue: .main, using: { notification in -#if DEBUG - print("Player Error: \(notification.description)") -#endif - }) - self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: item, queue: .main, using: { [weak item] notification in - if let item { - let event = item.errorLog()?.events.last - if let event { - let _ = event -#if DEBUG - print("Player Error: \(event.errorComment ?? "")") -#endif - } + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return Signal { subscriber in + if path == "index.html" { + if let path = getAppBundle().path(forResource: "HLSVideoPlayer", ofType: "html"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + subscriber.putNext((data, "text/html")) + } else { + subscriber.putNext(nil) } - }) - item.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil) - } - - if let playerItem = self.playerItem { - playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) - playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) - playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) - playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) - self.playerItemFailedToPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { [weak self] _ in - guard let self else { - return + } else if path == "hls.js" { + if let path = getAppBundle().path(forResource: "hls", ofType: "js"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + subscriber.putNext((data, "application/javascript")) + } else { + subscriber.putNext(nil) } - let _ = self - }) - } - - self.player?.replaceCurrentItem(with: self.playerItem) - } - - private func updateStatus() { - guard let player = self.player else { - return - } - let isPlaying = !player.rate.isZero - let status: MediaPlayerPlaybackStatus - if self.isBuffering { - status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) - } else { - status = isPlaying ? .playing : .paused - } - var timestamp = player.currentTime().seconds - if timestamp.isFinite && !timestamp.isNaN { - } else { - timestamp = 0.0 - } - self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, seekId: self.seekId, status: status, soundEnabled: true) - self._status.set(self.statusValue) - - if case .playing = status { - if self.statusTimer == nil { - self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - self.updateStatus() - }) - } - } else if let statusTimer = self.statusTimer { - self.statusTimer = nil - statusTimer.invalidate() - } - } - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == "rate" { - if let player = self.player { - let isPlaying = !player.rate.isZero - if isPlaying { - self.isBuffering = false - } - } - self.updateStatus() - } else if keyPath == "playbackBufferEmpty" { - self.isBuffering = true - self.updateStatus() - } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { - self.isBuffering = false - self.updateStatus() - } else if keyPath == "presentationSize" { - if let currentItem = self.player?.currentItem { - print("Presentation size: \(Int(currentItem.presentationSize.height))") - } - } - } - - private func performActionAtEnd() { - for listener in self.playbackCompletedListeners.copyItems() { - listener() - } - } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) - transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) - - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) - - if let dimensions = self.dimensions { - let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) - let makeLayout = self.imageNode.asyncLayout() - let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) - applyLayout() - } - } - - func play() { - assert(Queue.mainQueue().isCurrent()) - if !self.initializedStatus { - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) - } - if !self.hasAudioSession { - if self.player?.volume != 0.0 { - self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in - guard let self else { - return - } - self.hasAudioSession = true - self.player?.play() - }, deactivate: { [weak self] _ in - guard let self else { - return .complete() - } - self.hasAudioSession = false - self.player?.pause() - - return .complete() - })) } else { - self.player?.play() + subscriber.putNext(nil) } - } else { - self.player?.play() + + subscriber.putCompletion() + + return EmptyDisposable } } - func pause() { - assert(Queue.mainQueue().isCurrent()) - self.player?.pause() - } - - func togglePlayPause() { - assert(Queue.mainQueue().isCurrent()) + func masterPlaylistData() -> Signal { + var playlistString: String = "" + playlistString.append("#EXTM3U\n") - guard let player = self.player else { - return - } - - if player.rate.isZero { - self.play() - } else { - self.pause() - } - } - - func setSoundEnabled(_ value: Bool) { - assert(Queue.mainQueue().isCurrent()) - if value { - if !self.hasAudioSession { - self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in - self?.hasAudioSession = true - self?.player?.volume = 1.0 - }, deactivate: { [weak self] _ in - self?.hasAudioSession = false - self?.player?.pause() - return .complete() - })) + for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { + let width = file.media.dimensions?.width ?? 1280 + let height = file.media.dimensions?.height ?? 720 + + let bandwidth: Int + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + bandwidth = Int(Double(size) / duration) * 8 + } else { + bandwidth = 1000000 } - } else { - self.player?.volume = 0.0 - self.hasAudioSession = false - self.audioSessionDisposable.set(nil) + + playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") + playlistString.append("hls_level_\(quality).m3u8\n") } + return .single(playlistString) } - func seek(_ timestamp: Double) { - assert(Queue.mainQueue().isCurrent()) - self.seekId += 1 - self.player?.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) - } - - func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { - self.player?.volume = 1.0 - self.play() - } - - func setSoundMuted(soundMuted: Bool) { - self.player?.volume = soundMuted ? 0.0 : 1.0 - } - - func continueWithOverridingAmbientMode(isAmbient: Bool) { - } - - func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { - } - - func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { - self.player?.volume = 0.0 - self.hasAudioSession = false - self.audioSessionDisposable.set(nil) - } - - func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { - } - - func setBaseRate(_ baseRate: Double) { - guard let player = self.player else { - return + func playlistData(quality: Int) -> Signal { + guard let playlistFile = self.playlistFiles[quality] else { + return .never() } - self.baseRate = baseRate - if #available(iOS 16.0, *) { - player.defaultRate = Float(baseRate) - } - if player.rate != 0.0 { - player.rate = Float(baseRate) - } - self.updateStatus() - } - - func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { - self.preferredVideoQuality = videoQuality - - guard let currentItem = self.player?.currentItem else { - return - } - guard let playerSource = self.playerSource else { - return + if self.playlistFetchDisposables[quality] == nil { + self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() } - switch videoQuality { - case .auto: - currentItem.preferredPeakBitRate = 0.0 - case let .quality(qualityValue): - if let file = playerSource.qualityFiles[qualityValue] { - if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { - let bandwidth = Int(Double(size) / duration) * 8 - currentItem.preferredPeakBitRate = Double(bandwidth) + return self.postbox.mediaBox.resourceData(playlistFile.media.resource) + |> filter { data in + return data.complete + } + |> map { data -> String in + guard data.complete else { + return "" + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return "" + } + guard var playlistString = String(data: data, encoding: .utf8) else { + return "" + } + let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) + let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in results.reversed() { + if let range = Range(result.range, in: playlistString) { + if let fileIdRange = Range(result.range(at: 1), in: playlistString) { + let fileId = String(playlistString[fileIdRange]) + playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") + } + } + } + return playlistString + } + } + + func partData(index: Int, quality: Int) -> Signal { + return .never() + } + + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { + return .single(nil) + } + let _ = quality + guard let size = file.media.size else { + return .single(nil) + } + + let postbox = self.postbox + let userLocation = self.userLocation + + let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) + + let queue = postbox.mediaBox.dataQueue + let fetchFromRemote: Signal<(TempBoxFile, Range, Int)?, NoError> = Signal { subscriber in + let partialFile = TempBox.shared.tempFile(fileName: "data") + + if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) { + #if DEBUG + print("Fetched \(quality)p part from cache") + #endif + + let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite) + if let outputFile { + let blockSize = 128 * 1024 + var tempBuffer = Data(count: blockSize) + var blockOffset = 0 + while blockOffset < cachedData.length { + let currentBlockSize = min(cachedData.length - blockOffset, blockSize) + + tempBuffer.withUnsafeMutableBytes { bytes -> Void in + let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize) + let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize) + } + + blockOffset += blockSize + } + outputFile._unsafeClose() + subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size))) + subscriber.putCompletion() + } else { + #if DEBUG + print("Error writing cached file to disk") + #endif + } + + return EmptyDisposable + } + + guard let fetchResource = postbox.mediaBox.fetchResource else { + return EmptyDisposable + } + + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) + let params = MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), + info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), + location: location, + contentType: .video, + isRandomAccessAllowed: true + ) + + let completeFile = TempBox.shared.tempFile(fileName: "data") + let metaFile = TempBox.shared.tempFile(fileName: "data") + + guard let fileContext = MediaBoxFileContextV2Impl( + queue: queue, + manager: postbox.mediaBox.dataFileManager, + storageBox: nil, + resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, + path: completeFile.path, + partialPath: partialFile.path, + metaPath: metaFile.path + ) else { + return EmptyDisposable + } + + let fetchDisposable = fileContext.fetched( + range: mappedRange, + priority: .default, + fetch: { intervals in + return fetchResource(file.media.resource, intervals, params) + }, + error: { _ in + }, + completed: { + } + ) + + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + + let dataDisposable = fileContext.data( + range: mappedRange, + waitUntilAfterInitialFetch: true, + next: { result in + if result.complete { + #if DEBUG + let fetchTime = CFAbsoluteTimeGetCurrent() - startTime + print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") + #endif + subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) + subscriber.putCompletion() + } + } + ) + + return ActionDisposable { + queue.async { + fetchDisposable.dispose() + dataDisposable.dispose() + fileContext.cancelFullRangeFetches() + + TempBox.shared.dispose(completeFile) + TempBox.shared.dispose(metaFile) } } } + |> runOn(queue) - } - - func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { - guard let currentItem = self.player?.currentItem else { - return nil - } - guard let playerSource = self.playerSource else { - return nil - } - let current = Int(currentItem.presentationSize.height) - var available: [Int] = Array(playerSource.qualityFiles.keys) - available.sort(by: { $0 > $1 }) - return (current, self.preferredVideoQuality, available) - } - - func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { - return self.playbackCompletedListeners.add(f) - } - - func removePlaybackCompleted(_ index: Int) { - self.playbackCompletedListeners.remove(index) - } - - func fetchControl(_ control: UniversalVideoNodeFetchControl) { - } - - func notifyPlaybackControlsHidden(_ hidden: Bool) { - } - - func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + return fetchFromRemote } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift new file mode 100644 index 0000000000..9e82785f4b --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift @@ -0,0 +1,777 @@ +import Foundation +import SwiftSignalKit +import UniversalMediaPlayer +import Postbox +import TelegramCore +import WebKit +import AsyncDisplayKit +import AccountContext +import TelegramAudio +import Display +import PhotoResources +import TelegramVoip +import RangeSet + +private func parseRange(from rangeString: String) -> Range? { + guard rangeString.hasPrefix("bytes=") else { + return nil + } + + let rangeValues = rangeString.dropFirst("bytes=".count).split(separator: "-") + + guard rangeValues.count == 2, + let start = Int(rangeValues[0]), + let end = Int(rangeValues[1]) else { + return nil + } + return start..(value: false) + var disposable: Disposable? + + init(source: HLSServerSource, sourceTask: any WKURLSchemeTask) { + self.sourceTask = sourceTask + + var requestRange: Range? + if let rangeString = sourceTask.request.allHTTPHeaderFields?["Range"] { + requestRange = parseRange(from: rangeString) + } + + guard let url = sourceTask.request.url else { + return + } + let filePath = (url.absoluteString as NSString).lastPathComponent + + if filePath == "master.m3u8" { + self.disposable = source.masterPlaylistData().startStrict(next: { [weak self] data in + guard let self else { + return + } + self.sendResponseAndClose(data: data.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") { + guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else { + self.sendErrorAndClose() + return + } + + self.disposable = source.playlistData(quality: levelIndex).startStrict(next: { [weak self] data in + guard let self else { + return + } + self.sendResponseAndClose(data: data.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)]) + guard let fileIdValue = Int64(fileId) else { + self.sendErrorAndClose() + return + } + guard let requestRange else { + self.sendErrorAndClose() + return + } + self.disposable = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let (file, range, totalSize) = result { + guard let allData = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe) else { + return + } + let data = allData.subdata(in: range) + + self.sendResponseAndClose(data: data, range: requestRange, totalSize: totalSize) + } else { + self.sendErrorAndClose() + } + }) + } else { + self.sendErrorAndClose() + } + } + + deinit { + self.disposable?.dispose() + } + + func cancel() { + } + + func sendErrorAndClose() { + self.sourceTask.didFailWithError(NSError(domain: "LocalVideoError", code: 500, userInfo: nil)) + } + + private func sendResponseAndClose(data: Data, range: Range? = nil, totalSize: Int? = nil) { + // Create the response with the appropriate content-type and content-length + //let mimeType = "application/octet-stream" + let responseLength = data.count + + // Construct URLResponse with optional range headers (for partial content responses) + var headers: [String: String] = [ + "Content-Length": "\(responseLength)", + "Connection": "close", + "Access-Control-Allow-Origin": "*" + ] + + if let range = range, let totalSize = totalSize { + headers["Content-Range"] = "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)" + } + + // Create the URLResponse object + let response = HTTPURLResponse(url: self.sourceTask.request.url!, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: headers) + + // Send the response headers + self.sourceTask.didReceive(response!) + + // Send the response data + self.sourceTask.didReceive(data) + + // Complete the task + self.sourceTask.didFinish() + } + } + + private let source: HLSServerSource + private var pendingTasks: [PendingTask] = [] + + init(source: HLSServerSource) { + self.source = source + } + + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { + self.pendingTasks.append(PendingTask(source: self.source, sourceTask: urlSchemeTask)) + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + if let index = self.pendingTasks.firstIndex(where: { $0.sourceTask === urlSchemeTask }) { + let task = self.pendingTasks[index] + self.pendingTasks.remove(at: index) + task.cancel() + } + } +} + +private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + +final class HLSVideoJSContentNode: ASDisplayNode, UniversalVideoContentNode { + private struct Level { + let bitrate: Int + let width: Int + let height: Int + + init(bitrate: Int, width: Int, height: Int) { + self.bitrate = bitrate + self.width = width + self.height = height + } + } + + private static var sharedBandwidthEstimate: Double? + + private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let fileReference: FileMediaReference + private let approximateDuration: Double + private let intrinsicDimensions: CGSize + + private let audioSessionManager: ManagedAudioSession + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + private let playerSource: HLSServerSource? + private var serverDisposable: Disposable? + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + private var isBuffering = false + private var seekId: Int = 0 + private let _status = ValuePromise() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(RangeSet, Int64)?>() + var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { + return self._bufferingStatus.get() + } + + private let _isNativePictureInPictureActive = ValuePromise(false, ignoreRepeated: true) + var isNativePictureInPictureActive: Signal { + return self._isNativePictureInPictureActive.get() + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private let imageNode: TransformImageNode + private let webView: WKWebView + + private let fetchDisposable = MetaDisposable() + + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: (size: CGSize, actualSize: CGSize)? + + private var statusTimer: Foundation.Timer? + + private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto + + private var playerIsReady: Bool = false + private var playerIsFirstFrameReady: Bool = false + private var playerIsPlaying: Bool = false + private var playerRate: Double = 0.0 + private var playerDefaultRate: Double = 1.0 + private var playerTime: Double = 0.0 + private var playerTimeGenerationTimestamp: Double = 0.0 + private var playerAvailableLevels: [Int: Level] = [:] + private var playerCurrentLevelIndex: Int? + + private var hasRequestedPlayerLoad: Bool = false + + private var requestedPlaying: Bool = false + private var requestedBaseRate: Double = 1.0 + private var requestedLevelIndex: Int? + + init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + self.postbox = postbox + self.fileReference = fileReference + self.approximateDuration = fileReference.media.duration ?? 0.0 + self.audioSessionManager = audioSessionManager + self.userLocation = userLocation + self.requestedBaseRate = baseRate + + /*#if DEBUG + if let minimizedQualityFile = HLSVideoContent.minimizedHLSQualityFile(file: self.fileReference) { + let _ = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: minimizedQualityFile.resourceReference(minimizedQualityFile.media.resource), range: (0 ..< 5 * 1024 * 1024, .default)).startStandalone() + } + #endif*/ + + if var dimensions = fileReference.media.dimensions { + if let thumbnail = fileReference.media.previewRepresentations.first { + let dimensionsVertical = dimensions.width < dimensions.height + let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height + if dimensionsVertical != thumbnailVertical { + dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width) + } + } + self.dimensions = dimensions.cgSize + } else { + self.dimensions = CGSize(width: 128.0, height: 128.0) + } + + self.imageNode = TransformImageNode() + + let config = WKWebViewConfiguration() + config.allowsInlineMediaPlayback = true + config.mediaTypesRequiringUserActionForPlayback = [] + config.allowsPictureInPictureMediaPlayback = true + + var playerSource: HLSServerSource? + if let qualitySet = HLSQualitySet(baseFile: fileReference) { + let playerSourceValue = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) + playerSource = playerSourceValue + let schemeHandler = CustomVideoSchemeHandler(source: playerSourceValue) + config.setURLSchemeHandler(schemeHandler, forURLScheme: "tghls") + } + self.playerSource = playerSource + + let userController = WKUserContentController() + + var handleScriptMessage: ((WKScriptMessage) -> Void)? + userController.add(WeakScriptMessageHandler { message in + handleScriptMessage?(message) + }, name: "performAction") + + let isDebug: Bool + #if DEBUG + isDebug = true + #else + isDebug = false + #endif + + let mediaDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) + var intrinsicDimensions = mediaDimensions.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) + + let userScriptJs = """ + playerInitialize({ + 'debug': \(isDebug), + 'width': \(Int(intrinsicDimensions.width)), + 'height': \(Int(intrinsicDimensions.height)), + 'bandwidthEstimate': \(HLSVideoJSContentNode.sharedBandwidthEstimate ?? 500000.0) + }); + """; + let userScript = WKUserScript(source: userScriptJs, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + userController.addUserScript(userScript) + + config.userContentController = userController + + intrinsicDimensions.width = floor(intrinsicDimensions.width / UIScreenScale) + intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale) + self.intrinsicDimensions = intrinsicDimensions + + self.webView = WKWebView(frame: CGRect(origin: CGPoint(), size: self.intrinsicDimensions), configuration: config) + self.webView.scrollView.isScrollEnabled = false + self.webView.allowsLinkPreview = false + self.webView.allowsBackForwardNavigationGestures = false + self.webView.accessibilityIgnoresInvertColors = true + self.webView.scrollView.contentInsetAdjustmentBehavior = .never + self.webView.alpha = 0.0 + + if #available(iOS 16.4, *) { + #if DEBUG + self.webView.isInspectable = true + #endif + } + + super.init() + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) + } + } + } + } + return getData + }) + + self.addSubnode(self.imageNode) + self.view.addSubview(self.webView) + + self.imageNode.imageUpdated = { [weak self] _ in + self?._ready.set(.single(Void())) + } + + self._bufferingStatus.set(.single(nil)) + + handleScriptMessage = { [weak self] message in + Queue.mainQueue().async { + guard let self else { + return + } + guard let body = message.body as? [String: Any] else { + return + } + guard let eventName = body["event"] as? String else { + return + } + + switch eventName { + case "playerStatus": + guard let eventData = body["data"] as? [String: Any] else { + return + } + if let isReady = eventData["isReady"] as? Bool { + self.playerIsReady = isReady + } else { + self.playerIsReady = false + } + if let isFirstFrameReady = eventData["isFirstFrameReady"] as? Bool { + self.playerIsFirstFrameReady = isFirstFrameReady + } else { + self.playerIsFirstFrameReady = false + } + if let isPlaying = eventData["isPlaying"] as? Bool { + self.playerIsPlaying = isPlaying + } else { + self.playerIsPlaying = false + } + if let rate = eventData["rate"] as? Double { + self.playerRate = rate + } else { + self.playerRate = 0.0 + } + if let defaultRate = eventData["defaultRate"] as? Double { + self.playerDefaultRate = defaultRate + } else { + self.playerDefaultRate = 0.0 + } + if let levels = eventData["levels"] as? [[String: Any]] { + self.playerAvailableLevels.removeAll() + + for level in levels { + guard let levelIndex = level["index"] as? Int else { + continue + } + guard let levelBitrate = level["bitrate"] as? Int else { + continue + } + guard let levelWidth = level["width"] as? Int else { + continue + } + guard let levelHeight = level["height"] as? Int else { + continue + } + self.playerAvailableLevels[levelIndex] = Level( + bitrate: levelBitrate, + width: levelWidth, + height: levelHeight + ) + } + } else { + self.playerAvailableLevels.removeAll() + } + + self._isNativePictureInPictureActive.set(eventData["isPictureInPictureActive"] as? Bool ?? false) + + if let currentLevel = eventData["currentLevel"] as? Int { + if self.playerAvailableLevels[currentLevel] != nil { + self.playerCurrentLevelIndex = currentLevel + } else { + self.playerCurrentLevelIndex = nil + } + } else { + self.playerCurrentLevelIndex = nil + } + + self.webView.alpha = self.playerIsFirstFrameReady ? 1.0 : 0.0 + if self.playerIsReady { + if !self.hasRequestedPlayerLoad { + if !self.playerAvailableLevels.isEmpty { + var selectedLevelIndex: Int? + if let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file { + if let dimensions = minimizedQualityFile.media.dimensions { + for (index, level) in self.playerAvailableLevels { + if level.height == Int(dimensions.height) { + selectedLevelIndex = index + break + } + } + } + } + if selectedLevelIndex == nil { + selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key + } + if let selectedLevelIndex { + self.hasRequestedPlayerLoad = true + self.webView.evaluateJavaScript("playerLoad(\(selectedLevelIndex));", completionHandler: nil) + } + } + } + + self.webView.evaluateJavaScript("playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil) + + if self.requestedPlaying { + self.requestPlay() + } else { + self.requestPause() + } + } + + self.updateStatus() + case "playerCurrentTime": + guard let eventData = body["data"] as? [String: Any] else { + return + } + guard let value = eventData["value"] as? Double else { + return + } + + self.playerTime = value + self.playerTimeGenerationTimestamp = CACurrentMediaTime() + + var bandwidthEstimate = eventData["bandwidthEstimate"] as? Double + if let bandwidthEstimateValue = bandwidthEstimate, bandwidthEstimateValue.isNaN || bandwidthEstimateValue.isInfinite { + bandwidthEstimate = nil + } + + HLSVideoJSContentNode.sharedBandwidthEstimate = bandwidthEstimate + + self.updateStatus() + default: + break + } + } + } + + if let playerSource = self.playerSource { + self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + + let htmlUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/index.html" + self.webView.load(URLRequest(url: URL(string: htmlUrl)!)) + } + }) + } + } + + deinit { + self.serverDisposable?.dispose() + self.audioSessionDisposable.dispose() + + self.statusTimer?.invalidate() + } + + private func updateStatus() { + let isPlaying = self.requestedPlaying && self.playerRate != 0.0 + let status: MediaPlayerPlaybackStatus + if self.requestedPlaying && !isPlaying { + status = .buffering(initial: false, whilePlaying: self.requestedPlaying, progress: 0.0, display: true) + } else { + status = self.requestedPlaying ? .playing : .paused + } + var timestamp = self.playerTime + if timestamp.isFinite && !timestamp.isNaN { + } else { + timestamp = 0.0 + } + self.statusValue = MediaPlayerStatus(generationTimestamp: self.playerTimeGenerationTimestamp, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.requestedBaseRate, seekId: self.seekId, status: status, soundEnabled: true) + self._status.set(self.statusValue) + + if case .playing = status { + if self.statusTimer == nil { + self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateStatus() + }) + } + } else if let statusTimer = self.statusTimer { + self.statusTimer = nil + statusTimer.invalidate() + } + } + + private func performActionAtEnd() { + for listener in self.playbackCompletedListeners.copyItems() { + listener() + } + } + + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + transition.updatePosition(layer: self.webView.layer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(layer: self.webView.layer, scale: size.width / self.intrinsicDimensions.width) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + + if let dimensions = self.dimensions { + let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + } + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) + } + /*if !self.hasAudioSession { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + Queue.mainQueue().async { + guard let self else { + return + } + self.hasAudioSession = true + self.requestPlay() + } + }, deactivate: { [weak self] _ in + return Signal { subscriber in + if let self { + self.hasAudioSession = false + self.requestPause() + } + + subscriber.putCompletion() + + return EmptyDisposable + } + |> runOn(.mainQueue()) + })) + } else*/ do { + self.requestPlay() + } + } + + private func requestPlay() { + self.requestedPlaying = true + if self.playerIsReady { + self.webView.evaluateJavaScript("playerPlay();", completionHandler: nil) + } + self.updateStatus() + } + + private func requestPause() { + self.requestedPlaying = false + if self.playerIsReady { + self.webView.evaluateJavaScript("playerPause();", completionHandler: nil) + } + self.updateStatus() + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.requestPause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + + if self.requestedPlaying { + self.pause() + } else { + self.play() + } + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + /*if value { + if !self.hasAudioSession { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + self?.hasAudioSession = true + self?.player?.volume = 1.0 + }, deactivate: { [weak self] _ in + self?.hasAudioSession = false + self?.player?.pause() + return .complete() + })) + } + } else { + self.player?.volume = 0.0 + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + }*/ + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 + + self.webView.evaluateJavaScript("playerSeek(\(timestamp));", completionHandler: nil) + } + + func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.webView.evaluateJavaScript("playerSetIsMuted(false);", completionHandler: nil) + + self.play() + } + + func setSoundMuted(soundMuted: Bool) { + self.webView.evaluateJavaScript("playerSetIsMuted(\(soundMuted));", completionHandler: nil) + } + + func continueWithOverridingAmbientMode(isAmbient: Bool) { + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + } + + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.webView.evaluateJavaScript("playerSetIsMuted(true);", completionHandler: nil) + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + } + + func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + } + + func setBaseRate(_ baseRate: Double) { + self.requestedBaseRate = baseRate + if self.playerIsReady { + self.webView.evaluateJavaScript("playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil) + } + self.updateStatus() + } + + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.preferredVideoQuality = videoQuality + + switch videoQuality { + case .auto: + self.requestedLevelIndex = nil + case let .quality(quality): + if let level = self.playerAvailableLevels.first(where: { $0.value.height == quality }) { + self.requestedLevelIndex = level.key + } else { + self.requestedLevelIndex = nil + } + } + + if self.playerIsReady { + self.webView.evaluateJavaScript("playerSetLevel(\(self.requestedLevelIndex ?? -1));", completionHandler: nil) + } + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else { + return nil + } + guard let currentLevel = self.playerAvailableLevels[playerCurrentLevelIndex] else { + return nil + } + + var available = self.playerAvailableLevels.values.map(\.height) + available.sort(by: { $0 > $1 }) + + return (currentLevel.height, self.preferredVideoQuality, available) + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } + + func notifyPlaybackControlsHidden(_ hidden: Bool) { + } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } + + func enterNativePictureInPicture() -> Bool { + self.webView.evaluateJavaScript("playerRequestPictureInPicture();", completionHandler: nil) + return true + } + + func exitNativePictureInPicture() { + self.webView.evaluateJavaScript("playerStopPictureInPicture();", completionHandler: nil) + } +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index e7fd264a92..07909d446f 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -206,6 +206,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -217,7 +221,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var dimensions: CGSize? private let dimensionsPromise = ValuePromise(CGSize()) - private var validLayout: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? private var shouldPlay: Bool = false @@ -306,8 +310,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent if let dimensions = getSize() { strongSelf.dimensions = dimensions strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -432,8 +436,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) if let dimensions = self.dimensions { let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) @@ -685,4 +689,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy) } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift index 0f5b97e1b9..975b534476 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift @@ -47,7 +47,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { private let statusDisposable = MetaDisposable() - private var validLayoutSize: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(contentDimensions: CGSize, unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void, controlsAreShowingUpdated: @escaping (Bool) -> Void) { self.contentDimensions = contentDimensions @@ -106,9 +106,9 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = self.frameForContent(size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let validLayout = self.validLayout { + contentNode.frame = self.frameForContent(size: validLayout.size) + contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -118,8 +118,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { func updateContentNodeSnapshot(_ snapshot: UIView?) { } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayoutSize = size + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, actualSize) let contentFrame = self.frameForContent(size: size) @@ -146,7 +146,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: contentFrame) - contentNode.updateLayout(size: contentFrame.size, transition: transition) + contentNode.updateLayout(size: contentFrame.size, actualSize: contentFrame.size, transition: transition) } } @@ -209,8 +209,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if self.minimizedBlurView == nil { let minimizedBlurView = UIVisualEffectView(effect: nil) self.minimizedBlurView = minimizedBlurView - if let validLayoutSize = self.validLayoutSize { - minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + if let validLayout = self.validLayout { + minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayout.size) } minimizedBlurView.isHidden = true self.foregroundContainerNode.view.addSubview(minimizedBlurView) @@ -222,8 +222,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { self.minimizedBlurView?.contentView.addSubview(minimizedArrowView) } if let minimizedArrowView = self.minimizedArrowView { - if let validLayoutSize = self.validLayoutSize { - setupArrowFrame(size: validLayoutSize, edge: edge, view: minimizedArrowView) + if let validLayout = self.validLayout { + setupArrowFrame(size: validLayout.size, edge: edge, view: minimizedArrowView) } minimizedArrowView.setAngled(!adjusting, animated: true) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index e673a81ed4..a7e26bb004 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -141,6 +141,10 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -170,7 +174,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private var dimensions: CGSize? private let dimensionsPromise = ValuePromise(CGSize()) - private var validLayout: CGSize? + private var validLayout: (size: CGSize, actualSize: CGSize)? init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { self.postbox = postbox @@ -203,8 +207,8 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte if let dimensions = getSize() { strongSelf.dimensions = dimensions strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate) } } } @@ -371,7 +375,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -471,4 +475,11 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 8f436da4c4..46d2e9ed8e 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -58,6 +58,10 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private let _ready = Promise() var ready: Signal { return self._ready.get() @@ -207,7 +211,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -308,5 +312,12 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift b/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift index 84bef2160a..0538d0d4a8 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/UniversalVideoContentManager.swift @@ -31,9 +31,12 @@ private final class UniversalVideoContentHolder { var bufferingStatusDisposable: Disposable? var bufferingStatusValue: (RangeSet, Int64)? + var isNativePictureInPictureActiveDisposable: Disposable? + var isNativePictureInPictureActiveValue: Bool = false + var playbackCompletedIndex: Int? - init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet, Int64)?) -> Void, playbackCompleted: @escaping () -> Void) { + init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet, Int64)?) -> Void, playbackCompleted: @escaping () -> Void, isNativePictureInPictureActiveUpdated: @escaping (Bool) -> Void) { self.content = content self.contentNode = contentNode @@ -51,6 +54,13 @@ private final class UniversalVideoContentHolder { } }) + self.isNativePictureInPictureActiveDisposable = (contentNode.isNativePictureInPictureActive |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.isNativePictureInPictureActiveValue = value + isNativePictureInPictureActiveUpdated(value) + } + }) + self.playbackCompletedIndex = contentNode.addPlaybackCompleted { playbackCompleted() } @@ -59,6 +69,7 @@ private final class UniversalVideoContentHolder { deinit { self.statusDisposable?.dispose() self.bufferingStatusDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() if let playbackCompletedIndex = self.playbackCompletedIndex { self.contentNode.removePlaybackCompleted(playbackCompletedIndex) } @@ -133,9 +144,10 @@ private final class UniversalVideoContentHolderCallbacks { let playbackCompleted = Bag<() -> Void>() let status = Bag<(MediaPlayerStatus?) -> Void>() let bufferingStatus = Bag<((RangeSet, Int64)?) -> Void>() + let isNativePictureInPictureActive = Bag<(Bool) -> Void>() var isEmpty: Bool { - return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty + return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty && self.isNativePictureInPictureActive.isEmpty } } @@ -190,6 +202,14 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager { } } } + }, isNativePictureInPictureActiveUpdated: { [weak self] value in + if let strongSelf = self { + if let current = strongSelf.holderCallbacks[content.id] { + for subscriber in current.isNativePictureInPictureActive.copyItems() { + subscriber(value) + } + } + } }) self.holders[content.id] = holder } @@ -311,4 +331,37 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager { } } |> runOn(Queue.mainQueue()) } + + public func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal { + return Signal { subscriber in + var callbacks: UniversalVideoContentHolderCallbacks + if let current = self.holderCallbacks[content.id] { + callbacks = current + } else { + callbacks = UniversalVideoContentHolderCallbacks() + self.holderCallbacks[content.id] = callbacks + } + + let index = callbacks.isNativePictureInPictureActive.add({ value in + subscriber.putNext(value) + }) + + if let current = self.holders[content.id] { + subscriber.putNext(current.isNativePictureInPictureActiveValue) + } else { + subscriber.putNext(false) + } + + return ActionDisposable { + Queue.mainQueue().async { + if let current = self.holderCallbacks[content.id] { + current.status.remove(index) + if current.playbackCompleted.isEmpty { + self.holderCallbacks.removeValue(forKey: content.id) + } + } + } + } + } |> runOn(Queue.mainQueue()) + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index abe11070b1..294aa1211c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -227,4 +227,11 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate { func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 5b204ce3d0..71c3c44d87 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -58,6 +58,10 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { return self._bufferingStatus.get() } + var isNativePictureInPictureActive: Signal { + return .single(false) + } + private var seekId: Int = 0 private let _ready = Promise() @@ -116,7 +120,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { self.readyDisposable.dispose() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) @@ -207,4 +211,11 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } } diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 8610235ba8..5a405bff0c 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -336,6 +336,10 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { impl.fileData(id: id, range: range).start(next: subscriber.putNext) } } + + public func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return .single(nil) + } } public protocol SharedHLSServerSource: AnyObject { @@ -345,6 +349,7 @@ public protocol SharedHLSServerSource: AnyObject { func playlistData(quality: Int) -> Signal func partData(index: Int, quality: Int) -> Signal func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> } @available(iOS 12.0, macOS 14.0, *) @@ -651,7 +656,19 @@ public final class SharedHLSServer { } }) } else { - self.sendErrorAndClose(connection: connection, error: .notFound) + let _ = (source.arbitraryFileData(path: filePath) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let result { + self.sendResponseAndClose(connection: connection, data: result.data, contentType: result.contentType) + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + }) } } @@ -665,13 +682,14 @@ public final class SharedHLSServer { }) } - private func sendResponseAndClose(connection: NWConnection, data: Data, range: Range? = nil, totalSize: Int? = nil) { + private func sendResponseAndClose(connection: NWConnection, data: Data, contentType: String = "application/octet-stream", range: Range? = nil, totalSize: Int? = nil) { var responseHeaders = "HTTP/1.1 200 OK\r\n" responseHeaders.append("Content-Length: \(data.count)\r\n") if let range, let totalSize { responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n") } - responseHeaders.append("Content-Type: application/octet-stream\r\n") + + responseHeaders.append("Content-Type: \(contentType)\r\n") responseHeaders.append("Connection: close\r\n") responseHeaders.append("Access-Control-Allow-Origin: *\r\n") responseHeaders.append("\r\n") diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 846f704048..b6e7349b98 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 846f7040480f52b8bc0382fb9e2e78e8ef60c633 +Subproject commit b6e7349b98c5d3999f45e9468eee068aff86ee37 diff --git a/versions.json b/versions.json index da4ab2f6e5..3556a03d85 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.2", + "app": "11.2.1", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0"