diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63fb1a6983..edf1022498 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -73,7 +73,9 @@ experimental_i: except: - tags script: - - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - export PATH=/opt/homebrew/opt/ruby/bin:$PATH + - export PATH=`gem environment gemdir`/bin:$PATH + - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: experimental diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 266ac1fa0d..4d9e3b3c3b 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -593,8 +593,10 @@ private struct NotificationContent: CustomStringConvertible { if !self.userInfo.isEmpty { content.userInfo = self.userInfo } - if !self.attachments.isEmpty { - content.attachments = self.attachments + if self.isLockedMessage == nil { + if !self.attachments.isEmpty { + content.attachments = self.attachments + } } if #available(iOS 15.0, *) { @@ -980,7 +982,7 @@ private final class NotificationServiceHandler { enum Action { case logout - case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?) + case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?, reportDelivery: Bool) case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32, isReaction: Bool) case deleteMessage([MessageId]) case readReactions([MessageId]) @@ -999,7 +1001,7 @@ private final class NotificationServiceHandler { action = .logout case "MESSAGE_MUTED": if let peerId = peerId { - action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil) + action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil, reportDelivery: false) } case "MESSAGE_DELETED": if let peerId = peerId { @@ -1183,9 +1185,16 @@ private final class NotificationServiceHandler { action = .pollStories(peerId: peerId, content: content, storyId: storyId, isReaction: isReaction) } else { - action = .poll(peerId: peerId, content: content, messageId: messageIdValue) + var reportDelivery = false + if let reportDeliveryUntilDate = aps["report_delivery_until_date"] as? Int32, let messageId = messageIdValue { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if reportDeliveryUntilDate > currentTime { + reportDelivery = true + } + } + action = .poll(peerId: peerId, content: content, messageId: messageIdValue, reportDelivery: reportDelivery) } - + updateCurrentContent(content) } } @@ -1245,7 +1254,7 @@ private final class NotificationServiceHandler { let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() - case let .poll(peerId, initialContent, messageId): + case let .poll(peerId, initialContent, messageId, reportDelivery): Logger.shared.log("NotificationService \(episode)", "Will poll") if let stateManager = strongSelf.stateManager { let shouldKeepConnection = stateManager.network.shouldKeepConnection @@ -1683,12 +1692,23 @@ private final class NotificationServiceHandler { pollWithUpdatedContent = pollSignal |> map { _ -> (NotificationContent, Media?) in } } + + let reportDeliverySignal: Signal + if reportDelivery, let messageId { + reportDeliverySignal = _internal_reportMessageDelivery(postbox: stateManager.postbox, network: stateManager.network, messageIds: [messageId], fromPushNotification: true) + |> mapToSignal { _ -> Signal in + return .single(true) + } + |> then(.single(true)) + } else { + reportDeliverySignal = .single(true) + } var updatedContent = initialContent var updatedMedia: Media? - strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content, media in - updatedContent = content - updatedMedia = media + strongSelf.pollDisposable.set(combineLatest(pollWithUpdatedContent, reportDeliverySignal).start(next: { contentAndMedia, _ in + updatedContent = contentAndMedia.0 + updatedMedia = contentAndMedia.1 }, completed: { pollCompletion(updatedContent, updatedMedia) })) @@ -1951,6 +1971,8 @@ private final class NotificationServiceHandler { } else { pollWithUpdatedContent = .complete() } + + var updatedContent = initialContent strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in diff --git a/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs b/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs new file mode 100644 index 0000000000..aff88f5e7f Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 02432016b8..7de9fbddd0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12299,6 +12299,7 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.ConvertedGift" = "Converted Gift"; "Stars.Intro.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Intro.Transaction.Refund" = "Refund"; +"Stars.Intro.Transaction.GiftUpgrade" = "Gift Upgrade"; "Stars.Intro.PurchasedTitle" = "Stars Acquired"; "Stars.Intro.PurchasedText" = "**%@** added to your balance."; @@ -13503,9 +13504,9 @@ Sorry for the inconvenience."; "Gift.Unique.Availability" = "Availability"; "Gift.Unique.Issued" = "%@ issued"; "Gift.Unique.OriginalInfo" = "Gifted to %1$@ on %2$@."; -"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\""; +"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\"."; "Gift.Unique.OriginalInfoSender" = "Gifted by %1$@ to %2$@ on %3$@."; -"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\""; +"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\"."; "Gift.Unique.AttributeDescription" = "Only %@ of such collectibles have this attribute."; @@ -13539,6 +13540,20 @@ Sorry for the inconvenience."; "PeerInfo.VerificationInfo.Group" = "This group is verified as official by the representatives of Telegram."; "PeerInfo.VerificationInfo.URL" = "https://telegram.org/verify"; +"ChatList.ToastFolderMutedV2" = "All chats in {folder} are now muted"; +"ChatList.ToastFolderUnmutedV2" = "All chats in {folder} are now unmuted"; +"ChatList.AddedToFolderTooltipV2" = "{chat} has been added to folder {folder}"; +"ChatList.RemovedFromFolderTooltipV2" = "{chat} has been removed from folder {folder}"; + +"FolderLinkScreen.TitleDescriptionDeselectedV2" = "Anyone with this link can add {folder} folder and the chats selected below."; +"FolderLinkScreen.TitleDescriptionSelectedV2" = "Anyone with this link can add {folder} folder and {chats} selected below."; +"Chat.NextChannelFolderSwipeProgressV2" = "Swipe up to go to the {folder} folder"; +"Chat.NextChannelFolderSwipeActionV2" = "Release to go to the {folder} folder"; +"FolderLinkPreview.TextAddChatsV2" = "Do you want to add {chats} to the\nfolder {folder}?"; +"FolderLinkPreview.ToastLeftTitleV2" = "Folder {folder} deleted"; +"FolderLinkPreview.ToastChatsAddedTitleV2" = "Folder {folder} Updated"; +"FolderLinkPreview.ToastFolderAddedTitleV2" = "Folder {folder} Added"; + "Stars.Purchase.UpgradeStarGiftInfo" = "Buy Stars to upgrade yort gift into a unique collectible."; "Gift.Send.Upgrade" = "Make Unique for %@"; @@ -13561,4 +13576,14 @@ Sorry for the inconvenience."; "Notification.StarGift.Subtitle.Refunded" = "This gift cannot be converted to Stars because the payment related to it was refunded."; "Notification.StarGift.Subtitle.Downgraded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned."; -"Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it."; +"Notification.StarGift.Subtitle.Other" = "%1$@ can turn this gift to a unique collectible."; + +"Stars.Transaction.GiftFrom" = "Gift From"; +"Stars.Transaction.GiftUpgrade" = "Gift Upgrade"; + +"Gift.View.KeepOrUpgradeDescription" = "Tap \"Unpack\" to turn this gift to a unique collectible."; + +"VideoChat.IncomingVideoQuality.AudioOnly" = "Audio Only"; +"VideoChat.IncomingVideoQuality.Title" = "Receive Video Quality"; + +"ChatList.EmptyResult.SearchInAll" = "Search in All Messages"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index b03a2b51e5..b44d75c0be 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -413,7 +413,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case monospace case textMention(EnginePeer.Id) case textUrl(String) - case customEmoji(stickerPack: StickerPackReference?, fileId: Int64) + case customEmoji(stickerPack: StickerPackReference?, fileId: Int64, enableAnimation: Bool) case strikethrough case underline case spoiler @@ -440,7 +440,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case 5: let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s") let fileId = try container.decode(Int64.self, forKey: "f") - self = .customEmoji(stickerPack: stickerPack, fileId: fileId) + let enableAnimation = try container.decodeIfPresent(Bool.self, forKey: "ea") ?? true + self = .customEmoji(stickerPack: stickerPack, fileId: fileId, enableAnimation: enableAnimation) case 6: self = .strikethrough case 7: @@ -474,10 +475,11 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case let .textUrl(url): try container.encode(4 as Int32, forKey: "t") try container.encode(url, forKey: "url") - case let .customEmoji(stickerPack, fileId): + case let .customEmoji(stickerPack, fileId, enableAnimation): try container.encode(5 as Int32, forKey: "t") try container.encodeIfPresent(stickerPack, forKey: "s") try container.encode(fileId, forKey: "f") + try container.encode(enableAnimation, forKey: "ea") case .strikethrough: try container.encode(6 as Int32, forKey: "t") case .underline: @@ -560,7 +562,7 @@ public struct ChatTextInputStateText: Codable, Equatable { } else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { - parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length))) + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId, enableAnimation: value.enableAnimation), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.strikethrough { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.underline { @@ -618,8 +620,8 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case let .textUrl(url): result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) - case let .customEmoji(_, fileId): - result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case let .customEmoji(_, fileId, enableAnimation): + result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil, enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .strikethrough: result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .underline: @@ -1204,3 +1206,40 @@ public protocol ChatHistoryListNode: ListView { var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set } } + +public extension ChatFolderTitle { + init(attributedString: NSAttributedString, enableAnimations: Bool) { + let inputStateText = ChatTextInputStateText(attributedText: attributedString) + self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in + if case let .customEmoji(_, fileId, _) = attribute.type { + return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId)) + } + return nil + }, enableAnimations: enableAnimations) + } + + var rawAttributedString: NSAttributedString { + let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in + if case let .CustomEmoji(_, fileId) = entity.type { + return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: self.enableAnimations), range: entity.range) + } + return nil + }) + return inputStateText.attributedText() + } + + func attributedString(attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { + let result = NSMutableAttributedString(attributedString: self.rawAttributedString) + result.addAttributes(attributes, range: NSRange(location: 0, length: result.length)) + return result + } + + func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString { + let result = NSMutableAttributedString(attributedString: self.rawAttributedString) + result.addAttributes([ + .font: font, + .foregroundColor: textColor + ], range: NSRange(location: 0, length: result.length)) + return result + } +} diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index a0e4ecc34a..4c8c0fd8e4 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -146,7 +146,7 @@ public protocol MediaManager: AnyObject { var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get } var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { get } - func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) + func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 80273b5756..6e98693b7b 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -53,7 +53,7 @@ public protocol UniversalVideoContent { var dimensions: CGSize { get } var duration: Double { get } - func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode + func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode func isEqual(to other: UniversalVideoContent) -> Bool } @@ -95,7 +95,7 @@ public enum UniversalVideoNodeFetchControl { } public final class UniversalVideoNode: ASDisplayNode { - private let accountId: AccountRecordId + private let context: AccountContext private let postbox: Postbox private let audioSession: ManagedAudioSession private let manager: UniversalVideoManager @@ -146,12 +146,12 @@ public final class UniversalVideoNode: ASDisplayNode { if self.canAttachContent { assert(self.contentRequestIndex == nil) - let accountId = self.accountId + let context = self.context let content = self.content let postbox = self.postbox let audioSession = self.audioSession self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: { - return content.makeContentNode(accountId: accountId, postbox: postbox, audioSession: audioSession) + return content.makeContentNode(context: context, postbox: postbox, audioSession: audioSession) }, update: { [weak self] contentNodeAndFlags in if let strongSelf = self { strongSelf.updateContentNode(contentNodeAndFlags) @@ -172,8 +172,8 @@ public final class UniversalVideoNode: ASDisplayNode { return self.contentNode != nil } - public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { - self.accountId = accountId + public init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + self.context = context self.postbox = postbox self.audioSession = audioSession self.manager = manager diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index d9eb6947ce..f9ee14ae3b 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -218,7 +218,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke public var isPlaying: Bool = false private var currentLoopCount: Int = 0 private var canDisplayFirstFrame: Bool = false - private var playbackMode: AnimatedStickerPlaybackMode = .loop + public var playbackMode: AnimatedStickerPlaybackMode = .loop public var stopAtNearestLoop: Bool = false diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index ad45b10348..425a5f2b75 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -234,7 +234,7 @@ public final class AvatarVideoNode: ASDisplayNode { if self.videoNode == nil { let context = self.context let mediaManager = context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.clipsToBounds = true videoNode.isUserInteractionEnabled = false videoNode.isHidden = true diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index cbf8994a0f..ae27df6f66 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1527,7 +1527,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } switch result { - case let .done(receiptMessageId, _): + case let .done(receiptMessageId, _, _): proceedWithCompletion(true, receiptMessageId) case let .externalVerificationRequired(url): strongSelf.updateActionButton() diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 2d189e839c..8fb66cee92 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -1076,7 +1076,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } } } - self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) + self.context.sharedContext.mediaManager.setPlaylist((self.context, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) return } diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 9634edd0a8..7919d85b1e 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -464,7 +464,7 @@ public final class ChatImportActivityScreen: ViewController { let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) videoNode.alpha = 0.01 self.videoNode = videoNode diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index b26a832fbb..4d2512beef 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -103,6 +103,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextFieldComponent", @@ -110,6 +111,7 @@ swift_library( "//submodules/ComposePollUI", "//submodules/ChatPresentationInterfaceState", "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 9b64ab804e..045a86eabc 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -223,8 +223,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } |> deliverOnMainQueue).startStandalone(completed: { c?.dismiss(completion: { - //TODO:release - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -274,8 +273,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } let filterType = chatListFilterType(data) - //TODO:release - updatedItems.append(.action(ContextMenuActionItem(text: title.text, icon: { theme in + updatedItems.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, icon: { theme in let imageName: String switch filterType { case .generic: @@ -339,8 +337,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } return filters }).startStandalone() - //TODO:release - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + chatListController?.present(UndoOverlayController( presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -348,7 +345,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } } - c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) + c?.setItems(.single(ContextController.Items(content: .list(updatedItems), context: context)), minHeight: nil, animated: true) }))) items.append(.separator) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 68cc968256..0dc0f80c7a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -51,6 +51,7 @@ import PeerInfoStoryGridScreen import ArchiveInfoScreen import BirthdayPickerScreen import OldChannelsController +import TextFormat private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -1453,7 +1454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) dismissPreviewingImpl = { [weak contextController] in @@ -1526,7 +1527,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -1811,28 +1812,42 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - //TODO:release let iconColor: UIColor = .white let overlayController: UndoOverlayController if !filterPeersAreMuted.areMuted { - let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title.text).string - overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ + let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderMutedV2) + let folderNameRange = (text.string as NSString).range(of: "{folder}") + if folderNameRange.location != NSNotFound { + text.replaceCharacters(in: folderNameRange, with: "") + text.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: folderNameRange.location) + } + + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profilemute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + ], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) } else { - //TODO:release - let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title.text).string - overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ + let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderUnmutedV2) + let folderNameRange = (text.string as NSString).range(of: "{folder}") + if folderNameRange.location != NSNotFound { + text.replaceCharacters(in: folderNameRange, with: "") + text.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: folderNameRange.location) + } + + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profileunmute", scale: 0.075, colors: [ "Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + ], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) } strongSelf.present(overlayController, in: .current) }) @@ -4736,7 +4751,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -4842,7 +4857,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -4914,7 +4929,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -5274,7 +5289,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: strongSelf.presentationData.strings.Undo_ChatCleared, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: strongSelf.presentationData.strings.Undo_ChatCleared), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -5496,7 +5511,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let statusText = self.presentationData.strings.Undo_DeletedTopic - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let self else { return false } @@ -5793,7 +5808,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { return false } @@ -5933,7 +5948,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) } } - //TODO:release items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: badge, icon: { theme in let imageName: String if isDisabled { @@ -6347,9 +6361,9 @@ private final class ChatListLocationContext { let peerView = Promise() peerView.set(context.account.viewTracker.peerView(peerId)) - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) - let recentOnlineSignal: Signal = peerView.get() + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get() |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -6364,17 +6378,21 @@ private final class ChatListLocationContext { } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6783,7 +6801,7 @@ private final class ChatListLocationContext { private func updateForum( peerId: EnginePeer.Id, peerView: PeerView, - onlineMemberCount: Int32?, + onlineMemberCount: (total: Int32?, recent: Int32?), stateAndFilterId: (state: ChatListNodeState, filterId: Int32?), presentationData: PresentationData ) { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index b2b18055d9..5004a63bea 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -35,6 +35,7 @@ private final class ChatListFilterPresetControllerArguments { let context: AccountContext let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void let updateName: (ChatFolderTitle) -> Void + let toggleNameInputMode: () -> Void let toggleNameAnimations: () -> Void let openAddIncludePeer: () -> Void let openAddExcludePeer: () -> Void @@ -58,6 +59,7 @@ private final class ChatListFilterPresetControllerArguments { context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, updateName: @escaping (ChatFolderTitle) -> Void, + toggleNameInputMode: @escaping () -> Void, toggleNameAnimations: @escaping () -> Void, openAddIncludePeer: @escaping () -> Void, openAddExcludePeer: @escaping () -> Void, @@ -80,6 +82,7 @@ private final class ChatListFilterPresetControllerArguments { self.context = context self.updateState = updateState self.updateName = updateName + self.toggleNameInputMode = toggleNameInputMode self.toggleNameAnimations = toggleNameAnimations self.openAddIncludePeer = openAddIncludePeer self.openAddExcludePeer = openAddExcludePeer @@ -228,7 +231,7 @@ private enum ChatListFilterRevealedItemId: Equatable { private enum ChatListFilterPresetEntry: ItemListNodeEntry { case screenHeader - case nameHeader(title: String, enableAnimations: Bool) + case nameHeader(title: String, enableAnimations: Bool?) case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool) case includePeersHeader(String) case addIncludePeer(title: String) @@ -376,7 +379,11 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section) case let .nameHeader(title, enableAnimations): //TODO:localize - return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: { + var actionText: String? + if let enableAnimations { + actionText = enableAnimations ? "Disable Animations" : "Enable Animations" + } + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: actionText, action: { arguments.toggleNameAnimations() }, sectionId: self.section) case let .name(placeholder, value, inputMode, enableAnimations): @@ -393,15 +400,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { arguments.updateName(ChatFolderTitle(attributedString: value, enableAnimations: true)) }, toggleInputMode: { - arguments.updateState { current in - var state = current - if state.nameInputMode == .emoji { - state.nameInputMode = .keyboard - } else { - state.nameInputMode = .emoji - } - return state - } + arguments.toggleNameInputMode() } ) case .includePeersHeader(let text), .excludePeersHeader(let text): @@ -545,37 +544,6 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { } } -extension ChatFolderTitle { - init(attributedString: NSAttributedString, enableAnimations: Bool) { - let inputStateText = ChatTextInputStateText(attributedText: attributedString) - self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in - if case let .customEmoji(_, fileId) = attribute.type { - return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId)) - } - return nil - }, enableAnimations: enableAnimations) - } - - var rawAttributedString: NSAttributedString { - let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in - if case let .CustomEmoji(_, fileId) = entity.type { - return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range) - } - return nil - }) - return inputStateText.attributedText() - } - - func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString { - let result = NSMutableAttributedString(attributedString: self.rawAttributedString) - result.addAttributes([ - .font: font, - .foregroundColor: textColor - ], range: NSRange(location: 0, length: result.length)) - return result - } -} - private struct ChatListFilterPresetControllerState: Equatable { var name: ChatFolderTitle var changedName: Bool @@ -624,7 +592,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres entries.append(.screenHeader) } - entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations)) + entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.entities.isEmpty ? nil : state.name.enableAnimations)) entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name.rawAttributedString, inputMode: state.nameInputMode, enableAnimations: state.name.enableAnimations)) entries.append(.includePeersHeader(presentationData.strings.ChatListFolder_IncludedSectionHeader)) @@ -1584,6 +1552,18 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } } }, + toggleNameInputMode: { + updateState { current in + var state = current + if state.nameInputMode == .emoji { + state.nameInputMode = .keyboard + } else { + state.nameInputMode = .emoji + } + return state + } + focusOnNameImpl?() + }, toggleNameAnimations: { updateState { current in var name = current.name @@ -2145,7 +2125,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi return } controller.forEachItemNode { itemNode in - if let itemNode = itemNode as? ItemListSingleLineInputItemNode { + if let itemNode = itemNode as? ItemListFilterTitleInputItemNode { itemNode.focus() } } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index e1105affa4..8b0636d941 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -16,7 +16,7 @@ import ChatFolderLinkPreviewScreen private final class ChatListFilterPresetListControllerArguments { let context: AccountContext - let addSuggestedPressed: (String, ChatListFilterData) -> Void + let addSuggestedPressed: (ChatFolderTitle, ChatListFilterData) -> Void let openPreset: (ChatListFilter) -> Void let addNew: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void @@ -24,7 +24,7 @@ private final class ChatListFilterPresetListControllerArguments { let updateDisplayTags: (Bool) -> Void let updateDisplayTagsLocked: () -> Void - init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { + init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { self.context = context self.addSuggestedPressed = addSuggestedPressed self.openPreset = openPreset @@ -92,10 +92,10 @@ private struct PresetIndex: Equatable { private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case screenHeader(String) case suggestedListHeader(String) - case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) + case suggestedPreset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) + case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) case displayTags(Bool?) @@ -176,7 +176,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case let .suggestedListHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) case let .suggestedPreset(_, title, label, preset): - return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title, label: label, sectionId: self.section, style: .blocks, installAction: { + return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: { arguments.addSuggestedPressed(title, preset) }, tag: nil) case let .suggestedAddCustom(text): @@ -194,7 +194,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { } } - return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { + return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { if isDisabled { arguments.addNew() } else { @@ -285,12 +285,11 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present var folderCount = 0 for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if case .allChats = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) } if case let .filter(_, title, _, _) = filter { folderCount += 1 - //TODO:release - entries.append(.preset(index: PresetIndex(value: entries.count), title: title.text, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) } } @@ -300,8 +299,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount { entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection)) for filter in filteredSuggestedFilters { - //TODO:release - entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title.text, label: filter.description, preset: filter.data)) + entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data)) } if filters.isEmpty { entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder)) @@ -389,8 +387,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters let id = context.engine.peers.generateNewChatListFilterId(filters: filters) - //TODO:release - filters.append(.filter(id: id, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: nil, data: data)) + filters.append(.filter(id: id, title: title, emoticon: nil, data: data)) return filters } |> deliverOnMainQueue).start(next: { _ in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 91f2b2acfa..01d1063a17 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -7,6 +7,8 @@ import TelegramCore import TelegramPresentationData import ItemListUI import TelegramUIPreferences +import AccountContext +import TextNodeWithEntities struct ChatListFilterPresetListItemEditing: Equatable { let editable: Bool @@ -15,9 +17,10 @@ struct ChatListFilterPresetListItemEditing: Equatable { } final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData let preset: ChatListFilter - let title: String + let title: ChatFolderTitle let label: String let tagColor: UIColor? let editing: ChatListFilterPresetListItemEditing @@ -31,9 +34,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let remove: () -> Void init( + context: AccountContext, presentationData: ItemListPresentationData, preset: ChatListFilter, - title: String, + title: ChatFolderTitle, label: String, tagColor: UIColor?, editing: ChatListFilterPresetListItemEditing, @@ -46,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, remove: @escaping () -> Void ) { + self.context = context self.presentationData = presentationData self.preset = preset self.title = title @@ -124,7 +129,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { return self.containerNode } - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let labelNode: TextNode private let arrowNode: ASImageNode private let sharedIconNode: ASImageNode @@ -145,6 +150,15 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { return true } + override var visibility: ListViewItemNodeVisibility { + didSet { + if self.visibility != oldValue { + let enableAnimations = self.item?.title.enableAnimations ?? true + self.titleNode.visibilityRect = (self.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite + } + } + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -160,10 +174,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale + self.titleNode = TextNodeWithEntities() + self.titleNode.textNode.isUserInteractionEnabled = false + self.titleNode.textNode.contentMode = .left + self.titleNode.textNode.contentsScale = UIScreen.main.scale + self.titleNode.resetEmojiToFirstFrameAutomatically = true self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false @@ -186,7 +201,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.titleNode.textNode) self.containerNode.addSubnode(self.labelNode) self.containerNode.addSubnode(self.arrowNode) self.containerNode.addSubnode(self.sharedIconNode) @@ -199,7 +214,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) @@ -230,7 +245,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } let titleAttributedString = NSMutableAttributedString() - titleAttributedString.append(NSAttributedString(string: item.isAllChats ? item.presentationData.strings.ChatList_FolderAllChats : item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + if item.isAllChats { + titleAttributedString.append(NSAttributedString(string: item.presentationData.strings.ChatList_FolderAllChats, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + } else { + titleAttributedString.append(item.title.attributedString(font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + } var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -337,9 +356,18 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } strongSelf.editableControlNode?.isHidden = !item.canBeDeleted - let _ = titleApply() + let _ = titleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) let _ = labelApply() + let enableAnimations = item.title.enableAnimations + strongSelf.titleNode.visibilityRect = (strongSelf.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite + if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -385,7 +413,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: 11.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -542,7 +570,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + transition.updateFrame(node: self.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.textNode.frame.minY), size: self.titleNode.textNode.bounds.size)) var labelFrame = self.labelNode.frame labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 37f621cdfa..6cea53242d 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -109,11 +109,13 @@ private final class ItemNode: ASDisplayNode { self.titleNode = ImmediateTextNodeWithEntities() self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.titleNode.resetEmojiToFirstFrameAutomatically = true self.titleActiveNode = ImmediateTextNodeWithEntities() self.titleActiveNode.displaysAsynchronously = false self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) self.titleActiveNode.alpha = 0.0 + self.titleActiveNode.resetEmojiToFirstFrameAutomatically = true self.shortTitleContainer = ASDisplayNode() @@ -121,12 +123,14 @@ private final class ItemNode: ASDisplayNode { self.shortTitleNode.displaysAsynchronously = false self.shortTitleNode.alpha = 0.0 self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.shortTitleNode.resetEmojiToFirstFrameAutomatically = true self.shortTitleActiveNode = ImmediateTextNodeWithEntities() self.shortTitleActiveNode.displaysAsynchronously = false self.shortTitleActiveNode.alpha = 0.0 self.shortTitleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) self.shortTitleActiveNode.alpha = 0.0 + self.shortTitleActiveNode.resetEmojiToFirstFrameAutomatically = true self.badgeContainerNode = ASDisplayNode() diff --git a/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift index f6cca407a6..93438c9af5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTagSectionHeaderItem.swift @@ -239,7 +239,8 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, - attemptSynchronous: true + attemptSynchronous: true, + emojiOffset: CGPoint(x: 0.0, y: -1.0) )) let badgeSideInset: CGFloat = 4.0 let badgeBackgroundSize: CGSize @@ -248,7 +249,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { } else { badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0) } - let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize) + let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize) let badgeBackgroundLayer: SimpleLayer if let current = strongSelf.badgeBackgroundLayer { @@ -262,6 +263,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode { if strongSelf.badgeTextNode !== badgeTextNode { strongSelf.badgeTextNode?.textNode.removeFromSupernode() strongSelf.badgeTextNode = badgeTextNode + badgeTextNode.resetEmojiToFirstFrameAutomatically = true strongSelf.addSubnode(badgeTextNode.textNode) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index e160a63e0c..9991b69e9e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -197,12 +197,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else if case let .user(user) = primaryPeer { let servicePeer = isServicePeer(primaryPeer._asPeer()) if user.flags.contains(.isSupport) && !servicePeer { - status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Bot_GenericSupportStatus), multiline: false, isActive: false, icon: nil) } else if let _ = user.botInfo { if let subscriberCount = user.subscriberCount { - status = .custom(string: strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil) } } else if user.id != context.account.peerId && !servicePeer { let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0) @@ -211,19 +211,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable { status = .none } } else if case let .legacyGroup(group) = primaryPeer { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) } else if case let .channel(channel) = primaryPeer { if case .group = channel.info { if let count = peer.subpeerSummary?.count, count > 0 { - status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(count))), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil) } } else { if let count = peer.subpeerSummary?.count, count > 0 { - status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(Int32(count))), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil) } } } else { @@ -380,6 +380,7 @@ public enum ChatListSearchEntryStableId: Hashable { case globalPeerId(EnginePeer.Id) case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection) case messagePlaceholder(Int32) + case emptyMessagesFooter case addContact } @@ -442,6 +443,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { 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, TelegramSearchPeersScope) case messagePlaceholder(Int32, ChatListPresentationData, TelegramSearchPeersScope) + case emptyMessagesFooter(ChatListPresentationData, TelegramSearchPeersScope, String?) case addContact(String, PresentationTheme, PresentationStrings) public var stableId: ChatListSearchEntryStableId { @@ -458,6 +460,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .messageId(message.id, section) case let .messagePlaceholder(index, _, _): return .messagePlaceholder(index) + case .emptyMessagesFooter: + return .emptyMessagesFooter case .addContact: return .addContact } @@ -561,6 +565,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } + case let .emptyMessagesFooter(lhsPresentationData, lhsSearchScope, lhsQuery): + if case let .emptyMessagesFooter(rhsPresentationData, rhsSearchScope, rhsQuery) = rhs { + if lhsPresentationData !== rhsPresentationData { + return false + } + if lhsSearchScope != rhsSearchScope { + return false + } + if lhsQuery != rhsQuery { + return false + } + return true + } else { + return false + } case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings): if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs { if lhsPhoneNumber != rhsPhoneNumber { @@ -601,7 +620,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return false case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex - case .globalPeer, .message, .messagePlaceholder, .addContact: + case .globalPeer, .message, .messagePlaceholder, .emptyMessagesFooter, .addContact: return true } case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): @@ -610,7 +629,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return false case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex - case .message, .messagePlaceholder, .addContact: + case .message, .messagePlaceholder, .emptyMessagesFooter, .addContact: return true } case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _, _, _, _): @@ -618,6 +637,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return lhsKey < rhsKey } else if case .messagePlaceholder = rhs { return true + } else if case .emptyMessagesFooter = rhs { + return true } else if case .addContact = rhs { return true } else { @@ -626,11 +647,19 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case let .messagePlaceholder(lhsIndex, _, _): if case let .messagePlaceholder(rhsIndex, _, _) = rhs { return lhsIndex < rhsIndex + } else if case .emptyMessagesFooter = rhs { + return true } else if case .addContact = rhs { return true } else { return false } + case .emptyMessagesFooter: + if case .addContact = rhs { + return true + } else { + return false + } case .addContact: return false } @@ -658,7 +687,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void, - openMessagesFilter: @escaping (ASDisplayNode) -> Void + openMessagesFilter: @escaping (ASDisplayNode) -> Void, + switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void ) -> ListViewItem { switch self { case let .topic(peer, threadInfo, _, theme, strings, expandType): @@ -870,9 +900,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var status: ContactsPeerItemStatus = .none if case let .user(user) = primaryPeer, let _ = user.botInfo, !primaryPeer.id.isVerificationCodes { if let subscriberCount = user.subscriberCount { - status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: presentationData.strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: presentationData.strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil) } } @@ -1128,6 +1158,33 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openMessagesFilter(sourceNode) }) return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction) + case let .emptyMessagesFooter(presentationData, searchScope, searchQuery): + var actionTitle: String? + let filterTitle: String + switch searchScope { + case .everywhere: + filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats + case .channels: + filterTitle = presentationData.strings.ChatList_Search_Messages_Channels + case .groups: + filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats + case .privateChats: + filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + } + actionTitle = "\(filterTitle) <" + + let header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in + openMessagesFilter(sourceNode) + }) + return ChatListSearchEmptyFooterItem( + theme: presentationData.theme, + strings: presentationData.strings, + header: header, + searchQuery: searchQuery, + searchAllMessages: searchScope == .everywhere ? nil : { + switchMessagesFilter(.everywhere) + } + ) case let .addContact(phoneNumber, theme, strings): return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { interaction.addContact(phoneNumber) @@ -1201,12 +1258,42 @@ private func chatListSearchContainerPreparedRecentTransition( return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void, openMessagesFilter: @escaping (ASDisplayNode) -> Void) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition( + from fromEntries: [ChatListSearchEntry], + to toEntries: [ChatListSearchEntry], + displayingResults: Bool, + isEmpty: Bool, + isLoading: Bool, + animated: Bool, + context: AccountContext, + presentationData: PresentationData, + enableHeaders: Bool, + filter: ChatListNodePeersFilter, + requestPeerType: [ReplyMarkupButtonRequestPeerType]?, + location: ChatListControllerLocation, + key: ChatListSearchPaneKey, + tagMask: EngineMessage.Tags?, + interaction: ChatListNodeInteraction, + listInteraction: ListMessageItemInteraction, + peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, + toggleExpandLocalResults: @escaping () -> Void, + toggleExpandGlobalResults: @escaping () -> Void, + searchPeer: @escaping (EnginePeer) -> Void, + searchQuery: String?, + searchOptions: ChatListSearchOptions?, + messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, + openClearRecentlyDownloaded: @escaping () -> Void, + toggleAllPaused: @escaping () -> Void, + openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, + openPublicPosts: @escaping () -> Void, + openMessagesFilter: @escaping (ASDisplayNode) -> Void, + switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void +) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -2912,6 +2999,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { index += 1 } } else { + var hasAnyMessages = false for foundRemoteMessageSet in foundRemoteMessages.0 { for message in foundRemoteMessageSet.messages { if existingMessageIds.contains(message.id) { @@ -2936,10 +3024,22 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } //TODO:requiresPremiumForMessaging + hasAnyMessages = true entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false, nil, false, searchScope)) index += 1 } } + if !hasAnyMessages { + switch searchScope { + case .everywhere: + break + default: + if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_empty_search_footer"] != nil { + } else { + entries.append(.emptyMessagesFooter(presentationData, searchScope, query)) + } + } + } } } @@ -3444,6 +3544,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.interaction.switchToFilter(.publicPosts) }, openMessagesFilter: { sourceNode in strongSelf.openMessagesFilter(sourceNode: sourceNode) + }, switchMessagesFilter: { filter in + strongSelf.searchScopePromise.set(.everywhere) }) strongSelf.currentEntries = newEntries if strongSelf.key == .downloads { diff --git a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift index bc91aa74dd..b511eaced3 100644 --- a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift +++ b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift @@ -238,6 +238,7 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele backspaceKeyAction: nil, selection: nil, inputMode: item.inputMode, + alwaysDisplayInputModeSelector: true, toggleInputMode: { [weak self] in guard let self else { return @@ -268,6 +269,9 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele } public func focus() { + if let textFieldView = self.textField.view as? ListComposePollOptionComponent.View { + textFieldView.activateInput() + } } public func selectAll() { diff --git a/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift b/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift index 622e292c90..4434f59e2f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import ComponentFlow +import LottieComponent class ChatListHoleItem: ListViewItem { let theme: PresentationTheme @@ -78,3 +80,252 @@ class ChatListHoleItemNode: ListViewItemNode { } } } + +class ChatListSearchEmptyFooterItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings + let searchQuery: String? + let searchAllMessages: (() -> Void)? + + let header: ListViewItemHeader? + let selectable: Bool = false + + init(theme: PresentationTheme, strings: PresentationStrings, header: ListViewItemHeader?, searchQuery: String?, searchAllMessages: (() -> Void)?) { + self.theme = theme + self.strings = strings + self.header = header + self.searchQuery = searchQuery + self.searchAllMessages = searchAllMessages + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ChatListSearchEmptyFooterItemNode() + let (layout, apply) = node.asyncLayout()(self, params) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ChatListSearchEmptyFooterItemNode) + if let nodeValue = node() as? ChatListSearchEmptyFooterItemNode { + + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply() + }) + } + } + } + } + } +} + +class ChatListSearchEmptyFooterItemNode: ListViewItemNode { + private let contentNode: ASDisplayNode + private let titleNode: TextNode + private let textNode: TextNode + private let searchAllMessagesButton: HighlightableButtonNode + private let searchAllMessagesTitle: TextNode + + private let icon = ComponentView() + + private var item: ChatListSearchEmptyFooterItem? + + required init() { + self.contentNode = ASDisplayNode() + self.titleNode = TextNode() + self.textNode = TextNode() + + self.searchAllMessagesButton = HighlightableButtonNode() + self.searchAllMessagesTitle = TextNode() + self.searchAllMessagesTitle.isUserInteractionEnabled = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.titleNode) + self.contentNode.addSubnode(self.textNode) + + self.contentNode.addSubnode(self.searchAllMessagesButton) + self.searchAllMessagesButton.addSubnode(self.searchAllMessagesTitle) + + self.searchAllMessagesButton.addTarget(self, action: #selector(self.searchAllMessagesButtonPressed), forControlEvents: .touchUpInside) + + self.wantsTrailingItemSpaceUpdates = true + } + + @objc private func searchAllMessagesButtonPressed() { + self.item?.searchAllMessages?() + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + let layout = self.asyncLayout() + let (_, apply) = layout(item as! ChatListSearchEmptyFooterItem, params) + apply() + } + + override func headers() -> [ListViewItemHeader]? { + if let item = self.item { + return item.header.flatMap { [$0] } + } else { + return nil + } + } + + override func updateTrailingItemSpace(_ trailingItemSpace: CGFloat, transition: ContainedViewLayoutTransition) { + var contentFrame = self.contentNode.frame + contentFrame.origin.y = max(0.0, floor(trailingItemSpace * 0.5)) + self.contentNode.frame = contentFrame + } + + func asyncLayout() -> (_ item: ChatListSearchEmptyFooterItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleNodeLayout = TextNode.asyncLayout(self.titleNode) + let makeTextNodeLayout = TextNode.asyncLayout(self.textNode) + let makeSearchAllMessagesTitleLayout = TextNode.asyncLayout(self.searchAllMessagesTitle) + + return { [weak self] item, params in + let titleLayout = makeTitleNodeLayout(TextNodeLayoutArguments( + attributedString: NSAttributedString(string: item.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: item.theme.list.freeTextColor), + maximumNumberOfLines: 1, + truncationType: .end, + constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0) + )) + + let textValue: String + if let searchQuery = item.searchQuery { + textValue = item.strings.ChatList_Search_NoResultsQueryDescription(searchQuery).string + } else { + textValue = item.strings.ChatList_Search_NoResults + } + + let textLayout = makeTextNodeLayout(TextNodeLayoutArguments( + attributedString: NSAttributedString(string: textValue, font: Font.regular(16.0), textColor: item.theme.list.freeTextColor), + maximumNumberOfLines: 0, + truncationType: .end, + constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0), + alignment: .center, + lineSpacing: 0.1 + )) + + let searchAllMessagesTitleLayout = makeSearchAllMessagesTitleLayout(TextNodeLayoutArguments( + attributedString: NSAttributedString(string: item.strings.ChatList_EmptyResult_SearchInAll, font: Font.regular(17.0), textColor: item.theme.list.itemAccentColor), + maximumNumberOfLines: 1, + truncationType: .end, + constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0) + )) + + var contentHeight: CGFloat = 0.0 + + let topInset: CGFloat = 40.0 + let bottomInset: CGFloat = 10.0 + let iconSpacing: CGFloat = 20.0 + let titleSpacing: CGFloat = 6.0 + + let buttonSpacing: CGFloat = 14.0 + let buttonInset: CGFloat = 11.0 + + let iconSize = CGSize(width: 128.0, height: 128.0) + + contentHeight += topInset + contentHeight += iconSize.height + contentHeight += iconSpacing + contentHeight += titleLayout.0.size.height + contentHeight += titleSpacing + contentHeight += textLayout.0.size.height + + if item.searchAllMessages != nil { + contentHeight += buttonSpacing + contentHeight += buttonInset + contentHeight += searchAllMessagesTitleLayout.0.size.height + contentHeight += buttonInset + } + + contentHeight += bottomInset + + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: UIEdgeInsets()) + + return (layout, { [weak self] in + guard let self else { + return + } + + self.item = item + self.contentSize = layout.contentSize + self.insets = layout.insets + + let _ = titleLayout.1() + let _ = textLayout.1() + let _ = searchAllMessagesTitleLayout.1() + + var contentY: CGFloat = 0.0 + contentY += topInset + + let _ = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: "ChatListNoResults" + ), + color: nil, + placeholderColor: nil, + startingPosition: .begin, + size: iconSize, + renderingScale: nil, + loop: false, + playOnce: nil + )), + environment: {}, containerSize: iconSize + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((params.width - iconSize.width) * 0.5), y: contentY), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.contentNode.view.addSubview(iconView) + } + iconView.frame = iconFrame + } + + contentY += iconSize.height + contentY += iconSpacing + + let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: contentY), size: titleLayout.0.size) + self.titleNode.frame = titleFrame + contentY += titleLayout.0.size.height + contentY += titleSpacing + + let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: contentY), size: textLayout.0.size) + self.textNode.frame = textFrame + contentY += textLayout.0.size.height + + if item.searchAllMessages != nil { + contentY += buttonSpacing + let searchAllMessagesButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - searchAllMessagesTitleLayout.0.size.width) * 0.5), y: contentY), size: CGSize(width: searchAllMessagesTitleLayout.0.size.width, height: searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0)) + contentY += searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0 + + self.searchAllMessagesButton.frame = searchAllMessagesButtonFrame + self.searchAllMessagesTitle.frame = CGRect(origin: CGPoint(x: 0.0, y: buttonInset), size: searchAllMessagesTitleLayout.0.size) + contentY += buttonInset + contentY += searchAllMessagesTitleLayout.0.size.height + contentY += buttonInset + } + + contentY += bottomInset + + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: self.contentNode.frame.minY), size: CGSize(width: params.width, height: contentHeight)) + self.contentNode.frame = contentFrame + }) + } + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index c56255ddea..295f34f521 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -28,6 +28,7 @@ import EmojiStatusComponent import AvatarVideoNode import AppBundle import MultilineTextComponent +import MultilineTextWithEntitiesComponent import ShimmerEffect public enum ChatListItemContent { @@ -82,10 +83,10 @@ public enum ChatListItemContent { public struct Tag: Equatable { public var id: Int32 - public var title: String + public var title: ChatFolderTitle public var colorId: Int32 - public init(id: Int32, title: String, colorId: Int32) { + public init(id: Int32, title: ChatFolderTitle, colorId: Int32) { self.id = id self.title = title self.colorId = colorId @@ -269,6 +270,8 @@ private final class ChatListItemTagListComponent: Component { let backgroundView: UIImageView let title = ComponentView() + private var currentTitle: ChatFolderTitle? + override init(frame: CGRect) { self.backgroundView = UIImageView(image: tagBackgroundImage) @@ -281,11 +284,20 @@ private final class ChatListItemTagListComponent: Component { preconditionFailure() } - func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize { + func update(context: AccountContext, title: ChatFolderTitle, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize { + self.currentTitle = title + + let titleValue = ChatFolderTitle(text: title.text.isEmpty ? " " : title.text, entities: title.entities, enableAnimations: title.enableAnimations) let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)) + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + placeholderColor: foregroundColor.withMultipliedAlpha(0.1), + text: .plain(titleValue.attributedString(font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)), + manualVisibilityControl: true, + resetAnimationsOnVisibilityChange: true )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -309,11 +321,30 @@ private final class ChatListItemTagListComponent: Component { return backgroundSize } + + func updateVisibility(_ isVisible: Bool) { + guard let currentTitle = self.currentTitle else { + return + } + if let titleView = self.title.view as? MultilineTextWithEntitiesComponent.View { + titleView.updateVisibility(isVisible && currentTitle.enableAnimations) + } + } } final class View: UIView { private var itemViews: [Int32: ItemView] = [:] + var isVisible: Bool = false { + didSet { + if self.isVisible != oldValue { + for (_, itemView) in self.itemViews { + itemView.updateVisibility(self.isVisible) + } + } + } + } + override init(frame: CGRect) { super.init(frame: frame) } @@ -332,13 +363,13 @@ private final class ChatListItemTagListComponent: Component { } let itemId: Int32 - let itemTitle: String + let itemTitle: ChatFolderTitle let itemBackgroundColor: UIColor let itemForegroundColor: UIColor if validIds.count >= 3 { itemId = Int32.max - itemTitle = "+\(component.tags.count - validIds.count)" + itemTitle = ChatFolderTitle(text: "+\(component.tags.count - validIds.count)", entities: [], enableAnimations: true) itemForegroundColor = component.theme.chatList.dateTextColor itemBackgroundColor = itemForegroundColor.withMultipliedAlpha(0.1) } else { @@ -347,7 +378,7 @@ private final class ChatListItemTagListComponent: Component { let tagColor = PeerNameColor(rawValue: tag.colorId) let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance) - itemTitle = tag.title.uppercased() + itemTitle = ChatFolderTitle(text: tag.title.text.uppercased(), entities: tag.title.entities, enableAnimations: tag.title.enableAnimations) itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1) itemForegroundColor = resolvedColor.main } @@ -364,6 +395,7 @@ private final class ChatListItemTagListComponent: Component { let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor, sizeFactor: component.sizeFactor) let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize) itemView.frame = itemFrame + itemView.updateVisibility(self.isVisible) validIds.append(itemId) nextX += itemSize.width @@ -1451,6 +1483,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ) } self.authorNode.visibilityStatus = self.visibilityStatus + + if let itemTagListView = self.itemTagList?.view as? ChatListItemTagListComponent.View { + itemTagListView.isVisible = self.visibilityStatus + } } } } @@ -4239,13 +4275,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { environment: {}, containerSize: itemTagListFrame.size ) - if let itemTagListView = itemTagList.view { + if let itemTagListView = itemTagList.view as? ChatListItemTagListComponent.View { if itemTagListView.superview == nil { itemTagListView.isUserInteractionEnabled = false strongSelf.mainContentContainerNode.view.addSubview(itemTagListView) } itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame) + itemTagListView.isVisible = strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji } } else { if let itemTagList = strongSelf.itemTagList { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 6a54adc920..dfc8c12f68 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -4276,33 +4276,32 @@ public final class ChatListNode: ListView { } } -private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (String, Bool, Bool, ContactsPeerItemStatus.Icon?)? { +private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (NSAttributedString, Bool, Bool, ContactsPeerItemStatus.Icon?)? { if accountPeerId == peer.id { return nil } if displayAutoremoveTimeout { if let autoremoveTimeout = autoremoveTimeout { - return (strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string, false, true, .autoremove) + return (NSAttributedString(string: strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string), false, true, .autoremove) } else { - return (strings.ChatList_LabelAutodeleteDisabled, false, false, .autoremove) + return (NSAttributedString(string: strings.ChatList_LabelAutodeleteDisabled), false, false, .autoremove) } } if let chatListFilters = chatListFilters { - var result = "" + let result = NSMutableAttributedString(string: "") for case let .filter(_, title, _, data) in chatListFilters { let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { - if !result.isEmpty { - result.append(", ") + if result.length != 0 { + result.append(NSAttributedString(string: ", ")) } - //TODO:release - result.append(title.text) + result.append(title.rawAttributedString) } } - if result.isEmpty { + if result.length == 0 { return nil } else { return (result, true, false, nil) @@ -4313,30 +4312,30 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres return nil } else if case let .user(user) = peer { if user.botInfo != nil || user.flags.contains(.isSupport) { - return (strings.ChatList_PeerTypeBot, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeBot), false, false, nil) } else { if isContact { - return (strings.ChatList_PeerTypeContact, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } } } else if case .secretChat = peer { if isContact { - return (strings.ChatList_PeerTypeContact, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } } else if case .legacyGroup = peer { - return (strings.ChatList_PeerTypeGroup, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil) } else if case let .channel(channel) = peer { if case .group = channel.info { - return (strings.ChatList_PeerTypeGroup, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil) } else { - return (strings.ChatList_PeerTypeChannel, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeChannel), false, false, nil) } } - return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) + return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil) } public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { @@ -4425,10 +4424,9 @@ func chatListItemTags(location: ChatListControllerLocation, accountPeerId: Engin if data.color != nil { let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { - //TODO:release result.append(ChatListItemContent.Tag( id: id, - title: title.text, + title: title, colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue )) } diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index e86e95faad..7949ea417e 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -31,6 +31,8 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? public let handleSpoilers: Bool + public let manualVisibilityControl: Bool + public let resetAnimationsOnVisibilityChange: Bool public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -52,6 +54,8 @@ public final class MultilineTextWithEntitiesComponent: Component { textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, handleSpoilers: Bool = false, + manualVisibilityControl: Bool = false, + resetAnimationsOnVisibilityChange: Bool = false, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -73,6 +77,8 @@ public final class MultilineTextWithEntitiesComponent: Component { self.highlightColor = highlightColor self.highlightAction = highlightAction self.handleSpoilers = handleSpoilers + self.manualVisibilityControl = manualVisibilityControl + self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange self.tapAction = tapAction self.longTapAction = longTapAction } @@ -105,6 +111,12 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.handleSpoilers != rhs.handleSpoilers { return false } + if lhs.manualVisibilityControl != rhs.manualVisibilityControl { + return false + } + if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -151,6 +163,10 @@ public final class MultilineTextWithEntitiesComponent: Component { fatalError("init(coder:) has not been implemented") } + public func updateVisibility(_ isVisible: Bool) { + self.textNode.visibility = isVisible + } + public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedString: NSAttributedString switch component.text { @@ -176,6 +192,8 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.highlightAttributeAction = component.highlightAction self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction + + self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string { if let snapshotView = self.snapshotContentTree() { @@ -189,7 +207,9 @@ public final class MultilineTextWithEntitiesComponent: Component { } } - self.textNode.visibility = true + if !component.manualVisibilityControl { + self.textNode.visibility = true + } if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor { self.textNode.arguments = TextNodeWithEntities.Arguments( context: context, diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift index 8e371e3220..6a0986e23d 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -82,6 +82,7 @@ public final class ListComposePollOptionComponent: Component { public let backspaceKeyAction: (() -> Void)? public let selection: Selection? public let inputMode: InputMode? + public let alwaysDisplayInputModeSelector: Bool public let toggleInputMode: (() -> Void)? public let tag: AnyObject? @@ -100,6 +101,7 @@ public final class ListComposePollOptionComponent: Component { backspaceKeyAction: (() -> Void)?, selection: Selection?, inputMode: InputMode?, + alwaysDisplayInputModeSelector: Bool = false, toggleInputMode: (() -> Void)?, tag: AnyObject? = nil ) { @@ -117,6 +119,7 @@ public final class ListComposePollOptionComponent: Component { self.backspaceKeyAction = backspaceKeyAction self.selection = selection self.inputMode = inputMode + self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector self.toggleInputMode = toggleInputMode self.tag = tag } @@ -158,6 +161,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.inputMode != rhs.inputMode { return false } + if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector { + return false + } return true } @@ -490,15 +496,17 @@ public final class ListComposePollOptionComponent: Component { ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001) } - if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { - animationView.playOnce() + if let animationView = modeSelectorView.contentView as? LottieComponent.View { + if playAnimation { + animationView.playOnce() + } } modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) if let externalState = component.externalState { - let displaySelector = externalState.isEditing + let displaySelector = externalState.isEditing || component.alwaysDisplayInputModeSelector alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index a1d689ba7b..bee75c0ea5 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -165,19 +165,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if let _ = peer as? TelegramUser { status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) } else if let group = peer as? TelegramGroup { - status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) } else if let channel = peer as? TelegramChannel { if case .group = channel.info { if let participantCount = participantCount, participantCount != 0 { - status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(participantCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil) } } else { if let participantCount = participantCount, participantCount != 0 { - status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(participantCount)), multiline: false, isActive: false, icon: nil) } else { - status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil) } } } else { @@ -227,7 +227,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { let text: String text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count)) - status = .custom(string: text, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil) } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 4d28125285..661c3fdedf 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -674,7 +674,7 @@ public class ContactsController: ViewController { let text = self.presentationData.strings.ContactList_DeletedContacts(Int32(peerIds.count)) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let self else { return false } diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 165ea48bd3..504582fbac 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -52,7 +52,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder): let status: ContactsPeerItemStatus if count != 0 { - status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: strings.Contacts_ImportersCount(count)), multiline: false, isActive: false, icon: nil) } else { status = .none } diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index 921376ec47..e4edd69b3b 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -32,6 +32,8 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/MoreButtonNode", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 3bc9738206..d385ecd5ff 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -21,6 +21,8 @@ import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent import MoreButtonNode +import TextFormat +import TextNodeWithEntities public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -39,7 +41,7 @@ public enum ContactsPeerItemStatus { case none case presence(EnginePeer.Presence, PresentationDateTimeFormat) case addressName(String) - case custom(string: String, multiline: Bool, isActive: Bool, icon: Icon?) + case custom(string: NSAttributedString, multiline: Bool, isActive: Bool, icon: Icon?) } public enum ContactsPeerItemSelection: Equatable { @@ -437,7 +439,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var credibilityIconComponent: EmojiStatusComponent? private var verifiedIconView: ComponentHostView? private var verifiedIconComponent: EmojiStatusComponent? - public let statusNode: TextNode + public let statusNode: TextNodeWithEntities private var statusIconNode: ASImageNode? private var badgeBackgroundNode: ASImageNode? private var badgeTextNode: TextNode? @@ -519,6 +521,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: avatarIconView.bounds.size ) } + self.statusNode.visibilityRect = self.visibilityStatus == false ? CGRect.zero : CGRect.infinite } } } @@ -554,7 +557,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.avatarNode.isLayerBacked = false self.titleNode = TextNode() - self.statusNode = TextNode() + self.statusNode = TextNodeWithEntities() super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) @@ -575,7 +578,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.avatarNodeContainer.addSubnode(self.avatarNode) self.offsetContainerNode.addSubnode(self.avatarNodeContainer) self.offsetContainerNode.addSubnode(self.titleNode) - self.offsetContainerNode.addSubnode(self.statusNode) + self.offsetContainerNode.addSubnode(self.statusNode.textNode) self.addSubnode(self.maskNode) @@ -708,7 +711,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool, Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeStatusLayout = TextNodeWithEntities.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) @@ -939,7 +942,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } case let .custom(text, multiline, isActive, icon): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + let statusAttributedStringValue = NSMutableAttributedString(string: text.string) + statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: statusAttributedStringValue.length)) + statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: statusAttributedStringValue.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + statusAttributedStringValue.addAttribute(key, value: value, range: range) + } + } + }) + + statusAttributedString = statusAttributedStringValue statusIcon = icon statusIsActive = isActive multilineStatus = multiline @@ -964,7 +984,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { switch item.status { case let .custom(text, multiline, isActive, icon): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + let statusAttributedStringValue = NSMutableAttributedString(string: "") + statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: text.length)) + statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: text.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + statusAttributedStringValue.addAttribute(key, value: value, range: range) + } + } + }) + statusAttributedString = statusAttributedStringValue multilineStatus = multiline statusIsActive = isActive statusIcon = icon @@ -1352,6 +1388,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() var titleLeftOffset: CGFloat = 0.0 + var nextIconX: CGFloat = titleFrame.maxX if let verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1375,17 +1412,30 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { emojiFileUpdated: nil ) strongSelf.verifiedIconComponent = verifiedIconComponent + + let iconOrigin: CGFloat + if case .animation = verifiedIcon { + iconOrigin = titleFrame.minX + } else { + nextIconX += 4.0 + iconOrigin = nextIconX + } + let containerSize = CGSize(width: 16.0, height: 16.0) let iconSize = verifiedIconView.update( transition: .immediate, component: AnyComponent(verifiedIconComponent), environment: {}, - containerSize: CGSize(width: 16.0, height: 16.0) + containerSize: containerSize ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - titleLeftOffset += iconSize.width + 4.0 + if case .animation = verifiedIcon { + titleLeftOffset += iconSize.width + 4.0 + } else { + nextIconX += iconSize.width + } } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() @@ -1395,17 +1445,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 - strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 + strongSelf.statusNode.textNode.alpha = item.enabled ? 1.0 : 1.0 - let _ = statusApply() + strongSelf.statusNode.visibilityRect = strongSelf.visibilityStatus == false ? CGRect.zero : CGRect.infinite + let _ = statusApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: false + )) var statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size) if let statusIconImage { statusFrame.origin.x += statusIconImage.size.width + 1.0 } - let previousStatusFrame = strongSelf.statusNode.frame + let previousStatusFrame = strongSelf.statusNode.textNode.frame - strongSelf.statusNode.frame = statusFrame - transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) + strongSelf.statusNode.textNode.frame = statusFrame + transition.animatePositionAdditive(node: strongSelf.statusNode.textNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) if let statusIconImage { let statusIconNode: ASImageNode @@ -1413,7 +1470,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusIconNode = current } else { statusIconNode = ASImageNode() - strongSelf.statusNode.addSubnode(statusIconNode) + strongSelf.statusNode.textNode.addSubnode(statusIconNode) } statusIconNode.image = statusIconImage statusIconNode.frame = CGRect(origin: CGPoint(x: -statusIconImage.size.width - 1.0, y: floor((statusFrame.height - statusIconImage.size.height) / 2.0) + 1.0), size: statusIconImage.size) @@ -1424,7 +1481,6 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - var nextIconX: CGFloat = titleFrame.maxX if let credibilityIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 717558faf1..2862676fbd 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -317,7 +317,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking if !self.item.entities.isEmpty { let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in if case let .CustomEmoji(_, fileId) = entity.type { - return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range) + return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range) } return nil }) diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 2650b1ca4f..e1547a1427 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -106,6 +106,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case disableCallV2(Bool) case experimentalCallMute(Bool) case conferenceCalls(Bool) + case playerV2(Bool) case benchmarkReflectors case enableLocalTranslation(Bool) case preferredVideoCodec(Int, String, String?, Bool) @@ -132,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .conferenceCalls, .benchmarkReflectors, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .conferenceCalls, .playerV2, .benchmarkReflectors, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -251,12 +252,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 52 case .conferenceCalls: return 53 - case .benchmarkReflectors: + case .playerV2: return 54 - case .enableLocalTranslation: + case .benchmarkReflectors: return 55 + case .enableLocalTranslation: + return 56 case let .preferredVideoCodec(index, _, _, _): - return 56 + index + return 57 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -1355,6 +1358,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .playerV2(value): + return ItemListSwitchItem(presentationData: presentationData, title: "PlayerV2", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.playerV2 = value + return PreferencesEntry(settings) + }) + }).start() + }) case .benchmarkReflectors: return ItemListActionItem(presentationData: presentationData, title: "Benchmark Reflectors", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { @@ -1578,6 +1591,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) entries.append(.conferenceCalls(experimentalSettings.conferenceCalls)) + entries.append(.playerV2(experimentalSettings.playerV2)) entries.append(.benchmarkReflectors) entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation)) diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 87cf1152c5..c8ddef511c 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -126,7 +126,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } let attributedString = NSMutableAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center) - if let range = attributedString.string.range(of: "⭐️") { + if let range = attributedString.string.range(of: "$") { attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.foregroundColor, value: color, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index e73f00da42..99f3629b04 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -330,7 +330,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private func setupWithVideo(_ file: TelegramMediaFile) { let videoNode = UniversalVideoNode( - accountId: self.context.account.id, + context: self.context, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodec.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodec.h index b4e1eb3014..222c2d70c6 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodec.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodec.h @@ -4,7 +4,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FFMpegAVCodec : NSObject -+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId; ++ (FFMpegAVCodec * _Nullable)findForId:(int)codecId preferHardwareAccelerationCapable:(bool)preferHardwareAccelerationCapable; - (void *)impl; diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h index edc65d22d4..276e93d841 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h @@ -25,6 +25,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVCodecContextReceiveResult) - (bool)open; - (bool)sendEnd; +- (void)setupHardwareAccelerationIfPossible; - (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame; - (void)flushBuffers; diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h index 3b244d6fb8..f1749fabbb 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h @@ -12,6 +12,11 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) { FFMpegAVFramePixelFormatYUVA }; +typedef NS_ENUM(NSUInteger, FFMpegAVFrameNativePixelFormat) { + FFMpegAVFrameNativePixelFormatUnknown, + FFMpegAVFrameNativePixelFormatVideoToolbox +}; + @interface FFMpegAVFrame : NSObject @property (nonatomic, readonly) int32_t width; @@ -27,6 +32,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) { - (instancetype)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height; - (void *)impl; +- (FFMpegAVFrameNativePixelFormat)nativePixelFormat; @end diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m index 0ea2b101a6..22936d3b6b 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m @@ -18,7 +18,26 @@ return self; } -+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId { ++ (FFMpegAVCodec * _Nullable)findForId:(int)codecId preferHardwareAccelerationCapable:(_Bool)preferHardwareAccelerationCapable { + if (preferHardwareAccelerationCapable && codecId == AV_CODEC_ID_AV1) { + void *codecIterationState = nil; + while (true) { + AVCodec const *codec = av_codec_iterate(&codecIterationState); + if (!codec) { + break; + } + if (!av_codec_is_decoder(codec)) { + continue; + } + if (codec->id != codecId) { + continue; + } + if (strncmp(codec->name, "av1", 2) == 0) { + return [[FFMpegAVCodec alloc] initWithImpl:codec]; + } + } + } + AVCodec const *codec = avcodec_find_decoder(codecId); if (codec) { return [[FFMpegAVCodec alloc] initWithImpl:codec]; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m index cf02867ebe..ee2cde92c0 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m @@ -6,6 +6,10 @@ #import "libavformat/avformat.h" #import "libavcodec/avcodec.h" +static enum AVPixelFormat getPreferredPixelFormat(__unused AVCodecContext *ctx, __unused const enum AVPixelFormat *pix_fmts) { + return AV_PIX_FMT_VIDEOTOOLBOX; +} + @interface FFMpegAVCodecContext () { FFMpegAVCodec *_codec; AVCodecContext *_impl; @@ -61,6 +65,11 @@ return status == 0; } +- (void)setupHardwareAccelerationIfPossible { + av_hwdevice_ctx_create(&_impl->hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nil, nil, 0); + _impl->get_format = getPreferredPixelFormat; +} + - (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame { int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]); if (status == 0) { diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m index 754f88f230..fda8224dc7 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m @@ -64,6 +64,17 @@ return _impl->pts; } +- (FFMpegAVFrameNativePixelFormat)nativePixelFormat { + switch (_impl->format) { + case AV_PIX_FMT_VIDEOTOOLBOX: { + return FFMpegAVFrameNativePixelFormatVideoToolbox; + } + default: { + return FFMpegAVFrameNativePixelFormatUnknown; + } + } +} + - (int64_t)duration { #if LIBAVFORMAT_VERSION_MAJOR >= 59 return _impl->duration; diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index a3df6c7ceb..4ea9a2540d 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -259,7 +259,7 @@ public func galleryItemForEntry( } if isHLS { - content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos, codecConfiguration: HLSCodecConfiguration(context: context)) + content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos, autoFetchFullSizeThumbnail: true, codecConfiguration: HLSCodecConfiguration(context: context)) } else { content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 40c8ed7a7e..47da141849 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1762,7 +1762,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoScale: CGFloat if item.content is WebEmbedVideoContent { videoScale = 1.0 @@ -2849,7 +2849,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let baseNavigationController = self.baseNavigationController() let mediaManager = self.context.sharedContext.mediaManager var expandImpl: (() -> Void)? - let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { + let overlayNode = OverlayUniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) @@ -3073,7 +3073,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { 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) + let overlayVideoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil) overlayVideoNode.frame = absoluteRect overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate) @@ -3156,7 +3156,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { shouldBeDismissed = .single(false) } - let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { + let overlayNode = OverlayUniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 1e55bb8ec9..8f2967487c 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1449,7 +1449,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } } } - self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) + self.context.sharedContext.mediaManager.setPlaylist((self.context, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) return } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index cdbc22dc5f..12efc18acd 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -58,7 +58,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler fileValue = file } - self.videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true) + self.videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true) self.videoNode.isUserInteractionEnabled = false self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 286813dbf3..5aebfeb694 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -62,7 +62,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case peer(EnginePeer.Id) } - case header(String) + case header(NSAttributedString) case mainLinkHeader(String) case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) @@ -225,13 +225,13 @@ private enum InviteLinksListEntry: ItemListNodeEntry { private func folderInviteLinkListControllerEntries( presentationData: PresentationData, state: FolderInviteLinkListControllerState, - title: String, + title: ChatFolderTitle, allPeers: [EnginePeer] ) -> [InviteLinksListEntry] { var entries: [InviteLinksListEntry] = [] var infoString: String? - let chatCountString: String + let chatCountString: NSAttributedString let peersHeaderString: String let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) @@ -241,16 +241,36 @@ private func folderInviteLinkListControllerEntries( if !canShareChats { infoString = presentationData.strings.FolderLinkScreen_TitleDescriptionUnavailable - chatCountString = presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable + chatCountString = NSAttributedString(string: presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable) peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderUnavailable } else if state.selectedPeerIds.isEmpty { - chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionDeselected(title).string + let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionDeselectedV2) + let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: folderRange, with: "") + chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) + } + + chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeader if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll } } else { - chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionSelected(title, presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count))).string + let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedV2) + let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: folderRange, with: "") + chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) + } + let chatsRange = (chatCountStringValue.string as NSString).range(of: "{chats}") + if chatsRange.location != NSNotFound { + chatCountStringValue.replaceCharacters(in: chatsRange, with: "") + let countValue = presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count)) + chatCountStringValue.insert(NSAttributedString(string: countValue), at: chatsRange.location) + } + + chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderSelected(Int32(state.selectedPeerIds.count)) if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll @@ -699,11 +719,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - //TODO:release let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries( presentationData: presentationData, state: state, - title: filterTitle.text, + title: filterTitle, allPeers: allPeers ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 60730c0c5c..d51c7950ae 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -11,18 +11,19 @@ import TelegramAnimatedStickerNode import AccountContext import Markdown import TextFormat +import TextNodeWithEntities public class InviteLinkHeaderItem: ListViewItem, ItemListItem { public let context: AccountContext public let theme: PresentationTheme public let title: String? - public let text: String + public let text: NSAttributedString public let animationName: String public let hideOnSmallScreens: Bool public let sectionId: ItemListSectionId public let linkAction: ((ItemListTextItemLinkAction) -> Void)? - public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: String, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: NSAttributedString, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { self.context = context self.theme = theme self.title = title @@ -75,7 +76,7 @@ private let textFont = Font.regular(14.0) class InviteLinkHeaderItemNode: ListViewItemNode { private let titleNode: TextNode - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var animationNode: AnimatedStickerNode private var item: InviteLinkHeaderItem? @@ -86,17 +87,17 @@ class InviteLinkHeaderItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .left - self.textNode.contentsScale = UIScreen.main.scale + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .left + self.textNode.textNode.contentsScale = UIScreen.main.scale self.animationNode = DefaultAnimatedStickerNodeImpl() super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.animationNode) } @@ -112,7 +113,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode { func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) return { item, params, neighbors in let leftInset: CGFloat = 24.0 + params.leftInset @@ -131,9 +132,22 @@ class InviteLinkHeaderItemNode: ListViewItemNode { let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) - let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - })) + let attributedText = NSMutableAttributedString(string: item.text.string) + attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: attributedText.length)) + attributedText.addAttribute(.foregroundColor, value: item.theme.list.freeTextColor, range: NSRange(location: 0, length: attributedText.length)) + item.text.enumerateAttributes(in: NSRange(location: 0, length: item.text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + attributedText.addAttribute(key, value: value, range: range) + } + } + }) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -168,8 +182,15 @@ class InviteLinkHeaderItemNode: ListViewItemNode { origin += titleLayout.size.height + spacing } - let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size) + let _ = textApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: item.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size) + strongSelf.textNode.visibilityRect = .infinite } }) } @@ -189,9 +210,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let textFrame = self.textNode.frame + let textFrame = self.textNode.textNode.frame if let item = self.item, textFrame.contains(location) { - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) { + if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { item.linkAction?(.tap(url)) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index d2db511cc2..62acacd470 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -56,7 +56,7 @@ private enum InviteLinksListSection: Int32 { } private enum InviteLinksListEntry: ItemListNodeEntry { - case header(PresentationTheme, String) + case header(PresentationTheme, NSAttributedString) case mainLinkHeader(PresentationTheme, String) case mainLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool) @@ -278,7 +278,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, } else { helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp } - entries.append(.header(presentationData.theme, helpText)) + entries.append(.header(presentationData.theme, NSAttributedString(string: helpText))) } let mainInvite: ExportedInvitation? diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 2b592ce5ab..5789250a20 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -100,7 +100,7 @@ private enum InviteRequestsEntry: ItemListNodeEntry { let arguments = arguments as! InviteRequestsControllerArguments switch self { case let .header(theme, text): - return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: text, animationName: "Requests", sectionId: self.section, linkAction: { _ in + return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: NSAttributedString(string: text), animationName: "Requests", sectionId: self.section, linkAction: { _ in arguments.openLinks() }) case let .requestsHeader(_, text): diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 4a908a11b7..3882bae635 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -1436,6 +1436,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size) var titleLeftOffset: CGFloat = 0.0 + var nextIconX: CGFloat = titleFrame.maxX if let verifiedIcon = verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1461,6 +1462,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo emojiFileUpdated: nil ) strongSelf.verifiedIconComponent = verifiedIconComponent + + let iconOrigin: CGFloat + if case .animation = verifiedIcon { + iconOrigin = titleFrame.minX + } else { + nextIconX += 4.0 + iconOrigin = nextIconX + } + let iconSize = verifiedIconView.update( transition: .immediate, component: AnyComponent(verifiedIconComponent), @@ -1468,9 +1478,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo containerSize: CGSize(width: 20.0, height: 20.0) ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - - titleLeftOffset += iconSize.width + 4.0 + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + + if case .animation = verifiedIcon { + titleLeftOffset += iconSize.width + 4.0 + } else { + nextIconX += iconSize.width + } } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() @@ -1512,7 +1526,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo containerSize: CGSize(width: 20.0, height: 20.0) ) - transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += 4.0 + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) } else if let credibilityIconView = strongSelf.credibilityIconView { strongSelf.credibilityIconView = nil credibilityIconView.removeFromSuperview() diff --git a/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift b/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift index 3b834bba58..ad0f9b8033 100644 --- a/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift +++ b/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift @@ -10,7 +10,7 @@ public func convertOpusToAAC(sourcePath: String, allocateTempFile: @escaping () queue.async { do { - let audioSource = SoftwareAudioSource(path: sourcePath, focusedPart: nil) + let audioSource = SoftwareAudioSource(path: sourcePath) let outputPath = allocateTempFile() diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index 318c28c7c5..af588856e6 100644 --- a/submodules/MediaPlayer/BUILD +++ b/submodules/MediaPlayer/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/YuvConversion:YuvConversion", "//submodules/Utils/RangeSet:RangeSet", "//submodules/TextFormat:TextFormat", + "//submodules/ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift index 70e2969fef..2d363ac114 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift @@ -9,7 +9,7 @@ import TelegramAudio public final class ChunkMediaPlayerPart { public enum Id: Hashable { case tempFile(path: String) - case directFile(path: String, audio: DirectStream?, video: DirectStream?) + case directStream } public struct DirectStream: Hashable { @@ -26,51 +26,29 @@ public final class ChunkMediaPlayerPart { } } - public enum Content { - public final class TempFile { - public let file: TempBoxFile - - public init(file: TempBoxFile) { - self.file = file - } - - deinit { - TempBox.shared.dispose(self.file) - } + public final class TempFile { + public let file: TempBoxFile + + public init(file: TempBoxFile) { + self.file = file } - public final class FFMpegDirectFile { - public let path: String - public let audio: DirectStream? - public let video: DirectStream? - - public init(path: String, audio: DirectStream?, video: DirectStream?) { - self.path = path - self.audio = audio - self.video = video - } + deinit { + TempBox.shared.dispose(self.file) } - - case tempFile(TempFile) - case directFile(FFMpegDirectFile) } public let startTime: Double public let endTime: Double - public let content: Content + public let content: TempFile public let clippedStartTime: Double? public let codecName: String? public var id: Id { - switch self.content { - case let .tempFile(tempFile): - return .tempFile(path: tempFile.file.path) - case let .directFile(directFile): - return .directFile(path: directFile.path, audio: directFile.audio, video: directFile.video) - } + return .tempFile(path: self.content.file.path) } - public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, content: Content, codecName: String?) { + public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, content: TempFile, codecName: String?) { self.startTime = startTime self.clippedStartTime = clippedStartTime self.endTime = endTime @@ -80,12 +58,63 @@ public final class ChunkMediaPlayerPart { } public final class ChunkMediaPlayerPartsState { - public let duration: Double? - public let parts: [ChunkMediaPlayerPart] + public final class DirectReader { + public struct Stream { + public let mediaBox: MediaBox + public let resource: MediaResource + public let size: Int64 + public let index: Int + public let seek: (streamIndex: Int, pts: Int64) + public let maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)? + public let codecName: String? + + public init(mediaBox: MediaBox, resource: MediaResource, size: Int64, index: Int, seek: (streamIndex: Int, pts: Int64), maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?, codecName: String?) { + self.mediaBox = mediaBox + self.resource = resource + self.size = size + self.index = index + self.seek = seek + self.maxReadablePts = maxReadablePts + self.codecName = codecName + } + } + + public final class Impl { + public let video: Stream? + public let audio: Stream? + + public init(video: Stream?, audio: Stream?) { + self.video = video + self.audio = audio + } + } + + public let id: Int + public let seekPosition: Double + public let availableUntilPosition: Double + public let bufferedUntilEnd: Bool + public let impl: Impl? + + public init(id: Int, seekPosition: Double, availableUntilPosition: Double, bufferedUntilEnd: Bool, impl: Impl?) { + self.id = id + self.seekPosition = seekPosition + self.availableUntilPosition = availableUntilPosition + self.bufferedUntilEnd = bufferedUntilEnd + self.impl = impl + } + } - public init(duration: Double?, parts: [ChunkMediaPlayerPart]) { + public enum Content { + case parts([ChunkMediaPlayerPart]) + case directReader(DirectReader) + } + + public let duration: Double? + public let content: Content + + public init(duration: Double?, content: Content) { self.duration = duration - self.parts = parts + self.content = content } } diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift index 104868c3ad..411cd10786 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift @@ -6,390 +6,454 @@ import TelegramCore import FFMpegBinding import RangeSet -private final class FFMpegMediaFrameExtractContext { - let fd: Int32 - var readPosition: Int = 0 - let size: Int +private func FFMpegLookaheadReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - var accessedRanges = RangeSet() - var maskRanges: RangeSet? - var recordAccessedRanges = false + let readCount = min(256 * 1024, Int64(bufferSize)) + let requestRange: Range = context.readingOffset ..< (context.readingOffset + readCount) - init(fd: Int32, size: Int) { - self.fd = fd - self.size = size - } -} - -private func FFMpegMediaFrameExtractContextReadPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { - let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - if context.recordAccessedRanges { - context.accessedRanges.insert(contentsOf: context.readPosition ..< (context.readPosition + Int(bufferSize))) + var fetchedData: Data? + let fetchDisposable = MetaDisposable() + + let semaphore = DispatchSemaphore(value: 0) + let disposable = context.params.getDataInRange(requestRange, { data in + if let data { + fetchedData = data + semaphore.signal() + } + }) + var isCancelled = false + let cancelDisposable = context.params.cancel.start(next: { _ in + isCancelled = true + semaphore.signal() + }) + semaphore.wait() + + if isCancelled { + context.isCancelled = true } - let result: Int - if let maskRanges = context.maskRanges { - let readRange = context.readPosition ..< (context.readPosition + Int(bufferSize)) - let _ = maskRanges - let _ = readRange - result = read(context.fd, buffer, Int(bufferSize)) + disposable.dispose() + cancelDisposable.dispose() + fetchDisposable.dispose() + + if let fetchedData = fetchedData { + fetchedData.withUnsafeBytes { byteBuffer -> Void in + guard let bytes = byteBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return + } + memcpy(buffer, bytes, fetchedData.count) + } + let fetchedCount = Int32(fetchedData.count) + context.setReadingOffset(offset: context.readingOffset + Int64(fetchedCount)) + if fetchedCount == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + return fetchedCount } else { - result = read(context.fd, buffer, Int(bufferSize)) - } - context.readPosition += Int(bufferSize) - if result == 0 { return FFMPEG_CONSTANT_AVERROR_EOF } - return Int32(result) } -private func FFMpegMediaFrameExtractContextSeekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { - let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() +private func FFMpegLookaheadReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() if (whence & FFMPEG_AVSEEK_SIZE) != 0 { - return Int64(context.size) + return context.params.size } else { - context.readPosition = Int(offset) - lseek(context.fd, off_t(offset), SEEK_SET) + context.setReadingOffset(offset: offset) + return offset } } -private struct FFMpegFrameSegment { - struct Stream { - let index: Int - let startPts: CMTime - let startPosition: Int64 - var endPts: CMTime - var endPosition: Int64 - var duration: Double - } - - var audio: Stream? - var video: Stream? - - init() { - } - - mutating func addFrame(isVideo: Bool, index: Int, pts: CMTime, duration: Double, position: Int64, size: Int64) { - if var stream = isVideo ? self.video : self.audio { - stream.endPts = pts - stream.duration += duration - stream.endPosition = max(stream.endPosition, position + size) - if isVideo { - self.video = stream - } else { - self.audio = stream - } - } else { - let stream = Stream(index: index, startPts: pts, startPosition: position, endPts: pts, endPosition: position + size, duration: duration) - if isVideo { - self.video = stream - } else { - self.audio = stream - } - } - } +private func range(_ outer: Range, fullyContains inner: Range) -> Bool { + return inner.lowerBound >= outer.lowerBound && inner.upperBound <= outer.upperBound } -private final class FFMpegFrameSegmentInfo { - let headerAccessRanges: RangeSet - let segments: [FFMpegFrameSegment] +private final class FFMpegLookaheadReader { + let params: FFMpegLookaheadThread.Params - init(headerAccessRanges: RangeSet, segments: [FFMpegFrameSegment]) { - self.headerAccessRanges = headerAccessRanges - self.segments = segments - } -} - -private func extractFFMpegFrameSegmentInfo(path: String) -> FFMpegFrameSegmentInfo? { - let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + var avIoContext: FFMpegAVIOContext? + var avFormatContext: FFMpegAVFormatContext? - var s = stat() - stat(path, &s) - let size = Int32(s.st_size) + var audioStream: FFMpegFileReader.StreamInfo? + var videoStream: FFMpegFileReader.StreamInfo? - let fd = open(path, O_RDONLY, S_IRUSR) - if fd < 0 { - return nil - } - defer { - close(fd) - } + var seekInfo: FFMpegLookaheadThread.State.Seek? + var maxReadPts: FFMpegLookaheadThread.State.Seek? + var audioStreamState: FFMpegLookaheadThread.StreamState? + var videoStreamState: FFMpegLookaheadThread.StreamState? - let avFormatContext = FFMpegAVFormatContext() - let ioBufferSize = 32 * 1024 + var reportedState: FFMpegLookaheadThread.State? - let context = FFMpegMediaFrameExtractContext(fd: fd, size: Int(size)) - context.recordAccessedRanges = true + var readingOffset: Int64 = 0 + var isCancelled: Bool = false + var isEnded: Bool = false - guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(context).toOpaque(), readPacket: FFMpegMediaFrameExtractContextReadPacketCallback, writePacket: nil, seek: FFMpegMediaFrameExtractContextSeekCallback, isSeekable: true) else { - return nil - } + private var currentFetchRange: Range? + private var currentFetchDisposable: Disposable? - avFormatContext.setIO(avIoContext) + var currentTimestamp: Double? - if !avFormatContext.openInput(withDirectFilePath: nil) { - return nil - } - - if !avFormatContext.findStreamInfo() { - return nil - } - - var audioStream: FFMpegMediaInfo.Info? - var videoStream: FFMpegMediaInfo.Info? - - for typeIndex in 0 ..< 2 { - let isVideo = typeIndex == 0 + init?(params: FFMpegLookaheadThread.Params) { + self.params = params - for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) { - let streamIndex = streamIndexNumber.int32Value - if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { - continue - } - - let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) - let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) - - let startTime: CMTime - let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) - if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { - startTime = CMTime(value: 0, timescale: timebase.timescale) - } else { - startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) - } - var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) - duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime)) - - var codecName: String? - let codecId = avFormatContext.codecId(atStreamIndex: streamIndex) - if codecId == FFMpegCodecIdMPEG4 { - codecName = "mpeg4" - } else if codecId == FFMpegCodecIdH264 { - codecName = "h264" - } else if codecId == FFMpegCodecIdHEVC { - codecName = "hevc" - } else if codecId == FFMpegCodecIdAV1 { - codecName = "av1" - } else if codecId == FFMpegCodecIdVP9 { - codecName = "vp9" - } else if codecId == FFMpegCodecIdVP8 { - codecName = "vp8" - } - - let info = FFMpegMediaInfo.Info( - index: Int(streamIndex), - timescale: timebase.timescale, - startTime: startTime, - duration: duration, - fps: fps, - codecName: codecName - ) - - if isVideo { - videoStream = info - } else { - audioStream = info - } + let ioBufferSize = 64 * 1024 + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegLookaheadReader_readPacketCallback, writePacket: nil, seek: FFMpegLookaheadReader_seekCallback, isSeekable: true) else { + return nil } - } - - var segments: [FFMpegFrameSegment] = [] - let maxSegmentDuration: Double = 5.0 - - if let videoStream { - let indexEntryCount = avFormatContext.numberOfIndexEntries(atStreamIndex: Int32(videoStream.index)) + self.avIoContext = avIoContext - if indexEntryCount > 0 { - let frameDuration = 1.0 / videoStream.fps.seconds - - var indexEntry = FFMpegAVIndexEntry() - for i in 0 ..< indexEntryCount { - if !avFormatContext.fillIndexEntry(atStreamIndex: Int32(videoStream.index), entryIndex: Int32(i), outEntry: &indexEntry) { + let avFormatContext = FFMpegAVFormatContext() + avFormatContext.setIO(avIoContext) + + if !avFormatContext.openInput(withDirectFilePath: nil) { + return nil + } + if !avFormatContext.findStreamInfo() { + return nil + } + + self.avFormatContext = avFormatContext + + var audioStream: FFMpegFileReader.StreamInfo? + var videoStream: FFMpegFileReader.StreamInfo? + + for streamType in 0 ..< 2 { + let isVideo = streamType == 0 + for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) { + let streamIndex = streamIndexNumber.int32Value + if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { continue } - let packetPts = CMTime(value: indexEntry.timestamp, timescale: videoStream.timescale) - //print("index: \(packetPts.seconds), isKeyframe: \(indexEntry.isKeyframe), position: \(indexEntry.pos), size: \(indexEntry.size)") + let codecId = avFormatContext.codecId(atStreamIndex: streamIndex) - var startNewSegment = segments.isEmpty - if indexEntry.isKeyframe { - if segments.isEmpty { - startNewSegment = true - } else if let video = segments[segments.count - 1].video { - if packetPts.seconds - video.startPts.seconds > maxSegmentDuration { - startNewSegment = true - } - } - } + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) - if startNewSegment { - segments.append(FFMpegFrameSegment()) + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) } - segments[segments.count - 1].addFrame(isVideo: true, index: videoStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size)) - } - if !segments.isEmpty, let video = segments[segments.count - 1].video { - if video.endPts.seconds + 1.0 / videoStream.fps.seconds + 0.001 < videoStream.duration.seconds { - segments[segments.count - 1].video?.duration = videoStream.duration.seconds - video.startPts.seconds - segments[segments.count - 1].video?.endPts = videoStream.duration + var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) + duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime)) + + //let metrics = avFormatContext.metricsForStream(at: streamIndex) + //let rotationAngle: Double = metrics.rotationAngle + //let aspect = Double(metrics.width) / Double(metrics.height) + + let stream = FFMpegFileReader.StreamInfo( + index: streamIndexNumber.intValue, + codecId: codecId, + startTime: startTime, + duration: duration, + timeBase: timebase.value, + timeScale: timebase.timescale, + fps: fps + ) + + if isVideo { + videoStream = stream + } else { + audioStream = stream } } } + + self.audioStream = audioStream + self.videoStream = videoStream + + if let preferredStream = self.videoStream ?? self.audioStream { + let pts = CMTimeMakeWithSeconds(params.seekToTimestamp, preferredTimescale: preferredStream.timeScale) + self.seekInfo = FFMpegLookaheadThread.State.Seek(streamIndex: preferredStream.index, pts: pts.value) + avFormatContext.seekFrame(forStreamIndex: Int32(preferredStream.index), pts: pts.value, positionOnKeyframe: true) + } + + self.updateCurrentTimestamp() } - if let audioStream { - let indexEntryCount = avFormatContext.numberOfIndexEntries(atStreamIndex: Int32(audioStream.index)) - if indexEntryCount > 0 { - var minSegmentIndex = 0 - var minSegmentStartTime: Double = -100000.0 - - let frameDuration = 1.0 / audioStream.fps.seconds - - var indexEntry = FFMpegAVIndexEntry() - for i in 0 ..< indexEntryCount { - if !avFormatContext.fillIndexEntry(atStreamIndex: Int32(audioStream.index), entryIndex: Int32(i), outEntry: &indexEntry) { - continue - } - - let packetPts = CMTime(value: indexEntry.timestamp, timescale: audioStream.timescale) - //print("index: \(packetPts.value), timestamp: \(packetPts.seconds), isKeyframe: \(indexEntry.isKeyframe), position: \(indexEntry.pos), size: \(indexEntry.size)") - - if videoStream != nil { - for i in minSegmentIndex ..< segments.count { - if let video = segments[i].video { - if minSegmentStartTime <= packetPts.seconds && video.endPts.seconds >= packetPts.seconds { - segments[i].addFrame(isVideo: false, index: audioStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size)) - if minSegmentIndex != i { - minSegmentIndex = i - minSegmentStartTime = video.startPts.seconds - } - break - } - } + + deinit { + self.currentFetchDisposable?.dispose() + } + + func setReadingOffset(offset: Int64) { + self.readingOffset = offset + + let readRange: Range = offset ..< (offset + 512 * 1024) + if !self.params.isDataCachedInRange(readRange) { + if let currentFetchRange = self.currentFetchRange { + if currentFetchRange.overlaps(readRange) { + if !range(currentFetchRange, fullyContains: readRange) { + self.setFetchRange(range: currentFetchRange.lowerBound ..< max(currentFetchRange.upperBound, readRange.upperBound + 2 * 1024 * 1024)) } } else { - if segments.isEmpty { - segments.append(FFMpegFrameSegment()) - } - segments[segments.count - 1].addFrame(isVideo: false, index: audioStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size)) + self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024)) } - } - } - if !segments.isEmpty, let audio = segments[segments.count - 1].audio { - if audio.endPts.seconds + 0.001 < audioStream.duration.seconds { - segments[segments.count - 1].audio?.duration = audioStream.duration.seconds - audio.startPts.seconds - segments[segments.count - 1].audio?.endPts = audioStream.duration + } else { + self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024)) } } } - let headerAccessRanges = context.accessedRanges + private func setFetchRange(range: Range) { + if self.currentFetchRange != range { + self.currentFetchRange = range + + self.currentFetchDisposable?.dispose() + self.currentFetchDisposable = self.params.fetchInRange(range) + } + } - for i in 1 ..< segments.count { - let segment = segments[i] + func updateCurrentTimestamp() { + self.currentTimestamp = self.params.currentTimestamp.with({ $0 }) - if let video = segment.video { - context.maskRanges = headerAccessRanges - context.maskRanges?.insert(contentsOf: Int(video.startPosition) ..< Int(video.endPosition)) + self.updateReadIfNeeded() + } + + private func updateReadIfNeeded() { + guard let avFormatContext = self.avFormatContext else { + return + } + guard let currentTimestamp = self.currentTimestamp else { + return + } + + let maxPtsSeconds = max(self.params.seekToTimestamp, currentTimestamp) + 10.0 + + var currentAudioPtsSecondsAdvanced: Double = 0.0 + var currentVideoPtsSecondsAdvanced: Double = 0.0 + + let packet = FFMpegPacket() + while !self.isCancelled && !self.isEnded { + var audioAlreadyRead: Bool = false + var videoAlreadyRead: Bool = false - context.accessedRanges = RangeSet() - context.recordAccessedRanges = true - - avFormatContext.seekFrame(forStreamIndex: Int32(video.index), byteOffset: video.startPosition) - - let packet = FFMpegPacket() - while true { - if !avFormatContext.readFrame(into: packet) { - break - } - - if Int(packet.streamIndex) == video.index { - let packetPts = CMTime(value: packet.pts, timescale: video.startPts.timescale) - if packetPts.value >= video.endPts.value { - break - } + if let audioStreamState = self.audioStreamState { + if audioStreamState.readableToTime.seconds >= maxPtsSeconds { + audioAlreadyRead = true } + } else if self.audioStream == nil { + audioAlreadyRead = true } - print("Segment \(i): \(video.startPosition) ..< \(video.endPosition) accessed \(context.accessedRanges.ranges)") + if let videoStreamState = self.videoStreamState { + if videoStreamState.readableToTime.seconds >= maxPtsSeconds { + videoAlreadyRead = true + } + } else if self.videoStream == nil { + videoAlreadyRead = true + } + + if audioAlreadyRead && videoAlreadyRead { + break + } + + if !avFormatContext.readFrame(into: packet) { + self.isEnded = true + break + } + + self.maxReadPts = FFMpegLookaheadThread.State.Seek(streamIndex: Int(packet.streamIndex), pts: packet.pts) + + if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index { + let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timeScale) + if let audioStreamState = self.audioStreamState { + currentAudioPtsSecondsAdvanced += pts.seconds - audioStreamState.readableToTime.seconds + } + self.audioStreamState = FFMpegLookaheadThread.StreamState( + info: audioStream, + readableToTime: pts + ) + } else if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index { + let pts = CMTimeMake(value: packet.pts, timescale: videoStream.timeScale) + if let videoStreamState = self.videoStreamState { + currentVideoPtsSecondsAdvanced += pts.seconds - videoStreamState.readableToTime.seconds + } + self.videoStreamState = FFMpegLookaheadThread.StreamState( + info: videoStream, + readableToTime: pts + ) + } + + if min(currentAudioPtsSecondsAdvanced, currentVideoPtsSecondsAdvanced) >= 0.1 { + self.reportStateIfNeeded() + } + } + + self.reportStateIfNeeded() + } + + private func reportStateIfNeeded() { + guard let seekInfo = self.seekInfo else { + return + } + var stateIsFullyInitialised = true + if self.audioStream != nil && self.audioStreamState == nil { + stateIsFullyInitialised = false + } + if self.videoStream != nil && self.videoStreamState == nil { + stateIsFullyInitialised = false + } + + let state = FFMpegLookaheadThread.State( + seek: seekInfo, + maxReadablePts: self.maxReadPts, + audio: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.audioStreamState : nil, + video: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.videoStreamState : nil, + isEnded: self.isEnded + ) + if self.reportedState != state { + self.reportedState = state + self.params.updateState(state) + } + } +} + +private final class FFMpegLookaheadThread: NSObject { + struct StreamState: Equatable { + let info: FFMpegFileReader.StreamInfo + let readableToTime: CMTime + + init(info: FFMpegFileReader.StreamInfo, readableToTime: CMTime) { + self.info = info + self.readableToTime = readableToTime } } - /*{ - if let videoStream { - avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) + struct State: Equatable { + struct Seek: Equatable { + var streamIndex: Int + var pts: Int64 - let packet = FFMpegPacket() - while true { - if !avFormatContext.readFrame(into: packet) { - break - } - - if Int(packet.streamIndex) == videoStream.index { - let packetPts = CMTime(value: packet.pts, timescale: videoStream.timescale) - let packetDuration = CMTime(value: packet.duration, timescale: videoStream.timescale) - - var startNewSegment = segments.isEmpty - if packet.isKeyframe { - if segments.isEmpty { - startNewSegment = true - } else if let video = segments[segments.count - 1].video { - if packetPts.seconds - video.startPts.seconds > maxSegmentDuration { - startNewSegment = true - } - } - } - - if startNewSegment { - segments.append(FFMpegFrameSegment()) - } - segments[segments.count - 1].addFrame(isVideo: true, index: Int(packet.streamIndex), pts: packetPts, duration: packetDuration.seconds) - } + init(streamIndex: Int, pts: Int64) { + self.streamIndex = streamIndex + self.pts = pts } } - if let audioStream { - avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: true) - - var minSegmentIndex = 0 - - let packet = FFMpegPacket() - while true { - if !avFormatContext.readFrame(into: packet) { - break - } - - if Int(packet.streamIndex) == audioStream.index { - let packetPts = CMTime(value: packet.pts, timescale: audioStream.timescale) - let packetDuration = CMTime(value: packet.duration, timescale: audioStream.timescale) - - if videoStream != nil { - for i in minSegmentIndex ..< segments.count { - if let video = segments[i].video { - if video.startPts.seconds <= packetPts.seconds && video.endPts.seconds >= packetPts.seconds { - segments[i].addFrame(isVideo: false, index: Int(audioStream.index), pts: packetPts, duration: packetDuration.seconds) - minSegmentIndex = i - break - } - } - } - } else { - if segments.isEmpty { - segments.append(FFMpegFrameSegment()) - } - segments[segments.count - 1].addFrame(isVideo: false, index: Int(packet.streamIndex), pts: packetPts, duration: packetDuration.seconds) - } - } + + let seek: Seek + let maxReadablePts: Seek? + let audio: StreamState? + let video: StreamState? + let isEnded: Bool + + init(seek: Seek, maxReadablePts: Seek?, audio: StreamState?, video: StreamState?, isEnded: Bool) { + self.seek = seek + self.maxReadablePts = maxReadablePts + self.audio = audio + self.video = video + self.isEnded = isEnded + } + } + + final class Params: NSObject { + let seekToTimestamp: Double + let updateState: (State) -> Void + let fetchInRange: (Range) -> Disposable + let getDataInRange: (Range, @escaping (Data?) -> Void) -> Disposable + let isDataCachedInRange: (Range) -> Bool + let size: Int64 + let cancel: Signal + let currentTimestamp: Atomic + + init( + seekToTimestamp: Double, + updateState: @escaping (State) -> Void, + fetchInRange: @escaping (Range) -> Disposable, + getDataInRange: @escaping (Range, @escaping (Data?) -> Void) -> Disposable, + isDataCachedInRange: @escaping (Range) -> Bool, + size: Int64, + cancel: Signal, + currentTimestamp: Atomic + ) { + self.seekToTimestamp = seekToTimestamp + self.updateState = updateState + self.fetchInRange = fetchInRange + self.getDataInRange = getDataInRange + self.isDataCachedInRange = isDataCachedInRange + self.size = size + self.cancel = cancel + self.currentTimestamp = currentTimestamp + } + } + + @objc static func entryPoint(_ params: Params) { + let runLoop = RunLoop.current + + let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: FFMpegLookaheadThread.self, selector: #selector(FFMpegLookaheadThread.none), userInfo: nil, repeats: false) + runLoop.add(timer, forMode: .common) + + Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] = FFMpegLookaheadReader(params: params) + + while true { + runLoop.run(mode: .default, before: .distantFuture) + if Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] != nil { + break } } - }*/ + + Thread.current.threadDictionary.removeObject(forKey: "FFMpegLookaheadThread_params") + } - /*for i in 0 ..< segments.count { - print("Segment \(i):\n video \(segments[i].video?.startPts.seconds ?? -1.0) ... \(segments[i].video?.endPts.seconds ?? -1.0)\n audio \(segments[i].audio?.startPts.seconds ?? -1.0) ... \(segments[i].audio?.endPts.seconds ?? -1.0)") - }*/ + @objc static func none() { + } - return FFMpegFrameSegmentInfo( - headerAccessRanges: context.accessedRanges, - segments: segments - ) + @objc static func stop() { + Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] = "true" + } + + @objc static func updateCurrentTimestamp() { + if let reader = Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] as? FFMpegLookaheadReader { + reader.updateCurrentTimestamp() + } + } +} + +private final class FFMpegLookahead { + private let cancel = Promise() + private let currentTimestamp = Atomic(value: nil) + private let thread: Thread + + init( + seekToTimestamp: Double, + updateState: @escaping (FFMpegLookaheadThread.State) -> Void, + fetchInRange: @escaping (Range) -> Disposable, + getDataInRange: @escaping (Range, @escaping (Data?) -> Void) -> Disposable, + isDataCachedInRange: @escaping (Range) -> Bool, + size: Int64 + ) { + self.thread = Thread( + target: FFMpegLookaheadThread.self, + selector: #selector(FFMpegLookaheadThread.entryPoint(_:)), + object: FFMpegLookaheadThread.Params( + seekToTimestamp: seekToTimestamp, + updateState: updateState, + fetchInRange: fetchInRange, + getDataInRange: getDataInRange, + isDataCachedInRange: isDataCachedInRange, + size: size, + cancel: self.cancel.get(), + currentTimestamp: self.currentTimestamp + ) + ) + self.thread.name = "FFMpegLookahead" + self.thread.start() + } + + deinit { + self.cancel.set(.single(Void())) + FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.stop), on: self.thread, with: nil, waitUntilDone: false) + } + + func updateCurrentTimestamp(timestamp: Double) { + let _ = self.currentTimestamp.swap(timestamp) + FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.updateCurrentTimestamp), on: self.thread, with: timestamp as NSNumber, waitUntilDone: false) + } } final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl { @@ -401,7 +465,10 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl { } private var completeFetchDisposable: Disposable? - private var dataDisposable: Disposable? + + private var seekTimestamp: Double? + private var currentLookaheadId: Int = 0 + private var lookahead: FFMpegLookahead? init(resource: ChunkMediaPlayerV2.SourceDescription.ResourceDescription) { self.resource = resource @@ -416,71 +483,134 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl { preferBackgroundReferenceRevalidation: true ).startStrict() } - - self.dataDisposable = (resource.postbox.mediaBox.resourceData(resource.reference.resource) - |> deliverOnMainQueue).startStrict(next: { [weak self] data in - guard let self else { - return - } - if data.complete { - if let mediaInfo = extractFFMpegMediaInfo(path: data.path), let mainTrack = mediaInfo.audio ?? mediaInfo.video, let segmentInfo = extractFFMpegFrameSegmentInfo(path: data.path) { - var parts: [ChunkMediaPlayerPart] = [] - for segment in segmentInfo.segments { - guard let mainStream = segment.video ?? segment.audio else { - assertionFailure() - continue - } - parts.append(ChunkMediaPlayerPart( - startTime: mainStream.startPts.seconds, - endTime: mainStream.startPts.seconds + mainStream.duration, - content: .directFile(ChunkMediaPlayerPart.Content.FFMpegDirectFile( - path: data.path, - audio: segment.audio.flatMap { stream in - return ChunkMediaPlayerPart.DirectStream( - index: stream.index, - startPts: stream.startPts, - endPts: stream.endPts, - duration: stream.duration - ) - }, - video: segment.video.flatMap { stream in - return ChunkMediaPlayerPart.DirectStream( - index: stream.index, - startPts: stream.startPts, - endPts: stream.endPts, - duration: stream.duration - ) - } - )), - codecName: mediaInfo.video?.codecName - )) - } - - self.partsStateValue.set(.single(ChunkMediaPlayerPartsState( - duration: mainTrack.duration.seconds, - parts: parts - ))) - } else { - self.partsStateValue.set(.single(ChunkMediaPlayerPartsState( - duration: nil, - parts: [] - ))) - } - } else { - self.partsStateValue.set(.single(ChunkMediaPlayerPartsState( - duration: nil, - parts: [] - ))) - } - }) } deinit { self.completeFetchDisposable?.dispose() - self.dataDisposable?.dispose() } - func updatePlaybackState(position: Double, isPlaying: Bool) { + func seek(id: Int, position: Double) { + self.seekTimestamp = position + self.currentLookaheadId += 1 + let lookaheadId = self.currentLookaheadId + + let resource = self.resource + let updateState: (FFMpegLookaheadThread.State) -> Void = { [weak self] state in + Queue.mainQueue().async { + guard let self else { + return + } + if self.currentLookaheadId != lookaheadId { + return + } + guard let mainTrack = state.video ?? state.audio else { + self.partsStateValue.set(.single(ChunkMediaPlayerPartsState( + duration: nil, + content: .directReader(ChunkMediaPlayerPartsState.DirectReader( + id: id, + seekPosition: position, + availableUntilPosition: position, + bufferedUntilEnd: true, + impl: nil + )) + ))) + + return + } + + var minAvailableUntilPosition: Double? + if let audio = state.audio { + if let minAvailableUntilPositionValue = minAvailableUntilPosition { + minAvailableUntilPosition = min(minAvailableUntilPositionValue, audio.readableToTime.seconds) + } else { + minAvailableUntilPosition = audio.readableToTime.seconds + } + } + if let video = state.video { + if let minAvailableUntilPositionValue = minAvailableUntilPosition { + minAvailableUntilPosition = min(minAvailableUntilPositionValue, video.readableToTime.seconds) + } else { + minAvailableUntilPosition = video.readableToTime.seconds + } + } + + self.partsStateValue.set(.single(ChunkMediaPlayerPartsState( + duration: mainTrack.info.duration.seconds, + content: .directReader(ChunkMediaPlayerPartsState.DirectReader( + id: id, + seekPosition: position, + availableUntilPosition: minAvailableUntilPosition ?? position, + bufferedUntilEnd: state.isEnded, + impl: ChunkMediaPlayerPartsState.DirectReader.Impl( + video: state.video.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in + guard let maxReadablePts = state.maxReadablePts else { + return nil + } + + return ChunkMediaPlayerPartsState.DirectReader.Stream( + mediaBox: resource.postbox.mediaBox, + resource: resource.reference.resource, + size: resource.size, + index: media.info.index, + seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts), + maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded), + codecName: resolveFFMpegCodecName(id: media.info.codecId) + ) + }, + audio: state.audio.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in + guard let maxReadablePts = state.maxReadablePts else { + return nil + } + return ChunkMediaPlayerPartsState.DirectReader.Stream( + mediaBox: resource.postbox.mediaBox, + resource: resource.reference.resource, + size: resource.size, + index: media.info.index, + seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts), + maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded), + codecName: resolveFFMpegCodecName(id: media.info.codecId) + ) + } + ) + )) + ))) + } + } + + self.lookahead = FFMpegLookahead( + seekToTimestamp: position, + updateState: updateState, + fetchInRange: { range in + return fetchedMediaResource( + mediaBox: resource.postbox.mediaBox, + userLocation: resource.userLocation, + userContentType: resource.userContentType, + reference: resource.reference, + range: (range, .elevated), + statsCategory: resource.statsCategory, + preferBackgroundReferenceRevalidation: true + ).startStrict() + }, + getDataInRange: { range, completion in + return resource.postbox.mediaBox.resourceData(resource.reference.resource, size: resource.size, in: range, mode: .complete).start(next: { result, isComplete in + completion(isComplete ? result : nil) + }) + }, + isDataCachedInRange: { range in + return resource.postbox.mediaBox.internal_resourceDataIsCached( + id: resource.reference.resource.id, + size: resource.size, + in: range + ) + }, + size: self.resource.size + ) + } + + func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) { + if self.seekTimestamp == seekTimestamp { + self.lookahead?.updateCurrentTimestamp(timestamp: position) + } } } diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift index 1eb9b58a6a..fe583cc8c3 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift @@ -14,7 +14,8 @@ public let internal_isHardwareAv1Supported: Bool = { protocol ChunkMediaPlayerSourceImpl: AnyObject { var partsState: Signal { get } - func updatePlaybackState(position: Double, isPlaying: Bool) + func seek(id: Int, position: Double) + func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) } private final class ChunkMediaPlayerExternalSourceImpl: ChunkMediaPlayerSourceImpl { @@ -24,7 +25,10 @@ private final class ChunkMediaPlayerExternalSourceImpl: ChunkMediaPlayerSourceIm self.partsState = partsState } - func updatePlaybackState(position: Double, isPlaying: Bool) { + func seek(id: Int, position: Double) { + } + + func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) { } } @@ -32,14 +36,16 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { public enum SourceDescription { public final class ResourceDescription { public let postbox: Postbox + public let size: Int64 public let reference: MediaResourceReference public let userLocation: MediaResourceUserLocation public let userContentType: MediaResourceUserContentType public let statsCategory: MediaResourceStatsCategory public let fetchAutomatically: Bool - public init(postbox: Postbox, reference: MediaResourceReference, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, statsCategory: MediaResourceStatsCategory, fetchAutomatically: Bool) { + public init(postbox: Postbox, size: Int64, reference: MediaResourceReference, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, statsCategory: MediaResourceStatsCategory, fetchAutomatically: Bool) { self.postbox = postbox + self.size = size self.reference = reference self.userLocation = userLocation self.userContentType = userContentType @@ -52,10 +58,23 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { case directFetch(ResourceDescription) } + public struct MediaDataReaderParams { + public var useV2Reader: Bool + + public init(useV2Reader: Bool) { + self.useV2Reader = useV2Reader + } + } + private final class LoadedPart { + enum Content { + case tempFile(ChunkMediaPlayerPart.TempFile) + case directStream(ChunkMediaPlayerPartsState.DirectReader.Stream) + } + final class Media { let queue: Queue - let content: ChunkMediaPlayerPart.Content + let content: Content let mediaType: AVMediaType let codecName: String? @@ -64,7 +83,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { var didBeginReading: Bool = false var isFinished: Bool = false - init(queue: Queue, content: ChunkMediaPlayerPart.Content, mediaType: AVMediaType, codecName: String?) { + init(queue: Queue, content: Content, mediaType: AVMediaType, codecName: String?) { assert(queue.isCurrent()) self.queue = queue @@ -77,12 +96,21 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { assert(self.queue.isCurrent()) } - func load() { + func load(params: MediaDataReaderParams) { let reader: MediaDataReader - if case let .tempFile(tempFile) = self.content, self.mediaType == .video, (self.codecName == "av1" || self.codecName == "av01"), internal_isHardwareAv1Supported { - reader = AVAssetVideoDataReader(filePath: tempFile.file.path, isVideo: self.mediaType == .video) - } else { - reader = FFMpegMediaDataReader(content: self.content, isVideo: self.mediaType == .video, codecName: self.codecName) + switch self.content { + case let .tempFile(tempFile): + if self.mediaType == .video, (self.codecName == "av1" || self.codecName == "av01"), internal_isHardwareAv1Supported { + reader = AVAssetVideoDataReader(filePath: tempFile.file.path, isVideo: self.mediaType == .video) + } else { + if params.useV2Reader { + reader = FFMpegMediaDataReaderV2(content: .tempFile(tempFile), isVideo: self.mediaType == .video, codecName: self.codecName) + } else { + reader = FFMpegMediaDataReaderV1(filePath: tempFile.file.path, isVideo: self.mediaType == .video, codecName: self.codecName) + } + } + case let .directStream(directStream): + reader = FFMpegMediaDataReaderV2(content: .directStream(directStream), isVideo: self.mediaType == .video, codecName: self.codecName) } if self.mediaType == .video { if reader.hasVideo { @@ -94,15 +122,23 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } } } + + func update(content: Content) { + if let reader = self.reader { + if let reader = reader as? FFMpegMediaDataReaderV2, case let .directStream(directStream) = content { + reader.update(content: .directStream(directStream)) + } else { + assertionFailure() + } + } + } } final class MediaData { - let part: ChunkMediaPlayerPart let video: Media? let audio: Media? - init(part: ChunkMediaPlayerPart, video: Media?, audio: Media?) { - self.part = part + init(video: Media?, audio: Media?) { self.video = video self.audio = audio } @@ -118,6 +154,8 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private final class LoadedPartsMediaData { var ids: [ChunkMediaPlayerPart.Id] = [] var parts: [ChunkMediaPlayerPart.Id: LoadedPart.MediaData] = [:] + var directMediaData: LoadedPart.MediaData? + var directReaderId: Double? var notifiedHasSound: Bool = false var seekFromMinTimestamp: Double? } @@ -125,6 +163,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private static let sharedDataQueue = Queue(name: "ChunkMediaPlayerV2-DataQueue") private let dataQueue: Queue + private let mediaDataReaderParams: MediaDataReaderParams private let audioSessionManager: ManagedAudioSession private let onSeeked: (() -> Void)? @@ -132,7 +171,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private var videoRenderer: AVSampleBufferDisplayLayer private var audioRenderer: AVSampleBufferAudioRenderer? - private var partsState = ChunkMediaPlayerPartsState(duration: nil, parts: []) + private var partsState = ChunkMediaPlayerPartsState(duration: nil, content: .parts([])) private var loadedParts: [LoadedPart] = [] private var loadedPartsMediaData: QueueLocalObject private var hasSound: Bool = false @@ -155,12 +194,14 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { public var actionAtEnd: MediaPlayerActionAtEnd = .stop + private var didSeekOnce: Bool = false private var isPlaying: Bool = false private var baseRate: Double = 1.0 private var isSoundEnabled: Bool private var isMuted: Bool private var seekId: Int = 0 + private var seekTimestamp: Double = 0.0 private var pendingSeekTimestamp: Double? private var pendingContinuePlaybackAfterSeekToTimestamp: Double? private var shouldNotifySeeked: Bool = false @@ -171,6 +212,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private var audioIsRequestingMediaData: Bool = false private let source: ChunkMediaPlayerSourceImpl + private var didSetSourceSeek: Bool = false private var partsStateDisposable: Disposable? private var updateTimer: Foundation.Timer? @@ -178,6 +220,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private var hasAudioSession: Bool = false public init( + params: MediaDataReaderParams, audioSessionManager: ManagedAudioSession, source: SourceDescription, video: Bool, @@ -196,6 +239,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { ) { self.dataQueue = ChunkMediaPlayerV2.sharedDataQueue + self.mediaDataReaderParams = params self.audioSessionManager = audioSessionManager self.onSeeked = onSeeked @@ -328,6 +372,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } } + if !self.didSeekOnce { + self.didSeekOnce = true + self.seek(timestamp: 0.0, play: nil) + return + } + let timestamp: CMTime if let pendingSeekTimestamp = self.pendingSeekTimestamp { timestamp = CMTimeMakeWithSeconds(pendingSeekTimestamp, preferredTimescale: 44000) @@ -337,6 +387,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { let timestampSeconds = timestamp.seconds self.source.updatePlaybackState( + seekTimestamp: self.seekTimestamp, position: timestampSeconds, isPlaying: self.isPlaying ) @@ -346,38 +397,16 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { duration = partsStateDuration } - var validParts: [ChunkMediaPlayerPart] = [] + let isBuffering: Bool - var minStartTime: Double = 0.0 - for i in 0 ..< self.partsState.parts.count { - let part = self.partsState.parts[i] - - let partStartTime = max(minStartTime, part.startTime) - let partEndTime = max(partStartTime, part.endTime) - if partStartTime >= partEndTime { - continue - } - - var partMatches = false - if timestampSeconds >= partStartTime - 0.5 && timestampSeconds < partEndTime + 0.5 { - partMatches = true - } - - if partMatches { - validParts.append(ChunkMediaPlayerPart( - startTime: part.startTime, - clippedStartTime: partStartTime == part.startTime ? nil : partStartTime, - endTime: part.endTime, - content: part.content, - codecName: part.codecName - )) - minStartTime = max(minStartTime, partEndTime) - } - } + let mediaDataReaderParams = self.mediaDataReaderParams - if let lastValidPart = validParts.last { - for i in 0 ..< self.partsState.parts.count { - let part = self.partsState.parts[i] + switch self.partsState.content { + case let .parts(partsStateParts): + var validParts: [ChunkMediaPlayerPart] = [] + var minStartTime: Double = 0.0 + for i in 0 ..< partsStateParts.count { + let part = partsStateParts[i] let partStartTime = max(minStartTime, part.startTime) let partEndTime = max(partStartTime, part.endTime) @@ -385,7 +414,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { continue } - if lastValidPart !== part && partStartTime > (lastValidPart.clippedStartTime ?? lastValidPart.startTime) && partStartTime <= lastValidPart.endTime + 0.5 { + var partMatches = false + if timestampSeconds >= partStartTime - 0.5 && timestampSeconds < partEndTime + 0.5 { + partMatches = true + } + + if partMatches { validParts.append(ChunkMediaPlayerPart( startTime: part.startTime, clippedStartTime: partStartTime == part.startTime ? nil : partStartTime, @@ -394,141 +428,247 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { codecName: part.codecName )) minStartTime = max(minStartTime, partEndTime) - break } } - } - - if validParts.isEmpty, let pendingContinuePlaybackAfterSeekToTimestamp = self.pendingContinuePlaybackAfterSeekToTimestamp { - for part in self.partsState.parts { - if pendingContinuePlaybackAfterSeekToTimestamp >= part.startTime - 0.2 && pendingContinuePlaybackAfterSeekToTimestamp < part.endTime { - self.renderSynchronizer.setRate(Float(self.renderSynchronizerRate), time: CMTimeMakeWithSeconds(part.startTime, preferredTimescale: 44000)) - break - } - } - } - - self.loadedParts.removeAll(where: { partState in - if !validParts.contains(where: { $0.id == partState.part.id }) { - return true - } - return false - }) - - for part in validParts { - if !self.loadedParts.contains(where: { $0.part.id == part.id }) { - self.loadedParts.append(LoadedPart(part: part)) - self.loadedParts.sort(by: { $0.part.startTime < $1.part.startTime }) - } - } - - if self.pendingSeekTimestamp != nil { - return - } - - let loadedParts = self.loadedParts - let dataQueue = self.dataQueue - let isSoundEnabled = self.isSoundEnabled - self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in - loadedPartsMediaData.ids = loadedParts.map(\.part.id) - for part in loadedParts { - if let loadedPart = loadedPartsMediaData.parts[part.part.id] { - if let audio = loadedPart.audio, audio.didBeginReading, !isSoundEnabled { - let cleanAudio = LoadedPart.Media( + if let lastValidPart = validParts.last { + for i in 0 ..< partsStateParts.count { + let part = partsStateParts[i] + + let partStartTime = max(minStartTime, part.startTime) + let partEndTime = max(partStartTime, part.endTime) + if partStartTime >= partEndTime { + continue + } + + if lastValidPart !== part && partStartTime > (lastValidPart.clippedStartTime ?? lastValidPart.startTime) && partStartTime <= lastValidPart.endTime + 0.5 { + validParts.append(ChunkMediaPlayerPart( + startTime: part.startTime, + clippedStartTime: partStartTime == part.startTime ? nil : partStartTime, + endTime: part.endTime, + content: part.content, + codecName: part.codecName + )) + minStartTime = max(minStartTime, partEndTime) + break + } + } + } + + if validParts.isEmpty, let pendingContinuePlaybackAfterSeekToTimestamp = self.pendingContinuePlaybackAfterSeekToTimestamp { + for part in partsStateParts { + if pendingContinuePlaybackAfterSeekToTimestamp >= part.startTime - 0.2 && pendingContinuePlaybackAfterSeekToTimestamp < part.endTime { + self.renderSynchronizer.setRate(Float(self.renderSynchronizerRate), time: CMTimeMakeWithSeconds(part.startTime, preferredTimescale: 44000)) + break + } + } + } + + self.loadedParts.removeAll(where: { partState in + if !validParts.contains(where: { $0.id == partState.part.id }) { + return true + } + return false + }) + + for part in validParts { + if !self.loadedParts.contains(where: { $0.part.id == part.id }) { + self.loadedParts.append(LoadedPart(part: part)) + self.loadedParts.sort(by: { $0.part.startTime < $1.part.startTime }) + } + } + + var playableDuration: Double = 0.0 + var previousValidPartEndTime: Double? + + for part in partsStateParts { + if let previousValidPartEndTime { + if part.startTime > previousValidPartEndTime + 0.5 { + break + } + } else if !validParts.contains(where: { $0.id == part.id }) { + continue + } + + let partDuration: Double + if part.startTime - 0.5 <= timestampSeconds && part.endTime + 0.5 > timestampSeconds { + partDuration = part.endTime - timestampSeconds + } else if part.startTime - 0.5 > timestampSeconds { + partDuration = part.endTime - part.startTime + } else { + partDuration = 0.0 + } + playableDuration += partDuration + previousValidPartEndTime = part.endTime + } + + if self.pendingSeekTimestamp != nil { + return + } + + let loadedParts = self.loadedParts + let dataQueue = self.dataQueue + let isSoundEnabled = self.isSoundEnabled + self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in + loadedPartsMediaData.ids = loadedParts.map(\.part.id) + + for part in loadedParts { + if let loadedPart = loadedPartsMediaData.parts[part.part.id] { + if let audio = loadedPart.audio, audio.didBeginReading, !isSoundEnabled { + let cleanAudio = LoadedPart.Media( + queue: dataQueue, + content: .tempFile(part.part.content), + mediaType: .audio, + codecName: part.part.codecName + ) + cleanAudio.load(params: mediaDataReaderParams) + + loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData( + video: loadedPart.video, + audio: cleanAudio.reader != nil ? cleanAudio : nil + ) + } + } else { + let video = LoadedPart.Media( queue: dataQueue, - content: part.part.content, + content: .tempFile(part.part.content), + mediaType: .video, + codecName: part.part.codecName + ) + video.load(params: mediaDataReaderParams) + + let audio = LoadedPart.Media( + queue: dataQueue, + content: .tempFile(part.part.content), mediaType: .audio, codecName: part.part.codecName ) - cleanAudio.load() + audio.load(params: mediaDataReaderParams) loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData( - part: part.part, - video: loadedPart.video, - audio: cleanAudio.reader != nil ? cleanAudio : nil + video: video, + audio: audio.reader != nil ? audio : nil ) } - } else { - let video = LoadedPart.Media( - queue: dataQueue, - content: part.part.content, - mediaType: .video, - codecName: part.part.codecName - ) - video.load() - - let audio = LoadedPart.Media( - queue: dataQueue, - content: part.part.content, - mediaType: .audio, - codecName: part.part.codecName - ) - audio.load() - - loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData( - part: part.part, - video: video, - audio: audio.reader != nil ? audio : nil - ) } - } - - var removedKeys: [ChunkMediaPlayerPart.Id] = [] - for (id, _) in loadedPartsMediaData.parts { - if !loadedPartsMediaData.ids.contains(id) { - removedKeys.append(id) - } - } - for id in removedKeys { - loadedPartsMediaData.parts.removeValue(forKey: id) - } - - if !loadedPartsMediaData.notifiedHasSound, let part = loadedPartsMediaData.parts.values.first { - loadedPartsMediaData.notifiedHasSound = true - let hasSound = part.audio?.reader != nil - Queue.mainQueue().async { - guard let self else { - return + + var removedKeys: [ChunkMediaPlayerPart.Id] = [] + for (id, _) in loadedPartsMediaData.parts { + if !loadedPartsMediaData.ids.contains(id) { + removedKeys.append(id) } - if self.hasSound != hasSound { - self.hasSound = hasSound - self.updateInternalState() + } + for id in removedKeys { + loadedPartsMediaData.parts.removeValue(forKey: id) + } + + if !loadedPartsMediaData.notifiedHasSound, let part = loadedPartsMediaData.parts.values.first { + loadedPartsMediaData.notifiedHasSound = true + let hasSound = part.audio?.reader != nil + Queue.mainQueue().async { + guard let self else { + return + } + if self.hasSound != hasSound { + self.hasSound = hasSound + self.updateInternalState() + } } } } - } - - var playableDuration: Double = 0.0 - var previousValidPartEndTime: Double? - for part in self.partsState.parts { - if let previousValidPartEndTime { - if part.startTime > previousValidPartEndTime + 0.5 { - break - } - } else if !validParts.contains(where: { $0.id == part.id }) { - continue - } - let partDuration: Double - if part.startTime - 0.5 <= timestampSeconds && part.endTime + 0.5 > timestampSeconds { - partDuration = part.endTime - timestampSeconds - } else if part.startTime - 0.5 > timestampSeconds { - partDuration = part.endTime - part.startTime + if let previousValidPartEndTime, previousValidPartEndTime >= duration - 0.5 { + isBuffering = false } else { - partDuration = 0.0 + isBuffering = playableDuration < 1.0 + } + case let .directReader(directReader): + var readerImpl: ChunkMediaPlayerPartsState.DirectReader.Impl? + var playableDuration: Double = 0.0 + let directReaderSeekPosition = directReader.seekPosition + if directReader.id == self.seekId { + readerImpl = directReader.impl + playableDuration = max(0.0, directReader.availableUntilPosition - timestampSeconds) + if directReader.bufferedUntilEnd { + isBuffering = false + } else { + isBuffering = playableDuration < 1.0 + } + } else { + playableDuration = 0.0 + isBuffering = true + } + + let dataQueue = self.dataQueue + self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in + if !loadedPartsMediaData.ids.isEmpty { + loadedPartsMediaData.ids = [] + } + if !loadedPartsMediaData.parts.isEmpty { + loadedPartsMediaData.parts.removeAll() + } + + if let readerImpl { + if let currentDirectMediaData = loadedPartsMediaData.directMediaData, let currentDirectReaderId = loadedPartsMediaData.directReaderId, currentDirectReaderId == directReaderSeekPosition { + if let video = currentDirectMediaData.video, let videoStream = readerImpl.video { + video.update(content: .directStream(videoStream)) + } + if let audio = currentDirectMediaData.audio, let audioStream = readerImpl.audio { + audio.update(content: .directStream(audioStream)) + } + } else { + let video = readerImpl.video.flatMap { media in + return LoadedPart.Media( + queue: dataQueue, + content: .directStream(media), + mediaType: .video, + codecName: media.codecName + ) + } + video?.load(params: mediaDataReaderParams) + + let audio = readerImpl.audio.flatMap { media in + return LoadedPart.Media( + queue: dataQueue, + content: .directStream(media), + mediaType: .audio, + codecName: media.codecName + ) + } + audio?.load(params: mediaDataReaderParams) + + loadedPartsMediaData.directMediaData = LoadedPart.MediaData( + video: video, + audio: audio + ) + } + loadedPartsMediaData.directReaderId = directReaderSeekPosition + + if !loadedPartsMediaData.notifiedHasSound { + loadedPartsMediaData.notifiedHasSound = true + let hasSound = readerImpl.audio != nil + Queue.mainQueue().async { + guard let self else { + return + } + if self.hasSound != hasSound { + self.hasSound = hasSound + self.updateInternalState() + } + } + } + } else { + loadedPartsMediaData.directMediaData = nil + loadedPartsMediaData.directReaderId = nil + } + } + + if self.pendingSeekTimestamp != nil { + return } - playableDuration += partDuration - previousValidPartEndTime = part.endTime } var effectiveRate: Double = 0.0 - let isBuffering: Bool - if let previousValidPartEndTime, previousValidPartEndTime >= duration - 0.5 { - isBuffering = false - } else { - isBuffering = playableDuration < 1.0 - } if self.isPlaying { if !isBuffering { effectiveRate = self.baseRate @@ -671,7 +811,30 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } private func seek(timestamp: Double, play: Bool?, notify: Bool) { + let currentTimestamp: CMTime + if let pendingSeekTimestamp = self.pendingSeekTimestamp { + currentTimestamp = CMTimeMakeWithSeconds(pendingSeekTimestamp, preferredTimescale: 44000) + } else { + currentTimestamp = self.renderSynchronizer.currentTime() + } + let currentTimestampSeconds = currentTimestamp.seconds + if currentTimestampSeconds == timestamp { + if let play { + self.isPlaying = play + } + if notify { + self.shouldNotifySeeked = true + } + if !self.didSetSourceSeek { + self.didSetSourceSeek = true + self.source.seek(id: self.seekId, position: timestamp) + } + self.updateInternalState() + return + } + self.seekId += 1 + self.seekTimestamp = timestamp let seekId = self.seekId self.pendingSeekTimestamp = timestamp self.pendingContinuePlaybackAfterSeekToTimestamp = timestamp @@ -699,6 +862,9 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { audioRenderer.stopRequestingMediaData() } + self.didSetSourceSeek = true + self.source.seek(id: self.seekId, position: timestamp) + self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in loadedPartsMediaData.parts.removeAll() loadedPartsMediaData.seekFromMinTimestamp = timestamp @@ -733,7 +899,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private func triggerRequestMediaData() { let loadedPartsMediaData = self.loadedPartsMediaData - if !self.videoIsRequestingMediaData && "".isEmpty { + if !self.videoIsRequestingMediaData { self.videoIsRequestingMediaData = true let videoTarget: AVQueuedSampleBufferRendering @@ -804,7 +970,9 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { continue } media.didBeginReading = true - if var sampleBuffer = reader.readSampleBuffer() { + switch reader.readSampleBuffer() { + case let .frame(sampleBuffer): + var sampleBuffer = sampleBuffer if let seekFromMinTimestamp = loadedPartsMediaData.seekFromMinTimestamp, CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds < seekFromMinTimestamp { if isVideo { var updatedSampleBuffer: CMSampleBuffer? @@ -829,7 +997,54 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { bufferTarget.enqueue(sampleBuffer) hasData = true continue outer - } else { + case .waitingForMoreData, .endOfStream, .error: + media.isFinished = true + } + } + outerDirect: while true { + guard let directMediaData = loadedPartsMediaData.directMediaData else { + break outer + } + guard let media = isVideo ? directMediaData.video : directMediaData.audio else { + break outer + } + if media.isFinished { + break outer + } + guard let reader = media.reader else { + break outer + } + media.didBeginReading = true + switch reader.readSampleBuffer() { + case let .frame(sampleBuffer): + var sampleBuffer = sampleBuffer + if let seekFromMinTimestamp = loadedPartsMediaData.seekFromMinTimestamp, CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds < seekFromMinTimestamp { + if isVideo { + var updatedSampleBuffer: CMSampleBuffer? + CMSampleBufferCreateCopy(allocator: nil, sampleBuffer: sampleBuffer, sampleBufferOut: &updatedSampleBuffer) + if let updatedSampleBuffer { + if let attachments = CMSampleBufferGetSampleAttachmentsArray(updatedSampleBuffer, createIfNecessary: true) { + let attachments = attachments as NSArray + let dict = attachments[0] as! NSMutableDictionary + + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + + sampleBuffer = updatedSampleBuffer + } + } + } else { + continue outer + } + } + /*if !isVideo { + print("Enqueue audio \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value) next: \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value + 1024)") + }*/ + bufferTarget.enqueue(sampleBuffer) + hasData = true + continue outer + case .waitingForMoreData: + break outer + case .endOfStream, .error: media.isFinished = true } } diff --git a/submodules/MediaPlayer/Sources/FFMpegFileReader.swift b/submodules/MediaPlayer/Sources/FFMpegFileReader.swift new file mode 100644 index 0000000000..6f1cf016ed --- /dev/null +++ b/submodules/MediaPlayer/Sources/FFMpegFileReader.swift @@ -0,0 +1,473 @@ +import Foundation +#if !os(macOS) +import UIKit +#else +import AppKit +import TGUIKit +#endif +import CoreMedia +import SwiftSignalKit +import FFMpegBinding +import Postbox +import ManagedFile + +private func FFMpegFileReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + guard let buffer else { + return FFMPEG_CONSTANT_AVERROR_EOF + } + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + + switch context.source { + case let .file(file): + let result = file.read(buffer, Int(bufferSize)) + if result == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + return Int32(result) + case let .resource(resource): + let readCount = min(256 * 1024, Int64(bufferSize)) + let requestRange: Range = resource.readingPosition ..< (resource.readingPosition + readCount) + + //TODO:improve thread safe read if incomplete + if let (file, readSize) = resource.mediaBox.internal_resourceData(id: resource.resource.id, size: resource.size, in: requestRange) { + let result = file.read(buffer, readSize) + if result == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + resource.readingPosition += Int64(result) + return Int32(result) + } else { + return FFMPEG_CONSTANT_AVERROR_EOF + } + } +} + +private func FFMpegFileReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if (whence & FFMPEG_AVSEEK_SIZE) != 0 { + switch context.source { + case let .file(file): + return file.getSize() ?? 0 + case let .resource(resource): + return resource.size + } + } else { + switch context.source { + case let .file(file): + let _ = file.seek(position: offset) + case let .resource(resource): + resource.readingPosition = offset + } + return offset + } +} + +final class FFMpegFileReader { + enum SourceDescription { + case file(String) + case resource(mediaBox: MediaBox, resource: MediaResource, size: Int64) + } + + final class StreamInfo: Equatable { + let index: Int + let codecId: Int32 + let startTime: CMTime + let duration: CMTime + let timeBase: CMTimeValue + let timeScale: CMTimeScale + let fps: CMTime + + init(index: Int, codecId: Int32, startTime: CMTime, duration: CMTime, timeBase: CMTimeValue, timeScale: CMTimeScale, fps: CMTime) { + self.index = index + self.codecId = codecId + self.startTime = startTime + self.duration = duration + self.timeBase = timeBase + self.timeScale = timeScale + self.fps = fps + } + + static func ==(lhs: StreamInfo, rhs: StreamInfo) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.codecId != rhs.codecId { + return false + } + if lhs.startTime != rhs.startTime { + return false + } + if lhs.duration != rhs.duration { + return false + } + if lhs.timeBase != rhs.timeBase { + return false + } + if lhs.timeScale != rhs.timeScale { + return false + } + if lhs.fps != rhs.fps { + return false + } + return true + } + } + + fileprivate enum Source { + final class Resource { + let mediaBox: MediaBox + let resource: MediaResource + let size: Int64 + var readingPosition: Int64 = 0 + + init(mediaBox: MediaBox, resource: MediaResource, size: Int64) { + self.mediaBox = mediaBox + self.resource = resource + self.size = size + } + } + + case file(ManagedFile) + case resource(Resource) + } + + private enum Decoder { + case videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder) + case video(FFMpegMediaVideoFrameDecoder) + case audio(FFMpegAudioFrameDecoder) + + func send(frame: MediaTrackDecodableFrame) -> Bool { + switch self { + case let .videoPassthrough(decoder): + decoder.send(frame: frame) + case let .video(decoder): + decoder.send(frame: frame) + case let .audio(decoder): + decoder.send(frame: frame) + } + } + + func sendEnd() -> Bool { + switch self { + case let .videoPassthrough(decoder): + return decoder.sendEndToDecoder() + case let .video(decoder): + return decoder.sendEndToDecoder() + case let .audio(decoder): + return decoder.sendEndToDecoder() + } + } + } + + private final class Stream { + let info: StreamInfo + let decoder: Decoder + + init(info: StreamInfo, decoder: Decoder) { + self.info = info + self.decoder = decoder + } + } + + enum SelectedStream { + enum MediaType { + case audio + case video + } + + case mediaType(MediaType) + case index(Int) + } + + enum ReadFrameResult { + case frame(MediaTrackFrame) + case waitingForMoreData + case endOfStream + case error + } + + private(set) var readingError = false + private var stream: Stream? + private var avIoContext: FFMpegAVIOContext? + private var avFormatContext: FFMpegAVFormatContext? + + fileprivate let source: Source + + private var didSendEndToDecoder: Bool = false + private var hasReadToEnd: Bool = false + + private var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)? + private var lastReadPts: (streamIndex: Int, pts: Int64)? + private var isWaitingForMoreData: Bool = false + + public init?(source: SourceDescription, passthroughDecoder: Bool = false, useHardwareAcceleration: Bool, selectedStream: SelectedStream, seek: (streamIndex: Int, pts: Int64)?, maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) { + let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + + switch source { + case let .file(path): + guard let file = ManagedFile(queue: nil, path: path, mode: .read) else { + return nil + } + self.source = .file(file) + case let .resource(mediaBox, resource, size): + self.source = .resource(Source.Resource(mediaBox: mediaBox, resource: resource, size: size)) + } + + self.maxReadablePts = maxReadablePts + + let avFormatContext = FFMpegAVFormatContext() + /*if hintVP9 { + avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9) + }*/ + let ioBufferSize = 64 * 1024 + + let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegFileReader_readPacketCallback, writePacket: nil, seek: FFMpegFileReader_seekCallback, isSeekable: true) + self.avIoContext = avIoContext + + avFormatContext.setIO(self.avIoContext!) + + if !avFormatContext.openInput(withDirectFilePath: nil) { + self.readingError = true + return nil + } + + if !avFormatContext.findStreamInfo() { + self.readingError = true + return nil + } + + self.avFormatContext = avFormatContext + + var stream: Stream? + outer: for mediaType in [.audio, .video] as [SelectedStream.MediaType] { + streamSearch: for streamIndexNumber in avFormatContext.streamIndices(for: mediaType == .video ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) { + let streamIndex = Int(streamIndexNumber.int32Value) + if avFormatContext.isAttachedPic(atStreamIndex: Int32(streamIndex)) { + continue + } + + switch selectedStream { + case let .mediaType(selectedMediaType): + if mediaType != selectedMediaType { + continue streamSearch + } + case let .index(index): + if streamIndex != index { + continue streamSearch + } + } + + let codecId = avFormatContext.codecId(atStreamIndex: Int32(streamIndex)) + + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: Int32(streamIndex), defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: Int32(streamIndex)) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: Int32(streamIndex)), timescale: timebase.timescale) + + let metrics = avFormatContext.metricsForStream(at: Int32(streamIndex)) + + let rotationAngle: Double = metrics.rotationAngle + //let aspect = Double(metrics.width) / Double(metrics.height) + + let info = StreamInfo( + index: streamIndex, + codecId: codecId, + startTime: startTime, + duration: duration, + timeBase: timebase.value, + timeScale: timebase.timescale, + fps: fps + ) + + switch mediaType { + case .video: + if passthroughDecoder { + var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData? + if codecId == FFMpegCodecIdMPEG4 { + videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize))) + } else if codecId == FFMpegCodecIdH264 { + videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize))) + } else if codecId == FFMpegCodecIdHEVC { + videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize))) + } else if codecId == FFMpegCodecIdAV1 { + videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize))) + } + + if let videoFormatData { + stream = Stream( + info: info, + decoder: .videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle)) + ) + break outer + } + } else { + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: useHardwareAcceleration) { + let codecContext = FFMpegAVCodecContext(codec: codec) + if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) { + if useHardwareAcceleration { + codecContext.setupHardwareAccelerationIfPossible() + } + + if codecContext.open() { + stream = Stream( + info: info, + decoder: .video(FFMpegMediaVideoFrameDecoder(codecContext: codecContext)) + ) + break outer + } + } + } + } + case .audio: + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) { + let codecContext = FFMpegAVCodecContext(codec: codec) + if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) { + if codecContext.open() { + stream = Stream( + info: info, + decoder: .audio(FFMpegAudioFrameDecoder(codecContext: codecContext, sampleRate: 48000, channelCount: 1)) + ) + break outer + } + } + } + } + } + } + + guard let stream else { + self.readingError = true + return nil + } + + self.stream = stream + + if let seek { + avFormatContext.seekFrame(forStreamIndex: Int32(seek.streamIndex), pts: seek.pts, positionOnKeyframe: true) + } else { + avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: 0, positionOnKeyframe: true) + } + } + + deinit { + } + + private func readPacketInternal() -> FFMpegPacket? { + guard let avFormatContext = self.avFormatContext else { + return nil + } + + if let maxReadablePts = self.maxReadablePts, !maxReadablePts.isEnded, let lastReadPts = self.lastReadPts, lastReadPts.streamIndex == maxReadablePts.streamIndex, lastReadPts.pts == maxReadablePts.pts { + self.isWaitingForMoreData = true + return nil + } + + let packet = FFMpegPacket() + if avFormatContext.readFrame(into: packet) { + self.lastReadPts = (Int(packet.streamIndex), packet.pts) + return packet + } else { + self.hasReadToEnd = true + return nil + } + } + + func readDecodableFrame() -> MediaTrackDecodableFrame? { + while !self.readingError && !self.hasReadToEnd && !self.isWaitingForMoreData { + if let packet = self.readPacketInternal() { + if let stream = self.stream, Int(packet.streamIndex) == stream.info.index { + let packetPts = packet.pts + + /*if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value { + self.hasReadToEnd = true + }*/ + + let pts = CMTimeMake(value: packetPts, timescale: stream.info.timeScale) + let dts = CMTimeMake(value: packet.dts, timescale: stream.info.timeScale) + + let duration: CMTime + + let frameDuration = packet.duration + if frameDuration != 0 { + duration = CMTimeMake(value: frameDuration * stream.info.timeBase, timescale: stream.info.timeScale) + } else { + duration = stream.info.fps + } + + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) + return frame + } + } else { + break + } + } + + return nil + } + + public func readFrame() -> ReadFrameResult { + guard let stream = self.stream else { + return .error + } + + while true { + var result: MediaTrackFrame? + switch stream.decoder { + case let .video(decoder): + result = decoder.decode(ptsOffset: nil, forceARGB: false, unpremultiplyAlpha: false, displayImmediately: false) + case let .videoPassthrough(decoder): + result = decoder.decode() + case let .audio(decoder): + result = decoder.decode() + } + if let result { + if self.didSendEndToDecoder { + assert(true) + } + return .frame(result) + } + + if !self.isWaitingForMoreData && !self.readingError && !self.hasReadToEnd { + if let decodableFrame = self.readDecodableFrame() { + let _ = stream.decoder.send(frame: decodableFrame) + } + } else if self.hasReadToEnd && !self.didSendEndToDecoder { + self.didSendEndToDecoder = true + let _ = stream.decoder.sendEnd() + } else { + break + } + } + + if self.isWaitingForMoreData { + return .waitingForMoreData + } else { + return .endOfStream + } + } + + public func updateMaxReadablePts(pts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) { + if self.maxReadablePts?.streamIndex != pts?.streamIndex || self.maxReadablePts?.pts != pts?.pts { + self.maxReadablePts = pts + + if let pts { + if pts.isEnded { + self.isWaitingForMoreData = false + } else { + if self.lastReadPts?.streamIndex != pts.streamIndex || self.lastReadPts?.pts != pts.pts { + self.isWaitingForMoreData = false + } + } + } else { + self.isWaitingForMoreData = false + } + } + } +} diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index 152f2270b8..82e8c5aa3f 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -490,7 +490,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { let aspect = Double(metrics.width) / Double(metrics.height) if self.preferSoftwareDecoding { - if let codec = FFMpegAVCodec.find(forId: codecId) { + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { @@ -523,7 +523,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { var codec: FFMpegAVCodec? if codec == nil { - codec = FFMpegAVCodec.find(forId: codecId) + codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) } if let codec = codec { diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 5bae492ce9..598916b345 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -277,6 +277,43 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } private func convertVideoFrame(_ frame: FFMpegAVFrame, pts: CMTime, dts: CMTime, duration: CMTime, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? { + if frame.nativePixelFormat() == FFMpegAVFrameNativePixelFormat.videoToolbox { + guard let pixelBufferRef = frame.data[3] else { + return nil + } + let unmanagedPixelBuffer = Unmanaged.fromOpaque(UnsafeRawPointer(pixelBufferRef)) + let pixelBuffer = unmanagedPixelBuffer.takeUnretainedValue() + + var formatRef: CMVideoFormatDescription? + let formatStatus = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatRef) + + guard let format = formatRef, formatStatus == 0 else { + return nil + } + + var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts) + var sampleBuffer: CMSampleBuffer? + + guard CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) == noErr else { + return nil + } + + let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + + let resetDecoder = self.resetDecoderOnNextFrame + if self.resetDecoderOnNextFrame { + self.resetDecoderOnNextFrame = false + //dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString as String) + } + + if displayImmediately { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + } + + return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true) + } + if frame.data[0] == nil { return nil } diff --git a/submodules/MediaPlayer/Sources/MediaDataReader.swift b/submodules/MediaPlayer/Sources/MediaDataReader.swift index 6c2d695a28..ec7444fcef 100644 --- a/submodules/MediaPlayer/Sources/MediaDataReader.swift +++ b/submodules/MediaPlayer/Sources/MediaDataReader.swift @@ -4,6 +4,7 @@ import CoreMedia import FFMpegBinding import VideoToolbox import Postbox + #if os(macOS) private let internal_isHardwareAv1Supported: Bool = { let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) @@ -96,15 +97,131 @@ public final class ChunkMediaPlayerPartsState { #endif +public enum MediaDataReaderReadSampleBufferResult { + case frame(CMSampleBuffer) + case waitingForMoreData + case endOfStream + case error +} + public protocol MediaDataReader: AnyObject { var hasVideo: Bool { get } var hasAudio: Bool { get } - func readSampleBuffer() -> CMSampleBuffer? + func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult } -public final class FFMpegMediaDataReader: MediaDataReader { - private let content: ChunkMediaPlayerPart.Content +public final class FFMpegMediaDataReaderV2: MediaDataReader { + public enum Content { + case tempFile(ChunkMediaPlayerPart.TempFile) + case directStream(ChunkMediaPlayerPartsState.DirectReader.Stream) + } + + private let content: Content + private let isVideo: Bool + private let videoSource: FFMpegFileReader? + private let audioSource: FFMpegFileReader? + + public var hasVideo: Bool { + return self.videoSource != nil + } + + public var hasAudio: Bool { + return self.audioSource != nil + } + + public init(content: Content, isVideo: Bool, codecName: String?) { + self.content = content + self.isVideo = isVideo + + let source: FFMpegFileReader.SourceDescription + var seek: (streamIndex: Int, pts: Int64)? + var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)? + switch content { + case let .tempFile(tempFile): + source = .file(tempFile.file.path) + case let .directStream(directStream): + source = .resource(mediaBox: directStream.mediaBox, resource: directStream.resource, size: directStream.size) + seek = (directStream.seek.streamIndex, directStream.seek.pts) + maxReadablePts = directStream.maxReadablePts + } + + if self.isVideo { + var passthroughDecoder = true + var useHardwareAcceleration = false + + if (codecName == "av1" || codecName == "av01") { + passthroughDecoder = false + useHardwareAcceleration = internal_isHardwareAv1Supported + } + if codecName == "vp9" || codecName == "vp8" { + passthroughDecoder = false + } + + /*#if DEBUG + if codecName == "h264" { + passthroughDecoder = false + useHardwareAcceleration = true + } + #endif*/ + + if let videoSource = FFMpegFileReader(source: source, passthroughDecoder: passthroughDecoder, useHardwareAcceleration: useHardwareAcceleration, selectedStream: .mediaType(.video), seek: seek, maxReadablePts: maxReadablePts) { + self.videoSource = videoSource + } else { + self.videoSource = nil + } + self.audioSource = nil + } else { + if let audioSource = FFMpegFileReader(source: source, passthroughDecoder: false, useHardwareAcceleration: false, selectedStream: .mediaType(.audio), seek: seek, maxReadablePts: maxReadablePts) { + self.audioSource = audioSource + } else { + self.audioSource = nil + } + self.videoSource = nil + } + } + + public func update(content: Content) { + guard case let .directStream(directStream) = content else { + return + } + if let audioSource = self.audioSource { + audioSource.updateMaxReadablePts(pts: directStream.maxReadablePts) + } else if let videoSource = self.videoSource { + videoSource.updateMaxReadablePts(pts: directStream.maxReadablePts) + } + } + + public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult { + if let videoSource { + switch videoSource.readFrame() { + case let .frame(frame): + return .frame(frame.sampleBuffer) + case .waitingForMoreData: + return .waitingForMoreData + case .endOfStream: + return .endOfStream + case .error: + return .error + } + } else if let audioSource { + switch audioSource.readFrame() { + case let .frame(frame): + return .frame(frame.sampleBuffer) + case .waitingForMoreData: + return .waitingForMoreData + case .endOfStream: + return .endOfStream + case .error: + return .error + } + } else { + return .endOfStream + } + } +} + +public final class FFMpegMediaDataReaderV1: MediaDataReader { private let isVideo: Bool private let videoSource: SoftwareVideoReader? private let audioSource: SoftwareAudioSource? @@ -117,42 +234,15 @@ public final class FFMpegMediaDataReader: MediaDataReader { return self.audioSource != nil } - public init(content: ChunkMediaPlayerPart.Content, isVideo: Bool, codecName: String?) { - self.content = content + public init(filePath: String, isVideo: Bool, codecName: String?) { self.isVideo = isVideo - let filePath: String - var focusedPart: MediaStreamFocusedPart? - switch content { - case let .tempFile(tempFile): - filePath = tempFile.file.path - case let .directFile(directFile): - filePath = directFile.path - - let stream = isVideo ? directFile.video : directFile.audio - guard let stream else { - self.videoSource = nil - self.audioSource = nil - return - } - - focusedPart = MediaStreamFocusedPart( - seekStreamIndex: stream.index, - startPts: stream.startPts, - endPts: stream.endPts - ) - } - if self.isVideo { var passthroughDecoder = true if (codecName == "av1" || codecName == "av01") && !internal_isHardwareAv1Supported { passthroughDecoder = false } - if codecName == "vp9" || codecName == "vp8" { - passthroughDecoder = false - } - - let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder, focusedPart: focusedPart) + let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder) if videoSource.hasStream { self.videoSource = videoSource } else { @@ -160,7 +250,7 @@ public final class FFMpegMediaDataReader: MediaDataReader { } self.audioSource = nil } else { - let audioSource = SoftwareAudioSource(path: filePath, focusedPart: focusedPart) + let audioSource = SoftwareAudioSource(path: filePath) if audioSource.hasStream { self.audioSource = audioSource } else { @@ -170,19 +260,23 @@ public final class FFMpegMediaDataReader: MediaDataReader { } } - public func readSampleBuffer() -> CMSampleBuffer? { + public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult { if let videoSource { let frame = videoSource.readFrame() if let frame { - return frame.sampleBuffer + return .frame(frame.sampleBuffer) } else { - return nil + return .endOfStream } } else if let audioSource { - return audioSource.readSampleBuffer() + if let sampleBuffer = audioSource.readSampleBuffer() { + return .frame(sampleBuffer) + } else { + return .endOfStream + } + } else { + return .endOfStream } - - return nil } } @@ -225,14 +319,18 @@ public final class AVAssetVideoDataReader: MediaDataReader { } } - public func readSampleBuffer() -> CMSampleBuffer? { + public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult { guard let mediaInfo = self.mediaInfo, let assetReader = self.assetReader, let assetOutput = self.assetOutput else { - return nil + return .endOfStream } var retryCount = 0 while true { if let sampleBuffer = assetOutput.copyNextSampleBuffer() { - return createSampleBuffer(fromSampleBuffer: sampleBuffer, withTimeOffset: mediaInfo.startTime, duration: nil) + if let convertedSampleBuffer = createSampleBuffer(fromSampleBuffer: sampleBuffer, withTimeOffset: mediaInfo.startTime, duration: nil) { + return .frame(convertedSampleBuffer) + } else { + return .endOfStream + } } else if assetReader.status == .reading && retryCount < 100 { Thread.sleep(forTimeInterval: 1.0 / 60.0) retryCount += 1 @@ -241,7 +339,7 @@ public final class AVAssetVideoDataReader: MediaDataReader { } } - return nil + return .endOfStream } } diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 2467d9539e..f8491f963e 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -163,7 +163,7 @@ public final class SoftwareVideoSource { break } } else { - if let codec = FFMpegAVCodec.find(forId: codecId) { + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { @@ -382,7 +382,6 @@ private func SoftwareAudioSource_seekCallback(userData: UnsafeMutableRawPointer? } public final class SoftwareAudioSource { - private let focusedPart: MediaStreamFocusedPart? private var readingError = false private var audioStream: SoftwareAudioStream? private var avIoContext: FFMpegAVIOContext? @@ -397,11 +396,9 @@ public final class SoftwareAudioSource { return self.audioStream != nil } - public init(path: String, focusedPart: MediaStreamFocusedPart?) { + public init(path: String) { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals - self.focusedPart = focusedPart - var s = stat() stat(path, &s) self.size = Int32(s.st_size) @@ -451,7 +448,7 @@ public final class SoftwareAudioSource { let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) - let codec = FFMpegAVCodec.find(forId: codecId) + let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) if let codec = codec { let codecContext = FFMpegAVCodecContext(codec: codec) @@ -466,12 +463,8 @@ public final class SoftwareAudioSource { self.audioStream = audioStream - if let focusedPart = self.focusedPart { - avFormatContext.seekFrame(forStreamIndex: Int32(focusedPart.seekStreamIndex), pts: focusedPart.startPts.value, positionOnKeyframe: true) - } else { - if let audioStream = self.audioStream { - avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false) - } + if let audioStream = self.audioStream { + avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false) } } @@ -502,10 +495,6 @@ public final class SoftwareAudioSource { if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index { let packetPts = packet.pts - if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value { - self.hasReadToEnd = true - } - let pts = CMTimeMake(value: packetPts, timescale: audioStream.timebase.timescale) let dts = CMTimeMake(value: packet.dts, timescale: audioStream.timebase.timescale) @@ -579,18 +568,6 @@ public final class SoftwareAudioSource { } } -public struct MediaStreamFocusedPart { - public let seekStreamIndex: Int - public let startPts: CMTime - public let endPts: CMTime - - public init(seekStreamIndex: Int, startPts: CMTime, endPts: CMTime) { - self.seekStreamIndex = seekStreamIndex - self.startPts = startPts - self.endPts = endPts - } -} - private func SoftwareVideoReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() if let fd = context.fd { @@ -617,7 +594,6 @@ private func SoftwareVideoReader_seekCallback(userData: UnsafeMutableRawPointer? } final class SoftwareVideoReader { - private let focusedPart: MediaStreamFocusedPart? private var readingError = false private var videoStream: SoftwareVideoStream? private var avIoContext: FFMpegAVIOContext? @@ -636,11 +612,9 @@ final class SoftwareVideoReader { return self.videoStream != nil } - public init(path: String, hintVP9: Bool, passthroughDecoder: Bool = false, focusedPart: MediaStreamFocusedPart?) { + public init(path: String, hintVP9: Bool, passthroughDecoder: Bool = false) { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals - self.focusedPart = focusedPart - var s = stat() stat(path, &s) self.size = Int32(s.st_size) @@ -721,7 +695,7 @@ final class SoftwareVideoReader { break } } else { - if let codec = FFMpegAVCodec.find(forId: codecId) { + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { @@ -737,12 +711,8 @@ final class SoftwareVideoReader { self.videoStream = videoStream - if let focusedPart = self.focusedPart { - avFormatContext.seekFrame(forStreamIndex: Int32(focusedPart.seekStreamIndex), pts: focusedPart.startPts.value, positionOnKeyframe: true) - } else { - if let videoStream = self.videoStream { - avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) - } + if let videoStream = self.videoStream { + avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) } } @@ -775,10 +745,6 @@ final class SoftwareVideoReader { if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index { let packetPts = packet.pts - if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value { - self.hasReadToEnd = true - } - let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale) let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale) @@ -958,21 +924,8 @@ public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? { var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime)) - var codecName: String? let codecId = avFormatContext.codecId(atStreamIndex: streamIndex) - if codecId == FFMpegCodecIdMPEG4 { - codecName = "mpeg4" - } else if codecId == FFMpegCodecIdH264 { - codecName = "h264" - } else if codecId == FFMpegCodecIdHEVC { - codecName = "hevc" - } else if codecId == FFMpegCodecIdAV1 { - codecName = "av1" - } else if codecId == FFMpegCodecIdVP9 { - codecName = "vp9" - } else if codecId == FFMpegCodecIdVP8 { - codecName = "vp8" - } + let codecName = resolveFFMpegCodecName(id: codecId) streamInfos.append((isVideo: isVideo, info: FFMpegMediaInfo.Info( index: Int(streamIndex), @@ -987,3 +940,21 @@ public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? { return FFMpegMediaInfo(audio: streamInfos.first(where: { !$0.isVideo })?.info, video: streamInfos.first(where: { $0.isVideo })?.info) } + +public func resolveFFMpegCodecName(id: Int32) -> String? { + if id == FFMpegCodecIdMPEG4 { + return "mpeg4" + } else if id == FFMpegCodecIdH264 { + return "h264" + } else if id == FFMpegCodecIdHEVC { + return "hevc" + } else if id == FFMpegCodecIdAV1 { + return "av1" + } else if id == FFMpegCodecIdVP9 { + return "vp9" + } else if id == FFMpegCodecIdVP8 { + return "vp8" + } else { + return nil + } +} diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index f6800ae681..41dfca3845 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -146,7 +146,6 @@ private final class UniversalSoftwareVideoSourceImpl { self.size = sizeValue } - self.mediaBox = mediaBox self.source = source self.automaticallyFetchHeader = automaticallyFetchHeader @@ -210,7 +209,7 @@ private final class UniversalSoftwareVideoSourceImpl { let rotationAngle: Double = metrics.rotationAngle let aspect = Double(metrics.width) / Double(metrics.height) - if let codec = FFMpegAVCodec.find(forId: codecId) { + if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { diff --git a/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift index 54e5ac3402..2a23bed3ce 100644 --- a/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift +++ b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift @@ -112,7 +112,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { let arguments = arguments as! PaymentMethodListScreenArguments switch self { case let .header(text): - return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "Invite", sectionId: self.section) + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: NSAttributedString(string: text), animationName: "Invite", sectionId: self.section) case let .methodsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .addMethod(text): diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 759746fba5..de69497b10 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -281,7 +281,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = self.context.sharedContext.mediaManager let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) + let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true self.videoStartTimestamp = video.representation.startTimestamp diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index dfb059861b..34b97983cf 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -366,7 +366,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.canAttachContent = true videoNode.isHidden = true diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 6f6e2f63f5..0116746e4a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -158,7 +158,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { case let .participant(participant, label, revealActions, revealed, enabled): let status: ContactsPeerItemStatus if let label = label { - status = .custom(string: label, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil) } else if let presence = participant.presences[participant.peer.id], self.addIcon { status = .presence(EnginePeer.Presence(presence), dateTimeFormat) } else { diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index c85e72cd03..b9f3bf040d 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -129,7 +129,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { case let .peer(_, participant, editing, label, enabled, isChannel, isContact): let status: ContactsPeerItemStatus if let label = label { - status = .custom(string: label, multiline: false, isActive: false, icon: nil) + status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil) } else if participant.peer.id != context.account.peerId { let presence = participant.presences[participant.peer.id] ?? TelegramUserPresence(status: .none, lastActivity: 0) status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat) diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index caca7b1d64..fea99ac088 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -705,6 +705,17 @@ public final class MediaBox { } } + public func internal_resourceDataIsCached(id: MediaResourceId, size: Int64, in range: Range) -> Bool { + let paths = self.storePathsForId(id) + + if let _ = fileSize(paths.complete) { + return true + } else { + let tempManager = MediaBoxFileManager(queue: nil) + return MediaBoxPartialFile.internal_isPartialDataCached(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 cb6db26c64..f6ecebed0f 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -106,6 +106,16 @@ final class MediaBoxPartialFile { return (fd, Int(clippedRange.upperBound - clippedRange.lowerBound)) } + static func internal_isPartialDataCached(manager: MediaBoxFileManager, path: String, metaPath: String, range: Range) -> Bool { + guard let fileMap = try? MediaBoxFileMap.read(manager: manager, path: metaPath) else { + return false + } + guard let _ = fileMap.contains(range) else { + return false + } + return true + } + var storedSize: Int64 { assert(self.queue.isCurrent()) return self.fileMap.sum diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index d0b9fddc61..978ec8e094 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -513,7 +513,11 @@ private final class TimeBasedCleanupImpl { } func touch(paths: [String]) { - self.scheduledTouches.append(contentsOf: paths) + for path in paths { + if !self.scheduledTouches.contains(path) { + self.scheduledTouches.append(path) + } + } self.scheduleTouches() } diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 8b963e3336..7f9f46566c 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -233,7 +233,7 @@ private final class PhoneView: UIView { hintDimensions: CGSize(width: 1170, height: 1754), storeAfterDownload: nil ) - let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.canAttachContent = true self.videoNode = videoNode diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index 194d60b171..f7517c7427 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -117,7 +117,7 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { let arguments = arguments as! DeleteAccountDataArguments switch self { case let .header(theme, animation, title, text, hideOnSmallScreens): - return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil) + return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: NSAttributedString(string: text), animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil) case let .peers(_, peers): return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section) case let .info(_, text): diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index eb958dece8..e6bd8fd778 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -277,20 +277,25 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte })) if let postbox, let mediaManager = environment.mediaManager, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) { + let _ = postbox + let _ = mediaManager + let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black) + let _ = decoration let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) + let _ = videoContent - let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + /*let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) videoNode.alpha = 0.01 self.videoNode = videoNode self.addSubnode(videoNode) videoNode.canAttachContent = true - videoNode.play() + videoNode.play()*/ } } diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 07fc5652d8..9a9790fd9b 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -336,7 +336,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { theme: item.presentationData.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index b04192dc84..08827059e0 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -234,8 +234,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1135897376] = { return Api.DefaultHistoryTTL.parse_defaultHistoryTTL($0) } dict[-712374074] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } - dict[1605718587] = { return Api.DialogFilter.parse_dialogFilter($0) } - dict[-1612542300] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } + dict[-1438177711] = { return Api.DialogFilter.parse_dialogFilter($0) } + dict[-1772913705] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) } @@ -1226,7 +1226,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1044107055] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultAdsHidden($0) } dict[-2073059774] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultChooseOption($0) } dict[-1384544183] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultReported($0) } - dict[500007837] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) } + dict[-250687953] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) } dict[-91752871] = { return Api.chatlists.ChatlistInvite.parse_chatlistInviteAlready($0) } dict[-1816295539] = { return Api.chatlists.ChatlistUpdates.parse_chatlistUpdates($0) } dict[283567014] = { return Api.chatlists.ExportedChatlistInvite.parse_exportedChatlistInvite($0) } diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index ea00fe9e2c..8fff493cf4 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -476,17 +476,17 @@ public extension Api.channels { } public extension Api.chatlists { enum ChatlistInvite: TypeConstructorDescription { - case chatlistInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + case chatlistInvite(flags: Int32, title: Api.TextWithEntities, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) case chatlistInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .chatlistInvite(let flags, let title, let emoticon, let peers, let chats, let users): if boxed { - buffer.appendInt32(500007837) + buffer.appendInt32(-250687953) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(peers.count)) @@ -545,8 +545,10 @@ public extension Api.chatlists { public static func parse_chatlistInvite(_ reader: BufferReader) -> ChatlistInvite? { var _1: Int32? _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + var _2: Api.TextWithEntities? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _3: String? if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } var _4: [Api.Peer]? diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 4b53dff4ca..609de6bbae 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1166,19 +1166,19 @@ public extension Api { } public extension Api { enum DialogFilter: TypeConstructorDescription { - case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) - case dialogFilterChatlist(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) + case dialogFilter(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) + case dialogFilterChatlist(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) case dialogFilterDefault public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .dialogFilter(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers, let excludePeers): if boxed { - buffer.appendInt32(1605718587) + buffer.appendInt32(-1438177711) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) @@ -1199,11 +1199,11 @@ public extension Api { break case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers): if boxed { - buffer.appendInt32(-1612542300) + buffer.appendInt32(-1772913705) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) + title.serialize(buffer, true) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) @@ -1242,8 +1242,10 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) + var _3: Api.TextWithEntities? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } var _5: Int32? @@ -1280,8 +1282,10 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) + var _3: Api.TextWithEntities? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } var _5: Int32? diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 6df318a5fd..50b17933d5 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -36,8 +36,6 @@ public final class PresentationCallImpl: PresentationCall { public let preferredVideoCodec: String? public let peer: EnginePeer? - private let isExpectedToBeConference: Bool - private let serializedData: String? private let dataSaving: VoiceCallDataSaving private let proxyServer: ProxyServerSettings? @@ -213,8 +211,6 @@ public final class PresentationCallImpl: PresentationCall { self.currentNetworkType = currentNetworkType self.updatedNetworkType = updatedNetworkType - self.isExpectedToBeConference = self.context.sharedContext.immediateExperimentalUISettings.conferenceCalls - var didReceiveAudioOutputs = false var callSessionState: Signal = .complete() @@ -326,18 +322,6 @@ public final class PresentationCallImpl: PresentationCall { self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in } } - - if self.isExpectedToBeConference { - self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in - Queue.mainQueue().async { - guard let self else { - return - } - - self.processConferenceSignalingData(dataList: dataList) - } - }) - } } deinit { @@ -554,6 +538,14 @@ public final class PresentationCallImpl: PresentationCall { presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case let .active(_, _, keyVisualHash, _, _, _, _, _, _), let .switchedToConference(_, keyVisualHash, _): self.callWasActive = true + + var isConference = false + if case let .active(_, _, _, _, _, version, _, _, _) = sessionState.state { + isConference = version == "13.0.0" + } else if case .switchedToConference = sessionState.state { + isConference = true + } + if let callContextState = callContextState { switch callContextState.state { case .initializing: @@ -580,7 +572,7 @@ public final class PresentationCallImpl: PresentationCall { } presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) } - } else if !self.isExpectedToBeConference { + } else if !isConference { presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) } } @@ -668,8 +660,7 @@ public final class PresentationCallImpl: PresentationCall { conferenceCall.hasActiveIncomingData ) |> map { remoteConferenceIsConnected, hasActiveIncomingData -> Bool in - //return remoteConferenceIsConnected || hasActiveIncomingData - return true + return remoteConferenceIsConnected || hasActiveIncomingData } |> distinctUntilChanged @@ -764,7 +755,7 @@ public final class PresentationCallImpl: PresentationCall { } if !self.didDropCall { self.didDropCall = true - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) + self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil)) } }) @@ -795,6 +786,28 @@ public final class PresentationCallImpl: PresentationCall { strongSelf.audioLevelPromise.set(level) } }) + + let localIsConnected = conferenceCall.state + |> map { state -> Bool in + switch state.networkState { + case .connected: + return true + default: + return false + } + } + |> distinctUntilChanged + + let bothLocalAndRemoteConnected = combineLatest(queue: .mainQueue(), + localIsConnected, + remoteIsConnectedAggregated + ) + |> map { localIsConnected, remoteIsConnectedAggregated -> Bool in + return localIsConnected && remoteIsConnectedAggregated + } + |> distinctUntilChanged + + conferenceCall.internal_isRemoteConnected.set(bothLocalAndRemoteConnected) }) } } @@ -805,13 +818,25 @@ public final class PresentationCallImpl: PresentationCall { self.audioSessionShouldBeActive.set(true) } case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall): - if conferenceCall == nil, self.isExpectedToBeConference { + if conferenceCall == nil, version == "13.0.0" { self.createConferenceIfPossible() } self.audioSessionShouldBeActive.set(true) - if self.isExpectedToBeConference || conferenceCallData != nil { + if version == "13.0.0" && self.conferenceSignalingDataDisposable == nil { + self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in + Queue.mainQueue().async { + guard let self else { + return + } + + self.processConferenceSignalingData(dataList: dataList) + } + }) + } + + if version == "13.0.0" || conferenceCallData != nil { if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } @@ -1112,9 +1137,9 @@ public final class PresentationCallImpl: PresentationCall { } private func sendConferenceSignalingMessage(dict: [String: Any]) { - /*if let data = try? JSONSerialization.data(withJSONObject: dict) { + if let data = try? JSONSerialization.data(withJSONObject: dict) { self.context.account.callSessionManager.sendSignalingData(internalId: self.internalId, data: data) - }*/ + } } private func updateIsAudioSessionActive(_ value: Bool) { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index cd67f6fc57..641110e91b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -871,6 +871,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let conferenceFromCallId: CallId? private let isConference: Bool + var internal_isRemoteConnected = Promise() + private var internal_isRemoteConnectedDisposable: Disposable? + public var onMutedSpeechActivityDetected: ((Bool) -> Void)? init( @@ -1228,6 +1231,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.screencastFramesDisposable?.dispose() self.screencastAudioDataDisposable?.dispose() self.screencastStateDisposable?.dispose() + + self.internal_isRemoteConnectedDisposable?.dispose() } private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) { @@ -1712,9 +1717,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var encryptionKey: Data? encryptionKey = self.encryptionKey?.key - if "".isEmpty { - encryptionKey = nil - } genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: self.audioSessionActive.get(), video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in let disposable = MetaDisposable() @@ -1786,6 +1788,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) self.signalBarsPromise.set(callContext.signalBars) + + self.internal_isRemoteConnectedDisposable = (self.internal_isRemoteConnected.get() + |> distinctUntilChanged + |> deliverOnMainQueue).startStrict(next: { [weak callContext] isRemoteConnected in + guard let callContext else { + return + } + callContext.addRemoteConnectedEvent(isRemoteConntected: isRemoteConnected) + }) } } @@ -2365,22 +2376,28 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let muteState = filteredMuteState { if muteState.canUnmute { - switch strongSelf.isMutedValue { - case let .muted(isPushToTalkActive): - if !isPushToTalkActive { - strongSelf.genericCallContext?.setIsMuted(true) - } - case .unmuted: + if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute { strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.isMutedPromise.set(strongSelf.isMutedValue) + strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) strongSelf.genericCallContext?.setIsMuted(true) + } else { + switch strongSelf.isMutedValue { + case .muted: + break + case .unmuted: + let _ = strongSelf.updateMuteState(peerId: strongSelf.joinAsPeerId, isMuted: false) + } } } else { strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.isMutedPromise.set(strongSelf.isMutedValue) strongSelf.genericCallContext?.setIsMuted(true) + strongSelf.stateValue.muteState = muteState } - strongSelf.stateValue.muteState = muteState } else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute { strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.isMutedPromise.set(strongSelf.isMutedValue) strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) strongSelf.genericCallContext?.setIsMuted(true) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 617d4df551..c3eea4b4eb 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -419,7 +419,12 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } - let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription) + let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? + if component.isMyPeer && component.isPresentation { + videoDescription = nil + } else { + videoDescription = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription) + } var isEffectivelyPaused = false if let videoDescription, videoDescription.isPaused { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index 1e05a29420..4bbd537ed0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -152,16 +152,15 @@ extension VideoChatScreenComponent.View { } } - //TODO:localize let qualityList: [(Int, String)] = [ - (0, "Audio Only"), + (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), (180, "180p"), (360, "360p"), (Int.max, "720p") ] let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" - items.append(.action(ContextMenuActionItem(text: "Receive Video Quality", textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in + items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in return nil }, action: { [weak self] c, _ in guard let self else { diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 1b02ac704f..9973824ba0 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -132,6 +132,7 @@ enum AccountStateMutationOperation { case UpdateStarsBalance(peerId: PeerId, balance: Api.StarsAmount) case UpdateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances) case UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: Bool) + case ReportMessageDelivery([MessageId]) } struct HoleFromPreviousState { @@ -702,9 +703,13 @@ struct AccountMutableState { self.addOperation(.UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: isAnonymous)) } + mutating func addReportMessageDelivery(messageIds: [MessageId]) { + self.addOperation(.ReportMessageDelivery(messageIds)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery: break case let .AddMessages(messages, location): for message in messages { @@ -852,6 +857,7 @@ struct AccountReplayedFinalState { let updatedStarsBalance: [PeerId: StarsAmount] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] let sentScheduledMessageIds: Set + let reportMessageDelivery: Set } struct AccountFinalStateEvents { @@ -882,12 +888,13 @@ struct AccountFinalStateEvents { let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: StarsAmount] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] + let reportMessageDelivery: Set var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set()) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set()) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -915,6 +922,7 @@ struct AccountFinalStateEvents { self.updatedStarsBalance = updatedStarsBalance self.updatedStarsRevenueStatus = updatedStarsRevenueStatus self.sentScheduledMessageIds = sentScheduledMessageIds + self.reportMessageDelivery = reportMessageDelivery } init(state: AccountReplayedFinalState) { @@ -945,6 +953,7 @@ struct AccountFinalStateEvents { self.updatedStarsBalance = state.updatedStarsBalance self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus self.sentScheduledMessageIds = state.sentScheduledMessageIds + self.reportMessageDelivery = state.reportMessageDelivery } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -977,6 +986,9 @@ struct AccountFinalStateEvents { var sentScheduledMessageIds = self.sentScheduledMessageIds sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds) + var reportMessageDelivery = self.reportMessageDelivery + reportMessageDelivery.formUnion(other.reportMessageDelivery) + + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds, reportMessageDelivery: reportMessageDelivery) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 65752ba302..ec8a8046fa 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -171,7 +171,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) - case let .messageActionStarGift(flags, apiGift, message, convertStars, _, upgradeStars): + case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars): let text: String? let entities: [MessageTextEntity]? switch message { @@ -185,7 +185,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0)) + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId)) case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars): guard let gift = StarGift(apiStarGift: apiGift) else { return nil diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e008aa32e1..b5d80567dc 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -721,6 +721,7 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network: updatedState.mergeChats(chats) updatedState.mergeUsers(users) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for message in messages { if let preCachedResources = message.preCachedResources { for (resource, data) in preCachedResources { @@ -738,6 +739,10 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network: } if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { updatedState.addMessages([message], location: .UpperHistoryBlock) + + if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime { + updatedState.addReportMessageDelivery(messageIds: [id]) + } } } @@ -901,6 +906,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var missingUpdatesFromChannels = Set() for update in sortedUpdates(updates) { @@ -1110,6 +1117,10 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } updatedState.addMessages([message], location: .UpperHistoryBlock) + + if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime { + updatedState.addReportMessageDelivery(messageIds: [id]) + } } case let .updateServiceNotification(flags, date, type, text, media, entities): let popup = (flags & (1 << 0)) != 0 @@ -3282,7 +3293,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3421,6 +3432,7 @@ func replayFinalState( var updatedStarsBalance: [PeerId: StarsAmount] = [:] var updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:] var updatedStarsReactionsAreAnonymousByDefault: Bool? + var reportMessageDelivery = Set() var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -4855,6 +4867,8 @@ func replayFinalState( updatedStarsRevenueStatus[peerId] = status case let .UpdateStarsReactionsAreAnonymousByDefault(value): updatedStarsReactionsAreAnonymousByDefault = value + case let .ReportMessageDelivery(messageIds): + reportMessageDelivery = Set(messageIds) } } @@ -5376,6 +5390,7 @@ func replayFinalState( updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus, - sentScheduledMessageIds: finalState.state.sentScheduledMessageIds + sentScheduledMessageIds: finalState.state.sentScheduledMessageIds, + reportMessageDelivery: reportMessageDelivery ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index d975eef92f..d6494b0923 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -352,6 +352,7 @@ public final class AccountStateManager { private let appliedMaxMessageIdDisposable = MetaDisposable() private let appliedQtsPromise = Promise(nil) private let appliedQtsDisposable = MetaDisposable() + private let reportMessageDeliveryDisposable = DisposableSet() let updateConfigRequested: (() -> Void)? let isPremiumUpdated: (() -> Void)? @@ -391,6 +392,7 @@ public final class AccountStateManager { self.operationDisposable.dispose() self.appliedMaxMessageIdDisposable.dispose() self.appliedQtsDisposable.dispose() + self.reportMessageDeliveryDisposable.dispose() } public func reset() { @@ -1130,6 +1132,9 @@ public final class AccountStateManager { if !events.sentScheduledMessageIds.isEmpty { strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds) } + if !events.reportMessageDelivery.isEmpty { + strongSelf.reportMessageDeliveryDisposable.add(_internal_reportMessageDelivery(postbox: strongSelf.postbox, network: strongSelf.network, messageIds: Array(events.reportMessageDelivery), fromPushNotification: false).start()) + } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) } diff --git a/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift index 7f9ce3a540..489a5014d9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift @@ -4,24 +4,20 @@ import TelegramApi public final class ReportDeliveryMessageAttribute: Equatable, MessageAttribute { public let untilDate: Int32 - public let isReported: Bool public init(untilDate: Int32, isReported: Bool) { self.untilDate = untilDate - self.isReported = isReported } required public init(decoder: PostboxDecoder) { self.untilDate = decoder.decodeInt32ForKey("d", orElse: 0) - self.isReported = decoder.decodeBoolForKey("r", orElse: false) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.untilDate, forKey: "d") - encoder.encodeBool(self.isReported, forKey: "r") } public static func ==(lhs: ReportDeliveryMessageAttribute, rhs: ReportDeliveryMessageAttribute) -> Bool { - return lhs.untilDate == rhs.untilDate && lhs.isReported == rhs.isReported + return lhs.untilDate == rhs.untilDate } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index bd1dde1685..a0dd0fbc3f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -130,7 +130,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) - case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool) + case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool) public init(decoder: PostboxDecoder) { @@ -253,7 +253,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) case 44: - self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false)) + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId")) case 45: self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false)) default: @@ -548,7 +548,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId): encoder.encodeInt32(44, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") if let convertStars { @@ -574,6 +574,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "upgradeStars") } encoder.encodeBool(isRefunded, forKey: "isRefunded") + if let upgradeMessageId { + encoder.encodeInt32(upgradeMessageId, forKey: "upgradeMessageId") + } else { + encoder.encodeNil(forKey: "upgradeMessageId") + } case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift new file mode 100644 index 0000000000..a52477b9c1 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift @@ -0,0 +1,33 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +public func _internal_reportMessageDelivery(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal { + var signals: [Signal] = [] + for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) { + signals.append(_internal_reportMessageDeliveryByPeerId(postbox: postbox, network: network, peerId: peerId, messageIds: messageIds, fromPushNotification: fromPushNotification)) + } + return combineLatest(signals) + |> ignoreValues +} + +private func _internal_reportMessageDeliveryByPeerId(postbox: Postbox, network: Network, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { + return .complete() + } + var flags: Int32 = 0 + if fromPushNotification { + flags |= (1 << 0) + } + return network.request(Api.functions.messages.reportMessagesDelivery(flags: flags, peer: inputPeer, id: messageIds.map { $0.id })) + |> `catch` { error -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ in + return .complete() + } + } + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index a64a77a28b..c5ae9f3fce 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -175,6 +175,7 @@ extension BotPaymentMethod { public enum BotPaymentFormRequestError { case generic case alreadyActive + case noPaymentNeeded } extension BotPaymentInvoice { @@ -457,7 +458,10 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw } return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: serializedThemeParams)) - |> `catch` { _ -> Signal in + |> `catch` { error -> Signal in + if error.errorDescription == "NO_PAYMENT_NEEDED" { + return .fail(.noPaymentNeeded) + } return .fail(.generic) } |> mapToSignal { result -> Signal in @@ -622,7 +626,7 @@ public enum SendBotPaymentFormError { } public enum SendBotPaymentResult { - case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?) + case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?, uniqueStarGift: ProfileGiftsContext.State.StarGift?) case externalVerificationRequired(url: String) } @@ -671,12 +675,12 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa case .starsChatSubscription: let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } if let first = chats.first { - return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil) } default: break } - + for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { @@ -721,7 +725,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: nil) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index ecd058ecb1..1ddc78009f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -689,15 +689,23 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, messageId: Engi } else { let source: BotPaymentInvoiceSource = .starGiftTransfer(messageId: messageId, toPeerId: peerId) return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) - |> mapError { _ -> TransferStarGiftError in - return .generic + |> map(Optional.init) + |> `catch` { error -> Signal in + if case .noPaymentNeeded = error { + return .single(nil) + } + return .fail(.generic) } |> mapToSignal { paymentForm in - return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) - |> mapError { _ -> TransferStarGiftError in - return .generic + if let paymentForm { + return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) + |> mapError { _ -> TransferStarGiftError in + return .generic + } + |> ignoreValues + } else { + return _internal_transferStarGift(account: account, prepaid: true, messageId: messageId, peerId: peerId) } - |> ignoreValues } } } @@ -714,8 +722,12 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, messageId: Engi |> mapError { _ -> UpgradeStarGiftError in return .generic } - |> mapToSignal { _ in - return .complete() + |> mapToSignal { result in + if case let .done(_, _, gift) = result, let gift { + return .single(gift) + } else { + return .complete() + } } } else { var flags: Int32 = 0 @@ -970,16 +982,30 @@ private final class ProfileGiftsContextImpl { self.pushState() } - func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) { - self.actionDisposable.set( - _internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in - guard let self else { - return - } - let _ = self - }) - ) - self.pushState() + func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + let disposable = MetaDisposable() + disposable.set( + _internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in + guard let self else { + return + } + if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) { + self.gifts[index] = result + self.pushState() + } + subscriber.putNext(result) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + ) + return disposable + } } private func pushState() { @@ -1186,9 +1212,19 @@ public final class ProfileGiftsContext { } } - public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) { - self.impl.with { impl in - impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).start(next: { value in + subscriber.putNext(value) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index ed4e4f3220..2fb2492280 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1418,12 +1418,13 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .starsChatSubscription: let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } if let first = chats.first { - return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil) } default: break } var receiptMessageId: MessageId? + var resultGift: ProfileGiftsContext.State.StarGift? for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { @@ -1463,12 +1464,28 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer: receiptMessageId = nil } + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _) = action.action, case let .Id(messageId) = message.id { + resultGift = ProfileGiftsContext.State.StarGift( + gift: gift, + fromPeer: nil, + date: message.timestamp, + text: nil, + entities: nil, + messageId: messageId, + nameHidden: false, + savedToProfile: savedToProfile, + convertStars: nil, + canUpgrade: false, + canExportDate: canExportDate, + upgradeStars: nil, + transferStars: transferStars + ) } } } } } - return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: resultGift) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 14e5750201..1c9193d8bf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -342,9 +342,17 @@ extension ChatListFilter { case .dialogFilterDefault: self = .allChats case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers): + let titleText: String + let titleEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + titleText = text + titleEntities = messageTextEntitiesFromApiEntities(entities) + } + let disableTitleAnimations = (flags & (1 << 28)) != 0 self = .filter( id: id, - title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), + title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations), emoticon: emoticon, data: ChatListFilterData( isShared: false, @@ -392,9 +400,18 @@ extension ChatListFilter { ) ) case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers): + let titleText: String + let titleEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + titleText = text + titleEntities = messageTextEntitiesFromApiEntities(entities) + } + let disableTitleAnimations = (flags & (1 << 28)) != 0 + self = .filter( id: id, - title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), + title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations), emoticon: emoticon, data: ChatListFilterData( isShared: true, @@ -446,7 +463,10 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilterChatlist(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if !title.enableAnimations { + flags |= 1 << 28 + } + return .dialogFilterChatlist(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { @@ -472,7 +492,10 @@ extension ChatListFilter { if data.color != nil { flags |= 1 << 27 } - return .dialogFilter(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if !title.enableAnimations { + flags |= 1 << 28 + } + return .dialogFilter(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 7d6345288d..a755647731 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -273,9 +273,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ChatFolderLinkContents in switch result { - case let .chatlistInvite(_, title, emoticon, peers, chats, users): + case let .chatlistInvite(flags, title, emoticon, peers, chats, users): let _ = emoticon + let disableTitleAnimation = (flags & (1 << 1)) != 0 + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) var memberCounts: [PeerId: Int] = [:] @@ -301,7 +303,15 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal 0 { text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string } else { text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle @@ -500,16 +500,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { peerName = EnginePeer(peer).compactDisplayTitle } if peerName.isEmpty { - if let convertStars { + if let convertStars, convertStars > 0 { text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string } else { text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle } } else { - let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars ?? 0))) - text = formattedString.string - if let starsRange = formattedString.ranges.last { - entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold)) + if let convertStars, convertStars > 0 { + let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars))) + text = formattedString.string + if let starsRange = formattedString.ranges.last { + entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold)) + } + } else { + } } } @@ -524,17 +528,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } ribbonTitle = item.presentationData.strings.Notification_StarGift_OneOf(availabilityString).string } - if incoming, let upgradeStars, upgradeStars > 0, !upgraded { + if incoming || item.presentationData.isPreview, let upgradeStars, upgradeStars > 0, !upgraded { buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack buttonIcon = "Premium/GiftUnpack" } else { buttonTitle = item.presentationData.strings.Notification_StarGift_View } } - case let .starGiftUnique(gift, _, _, _, _, _, isRefunded): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded): if case let .unique(uniqueGift) = gift { isStarGift = true - let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + let authorName: String + if isUpgrade && item.message.author?.id == item.context.account.peerId { + authorName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + } else { + authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + } title = item.presentationData.strings.Notification_StarGift_Title(authorName).string text = "**\(uniqueGift.title) #\(uniqueGift.number)**" ribbonTitle = item.presentationData.strings.Notification_StarGift_Gift diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 9bd38d928d..a55f886df6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -763,7 +763,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { }) } let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { + let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { if let strongSelf = self { if let item = strongSelf.item { if strongSelf.infoBackgroundNode.alpha.isZero { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 0df17ee073..26da378aa8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1830,7 +1830,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } ) } - let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(context: context, 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 if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index cbcbc37fc4..11008ec607 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -222,7 +222,7 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD index 6596b91582..b8e4d8a129 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/WallpaperBackgroundNode", "//submodules/TelegramPresentationData", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index 031d172426..ef42321985 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -11,6 +11,7 @@ import Markdown import WallpaperBackgroundNode import EmojiStatusComponent import TelegramPresentationData +import TextNodeWithEntities final class BlurredRoundedRectangle: Component { let color: UIColor @@ -1210,21 +1211,42 @@ public final class ChatOverscrollControl: CombinedComponent { } public final class ChatInputPanelOverscrollNode: ASDisplayNode { - public let text: (String, [(Int, NSRange)]) + public let text: NSAttributedString public let priority: Int - private let titleNode: ImmediateTextNode + private let titleNode: ImmediateTextNodeWithEntities - public init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) { + public init(context: AccountContext, text: NSAttributedString, color: UIColor, priority: Int) { self.text = text self.priority = priority - self.titleNode = ImmediateTextNode() + self.titleNode = ImmediateTextNodeWithEntities() super.init() - let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: color) - let bold = MarkdownAttributeSet(font: Font.bold(14.0), textColor: color) - - self.titleNode.attributedText = addAttributesToStringWithRanges(text, body: body, argumentAttributes: [0: bold]) + let attributedText = NSMutableAttributedString(string: text.string) + attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: text.length)) + attributedText.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: text.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + attributedText.addAttribute(.font, value: Font.bold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + attributedText.addAttribute(key, value: value, range: range) + } + } + }) + self.titleNode.attributedText = attributedText + self.titleNode.visibility = true + self.titleNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: color.withMultipliedAlpha(0.1), + attemptSynchronous: true + ) self.addSubnode(self.titleNode) } diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 9cef08d2d2..fe0ddd4798 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -2286,7 +2286,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { } } else { let videoContent = NativeVideoContent(id: .message(message.stableId, video.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: video), streamVideo: .conservative, loopVideo: true, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: self.isStatic, continuePlayingWithoutSoundOnLostAudioSession: true, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) - let videoNode = 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: videoContent, priority: .overlay, autoplay: !self.isStatic) + let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) self.videoStatusDisposable.set((videoNode.status |> deliverOnMainQueue).startStrict(next: { [weak self] status in diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 0adf863528..664a0e0e73 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/PremiumUI", "//submodules/QrCodeUI", "//submodules/InviteLinksUI", + "//submodules/Components/MultilineTextWithEntitiesComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift index 772bff6d42..ed38ce437d 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift @@ -7,6 +7,7 @@ import AccountContext import MultilineTextComponent import TelegramPresentationData import TelegramCore +import MultilineTextWithEntitiesComponent final class BadgeComponent: Component { let fillColor: UIColor @@ -84,17 +85,20 @@ final class BadgeComponent: Component { } final class ChatFolderLinkHeaderComponent: Component { + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let title: ChatFolderTitle let badge: String? init( + context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, title: ChatFolderTitle, badge: String? ) { + self.context = context self.theme = theme self.strings = strings self.title = title @@ -213,10 +217,16 @@ final class ChatFolderLinkHeaderComponent: Component { } contentWidth += spacing - //TODO:release let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: component.title.text, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + text: .plain(component.title.attributedString(font: Font.semibold(17.0), textColor: component.theme.list.itemAccentColor)), + manualVisibilityControl: false + )), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 540ec1764a..7b623f3485 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent +import MultilineTextWithEntitiesComponent import SolidRoundedButtonComponent import PresentationDataUtils import Markdown @@ -437,6 +438,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let topIconSize = self.topIcon.update( transition: contentTransition, component: AnyComponent(ChatFolderLinkHeaderComponent( + context: component.context, theme: environment.theme, strings: environment.strings, title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true), @@ -457,37 +459,53 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += topIconSize.height contentHeight += 20.0 - let text: String + let text: NSAttributedString if case .linkList = component.subject { - text = environment.strings.FolderLinkPreview_TextLinkList + text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextLinkList) } else if let linkContents = component.linkContents { if case .remove = component.subject { - text = environment.strings.FolderLinkPreview_TextRemoveFolder + text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextRemoveFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) } else if allChatsAdded { - text = environment.strings.FolderLinkPreview_TextAllAdded + text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAllAdded, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) } else if linkContents.localFilterId == nil { - text = environment.strings.FolderLinkPreview_TextAddFolder - } else { + text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAddFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) + } else if let title = linkContents.title { let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount)) - //TODO:release - text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title?.text ?? "").string + + let textValue = NSMutableAttributedString(string: environment.strings.FolderLinkPreview_TextAddChatsV2) + textValue.addAttributes([ + .font: Font.regular(15.0), + .foregroundColor: environment.theme.list.freeTextColor + ], range: NSRange(location: 0, length: textValue.length)) + + let folderRange = (textValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + textValue.replaceCharacters(in: folderRange, with: "") + textValue.insert(title.attributedString(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: folderRange.location) + } + + let chatsRange = (textValue.string as NSString).range(of: "{chats}") + if chatsRange.location != NSNotFound { + textValue.replaceCharacters(in: chatsRange, with: "") + textValue.insert(NSAttributedString(string: chatCountString, font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: chatsRange.location) + } + + text = textValue + } else { + text = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) } } else { - text = " " + text = NSAttributedString(string: " ") } - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor) - let descriptionTextSize = self.descriptionText.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: text, attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - )), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: environment.theme.list.freeTextColor.withMultipliedAlpha(0.1), + text: .plain(text), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -982,8 +1000,12 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems))) disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) - //TODO:release - let folderTitle = linkContents.title?.text ?? "" + let folderTitle: ChatFolderTitle + if let title = linkContents.title { + folderTitle = title + } else { + folderTitle = ChatFolderTitle(text: "", entities: [], enableAnimations: true) + } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) @@ -1015,11 +1037,18 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } + let undoText = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastLeftTitleV2) + let folderRange = (undoText.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + undoText.replaceCharacters(in: folderRange, with: "") + undoText.insert(folderTitle.rawAttributedString, at: folderRange.location) + } + let context = component.context let selectedItems = self.selectedItems let undoOverlayController = UndoOverlayController( presentationData: presentationData, - content: .removedChat(title: presentationData.strings.FolderLinkPreview_ToastLeftTitle(folderTitle).string, text: additionalText), + content: .removedChat(context: component.context, title: undoText, text: additionalText), elevatedLayout: false, action: { value in if case .commit = value { @@ -1112,8 +1141,14 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } if isUpdates { - //TODO:release - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else if result.newChatCount != 0 { let animationBackgroundColor: UIColor if presentationData.theme.overallDarkAppearance { @@ -1121,8 +1156,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { animationBackgroundColor = UIColor(rgb: 0x474747) } - //TODO:release - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else { let animationBackgroundColor: UIColor if presentationData.theme.overallDarkAppearance { @@ -1130,8 +1172,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { animationBackgroundColor = UIColor(rgb: 0x474747) } - //TODO:release - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: ""), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } }) } @@ -1310,7 +1359,6 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { }) let navigationController = controller.navigationController - //TODO:release controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) })) diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index b79872b7a1..bb0ca5c79f 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -90,7 +90,7 @@ public enum ChatTitleContent: Equatable { case replies } - case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) + case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) case replyThread(type: ReplyThreadType, count: Int) case custom(String, String?, Bool) @@ -104,7 +104,7 @@ public enum ChatTitleContent: Equatable { if customTitle != rhsCustomTitle { return false } - if onlineMemberCount != rhsOnlineMemberCount { + if onlineMemberCount.0 != rhsOnlineMemberCount.0 || onlineMemberCount.1 != rhsOnlineMemberCount.1 { return false } if isScheduledMessages != rhsIsScheduledMessages { @@ -616,7 +616,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if channel.flags.contains(.isForum), customTitle != nil { let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) - } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = onlineMemberCount.total ?? cachedChannelData.participantsSummary.memberCount { if memberCount == 0 { let string: NSAttributedString if case .group = channel.info { @@ -626,7 +626,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } state = .info(string, .generic) } else { - if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + if case .group = channel.info, let onlineMemberCount = onlineMemberCount.recent, onlineMemberCount > 1 { let string = NSMutableAttributedString() string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index e95be0a80b..b942c311a5 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -382,6 +382,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public var enableAnimation: Bool = true + public weak var mirrorLayer: CALayer? { didSet { if let mirrorLayer = self.mirrorLayer { @@ -742,6 +744,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.loadAnimation() } else { let isTemplate = file.isCustomTemplateEmoji + self.updateTintColor() let pointSize = arguments.pointSize let placeholderColor = arguments.placeholderColor diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD index 2fec2704be..518a36b234 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -24,6 +24,9 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift index c5d161e3de..d6ad0989b9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift @@ -13,15 +13,21 @@ public final class GiftAnimationComponent: Component { let context: AccountContext let theme: PresentationTheme let file: TelegramMediaFile? + let still: Bool + let size: CGSize? public init( context: AccountContext, theme: PresentationTheme, - file: TelegramMediaFile? + file: TelegramMediaFile?, + still: Bool = false, + size: CGSize? = nil ) { self.context = context self.theme = theme self.file = file + self.still = still + self.size = size } public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { @@ -34,6 +40,12 @@ public final class GiftAnimationComponent: Component { if lhs.file != rhs.file { return false } + if lhs.still != rhs.still { + return false + } + if lhs.size != rhs.size { + return false + } return true } @@ -61,7 +73,7 @@ public final class GiftAnimationComponent: Component { file: component.file ) - let iconSize = availableSize + let iconSize = component.size ?? availableSize if self.animationLayer == nil { let animationLayer = InlineStickerItemLayer( context: .account(component.context), @@ -71,12 +83,12 @@ public final class GiftAnimationComponent: Component { file: component.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, - unique: true, + unique: !component.still, placeholderColor: component.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), - loopCount: 1 + loopCount: component.still ? 0 : 1 ) - animationLayer.isVisibleForAnimations = true + animationLayer.isVisibleForAnimations = !component.still self.animationLayer = animationLayer self.layer.addSublayer(animationLayer) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift similarity index 82% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift rename to submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 8f084fc3f8..1efa05a41b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -13,7 +13,7 @@ import PeerInfoCoverComponent import AnimatedStickerNode import TelegramAnimatedStickerNode -final class GiftCompositionComponent: Component { +public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? public init() { @@ -21,7 +21,7 @@ final class GiftCompositionComponent: Component { } } - enum Subject: Equatable { + public enum Subject: Equatable { case generic(TelegramMediaFile) case unique(StarGift.UniqueGift) case preview([StarGift.UniqueGift.Attribute]) @@ -30,15 +30,15 @@ final class GiftCompositionComponent: Component { let context: AccountContext let theme: PresentationTheme let subject: Subject - let externalState: ExternalState + let externalState: ExternalState? let requestUpdate: () -> Void - init( + public init( context: AccountContext, theme: PresentationTheme, subject: Subject, - externalState: ExternalState, - requestUpdate: @escaping () -> Void + externalState: ExternalState? = nil, + requestUpdate: @escaping () -> Void = {} ) { self.context = context self.theme = theme @@ -47,7 +47,7 @@ final class GiftCompositionComponent: Component { self.requestUpdate = requestUpdate } - static func ==(lhs: GiftCompositionComponent, rhs: GiftCompositionComponent) -> Bool { + public static func ==(lhs: GiftCompositionComponent, rhs: GiftCompositionComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -60,7 +60,7 @@ final class GiftCompositionComponent: Component { return true } - final class View: UIView { + public final class View: UIView { private var component: GiftCompositionComponent? private weak var componentState: EmptyComponentState? @@ -84,6 +84,8 @@ final class GiftCompositionComponent: Component { override init(frame: CGRect) { super.init(frame: frame) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) } required init?(coder: NSCoder) { @@ -94,7 +96,16 @@ final class GiftCompositionComponent: Component { self.disposables.dispose() } - func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + @objc private func handleTap() { + guard let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl else { + return + } + if case .once = animationNode.playbackMode, !animationNode.isPlaying { + animationNode.playOnce() + } + } + + public func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -107,6 +118,7 @@ final class GiftCompositionComponent: Component { var patternFile: TelegramMediaFile? var files: [Int64: TelegramMediaFile] = [:] + var loop = false switch component.subject { case let .generic(file): animationFile = file @@ -142,6 +154,8 @@ final class GiftCompositionComponent: Component { self.previewTimer = nil } case let .preview(sampleAttributes): + loop = true + if self.previewModels.isEmpty { var models: [StarGift.UniqueGift.Attribute] = [] var patterns: [StarGift.UniqueGift.Attribute] = [] @@ -198,8 +212,21 @@ final class GiftCompositionComponent: Component { return } self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count) - self.previewPatternIndex = (self.previewPatternIndex + 1) % Int32(self.previewPatterns.count) - self.previewBackdropIndex = (self.previewBackdropIndex + 1) % Int32(self.previewBackdrops.count) + + let previousPatternIndex = self.previewPatternIndex + var randomPatternIndex = previousPatternIndex + while randomPatternIndex == previousPatternIndex { + randomPatternIndex = Int32.random(in: 0 ..< Int32(self.previewPatterns.count)) + } + self.previewPatternIndex = randomPatternIndex + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex { + randomBackdropIndex = Int32.random(in: 0 ..< Int32(self.previewBackdrops.count)) + } + self.previewBackdropIndex = randomBackdropIndex + self.animatePreviewTransition = true self.componentState?.updated(transition: .easeInOut(duration: 0.25)) }, queue: Queue.mainQueue()) @@ -207,7 +234,7 @@ final class GiftCompositionComponent: Component { } } - component.externalState.previewPatternColor = secondBackgroundColor + component.externalState?.previewPatternColor = secondBackgroundColor var animateTransition = false if self.animatePreviewTransition { @@ -247,6 +274,7 @@ final class GiftCompositionComponent: Component { if backgroundView.superview == nil { backgroundTransition = .immediate backgroundView.clipsToBounds = true + backgroundView.isUserInteractionEnabled = false self.insertSubview(backgroundView, at: 0) backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) @@ -259,7 +287,7 @@ final class GiftCompositionComponent: Component { }) } - let iconSize = CGSize(width: 128.0, height: 128.0) + let iconSize = CGSize(width: 136.0, height: 136.0) var startFromIndex: Int? if animateTransition, let disappearingAnimationNode = self.animationNode { @@ -274,17 +302,25 @@ final class GiftCompositionComponent: Component { let animationNode: AnimatedStickerNode if self.animationNode == nil { animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.isUserInteractionEnabled = false self.animationNode = animationNode self.addSubview(animationNode.view) let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) animationNode.setup(source: AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(iconSize.width * 1.6), height: Int(iconSize.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) - + if let startFromIndex { + if let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl { + animationNode.playbackMode = loop ? .loop : .once + } animationNode.play(firstFrame: false, fromIndex: startFromIndex) } else { - animationNode.playLoop() + if loop { + animationNode.playLoop() + } else { + animationNode.playOnce() + } } animationNode.visibility = true animationNode.updateLayout(size: iconSize) @@ -295,7 +331,7 @@ final class GiftCompositionComponent: Component { } } if let animationNode = self.animationNode { - transition.setFrame(layer: animationNode.layer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: 25.0), size: iconSize)) + transition.setFrame(layer: animationNode.layer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: 20.0), size: iconSize)) } return availableSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index cd14f41cdb..ac21727aac 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -76,6 +76,12 @@ public final class GiftItemComponent: Component { case anonymous } + public enum Mode: Equatable { + case generic + case profile + case thumbnail + } + let context: AccountContext let theme: PresentationTheme let peer: GiftItemComponent.Peer? @@ -86,7 +92,7 @@ public final class GiftItemComponent: Component { let isLoading: Bool let isHidden: Bool let isSoldOut: Bool - let isSquare: Bool + let mode: Mode public init( context: AccountContext, @@ -99,7 +105,7 @@ public final class GiftItemComponent: Component { isLoading: Bool = false, isHidden: Bool = false, isSoldOut: Bool = false, - isSquare: Bool = false + mode: Mode = .generic ) { self.context = context self.theme = theme @@ -111,7 +117,7 @@ public final class GiftItemComponent: Component { self.isLoading = isLoading self.isHidden = isHidden self.isSoldOut = isSoldOut - self.isSquare = isSquare + self.mode = mode } public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { @@ -145,7 +151,7 @@ public final class GiftItemComponent: Component { if lhs.isSoldOut != rhs.isSoldOut { return false } - if lhs.isSquare != rhs.isSquare { + if lhs.mode != rhs.mode { return false } return true @@ -208,15 +214,21 @@ public final class GiftItemComponent: Component { let size: CGSize let iconSize: CGSize let cornerRadius: CGFloat - if component.isSquare { - size = CGSize(width: 60.0, height: 60.0) - iconSize = CGSize(width: 42.0, height: 42.0) - cornerRadius = 12.0 - } else { + switch component.mode { + case .generic: size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0) iconSize = CGSize(width: 88.0, height: 88.0) cornerRadius = 10.0 + case .profile: + size = CGSize(width: availableSize.width, height: availableSize.width) + iconSize = CGSize(width: 88.0, height: 88.0) + cornerRadius = 10.0 + case .thumbnail: + size = CGSize(width: 60.0, height: 60.0) + iconSize = CGSize(width: 42.0, height: 42.0) + cornerRadius = 12.0 } + self.backgroundLayer.cornerRadius = cornerRadius if component.isLoading { @@ -326,7 +338,7 @@ public final class GiftItemComponent: Component { self.layer.addSublayer(animationLayer) } - let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.isSquare ? floorToScreenPixels((size.height - iconSize.height) / 2.0) : animationOffset), size: iconSize) + let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.mode == .generic ? animationOffset : floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) if let animationLayer = self.animationLayer { transition.setFrame(layer: animationLayer, frame: animationFrame) } @@ -361,7 +373,7 @@ public final class GiftItemComponent: Component { } } - if !component.isSquare { + if case .generic = component.mode { if let title = component.title { let titleSize = self.title.update( transition: transition, @@ -503,7 +515,7 @@ public final class GiftItemComponent: Component { avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true)) } - avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0)) + avatarNode.frame = CGRect(origin: CGPoint(x: 5.0, y: 5.0), size: CGSize(width: 20.0, height: 20.0)) } if let backgroundColor, let _ = secondBackgroundColor { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index f7b3331e93..f1e0c80cce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -227,7 +227,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { case let .starGift(gift): media = [ TelegramMediaAction( - action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: true, upgradeStars: item.includeUpgrade ? gift.upgradeStars : 0, isRefunded: false) + action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: true, upgradeStars: item.includeUpgrade ? 1 : nil, isRefunded: false, upgradeMessageId: nil) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e382678172..a5e6cf3e74 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -139,7 +139,7 @@ final class GiftSetupScreenComponent: Component { override init(frame: CGRect) { self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delaysContentTouches = false @@ -253,6 +253,10 @@ final class GiftSetupScreenComponent: Component { |> deliverOnMainQueue).start(next: { [weak self] status in if let completion { completion() + + if let self, let controller = self.environment?.controller() { + controller.dismiss() + } } else { guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { return @@ -631,7 +635,6 @@ final class GiftSetupScreenComponent: Component { transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) } - let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sectionSpacing: CGFloat = 24.0 @@ -842,8 +845,8 @@ final class GiftSetupScreenComponent: Component { body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), - linkAttribute: { url in - return ("URL", url) + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) })) let upgradeFooterText = NSMutableAttributedString(attributedString: parsedString) @@ -986,19 +989,19 @@ final class GiftSetupScreenComponent: Component { contentHeight += hideSectionSize.height } - contentHeight += bottomContentInset + contentHeight += 24.0 - let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) - contentHeight += combinedBottomInset - - 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 buttonHeight: CGFloat = 50.0 let bottomPanelPadding: CGFloat = 12.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) + contentHeight += max(bottomPanelHeight, combinedBottomInset) + + 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 bottomPanelSize = self.buttonBackground.update( transition: transition, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 713eeb0674..942da7286a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -37,12 +37,9 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", - "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/TelegramUI/Components/CheckComponent", "//submodules/UndoUI", "//submodules/ConfettiEffect", - "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", ], diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index ae5f5e87ef..31105495aa 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -152,7 +152,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode { theme: self.presentationTheme, peer: nil, subject: .uniqueGift(gift: self.gift), - isSquare: true + mode: .thumbnail ) ), environment: {}, @@ -259,7 +259,7 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. let buttonText: String if transferStars > 0 { text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(transferStars))).string - buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) ⭐️\(transferStars)" + buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) $ \(transferStars)" } else { text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string buttonText = strings.Gift_Transfer_Confirmation_TransferFree diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 75cbba5bbb..9c5f2e592d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -27,6 +27,7 @@ import ConfettiEffect import PlainButtonComponent import CheckComponent import TooltipUI +import GiftAnimationComponent private let modelButtonTag = GenericComponentViewTag() private let backdropButtonTag = GenericComponentViewTag() @@ -45,6 +46,7 @@ private final class GiftViewSheetContent: CombinedComponent { let sendGift: (EnginePeer.Id) -> Void let openMyGifts: () -> Void let transferGift: () -> Void + let upgradeGift: ((Int64?, Bool) -> Signal) let showAttributeInfo: (Any, Float) -> Void let getController: () -> ViewController? @@ -59,6 +61,7 @@ private final class GiftViewSheetContent: CombinedComponent { sendGift: @escaping (EnginePeer.Id) -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), showAttributeInfo: @escaping (Any, Float) -> Void, getController: @escaping () -> ViewController? ) { @@ -72,6 +75,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.sendGift = sendGift self.openMyGifts = openMyGifts self.transferGift = transferGift + self.upgradeGift = upgradeGift self.showAttributeInfo = showAttributeInfo self.getController = getController } @@ -88,7 +92,8 @@ private final class GiftViewSheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext - var subject: GiftViewScreen.Subject + private(set) var subject: GiftViewScreen.Subject + private let upgradeGift: ((Int64?, Bool) -> Signal) private let getController: () -> ViewController? private var disposable: Disposable? @@ -109,6 +114,7 @@ private final class GiftViewSheetContent: CombinedComponent { var inUpgradePreview = false var upgradeForm: BotPaymentForm? + var upgradeFormDisposable: Disposable? var upgradeDisposable: Disposable? var sampleGiftAttributes: [StarGift.UniqueGift.Attribute]? @@ -130,14 +136,24 @@ private final class GiftViewSheetContent: CombinedComponent { var upgradedMockBackgroundColor: UIColor = .white var upgradedMockIcon: TelegramMediaFile? - init(context: AccountContext, subject: GiftViewScreen.Subject, getController: @escaping () -> ViewController?) { + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), + getController: @escaping () -> ViewController? + ) { self.context = context self.subject = subject + self.upgradeGift = upgradeGift self.getController = getController super.init() if let arguments = subject.arguments { + if let upgradeStars = arguments.upgradeStars, upgradeStars > 0 { + self.keepOriginalInfo = true + } + var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) { peerIds.append(fromPeerId) @@ -177,7 +193,7 @@ private final class GiftViewSheetContent: CombinedComponent { })) if arguments.upgradeStars == nil, let messageId = arguments.messageId { - self.upgradeDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, messageId: messageId), themeParams: nil) + self.upgradeFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, messageId: messageId), themeParams: nil) |> deliverOnMainQueue).start(next: { [weak self] paymentForm in guard let self else { return @@ -237,11 +253,12 @@ private final class GiftViewSheetContent: CombinedComponent { deinit { self.disposable?.dispose() self.sampleDisposable.dispose() + self.upgradeFormDisposable?.dispose() self.upgradeDisposable?.dispose() } func requestUpgradePreview() { - guard let _ = self.subject.arguments?.upgradeStars else { + guard let arguments = self.subject.arguments, arguments.canUpgrade || arguments.upgradeStars != nil else { return } self.context.starsContext?.load(force: false) @@ -251,7 +268,7 @@ private final class GiftViewSheetContent: CombinedComponent { } func commitUpgrade() { - guard let arguments = self.subject.arguments, let messageId = arguments.messageId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { + guard let arguments = self.subject.arguments, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { return } let peerId = arguments.peerId @@ -259,7 +276,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.inProgress = true self.updated() - let _ = (self.context.engine.payments.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: self.keepOriginalInfo) + self.upgradeDisposable = (self.upgradeGift(formId, self.keepOriginalInfo) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, let controller = self.getController() as? GiftViewScreen else { return @@ -307,7 +324,7 @@ private final class GiftViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, subject: self.subject, getController: self.getController) + return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, getController: self.getController) } static var body: Body { @@ -379,8 +396,6 @@ private final class GiftViewSheetContent: CombinedComponent { entities = arguments.entities limitTotal = gift.availability?.total convertStars = arguments.convertStars - incoming = arguments.incoming || arguments.peerId == component.context.account.peerId - savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = gift.id date = arguments.date @@ -395,6 +410,8 @@ private final class GiftViewSheetContent: CombinedComponent { convertStars = nil uniqueGift = gift } + savedToProfile = arguments.savedToProfile + incoming = arguments.incoming || arguments.peerId == component.context.account.peerId nameHidden = arguments.nameHidden titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title } else { @@ -748,8 +765,8 @@ private final class GiftViewSheetContent: CombinedComponent { // originY -= 12.0 // } - let linkColor = theme.actionSheet.controlAccentColor if !descriptionText.isEmpty { + let linkColor = theme.actionSheet.controlAccentColor if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } @@ -767,7 +784,6 @@ private final class GiftViewSheetContent: CombinedComponent { textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor } - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -850,45 +866,65 @@ private final class GiftViewSheetContent: CombinedComponent { if !soldOut { if let uniqueGift { if let peer = state.peerMap[uniqueGift.ownerPeerId] { - let ownerComponent = AnyComponent( - HStack([ - AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( + let ownerComponent: AnyComponent + if let _ = subject.arguments?.transferStars { + ownerComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( context: component.context, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - )) + text: strings.Gift_Unique_Transfer, + color: theme.list.itemAccentColor + )), + action: { + component.transferGift() + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + ownerComponent = AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) ), - AnyComponentWithIdentity( - id: AnyHashable(1), - component: AnyComponent(Button( - content: AnyComponent(ButtonContentComponent( - context: component.context, - text: strings.Gift_Unique_Transfer, - color: theme.list.itemAccentColor - )), - action: { - component.transferGift() - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - )) - ) - ], spacing: 4.0) - ) + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + } tableItems.append(.init( id: "owner", title: strings.Gift_Unique_Owner, @@ -1042,13 +1078,22 @@ private final class GiftViewSheetContent: CombinedComponent { let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "") let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - + if let _ = senderName { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + } else { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + } value = string } else { let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : strings.Gift_Unique_OriginalInfo(recipientName, dateString) let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + if let _ = senderName { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + } else { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + } value = string } @@ -1068,6 +1113,7 @@ private final class GiftViewSheetContent: CombinedComponent { animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, text: .plain(value), + horizontalAlignment: .center, maximumNumberOfLines: 0, handleSpoilers: true ) @@ -1281,64 +1327,65 @@ private final class GiftViewSheetContent: CombinedComponent { .disappear(.default(alpha: true)) ) originY += table.size.height + 23.0 - - if incoming && !converted { - if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { - state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) - } - let descriptionText: String - if savedToProfile { - descriptionText = strings.Gift_View_DisplayedInfoHide - } else if let upgradeStars, upgradeStars > 0 && !upgraded { - descriptionText = strings.Gift_View_HiddenInfoShow - } else { - descriptionText = strings.Gift_View_HiddenInfo - } - - let textFont = Font.regular(13.0) - let textColor = theme.list.itemSecondaryTextColor - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) - let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString - if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { - attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) - } - - originY -= 5.0 - let additionalText = additionalText.update( - component: MultilineTextComponent( - text: .plain(attributedString), - horizontalAlignment: .center, - maximumNumberOfLines: 5, - lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { _, _ in - component.updateSavedToProfile(!savedToProfile) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - context.add(additionalText - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - originY += additionalText.size.height - originY += 16.0 + } + + if incoming && !converted && !upgraded && !showUpgradePreview { + let linkColor = theme.actionSheet.controlAccentColor + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } + let descriptionText: String + if savedToProfile { + descriptionText = strings.Gift_View_DisplayedInfoHide + } else if let upgradeStars, upgradeStars > 0 && !upgraded { + descriptionText = strings.Gift_View_HiddenInfoShow + } else { + descriptionText = strings.Gift_View_HiddenInfo + } + + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + + originY -= 5.0 + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.updateSavedToProfile(!savedToProfile) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + originY += additionalText.size.height + originY += 16.0 } let buttonChild: _UpdatedChildComponent @@ -1480,6 +1527,7 @@ private final class GiftViewSheetComponent: CombinedComponent { let sendGift: (EnginePeer.Id) -> Void let openMyGifts: () -> Void let transferGift: () -> Void + let upgradeGift: ((Int64?, Bool) -> Signal) let showAttributeInfo: (Any, Float) -> Void init( @@ -1492,6 +1540,7 @@ private final class GiftViewSheetComponent: CombinedComponent { sendGift: @escaping (EnginePeer.Id) -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), showAttributeInfo: @escaping (Any, Float) -> Void ) { self.context = context @@ -1503,6 +1552,7 @@ private final class GiftViewSheetComponent: CombinedComponent { self.sendGift = sendGift self.openMyGifts = openMyGifts self.transferGift = transferGift + self.upgradeGift = upgradeGift self.showAttributeInfo = showAttributeInfo } @@ -1550,6 +1600,7 @@ private final class GiftViewSheetComponent: CombinedComponent { sendGift: context.component.sendGift, openMyGifts: context.component.openMyGifts, transferGift: context.component.transferGift, + upgradeGift: context.component.upgradeGift, showAttributeInfo: context.component.showAttributeInfo, getController: controller )), @@ -1629,20 +1680,20 @@ public class GiftViewScreen: ViewControllerComponentContainer { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _, _): 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, upgraded, canUpgrade, upgradeStars, nil, nil) - case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _): - return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, nil, nil, nil, false, savedToProfile, false, true, false, nil, transferStars, canExportDate) + case let .starGiftUnique(gift, isUpgrade, _, savedToProfile, canExportDate, transferStars, _): + var incoming = message.flags.contains(.Incoming) + if isUpgrade && message.author?.id != message.id.peerId { + incoming = true + } + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, false, false, false, nil, transferStars, canExportDate) default: return nil } } case let .profileGift(peerId, gift): - var upgraded = false - if case .unique = gift.gift { - upgraded = true - } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false, upgraded, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate) case .soldOutGift: return nil case .upgradePreview: @@ -1663,7 +1714,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { subject: GiftViewScreen.Subject, forceDark: Bool = false, updateSavedToProfile: ((Bool) -> Void)? = nil, - convertToStars: (() -> Void)? = nil + convertToStars: (() -> Void)? = nil, + transferGift: ((Bool, EnginePeer.Id) -> Void)? = nil, + upgradeGift: ((Int64?, Bool) -> Signal)? = nil ) { self.context = context self.subject = subject @@ -1676,6 +1729,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { var openMyGiftsImpl: (() -> Void)? var transferGiftImpl: (() -> Void)? var showAttributeInfoImpl: ((Any, Float) -> Void)? + var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? super.init( context: context, @@ -1703,6 +1757,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { transferGift: { transferGiftImpl?() }, + upgradeGift: { formId, keepOriginalInfo in + return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete() + }, showAttributeInfo: { tag, rarity in showAttributeInfoImpl?(tag, rarity) } @@ -1737,6 +1794,20 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self, let arguments = self.subject.arguments, let messageId = arguments.messageId else { return } + + var animationFile: TelegramMediaFile? + switch arguments.gift { + case let .generic(gift): + animationFile = gift.file + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, file, _) = attribute { + animationFile = file + break + } + } + } + if let updateSavedToProfile { updateSavedToProfile(added) } else { @@ -1749,10 +1820,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText if let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.5) { - if let lastController = navigationController.viewControllers.last as? ViewController, case let .generic(gift) = arguments.gift { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), + content: .sticker(context: context, file: animationFile, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { [weak navigationController] action in if case .undo = action, let navigationController { @@ -1792,7 +1863,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > starsConvertMaxDate { let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0)) let controller = textAlertController( @@ -1857,6 +1927,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.present(controller, in: .window(.root)) } } + openStarsIntroImpl = { [weak self] in guard let self else { return @@ -1864,6 +1935,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let introController = context.sharedContext.makeStarsIntroScreen(context: context) self.push(introController) } + sendGiftImpl = { [weak self] peerId in guard let self else { return @@ -1876,6 +1948,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.push(controller) }) } + openMyGiftsImpl = { [weak self] in guard let self, let navigationController = self.navigationController as? NavigationController else { return @@ -1906,11 +1979,42 @@ public class GiftViewScreen: ViewControllerComponentContainer { let _ = (context.account.stateManager.contactBirthdays |> take(1) |> deliverOnMainQueue).start(next: { birthdays in - let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, messageId, gift, transferStars, arguments.canExportDate), completion: nil) + let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, messageId, gift, transferStars, arguments.canExportDate), completion: { peerIds in + guard let peerId = peerIds.first else { + return + } + if let transferGift { + transferGift(transferStars == 0, peerId) + } else { + let _ = (context.engine.payments.transferStarGift(prepaid: transferStars == 0, messageId: messageId, peerId: peerId) + |> deliverOnMainQueue).start() + } + Queue.mainQueue().after(1.0, { + if transferStars > 0 { + context.starsContext?.load(force: true) + } + }) + }) navigationController.pushViewController(controller) }) } + upgradeGiftImpl = { [weak self] formId, keepOriginalInfo in + guard let self, let arguments = self.subject.arguments, let messageId = arguments.messageId else { + return .complete() + } + if let upgradeGift { + return upgradeGift(formId, keepOriginalInfo) + } else { + return self.context.engine.payments.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + |> afterCompleted { + if formId != nil { + context.starsContext?.load(force: true) + } + } + } + } + showAttributeInfoImpl = { [weak self] tag, rarity in guard let self else { return @@ -1950,6 +2054,17 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate func animateSuccess() { self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds)) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .universal( + animation: "GiftUpgraded", + scale: 0.066, + colors: [:], + title: presentationData.strings.Gift_Upgrade_Succeed_Title, + text: presentationData.strings.Gift_Upgrade_Succeed_Text, + customUndoText: nil, + timeout: 4.0 + ), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current) } public func dismissAnimated() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 2b85d1e5f8..6a191d60a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -330,7 +330,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS } let context = self.context - let undoController = UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: self.presentationData.strings.SavedMessages_SubChatDeleted, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + let undoController = UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: self.presentationData.strings.SavedMessages_SubChatDeleted), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in if value == .commit { let _ = context.engine.messages.clearHistoryInteractively(peerId: context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: { guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index c2870b3538..49c8b45f2e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -351,10 +351,21 @@ public final class PeerInfoCoverComponent: Component { self.backgroundView.backgroundColor = secondaryBackgroundColor - self.backgroundGradientLayer.type = .axial - self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) - self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) - self.backgroundGradientLayer.colors = [backgroundColor.cgColor, secondaryBackgroundColor.cgColor] + if case .custom = component.subject { + if availableSize.width < availableSize.height { + self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.25) + } else { + self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + } + self.backgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.backgroundGradientLayer.type = .radial + self.backgroundGradientLayer.colors = [secondaryBackgroundColor.cgColor, backgroundColor.cgColor] + } else { + self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) + self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) + self.backgroundGradientLayer.type = .axial + self.backgroundGradientLayer.colors = [backgroundColor.cgColor, secondaryBackgroundColor.cgColor] + } self.backgroundGradientLayer.anchorPoint = CGPoint(x: 0.0, y: 1.0) let gradientHeight: CGFloat = component.defaultHeight @@ -431,7 +442,7 @@ public final class PeerInfoCoverComponent: Component { } else if availableSize.width < 150.0 { baseDistance *= 0.6 baseRowDistance *= 0.6 - baseItemSize *= 0.75 + baseItemSize *= 0.83 } var avatarBackgroundPatternLayerCount = 0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index a07718b06f..248f757820 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -333,7 +333,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 1f5fdd62ab..62840ba730 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1659,7 +1659,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen ) } case let .group(groupId): - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) if peerId.namespace == Namespaces.Peer.CloudChannel { onlineMemberCount = context.account.viewTracker.peerView(groupId, updateData: false) |> map { view -> Bool? in @@ -1676,17 +1676,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } } @@ -1695,9 +1699,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen context.account.viewTracker.peerView(groupId, updateData: false), onlineMemberCount ) - |> map { peerView, onlineMemberCount -> PeerInfoStatusData? in - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - if let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + |> map { peerView, memberCountData -> PeerInfoStatusData? in + let (preciseTotalMemberCount, onlineMemberCount) = memberCountData + + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = preciseTotalMemberCount ?? cachedChannelData.participantsSummary.memberCount { + if let onlineMemberCount, onlineMemberCount > 1 { var string = "" string.append("\(strings.Conversation_StatusMembers(Int32(memberCount))), ") diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index 9d34dd106c..83e663631e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -168,7 +168,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) + let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) videoNode.isUserInteractionEnabled = false self.videoStartTimestamp = video.representation.startTimestamp self.videoContent = videoContent diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 2ec2c10548..aa3cc094e2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1306,7 +1306,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var nextIconX: CGFloat = titleSize.width var nextExpandedIconX: CGFloat = titleExpandedSize.width - if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { + if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize, credibilityIconSize.width > 0.0 { let offset = (credibilityIconSize.width + 4.0) / 2.0 let leftOffset: CGFloat = nextIconX + 4.0 @@ -1325,13 +1325,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { nextExpandedIconX += 4.0 + titleExpandedCredibilityIconSize.width } - if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize { - let offset = (verifiedIconSize.width + 4.0) / 2.0 - titleHorizontalOffset += offset - titleExpandedHorizontalOffset += offset - - let leftOffset: CGFloat = -verifiedIconSize.width - 4.0 - let leftExpandedOffset: CGFloat = -titleExpandedVerifiedIconSize.width - 4.0 + if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize, verifiedIconSize.width > 0.0 { + let leftOffset: CGFloat + let leftExpandedOffset: CGFloat + if case .verified = verifiedIcon { + titleHorizontalOffset -= (verifiedIconSize.width + 4.0) / 2.0 + + leftOffset = nextIconX + 4.0 + leftExpandedOffset = nextExpandedIconX + 4.0 + } else { + titleHorizontalOffset += (verifiedIconSize.width + 4.0) / 2.0 + titleExpandedHorizontalOffset += titleExpandedVerifiedIconSize.width + + leftOffset = -verifiedIconSize.width - 4.0 + leftExpandedOffset = -titleExpandedVerifiedIconSize.width - 8.0 + } var collapsedTransitionOffset: CGFloat = 0.0 if let navigationTransition = self.navigationTransition { @@ -1340,6 +1348,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrame(view: self.titleVerifiedIconView, frame: CGRect(origin: CGPoint(x: leftOffset + collapsedTransitionOffset, y: floor((titleSize.height - verifiedIconSize.height) / 2.0)), size: verifiedIconSize)) transition.updateFrame(view: self.titleExpandedVerifiedIconView, frame: CGRect(origin: CGPoint(x: leftExpandedOffset, y: floor((titleExpandedSize.height - titleExpandedVerifiedIconSize.height) / 2.0) + 1.0), size: titleExpandedVerifiedIconSize)) + + if case .verified = verifiedIcon { + nextIconX += 4.0 + verifiedIconSize.width + nextExpandedIconX += 4.0 + titleExpandedVerifiedIconSize.width + } } var titleFrame: CGRect @@ -1759,7 +1772,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { var titleFrame = titleFrame if !self.isAvatarExpanded { - titleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? titleExpandedHorizontalOffset : titleHorizontalOffset * titleScale, dy: 0.0) + titleFrame = titleFrame.offsetBy(dx: titleHorizontalOffset * titleScale, dy: 0.0) + } else { + titleFrame = titleFrame.offsetBy(dx: titleExpandedHorizontalOffset, dy: 0.0) } let titleCenter = CGPoint(x: transitionFraction * transitionSourceTitleFrame.midX + (1.0 - transitionFraction) * titleFrame.midX, y: transitionFraction * transitionSourceTitleFrame.midY + (1.0 - transitionFraction) * titleFrame.midY) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b2df007b98..51d7b0bbf1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3986,8 +3986,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.controller?.updateProfilePhoto(image, mode: .generic) } galleryController.avatarVideoEditCompletion = { [weak self] image, asset, adjustments in - let _ = self - //self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic) + self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic) } galleryController.removedEntry = { [weak self] entry in if let item = PeerInfoAvatarListItem(entry: entry) { @@ -9605,7 +9604,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - fileprivate func oldOpenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { + func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { guard let peer = self.data?.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else { return } @@ -9694,21 +9693,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings || strongSelf.isMyProfile, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))! mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context) let _ = strongSelf.currentAvatarMixin.swap(mixin) -// mixin.requestSearchController = { [weak self, weak parentController] assetsController in -// guard let strongSelf = self else { -// return -// } -// let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: (strongSelf.isSettings || strongSelf.isMyProfile) ? nil : peer.compactDisplayTitle, completion: { [weak self] result in -// assetsController?.dismiss() -// self?.updateProfilePhoto(result, mode: mode) -// })) -// controller.navigationPresentation = .modal -// parentController?.push(controller) -// -// if fromGallery { -// completion(nil) -// } -// } var isFromEditor = false mixin.requestAvatarEditor = { [weak self, weak parentController] imageCompletion, videoCompletion in guard let strongSelf = self, let imageCompletion, let videoCompletion else { @@ -9755,18 +9739,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } } -// mixin.didFinishWithImage = { [weak self] image in -// if let image = image { -// completion(image) -// self?.updateProfilePhoto(image, mode: mode) -// } -// } -// mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in -// if let image = image, let asset = asset { -// completion(image) -// self?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) -// } -// } + mixin.didFinishWithImage = { [weak self] image in + if let image = image { + completion(image) + self?.controller?.updateProfilePhoto(image, mode: mode) + } + } + mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in + if let image = image, let asset = asset { + completion(image) + self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) + } + } mixin.didFinishWithDelete = { guard let strongSelf = self else { return @@ -12766,6 +12750,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc proceed() } } + + func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { + self.controllerNode.openAvatarForEditing(mode: mode, fromGallery: fromGallery, completion: completion) + } static func openPeer(context: AccountContext, peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer, navigationController: NavigationController) { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 49826fe63e..3b2b7ad43e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -15,166 +15,167 @@ import OverlayStatusController import UndoUI import PeerAvatarGalleryUI import PresentationDataUtils +import LegacyComponents extension PeerInfoScreenImpl { - func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { - guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else { - return - } - self.view.endEditing(true) - - let peerId = self.peerId - var isForum = false - if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) { - isForum = true - } - - var currentIsVideo = false - var emojiMarkup: TelegramMediaImage.EmojiMarkup? - let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item - if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item { - currentIsVideo = !videoRepresentations.isEmpty - emojiMarkup = emojiMarkupValue - } - - let _ = isForum - let _ = currentIsVideo - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer else { - return - } - - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser)) - - var hasPhotos = false - if !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - var hasDeleteButton = false - if case .generic = mode { - hasDeleteButton = hasPhotos && !fromGallery - } else if case .custom = mode { - hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true - } else if case .fallback = mode { - if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto { - hasDeleteButton = photo != nil - } - } - - let _ = hasDeleteButton - - let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController - - var dismissImpl: (() -> Void)? - let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in - self?.openAvatarRemoval(mode: mode, peer: peer, item: item) - }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in - let subject: Signal - if let asset = result as? PHAsset { - subject = .single(.asset(asset)) - } else if let image = result as? UIImage { - subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) - } else if let result = result as? Signal { - subject = result - |> map { value -> MediaEditorScreenImpl.Subject? in - switch value { - case .pendingImage: - return nil - case let .image(image): - return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft) - case let .video(video): - return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft) - default: - return nil - } - } - } else { - let peerType: AvatarEditorScreen.PeerType - if mode == .suggest { - peerType = .suggest - } else if case .legacyGroup = peer { - peerType = .group - } else if case let .channel(channel) = peer { - if case .group = channel.info { - peerType = channel.flags.contains(.isForum) ? .forum : .group - } else { - peerType = .channel - } - } else { - peerType = .user - } - let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) - //controller.imageCompletion = imageCompletion - //controller.videoCompletion = videoCompletion - parentController?.push(controller) - //isFromEditor = true - return - } - - let editorController = MediaEditorScreenImpl( - context: self.context, - mode: .avatarEditor, - subject: subject, - transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( - MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( - sourceView: $0, - sourceRect: transitionRect, - sourceImage: transitionImage - ) - ) }), - transitionOut: { finished, isNew in - if !finished, let transitionView { - return MediaEditorScreenImpl.TransitionOut( - destinationView: transitionView, - destinationRect: transitionView.bounds, - destinationCornerRadius: 0.0 - ) - } - return nil - }, completion: { [weak self] result, commit in - dismissImpl?() - - switch result.media { - case let .image(image, _): - self?.updateProfilePhoto(image, mode: mode) - commit({}) - case let .video(video, coverImage, values, _, _): - if let coverImage { - self?.updateProfileVideo(coverImage, asset: video, adjustments: values, mode: mode) - } - commit({}) - default: - break - } - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void - ) - editorController.cancelled = { _ in - cancelled() - } - self.push(editorController) - }, dismissed: { - - }) - dismissImpl = { [weak mainController] in - if let mainController, let navigationController = mainController.navigationController { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { c in - return !(c is CameraScreen) && c !== mainController - } - navigationController.setViewControllers(viewControllers, animated: false) - } - } - mainController.navigationPresentation = .flatModal - mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.push(mainController) - }) - } +// func newopenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { +// guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else { +// return +// } +// self.view.endEditing(true) +// +// let peerId = self.peerId +// var isForum = false +// if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) { +// isForum = true +// } +// +// var currentIsVideo = false +// var emojiMarkup: TelegramMediaImage.EmojiMarkup? +// let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item +// if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item { +// currentIsVideo = !videoRepresentations.isEmpty +// emojiMarkup = emojiMarkupValue +// } +// +// let _ = isForum +// let _ = currentIsVideo +// +// let _ = (self.context.engine.data.get( +// TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) +// ) +// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in +// guard let self, let peer else { +// return +// } +// +// let keyboardInputData = Promise() +// keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser)) +// +// var hasPhotos = false +// if !peer.profileImageRepresentations.isEmpty { +// hasPhotos = true +// } +// +// var hasDeleteButton = false +// if case .generic = mode { +// hasDeleteButton = hasPhotos && !fromGallery +// } else if case .custom = mode { +// hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true +// } else if case .fallback = mode { +// if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto { +// hasDeleteButton = photo != nil +// } +// } +// +// let _ = hasDeleteButton +// +// let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController +// +// var dismissImpl: (() -> Void)? +// let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in +// self?.openAvatarRemoval(mode: mode, peer: peer, item: item) +// }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in +// let subject: Signal +// if let asset = result as? PHAsset { +// subject = .single(.asset(asset)) +// } else if let image = result as? UIImage { +// subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) +// } else if let result = result as? Signal { +// subject = result +// |> map { value -> MediaEditorScreenImpl.Subject? in +// switch value { +// case .pendingImage: +// return nil +// case let .image(image): +// return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft) +// case let .video(video): +// return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft) +// default: +// return nil +// } +// } +// } else { +// let peerType: AvatarEditorScreen.PeerType +// if mode == .suggest { +// peerType = .suggest +// } else if case .legacyGroup = peer { +// peerType = .group +// } else if case let .channel(channel) = peer { +// if case .group = channel.info { +// peerType = channel.flags.contains(.isForum) ? .forum : .group +// } else { +// peerType = .channel +// } +// } else { +// peerType = .user +// } +// let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) +// //controller.imageCompletion = imageCompletion +// //controller.videoCompletion = videoCompletion +// parentController?.push(controller) +// //isFromEditor = true +// return +// } +// +// let editorController = MediaEditorScreenImpl( +// context: self.context, +// mode: .avatarEditor, +// subject: subject, +// transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( +// MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( +// sourceView: $0, +// sourceRect: transitionRect, +// sourceImage: transitionImage +// ) +// ) }), +// transitionOut: { finished, isNew in +// if !finished, let transitionView { +// return MediaEditorScreenImpl.TransitionOut( +// destinationView: transitionView, +// destinationRect: transitionView.bounds, +// destinationCornerRadius: 0.0 +// ) +// } +// return nil +// }, completion: { [weak self] result, commit in +// dismissImpl?() +// +// switch result.media { +// case let .image(image, _): +// self?.updateProfilePhoto(image, mode: mode) +// commit({}) +// case let .video(video, coverImage, values, _, _): +// if let coverImage { +// self?.updateProfileVideo(coverImage, asset: video, adjustments: values, mode: mode) +// } +// commit({}) +// default: +// break +// } +// } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void +// ) +// editorController.cancelled = { _ in +// cancelled() +// } +// self.push(editorController) +// }, dismissed: { +// +// }) +// dismissImpl = { [weak mainController] in +// if let mainController, let navigationController = mainController.navigationController { +// var viewControllers = navigationController.viewControllers +// viewControllers = viewControllers.filter { c in +// return !(c is CameraScreen) && c !== mainController +// } +// navigationController.setViewControllers(viewControllers, animated: false) +// } +// } +// mainController.navigationPresentation = .flatModal +// mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) +// self.push(mainController) +// }) +// } func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) { let proceed = { [weak self] in @@ -368,7 +369,7 @@ extension PeerInfoScreenImpl { })) } - public func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: MediaEditorValues?, mode: PeerInfoAvatarEditingMode) { + public func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: PeerInfoAvatarEditingMode) { guard let data = image.jpegData(compressionQuality: 0.6) else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 5200e0b359..6e5d83c23e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -133,7 +133,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let itemsInRow = min(starsProducts.count, 3) let optionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) - let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + let starsOptionSize = CGSize(width: optionWidth, height: optionWidth) let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0) @@ -212,7 +212,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr peer: peer, subject: subject, ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: ribbonColor) }, - isHidden: !product.savedToProfile + isHidden: !product.savedToProfile, + mode: .profile ) ), effectAlignment: .center, @@ -234,6 +235,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } self.profileGifts.convertStarGift(messageId: messageId) + }, + transferGift: { [weak self] prepaid, peerId in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.transferStarGift(prepaid: prepaid, messageId: messageId, peerId: peerId) + }, + upgradeGift: { [weak self] formId, keepOriginalInfo in + guard let self, let messageId = product.messageId else { + return .never() + } + return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) } ) self.parentController?.push(controller) diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index 96e46e2053..7b29fcea7a 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -172,7 +172,7 @@ private enum OldChannelsEntry: ItemListNodeEntry { case let .peersHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in arguments.togglePeer(peer.peer.id, true) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift index 1575419d2e..bfb63c09c7 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift @@ -154,7 +154,7 @@ private enum OldChannelsSearchEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem { switch self { case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in interaction.togglePeer(peer.peer.id) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift index 26eedf0e39..2c332f4e8d 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift @@ -171,7 +171,7 @@ final class PeerSelectionLoadingView: UIView { context: context, peerMode: .peer, peer: .peer(peer: peer1, chatPeer: peer1), - status: .custom(string: "status", multiline: false, isActive: false, icon: nil), + status: .custom(string: NSAttributedString(string: "status"), multiline: false, isActive: false, icon: nil), badge: nil, requiresPremiumForMessaging: false, enabled: true, @@ -242,7 +242,7 @@ final class PeerSelectionLoadingView: UIView { let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 100.0) - let textFrame = itemNodes[sampleIndex].statusNode.frame.offsetBy(dx: 0.0, dy: currentY) + let textFrame = itemNodes[sampleIndex].statusNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 40.0) context.setBlendMode(.normal) diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index c095335e97..b99ea5e371 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -119,7 +119,7 @@ final class PeerSelectionScreenComponent: Component { context: listNode.context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), - status: .custom(string: statusText, multiline: false, isActive: false, icon: nil), + status: .custom(string: NSAttributedString(string: statusText), multiline: false, isActive: false, icon: nil), badge: nil, requiresPremiumForMessaging: false, enabled: true, diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 976a5cde0a..ee90ffd29f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -18,15 +18,26 @@ public final class StarsAvatarComponent: Component { let peer: StarsContext.State.Transaction.Peer? let photo: TelegramMediaWebFile? let media: [Media] + let uniqueGift: StarGift.UniqueGift? let backgroundColor: UIColor let size: CGSize? - public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer?, photo: TelegramMediaWebFile?, media: [Media], backgroundColor: UIColor, size: CGSize? = nil) { + public init( + context: AccountContext, + theme: PresentationTheme, + peer: StarsContext.State.Transaction.Peer?, + photo: TelegramMediaWebFile?, + media: [Media], + uniqueGift: StarGift.UniqueGift?, + backgroundColor: UIColor, + size: CGSize? = nil + ) { self.context = context self.theme = theme self.peer = peer self.photo = photo self.media = media + self.uniqueGift = uniqueGift self.backgroundColor = backgroundColor self.size = size } @@ -47,6 +58,9 @@ public final class StarsAvatarComponent: Component { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.uniqueGift != rhs.uniqueGift { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index ff860f76e4..6eb28d7f2c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -39,6 +39,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let openAppExamples: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void + let sendGift: (EnginePeer.Id) -> Void init( context: AccountContext, @@ -49,7 +50,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, - updateSubscription: @escaping () -> Void + updateSubscription: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void ) { self.context = context self.subject = subject @@ -60,6 +62,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription + self.sendGift = sendGift } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -80,6 +83,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var peerMap: [EnginePeer.Id: EnginePeer] = [:] var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedOverlayCloseImage: UIImage? var cachedChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -149,7 +153,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let title = Child(MultilineTextComponent.self) let star = Child(StarsImageComponent.self) let activeStar = Child(PremiumStarComponent.self) - let gift = Child(GiftAnimationComponent.self) + let gift = Child(GiftCompositionComponent.self) let amountBackground = Child(RoundedRectangle.self) let amount = Child(BalancedTextComponent.self) let amountStar = Child(BundleIconComponent.self) @@ -188,17 +192,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { state.cachedCloseImage = (closeImage, theme) } - let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: closeImage)), - action: { [weak component] in - component?.cancel(true) - } - ), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate - ) - + let closeOverlayImage: UIImage + if let image = state.cachedOverlayCloseImage { + closeOverlayImage = image + } else { + closeOverlayImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: .white)! + state.cachedOverlayCloseImage = closeOverlayImage + } + let titleText: String let amountText: String var descriptionText: String @@ -219,7 +220,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { var via: String? var messageId: EngineMessage.Id? var toPeer: EnginePeer? -// var toString: String? var transactionPeer: StarsContext.State.Transaction.Peer? var media: [AnyMediaReference] = [] var photo: TelegramMediaWebFile? @@ -234,7 +234,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isReaction = false var giveawayMessageId: MessageId? var isBoost = false - var giftAnimation: TelegramMediaFile? + var giftAnimationSubject: GiftCompositionComponent.Subject? + var isGiftUpgrade = false + var giftAvailability: StarGift.Gift.Availability? var isRefProgram = false var delayedCloseOnOpenPeer = true @@ -250,7 +252,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { count = StarsAmount(value: stars, nanos: 0) date = boost.date toPeer = state.peerMap[peerId] -// toString = strings.Stars_Transaction_Giveaway_Boost_Subscribers(boost.quantity) giveawayMessageId = boost.giveawayMessageId isBoost = true case let .importer(peer, pricing, importer, usdRate): @@ -368,9 +369,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = peer } transactionPeer = transaction.peer - if case let .generic(gift) = starGift { - giftAnimation = gift.file + + switch starGift { + case let .generic(gift): + giftAnimationSubject = .generic(gift.file) + giftAvailability = gift.availability + case let .unique(gift): + giftAnimationSubject = .unique(gift) } + isGiftUpgrade = transaction.flags.contains(.isStarGiftUpgrade) } else if let giveawayMessageIdValue = transaction.giveawayMessageId { titleText = strings.Stars_Transaction_Giveaway_Title descriptionText = "" @@ -583,6 +590,28 @@ private final class StarsTransactionSheetContent: CombinedComponent { descriptionText = modifiedString } + var closeButtonImage = closeImage + if case .unique = giftAnimationSubject { + closeButtonImage = closeOverlayImage + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeButtonImage)), + action: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let headerTextColor: UIColor + if case .unique = giftAnimationSubject { + headerTextColor = .white + } else { + headerTextColor = theme.actionSheet.primaryTextColor + } + let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) let formattedAmount = presentationStringsFormattedNumber(absCount, dateTimeFormat.groupingSeparator) let countColor: UIColor @@ -601,18 +630,22 @@ private final class StarsTransactionSheetContent: CombinedComponent { countColor = theme.list.itemPrimaryTextColor } else if count < StarsAmount.zero { amountText = "- \(formattedAmount)" - countColor = theme.list.itemDestructiveColor + if case .unique = giftAnimationSubject { + countColor = .white + } else { + countColor = theme.list.itemDestructiveColor + } } else { amountText = "+ \(formattedAmount)" countColor = theme.list.itemDisclosureActions.constructive.fillColor } - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleText, font: Font.bold(25.0), - textColor: theme.actionSheet.primaryTextColor, + textColor: headerTextColor, paragraphAlignment: .center )), horizontalAlignment: .center, @@ -647,17 +680,25 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageIcon = nil } + var starOriginY: CGFloat = 81.0 var starChild: _UpdatedChildComponent - if let giftAnimation { + if let giftAnimationSubject { + let animationHeight: CGFloat + if case .unique = giftAnimationSubject { + animationHeight = 240.0 + } else { + animationHeight = 210.0 + } starChild = gift.update( - component: GiftAnimationComponent( + component: GiftCompositionComponent( context: component.context, theme: theme, - file: giftAnimation + subject: giftAnimationSubject ), - availableSize: CGSize(width: 128.0, height: 128.0), + availableSize: CGSize(width: context.availableSize.width, height: animationHeight), transition: .immediate ) + starOriginY = animationHeight / 2.0 } else if isBoost { starChild = activeStar.update( component: PremiumStarComponent( @@ -721,6 +762,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] + if isGiftUpgrade { + tableItems.append(.init( + id: "reason", + title: strings.Stars_Transaction_Giveaway_Reason, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_GiftUpgrade, font: tableFont, textColor: tableTextColor))) + ) + )) + } + if isGift, toPeer == nil { tableItems.append(.init( id: "from", @@ -746,7 +797,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } else if let toPeer, !isRefProgram { let title: String - if isSubscription { + if isGiftUpgrade { + title = strings.Stars_Transaction_GiftFrom + } else if isSubscription { if isBotSubscription { title = strings.Stars_Transaction_Subscription_Bot } else if isBusinessSubscription { @@ -759,10 +812,56 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { title = count < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From } - tableItems.append(.init( - id: "to", - title: title, - component: AnyComponent( + + let toComponent: AnyComponent + if let _ = giftAnimationSubject, !toPeer.isDeleted && !isGiftUpgrade { + toComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: toPeer + ) + ), + action: { + if delayedCloseOnOpenPeer { + component.openPeer(toPeer, false) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } else { + if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { + chatController.playShakeAnimation() + } + component.cancel(true) + } + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Send, + color: theme.list.itemAccentColor + )), + action: { + component.sendGift(toPeer.id) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + toComponent = AnyComponent( Button( content: AnyComponent( PeerCellComponent( @@ -786,6 +885,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } ) ) + } + tableItems.append(.init( + id: "to", + title: title, + component: toComponent )) if case let .subscription(subscription) = component.subject, let title = subscription.title { tableItems.append(.init( @@ -1001,6 +1105,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { Button( content: AnyComponent( TransactionCellComponent( + backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, textColor: tableTextColor, accentColor: tableLinkColor, transactionId: transactionId @@ -1048,6 +1153,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) + if let giftAvailability { + let remainsString = presentationStringsFormattedNumber(giftAvailability.remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(giftAvailability.total, environment.dateTimeFormat.groupingSeparator) + tableItems.append(.init( + id: "availability", + title: strings.Gift_View_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) + ) + )) + } if isSubscriber, let additionalDate { tableItems.append(.init( @@ -1103,15 +1219,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) context.add(starChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: 200.0 / 2.0 - 19.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: starOriginY)) ) + var originY: CGFloat = 156.0 + if let _ = giftAnimationSubject { + originY += 18.0 + } context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) ) - - var originY: CGFloat = 0.0 - originY += 200.0 - 23.0 + originY += 21.0 var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { @@ -1237,6 +1355,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { context.add(amountStar .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel + amountStarOffsetY)) ) + + if case .unique = giftAnimationSubject { + originY += 21.0 + } context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) @@ -1353,6 +1475,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let openAppExamples: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void + let sendGift: (EnginePeer.Id) -> Void init( context: AccountContext, @@ -1362,7 +1485,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, - updateSubscription: @escaping () -> Void + updateSubscription: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void ) { self.context = context self.subject = subject @@ -1372,6 +1496,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription + self.sendGift = sendGift } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -1416,7 +1541,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMedia: context.component.openMedia, openAppExamples: context.component.openAppExamples, copyTransactionId: context.component.copyTransactionId, - updateSubscription: context.component.updateSubscription + updateSubscription: context.component.updateSubscription, + sendGift: context.component.sendGift )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -1516,6 +1642,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? var updateSubscriptionImpl: (() -> Void)? + var sendGiftImpl: ((EnginePeer.Id) -> Void)? super.init( context: context, @@ -1539,6 +1666,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }, updateSubscription: { updateSubscriptionImpl?() + }, + sendGift: { peerId in + sendGiftImpl?(peerId) } ), navigationBarAppearance: .none, @@ -1690,6 +1820,19 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { } } } + + sendGiftImpl = { [weak self] peerId in + guard let self else { + return + } + let _ = (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 = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false) + self.push(controller) + }) + } } required public init(coder aDecoder: NSCoder) { @@ -2011,7 +2154,7 @@ private final class PeerCellComponent: Component { let avatarNaturalSize = self.avatar.update( transition: .immediate, component: AnyComponent( - StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) + StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: .clear) ), environment: {}, containerSize: CGSize(width: 40.0, height: 40.0) @@ -2063,18 +2206,23 @@ private final class PeerCellComponent: Component { } private final class TransactionCellComponent: Component { + let backgroundColor: UIColor let textColor: UIColor let accentColor: UIColor let transactionId: String - init(textColor: UIColor, accentColor: UIColor, transactionId: String) { + init(backgroundColor: UIColor, textColor: UIColor, accentColor: UIColor, transactionId: String) { + self.backgroundColor = backgroundColor self.textColor = textColor self.accentColor = accentColor self.transactionId = transactionId } static func ==(lhs: TransactionCellComponent, rhs: TransactionCellComponent) -> Bool { - if lhs.textColor !== rhs.textColor { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.textColor != rhs.textColor { return false } if lhs.accentColor != rhs.accentColor { @@ -2089,12 +2237,17 @@ private final class TransactionCellComponent: Component { final class View: UIView { private let text = ComponentView() private let button = ComponentView() + private let gradientView = UIImageView() private var component: TransactionCellComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) + + self.layer.allowsGroupOpacity = true + + self.gradientView.image = generateGradientImage(size: CGSize(width: 40.0, height: 1.0), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.65, 1.0], direction: .horizontal)?.withRenderingMode(.alwaysTemplate) } required init?(coder: NSCoder) { @@ -2104,55 +2257,49 @@ private final class TransactionCellComponent: Component { func update(component: TransactionCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state - - let spacing: CGFloat = 6.0 + + self.gradientView.tintColor = component.backgroundColor let buttonSize = self.button.update( transition: .immediate, component: AnyComponent( - BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.accentColor) + BundleIconComponent( + name: "Chat/Context Menu/Copy", + tintColor: component.accentColor + ) ), environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - - func brokenLine(_ string: String) -> String { - if string.count > 30 { - return string - } - let middleIndex = string.index(string.startIndex, offsetBy: string.count / 2) - var newString = string - newString.insert("\n", at: middleIndex) - return newString - } - - let text: String - if availableSize.width > 230.0 { - text = component.transactionId - } else { - text = brokenLine(component.transactionId) - } - + let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString( - string: text, + string: component.transactionId, font: Font.monospace(15.0), textColor: component.textColor, paragraphAlignment: .left )), - maximumNumberOfLines: 0, - lineSpacing: 0.2 + maximumNumberOfLines: 1 ) ), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSize.width - spacing, height: availableSize.height) + containerSize: CGSize(width: availableSize.width - buttonSize.width + 10.0, height: availableSize.height) ) let size = CGSize(width: availableSize.width, height: textSize.height) + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + 1.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + self.addSubview(self.gradientView) + } + transition.setFrame(view: textView, frame: textFrame) + } + let buttonFrame = CGRect(origin: CGPoint(x: availableSize.width - buttonSize.width - 2.0, y: floorToScreenPixels((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { @@ -2161,13 +2308,7 @@ private final class TransactionCellComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } - let textFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + 1.0), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.addSubview(textView) - } - transition.setFrame(view: textView, frame: textFrame) - } + self.gradientView.frame = CGRect(x: size.width - buttonSize.width - 32.0, y: 0.0, width: 40.0, height: size.height) return size } @@ -2202,3 +2343,92 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + + public init( + context: AccountContext, + text: String, + color: UIColor + ) { + self.context = context + self.text = text + self.color = color + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(attributedText)) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 6.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor = component.color.withAlphaComponent(0.1) + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + 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, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index e1d1eb0f04..393e97bd63 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponentResourceContent", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index b55d747d0a..f8f61409c3 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -16,7 +16,7 @@ import AvatarNode import BundleIconComponent import PhotoResources import StarsAvatarComponent -import LottieComponent +import GiftAnimationComponent private extension StarsContext.State.Transaction { var extendedId: String { @@ -300,13 +300,20 @@ final class StarsTransactionsListPanelComponent: Component { var itemDate: String var itemPeer = item.peer var itemFile: TelegramMediaFile? + var uniqueGift: StarGift.UniqueGift? switch item.peer { case let .peer(peer): if let starGift = item.starGift { - itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift - if case let .generic(gift) = starGift { - itemFile = gift.file + if item.flags.contains(.isStarGiftUpgrade), case let .unique(gift) = starGift { + itemTitle = "\(gift.title) #\(gift.number)" + itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade + uniqueGift = gift + } else { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + if case let .generic(gift) = starGift { + itemFile = gift.file + } } } else if let _ = item.giveawayMessageId { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) @@ -394,7 +401,7 @@ final class StarsTransactionsListPanelComponent: Component { itemDate += " – \(environment.strings.Monetization_Transaction_Failed)" itemDateColor = environment.theme.list.itemDestructiveColor } - + var titleComponents: [AnyComponentWithIdentity] = [] titleComponents.append( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( @@ -411,24 +418,23 @@ final class StarsTransactionsListPanelComponent: Component { if let itemFile { subtitleComponent = AnyComponent( HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(LottieComponent( - content: LottieComponent.ResourceContent( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + GiftAnimationComponent( context: component.context, + theme: environment.theme, file: itemFile, - attemptSynchronously: false, - providesPlaceholder: true - ), - color: nil, - placeholderColor: environment.theme.list.mediaPlaceholderColor, - size: CGSize(width: 20.0, height: 20.0), - loop: false - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: itemSubtitle, - font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), - textColor: environment.theme.list.itemPrimaryTextColor - )) + still: true, + size: CGSize(width: 20.0, height: 20.0) + ) + )), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemSubtitle, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + ) + ) ))) ], spacing: 2.0) ) @@ -463,7 +469,7 @@ final class StarsTransactionsListPanelComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, uniqueGift: uniqueGift, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 424777c77e..c3110b3a10 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -765,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component { if let photo = subscription.photo { nameGroupComponent = AnyComponent( HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: nil, photo: photo, media: [], backgroundColor: .clear, size: CGSize(width: 19.0, height: 19.0)))), + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: nil, photo: photo, media: [], uniqueGift: nil, backgroundColor: .clear, size: CGSize(width: 19.0, height: 19.0)))), AnyComponentWithIdentity(id: AnyHashable(1), component: nameComponent) ], spacing: 6.0) ) @@ -806,7 +806,7 @@ final class StarsTransactionsScreenComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], uniqueGift: nil, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: labelComponent, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index c0a5f2c7c6..f7adeea7cd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -200,7 +200,7 @@ final class StoryItemContentComponent: Component { if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { if self.videoNode == nil { let videoNode = UniversalVideoNode( - accountId: component.context.account.id, + context: component.context, postbox: component.context.account.postbox, audioSession: component.context.sharedContext.mediaManager.audioSession, manager: component.context.sharedContext.mediaManager.universalVideoManager, diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 62c09121a5..e805320c44 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -1320,7 +1320,9 @@ public final class TextFieldComponent: Component { return UIView() } let pointSize = floor(24.0 * 1.3) - return EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) + let emojiView = EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) + emojiView.updateTextColor(component.textColor) + return emojiView } self.chatInputTextNodeDidUpdateText() diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index dc3db47119..054b0fc279 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -20,11 +20,13 @@ private final class InlineStickerItem: Hashable { let emoji: ChatTextInputTextCustomEmojiAttribute let file: TelegramMediaFile? let fontSize: CGFloat + let enableAnimation: Bool - init(emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, fontSize: CGFloat) { + init(emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, fontSize: CGFloat, enableAnimation: Bool) { self.emoji = emoji self.file = file self.fontSize = fontSize + self.enableAnimation = enableAnimation } func hash(into hasher: inout Hasher) { @@ -42,6 +44,9 @@ private final class InlineStickerItem: Hashable { if lhs.fontSize != rhs.fontSize { return false } + if lhs.enableAnimation != rhs.enableAnimation { + return false + } return true } } @@ -65,19 +70,25 @@ public final class TextNodeWithEntities { public let renderer: MultiAnimationRenderer public let placeholderColor: UIColor public let attemptSynchronous: Bool + public let emojiOffset: CGPoint + public let fontSizeNorm: CGFloat public init( context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, - attemptSynchronous: Bool + attemptSynchronous: Bool, + emojiOffset: CGPoint = CGPoint(), + fontSizeNorm: CGFloat = 17.0 ) { self.context = context self.cache = cache self.renderer = renderer self.placeholderColor = placeholderColor self.attemptSynchronous = attemptSynchronous + self.emojiOffset = emojiOffset + self.fontSizeNorm = fontSizeNorm } public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments { @@ -86,7 +97,8 @@ public final class TextNodeWithEntities { cache: self.cache, renderer: self.renderer, placeholderColor: color, - attemptSynchronous: self.attemptSynchronous + attemptSynchronous: self.attemptSynchronous, + emojiOffset: self.emojiOffset ) } } @@ -96,6 +108,8 @@ public final class TextNodeWithEntities { private var enableLooping: Bool = true + public var resetEmojiToFirstFrameAutomatically: Bool = false + public var visibilityRect: CGRect? { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibilityRect { @@ -110,7 +124,13 @@ public final class TextNodeWithEntities { } else { isItemVisible = false } - itemLayer.isVisibleForAnimations = self.enableLooping && isItemVisible + let isVisibleForAnimations = self.enableLooping && isItemVisible && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } + } } } } @@ -141,7 +161,7 @@ public final class TextNodeWithEntities { let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) - updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize, enableAnimation: value.enableAnimation), range: replacementRange) updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) originalTextId += 1 @@ -197,7 +217,7 @@ public final class TextNodeWithEntities { if let maybeNode = maybeNode { if let applyArguments = applyArguments { - maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) + maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, emojiOffset: applyArguments.emojiOffset, fontSizeNorm: applyArguments.fontSizeNorm) } return maybeNode @@ -205,7 +225,7 @@ public final class TextNodeWithEntities { let resultNode = TextNodeWithEntities(textNode: result) if let applyArguments = applyArguments { - resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) + resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, emojiOffset: applyArguments.emojiOffset, fontSizeNorm: applyArguments.fontSizeNorm) } return resultNode @@ -222,7 +242,7 @@ public final class TextNodeWithEntities { } } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool, emojiOffset: CGPoint, fontSizeNorm: CGFloat) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] @@ -241,9 +261,9 @@ public final class TextNodeWithEntities { let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) - let itemSize = floorToScreenPixels(stickerItem.fontSize * 24.0 / 17.0) + let itemSize = floorToScreenPixels(stickerItem.fontSize * 24.0 / fontSizeNorm) - var itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 1.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + var itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left + emojiOffset.x, dy: textLayout.insets.top + 1.0 + emojiOffset.y).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) @@ -256,8 +276,13 @@ public final class TextNodeWithEntities { itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) + } + itemLayer.enableAnimation = stickerItem.enableAnimation + let isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } } itemLayer.frame = itemFrame @@ -301,12 +326,19 @@ public class ImmediateTextNodeWithEntities: TextNode { private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] public private(set) var dustNode: InvisibleInkDustNode? + public var resetEmojiToFirstFrameAutomatically: Bool = false + public var visibility: Bool = false { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility { for (_, itemLayer) in self.inlineStickerItemLayers { - let isItemVisible: Bool = self.visibility - itemLayer.isVisibleForAnimations = self.enableLooping && isItemVisible + let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } + } } } } @@ -375,7 +407,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) - updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize, enableAnimation: value.enableAnimation), range: replacementRange) updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) originalTextId += 1 @@ -437,7 +469,7 @@ public class ImmediateTextNodeWithEntities: TextNode { var enableAnimations = true if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) enableAnimations = arguments.context.sharedContext.energyUsageSettings.fullTranslucency } self.updateSpoilers(enableAnimations: enableAnimations, textLayout: layout) @@ -450,7 +482,7 @@ public class ImmediateTextNodeWithEntities: TextNode { return layout.size } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, fontSizeNorm: CGFloat) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] @@ -469,7 +501,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) - let itemSide = floor(stickerItem.fontSize * 24.0 / 17.0) + let itemSide = floor(stickerItem.fontSize * 24.0 / fontSizeNorm) var itemSize = CGSize(width: itemSide, height: itemSide) if let file = stickerItem.file, let customItemLayout = self.customItemLayout { itemSize = customItemLayout(itemSize, file) @@ -486,8 +518,15 @@ public class ImmediateTextNodeWithEntities: TextNode { itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = self.enableLooping && self.visibility + } + + itemLayer.enableAnimation = stickerItem.enableAnimation + let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + if itemLayer.isVisibleForAnimations != isVisibleForAnimations { + itemLayer.isVisibleForAnimations = isVisibleForAnimations + if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { + itemLayer.reloadAnimation() + } } itemLayer.frame = itemFrame @@ -535,7 +574,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let _ = apply() if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) } return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines) @@ -550,7 +589,7 @@ public class ImmediateTextNodeWithEntities: TextNode { let _ = apply() if let arguments = self.arguments { - self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor, fontSizeNorm: arguments.fontSizeNorm) } return layout diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4f13cdf748..d826577770 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1228,8 +1228,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } controller.videoCompletion = { [weak self] image, url, adjustments, commit in if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let _ = rootController.accountSettingsController as? PeerInfoScreenImpl { - //settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments) + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments, mode: .accept) commit() } } @@ -1263,8 +1263,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, videoCompletion: { [weak self] image, url, adjustments in if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let _ = rootController.accountSettingsController as? PeerInfoScreenImpl { - //settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments) + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments, mode: .accept) } } }) @@ -5221,11 +5221,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let peerId = chatLocationPeerId if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId { peerView.set(context.account.viewTracker.peerView(peerId)) - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) var hasScheduledMessages: Signal = .single(false) if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal = peerView.get() + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get() |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -5240,17 +5240,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6196,9 +6200,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - var onlineMemberCount: Signal = .single(nil) + var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal = peerView + let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView |> map { view -> Bool? in if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { if case .broadcast = peer.info { @@ -6213,17 +6217,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in + |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in if let isLarge = isLarge { if isLarge { return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (nil, value) + } } else { return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) + |> map { value -> (total: Int32?, recent: Int32?) in + return (value.total, value.recent) + } } } else { - return .single(nil) + return .single((nil, nil)) } } onlineMemberCount = recentOnlineSignal @@ -6325,7 +6333,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerPresences: [:], cachedData: nil ) - strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: nil, isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) + strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) strongSelf.peerView = peerView @@ -8555,7 +8563,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusText = self.presentationData.strings.Undo_ChatCleared } - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { [weak self] value in guard let strongSelf = self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index ef5d3f69b1..cfcd03c411 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -94,7 +94,7 @@ extension ChatControllerImpl { self.present( UndoOverlayController( presentationData: self.presentationData, - content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(title: title ?? text, text: title == nil ? nil : text), + content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(context: self.context, title: NSAttributedString(string: title ?? text), text: title == nil ? nil : text), elevatedLayout: false, action: { [weak self] action in guard let self else { @@ -357,7 +357,7 @@ extension ChatControllerImpl { self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: undoTitle, text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: undoTitle), text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in guard let self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index 8fa4517666..e58f51508a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -153,7 +153,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { value in guard let strongSelf = self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b65c8f6967..67b5531abc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -35,6 +35,7 @@ import ChatMessageTransitionNode import ChatControllerInteraction import DustEffect import UrlHandling +import TextFormat struct ChatTopVisibleMessageRange: Equatable { var lowerBound: MessageIndex @@ -2313,41 +2314,57 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.currentOverscrollExpandProgress = expandProgress if let nextChannelToRead = self.nextChannelToRead { - let swipeText: (String, [(Int, NSRange)]) - let releaseText: (String, [(Int, NSRange)]) + let swipeText: NSAttributedString + let releaseText: NSAttributedString switch nextChannelToRead.location { case .same: if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil { - swipeText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction) } else if nextChannelToRead.threadData != nil { - swipeText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction) } else { - swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction) } case .archived: - swipeText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction) case .unarchived: - swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, []) - releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, []) + swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress) + releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction) case let .folder(_, title): - //TODO:release - swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title.text)._tuple - releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title.text)._tuple + let swipeTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgressV2) + let swipeFolderRange = (swipeTextValue.string as NSString).range(of: "{folder}") + if swipeFolderRange.location != NSNotFound { + swipeTextValue.replaceCharacters(in: swipeFolderRange, with: "") + swipeTextValue.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: swipeFolderRange.location) + } + swipeText = swipeTextValue + + let releaseTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeActionV2) + let releaseTextFolderRange = (releaseTextValue.string as NSString).range(of: "{folder}") + if releaseTextFolderRange.location != NSNotFound { + releaseTextValue.replaceCharacters(in: releaseTextFolderRange, with: "") + releaseTextValue.insert(title.attributedString(attributes: [ + ChatTextInputAttributes.bold: true + ]), at: releaseTextFolderRange.location) + } + releaseText = releaseTextValue } if expandProgress < 0.1 { chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil) } else if expandProgress >= 1.0 { - if chatControllerNode.inputPanelOverscrollNode?.text.0 != releaseText.0 { - chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1)) + if chatControllerNode.inputPanelOverscrollNode?.text.string != releaseText.string { + chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1)) } } else { - if chatControllerNode.inputPanelOverscrollNode?.text.0 != swipeText.0 { - chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: swipeText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2)) + if chatControllerNode.inputPanelOverscrollNode?.text.string != swipeText.string { + chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: swipeText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2)) } } } else { diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index ec6001bee7..d8c6c1d87a 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -464,22 +464,22 @@ public final class MediaManagerImpl: NSObject, MediaManager { } } - public func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) { + public func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) { assert(Queue.mainQueue().isCurrent()) - let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> - if let (account, playlist) = playlist { + let inputData: Signal<(AccountContext, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> + if let (context, playlist) = playlist { inputData = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.musicPlaybackSettings]) |> take(1) - |> mapToSignal { sharedData -> Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> in + |> mapToSignal { sharedData -> Signal<(AccountContext, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.musicPlaybackSettings]?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings if let location = playlist.location as? PeerMessagesPlaylistLocation, let messageId = location.messageId { - return mediaPlaybackStoredState(engine: TelegramEngine(account: account), messageId: messageId) + return mediaPlaybackStoredState(engine: context.engine, messageId: messageId) |> map { storedState in - return (account, playlist, settings, storedState) + return (context, playlist, settings, storedState) } } else { - return .single((account, playlist, settings, nil)) + return .single((context, playlist, settings, nil)) } } } else { @@ -494,7 +494,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { switch type { case .voice: strongSelf.musicMediaPlayer?.control(.playback(.pause)) - if let (account, playlist, settings, storedState) = inputData { + if let (context, playlist, settings, storedState) = inputData { if areSharedMediaPlaylistsEqual(playlist, strongSelf.voiceMediaPlayer?.playlist), case .seek = control { strongSelf.voiceMediaPlayer?.control(control) } else { @@ -506,7 +506,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { controlPlaybackWithProximity = playlist.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) } - let voiceMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish) + let voiceMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: context.account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish) strongSelf.voiceMediaPlayer = voiceMediaPlayer voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer { @@ -535,12 +535,12 @@ public final class MediaManagerImpl: NSObject, MediaManager { } case .music, .file: strongSelf.voiceMediaPlayer?.control(.playback(.pause)) - if let (account, playlist, settings, storedState) = inputData { + if let (context, playlist, settings, storedState) = inputData { if areSharedMediaPlaylistsEqual(playlist, strongSelf.musicMediaPlayer?.playlist), case .seek = control { strongSelf.musicMediaPlayer?.control(control) } else { strongSelf.musicMediaPlayer?.stop() - let musicMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type, continueInstantVideoLoopAfterFinish: true) + let musicMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: context.account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type, continueInstantVideoLoopAfterFinish: true) strongSelf.musicMediaPlayer = musicMediaPlayer musicMediaPlayer.cancelled = { [weak musicMediaPlayer] in if let strongSelf = self, let musicMediaPlayer = musicMediaPlayer, musicMediaPlayer === strongSelf.musicMediaPlayer { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index daced6e649..86aa4c4867 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -308,7 +308,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } playerType = (file.isVoice || file.isInstantVideo) ? .voice : .file } - params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control) + params.context.sharedContext.mediaManager.setPlaylist((params.context, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control) return true case let .story(storyController): params.dismissInput() diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift index 6e801b2667..e4c763d9ca 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift @@ -40,14 +40,14 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode { var playbackEnded: (() -> Void)? - init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { + init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { self.close = close self.content = content var togglePlayPauseImpl: (() -> Void)? let decoration = OverlayInstantVideoDecoration(tapped: { togglePlayPauseImpl?() }) - self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) + self.videoNode = UniversalVideoNode(context: context, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) self.decoration = decoration super.init() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 457f6812d8..a18a74c999 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2472,12 +2472,31 @@ public final class SharedAccountContextImpl: SharedAccountContext { } presentTransferAlertImpl = { [weak controller] peer in - guard let controller, case let .starGiftTransfer(_, messageId, gift, transferStars, _) = source else { + guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _) = source else { return } let alertController = giftTransferAlertController(context: context, gift: gift, peer: peer, transferStars: transferStars, commit: { [weak controller] in - controller?.dismiss() - let _ = context.engine.payments.transferStarGift(prepaid: transferStars == 0, messageId: messageId, peerId: peer.id).start() + completion?([peer.id]) + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) }) controller.present(alertController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 6c6229f201..709dfaeab8 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -111,6 +111,7 @@ private enum SharedMediaPlaybackItem: Equatable { } final class SharedMediaPlayer { + private weak var context: AccountContext? private weak var mediaManager: MediaManager? let account: Account private let audioSession: ManagedAudioSession @@ -179,7 +180,8 @@ final class SharedMediaPlayer { let type: MediaManagerPlayerType - init(mediaManager: MediaManager, inForeground: Signal, account: Account, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, initialPlaybackRate: AudioPlaybackRate, playerIndex: Int32, controlPlaybackWithProximity: Bool, type: MediaManagerPlayerType, continueInstantVideoLoopAfterFinish: Bool) { + init(context: AccountContext, mediaManager: MediaManager, inForeground: Signal, account: Account, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, initialPlaybackRate: AudioPlaybackRate, playerIndex: Int32, controlPlaybackWithProximity: Bool, type: MediaManagerPlayerType, continueInstantVideoLoopAfterFinish: Bool) { + self.context = context self.mediaManager = mediaManager self.account = account self.audioSession = audioSession @@ -233,10 +235,10 @@ final class SharedMediaPlayer { strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice)) } case .instantVideo: - if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { + if let mediaManager = strongSelf.mediaManager, let context = strongSelf.context, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { case let .telegramFile(fileReference, _, _): - let videoNode = OverlayInstantVideoNode(accountId: strongSelf.account.id, postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in + let videoNode = OverlayInstantVideoNode(context: context, postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) strongSelf.playbackItem = .instantVideo(videoNode) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index db8fa85144..d009392ddb 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -63,6 +63,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableLocalTranslation: Bool public var autoBenchmarkReflectors: Bool? public var conferenceCalls: Bool + public var playerV2: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -103,7 +104,8 @@ public struct ExperimentalUISettings: Codable, Equatable { dynamicStreaming: false, enableLocalTranslation: false, autoBenchmarkReflectors: nil, - conferenceCalls: false + conferenceCalls: false, + playerV2: false ) } @@ -145,7 +147,8 @@ public struct ExperimentalUISettings: Codable, Equatable { dynamicStreaming: Bool, enableLocalTranslation: Bool, autoBenchmarkReflectors: Bool?, - conferenceCalls: Bool + conferenceCalls: Bool, + playerV2: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -185,6 +188,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableLocalTranslation = enableLocalTranslation self.autoBenchmarkReflectors = autoBenchmarkReflectors self.conferenceCalls = conferenceCalls + self.playerV2 = playerV2 } public init(from decoder: Decoder) throws { @@ -228,6 +232,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? false self.autoBenchmarkReflectors = try container.decodeIfPresent(Bool.self, forKey: "autoBenchmarkReflectors") self.conferenceCalls = try container.decodeIfPresent(Bool.self, forKey: "conferenceCalls") ?? false + self.playerV2 = try container.decodeIfPresent(Bool.self, forKey: "playerV2") ?? false } public func encode(to encoder: Encoder) throws { @@ -271,6 +276,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.enableLocalTranslation, forKey: "enableLocalTranslation") try container.encodeIfPresent(self.autoBenchmarkReflectors, forKey: "autoBenchmarkReflectors") try container.encodeIfPresent(self.conferenceCalls, forKey: "conferenceCalls") + try container.encodeIfPresent(self.playerV2, forKey: "playerV2") } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index 2feecdd919..15c2cc2b91 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -27,7 +27,7 @@ public struct HLSCodecConfiguration { public extension HLSCodecConfiguration { init(context: AccountContext) { - /*var isSoftwareAv1Supported = false + var isSoftwareAv1Supported = false var isHardwareAv1Supported = false var length: Int = 4 @@ -44,9 +44,7 @@ public extension HLSCodecConfiguration { isSoftwareAv1Supported = value != 0.0 } - self.init(isHardwareAv1Supported: isHardwareAv1Supported, isSoftwareAv1Supported: isSoftwareAv1Supported)*/ - - self.init(isHardwareAv1Supported: false, isSoftwareAv1Supported: false) + self.init(isHardwareAv1Supported: isHardwareAv1Supported, isSoftwareAv1Supported: isSoftwareAv1Supported) } } @@ -264,8 +262,8 @@ public final class HLSVideoContent: UniversalVideoContent { self.codecConfiguration = codecConfiguration } - public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return HLSVideoJSNativeContentNode(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, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, codecConfiguration: self.codecConfiguration) + public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return HLSVideoJSNativeContentNode(context: context, 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, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, codecConfiguration: self.codecConfiguration) } public func isEqual(to other: UniversalVideoContent) -> Bool { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift index 04f7b518e0..084cac039a 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -1022,14 +1022,14 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod private var didBecomeActiveObserver: NSObjectProtocol? private var willResignActiveObserver: NSObjectProtocol? - private let chunkPlayerPartsState = Promise(ChunkMediaPlayerPartsState(duration: nil, parts: [])) + private let chunkPlayerPartsState = Promise(ChunkMediaPlayerPartsState(duration: nil, content: .parts([]))) private var sourceBufferStateDisposable: Disposable? private var playerStatusDisposable: Disposable? private var contextDisposable: Disposable? - init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, codecConfiguration: HLSCodecConfiguration) { + init(context: AccountContext, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, codecConfiguration: HLSCodecConfiguration) { self.instanceId = HLSVideoJSNativeContentNode.nextInstanceId HLSVideoJSNativeContentNode.nextInstanceId += 1 @@ -1059,7 +1059,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod var playerSource: HLSJSServerSource? if let qualitySet = HLSQualitySet(baseFile: fileReference, codecConfiguration: codecConfiguration) { - let playerSourceValue = HLSJSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) + let playerSourceValue = HLSJSServerSource(accountId: context.account.id.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) playerSource = playerSourceValue } self.playerSource = playerSource @@ -1075,6 +1075,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod var onSeeked: (() -> Void)? self.player = ChunkMediaPlayerV2( + params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), audioSessionManager: audioSessionManager, source: .externalParts(self.chunkPlayerPartsState.get()), video: true, @@ -1102,9 +1103,9 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod self.playerNode.isHidden = false }*/ - //let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference)?.file ?? fileReference + let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference, codecConfiguration: self.codecConfiguration)?.file ?? fileReference - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, previewSourceFileReference: nil, imageReference: nil, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: thumbnailVideoReference, previewSourceFileReference: nil, imageReference: nil, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { @@ -1333,7 +1334,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod return } - self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, content: .parts(sourceBuffer.items)))) } fileprivate func onMediaSourceBuffersUpdated() { @@ -1347,7 +1348,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod return } - self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, content: .parts(sourceBuffer.items)))) if self.sourceBufferStateDisposable == nil { self.sourceBufferStateDisposable = (sourceBuffer.updated.signal() |> deliverOnMainQueue).startStrict(next: { [weak self, weak sourceBuffer] _ in @@ -1357,7 +1358,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod guard let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[sourceBuffer.mediaSourceId] else { return } - self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, content: .parts(sourceBuffer.items)))) self.updateBuffered() }) @@ -1832,7 +1833,7 @@ private final class SourceBuffer { let item = ChunkMediaPlayerPart( startTime: fragmentInfo.startTime.seconds, endTime: fragmentInfo.startTime.seconds + fragmentInfo.duration.seconds, - content: .tempFile(ChunkMediaPlayerPart.Content.TempFile(file: tempFile)), + content: ChunkMediaPlayerPart.TempFile(file: tempFile), codecName: videoCodecName ) self.items.append(item) diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index ec361c1208..d724cc1568 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -128,8 +128,8 @@ public final class NativeVideoContent: UniversalVideoContent { self.hasSentFramesToDisplay = hasSentFramesToDisplay } - public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, previewSourceFileReference: self.previewSourceFileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) + public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return NativeVideoContentNode(context: context, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, previewSourceFileReference: self.previewSourceFileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -277,6 +277,17 @@ private enum PlayerImpl { } } +extension ChunkMediaPlayerV2.MediaDataReaderParams { + init(context: AccountContext) { + var useV2Reader = true + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_video_v2_reader"] as? Double { + useV2Reader = value != 0.0 + } + + self.init(useV2Reader: useV2Reader) + } +} + private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let postbox: Postbox private let userLocation: MediaResourceUserLocation @@ -296,7 +307,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let continuePlayingWithoutSoundOnLostAudioSession: Bool private let displayImage: Bool - private var player: PlayerImpl + private var player: PlayerImpl? private var thumbnailPlayer: MediaPlayer? private let imageNode: TransformImageNode private let playerNode: MediaPlayerNode @@ -338,6 +349,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return self._ready.get() } + private var initializePlayerDisposable: Disposable? private let fetchDisposable = MetaDisposable() private let fetchStatusDisposable = MetaDisposable() @@ -347,10 +359,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var validLayout: (size: CGSize, actualSize: CGSize)? private var shouldPlay: Bool = false + private var pendingSetSoundEnabled: Bool? + private var pendingSeek: Double? + private var pendingPlayOnceWithSound: (playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)? + private var pendingForceAudioToSpeaker: Bool? + private var pendingSetSoundMuted: Bool? + private var pendingContinueWithOverridingAmbientMode: Bool? + private var pendingSetBaseRate: Double? + private var pendingContinuePlayingWithoutSound: MediaPlayerPlayOnceWithSoundActionAtEnd? + private var pendingSetContinuePlayingWithoutSoundOnLostAudioSession: Bool? private let hasSentFramesToDisplay: (() -> Void)? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, limitedFileRange: Range?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { + init(context: AccountContext, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, limitedFileRange: Range?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference @@ -385,67 +406,6 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.playerNode = MediaPlayerNode(backgroundThread: false, captureProtected: captureProtected) - if !"".isEmpty { - let mediaPlayer = MediaPlayer( - audioSessionManager: audioSessionManager, - postbox: postbox, - userLocation: userLocation, - userContentType: userContentType, - resourceReference: fileReference.resourceReference(selectedFile.resource), - tempFilePath: tempFilePath, - limitedFileRange: limitedFileRange, - streamable: streamVideo, - video: true, - preferSoftwareDecoding: false, - playAutomatically: false, - enableSound: enableSound, - baseRate: baseRate, - fetchAutomatically: fetchAutomatically, - soundMuted: soundMuted, - ambient: beginWithAmbientSound, - mixWithOthers: mixWithOthers, - continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, - storeAfterDownload: storeAfterDownload, - isAudioVideoMessage: isAudioVideoMessage - ) - self.player = .legacy(mediaPlayer) - mediaPlayer.attachPlayerNode(self.playerNode) - } else { - let mediaPlayer = ChunkMediaPlayerV2( - audioSessionManager: audioSessionManager, - source: .directFetch(ChunkMediaPlayerV2.SourceDescription.ResourceDescription( - postbox: postbox, - reference: fileReference.resourceReference(selectedFile.resource), - userLocation: userLocation, - userContentType: userContentType, - statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes), - fetchAutomatically: fetchAutomatically - )), - video: true, - playAutomatically: false, - enableSound: enableSound, - baseRate: baseRate, - soundMuted: soundMuted, - ambient: beginWithAmbientSound, - mixWithOthers: mixWithOthers, - continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, - isAudioVideoMessage: isAudioVideoMessage, - playerNode: self.playerNode - ) - self.player = .chunked(mediaPlayer) - } - - var actionAtEndImpl: (() -> Void)? - if enableSound && !loopVideo { - self.player.actionAtEnd = .action({ - actionAtEndImpl?() - }) - } else { - self.player.actionAtEnd = .loop({ - actionAtEndImpl?() - }) - } - self.dimensions = fileReference.media.dimensions?.cgSize if let dimensions = self.dimensions { self.dimensionsPromise.set(dimensions) @@ -469,10 +429,6 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.dimensionsPromise.set(dimensions) } - actionAtEndImpl = { [weak self] in - self?.performActionAtEnd() - } - if displayImage { if captureProtected { setLayerDisableScreenshots(self.imageNode.layer, captureProtected) @@ -497,10 +453,6 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } self.addSubnode(self.playerNode) - self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status) - |> map { dimensions, status in - return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) - }) self.fetchStatusDisposable.set((postbox.mediaBox.resourceStatus(selectedFile.resource) |> deliverOnMainQueue).start(next: { [weak self] status in @@ -536,15 +488,140 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent if let startTimestamp = startTimestamp { self.seek(startTimestamp) } + + var useLegacyImplementation = !context.sharedContext.immediateExperimentalUISettings.playerV2 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_video_legacyplayer"] as? Double { + useLegacyImplementation = value != 0.0 + } + + if useLegacyImplementation { + let mediaPlayer = MediaPlayer( + audioSessionManager: audioSessionManager, + postbox: postbox, + userLocation: userLocation, + userContentType: userContentType, + resourceReference: fileReference.resourceReference(selectedFile.resource), + tempFilePath: tempFilePath, + limitedFileRange: limitedFileRange, + streamable: streamVideo, + video: true, + preferSoftwareDecoding: false, + playAutomatically: false, + enableSound: enableSound, + baseRate: baseRate, + fetchAutomatically: fetchAutomatically, + soundMuted: soundMuted, + ambient: beginWithAmbientSound, + mixWithOthers: mixWithOthers, + continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, + storeAfterDownload: storeAfterDownload, + isAudioVideoMessage: isAudioVideoMessage + ) + mediaPlayer.attachPlayerNode(self.playerNode) + self.initializePlayer(player: .legacy(mediaPlayer)) + } else { + let mediaPlayer = ChunkMediaPlayerV2( + params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), + audioSessionManager: audioSessionManager, + source: .directFetch(ChunkMediaPlayerV2.SourceDescription.ResourceDescription( + postbox: postbox, + size: selectedFile.size ?? 0, + reference: fileReference.resourceReference(selectedFile.resource), + userLocation: userLocation, + userContentType: userContentType, + statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes), + fetchAutomatically: fetchAutomatically + )), + video: true, + playAutomatically: false, + enableSound: enableSound, + baseRate: baseRate, + soundMuted: soundMuted, + ambient: beginWithAmbientSound, + mixWithOthers: mixWithOthers, + continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, + isAudioVideoMessage: isAudioVideoMessage, + playerNode: self.playerNode + ) + self.initializePlayer(player: .chunked(mediaPlayer)) + } } deinit { - self.player.pause() + self.initializePlayerDisposable?.dispose() + self.player?.pause() self.thumbnailPlayer?.pause() self.fetchDisposable.dispose() self.fetchStatusDisposable.dispose() } + private func initializePlayer(player: PlayerImpl) { + var player = player + self.player = player + + var actionAtEndImpl: (() -> Void)? + if self.enableSound && !self.loopVideo { + player.actionAtEnd = .action({ + actionAtEndImpl?() + }) + } else { + player.actionAtEnd = .loop({ + actionAtEndImpl?() + }) + } + actionAtEndImpl = { [weak self] in + self?.performActionAtEnd() + } + + self._status.set(combineLatest(self.dimensionsPromise.get(), player.status) + |> map { dimensions, status in + return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) + }) + + if self.shouldPlay { + player.play() + } else { + player.pause() + } + + if let pendingSeek = self.pendingSeek { + self.pendingSeek = nil + self.seek(pendingSeek) + } + if let pendingSetSoundEnabled = self.pendingSetSoundEnabled { + self.pendingSetSoundEnabled = nil + self.setSoundEnabled(pendingSetSoundEnabled) + } + if let pendingPlayOnceWithSound = self.pendingPlayOnceWithSound { + self.pendingPlayOnceWithSound = nil + self.playOnceWithSound(playAndRecord: pendingPlayOnceWithSound.playAndRecord, seek: pendingPlayOnceWithSound.seek, actionAtEnd: pendingPlayOnceWithSound.actionAtEnd) + } + if let pendingForceAudioToSpeaker = self.pendingForceAudioToSpeaker { + self.pendingForceAudioToSpeaker = nil + self.setForceAudioToSpeaker(pendingForceAudioToSpeaker) + } + if let pendingSetSoundMuted = self.pendingSetSoundMuted { + self.pendingSetSoundMuted = nil + self.setSoundMuted(soundMuted: pendingSetSoundMuted) + } + if let pendingContinueWithOverridingAmbientMode = self.pendingContinueWithOverridingAmbientMode { + self.pendingContinueWithOverridingAmbientMode = nil + self.continueWithOverridingAmbientMode(isAmbient: pendingContinueWithOverridingAmbientMode) + } + if let pendingSetBaseRate = self.pendingSetBaseRate { + self.pendingSetBaseRate = nil + self.setBaseRate(pendingSetBaseRate) + } + if let pendingContinuePlayingWithoutSound = self.pendingContinuePlayingWithoutSound { + self.pendingContinuePlayingWithoutSound = nil + self.continuePlayingWithoutSound(actionAtEnd: pendingContinuePlayingWithoutSound) + } + if let pendingSetContinuePlayingWithoutSoundOnLostAudioSession = self.pendingSetContinuePlayingWithoutSoundOnLostAudioSession { + self.pendingSetContinuePlayingWithoutSoundOnLostAudioSession = nil + self.setContinuePlayingWithoutSoundOnLostAudioSession(pendingSetContinuePlayingWithoutSoundOnLostAudioSession) + } + } + private func createThumbnailPlayer() { guard let videoThumbnail = self.fileReference.media.videoThumbnails.first else { return @@ -639,41 +716,55 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func play() { assert(Queue.mainQueue().isCurrent()) - self.player.play() + self.player?.play() self.shouldPlay = true self.thumbnailPlayer?.play() } func pause() { assert(Queue.mainQueue().isCurrent()) - self.player.pause() + self.player?.pause() self.shouldPlay = false self.thumbnailPlayer?.pause() } func togglePlayPause() { assert(Queue.mainQueue().isCurrent()) - self.player.togglePlayPause() + self.player?.togglePlayPause() self.shouldPlay = !self.shouldPlay self.thumbnailPlayer?.togglePlayPause() } func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) - if value { - self.player.playOnceWithSound(playAndRecord: false, seek: .none) + if let player = self.player { + if value { + player.playOnceWithSound(playAndRecord: false, seek: .none) + } else { + player.continuePlayingWithoutSound(seek: .none) + } } else { - self.player.continuePlayingWithoutSound(seek: .none) + self.pendingSetSoundEnabled = value } } func seek(_ timestamp: Double) { assert(Queue.mainQueue().isCurrent()) - self.player.seek(timestamp: timestamp) + if let player = self.player { + player.seek(timestamp: timestamp) + } else { + self.pendingSeek = timestamp + } } func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) + + guard var player = self.player else { + self.pendingPlayOnceWithSound = (playAndRecord, seek, actionAtEnd) + return + } + let action = { [weak self] in Queue.mainQueue().async { self?.performActionAtEnd() @@ -681,49 +772,65 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } switch actionAtEnd { case .loop: - self.player.actionAtEnd = .loop({}) + player.actionAtEnd = .loop({}) case .loopDisablingSound: - self.player.actionAtEnd = .loopDisablingSound(action) + player.actionAtEnd = .loopDisablingSound(action) case .stop: - self.player.actionAtEnd = .action(action) + player.actionAtEnd = .action(action) case .repeatIfNeeded: - let _ = (self.player.status + let _ = (player.status |> deliverOnMainQueue |> take(1)).start(next: { [weak self] status in - guard let strongSelf = self else { + guard let strongSelf = self, var player = strongSelf.player else { return } if status.timestamp > status.duration * 0.1 { - strongSelf.player.actionAtEnd = .loop({ [weak self] in - guard let strongSelf = self else { + player.actionAtEnd = .loop({ [weak self] in + guard let strongSelf = self, var player = strongSelf.player else { return } - strongSelf.player.actionAtEnd = .loopDisablingSound(action) + player.actionAtEnd = .loopDisablingSound(action) }) } else { - strongSelf.player.actionAtEnd = .loopDisablingSound(action) + player.actionAtEnd = .loopDisablingSound(action) } }) } - self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) + player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) } func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { assert(Queue.mainQueue().isCurrent()) - self.player.setForceAudioToSpeaker(forceAudioToSpeaker) + if let player = self.player { + player.setForceAudioToSpeaker(forceAudioToSpeaker) + } else { + self.pendingForceAudioToSpeaker = forceAudioToSpeaker + } } func setSoundMuted(soundMuted: Bool) { - self.player.setSoundMuted(soundMuted: soundMuted) + if let player = self.player { + player.setSoundMuted(soundMuted: soundMuted) + } else { + self.pendingSetSoundMuted = soundMuted + } } func continueWithOverridingAmbientMode(isAmbient: Bool) { - self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient) + if let player = self.player { + player.continueWithOverridingAmbientMode(isAmbient: isAmbient) + } else { + self.pendingContinueWithOverridingAmbientMode = isAmbient + } } func setBaseRate(_ baseRate: Double) { - self.player.setBaseRate(baseRate) + if let player = self.player { + player.setBaseRate(baseRate) + } else { + self.pendingSetBaseRate = baseRate + } } func setVideoQuality(_ quality: UniversalVideoContentVideoQuality) { @@ -739,24 +846,34 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) + + guard var player = self.player else { + self.pendingContinuePlayingWithoutSound = actionAtEnd + return + } + let action = { [weak self] in Queue.mainQueue().async { self?.performActionAtEnd() } } switch actionAtEnd { - case .loop: - self.player.actionAtEnd = .loop({}) - case .loopDisablingSound, .repeatIfNeeded: - self.player.actionAtEnd = .loopDisablingSound(action) - case .stop: - self.player.actionAtEnd = .action(action) + case .loop: + player.actionAtEnd = .loop({}) + case .loopDisablingSound, .repeatIfNeeded: + player.actionAtEnd = .loopDisablingSound(action) + case .stop: + player.actionAtEnd = .action(action) } - self.player.continuePlayingWithoutSound() + player.continuePlayingWithoutSound() } func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { - self.player.setContinuePlayingWithoutSoundOnLostAudioSession(value) + if let player = self.player { + player.setContinuePlayingWithoutSoundOnLostAudioSession(value) + } else { + self.pendingSetContinuePlayingWithoutSoundOnLostAudioSession = value + } } func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index d71b1eec99..0da0605cdc 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -41,7 +41,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP private var statusDisposable: Disposable? private var status: MediaPlayerStatus? - public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { + public init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content self.defaultExpand = expand @@ -62,7 +62,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP }, controlsAreShowingUpdated: { value in controlsAreShowingUpdatedImpl?(value) }) - self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) + self.videoNode = UniversalVideoNode(context: context, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration super.init() diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 2481eb2a85..924d67dc0b 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -95,7 +95,7 @@ public final class PlatformVideoContent: UniversalVideoContent { self.fetchAutomatically = fetchAutomatically } - public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index fae099ae4c..32ee7caeda 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -29,7 +29,7 @@ public final class SystemVideoContent: UniversalVideoContent { self.duration = duration } - public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index ebe1c8b9ac..5a74e611e7 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -36,7 +36,7 @@ public final class WebEmbedVideoContent: UniversalVideoContent { self.openUrl = openUrl } - public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl) } } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index ddfb4b0f13..b0af5d23a2 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -1035,6 +1035,10 @@ public final class OngoingGroupCallContext { #endif } + + func addRemoteConnectedEvent(isRemoteConntected: Bool) { + self.context.addRemoteConnectedEvent(isRemoteConntected) + } } private let queue = Queue() @@ -1250,4 +1254,10 @@ public final class OngoingGroupCallContext { impl.setTone(tone: tone) } } + + public func addRemoteConnectedEvent(isRemoteConntected: Bool) { + self.impl.with { impl in + impl.addRemoteConnectedEvent(isRemoteConntected: isRemoteConntected) + } + } } diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 4c25ab81ff..e60a75f17b 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -550,7 +550,7 @@ public final class PeerChannelMemberCategoriesContextsManager { |> runOn(Queue.mainQueue()) } - public func recentOnlineSmall(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { + public func recentOnlineSmall(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal<(total: Int32, recent: Int32), NoError> { return Signal { [weak self] subscriber in var previousIds: Set? let statusesDisposable = MetaDisposable() @@ -587,7 +587,7 @@ public final class PeerChannelMemberCategoriesContextsManager { } |> distinctUntilChanged |> deliverOnMainQueue).start(next: { count in - subscriber.putNext(count) + subscriber.putNext((Int32(updatedIds.count), count)) })) } }) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 91266ba908..98531b6385 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -402,6 +402,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case file case topicId case topicInfo + case enableAnimation } public enum Custom: Codable { @@ -417,12 +418,14 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { public let fileId: Int64 public let file: TelegramMediaFile? public let custom: Custom? + public let enableAnimation: Bool - public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, custom: Custom? = nil) { + public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, custom: Custom? = nil, enableAnimation: Bool = true) { self.interactivelySelectedFromPackId = interactivelySelectedFromPackId self.fileId = fileId self.file = file self.custom = custom + self.enableAnimation = enableAnimation super.init() } @@ -433,6 +436,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { self.fileId = try container.decode(Int64.self, forKey: .fileId) self.file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) self.custom = nil + self.enableAnimation = try container.decodeIfPresent(Bool.self, forKey: .enableAnimation) ?? true } public func encode(to encoder: Encoder) throws { @@ -440,6 +444,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { try container.encodeIfPresent(self.interactivelySelectedFromPackId, forKey: .interactivelySelectedFromPackId) try container.encode(self.fileId, forKey: .fileId) try container.encodeIfPresent(self.file, forKey: .file) + try container.encode(self.enableAnimation, forKey: .enableAnimation) } override public func isEqual(_ object: Any?) -> Bool { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 7e88388dc6..6255cbebbd 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -453,6 +453,8 @@ isConference:(bool)isConference; - (void)getStats:(void (^ _Nonnull)(OngoingGroupCallStats * _Nonnull))completion; +- (void)addRemoteConnectedEvent:(bool)isRemoteConnected; + @end #endif diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 93a32e0d5a..0a8fcf3821 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -2323,6 +2323,12 @@ isConference:(bool)isConference { } } +- (void)addRemoteConnectedEvent:(bool)isRemoteConnected { + if (_instance) { + _instance->internal_addCustomNetworkEvent(isRemoteConnected); + } +} + @end @implementation OngoingGroupCallMediaChannelDescription diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index ab50f4e095..2d45febcfe 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit ab50f4e095d5793c39dc54b740a982fc3ba27ea5 +Subproject commit 2d45febcfe741be127eaee236e71be09acbc7a38 diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 0a4f70825d..acbe58cbcc 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -8,7 +8,7 @@ import ComponentFlow import AnimatedTextComponent public enum UndoOverlayContent { - case removedChat(title: String, text: String?) + case removedChat(context: AccountContext, title: NSAttributedString, text: String?) case archivedChat(peerId: Int64, title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) @@ -19,8 +19,8 @@ public enum UndoOverlayContent { case actionSucceeded(title: String?, text: String, cancel: String?, destructive: Bool) case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: StickerPackItem?, context: AccountContext) case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) - case chatAddedToFolder(chatTitle: String, folderTitle: String) - case chatRemovedFromFolder(chatTitle: String, folderTitle: String) + case chatAddedToFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) + case chatRemovedFromFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case setProximityAlert(title: String, text: String, cancelled: Bool) case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, title: String?, text: String, action: String?, duration: Double) @@ -45,6 +45,7 @@ public enum UndoOverlayContent { case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?) + case universalWithEntities(context: AccountContext, animation: String, scale: CGFloat, colors: [String: UIColor], title: NSAttributedString?, text: NSAttributedString, animateEntities: Bool, customUndoText: String?, timeout: Double?) case universalImage(image: UIImage, size: CGSize?, title: String?, text: String, customUndoText: String?, timeout: Double?) case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index bf5973ab97..80204a9663 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -46,7 +46,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var stickerSourceSize: CGSize? private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? - private let titleNode: ImmediateTextNode + private let titleNode: ImmediateTextNodeWithEntities private let textNode: ImmediateTextNodeWithEntities private var textComponent: ComponentView? private var animatedTextItems: [AnimatedTextComponent.Item]? @@ -92,7 +92,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.timerTextNode = ImmediateTextNode() self.timerTextNode.displaysAsynchronously = false - self.titleNode = ImmediateTextNode() + self.titleNode = ImmediateTextNodeWithEntities() self.titleNode.layer.anchorPoint = CGPoint() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 0 @@ -115,22 +115,51 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var isUserInteractionEnabled = false switch content { - case let .removedChat(title, text): + case let .removedChat(context, title, text): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil self.animatedStickerNode = nil + + let attributedTitle = NSMutableAttributedString(string: title.string) + attributedTitle.addAttribute(.font, value: Font.semibold(14.0), range: NSRange(location: 0, length: title.length)) + attributedTitle.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: title.length)) + title.enumerateAttributes(in: NSRange(location: 0, length: title.length), using: { attributes, range, _ in + for (key, value) in attributes { + attributedTitle.addAttribute(key, value: value, range: range) + } + }) + if let text { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.titleNode.attributedText = attributedTitle let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText } else { - self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = attributedTitle } + + self.titleNode.visibility = true + self.titleNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + + self.textNode.visibility = true + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + displayUndo = true self.originalRemainingSeconds = 5 self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) @@ -343,38 +372,67 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) displayUndo = false self.originalRemainingSeconds = 5 - case let .chatAddedToFolder(chatTitle, folderTitle): + case let .chatAddedToFolder(context, chatTitle, folderTitle), let .chatRemovedFromFolder(context, chatTitle, folderTitle): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil - self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + + let baseString: String + if case .chatAddedToFolder = content { + baseString = presentationData.strings.ChatList_AddedToFolderTooltipV2 + self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + } else { + baseString = presentationData.strings.ChatList_RemovedFromFolderTooltipV2 + self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + } self.animatedStickerNode = nil - let formattedString = presentationData.strings.ChatList_AddedToFolderTooltip(chatTitle, folderTitle) - - let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white)) - for range in formattedString.ranges { - string.addAttribute(.font, value: Font.regular(14.0), range: range.range) - } - - self.textNode.attributedText = string - displayUndo = false - self.originalRemainingSeconds = 5 - case let .chatRemovedFromFolder(chatTitle, folderTitle): - self.avatarNode = nil - self.iconNode = nil - self.iconCheckNode = nil - self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) - self.animatedStickerNode = nil - - let formattedString = presentationData.strings.ChatList_RemovedFromFolderTooltip(chatTitle, folderTitle) - - let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString.string, font: Font.regular(14.0), textColor: .white)) - for range in formattedString.ranges { - string.addAttribute(.font, value: Font.regular(14.0), range: range.range) + let string = NSMutableAttributedString(string: baseString) + string.addAttributes([ + .font: Font.regular(14.0), + .foregroundColor: UIColor.white + ], range: NSRange(location: 0, length: string.length)) + + let folderRange = (string.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + string.replaceCharacters(in: folderRange, with: "") + let processedFolderTitle = NSMutableAttributedString(string: folderTitle.string) + processedFolderTitle.addAttributes([ + .font: Font.semibold(14.0), + .foregroundColor: UIColor.white + ], range: NSRange(location: 0, length: processedFolderTitle.length)) + folderTitle.enumerateAttributes(in: NSRange(location: 0, length: folderTitle.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + processedFolderTitle.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + processedFolderTitle.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + processedFolderTitle.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + processedFolderTitle.addAttribute(key, value: value, range: range) + } + } + }) + string.insert(processedFolderTitle, at: folderRange.location) + } + + let chatRange = (string.string as NSString).range(of: "{chat}") + if chatRange.location != NSNotFound { + string.replaceCharacters(in: chatRange, with: "") + string.insert(NSAttributedString(string: chatTitle, font: Font.semibold(14.0), textColor: .white), at: chatRange.location) } self.textNode.attributedText = string + self.textNode.visibility = true + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + displayUndo = false self.originalRemainingSeconds = 5 case let .paymentSent(currencyValue, itemTitle): @@ -1072,6 +1130,86 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + case let .universalWithEntities(context, animation, scale, colors, title, text, animateEntities, customUndoText, timeout): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: animation, colors: colors, scale: scale) + self.animatedStickerNode = nil + + var attributedTitle: NSAttributedString? + if let title { + let attributedTitleValue = NSMutableAttributedString(string: title.string) + attributedTitleValue.addAttribute(.font, value: Font.semibold(14.0), range: NSRange(location: 0, length: title.length)) + attributedTitleValue.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: title.length)) + title.enumerateAttributes(in: NSRange(location: 0, length: title.length), using: { attributes, range, _ in + for (key, value) in attributes { + attributedTitleValue.addAttribute(key, value: value, range: range) + } + }) + attributedTitle = attributedTitleValue + } + + if let attributedTitle, text.length == 0 { + self.titleNode.attributedText = nil + self.textNode.attributedText = attributedTitle + } else { + if let attributedTitle { + self.titleNode.attributedText = attributedTitle + } else { + self.titleNode.attributedText = nil + } + + let attributedText = NSMutableAttributedString(string: text.string) + attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: text.length)) + attributedText.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(location: 0, length: text.length)) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + } else if key == ChatTextInputAttributes.italic { + attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) + } else if key == ChatTextInputAttributes.monospace { + attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) + } else { + attributedText.addAttribute(key, value: value, range: range) + } + } + }) + + self.textNode.attributedText = attributedText + } + + if text.string.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = timeout ?? (isUserInteractionEnabled ? 5 : 3) + + self.titleNode.visibility = animateEntities + self.titleNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + + self.textNode.maximumNumberOfLines = 5 + self.textNode.visibility = animateEntities + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + if let customUndoText = customUndoText { undoText = customUndoText displayUndo = true @@ -1431,7 +1569,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal,. universalImage, .premiumPaywall, .peers, .messageTagged: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .universalWithEntities, .universalImage, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index d3696d5d0d..676cce6782 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -165,7 +165,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) videoNode.updateLayout(size: videoSize, transition: .immediate) self.videoNode = videoNode diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index d8fb53f890..f045941424 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -49,17 +49,15 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libdav1d \ --enable-audiotoolbox \ --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ - --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at,vorbis,libdav1d \ + --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at,vorbis,libdav1d,av1 \ --enable-encoder=libvpx_vp9,aac_at \ - --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ + --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts, \ --enable-parser=aac,h264,mp3,libopus \ --enable-protocol=file \ --enable-muxer=mp4,matroska,mpegts \ + --enable-hwaccel=h264_videotoolbox,hevc_videotoolbox,av1_videotoolbox \ " - -#--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox \ - EXTRA_CFLAGS="-DCONFIG_SAFE_BITSTREAM_READER=1" if [ "$1" = "debug" ]; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/Changelog b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/Changelog index 0c05ad2946..b82b948074 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/Changelog +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/Changelog @@ -1,6 +1,8 @@ Entries are sorted chronologically from oldest to youngest within each release, releases are sorted from youngest to oldest. +version : + version 7.1: - Raw Captions with Time (RCWT) closed caption demuxer - LC3/LC3plus decoding/encoding using external library liblc3 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/MAINTAINERS b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/MAINTAINERS index 76651d5ff8..7ac5614c18 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/MAINTAINERS +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/MAINTAINERS @@ -206,7 +206,6 @@ Codecs: libgsm.c Michel Bardiaux libkvazaar.c Arttu Ylä-Outinen libopenh264enc.c Martin Storsjo, Linjie Fu - libopenjpeg.c Jaikrishnan Menon libopenjpegenc.c Michael Bradshaw libtheoraenc.c David Conrad libvorbis.c David Conrad @@ -391,7 +390,7 @@ Muxers/Demuxers: dss.c Oleksij Rempel dtsdec.c foo86 dv.c Roman Shaposhnik - dvdvideodec.c Marth64 + dvdvideodec.c [2] Marth64 electronicarts.c Peter Ross evc* Samsung (Dawid Kozinski) ffm* Baptiste Coudurier @@ -445,7 +444,8 @@ Muxers/Demuxers: pva.c Ivo van Poorten r3d.c Baptiste Coudurier raw.c Michael Niedermayer - rcwtenc.c Marth64 + rcwtdec.c [2] Marth64 + rcwtenc.c [2] Marth64 rdt.c Ronald S. Bultje rl2.c Sascha Sommer rmdec.c, rmenc.c Ronald S. Bultje diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE index 0f0fefae5a..72ec89de30 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE @@ -1 +1 @@ -7.1 +7.0.git diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE_NOTES b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE_NOTES deleted file mode 100644 index 851701a735..0000000000 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/RELEASE_NOTES +++ /dev/null @@ -1,15 +0,0 @@ - - ┌──────────────────────────────────────┐ - │ RELEASE NOTES for FFmpeg 7.1 "Péter" │ - └──────────────────────────────────────┘ - - The FFmpeg Project proudly presents FFmpeg 7.1 "Péter", about 6 - months after the release of FFmpeg 7.0. - - A complete Changelog is available at the root of the project, and the - complete Git history on https://git.ffmpeg.org/gitweb/ffmpeg.git - - We hope you will like this release as much as we enjoyed working on it, and - as usual, if you have any questions about it, or any FFmpeg related topic, - feel free to join us on the #ffmpeg IRC channel (on irc.libera.chat) or ask - on the mailing-lists. diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/VERSION b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/VERSION deleted file mode 100644 index 0f0fefae5a..0000000000 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.1 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/configure b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/configure index d77a55b653..dc1b9b2bea 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/configure +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/configure @@ -467,6 +467,8 @@ Optimization options (experts only): --disable-neon disable NEON optimizations --disable-dotprod disable DOTPROD optimizations --disable-i8mm disable I8MM optimizations + --disable-sve disable SVE optimizations + --disable-sve2 disable SVE2 optimizations --disable-inline-asm disable use of inline assembly --disable-x86asm disable use of standalone x86 assembly --disable-mipsdsp disable MIPS DSP ASE R1 optimizations @@ -2163,6 +2165,8 @@ ARCH_EXT_LIST_ARM=" vfp vfpv3 setend + sve + sve2 " ARCH_EXT_LIST_MIPS=" @@ -2432,6 +2436,8 @@ TOOLCHAIN_FEATURES=" as_arch_directive as_archext_dotprod_directive as_archext_i8mm_directive + as_archext_sve_directive + as_archext_sve2_directive as_dn_directive as_fpu_directive as_func @@ -2461,6 +2467,7 @@ TYPES_LIST=" kCMVideoCodecType_HEVC kCMVideoCodecType_HEVCWithAlpha kCMVideoCodecType_VP9 + kCMVideoCodecType_AV1 kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange @@ -2752,6 +2759,8 @@ vfpv3_deps="vfp" setend_deps="arm" dotprod_deps="aarch64 neon" i8mm_deps="aarch64 neon" +sve_deps="aarch64 neon" +sve2_deps="aarch64 neon sve" map 'eval ${v}_inline_deps=inline_asm' $ARCH_EXT_LIST_ARM @@ -3166,6 +3175,8 @@ av1_vaapi_hwaccel_deps="vaapi VADecPictureParameterBufferAV1_bit_depth_idx" av1_vaapi_hwaccel_select="av1_decoder" av1_vdpau_hwaccel_deps="vdpau VdpPictureInfoAV1" av1_vdpau_hwaccel_select="av1_decoder" +av1_videotoolbox_hwaccel_deps="videotoolbox" +av1_videotoolbox_hwaccel_select="av1_decoder" av1_vulkan_hwaccel_deps="vulkan" av1_vulkan_hwaccel_select="av1_decoder" h263_vaapi_hwaccel_deps="vaapi" @@ -6234,9 +6245,11 @@ if enabled aarch64; then # internal assembler in clang 3.3 does not support this instruction enabled neon && check_insn neon 'ext v0.8B, v0.8B, v1.8B, #1' - archext_list="dotprod i8mm" + archext_list="dotprod i8mm sve sve2" enabled dotprod && check_archext_insn dotprod 'udot v0.4s, v0.16b, v0.16b' enabled i8mm && check_archext_insn i8mm 'usdot v0.4s, v0.16b, v0.16b' + enabled sve && check_archext_insn sve 'whilelt p0.s, x0, x1' + enabled sve2 && check_archext_insn sve2 'sqrdmulh z0.s, z0.s, z0.s' # Disable the main feature (e.g. HAVE_NEON) if neither inline nor external # assembly support the feature out of the box. Skip this for the features @@ -6480,6 +6493,7 @@ check_cc intrinsics_sse2 emmintrin.h "__m128i test = _mm_setzero_si128()" check_ldflags -Wl,--as-needed check_ldflags -Wl,-z,noexecstack +check_ldflags -Wl,-no_warn_duplicate_libraries if ! disabled network; then check_func getaddrinfo $network_extralibs @@ -6697,6 +6711,7 @@ enabled videotoolbox && { check_func_headers CoreMedia/CMFormatDescription.h kCMVideoCodecType_HEVC "-framework CoreMedia" check_func_headers CoreMedia/CMFormatDescription.h kCMVideoCodecType_HEVCWithAlpha "-framework CoreMedia" check_func_headers CoreMedia/CMFormatDescription.h kCMVideoCodecType_VP9 "-framework CoreMedia" + check_func_headers CoreMedia/CMFormatDescription.h kCMVideoCodecType_AV1 "-framework CoreMedia" check_func_headers CoreVideo/CVPixelBuffer.h kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange "-framework CoreVideo" check_func_headers CoreVideo/CVPixelBuffer.h kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange "-framework CoreVideo" check_func_headers CoreVideo/CVPixelBuffer.h kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange "-framework CoreVideo" @@ -7919,6 +7934,8 @@ if enabled aarch64; then echo "NEON enabled ${neon-no}" echo "DOTPROD enabled ${dotprod-no}" echo "I8MM enabled ${i8mm-no}" + echo "SVE enabled ${sve-no}" + echo "SVE2 enabled ${sve2-no}" fi if enabled arm; then echo "ARMv5TE enabled ${armv5te-no}" diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/.gitignore new file mode 100644 index 0000000000..7628cecb99 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/.gitignore @@ -0,0 +1,9 @@ +/*.1 +/*.3 +/*.html +/*.pod +/config.texi +/avoptions_codec.texi +/avoptions_format.texi +/fate.txt +/print_options diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/APIchanges b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/APIchanges index 924a2b279e..d132dfab81 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/APIchanges +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/APIchanges @@ -2,6 +2,37 @@ The last version increases of all libraries were on 2024-03-07 API changes, most recent first: +2024-10-xx - xxxxxxxxxx - lavu 59.41.100 - log.h + Add AVClass.state_flags_offset and AV_CLASS_STATE_INITIALIZED. + +2024-09-30 - xxxxxxxxxx - lavf 61.9.100 - avformat.h + Add {nb_}coded_side_data to AVStreamGroupTileGrid. + +2024-09-xx - xxxxxxxxxx - lavu 59 + Deprecate av_int_list_length_for_size(), av_int_list_length(), and + av_opt_set_int_list() without replacement. All AVOptions using these + should be replaced with AV_OPT_TYPE_FLAG_ARRAY. + +2024-09-xx - xxxxxxxxxx - lavfi 10.6.100 + Buffersink now has array-type options + - pixel_formats + - colorspaces + - colorranges + replacing the int-list options + - pix_fmts + - color_spaces + - color_ranges + abuffersink now has array-type options + - sample_formats + - samplerates + - channel_layouts + replacing the int-list/string options + - sample_fmts + - sample_rates + - ch_layouts + +-------- 8< --------- FFmpeg 7.1 was cut here -------- 8< --------- + 2024-09-23 - 6940a6de2f0 - lavu 59.38.100 - frame.h Add AV_FRAME_DATA_VIEW_ID. diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/Doxyfile b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/Doxyfile index c6dc306197..572c532da5 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/Doxyfile +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = FFmpeg # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 7.1 +PROJECT_NUMBER = # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/doxy/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/doxy/.gitignore new file mode 100644 index 0000000000..ac7af2e80e --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/doxy/.gitignore @@ -0,0 +1 @@ +/html/ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/.gitignore new file mode 100644 index 0000000000..d787afdd4c --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/.gitignore @@ -0,0 +1,25 @@ +/avio_list_dir +/avio_reading +/decode_audio +/decode_video +/demuxing_decoding +/encode_audio +/encode_video +/extract_mvs +/filter_audio +/filtering_audio +/filtering_video +/http_multiclient +/hw_decode +/metadata +/muxing +/pc-uninstalled +/qsvdec +/remuxing +/resampling_audio +/scaling_video +/transcode_aac +/transcoding +/vaapi_encode +/vaapi_transcode +/qsv_transcode diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_audio.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_audio.c index d637ca1724..03e13ef66f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_audio.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_audio.c @@ -96,8 +96,7 @@ static int init_filters(const char *filters_descr) const AVFilter *abuffersink = avfilter_get_by_name("abuffersink"); AVFilterInOut *outputs = avfilter_inout_alloc(); AVFilterInOut *inputs = avfilter_inout_alloc(); - static const enum AVSampleFormat out_sample_fmts[] = { AV_SAMPLE_FMT_S16, -1 }; - static const int out_sample_rates[] = { 8000, -1 }; + static const int out_sample_rate = 8000; const AVFilterLink *outlink; AVRational time_base = fmt_ctx->streams[audio_stream_index]->time_base; @@ -123,34 +122,40 @@ static int init_filters(const char *filters_descr) } /* buffer audio sink: to terminate the filter chain. */ - ret = avfilter_graph_create_filter(&buffersink_ctx, abuffersink, "out", - NULL, NULL, filter_graph); - if (ret < 0) { + buffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "out"); + if (!buffersink_ctx) { av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer sink\n"); + ret = AVERROR(ENOMEM); goto end; } - ret = av_opt_set_int_list(buffersink_ctx, "sample_fmts", out_sample_fmts, -1, - AV_OPT_SEARCH_CHILDREN); + ret = av_opt_set(buffersink_ctx, "sample_formats", "s16", + AV_OPT_SEARCH_CHILDREN); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot set output sample format\n"); goto end; } - ret = av_opt_set(buffersink_ctx, "ch_layouts", "mono", - AV_OPT_SEARCH_CHILDREN); + ret = av_opt_set(buffersink_ctx, "channel_layouts", "mono", + AV_OPT_SEARCH_CHILDREN); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot set output channel layout\n"); goto end; } - ret = av_opt_set_int_list(buffersink_ctx, "sample_rates", out_sample_rates, -1, - AV_OPT_SEARCH_CHILDREN); + ret = av_opt_set_array(buffersink_ctx, "samplerates", AV_OPT_SEARCH_CHILDREN, + 0, 1, AV_OPT_TYPE_INT, &out_sample_rate); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot set output sample rate\n"); goto end; } + ret = avfilter_init_dict(buffersink_ctx, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot initialize audio buffer sink\n"); + goto end; + } + /* * Set the endpoints for the filter graph. The filter_graph will * be linked to the graph described by filters_descr. diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_video.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_video.c index b91ca56d4e..aacc91c2f7 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_video.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/decode_filter_video.c @@ -99,7 +99,6 @@ static int init_filters(const char *filters_descr) AVFilterInOut *outputs = avfilter_inout_alloc(); AVFilterInOut *inputs = avfilter_inout_alloc(); AVRational time_base = fmt_ctx->streams[video_stream_index]->time_base; - enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_GRAY8, AV_PIX_FMT_NONE }; filter_graph = avfilter_graph_alloc(); if (!outputs || !inputs || !filter_graph) { @@ -122,20 +121,26 @@ static int init_filters(const char *filters_descr) } /* buffer video sink: to terminate the filter chain. */ - ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", - NULL, NULL, filter_graph); - if (ret < 0) { + buffersink_ctx = avfilter_graph_alloc_filter(filter_graph, buffersink, "out"); + if (!buffersink_ctx) { av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n"); + ret = AVERROR(ENOMEM); goto end; } - ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts, - AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN); + ret = av_opt_set(buffersink_ctx, "pixel_formats", "gray8", + AV_OPT_SEARCH_CHILDREN); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n"); goto end; } + ret = avfilter_init_dict(buffersink_ctx, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot initialize buffer sink\n"); + goto end; + } + /* * Set the endpoints for the filter graph. The filter_graph will * be linked to the graph described by filters_descr. diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/transcode.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/transcode.c index cbe5088ef6..1dc1b50502 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/transcode.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/examples/transcode.c @@ -171,23 +171,38 @@ static int open_output_file(const char *filename) * sample rate etc.). These properties can be changed for output * streams easily using filters */ if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { + const enum AVPixelFormat *pix_fmts = NULL; + enc_ctx->height = dec_ctx->height; enc_ctx->width = dec_ctx->width; enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio; + + ret = avcodec_get_supported_config(dec_ctx, NULL, + AV_CODEC_CONFIG_PIX_FORMAT, 0, + (const void**)&pix_fmts, NULL); + /* take first format from list of supported formats */ - if (encoder->pix_fmts) - enc_ctx->pix_fmt = encoder->pix_fmts[0]; - else - enc_ctx->pix_fmt = dec_ctx->pix_fmt; + enc_ctx->pix_fmt = (ret >= 0 && pix_fmts) ? + pix_fmts[0] : dec_ctx->pix_fmt; + /* video time_base can be set to whatever is handy and supported by encoder */ enc_ctx->time_base = av_inv_q(dec_ctx->framerate); } else { + const enum AVSampleFormat *sample_fmts = NULL; + enc_ctx->sample_rate = dec_ctx->sample_rate; ret = av_channel_layout_copy(&enc_ctx->ch_layout, &dec_ctx->ch_layout); if (ret < 0) return ret; + + ret = avcodec_get_supported_config(dec_ctx, NULL, + AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, + (const void**)&sample_fmts, NULL); + /* take first format from list of supported formats */ - enc_ctx->sample_fmt = encoder->sample_fmts[0]; + enc_ctx->sample_fmt = (ret >= 0 && sample_fmts) ? + sample_fmts[0] : dec_ctx->sample_fmt; + enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate}; } @@ -283,10 +298,10 @@ static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx, goto end; } - ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", - NULL, NULL, filter_graph); - if (ret < 0) { + buffersink_ctx = avfilter_graph_alloc_filter(filter_graph, buffersink, "out"); + if (!buffersink_ctx) { av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n"); + ret = AVERROR(ENOMEM); goto end; } @@ -297,6 +312,12 @@ static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx, av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n"); goto end; } + + ret = avfilter_init_dict(buffersink_ctx, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot initialize buffer sink\n"); + goto end; + } } else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { char buf[64]; buffersrc = avfilter_get_by_name("abuffer"); @@ -322,10 +343,10 @@ static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx, goto end; } - ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", - NULL, NULL, filter_graph); - if (ret < 0) { + buffersink_ctx = avfilter_graph_alloc_filter(filter_graph, buffersink, "out"); + if (!buffersink_ctx) { av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer sink\n"); + ret = AVERROR(ENOMEM); goto end; } @@ -352,6 +373,15 @@ static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx, av_log(NULL, AV_LOG_ERROR, "Cannot set output sample rate\n"); goto end; } + + if (enc_ctx->frame_size > 0) + av_buffersink_set_frame_size(buffersink_ctx, enc_ctx->frame_size); + + ret = avfilter_init_dict(buffersink_ctx, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot initialize audio buffer sink\n"); + goto end; + } } else { ret = AVERROR_UNKNOWN; goto end; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/ffmpeg.texi b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/ffmpeg.texi index 34007f7ea2..de140067ae 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/ffmpeg.texi +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/doc/ffmpeg.texi @@ -21,22 +21,24 @@ ffmpeg [@var{global_options}] @{[@var{input_file_options}] -i @file{input_url}@} inputs - including live grabbing/recording devices - filter, and transcode them into a plethora of output formats. -@command{ffmpeg} reads from an arbitrary number of input "files" (which can be regular +@command{ffmpeg} reads from an arbitrary number of inputs (which can be regular files, pipes, network streams, grabbing devices, etc.), specified by the -@code{-i} option, and writes to an arbitrary number of output "files", which are -specified by a plain output url. Anything found on the command line which -cannot be interpreted as an option is considered to be an output url. +@code{-i} option, and writes to an arbitrary number of outputs, which are +specified by a plain output url. Anything found on the command line which cannot +be interpreted as an option is considered to be an output url. -Each input or output url can, in principle, contain any number of streams of -different types (video/audio/subtitle/attachment/data). The allowed number and/or -types of streams may be limited by the container format. Selecting which -streams from which inputs will go into which output is either done automatically -or with the @code{-map} option (see the Stream selection chapter). +Each input or output can, in principle, contain any number of elementary streams +of different types (video/audio/subtitle/attachment/data), though the allowed +stream counts and/or types may be limited by the container format. Selecting +which streams from which inputs will go into which output is either done +automatically or with the @code{-map} option (see the @ref{Stream selection} +chapter). -To refer to input files in options, you must use their indices (0-based). E.g. -the first input file is @code{0}, the second is @code{1}, etc. Similarly, streams -within a file are referred to by their indices. E.g. @code{2:3} refers to the -fourth stream in the third input file. Also see the Stream specifiers chapter. +To refer to inputs/outputs in options, you must use their indices (0-based). +E.g. the first input is @code{0}, the second is @code{1}, etc. Similarly, +streams within an input/output are referred to by their indices. E.g. @code{2:3} +refers to the fourth stream in the third input or output. Also see the +@ref{Stream specifiers} chapter. As a general rule, options are applied to the next specified file. Therefore, order is important, and you can have the same @@ -261,6 +263,7 @@ reads an input video and @c man end DETAILED DESCRIPTION +@anchor{Stream selection} @chapter Stream selection @c man begin STREAM SELECTION diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/.gitignore new file mode 100644 index 0000000000..adaf399e9f --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/.gitignore @@ -0,0 +1,7 @@ +/.config +/bin2c +/bin2c.exe +/config.fate +/config.log +/config.mak +/config.sh diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/arch.mak b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/arch.mak index 3fc40e5e5d..af71aacfd2 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/arch.mak +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/arch.mak @@ -3,6 +3,8 @@ OBJS-$(HAVE_ARMV6) += $(ARMV6-OBJS) $(ARMV6-OBJS-yes) OBJS-$(HAVE_ARMV8) += $(ARMV8-OBJS) $(ARMV8-OBJS-yes) OBJS-$(HAVE_VFP) += $(VFP-OBJS) $(VFP-OBJS-yes) OBJS-$(HAVE_NEON) += $(NEON-OBJS) $(NEON-OBJS-yes) +OBJS-$(HAVE_SVE) += $(SVE-OBJS) $(SVE-OBJS-yes) +OBJS-$(HAVE_SVE2) += $(SVE2-OBJS) $(SVE2-OBJS-yes) OBJS-$(HAVE_MIPSFPU) += $(MIPSFPU-OBJS) $(MIPSFPU-OBJS-yes) OBJS-$(HAVE_MIPSDSP) += $(MIPSDSP-OBJS) $(MIPSDSP-OBJS-yes) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/common.mak b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/common.mak index 87a3ffd2b0..023adb8567 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/common.mak +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/ffbuild/common.mak @@ -18,7 +18,7 @@ BIN2C = $(BIN2CEXE) ifndef V Q = @ ECHO = printf "$(1)\t%s\n" $(2) -BRIEF = CC CXX OBJCC HOSTCC HOSTLD AS X86ASM AR LD STRIP CP WINDRES NVCC BIN2C +BRIEF = CC CXX OBJCC HOSTCC HOSTLD AS X86ASM AR LD STRIP CP WINDRES NVCC BIN2C METALCC METALLIB SILENT = DEPCC DEPHOSTCC DEPAS DEPX86ASM RANLIB RM MSG = $@ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.c index 420ba3c6e4..dc321fb4a2 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.c @@ -728,7 +728,7 @@ static void print_stream_maps(void) av_log(NULL, AV_LOG_INFO, " (graph %d)", ost->filter->graph->index); av_log(NULL, AV_LOG_INFO, " -> Stream #%d:%d (%s)\n", ost->file->index, - ost->index, ost->enc_ctx->codec->name); + ost->index, ost->enc->enc_ctx->codec->name); continue; } @@ -737,9 +737,9 @@ static void print_stream_maps(void) ost->ist->index, ost->file->index, ost->index); - if (ost->enc_ctx) { + if (ost->enc) { const AVCodec *in_codec = ost->ist->dec; - const AVCodec *out_codec = ost->enc_ctx->codec; + const AVCodec *out_codec = ost->enc->enc_ctx->codec; const char *decoder_name = "?"; const char *in_codec_name = "?"; const char *encoder_name = "?"; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.h index 733d551fa4..9439be0f41 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg.h @@ -328,6 +328,8 @@ typedef struct OutputFilterOptions { enum AVColorRange color_range; enum VideoSyncMethod vsync_method; + AVRational frame_rate; + AVRational max_frame_rate; int sample_rate; AVChannelLayout ch_layout; @@ -461,14 +463,6 @@ typedef struct InputStream { * currently video and audio only */ InputFilter **filters; int nb_filters; - - /* - * Output targets that do not go through lavfi, i.e. subtitles or - * streamcopy. Those two cases are distinguished by the OutputStream - * having an encoder or not. - */ - struct OutputStream **outputs; - int nb_outputs; } InputStream; typedef struct InputFile { @@ -545,13 +539,6 @@ typedef struct EncStats { int lock_initialized; } EncStats; -extern const char *const forced_keyframes_const_names[]; - -typedef enum { - ENCODER_FINISHED = 1, - MUXER_FINISHED = 2, -} OSTFinished ; - enum { KF_FORCE_SOURCE = 1, #if FFMPEG_OPT_FORCE_KF_SOURCE_NO_DROP @@ -575,7 +562,15 @@ typedef struct KeyframeForceCtx { int dropped_keyframe; } KeyframeForceCtx; -typedef struct Encoder Encoder; +typedef struct Encoder { + const AVClass *class; + + AVCodecContext *enc_ctx; + + // number of frames/samples sent to the encoder + uint64_t frames_encoded; + uint64_t samples_encoded; +} Encoder; enum CroppingType { CROP_DISABLED = 0, @@ -594,12 +589,6 @@ typedef struct OutputStream { int index; /* stream index in the output file */ - /** - * Codec parameters for packets submitted to the muxer (i.e. before - * bitstream filtering, if any). - */ - AVCodecParameters *par_in; - /* input stream that is the source for this output stream; * may be NULL for streams with no well-defined source, e.g. * attachments or outputs from complex filtergraphs */ @@ -608,12 +597,8 @@ typedef struct OutputStream { AVStream *st; /* stream in the output file */ Encoder *enc; - AVCodecContext *enc_ctx; /* video only */ - AVRational frame_rate; - AVRational max_frame_rate; - int force_fps; #if FFMPEG_OPT_TOP int top_field_first; #endif @@ -636,9 +621,6 @@ typedef struct OutputStream { /* stats */ // number of packets send to the muxer atomic_uint_least64_t packets_written; - // number of frames/samples sent to the encoder - uint64_t frames_encoded; - uint64_t samples_encoded; /* packet quality factor */ atomic_int quality; @@ -762,10 +744,11 @@ int find_codec(void *logctx, const char *name, int parse_and_set_vsync(const char *arg, int *vsync_var, int file_idx, int st_idx, int is_global); int filtergraph_is_simple(const FilterGraph *fg); -int init_simple_filtergraph(InputStream *ist, OutputStream *ost, - char *graph_desc, - Scheduler *sch, unsigned sch_idx_enc, - const OutputFilterOptions *opts); +int fg_create_simple(FilterGraph **pfg, + InputStream *ist, + char *graph_desc, + Scheduler *sch, unsigned sched_idx_enc, + const OutputFilterOptions *opts); int fg_finalise_bindings(void); /** @@ -779,7 +762,7 @@ const FrameData *frame_data_c(AVFrame *frame); FrameData *packet_data (AVPacket *pkt); const FrameData *packet_data_c(AVPacket *pkt); -int ofilter_bind_ost(OutputFilter *ofilter, OutputStream *ost, +int ofilter_bind_enc(OutputFilter *ofilter, unsigned sched_idx_enc, const OutputFilterOptions *opts); @@ -858,7 +841,7 @@ int dec_request_view(Decoder *dec, const ViewSpecifier *vs, SchedulerNode *src); int enc_alloc(Encoder **penc, const AVCodec *codec, - Scheduler *sch, unsigned sch_idx); + Scheduler *sch, unsigned sch_idx, void *log_parent); void enc_free(Encoder **penc); int enc_open(void *opaque, const AVFrame *frame); @@ -871,7 +854,8 @@ int enc_loopback(Encoder *enc); * * Open the muxer once all the streams have been initialized. */ -int of_stream_init(OutputFile *of, OutputStream *ost); +int of_stream_init(OutputFile *of, OutputStream *ost, + const AVCodecContext *enc_ctx); int of_write_trailer(OutputFile *of); int of_open(const OptionsContext *o, const char *filename, Scheduler *sch); void of_free(OutputFile **pof); @@ -883,7 +867,8 @@ int64_t of_filesize(OutputFile *of); int ifile_open(const OptionsContext *o, const char *filename, Scheduler *sch); void ifile_close(InputFile **f); -int ist_output_add(InputStream *ist, OutputStream *ost); +int ist_use(InputStream *ist, int decoding_needed, + const ViewSpecifier *vs, SchedulerNode *src); int ist_filter_add(InputStream *ist, InputFilter *ifilter, int is_simple, const ViewSpecifier *vs, InputFilterOptions *opts, SchedulerNode *src); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_demux.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_demux.c index 13aef15eab..364f148f60 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_demux.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_demux.c @@ -840,7 +840,6 @@ static void ist_free(InputStream **pist) av_dict_free(&ds->decoder_opts); av_freep(&ist->filters); - av_freep(&ist->outputs); av_freep(&ds->dec_opts.hwaccel_device); avcodec_parameters_free(&ist->par); @@ -874,8 +873,8 @@ void ifile_close(InputFile **pf) av_freep(pf); } -static int ist_use(InputStream *ist, int decoding_needed, - const ViewSpecifier *vs, SchedulerNode *src) +int ist_use(InputStream *ist, int decoding_needed, + const ViewSpecifier *vs, SchedulerNode *src) { Demuxer *d = demuxer_from_ifile(ist->file); DemuxStream *ds = ds_from_ist(ist); @@ -975,25 +974,6 @@ static int ist_use(InputStream *ist, int decoding_needed, return 0; } -int ist_output_add(InputStream *ist, OutputStream *ost) -{ - DemuxStream *ds = ds_from_ist(ist); - SchedulerNode src; - int ret; - - ret = ist_use(ist, ost->enc ? DECODING_FOR_OST : 0, NULL, &src); - if (ret < 0) - return ret; - - ret = GROW_ARRAY(ist->outputs, ist->nb_outputs); - if (ret < 0) - return ret; - - ist->outputs[ist->nb_outputs - 1] = ost; - - return ost->enc ? ds->sch_idx_dec : ds->sch_idx_stream; -} - int ist_filter_add(InputStream *ist, InputFilter *ifilter, int is_simple, const ViewSpecifier *vs, InputFilterOptions *opts, SchedulerNode *src) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_enc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_enc.c index c1c8aa0e78..a46af4dce1 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_enc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_enc.c @@ -38,7 +38,12 @@ #include "libavcodec/avcodec.h" -struct Encoder { +typedef struct EncoderPriv { + Encoder e; + + void *log_parent; + char log_name[32]; + // combined size of all the packets received from the encoder uint64_t data_size; @@ -50,7 +55,12 @@ struct Encoder { Scheduler *sch; unsigned sch_idx; -}; +} EncoderPriv; + +static EncoderPriv *ep_from_enc(Encoder *enc) +{ + return (EncoderPriv*)enc; +} // data that is local to the decoder thread and not visible outside of it typedef struct EncoderThread { @@ -65,56 +75,90 @@ void enc_free(Encoder **penc) if (!enc) return; + if (enc->enc_ctx) + av_freep(&enc->enc_ctx->stats_in); + avcodec_free_context(&enc->enc_ctx); + av_freep(penc); } -int enc_alloc(Encoder **penc, const AVCodec *codec, - Scheduler *sch, unsigned sch_idx) +static const char *enc_item_name(void *obj) { - Encoder *enc; + const EncoderPriv *ep = obj; + + return ep->log_name; +} + +static const AVClass enc_class = { + .class_name = "Encoder", + .version = LIBAVUTIL_VERSION_INT, + .parent_log_context_offset = offsetof(EncoderPriv, log_parent), + .item_name = enc_item_name, +}; + +int enc_alloc(Encoder **penc, const AVCodec *codec, + Scheduler *sch, unsigned sch_idx, void *log_parent) +{ + EncoderPriv *ep; + int ret = 0; *penc = NULL; - enc = av_mallocz(sizeof(*enc)); - if (!enc) + ep = av_mallocz(sizeof(*ep)); + if (!ep) return AVERROR(ENOMEM); - enc->sch = sch; - enc->sch_idx = sch_idx; + ep->e.class = &enc_class; + ep->log_parent = log_parent; - *penc = enc; + ep->sch = sch; + ep->sch_idx = sch_idx; + + snprintf(ep->log_name, sizeof(ep->log_name), "enc:%s", codec->name); + + ep->e.enc_ctx = avcodec_alloc_context3(codec); + if (!ep->e.enc_ctx) { + ret = AVERROR(ENOMEM); + goto fail; + } + + *penc = &ep->e; return 0; +fail: + enc_free((Encoder**)&ep); + return ret; } -static int hw_device_setup_for_encode(OutputStream *ost, AVBufferRef *frames_ref) +static int hw_device_setup_for_encode(Encoder *e, AVCodecContext *enc_ctx, + AVBufferRef *frames_ref) { const AVCodecHWConfig *config; HWDevice *dev = NULL; if (frames_ref && ((AVHWFramesContext*)frames_ref->data)->format == - ost->enc_ctx->pix_fmt) { + enc_ctx->pix_fmt) { // Matching format, will try to use hw_frames_ctx. } else { frames_ref = NULL; } for (int i = 0;; i++) { - config = avcodec_get_hw_config(ost->enc_ctx->codec, i); + config = avcodec_get_hw_config(enc_ctx->codec, i); if (!config) break; if (frames_ref && config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX && (config->pix_fmt == AV_PIX_FMT_NONE || - config->pix_fmt == ost->enc_ctx->pix_fmt)) { - av_log(ost->enc_ctx, AV_LOG_VERBOSE, "Using input " + config->pix_fmt == enc_ctx->pix_fmt)) { + av_log(e, AV_LOG_VERBOSE, "Using input " "frames context (format %s) with %s encoder.\n", - av_get_pix_fmt_name(ost->enc_ctx->pix_fmt), - ost->enc_ctx->codec->name); - ost->enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ref); - if (!ost->enc_ctx->hw_frames_ctx) + av_get_pix_fmt_name(enc_ctx->pix_fmt), + enc_ctx->codec->name); + enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ref); + if (!enc_ctx->hw_frames_ctx) return AVERROR(ENOMEM); return 0; } @@ -125,11 +169,11 @@ static int hw_device_setup_for_encode(OutputStream *ost, AVBufferRef *frames_ref } if (dev) { - av_log(ost->enc_ctx, AV_LOG_VERBOSE, "Using device %s " + av_log(e, AV_LOG_VERBOSE, "Using device %s " "(type %s) with %s encoder.\n", dev->name, - av_hwdevice_get_type_name(dev->type), ost->enc_ctx->codec->name); - ost->enc_ctx->hw_device_ctx = av_buffer_ref(dev->device_ref); - if (!ost->enc_ctx->hw_device_ctx) + av_hwdevice_get_type_name(dev->type), enc_ctx->codec->name); + enc_ctx->hw_device_ctx = av_buffer_ref(dev->device_ref); + if (!enc_ctx->hw_device_ctx) return AVERROR(ENOMEM); } else { // No device required, or no device available. @@ -137,37 +181,13 @@ static int hw_device_setup_for_encode(OutputStream *ost, AVBufferRef *frames_ref return 0; } -static int set_encoder_id(OutputFile *of, OutputStream *ost) -{ - const char *cname = ost->enc_ctx->codec->name; - uint8_t *encoder_string; - int encoder_string_len; - - if (av_dict_get(ost->st->metadata, "encoder", NULL, 0)) - return 0; - - encoder_string_len = sizeof(LIBAVCODEC_IDENT) + strlen(cname) + 2; - encoder_string = av_mallocz(encoder_string_len); - if (!encoder_string) - return AVERROR(ENOMEM); - - if (!of->bitexact && !ost->bitexact) - av_strlcpy(encoder_string, LIBAVCODEC_IDENT " ", encoder_string_len); - else - av_strlcpy(encoder_string, "Lavc ", encoder_string_len); - av_strlcat(encoder_string, cname, encoder_string_len); - av_dict_set(&ost->st->metadata, "encoder", encoder_string, - AV_DICT_DONT_STRDUP_VAL | AV_DICT_DONT_OVERWRITE); - - return 0; -} - int enc_open(void *opaque, const AVFrame *frame) { OutputStream *ost = opaque; InputStream *ist = ost->ist; Encoder *e = ost->enc; - AVCodecContext *enc_ctx = ost->enc_ctx; + EncoderPriv *ep = ep_from_enc(e); + AVCodecContext *enc_ctx = e->enc_ctx; Decoder *dec = NULL; const AVCodec *enc = enc_ctx->codec; OutputFile *of = ost->file; @@ -175,7 +195,7 @@ int enc_open(void *opaque, const AVFrame *frame) int frame_samples = 0; int ret; - if (e->opened) + if (ep->opened) return 0; // frame is always non-NULL for audio and video @@ -200,10 +220,6 @@ int enc_open(void *opaque, const AVFrame *frame) } } - ret = set_encoder_id(of, ost); - if (ret < 0) - return ret; - if (ist) dec = ist->decoder; @@ -211,7 +227,6 @@ int enc_open(void *opaque, const AVFrame *frame) if (ost->type == AVMEDIA_TYPE_AUDIO || ost->type == AVMEDIA_TYPE_VIDEO) { enc_ctx->time_base = frame->time_base; enc_ctx->framerate = fd->frame_rate_filter; - ost->st->avg_frame_rate = fd->frame_rate_filter; } switch (enc_ctx->codec_type) { @@ -238,7 +253,7 @@ int enc_open(void *opaque, const AVFrame *frame) frame->height > 0); enc_ctx->width = frame->width; enc_ctx->height = frame->height; - enc_ctx->sample_aspect_ratio = ost->st->sample_aspect_ratio = + enc_ctx->sample_aspect_ratio = ost->frame_aspect_ratio.num ? // overridden by the -aspect cli option av_mul_q(ost->frame_aspect_ratio, (AVRational){ enc_ctx->height, enc_ctx->width }) : frame->sample_aspect_ratio; @@ -312,42 +327,31 @@ int enc_open(void *opaque, const AVFrame *frame) enc_ctx->flags |= AV_CODEC_FLAG_FRAME_DURATION; - ret = hw_device_setup_for_encode(ost, frame ? frame->hw_frames_ctx : NULL); + ret = hw_device_setup_for_encode(e, enc_ctx, frame ? frame->hw_frames_ctx : NULL); if (ret < 0) { - av_log(ost, AV_LOG_ERROR, + av_log(e, AV_LOG_ERROR, "Encoding hardware device setup failed: %s\n", av_err2str(ret)); return ret; } - if ((ret = avcodec_open2(ost->enc_ctx, enc, NULL)) < 0) { + if ((ret = avcodec_open2(enc_ctx, enc, NULL)) < 0) { if (ret != AVERROR_EXPERIMENTAL) - av_log(ost, AV_LOG_ERROR, "Error while opening encoder - maybe " + av_log(e, AV_LOG_ERROR, "Error while opening encoder - maybe " "incorrect parameters such as bit_rate, rate, width or height.\n"); return ret; } - e->opened = 1; + ep->opened = 1; - if (ost->enc_ctx->frame_size) - frame_samples = ost->enc_ctx->frame_size; + if (enc_ctx->frame_size) + frame_samples = enc_ctx->frame_size; - if (ost->enc_ctx->bit_rate && ost->enc_ctx->bit_rate < 1000 && - ost->enc_ctx->codec_id != AV_CODEC_ID_CODEC2 /* don't complain about 700 bit/s modes */) - av_log(ost, AV_LOG_WARNING, "The bitrate parameter is set too low." + if (enc_ctx->bit_rate && enc_ctx->bit_rate < 1000 && + enc_ctx->codec_id != AV_CODEC_ID_CODEC2 /* don't complain about 700 bit/s modes */) + av_log(e, AV_LOG_WARNING, "The bitrate parameter is set too low." " It takes bits/s as argument, not kbits/s\n"); - ret = avcodec_parameters_from_context(ost->par_in, ost->enc_ctx); - if (ret < 0) { - av_log(ost, AV_LOG_FATAL, - "Error initializing the output stream codec context.\n"); - return ret; - } - - // copy timebase while removing common factors - if (ost->st->time_base.num <= 0 || ost->st->time_base.den <= 0) - ost->st->time_base = av_add_q(ost->enc_ctx->time_base, (AVRational){0, 1}); - - ret = of_stream_init(of, ost); + ret = of_stream_init(of, ost, enc_ctx); if (ret < 0) return ret; @@ -369,19 +373,20 @@ static int do_subtitle_out(OutputFile *of, OutputStream *ost, const AVSubtitle * AVPacket *pkt) { Encoder *e = ost->enc; + EncoderPriv *ep = ep_from_enc(e); int subtitle_out_max_size = 1024 * 1024; int subtitle_out_size, nb, i, ret; AVCodecContext *enc; int64_t pts; if (sub->pts == AV_NOPTS_VALUE) { - av_log(ost, AV_LOG_ERROR, "Subtitle packets must have a pts\n"); + av_log(e, AV_LOG_ERROR, "Subtitle packets must have a pts\n"); return exit_on_error ? AVERROR(EINVAL) : 0; } if ((of->start_time != AV_NOPTS_VALUE && sub->pts < of->start_time)) return 0; - enc = ost->enc_ctx; + enc = e->enc_ctx; /* Note: DVB subtitle need one packet to draw them and one other packet to clear them */ @@ -420,11 +425,11 @@ static int do_subtitle_out(OutputFile *of, OutputStream *ost, const AVSubtitle * local_sub.rects += i; } - ost->frames_encoded++; + e->frames_encoded++; subtitle_out_size = avcodec_encode_subtitle(enc, pkt->data, pkt->size, &local_sub); if (subtitle_out_size < 0) { - av_log(ost, AV_LOG_FATAL, "Subtitle encoding failed\n"); + av_log(e, AV_LOG_FATAL, "Subtitle encoding failed\n"); return subtitle_out_size; } @@ -442,7 +447,7 @@ static int do_subtitle_out(OutputFile *of, OutputStream *ost, const AVSubtitle * } pkt->dts = pkt->pts; - ret = sch_enc_send(e->sch, e->sch_idx, pkt); + ret = sch_enc_send(ep->sch, ep->sch_idx, pkt); if (ret < 0) { av_packet_unref(pkt); return ret; @@ -457,6 +462,7 @@ void enc_stats_write(OutputStream *ost, EncStats *es, uint64_t frame_num) { Encoder *e = ost->enc; + EncoderPriv *ep = ep_from_enc(e); AVIOContext *io = es->io; AVRational tb = frame ? frame->time_base : pkt->time_base; int64_t pts = frame ? frame->pts : pkt->pts; @@ -494,7 +500,7 @@ void enc_stats_write(OutputStream *ost, EncStats *es, if (frame) { switch (c->type) { - case ENC_STATS_SAMPLE_NUM: avio_printf(io, "%"PRIu64, ost->samples_encoded); continue; + case ENC_STATS_SAMPLE_NUM: avio_printf(io, "%"PRIu64, e->samples_encoded); continue; case ENC_STATS_NB_SAMPLES: avio_printf(io, "%d", frame->nb_samples); continue; default: av_assert0(0); } @@ -512,7 +518,7 @@ void enc_stats_write(OutputStream *ost, EncStats *es, } case ENC_STATS_AVG_BITRATE: { double duration = pkt->dts * av_q2d(tb); - avio_printf(io, "%g", duration > 0 ? 8.0 * e->data_size / duration : -1.); + avio_printf(io, "%g", duration > 0 ? 8.0 * ep->data_size / duration : -1.); continue; } default: av_assert0(0); @@ -533,9 +539,10 @@ static inline double psnr(double d) static int update_video_stats(OutputStream *ost, const AVPacket *pkt, int write_vstats) { Encoder *e = ost->enc; + EncoderPriv *ep = ep_from_enc(e); const uint8_t *sd = av_packet_get_side_data(pkt, AV_PKT_DATA_QUALITY_STATS, NULL); - AVCodecContext *enc = ost->enc_ctx; + AVCodecContext *enc = e->enc_ctx; enum AVPictureType pict_type; int64_t frame_number; double ti1, bitrate, avg_bitrate; @@ -566,7 +573,7 @@ static int update_video_stats(OutputStream *ost, const AVPacket *pkt, int write_ } } - frame_number = e->packets_encoded; + frame_number = ep->packets_encoded; if (vstats_version <= 1) { fprintf(vstats_file, "frame= %5"PRId64" q= %2.1f ", frame_number, quality / (float)FF_QP2LAMBDA); @@ -586,9 +593,9 @@ static int update_video_stats(OutputStream *ost, const AVPacket *pkt, int write_ ti1 = 0.01; bitrate = (pkt->size * 8) / av_q2d(enc->time_base) / 1000.0; - avg_bitrate = (double)(e->data_size * 8) / ti1 / 1000.0; + avg_bitrate = (double)(ep->data_size * 8) / ti1 / 1000.0; fprintf(vstats_file, "s_size= %8.0fKiB time= %0.3f br= %7.1fkbits/s avg_br= %7.1fkbits/s ", - (double)e->data_size / 1024, ti1, bitrate, avg_bitrate); + (double)ep->data_size / 1024, ti1, bitrate, avg_bitrate); fprintf(vstats_file, "type= %c\n", av_get_picture_type_char(pict_type)); return 0; @@ -598,7 +605,8 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, AVPacket *pkt) { Encoder *e = ost->enc; - AVCodecContext *enc = ost->enc_ctx; + EncoderPriv *ep = ep_from_enc(e); + AVCodecContext *enc = e->enc_ctx; const char *type_desc = av_get_media_type_string(enc->codec_type); const char *action = frame ? "encode" : "flush"; int ret; @@ -613,13 +621,13 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, if (ost->enc_stats_pre.io) enc_stats_write(ost, &ost->enc_stats_pre, frame, NULL, - ost->frames_encoded); + e->frames_encoded); - ost->frames_encoded++; - ost->samples_encoded += frame->nb_samples; + e->frames_encoded++; + e->samples_encoded += frame->nb_samples; if (debug_ts) { - av_log(ost, AV_LOG_INFO, "encoder <- type:%s " + av_log(e, AV_LOG_INFO, "encoder <- type:%s " "frame_pts:%s frame_pts_time:%s time_base:%d/%d\n", type_desc, av_ts2str(frame->pts), av_ts2timestr(frame->pts, &enc->time_base), @@ -634,7 +642,7 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, ret = avcodec_send_frame(enc, frame); if (ret < 0 && !(ret == AVERROR_EOF && !frame)) { - av_log(ost, AV_LOG_ERROR, "Error submitting %s frame to the encoder\n", + av_log(e, AV_LOG_ERROR, "Error submitting %s frame to the encoder\n", type_desc); return ret; } @@ -659,7 +667,7 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, return 0; } else if (ret < 0) { if (ret != AVERROR_EOF) - av_log(ost, AV_LOG_ERROR, "%s encoding failed\n", type_desc); + av_log(e, AV_LOG_ERROR, "%s encoding failed\n", type_desc); return ret; } @@ -670,7 +678,7 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, // attach stream parameters to first packet if requested avcodec_parameters_free(&fd->par_enc); - if (e->attach_par && !e->packets_encoded) { + if (ep->attach_par && !ep->packets_encoded) { fd->par_enc = avcodec_parameters_alloc(); if (!fd->par_enc) return AVERROR(ENOMEM); @@ -690,10 +698,10 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, if (ost->enc_stats_post.io) enc_stats_write(ost, &ost->enc_stats_post, NULL, pkt, - e->packets_encoded); + ep->packets_encoded); if (debug_ts) { - av_log(ost, AV_LOG_INFO, "encoder -> type:%s " + av_log(e, AV_LOG_INFO, "encoder -> type:%s " "pkt_pts:%s pkt_pts_time:%s pkt_dts:%s pkt_dts_time:%s " "duration:%s duration_time:%s\n", type_desc, @@ -702,11 +710,11 @@ static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame, av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, &enc->time_base)); } - e->data_size += pkt->size; + ep->data_size += pkt->size; - e->packets_encoded++; + ep->packets_encoded++; - ret = sch_enc_send(e->sch, e->sch_idx, pkt); + ret = sch_enc_send(ep->sch, ep->sch_idx, pkt); if (ret < 0) { av_packet_unref(pkt); return ret; @@ -764,6 +772,7 @@ force_keyframe: static int frame_encode(OutputStream *ost, AVFrame *frame, AVPacket *pkt) { + Encoder *e = ost->enc; OutputFile *of = ost->file; enum AVMediaType type = ost->type; @@ -781,8 +790,8 @@ static int frame_encode(OutputStream *ost, AVFrame *frame, AVPacket *pkt) return AVERROR_EOF; if (type == AVMEDIA_TYPE_VIDEO) { - frame->quality = ost->enc_ctx->global_quality; - frame->pict_type = forced_kf_apply(ost, &ost->kf, frame); + frame->quality = e->enc_ctx->global_quality; + frame->pict_type = forced_kf_apply(e, &ost->kf, frame); #if FFMPEG_OPT_TOP if (ost->top_field_first >= 0) { @@ -791,9 +800,9 @@ static int frame_encode(OutputStream *ost, AVFrame *frame, AVPacket *pkt) } #endif } else { - if (!(ost->enc_ctx->codec->capabilities & AV_CODEC_CAP_PARAM_CHANGE) && - ost->enc_ctx->ch_layout.nb_channels != frame->ch_layout.nb_channels) { - av_log(ost, AV_LOG_ERROR, + if (!(e->enc_ctx->codec->capabilities & AV_CODEC_CAP_PARAM_CHANGE) && + e->enc_ctx->ch_layout.nb_channels != frame->ch_layout.nb_channels) { + av_log(e, AV_LOG_ERROR, "Audio channel count changed and encoder does not support parameter changes\n"); return 0; } @@ -807,7 +816,7 @@ static void enc_thread_set_name(const OutputStream *ost) { char name[16]; snprintf(name, sizeof(name), "enc%d:%d:%s", ost->file->index, ost->index, - ost->enc_ctx->codec->name); + ost->enc->enc_ctx->codec->name); ff_thread_setname(name); } @@ -842,6 +851,7 @@ int encoder_thread(void *arg) { OutputStream *ost = arg; Encoder *e = ost->enc; + EncoderPriv *ep = ep_from_enc(e); EncoderThread et; int ret = 0, input_status = 0; int name_set = 0; @@ -864,17 +874,17 @@ int encoder_thread(void *arg) } while (!input_status) { - input_status = sch_enc_receive(e->sch, e->sch_idx, et.frame); + input_status = sch_enc_receive(ep->sch, ep->sch_idx, et.frame); if (input_status < 0) { if (input_status == AVERROR_EOF) { - av_log(ost, AV_LOG_VERBOSE, "Encoder thread received EOF\n"); - if (e->opened) + av_log(e, AV_LOG_VERBOSE, "Encoder thread received EOF\n"); + if (ep->opened) break; - av_log(ost, AV_LOG_ERROR, "Could not open encoder before EOF\n"); + av_log(e, AV_LOG_ERROR, "Could not open encoder before EOF\n"); ret = AVERROR(EINVAL); } else { - av_log(ost, AV_LOG_ERROR, "Error receiving a frame for encoding: %s\n", + av_log(e, AV_LOG_ERROR, "Error receiving a frame for encoding: %s\n", av_err2str(ret)); ret = input_status; } @@ -893,9 +903,9 @@ int encoder_thread(void *arg) if (ret < 0) { if (ret == AVERROR_EOF) - av_log(ost, AV_LOG_VERBOSE, "Encoder returned EOF, finishing\n"); + av_log(e, AV_LOG_VERBOSE, "Encoder returned EOF, finishing\n"); else - av_log(ost, AV_LOG_ERROR, "Error encoding a frame: %s\n", + av_log(e, AV_LOG_ERROR, "Error encoding a frame: %s\n", av_err2str(ret)); break; } @@ -905,7 +915,7 @@ int encoder_thread(void *arg) if (ret == 0 || ret == AVERROR_EOF) { ret = frame_encode(ost, NULL, et.pkt); if (ret < 0 && ret != AVERROR_EOF) - av_log(ost, AV_LOG_ERROR, "Error flushing encoder: %s\n", + av_log(e, AV_LOG_ERROR, "Error flushing encoder: %s\n", av_err2str(ret)); } @@ -921,6 +931,7 @@ finish: int enc_loopback(Encoder *enc) { - enc->attach_par = 1; - return enc->sch_idx; + EncoderPriv *ep = ep_from_enc(enc); + ep->attach_par = 1; + return ep->sch_idx; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_filter.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_filter.c index 7ec328e04e..2ff7c4c4f4 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_filter.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_filter.c @@ -783,8 +783,7 @@ static int set_channel_layout(OutputFilterPriv *f, const AVChannelLayout *layout return 0; } -int ofilter_bind_ost(OutputFilter *ofilter, OutputStream *ost, - unsigned sched_idx_enc, +int ofilter_bind_enc(OutputFilter *ofilter, unsigned sched_idx_enc, const OutputFilterOptions *opts) { OutputFilterPriv *ofp = ofp_from_ofilter(ofilter); @@ -793,7 +792,8 @@ int ofilter_bind_ost(OutputFilter *ofilter, OutputStream *ost, int ret; av_assert0(!ofilter->bound); - av_assert0(ofilter->type == ost->type); + av_assert0(!opts->enc || + ofilter->type == opts->enc->type); ofilter->bound = 1; av_freep(&ofilter->linklabel); @@ -854,10 +854,9 @@ int ofilter_bind_ost(OutputFilter *ofilter, OutputStream *ost, return AVERROR(ENOMEM); ofp->fps.vsync_method = opts->vsync_method; - ofp->fps.framerate = ost->frame_rate; - ofp->fps.framerate_max = ost->max_frame_rate; - ofp->fps.framerate_supported = ost->force_fps || !opts->enc ? - NULL : opts->frame_rates; + ofp->fps.framerate = opts->frame_rate; + ofp->fps.framerate_max = opts->max_frame_rate; + ofp->fps.framerate_supported = opts->frame_rates; // reduce frame rate for mpeg4 to be within the spec limits if (opts->enc && opts->enc->id == AV_CODEC_ID_MPEG4) @@ -1181,25 +1180,27 @@ fail: return 0; } -int init_simple_filtergraph(InputStream *ist, OutputStream *ost, - char *graph_desc, - Scheduler *sch, unsigned sched_idx_enc, - const OutputFilterOptions *opts) +int fg_create_simple(FilterGraph **pfg, + InputStream *ist, + char *graph_desc, + Scheduler *sch, unsigned sched_idx_enc, + const OutputFilterOptions *opts) { + const enum AVMediaType type = ist->par->codec_type; FilterGraph *fg; FilterGraphPriv *fgp; int ret; - ret = fg_create(&ost->fg_simple, graph_desc, sch); + ret = fg_create(pfg, graph_desc, sch); if (ret < 0) return ret; - fg = ost->fg_simple; + fg = *pfg; fgp = fgp_from_fg(fg); fgp->is_simple = 1; snprintf(fgp->log_name, sizeof(fgp->log_name), "%cf%s", - av_get_media_type_string(ost->type)[0], opts->name); + av_get_media_type_string(type)[0], opts->name); if (fg->nb_inputs != 1 || fg->nb_outputs != 1) { av_log(fg, AV_LOG_ERROR, "Simple filtergraph '%s' was expected " @@ -1209,21 +1210,19 @@ int init_simple_filtergraph(InputStream *ist, OutputStream *ost, graph_desc, fg->nb_inputs, fg->nb_outputs); return AVERROR(EINVAL); } - if (fg->outputs[0]->type != ost->type) { + if (fg->outputs[0]->type != type) { av_log(fg, AV_LOG_ERROR, "Filtergraph has a %s output, cannot connect " "it to %s output stream\n", av_get_media_type_string(fg->outputs[0]->type), - av_get_media_type_string(ost->type)); + av_get_media_type_string(type)); return AVERROR(EINVAL); } - ost->filter = fg->outputs[0]; - ret = ifilter_bind_ist(fg->inputs[0], ist, opts->vs); if (ret < 0) return ret; - ret = ofilter_bind_ost(fg->outputs[0], ost, sched_idx_enc, opts); + ret = ofilter_bind_enc(fg->outputs[0], sched_idx_enc, opts); if (ret < 0) return ret; @@ -1351,8 +1350,10 @@ static int fg_complex_bind_input(FilterGraph *fg, InputFilter *ifilter) } else { ist = ist_find_unused(type); if (!ist) { - av_log(fg, AV_LOG_FATAL, "Cannot find a matching stream for " - "unlabeled input pad %s\n", ifilter->name); + av_log(fg, AV_LOG_FATAL, + "Cannot find an unused %s input stream to feed the " + "unlabeled input pad %s.\n", + av_get_media_type_string(type), ifilter->name); return AVERROR(EINVAL); } @@ -1585,14 +1586,18 @@ static int configure_output_audio_filter(FilterGraph *fg, AVFilterGraph *graph, int ret; snprintf(name, sizeof(name), "out_%s", ofp->name); - ret = avfilter_graph_create_filter(&ofp->filter, - avfilter_get_by_name("abuffersink"), - name, NULL, NULL, graph); - if (ret < 0) - return ret; + ofp->filter = avfilter_graph_alloc_filter(graph, + avfilter_get_by_name("abuffersink"), + name); + if (!ofp->filter) + return AVERROR(ENOMEM); if ((ret = av_opt_set_int(ofp->filter, "all_channel_counts", 1, AV_OPT_SEARCH_CHILDREN)) < 0) return ret; + ret = avfilter_init_dict(ofp->filter, NULL); + if (ret < 0) + return ret; + #define AUTO_INSERT_FILTER(opt_name, filter_name, arg) do { \ AVFilterContext *filt_ctx; \ \ @@ -1685,9 +1690,6 @@ static int configure_input_video_filter(FilterGraph *fg, AVFilterGraph *graph, AVFilterContext *last_filter; const AVFilter *buffer_filt = avfilter_get_by_name("buffer"); const AVPixFmtDescriptor *desc; - AVRational fr = ifp->opts.framerate; - AVRational sar; - AVBPrint args; char name[255]; int ret, pad_idx = 0; AVBufferSrcParameters *par = av_buffersrc_parameters_alloc(); @@ -1697,30 +1699,34 @@ static int configure_input_video_filter(FilterGraph *fg, AVFilterGraph *graph, if (ifp->type_src == AVMEDIA_TYPE_SUBTITLE) sub2video_prepare(ifp); - sar = ifp->sample_aspect_ratio; - if(!sar.den) - sar = (AVRational){0,1}; - av_bprint_init(&args, 0, AV_BPRINT_SIZE_AUTOMATIC); - av_bprintf(&args, - "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:" - "pixel_aspect=%d/%d:colorspace=%d:range=%d", - ifp->width, ifp->height, ifp->format, - ifp->time_base.num, ifp->time_base.den, sar.num, sar.den, - ifp->color_space, ifp->color_range); - if (fr.num && fr.den) - av_bprintf(&args, ":frame_rate=%d/%d", fr.num, fr.den); snprintf(name, sizeof(name), "graph %d input from stream %s", fg->index, ifp->opts.name); - - if ((ret = avfilter_graph_create_filter(&ifp->filter, buffer_filt, name, - args.str, NULL, graph)) < 0) + ifp->filter = avfilter_graph_alloc_filter(graph, buffer_filt, name); + if (!ifp->filter) { + ret = AVERROR(ENOMEM); goto fail; - par->hw_frames_ctx = ifp->hw_frames_ctx; + } + + par->format = ifp->format; + par->time_base = ifp->time_base; + par->frame_rate = ifp->opts.framerate; + par->width = ifp->width; + par->height = ifp->height; + par->sample_aspect_ratio = ifp->sample_aspect_ratio.den > 0 ? + ifp->sample_aspect_ratio : (AVRational){ 0, 1 }; + par->color_space = ifp->color_space; + par->color_range = ifp->color_range; + par->hw_frames_ctx = ifp->hw_frames_ctx; ret = av_buffersrc_parameters_set(ifp->filter, par); if (ret < 0) goto fail; av_freep(&par); + + ret = avfilter_init_dict(ifp->filter, NULL); + if (ret < 0) + goto fail; + last_filter = ifp->filter; desc = av_pix_fmt_desc_get(ifp->format); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.c index 71ff9b45ab..25f66dd185 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.c @@ -581,9 +581,9 @@ static int bsf_init(MuxStream *ms) int ret; if (!ctx) - return avcodec_parameters_copy(ost->st->codecpar, ost->par_in); + return avcodec_parameters_copy(ost->st->codecpar, ms->par_in); - ret = avcodec_parameters_copy(ctx->par_in, ost->par_in); + ret = avcodec_parameters_copy(ctx->par_in, ms->par_in); if (ret < 0) return ret; @@ -608,12 +608,29 @@ static int bsf_init(MuxStream *ms) return 0; } -int of_stream_init(OutputFile *of, OutputStream *ost) +int of_stream_init(OutputFile *of, OutputStream *ost, + const AVCodecContext *enc_ctx) { Muxer *mux = mux_from_of(of); MuxStream *ms = ms_from_ost(ost); int ret; + if (enc_ctx) { + // use upstream time base unless it has been overridden previously + if (ost->st->time_base.num <= 0 || ost->st->time_base.den <= 0) + ost->st->time_base = av_add_q(enc_ctx->time_base, (AVRational){0, 1}); + + ost->st->avg_frame_rate = enc_ctx->framerate; + ost->st->sample_aspect_ratio = enc_ctx->sample_aspect_ratio; + + ret = avcodec_parameters_from_context(ms->par_in, enc_ctx); + if (ret < 0) { + av_log(ost, AV_LOG_FATAL, + "Error initializing the output stream codec parameters.\n"); + return ret; + } + } + /* initialize bitstream filters for the output stream * needs to be done here, because the codec id for streamcopy is not * known until now */ @@ -644,8 +661,8 @@ static int check_written(OutputFile *of) total_packets_written += packets_written; - if (ost->enc_ctx && - (ost->enc_ctx->flags & (AV_CODEC_FLAG_PASS1 | AV_CODEC_FLAG_PASS2)) + if (ost->enc && + (ost->enc->enc_ctx->flags & (AV_CODEC_FLAG_PASS1 | AV_CODEC_FLAG_PASS2)) != AV_CODEC_FLAG_PASS1) pass1_used = 0; @@ -706,9 +723,9 @@ static void mux_final_stats(Muxer *mux) of->index, j, av_get_media_type_string(type)); if (ost->enc) { av_log(of, AV_LOG_VERBOSE, "%"PRIu64" frames encoded", - ost->frames_encoded); + ost->enc->frames_encoded); if (type == AVMEDIA_TYPE_AUDIO) - av_log(of, AV_LOG_VERBOSE, " (%"PRIu64" samples)", ost->samples_encoded); + av_log(of, AV_LOG_VERBOSE, " (%"PRIu64" samples)", ost->enc->samples_encoded); av_log(of, AV_LOG_VERBOSE, "; "); } @@ -806,7 +823,7 @@ static void ost_free(OutputStream **post) ost->logfile = NULL; } - avcodec_parameters_free(&ost->par_in); + avcodec_parameters_free(&ms->par_in); av_bsf_free(&ms->bsf_ctx); av_packet_free(&ms->bsf_pkt); @@ -820,10 +837,6 @@ static void ost_free(OutputStream **post) av_freep(&ost->attachment_filename); - if (ost->enc_ctx) - av_freep(&ost->enc_ctx->stats_in); - avcodec_free_context(&ost->enc_ctx); - enc_stats_uninit(&ost->enc_stats_pre); enc_stats_uninit(&ost->enc_stats_post); enc_stats_uninit(&ms->stats); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.h index 22d728a919..f41f2c18fa 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux.h @@ -36,6 +36,12 @@ typedef struct MuxStream { OutputStream ost; + /** + * Codec parameters for packets submitted to the muxer (i.e. before + * bitstream filtering, if any). + */ + AVCodecParameters *par_in; + // name used for logging char log_name[32]; @@ -79,6 +85,10 @@ typedef struct MuxStream { int ts_drop; #endif + AVRational frame_rate; + AVRational max_frame_rate; + int force_fps; + const char *apad; } MuxStream; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux_init.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux_init.c index 8afb018de8..944176ca5d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux_init.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffmpeg_mux_init.c @@ -67,8 +67,9 @@ static int check_opt_bitexact(void *ctx, const AVDictionary *opts, } static int choose_encoder(const OptionsContext *o, AVFormatContext *s, - OutputStream *ost, const AVCodec **enc) + MuxStream *ms, const AVCodec **enc) { + OutputStream *ost = &ms->ost; enum AVMediaType type = ost->type; const char *codec_name = NULL; @@ -90,20 +91,20 @@ static int choose_encoder(const OptionsContext *o, AVFormatContext *s, } if (!codec_name) { - ost->par_in->codec_id = av_guess_codec(s->oformat, NULL, s->url, NULL, ost->type); - *enc = avcodec_find_encoder(ost->par_in->codec_id); + ms->par_in->codec_id = av_guess_codec(s->oformat, NULL, s->url, NULL, ost->type); + *enc = avcodec_find_encoder(ms->par_in->codec_id); if (!*enc) { av_log(ost, AV_LOG_FATAL, "Automatic encoder selection failed " "Default encoder for format %s (codec %s) is " "probably disabled. Please choose an encoder manually.\n", - s->oformat->name, avcodec_get_name(ost->par_in->codec_id)); + s->oformat->name, avcodec_get_name(ms->par_in->codec_id)); return AVERROR_ENCODER_NOT_FOUND; } } else if (strcmp(codec_name, "copy")) { int ret = find_codec(ost, codec_name, ost->type, 1, enc); if (ret < 0) return ret; - ost->par_in->codec_id = (*enc)->id; + ms->par_in->codec_id = (*enc)->id; } return 0; @@ -423,27 +424,6 @@ static int ost_get_filters(const OptionsContext *o, AVFormatContext *oc, #endif opt_match_per_stream_str(ost, &o->filters, oc, ost->st, &filters); - if (!ost->enc) { - if ( -#if FFMPEG_OPT_FILTER_SCRIPT - filters_script || -#endif - filters) { - av_log(ost, AV_LOG_ERROR, - "%s '%s' was specified, but codec copy was selected. " - "Filtering and streamcopy cannot be used together.\n", -#if FFMPEG_OPT_FILTER_SCRIPT - filters ? "Filtergraph" : "Filtergraph script", - filters ? filters : filters_script -#else - "Filtergraph", filters -#endif - ); - return AVERROR(ENOSYS); - } - return 0; - } - if (!ost->ist) { if ( #if FFMPEG_OPT_FILTER_SCRIPT @@ -554,7 +534,7 @@ static enum AVPixelFormat pix_fmt_parse(OutputStream *ost, const char *name) return AV_PIX_FMT_NONE; } - ret = avcodec_get_supported_config(ost->enc_ctx, NULL, AV_CODEC_CONFIG_PIX_FORMAT, + ret = avcodec_get_supported_config(ost->enc->enc_ctx, NULL, AV_CODEC_CONFIG_PIX_FORMAT, 0, (const void **) &fmts, NULL); if (ret < 0) return AV_PIX_FMT_NONE; @@ -586,7 +566,7 @@ static enum AVPixelFormat pix_fmt_parse(OutputStream *ost, const char *name) } if (fmts && !fmt_in_list(fmts, fmt)) - fmt = choose_pixel_fmt(ost->enc_ctx, fmt); + fmt = choose_pixel_fmt(ost->enc->enc_ctx, fmt); return fmt; } @@ -604,13 +584,13 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, st = ost->st; opt_match_per_stream_str(ost, &o->frame_rates, oc, st, &frame_rate); - if (frame_rate && av_parse_video_rate(&ost->frame_rate, frame_rate) < 0) { + if (frame_rate && av_parse_video_rate(&ms->frame_rate, frame_rate) < 0) { av_log(ost, AV_LOG_FATAL, "Invalid framerate value: %s\n", frame_rate); return AVERROR(EINVAL); } opt_match_per_stream_str(ost, &o->max_frame_rates, oc, st, &max_frame_rate); - if (max_frame_rate && av_parse_video_rate(&ost->max_frame_rate, max_frame_rate) < 0) { + if (max_frame_rate && av_parse_video_rate(&ms->max_frame_rate, max_frame_rate) < 0) { av_log(ost, AV_LOG_FATAL, "Invalid maximum framerate value: %s\n", max_frame_rate); return AVERROR(EINVAL); } @@ -631,8 +611,8 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, ost->frame_aspect_ratio = q; } - if (ost->enc_ctx) { - AVCodecContext *video_enc = ost->enc_ctx; + if (ost->enc) { + AVCodecContext *video_enc = ost->enc->enc_ctx; const char *p = NULL, *fps_mode = NULL; const char *frame_size = NULL; const char *frame_pix_fmt = NULL; @@ -745,10 +725,10 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, ost->logfile_prefix ? ost->logfile_prefix : DEFAULT_PASS_LOGFILENAME_PREFIX, ost_idx); - if (!strcmp(ost->enc_ctx->codec->name, "libx264") || !strcmp(ost->enc_ctx->codec->name, "libvvenc")) { - if (av_opt_is_set_to_default_by_name(ost->enc_ctx, "stats", + if (!strcmp(video_enc->codec->name, "libx264") || !strcmp(video_enc->codec->name, "libvvenc")) { + if (av_opt_is_set_to_default_by_name(video_enc, "stats", AV_OPT_SEARCH_CHILDREN) > 0) - av_opt_set(ost->enc_ctx, "stats", logfilename, + av_opt_set(video_enc, "stats", logfilename, AV_OPT_SEARCH_CHILDREN); } else { if (video_enc->flags & AV_CODEC_FLAG_PASS2) { @@ -774,7 +754,7 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, } } - opt_match_per_stream_int(ost, &o->force_fps, oc, st, &ost->force_fps); + opt_match_per_stream_int(ost, &o->force_fps, oc, st, &ms->force_fps); #if FFMPEG_OPT_TOP ost->top_field_first = -1; @@ -795,7 +775,7 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, return ret; } - if ((ost->frame_rate.num || ost->max_frame_rate.num) && + if ((ms->frame_rate.num || ms->max_frame_rate.num) && !(*vsync_method == VSYNC_AUTO || *vsync_method == VSYNC_CFR || *vsync_method == VSYNC_VSCFR)) { av_log(ost, AV_LOG_FATAL, "One of -r/-fpsmax was specified " @@ -804,7 +784,7 @@ static int new_stream_video(Muxer *mux, const OptionsContext *o, } if (*vsync_method == VSYNC_AUTO) { - if (ost->frame_rate.num || ost->max_frame_rate.num) { + if (ms->frame_rate.num || ms->max_frame_rate.num) { *vsync_method = VSYNC_CFR; } else if (!strcmp(oc->oformat->name, "avi")) { *vsync_method = VSYNC_VFR; @@ -841,8 +821,8 @@ static int new_stream_audio(Muxer *mux, const OptionsContext *o, AVFormatContext *oc = mux->fc; AVStream *st = ost->st; - if (ost->enc_ctx) { - AVCodecContext *audio_enc = ost->enc_ctx; + if (ost->enc) { + AVCodecContext *audio_enc = ost->enc->enc_ctx; int channels = 0; const char *layout = NULL; const char *sample_fmt = NULL; @@ -880,8 +860,8 @@ static int new_stream_subtitle(Muxer *mux, const OptionsContext *o, st = ost->st; - if (ost->enc_ctx) { - AVCodecContext *subtitle_enc = ost->enc_ctx; + if (ost->enc) { + AVCodecContext *subtitle_enc = ost->enc->enc_ctx; AVCodecDescriptor const *input_descriptor = avcodec_descriptor_get(ost->ist->par->codec_id); @@ -916,14 +896,16 @@ static int new_stream_subtitle(Muxer *mux, const OptionsContext *o, static int ost_bind_filter(const Muxer *mux, MuxStream *ms, OutputFilter *ofilter, - const OptionsContext *o, char *filters, + const OptionsContext *o, AVRational enc_tb, enum VideoSyncMethod vsync_method, int keep_pix_fmt, int autoscale, int threads_manual, - const ViewSpecifier *vs) + const ViewSpecifier *vs, + SchedulerNode *src) { OutputStream *ost = &ms->ost; - AVCodecContext *enc_ctx = ost->enc_ctx; + AVCodecContext *enc_ctx = ost->enc->enc_ctx; char name[16]; + char *filters = NULL; int ret; OutputFilterOptions opts = { @@ -936,6 +918,8 @@ ost_bind_filter(const Muxer *mux, MuxStream *ms, OutputFilter *ofilter, .color_space = enc_ctx->colorspace, .color_range = enc_ctx->color_range, .vsync_method = vsync_method, + .frame_rate = ms->frame_rate, + .max_frame_rate = ms->max_frame_rate, .sample_rate = enc_ctx->sample_rate, .ch_layout = enc_ctx->ch_layout, .sws_opts = o->g->sws_dict, @@ -962,7 +946,7 @@ ost_bind_filter(const Muxer *mux, MuxStream *ms, OutputFilter *ofilter, if (ret < 0) return ret; } - if (!ost->force_fps) { + if (!ms->force_fps) { ret = avcodec_get_supported_config(enc_ctx, NULL, AV_CODEC_CONFIG_FRAME_RATE, 0, (const void **) &opts.frame_rates, NULL); @@ -1003,41 +987,73 @@ ost_bind_filter(const Muxer *mux, MuxStream *ms, OutputFilter *ofilter, return ret; } + ret = ost_get_filters(o, mux->fc, ost, &filters); + if (ret < 0) + return ret; + if (ofilter) { + av_assert0(!filters); ost->filter = ofilter; - ret = ofilter_bind_ost(ofilter, ost, ms->sch_idx_enc, &opts); + ret = ofilter_bind_enc(ofilter, ms->sch_idx_enc, &opts); } else { - ret = init_simple_filtergraph(ost->ist, ost, filters, - mux->sch, ms->sch_idx_enc, &opts); + ret = fg_create_simple(&ost->fg_simple, ost->ist, filters, + mux->sch, ms->sch_idx_enc, &opts); + if (ret >= 0) + ost->filter = ost->fg_simple->outputs[0]; + } av_freep(&opts.nb_threads); if (ret < 0) return ret; - ret = sch_connect(mux->sch, SCH_ENC(ms->sch_idx_enc), - SCH_MSTREAM(mux->sch_idx, ms->sch_idx)); - if (ret < 0) - return ret; + *src = SCH_ENC(ms->sch_idx_enc); - return ret; + return 0; } -static int streamcopy_init(const Muxer *mux, OutputStream *ost, AVDictionary **encoder_opts) +static int streamcopy_init(const OptionsContext *o, const Muxer *mux, + OutputStream *ost, AVDictionary **encoder_opts) { MuxStream *ms = ms_from_ost(ost); const InputStream *ist = ost->ist; const InputFile *ifile = ist->file; - AVCodecParameters *par = ost->par_in; + AVCodecParameters *par = ms->par_in; uint32_t codec_tag = par->codec_tag; AVCodecContext *codec_ctx = NULL; - AVRational fr = ost->frame_rate; + AVRational fr = ms->frame_rate; int ret = 0; + const char *filters = NULL; +#if FFMPEG_OPT_FILTER_SCRIPT + const char *filters_script = NULL; + + opt_match_per_stream_str(ost, &o->filter_scripts, mux->fc, ost->st, &filters_script); +#endif + opt_match_per_stream_str(ost, &o->filters, mux->fc, ost->st, &filters); + + if ( +#if FFMPEG_OPT_FILTER_SCRIPT + filters_script || +#endif + filters) { + av_log(ost, AV_LOG_ERROR, + "%s '%s' was specified, but codec copy was selected. " + "Filtering and streamcopy cannot be used together.\n", +#if FFMPEG_OPT_FILTER_SCRIPT + filters ? "Filtergraph" : "Filtergraph script", + filters ? filters : filters_script +#else + "Filtergraph", filters +#endif + ); + return AVERROR(EINVAL); + } + codec_ctx = avcodec_alloc_context3(NULL); if (!codec_ctx) return AVERROR(ENOMEM); @@ -1139,6 +1155,28 @@ fail: return ret; } +static int set_encoder_id(OutputStream *ost, const AVCodec *codec) +{ + const char *cname = codec->name; + uint8_t *encoder_string; + int encoder_string_len; + + encoder_string_len = sizeof(LIBAVCODEC_IDENT) + strlen(cname) + 2; + encoder_string = av_mallocz(encoder_string_len); + if (!encoder_string) + return AVERROR(ENOMEM); + + if (!ost->file->bitexact && !ost->bitexact) + av_strlcpy(encoder_string, LIBAVCODEC_IDENT " ", encoder_string_len); + else + av_strlcpy(encoder_string, "Lavc ", encoder_string_len); + av_strlcat(encoder_string, cname, encoder_string_len); + av_dict_set(&ost->st->metadata, "encoder", encoder_string, + AV_DICT_DONT_STRDUP_VAL | AV_DICT_DONT_OVERWRITE); + + return 0; +} + static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, InputStream *ist, OutputFilter *ofilter, const ViewSpecifier *vs, OutputStream **post) @@ -1148,13 +1186,14 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, OutputStream *ost; const AVCodec *enc; AVStream *st; + SchedulerNode src = { .type = SCH_NODE_TYPE_NONE }; AVDictionary *encoder_opts = NULL; int ret = 0, keep_pix_fmt = 0, autoscale = 1; int threads_manual = 0; AVRational enc_tb = { 0, 0 }; enum VideoSyncMethod vsync_method = VSYNC_AUTO; const char *bsfs = NULL, *time_base = NULL, *codec_tag = NULL; - char *filters = NULL, *next; + char *next; double qscale = -1; st = avformat_new_stream(oc, NULL); @@ -1198,8 +1237,8 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, } } - ost->par_in = avcodec_parameters_alloc(); - if (!ost->par_in) + ms->par_in = avcodec_parameters_alloc(); + if (!ms->par_in) return AVERROR(ENOMEM); ms->last_mux_dts = AV_NOPTS_VALUE; @@ -1207,27 +1246,23 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, ost->st = st; ost->ist = ist; ost->kf.ref_pts = AV_NOPTS_VALUE; - ost->par_in->codec_type = type; + ms->par_in->codec_type = type; st->codecpar->codec_type = type; - ret = choose_encoder(o, oc, ost, &enc); + ret = choose_encoder(o, oc, ms, &enc); if (ret < 0) { av_log(ost, AV_LOG_FATAL, "Error selecting an encoder\n"); return ret; } if (enc) { - ost->enc_ctx = avcodec_alloc_context3(enc); - if (!ost->enc_ctx) - return AVERROR(ENOMEM); - ret = sch_add_enc(mux->sch, encoder_thread, ost, ost->type == AVMEDIA_TYPE_SUBTITLE ? NULL : enc_open); if (ret < 0) return ret; ms->sch_idx_enc = ret; - ret = enc_alloc(&ost->enc, enc, mux->sch, ms->sch_idx_enc); + ret = enc_alloc(&ost->enc, enc, mux->sch, ms->sch_idx_enc, ost); if (ret < 0) return ret; @@ -1262,21 +1297,21 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, if (!ms->pkt) return AVERROR(ENOMEM); - if (ost->enc_ctx) { + if (ost->enc) { AVIOContext *s = NULL; char *buf = NULL, *arg = NULL; const char *enc_stats_pre = NULL, *enc_stats_post = NULL, *mux_stats = NULL; const char *enc_time_base = NULL, *preset = NULL; - ret = filter_codec_opts(o->g->codec_opts, ost->enc_ctx->codec_id, - oc, st, ost->enc_ctx->codec, &encoder_opts, + ret = filter_codec_opts(o->g->codec_opts, enc->id, + oc, st, enc, &encoder_opts, &mux->enc_opts_used); if (ret < 0) goto fail; opt_match_per_stream_str(ost, &o->presets, oc, st, &preset); opt_match_per_stream_int(ost, &o->autoscale, oc, st, &autoscale); - if (preset && (!(ret = get_preset_file_2(preset, ost->enc_ctx->codec->name, &s)))) { + if (preset && (!(ret = get_preset_file_2(preset, enc->name, &s)))) { AVBPrint bprint; av_bprint_init(&bprint, 0, AV_BPRINT_SIZE_UNLIMITED); do { @@ -1376,7 +1411,7 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, threads_manual = !!av_dict_get(encoder_opts, "threads", NULL, 0); - ret = av_opt_set_dict2(ost->enc_ctx, &encoder_opts, AV_OPT_SEARCH_CHILDREN); + ret = av_opt_set_dict2(ost->enc->enc_ctx, &encoder_opts, AV_OPT_SEARCH_CHILDREN); if (ret < 0) { av_log(ost, AV_LOG_ERROR, "Error applying encoder options: %s\n", av_err2str(ret)); @@ -1389,7 +1424,7 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, // default to automatic thread count if (!threads_manual) - ost->enc_ctx->thread_count = 0; + ost->enc->enc_ctx->thread_count = 0; } else { ret = filter_codec_opts(o->g->codec_opts, AV_CODEC_ID_NONE, oc, st, NULL, &encoder_opts, @@ -1401,8 +1436,14 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, if (o->bitexact) { ost->bitexact = 1; - } else if (ost->enc_ctx) { - ost->bitexact = !!(ost->enc_ctx->flags & AV_CODEC_FLAG_BITEXACT); + } else if (ost->enc) { + ost->bitexact = !!(ost->enc->enc_ctx->flags & AV_CODEC_FLAG_BITEXACT); + } + + if (enc) { + ret = set_encoder_id(ost, enc); + if (ret < 0) + return ret; } opt_match_per_stream_str(ost, &o->time_bases, oc, st, &time_base); @@ -1447,15 +1488,15 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, tag = AV_RL32(buf); } ost->st->codecpar->codec_tag = tag; - ost->par_in->codec_tag = tag; - if (ost->enc_ctx) - ost->enc_ctx->codec_tag = tag; + ms->par_in->codec_tag = tag; + if (ost->enc) + ost->enc->enc_ctx->codec_tag = tag; } opt_match_per_stream_dbl(ost, &o->qscale, oc, st, &qscale); - if (ost->enc_ctx && qscale >= 0) { - ost->enc_ctx->flags |= AV_CODEC_FLAG_QSCALE; - ost->enc_ctx->global_quality = FF_QP2LAMBDA * qscale; + if (ost->enc && qscale >= 0) { + ost->enc->enc_ctx->flags |= AV_CODEC_FLAG_QSCALE; + ost->enc->enc_ctx->global_quality = FF_QP2LAMBDA * qscale; } if (ms->sch_idx >= 0) { @@ -1477,8 +1518,8 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, opt_match_per_stream_int(ost, &o->fix_sub_duration_heartbeat, oc, st, &ost->fix_sub_duration_heartbeat); - if (oc->oformat->flags & AVFMT_GLOBALHEADER && ost->enc_ctx) - ost->enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + if (oc->oformat->flags & AVFMT_GLOBALHEADER && ost->enc) + ost->enc->enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; opt_match_per_stream_int(ost, &o->copy_initial_nonkeyframes, oc, st, &ms->copy_initial_nonkeyframes); @@ -1490,48 +1531,43 @@ static int ost_add(Muxer *mux, const OptionsContext *o, enum AVMediaType type, if (ret < 0) goto fail; - if (type == AVMEDIA_TYPE_VIDEO || type == AVMEDIA_TYPE_AUDIO) { - ret = ost_get_filters(o, oc, ost, &filters); - if (ret < 0) - goto fail; - } - if (ost->enc && (type == AVMEDIA_TYPE_VIDEO || type == AVMEDIA_TYPE_AUDIO)) { - ret = ost_bind_filter(mux, ms, ofilter, o, filters, enc_tb, vsync_method, - keep_pix_fmt, autoscale, threads_manual, vs); + ret = ost_bind_filter(mux, ms, ofilter, o, enc_tb, vsync_method, + keep_pix_fmt, autoscale, threads_manual, vs, &src); if (ret < 0) goto fail; } else if (ost->ist) { - int sched_idx = ist_output_add(ost->ist, ost); - if (sched_idx < 0) { + ret = ist_use(ost->ist, !!ost->enc, NULL, &src); + if (ret < 0) { av_log(ost, AV_LOG_ERROR, "Error binding an input stream\n"); - ret = sched_idx; goto fail; } - ms->sch_idx_src = sched_idx; + ms->sch_idx_src = src.idx; + // src refers to a decoder for transcoding, demux stream otherwise if (ost->enc) { - ret = sch_connect(mux->sch, SCH_DEC_OUT(sched_idx, 0), - SCH_ENC(ms->sch_idx_enc)); - if (ret < 0) - goto fail; - - ret = sch_connect(mux->sch, SCH_ENC(ms->sch_idx_enc), - SCH_MSTREAM(mux->sch_idx, ms->sch_idx)); - if (ret < 0) - goto fail; - } else { - ret = sch_connect(mux->sch, SCH_DSTREAM(ost->ist->file->index, sched_idx), - SCH_MSTREAM(ost->file->index, ms->sch_idx)); + ret = sch_connect(mux->sch, + src, SCH_ENC(ms->sch_idx_enc)); if (ret < 0) goto fail; + src = SCH_ENC(ms->sch_idx_enc); } } + if (src.type != SCH_NODE_TYPE_NONE) { + ret = sch_connect(mux->sch, + src, SCH_MSTREAM(mux->sch_idx, ms->sch_idx)); + if (ret < 0) + goto fail; + } else { + // only attachment streams don't have a source + av_assert0(type == AVMEDIA_TYPE_ATTACHMENT && ms->sch_idx < 0); + } + if (ost->ist && !ost->enc) { - ret = streamcopy_init(mux, ost, &encoder_opts); + ret = streamcopy_init(o, mux, ost, &encoder_opts); if (ret < 0) goto fail; } @@ -1799,6 +1835,7 @@ loop_end: static int of_add_attachments(Muxer *mux, const OptionsContext *o) { + MuxStream *ms; OutputStream *ost; int err; @@ -1866,9 +1903,11 @@ read_fail: return err; } + ms = ms_from_ost(ost); + ost->attachment_filename = attachment_filename; - ost->par_in->extradata = attachment; - ost->par_in->extradata_size = len; + ms->par_in->extradata = attachment; + ms->par_in->extradata_size = len; p = strrchr(o->attachments[i], '/'); av_dict_set(&ost->st->metadata, "filename", (p && *p) ? p + 1 : o->attachments[i], AV_DICT_DONT_OVERWRITE); @@ -2009,7 +2048,7 @@ static int setup_sync_queues(Muxer *mux, AVFormatContext *oc, int limit_frames = 0, limit_frames_av_enc = 0; #define IS_AV_ENC(ost, type) \ - (ost->enc_ctx && (type == AVMEDIA_TYPE_VIDEO || type == AVMEDIA_TYPE_AUDIO)) + (ost->enc && (type == AVMEDIA_TYPE_VIDEO || type == AVMEDIA_TYPE_AUDIO)) #define IS_INTERLEAVED(type) (type != AVMEDIA_TYPE_ATTACHMENT) for (int i = 0; i < oc->nb_streams; i++) { @@ -2021,8 +2060,8 @@ static int setup_sync_queues(Muxer *mux, AVFormatContext *oc, nb_interleaved += IS_INTERLEAVED(type); nb_av_enc += IS_AV_ENC(ost, type); - nb_audio_fs += (ost->enc_ctx && type == AVMEDIA_TYPE_AUDIO && - !(ost->enc_ctx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)); + nb_audio_fs += (ost->enc && type == AVMEDIA_TYPE_AUDIO && + !(ost->enc->enc_ctx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)); limit_frames |= ms->max_frames < INT64_MAX; limit_frames_av_enc |= (ms->max_frames < INT64_MAX) && IS_AV_ENC(ost, type); @@ -2977,9 +3016,6 @@ static int copy_meta(Muxer *mux, const OptionsContext *o) if (!ost->ist) /* this is true e.g. for attached files */ continue; av_dict_copy(&ost->st->metadata, ost->ist->st->metadata, AV_DICT_DONT_OVERWRITE); - if (ost->enc_ctx) { - av_dict_set(&ost->st->metadata, "encoder", NULL, 0); - } } return 0; @@ -3056,7 +3092,7 @@ finish: return ret; } -const char *const forced_keyframes_const_names[] = { +static const char *const forced_keyframes_const_names[] = { "n", "n_forced", "prev_forced_n", @@ -3158,7 +3194,7 @@ static int process_forced_keyframes(Muxer *mux, const OptionsContext *o) mux->fc, ost->st, &forced_keyframes); if (!(ost->type == AVMEDIA_TYPE_VIDEO && - ost->enc_ctx && forced_keyframes)) + ost->enc && forced_keyframes)) continue; if (!strncmp(forced_keyframes, "expr:", 5)) { @@ -3385,7 +3421,7 @@ int of_open(const OptionsContext *o, const char *filename, Scheduler *sch) OutputStream *ost = of->streams[i]; if (!ost->enc) { - err = of_stream_init(of, ost); + err = of_stream_init(of, ost, NULL); if (err < 0) return err; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffplay.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffplay.c index 60d8874eab..a596972769 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffplay.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/fftools/ffplay.c @@ -388,7 +388,6 @@ static const struct TextureFormatEntry { { AV_PIX_FMT_YUV420P, SDL_PIXELFORMAT_IYUV }, { AV_PIX_FMT_YUYV422, SDL_PIXELFORMAT_YUY2 }, { AV_PIX_FMT_UYVY422, SDL_PIXELFORMAT_UYVY }, - { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_UNKNOWN }, }; static int opt_add_vfilter(void *optctx, const char *opt, const char *arg) @@ -895,7 +894,7 @@ static void get_sdl_pix_fmt_and_blendmode(int format, Uint32 *sdl_pix_fmt, SDL_B format == AV_PIX_FMT_BGR32 || format == AV_PIX_FMT_BGR32_1) *sdl_blendmode = SDL_BLENDMODE_BLEND; - for (i = 0; i < FF_ARRAY_ELEMS(sdl_texture_format_map) - 1; i++) { + for (i = 0; i < FF_ARRAY_ELEMS(sdl_texture_format_map); i++) { if (format == sdl_texture_format_map[i].format) { *sdl_pix_fmt = sdl_texture_format_map[i].texture_fmt; return; @@ -941,7 +940,6 @@ static enum AVColorSpace sdl_supported_color_spaces[] = { AVCOL_SPC_BT709, AVCOL_SPC_BT470BG, AVCOL_SPC_SMPTE170M, - AVCOL_SPC_UNSPECIFIED, }; static void set_sdl_yuv_conversion_mode(AVFrame *frame) @@ -1861,7 +1859,6 @@ static int configure_video_filters(AVFilterGraph *graph, VideoState *is, const c { enum AVPixelFormat pix_fmts[FF_ARRAY_ELEMS(sdl_texture_format_map)]; char sws_flags_str[512] = ""; - char buffersrc_args[256]; int ret; AVFilterContext *filt_src = NULL, *filt_out = NULL, *last_filter = NULL; AVCodecParameters *codecpar = is->video_st->codecpar; @@ -1875,14 +1872,13 @@ static int configure_video_filters(AVFilterGraph *graph, VideoState *is, const c return AVERROR(ENOMEM); for (i = 0; i < renderer_info.num_texture_formats; i++) { - for (j = 0; j < FF_ARRAY_ELEMS(sdl_texture_format_map) - 1; j++) { + for (j = 0; j < FF_ARRAY_ELEMS(sdl_texture_format_map); j++) { if (renderer_info.texture_formats[i] == sdl_texture_format_map[j].texture_fmt) { pix_fmts[nb_pix_fmts++] = sdl_texture_format_map[j].format; break; } } } - pix_fmts[nb_pix_fmts] = AV_PIX_FMT_NONE; while ((e = av_dict_iterate(sws_dict, e))) { if (!strcmp(e->key, "sws_flags")) { @@ -1895,36 +1891,49 @@ static int configure_video_filters(AVFilterGraph *graph, VideoState *is, const c graph->scale_sws_opts = av_strdup(sws_flags_str); - snprintf(buffersrc_args, sizeof(buffersrc_args), - "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d:" - "colorspace=%d:range=%d", - frame->width, frame->height, frame->format, - is->video_st->time_base.num, is->video_st->time_base.den, - codecpar->sample_aspect_ratio.num, FFMAX(codecpar->sample_aspect_ratio.den, 1), - frame->colorspace, frame->color_range); - if (fr.num && fr.den) - av_strlcatf(buffersrc_args, sizeof(buffersrc_args), ":frame_rate=%d/%d", fr.num, fr.den); - if ((ret = avfilter_graph_create_filter(&filt_src, - avfilter_get_by_name("buffer"), - "ffplay_buffer", buffersrc_args, NULL, - graph)) < 0) + filt_src = avfilter_graph_alloc_filter(graph, avfilter_get_by_name("buffer"), + "ffplay_buffer"); + if (!filt_src) { + ret = AVERROR(ENOMEM); goto fail; + } + + par->format = frame->format; + par->time_base = is->video_st->time_base; + par->width = frame->width; + par->height = frame->height; + par->sample_aspect_ratio = codecpar->sample_aspect_ratio; + par->color_space = frame->colorspace; + par->color_range = frame->color_range; + par->frame_rate = fr; par->hw_frames_ctx = frame->hw_frames_ctx; ret = av_buffersrc_parameters_set(filt_src, par); if (ret < 0) goto fail; - ret = avfilter_graph_create_filter(&filt_out, - avfilter_get_by_name("buffersink"), - "ffplay_buffersink", NULL, NULL, graph); + ret = avfilter_init_dict(filt_src, NULL); if (ret < 0) goto fail; - if ((ret = av_opt_set_int_list(filt_out, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN)) < 0) + filt_out = avfilter_graph_alloc_filter(graph, avfilter_get_by_name("buffersink"), + "ffplay_buffersink"); + if (!filt_out) { + ret = AVERROR(ENOMEM); + goto fail; + } + + if ((ret = av_opt_set_array(filt_out, "pixel_formats", AV_OPT_SEARCH_CHILDREN, + 0, nb_pix_fmts, AV_OPT_TYPE_PIXEL_FMT, pix_fmts)) < 0) goto fail; if (!vk_renderer && - (ret = av_opt_set_int_list(filt_out, "color_spaces", sdl_supported_color_spaces, AVCOL_SPC_UNSPECIFIED, AV_OPT_SEARCH_CHILDREN)) < 0) + (ret = av_opt_set_array(filt_out, "colorspaces", AV_OPT_SEARCH_CHILDREN, + 0, FF_ARRAY_ELEMS(sdl_supported_color_spaces), + AV_OPT_TYPE_INT, sdl_supported_color_spaces)) < 0) + goto fail; + + ret = avfilter_init_dict(filt_out, NULL); + if (ret < 0) goto fail; last_filter = filt_out; @@ -1994,8 +2003,6 @@ fail: static int configure_audio_filters(VideoState *is, const char *afilters, int force_output_format) { - static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE }; - int sample_rates[2] = { 0, -1 }; AVFilterContext *filt_asrc = NULL, *filt_asink = NULL; char aresample_swr_opts[512] = ""; const AVDictionaryEntry *e = NULL; @@ -2029,30 +2036,28 @@ static int configure_audio_filters(VideoState *is, const char *afilters, int for if (ret < 0) goto end; - - ret = avfilter_graph_create_filter(&filt_asink, - avfilter_get_by_name("abuffersink"), "ffplay_abuffersink", - NULL, NULL, is->agraph); - if (ret < 0) + filt_asink = avfilter_graph_alloc_filter(is->agraph, avfilter_get_by_name("abuffersink"), + "ffplay_abuffersink"); + if (!filt_asink) { + ret = AVERROR(ENOMEM); goto end; + } - if ((ret = av_opt_set_int_list(filt_asink, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN)) < 0) - goto end; - if ((ret = av_opt_set_int(filt_asink, "all_channel_counts", 1, AV_OPT_SEARCH_CHILDREN)) < 0) + if ((ret = av_opt_set(filt_asink, "sample_formats", "s16", AV_OPT_SEARCH_CHILDREN)) < 0) goto end; if (force_output_format) { - av_bprint_clear(&bp); - av_channel_layout_describe_bprint(&is->audio_tgt.ch_layout, &bp); - sample_rates [0] = is->audio_tgt.freq; - if ((ret = av_opt_set_int(filt_asink, "all_channel_counts", 0, AV_OPT_SEARCH_CHILDREN)) < 0) + if ((ret = av_opt_set_array(filt_asink, "channel_layouts", AV_OPT_SEARCH_CHILDREN, + 0, 1, AV_OPT_TYPE_CHLAYOUT, &is->audio_tgt.ch_layout)) < 0) goto end; - if ((ret = av_opt_set(filt_asink, "ch_layouts", bp.str, AV_OPT_SEARCH_CHILDREN)) < 0) - goto end; - if ((ret = av_opt_set_int_list(filt_asink, "sample_rates" , sample_rates , -1, AV_OPT_SEARCH_CHILDREN)) < 0) + if ((ret = av_opt_set_array(filt_asink, "samplerates", AV_OPT_SEARCH_CHILDREN, + 0, 1, AV_OPT_TYPE_INT, &is->audio_tgt.freq)) < 0) goto end; } + ret = avfilter_init_dict(filt_asink, NULL); + if (ret < 0) + goto end; if ((ret = configure_filtergraph(is->agraph, afilters, filt_asrc, filt_asink)) < 0) goto end; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/.gitignore new file mode 100644 index 0000000000..28814f7233 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/.gitignore @@ -0,0 +1,6 @@ +/*_tablegen +/*_tables.c +/*_tables.h +/bsf_list.c +/codec_list.c +/parser_list.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/Makefile b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/Makefile index a4fcce3b42..21188b2479 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/Makefile +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/Makefile @@ -1008,6 +1008,7 @@ OBJS-$(CONFIG_AV1_D3D12VA_HWACCEL) += dxva2_av1.o d3d12va_av1.o OBJS-$(CONFIG_AV1_NVDEC_HWACCEL) += nvdec_av1.o OBJS-$(CONFIG_AV1_VAAPI_HWACCEL) += vaapi_av1.o OBJS-$(CONFIG_AV1_VDPAU_HWACCEL) += vdpau_av1.o +OBJS-$(CONFIG_AV1_VIDEOTOOLBOX_HWACCEL) += videotoolbox_av1.o OBJS-$(CONFIG_AV1_VULKAN_HWACCEL) += vulkan_decode.o vulkan_av1.o OBJS-$(CONFIG_H263_VAAPI_HWACCEL) += vaapi_mpeg4.o OBJS-$(CONFIG_H263_VIDEOTOOLBOX_HWACCEL) += videotoolbox.o diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/h26x/qpel_neon.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/h26x/qpel_neon.S index 5c3f0263b6..f5aa266ab5 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/h26x/qpel_neon.S +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/h26x/qpel_neon.S @@ -4141,9 +4141,9 @@ DISABLE_I8MM #endif function vvc_put_qpel_hv4_8_end_neon - vvc_load_qpel_filterh x5 - mov x7, #(VVC_MAX_PB_SIZE * 2) - b 1f + vvc_load_qpel_filterh x5 + mov x7, #(VVC_MAX_PB_SIZE * 2) + b 1f endfunc function hevc_put_hevc_qpel_hv4_8_end_neon diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/dsp_init.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/dsp_init.c index ad767d17e2..e47f1ab4cc 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/dsp_init.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/dsp_init.c @@ -52,6 +52,50 @@ void ff_vvc_avg_12_neon(uint8_t *dst, ptrdiff_t dst_stride, const int16_t *src0, const int16_t *src1, int width, int height); +void ff_vvc_w_avg_8_neon(uint8_t *_dst, ptrdiff_t _dst_stride, + const int16_t *src0, const int16_t *src1, + int width, int height, + uintptr_t w0_w1, uintptr_t offset_shift); +void ff_vvc_w_avg_10_neon(uint8_t *_dst, ptrdiff_t _dst_stride, + const int16_t *src0, const int16_t *src1, + int width, int height, + uintptr_t w0_w1, uintptr_t offset_shift); +void ff_vvc_w_avg_12_neon(uint8_t *_dst, ptrdiff_t _dst_stride, + const int16_t *src0, const int16_t *src1, + int width, int height, + uintptr_t w0_w1, uintptr_t offset_shift); +/* When passing arguments to functions, Apple platforms diverge from the ARM64 + * standard ABI for functions that require passing arguments on the stack. To + * simplify portability in the assembly function interface, use a different + * function signature that doesn't require passing arguments on the stack. + */ +#define W_AVG_FUN(bit_depth) \ +static void vvc_w_avg_ ## bit_depth(uint8_t *dst, ptrdiff_t dst_stride, \ + const int16_t *src0, const int16_t *src1, int width, int height, \ + int denom, int w0, int w1, int o0, int o1) \ +{ \ + int shift = denom + FFMAX(3, 15 - bit_depth); \ + int offset = ((o0 + o1) * (1 << (bit_depth - 8)) + 1) * (1 << (shift - 1)); \ + uintptr_t w0_w1 = ((uintptr_t)w0 << 32) | (uint32_t)w1; \ + uintptr_t offset_shift = ((uintptr_t)offset << 32) | (uint32_t)shift; \ + ff_vvc_w_avg_ ## bit_depth ## _neon(dst, dst_stride, src0, src1, width, height, w0_w1, offset_shift); \ +} + +W_AVG_FUN(8) +W_AVG_FUN(10) +W_AVG_FUN(12) + +#define DMVR_FUN(fn, bd) \ + void ff_vvc_dmvr_ ## fn ## bd ## _neon(int16_t *dst, \ + const uint8_t *_src, ptrdiff_t _src_stride, int height, \ + intptr_t mx, intptr_t my, int width); + +DMVR_FUN(, 8) +DMVR_FUN(, 12) +DMVR_FUN(hv_, 8) +DMVR_FUN(hv_, 10) +DMVR_FUN(hv_, 12) + void ff_vvc_dsp_init_aarch64(VVCDSPContext *const c, const int bd) { int cpu_flags = av_get_cpu_flags(); @@ -123,6 +167,9 @@ void ff_vvc_dsp_init_aarch64(VVCDSPContext *const c, const int bd) c->inter.put_uni_w[0][6][0][0] = ff_vvc_put_pel_uni_w_pixels128_8_neon; c->inter.avg = ff_vvc_avg_8_neon; + c->inter.w_avg = vvc_w_avg_8; + c->inter.dmvr[0][0] = ff_vvc_dmvr_8_neon; + c->inter.dmvr[1][1] = ff_vvc_dmvr_hv_8_neon; for (int i = 0; i < FF_ARRAY_ELEMS(c->sao.band_filter); i++) c->sao.band_filter[i] = ff_h26x_sao_band_filter_8x8_8_neon; @@ -163,11 +210,16 @@ void ff_vvc_dsp_init_aarch64(VVCDSPContext *const c, const int bd) } } else if (bd == 10) { c->inter.avg = ff_vvc_avg_10_neon; + c->inter.w_avg = vvc_w_avg_10; + c->inter.dmvr[1][1] = ff_vvc_dmvr_hv_10_neon; c->alf.filter[LUMA] = alf_filter_luma_10_neon; c->alf.filter[CHROMA] = alf_filter_chroma_10_neon; } else if (bd == 12) { c->inter.avg = ff_vvc_avg_12_neon; + c->inter.w_avg = vvc_w_avg_12; + c->inter.dmvr[0][0] = ff_vvc_dmvr_12_neon; + c->inter.dmvr[1][1] = ff_vvc_dmvr_hv_12_neon; c->alf.filter[LUMA] = alf_filter_luma_12_neon; c->alf.filter[CHROMA] = alf_filter_chroma_12_neon; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/inter.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/inter.S index 2f69274b86..b6b079b569 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/inter.S +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/aarch64/vvc/inter.S @@ -22,9 +22,9 @@ #define VVC_MAX_PB_SIZE 128 -.macro vvc_avg, bit_depth +.macro vvc_avg type, bit_depth -.macro vvc_avg_\bit_depth\()_2_4, tap +.macro vvc_\type\()_\bit_depth\()_2_4 tap .if \tap == 2 ldr s0, [src0] ldr s2, [src1] @@ -32,9 +32,19 @@ ldr d0, [src0] ldr d2, [src1] .endif + +.ifc \type, avg saddl v4.4s, v0.4h, v2.4h add v4.4s, v4.4s, v16.4s sqshrn v4.4h, v4.4s, #(15 - \bit_depth) +.else + mov v4.16b, v16.16b + smlal v4.4s, v0.4h, v19.4h + smlal v4.4s, v2.4h, v20.4h + sqshl v4.4s, v4.4s, v22.4s + sqxtn v4.4h, v4.4s +.endif + .if \bit_depth == 8 sqxtun v4.8b, v4.8h .if \tap == 2 @@ -57,7 +67,7 @@ add dst, dst, dst_stride .endm -function ff_vvc_avg_\bit_depth\()_neon, export=1 +function ff_vvc_\type\()_\bit_depth\()_neon, export=1 dst .req x0 dst_stride .req x1 src0 .req x2 @@ -67,42 +77,64 @@ function ff_vvc_avg_\bit_depth\()_neon, export=1 mov x10, #(VVC_MAX_PB_SIZE * 2) cmp width, #8 -.if \bit_depth == 8 - movi v16.4s, #64 +.ifc \type, avg + movi v16.4s, #(1 << (14 - \bit_depth)) .else -.if \bit_depth == 10 - mov w6, #1023 - movi v16.4s, #16 -.else - mov w6, #4095 - movi v16.4s, #4 -.endif + lsr x11, x6, #32 // weight0 + mov w12, w6 // weight1 + lsr x13, x7, #32 // offset + mov w14, w7 // shift + + dup v19.8h, w11 + neg w14, w14 // so we can use sqshl + dup v20.8h, w12 + dup v16.4s, w13 + dup v22.4s, w14 +.endif // avg + + .if \bit_depth >= 10 + // clip pixel + mov w6, #((1 << \bit_depth) - 1) movi v18.8h, #0 dup v17.8h, w6 .endif + b.eq 8f b.hi 16f cmp width, #4 b.eq 4f 2: // width == 2 subs height, height, #1 - vvc_avg_\bit_depth\()_2_4 2 + vvc_\type\()_\bit_depth\()_2_4 2 b.ne 2b b 32f 4: // width == 4 subs height, height, #1 - vvc_avg_\bit_depth\()_2_4 4 + vvc_\type\()_\bit_depth\()_2_4 4 b.ne 4b b 32f 8: // width == 8 ld1 {v0.8h}, [src0], x10 ld1 {v2.8h}, [src1], x10 +.ifc \type, avg saddl v4.4s, v0.4h, v2.4h saddl2 v5.4s, v0.8h, v2.8h add v4.4s, v4.4s, v16.4s add v5.4s, v5.4s, v16.4s sqshrn v4.4h, v4.4s, #(15 - \bit_depth) sqshrn2 v4.8h, v5.4s, #(15 - \bit_depth) +.else + mov v4.16b, v16.16b + mov v5.16b, v16.16b + smlal v4.4s, v0.4h, v19.4h + smlal v4.4s, v2.4h, v20.4h + smlal2 v5.4s, v0.8h, v19.8h + smlal2 v5.4s, v2.8h, v20.8h + sqshl v4.4s, v4.4s, v22.4s + sqshl v5.4s, v5.4s, v22.4s + sqxtn v4.4h, v4.4s + sqxtn2 v4.8h, v5.4s +.endif subs height, height, #1 .if \bit_depth == 8 sqxtun v4.8b, v4.8h @@ -122,6 +154,7 @@ function ff_vvc_avg_\bit_depth\()_neon, export=1 17: ldp q0, q1, [x7], #32 ldp q2, q3, [x8], #32 +.ifc \type, avg saddl v4.4s, v0.4h, v2.4h saddl2 v5.4s, v0.8h, v2.8h saddl v6.4s, v1.4h, v3.4h @@ -134,6 +167,28 @@ function ff_vvc_avg_\bit_depth\()_neon, export=1 sqshrn2 v4.8h, v5.4s, #(15 - \bit_depth) sqshrn v6.4h, v6.4s, #(15 - \bit_depth) sqshrn2 v6.8h, v7.4s, #(15 - \bit_depth) +.else // avg + mov v4.16b, v16.16b + mov v5.16b, v16.16b + mov v6.16b, v16.16b + mov v7.16b, v16.16b + smlal v4.4s, v0.4h, v19.4h + smlal v4.4s, v2.4h, v20.4h + smlal2 v5.4s, v0.8h, v19.8h + smlal2 v5.4s, v2.8h, v20.8h + smlal v6.4s, v1.4h, v19.4h + smlal v6.4s, v3.4h, v20.4h + smlal2 v7.4s, v1.8h, v19.8h + smlal2 v7.4s, v3.8h, v20.8h + sqshl v4.4s, v4.4s, v22.4s + sqshl v5.4s, v5.4s, v22.4s + sqshl v6.4s, v6.4s, v22.4s + sqshl v7.4s, v7.4s, v22.4s + sqxtn v4.4h, v4.4s + sqxtn v6.4h, v6.4s + sqxtn2 v4.8h, v5.4s + sqxtn2 v6.8h, v7.4s +.endif // w_avg subs w6, w6, #16 .if \bit_depth == 8 sqxtun v4.8b, v4.8h @@ -155,9 +210,411 @@ function ff_vvc_avg_\bit_depth\()_neon, export=1 b.ne 16b 32: ret + +.unreq dst +.unreq dst_stride +.unreq src0 +.unreq src1 +.unreq width +.unreq height endfunc .endm -vvc_avg 8 -vvc_avg 10 -vvc_avg 12 +vvc_avg avg, 8 +vvc_avg avg, 10 +vvc_avg avg, 12 +vvc_avg w_avg, 8 +vvc_avg w_avg, 10 +vvc_avg w_avg, 12 + +/* x0: int16_t *dst + * x1: const uint8_t *_src + * x2: ptrdiff_t _src_stride + * w3: int height + * x4: intptr_t mx + * x5: intptr_t my + * w6: int width + */ +function ff_vvc_dmvr_8_neon, export=1 + dst .req x0 + src .req x1 + src_stride .req x2 + height .req w3 + mx .req x4 + my .req x5 + width .req w6 + + sxtw x6, w6 + mov x7, #(VVC_MAX_PB_SIZE * 2 + 8) + cmp width, #16 + sub src_stride, src_stride, x6 + cset w15, gt // width > 16 + movi v16.8h, #2 // DMVR_SHIFT + sub x7, x7, x6, lsl #1 +1: + cbz w15, 2f + ldr q0, [src], #16 + uxtl v1.8h, v0.8b + uxtl2 v2.8h, v0.16b + ushl v1.8h, v1.8h, v16.8h + ushl v2.8h, v2.8h, v16.8h + stp q1, q2, [dst], #32 + b 3f +2: + ldr d0, [src], #8 + uxtl v1.8h, v0.8b + ushl v1.8h, v1.8h, v16.8h + str q1, [dst], #16 +3: + subs height, height, #1 + ldr s3, [src], #4 + uxtl v4.8h, v3.8b + ushl v4.4h, v4.4h, v16.4h + st1 {v4.4h}, [dst], x7 + + add src, src, src_stride + b.ne 1b + + ret +endfunc + +function ff_vvc_dmvr_12_neon, export=1 + sxtw x6, w6 + mov x7, #(VVC_MAX_PB_SIZE * 2 + 8) + cmp width, #16 + sub src_stride, src_stride, x6, lsl #1 + cset w15, gt // width > 16 + movi v16.8h, #2 // offset4 + sub x7, x7, x6, lsl #1 +1: + cbz w15, 2f + ldp q0, q1, [src], #32 + uaddl v2.4s, v0.4h, v16.4h + uaddl2 v3.4s, v0.8h, v16.8h + uaddl v4.4s, v1.4h, v16.4h + uaddl2 v5.4s, v1.8h, v16.8h + ushr v2.4s, v2.4s, #2 + ushr v3.4s, v3.4s, #2 + ushr v4.4s, v4.4s, #2 + ushr v5.4s, v5.4s, #2 + uqxtn v2.4h, v2.4s + uqxtn2 v2.8h, v3.4s + uqxtn v4.4h, v4.4s + uqxtn2 v4.8h, v5.4s + + stp q2, q4, [dst], #32 + b 3f +2: + ldr q0, [src], #16 + uaddl v2.4s, v0.4h, v16.4h + uaddl2 v3.4s, v0.8h, v16.8h + ushr v2.4s, v2.4s, #2 + ushr v3.4s, v3.4s, #2 + uqxtn v2.4h, v2.4s + uqxtn2 v2.8h, v3.4s + str q2, [dst], #16 +3: + subs height, height, #1 + ldr d0, [src], #8 + uaddl v3.4s, v0.4h, v16.4h + ushr v3.4s, v3.4s, #2 + uqxtn v3.4h, v3.4s + st1 {v3.4h}, [dst], x7 + + add src, src, src_stride + b.ne 1b + + ret +endfunc + +function ff_vvc_dmvr_hv_8_neon, export=1 + tmp0 .req x7 + tmp1 .req x8 + + sub sp, sp, #(VVC_MAX_PB_SIZE * 4) + + movrel x9, X(ff_vvc_inter_luma_dmvr_filters) + add x12, x9, mx, lsl #1 + ldrb w10, [x12] + ldrb w11, [x12, #1] + mov tmp0, sp + add tmp1, tmp0, #(VVC_MAX_PB_SIZE * 2) + // We know the value are positive + dup v0.8h, w10 // filter_x[0] + dup v1.8h, w11 // filter_x[1] + + add x12, x9, my, lsl #1 + ldrb w10, [x12] + ldrb w11, [x12, #1] + sxtw x6, w6 + movi v30.8h, #(1 << (8 - 7)) // offset1 + movi v31.8h, #8 // offset2 + dup v2.8h, w10 // filter_y[0] + dup v3.8h, w11 // filter_y[1] + + // Valid value for width can only be 8 + 4, 16 + 4 + cmp width, #16 + mov w10, #0 // start filter_y or not + add height, height, #1 + sub dst, dst, #(VVC_MAX_PB_SIZE * 2) + sub src_stride, src_stride, x6 + cset w15, gt // width > 16 +1: + mov x12, tmp0 + mov x13, tmp1 + mov x14, dst + cbz w15, 2f + + // width > 16 + ldur q5, [src, #1] + ldr q4, [src], #16 + uxtl v7.8h, v5.8b + uxtl2 v17.8h, v5.16b + uxtl v6.8h, v4.8b + uxtl2 v16.8h, v4.16b + mul v6.8h, v6.8h, v0.8h + mul v16.8h, v16.8h, v0.8h + mla v6.8h, v7.8h, v1.8h + mla v16.8h, v17.8h, v1.8h + add v6.8h, v6.8h, v30.8h + add v16.8h, v16.8h, v30.8h + ushr v6.8h, v6.8h, #(8 - 6) + ushr v7.8h, v16.8h, #(8 - 6) + stp q6, q7, [x13], #32 + + cbz w10, 3f + + ldp q16, q17, [x12], #32 + mul v16.8h, v16.8h, v2.8h + mul v17.8h, v17.8h, v2.8h + mla v16.8h, v6.8h, v3.8h + mla v17.8h, v7.8h, v3.8h + add v16.8h, v16.8h, v31.8h + add v17.8h, v17.8h, v31.8h + ushr v16.8h, v16.8h, #4 + ushr v17.8h, v17.8h, #4 + stp q16, q17, [x14], #32 + b 3f +2: + // width > 8 + ldur d5, [src, #1] + ldr d4, [src], #8 + uxtl v7.8h, v5.8b + uxtl v6.8h, v4.8b + mul v6.8h, v6.8h, v0.8h + mla v6.8h, v7.8h, v1.8h + add v6.8h, v6.8h, v30.8h + ushr v6.8h, v6.8h, #(8 - 6) + str q6, [x13], #16 + + cbz w10, 3f + + ldr q16, [x12], #16 + mul v16.8h, v16.8h, v2.8h + mla v16.8h, v6.8h, v3.8h + add v16.8h, v16.8h, v31.8h + ushr v16.8h, v16.8h, #4 + str q16, [x14], #16 +3: + ldr s5, [src, #1] + ldr s4, [src], #4 + uxtl v7.8h, v5.8b + uxtl v6.8h, v4.8b + mul v6.4h, v6.4h, v0.4h + mla v6.4h, v7.4h, v1.4h + add v6.4h, v6.4h, v30.4h + ushr v6.4h, v6.4h, #(8 - 6) + str d6, [x13], #8 + + cbz w10, 4f + + ldr d16, [x12], #8 + mul v16.4h, v16.4h, v2.4h + mla v16.4h, v6.4h, v3.4h + add v16.4h, v16.4h, v31.4h + ushr v16.4h, v16.4h, #4 + str d16, [x14], #8 +4: + subs height, height, #1 + mov w10, #1 + add src, src, src_stride + add dst, dst, #(VVC_MAX_PB_SIZE * 2) + eor tmp0, tmp0, tmp1 + eor tmp1, tmp0, tmp1 + eor tmp0, tmp0, tmp1 + b.ne 1b + + add sp, sp, #(VVC_MAX_PB_SIZE * 4) + ret +endfunc + +function ff_vvc_dmvr_hv_12_neon, export=1 + movi v29.4s, #(12 - 6) + movi v30.4s, #(1 << (12 - 7)) // offset1 + b 0f +endfunc + +function ff_vvc_dmvr_hv_10_neon, export=1 + movi v29.4s, #(10 - 6) + movi v30.4s, #(1 << (10 - 7)) // offset1 +0: + movi v31.4s, #8 // offset2 + neg v29.4s, v29.4s + + sub sp, sp, #(VVC_MAX_PB_SIZE * 4) + + movrel x9, X(ff_vvc_inter_luma_dmvr_filters) + add x12, x9, mx, lsl #1 + ldrb w10, [x12] + ldrb w11, [x12, #1] + mov tmp0, sp + add tmp1, tmp0, #(VVC_MAX_PB_SIZE * 2) + // We know the value are positive + dup v0.8h, w10 // filter_x[0] + dup v1.8h, w11 // filter_x[1] + + add x12, x9, my, lsl #1 + ldrb w10, [x12] + ldrb w11, [x12, #1] + sxtw x6, w6 + dup v2.8h, w10 // filter_y[0] + dup v3.8h, w11 // filter_y[1] + + // Valid value for width can only be 8 + 4, 16 + 4 + cmp width, #16 + mov w10, #0 // start filter_y or not + add height, height, #1 + sub dst, dst, #(VVC_MAX_PB_SIZE * 2) + sub src_stride, src_stride, x6, lsl #1 + cset w15, gt // width > 16 +1: + mov x12, tmp0 + mov x13, tmp1 + mov x14, dst + cbz w15, 2f + + // width > 16 + add x16, src, #2 + ldp q6, q16, [src], #32 + ldp q7, q17, [x16] + umull v4.4s, v6.4h, v0.4h + umull2 v5.4s, v6.8h, v0.8h + umull v18.4s, v16.4h, v0.4h + umull2 v19.4s, v16.8h, v0.8h + umlal v4.4s, v7.4h, v1.4h + umlal2 v5.4s, v7.8h, v1.8h + umlal v18.4s, v17.4h, v1.4h + umlal2 v19.4s, v17.8h, v1.8h + + add v4.4s, v4.4s, v30.4s + add v5.4s, v5.4s, v30.4s + add v18.4s, v18.4s, v30.4s + add v19.4s, v19.4s, v30.4s + ushl v4.4s, v4.4s, v29.4s + ushl v5.4s, v5.4s, v29.4s + ushl v18.4s, v18.4s, v29.4s + ushl v19.4s, v19.4s, v29.4s + uqxtn v6.4h, v4.4s + uqxtn2 v6.8h, v5.4s + uqxtn v7.4h, v18.4s + uqxtn2 v7.8h, v19.4s + stp q6, q7, [x13], #32 + + cbz w10, 3f + + ldp q4, q5, [x12], #32 + umull v17.4s, v4.4h, v2.4h + umull2 v18.4s, v4.8h, v2.8h + umull v19.4s, v5.4h, v2.4h + umull2 v20.4s, v5.8h, v2.8h + umlal v17.4s, v6.4h, v3.4h + umlal2 v18.4s, v6.8h, v3.8h + umlal v19.4s, v7.4h, v3.4h + umlal2 v20.4s, v7.8h, v3.8h + add v17.4s, v17.4s, v31.4s + add v18.4s, v18.4s, v31.4s + add v19.4s, v19.4s, v31.4s + add v20.4s, v20.4s, v31.4s + ushr v17.4s, v17.4s, #4 + ushr v18.4s, v18.4s, #4 + ushr v19.4s, v19.4s, #4 + ushr v20.4s, v20.4s, #4 + uqxtn v6.4h, v17.4s + uqxtn2 v6.8h, v18.4s + uqxtn v7.4h, v19.4s + uqxtn2 v7.8h, v20.4s + stp q6, q7, [x14], #32 + b 3f +2: + // width > 8 + ldur q7, [src, #2] + ldr q6, [src], #16 + umull v4.4s, v6.4h, v0.4h + umull2 v5.4s, v6.8h, v0.8h + umlal v4.4s, v7.4h, v1.4h + umlal2 v5.4s, v7.8h, v1.8h + + add v4.4s, v4.4s, v30.4s + add v5.4s, v5.4s, v30.4s + ushl v4.4s, v4.4s, v29.4s + ushl v5.4s, v5.4s, v29.4s + uqxtn v6.4h, v4.4s + uqxtn2 v6.8h, v5.4s + str q6, [x13], #16 + + cbz w10, 3f + + ldr q16, [x12], #16 + umull v17.4s, v16.4h, v2.4h + umull2 v18.4s, v16.8h, v2.8h + umlal v17.4s, v6.4h, v3.4h + umlal2 v18.4s, v6.8h, v3.8h + add v17.4s, v17.4s, v31.4s + add v18.4s, v18.4s, v31.4s + ushr v17.4s, v17.4s, #4 + ushr v18.4s, v18.4s, #4 + uqxtn v16.4h, v17.4s + uqxtn2 v16.8h, v18.4s + str q16, [x14], #16 +3: + ldr d7, [src, #2] + ldr d6, [src], #8 + umull v4.4s, v7.4h, v1.4h + umlal v4.4s, v6.4h, v0.4h + add v4.4s, v4.4s, v30.4s + ushl v4.4s, v4.4s, v29.4s + uqxtn v6.4h, v4.4s + str d6, [x13], #8 + + cbz w10, 4f + + ldr d16, [x12], #8 + umull v17.4s, v16.4h, v2.4h + umlal v17.4s, v6.4h, v3.4h + add v17.4s, v17.4s, v31.4s + ushr v17.4s, v17.4s, #4 + uqxtn v16.4h, v17.4s + str d16, [x14], #8 +4: + subs height, height, #1 + mov w10, #1 + add src, src, src_stride + add dst, dst, #(VVC_MAX_PB_SIZE * 2) + eor tmp0, tmp0, tmp1 + eor tmp1, tmp0, tmp1 + eor tmp0, tmp0, tmp1 + b.ne 1b + + add sp, sp, #(VVC_MAX_PB_SIZE * 4) + ret + +.unreq dst +.unreq src +.unreq src_stride +.unreq height +.unreq mx +.unreq my +.unreq width +.unreq tmp0 +.unreq tmp1 +endfunc diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.c index a47aea6108..225fb9df27 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.c @@ -766,11 +766,50 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) switch (avctx->codec->id) { case AV_CODEC_ID_H264: AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_INSERT_AUD, !!ctx->aud); + switch (frame->pict_type) { + case AV_PICTURE_TYPE_I: + if (ctx->forced_idr) { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_INSERT_SPS, 1); + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_INSERT_PPS, 1); + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_IDR); + } else { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_I); + } + break; + case AV_PICTURE_TYPE_P: + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_P); + break; + case AV_PICTURE_TYPE_B: + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_B); + break; + } break; case AV_CODEC_ID_HEVC: AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_HEVC_INSERT_AUD, !!ctx->aud); + switch (frame->pict_type) { + case AV_PICTURE_TYPE_I: + if (ctx->forced_idr) { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_HEVC_INSERT_HEADER, 1); + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_HEVC_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_HEVC_PICTURE_TYPE_IDR); + } else { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_HEVC_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_HEVC_PICTURE_TYPE_I); + } + break; + case AV_PICTURE_TYPE_P: + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_HEVC_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_HEVC_PICTURE_TYPE_P); + break; + } + break; + case AV_CODEC_ID_AV1: + if (frame->pict_type == AV_PICTURE_TYPE_I) { + if (ctx->forced_idr) { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_AV1_FORCE_INSERT_SEQUENCE_HEADER, 1); + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE_KEY); + } else { + AMF_ASSIGN_PROPERTY_INT64(res, surface, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE_INTRA_ONLY); + } + } break; - //case AV_CODEC_ID_AV1 not supported default: break; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.h index d985d01bb1..0f2abcbd82 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc.h @@ -114,6 +114,7 @@ typedef struct AmfContext { int max_b_frames; int qvbr_quality_level; int hw_high_motion_quality_boost; + int forced_idr; // HEVC - specific options diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_av1.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_av1.c index 2a7a782063..b40d54f70c 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_av1.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_av1.c @@ -116,6 +116,7 @@ static const AVOption options[] = { { "none", "no adaptive quantization", 0, AV_OPT_TYPE_CONST, {.i64 = AMF_VIDEO_ENCODER_AV1_AQ_MODE_NONE }, 0, 0, VE, .unit = "adaptive_quantisation_mode" }, { "caq", "context adaptive quantization", 0, AV_OPT_TYPE_CONST, {.i64 = AMF_VIDEO_ENCODER_AV1_AQ_MODE_CAQ }, 0, 0, VE, .unit = "adaptive_quantisation_mode" }, + { "forced_idr", "Force I frames to be IDR frames", OFFSET(forced_idr), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, { "align", "alignment mode", OFFSET(align), AV_OPT_TYPE_INT, {.i64 = AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_NO_RESTRICTIONS }, AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_64X16_ONLY, AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_NO_RESTRICTIONS, VE, .unit = "align" }, { "64x16", "", 0, AV_OPT_TYPE_CONST, {.i64 = AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_64X16_ONLY }, 0, 0, VE, .unit = "align" }, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_h264.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_h264.c index 8edd39c633..959be9eab6 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_h264.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_h264.c @@ -133,6 +133,7 @@ static const AVOption options[] = { { "me_half_pel", "Enable ME Half Pixel", OFFSET(me_half_pel), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VE }, { "me_quarter_pel", "Enable ME Quarter Pixel", OFFSET(me_quarter_pel),AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VE }, + { "forced_idr", "Force I frames to be IDR frames", OFFSET(forced_idr) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) , AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VE }, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_hevc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_hevc.c index 4898824f3a..f9f6f8adb3 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_hevc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/amfenc_hevc.c @@ -100,6 +100,7 @@ static const AVOption options[] = { { "me_half_pel", "Enable ME Half Pixel", OFFSET(me_half_pel), AV_OPT_TYPE_BOOL,{ .i64 = -1 }, -1, 1, VE }, { "me_quarter_pel", "Enable ME Quarter Pixel ", OFFSET(me_quarter_pel),AV_OPT_TYPE_BOOL,{ .i64 = -1 }, -1, 1, VE }, + { "forced_idr", "Force I frames to be IDR frames", OFFSET(forced_idr) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = -1 }, -1, 1, VE }, @@ -269,7 +270,7 @@ FF_ENABLE_DEPRECATION_WARNINGS if (avctx->slices > 1) { AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_SLICES_PER_FRAME, avctx->slices); } - AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_DE_BLOCKING_FILTER_DISABLE, deblocking_filter); + AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_DE_BLOCKING_FILTER_DISABLE, !deblocking_filter); if (ctx->header_insertion_mode != -1) { AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_HEADER_INSERTION_MODE, ctx->header_insertion_mode); @@ -511,6 +512,7 @@ static const FFCodecDefault defaults[] = { { "slices", "1" }, { "qmin", "-1" }, { "qmax", "-1" }, + { "flags", "+loop"}, { NULL }, }; static const AVClass hevc_amf_class = { @@ -536,7 +538,7 @@ const FFCodec ff_hevc_amf_encoder = { .caps_internal = FF_CODEC_CAP_NOT_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP, .p.pix_fmts = ff_amf_pix_fmts, - .color_ranges = AVCOL_RANGE_MPEG, /* FIXME: implement tagging */ + .color_ranges = AVCOL_RANGE_MPEG | AVCOL_RANGE_JPEG, .p.wrapper_name = "amf", .hw_configs = ff_amfenc_hw_configs, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.c index 1d5b9ef4f4..6a9de07d16 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.c @@ -541,6 +541,7 @@ static int get_pixel_format(AVCodecContext *avctx) CONFIG_AV1_NVDEC_HWACCEL + \ CONFIG_AV1_VAAPI_HWACCEL + \ CONFIG_AV1_VDPAU_HWACCEL + \ + CONFIG_AV1_VIDEOTOOLBOX_HWACCEL + \ CONFIG_AV1_VULKAN_HWACCEL) enum AVPixelFormat pix_fmts[HWACCEL_MAX + 2], *fmtp = pix_fmts; @@ -568,6 +569,9 @@ static int get_pixel_format(AVCodecContext *avctx) #if CONFIG_AV1_VDPAU_HWACCEL *fmtp++ = AV_PIX_FMT_VDPAU; #endif +#if CONFIG_AV1_VIDEOTOOLBOX_HWACCEL + *fmtp++ = AV_PIX_FMT_VIDEOTOOLBOX; +#endif #if CONFIG_AV1_VULKAN_HWACCEL *fmtp++ = AV_PIX_FMT_VULKAN; #endif @@ -592,6 +596,9 @@ static int get_pixel_format(AVCodecContext *avctx) #if CONFIG_AV1_VDPAU_HWACCEL *fmtp++ = AV_PIX_FMT_VDPAU; #endif +#if CONFIG_AV1_VIDEOTOOLBOX_HWACCEL + *fmtp++ = AV_PIX_FMT_VIDEOTOOLBOX; +#endif #if CONFIG_AV1_VULKAN_HWACCEL *fmtp++ = AV_PIX_FMT_VULKAN; #endif @@ -1439,6 +1446,10 @@ static int av1_receive_frame_internal(AVCodecContext *avctx, AVFrame *frame) if (raw_tile_group && (s->tile_num == raw_tile_group->tg_end + 1)) { int show_frame = s->raw_frame_header->show_frame; + // Set nb_unit to point at the next OBU, to indicate which + // OBUs have been processed for this current frame. (If this + // frame gets output, we set nb_unit to this value later too.) + s->nb_unit = i + 1; if (avctx->hwaccel && s->cur_frame.f) { ret = FF_HW_SIMPLE_CALL(avctx, end_frame); if (ret < 0) { @@ -1449,6 +1460,8 @@ static int av1_receive_frame_internal(AVCodecContext *avctx, AVFrame *frame) update_reference_list(avctx); + // Set start_unit to indicate the first OBU of the next frame. + s->start_unit = s->nb_unit; raw_tile_group = NULL; s->raw_frame_header = NULL; @@ -1478,7 +1491,7 @@ end: s->raw_frame_header = NULL; av_packet_unref(s->pkt); ff_cbs_fragment_reset(&s->current_obu); - s->nb_unit = 0; + s->nb_unit = s->start_unit = 0; } if (!ret && !frame->buf[0]) ret = AVERROR(EAGAIN); @@ -1505,7 +1518,7 @@ static int av1_receive_frame(AVCodecContext *avctx, AVFrame *frame) return ret; } - s->nb_unit = 0; + s->nb_unit = s->start_unit = 0; av_log(avctx, AV_LOG_DEBUG, "Total OBUs on this packet: %d.\n", s->current_obu.nb_units); } @@ -1526,7 +1539,7 @@ static void av1_decode_flush(AVCodecContext *avctx) av1_frame_unref(&s->cur_frame); s->operating_point_idc = 0; - s->nb_unit = 0; + s->nb_unit = s->start_unit = 0; s->raw_frame_header = NULL; s->raw_seq = NULL; s->cll = NULL; @@ -1594,6 +1607,9 @@ const FFCodec ff_av1_decoder = { #if CONFIG_AV1_VDPAU_HWACCEL HWACCEL_VDPAU(av1), #endif +#if CONFIG_AV1_VIDEOTOOLBOX_HWACCEL + HWACCEL_VIDEOTOOLBOX(av1), +#endif #if CONFIG_AV1_VULKAN_HWACCEL HWACCEL_VULKAN(av1), #endif diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.h index 8b2a7b0896..10c807f73f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/av1dec.h @@ -114,7 +114,8 @@ typedef struct AV1DecContext { AV1Frame ref[AV1_NUM_REF_FRAMES]; AV1Frame cur_frame; - int nb_unit; + int nb_unit; ///< The index of the next OBU to be processed. + int start_unit; ///< The index of the first OBU of the current frame. // AVOptions int operating_point; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.c index 333fb3d79b..56a36e479f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.c @@ -144,6 +144,8 @@ av_cold int ff_ffv1_init_slice_contexts(FFV1Context *f) sc->slice_height = sye - sys; sc->slice_x = sxs; sc->slice_y = sys; + sc->sx = sx; + sc->sy = sy; sc->sample_buffer = av_malloc_array((f->width + 6), 3 * MAX_PLANES * sizeof(*sc->sample_buffer)); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.h index 9aa0452922..a35197964e 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1.h @@ -75,6 +75,7 @@ typedef struct FFV1SliceContext { int slice_height; int slice_x; int slice_y; + int sx, sy; int run_index; int slice_coding_mode; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1dec.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1dec.c index 415c66be63..0afdeabd91 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1dec.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1dec.c @@ -354,6 +354,9 @@ static int decode_slice(AVCodecContext *c, void *arg) } } + if (sc->slice_damaged && (f->avctx->err_recognition & AV_EF_EXPLODE)) + return AVERROR_INVALIDDATA; + if ((c->active_thread_type & FF_THREAD_FRAME) && !f->frame_damaged) ff_progress_frame_report(&f->picture, si); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1enc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1enc.c index a6f405289e..266ef13b24 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1enc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/ffv1enc.c @@ -528,6 +528,11 @@ static av_cold int encode_init(AVCodecContext *avctx) avctx->slices > 1) s->version = FFMAX(s->version, 2); + if ((avctx->flags & (AV_CODEC_FLAG_PASS1 | AV_CODEC_FLAG_PASS2)) && s->ac == AC_GOLOMB_RICE) { + av_log(avctx, AV_LOG_ERROR, "2 Pass mode is not possible with golomb coding\n"); + return AVERROR(EINVAL); + } + // Unspecified level & slices, we choose version 1.2+ to ensure multithreaded decodability if (avctx->slices == 0 && avctx->level < 0 && avctx->width * avctx->height > 720*576) s->version = FFMAX(s->version, 2); @@ -918,10 +923,10 @@ static void encode_slice_header(FFV1Context *f, FFV1SliceContext *sc) int j; memset(state, 128, sizeof(state)); - put_symbol(c, state, (sc->slice_x +1)*f->num_h_slices / f->width , 0); - put_symbol(c, state, (sc->slice_y +1)*f->num_v_slices / f->height , 0); - put_symbol(c, state, (sc->slice_width +1)*f->num_h_slices / f->width -1, 0); - put_symbol(c, state, (sc->slice_height+1)*f->num_v_slices / f->height-1, 0); + put_symbol(c, state, sc->sx, 0); + put_symbol(c, state, sc->sy, 0); + put_symbol(c, state, 0, 0); + put_symbol(c, state, 0, 0); for (j=0; jplane_count; j++) { put_symbol(c, state, sc->plane[j].quant_table_index, 0); av_assert0(sc->plane[j].quant_table_index == f->context_model); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.c index 65fef03304..faca1e8953 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.c @@ -102,6 +102,8 @@ av_cold void ff_h264qpel_init(H264QpelContext *c, int bit_depth) ff_h264qpel_init_arm(c, bit_depth); #elif ARCH_PPC ff_h264qpel_init_ppc(c, bit_depth); +#elif ARCH_RISCV + ff_h264qpel_init_riscv(c, bit_depth); #elif ARCH_X86 ff_h264qpel_init_x86(c, bit_depth); #elif ARCH_MIPS diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.h index 0259e8de23..24baf826f9 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/h264qpel.h @@ -34,6 +34,7 @@ void ff_h264qpel_init(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_aarch64(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_arm(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_ppc(H264QpelContext *c, int bit_depth); +void ff_h264qpel_init_riscv(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_x86(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_mips(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_loongarch(H264QpelContext *c, int bit_depth); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/hwaccels.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/hwaccels.h index 5171e4c7d7..2b9bdc8fc9 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/hwaccels.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/hwaccels.h @@ -26,6 +26,7 @@ extern const struct FFHWAccel ff_av1_dxva2_hwaccel; extern const struct FFHWAccel ff_av1_nvdec_hwaccel; extern const struct FFHWAccel ff_av1_vaapi_hwaccel; extern const struct FFHWAccel ff_av1_vdpau_hwaccel; +extern const struct FFHWAccel ff_av1_videotoolbox_hwaccel; extern const struct FFHWAccel ff_av1_vulkan_hwaccel; extern const struct FFHWAccel ff_h263_vaapi_hwaccel; extern const struct FFHWAccel ff_h263_videotoolbox_hwaccel; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/Makefile b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/Makefile index 27befce929..1f1fa03329 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/Makefile +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/Makefile @@ -33,6 +33,8 @@ RVV-OBJS-$(CONFIG_H264CHROMA) += riscv/h264_mc_chroma.o OBJS-$(CONFIG_H264DSP) += riscv/h264dsp_init.o RVV-OBJS-$(CONFIG_H264DSP) += riscv/h264addpx_rvv.o riscv/h264dsp_rvv.o \ riscv/h264idct_rvv.o +OBJS-$(CONFIG_H264QPEL) += riscv/h264qpel_init.o +RVV-OBJS-$(CONFIG_H264QPEL) += riscv/h264qpel_rvv.o OBJS-$(CONFIG_HUFFYUV_DECODER) += riscv/huffyuvdsp_init.o RVV-OBJS-$(CONFIG_HUFFYUV_DECODER) += riscv/huffyuvdsp_rvv.o OBJS-$(CONFIG_IDCTDSP) += riscv/idctdsp_init.o diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_init.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_init.c index 9ffc9b0333..30dd272d6e 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_init.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_init.c @@ -96,14 +96,31 @@ av_cold void ff_h264dsp_init_riscv(H264DSPContext *dsp, const int bit_depth, if (flags & AV_CPU_FLAG_RVV_I32) { const bool zvl128b = ff_rv_vlen_least(128); - if (bit_depth == 8 && zvl128b) { - for (int i = 0; i < 4; i++) { - dsp->weight_h264_pixels_tab[i] = - ff_h264_weight_funcs_8_rvv[i].weight; - dsp->biweight_h264_pixels_tab[i] = - ff_h264_weight_funcs_8_rvv[i].biweight; + if (bit_depth == 8) { + if (zvl128b) { + if (flags & AV_CPU_FLAG_RVB) + dsp->weight_h264_pixels_tab[0] = + ff_h264_weight_funcs_8_rvv[0].weight; + dsp->biweight_h264_pixels_tab[0] = + ff_h264_weight_funcs_8_rvv[0].biweight; } + if (flags & AV_CPU_FLAG_RVV_I64) { + dsp->weight_h264_pixels_tab[1] = + ff_h264_weight_funcs_8_rvv[1].weight; + dsp->biweight_h264_pixels_tab[1] = + ff_h264_weight_funcs_8_rvv[1].biweight; + } + dsp->weight_h264_pixels_tab[2] = + ff_h264_weight_funcs_8_rvv[2].weight; + dsp->biweight_h264_pixels_tab[2] = + ff_h264_weight_funcs_8_rvv[2].biweight; + dsp->weight_h264_pixels_tab[3] = + ff_h264_weight_funcs_8_rvv[3].weight; + dsp->biweight_h264_pixels_tab[3] = + ff_h264_weight_funcs_8_rvv[3].biweight; + } + if (bit_depth == 8 && zvl128b) { dsp->h264_v_loop_filter_luma = ff_h264_v_loop_filter_luma_8_rvv; dsp->h264_h_loop_filter_luma = ff_h264_h_loop_filter_luma_8_rvv; dsp->h264_h_loop_filter_luma_mbaff = diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_rvv.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_rvv.S index 422ac02222..079896b17b 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_rvv.S +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264dsp_rvv.S @@ -28,20 +28,30 @@ #include "libavutil/riscv/asm.S" -func ff_h264_weight_pixels_simple_8_rvv, zve32x + .variant_cc ff_h264_weight_pixels_simple_8_rvv +func ff_h264_weight_pixels_simple_8_rvv, zve32x, b csrwi vxrm, 0 sll a5, a5, a3 1: - vsetvli zero, a6, e16, m2, ta, ma - vle8.v v8, (a0) - addi a2, a2, -1 + vsetvli zero, t6, e16, m2, ta, ma + add t0, a0, a1 + vle8.v v8, (a0) + addi a2, a2, -2 + vle8.v v9, (t0) vzext.vf2 v24, v8 + vzext.vf2 v26, v9 vmul.vx v16, v24, a4 + vmul.vx v18, v26, a4 vsadd.vx v16, v16, a5 - vmax.vx v16, v16, zero - vsetvli zero, zero, e8, m1, ta, ma + vsadd.vx v18, v18, a5 + vmax.vx v16, v16, zero + vmax.vx v18, v18, zero + vsetvli zero, zero, e8, m1, ta, ma vnclipu.wx v8, v16, a3 - vse8.v v8, (a0) + vnclipu.wx v9, v18, a3 + vse8.v v8, (a0) + vse8.v v9, (t0) + sh1add a0, a1, a0 add a0, a0, a1 bnez a2, 1b @@ -76,103 +86,77 @@ func ff_h264_biweight_pixels_simple_8_rvv, zve32x ret endfunc -func ff_h264_weight_pixels_8_rvv, zve32x +.macro h264_weight depth, w, b= +func ff_h264_weight_pixels\w\()_\depth\()_rvv, zve64x + lpad 0 + .ifb \b + li t6, \w + j ff_h264_weight_pixels_simple_\depth\()_rvv + .else csrwi vxrm, 0 sll a5, a5, a3 1: - mv t0, a0 - mv t6, a6 -2: - vsetvli t2, a2, e16, m8, ta, ma - vlsseg2e8.v v0, (t0), a1 - addi t6, t6, -2 - vzext.vf2 v16, v0 - vzext.vf2 v24, v4 - vmul.vx v16, v16, a4 - vmul.vx v24, v24, a4 + vsetvli t1, a2, e\b, m2, ta, ma + vlse\b\().v v8, (a0), a1 + vsetvli t0, zero, e16, m4, ta, ma + vzext.vf2 v24, v8 + sub a2, a2, t1 + vmul.vx v16, v24, a4 + mul t2, t1, a1 vsadd.vx v16, v16, a5 - vsadd.vx v24, v24, a5 vmax.vx v16, v16, zero - vmax.vx v24, v24, zero - vsetvli zero, zero, e8, m4, ta, ma - vnclipu.wx v0, v16, a3 - vnclipu.wx v4, v24, a3 - vssseg2e8.v v0, (t0), a1 - addi t0, t0, 2 - bnez t6, 2b - - mul t3, a1, t2 - sub a2, a2, t2 - add a0, a0, t3 + vsetvli zero, zero, e8, m2, ta, ma + vnclipu.wx v8, v16, a3 + vsetvli zero, t1, e\b, m2, ta, ma + vsse\b\().v v8, (a0), a1 + add a0, a0, t2 bnez a2, 1b ret + .endif endfunc - .variant_cc ff_h264_biweight_pixels_8_rvv -func ff_h264_biweight_pixels_8_rvv, zve32x +func ff_h264_biweight_pixels\w\()_\depth\()_rvv, zve64x + lpad 0 + li t6, \w + .ifb \b + j ff_h264_biweight_pixels_simple_8_rvv + .else csrwi vxrm, 2 addi a7, a7, 1 ori a7, a7, 1 sll a7, a7, a4 addi a4, a4, 1 1: - mv t0, a0 - mv t1, a1 - mv t5, t6 -2: - vsetvli t2, a3, e16, m8, ta, ma - vlsseg2e8.v v0, (t0), a2 - vlsseg2e8.v v8, (t1), a2 - addi t5, t5, -2 - vmv.v.x v16, a7 - vmv.v.x v24, a7 - vsetvli zero, zero, e8, m4, ta, ma - vwmaccsu.vx v16, a5, v0 - vwmaccsu.vx v24, a5, v4 - vwmaccsu.vx v16, a6, v8 - vwmaccsu.vx v24, a6, v12 - vsetvli zero, zero, e16, m8, ta, ma + vsetvli t1, a3, e\b, m2, ta, ma + vlse\b\().v v8, (a0), a2 + sub a3, a3, t1 + vlse\b\().v v12, (a1), a2 + mul t2, t1, a2 + vsetvli t0, zero, e16, m4, ta, ma + vmv.v.x v16, a7 + vsetvli zero, zero, e8, m2, ta, ma + vwmaccsu.vx v16, a5, v8 + add a1, a1, t2 + vwmaccsu.vx v16, a6, v12 + vsetvli zero, zero, e16, m4, ta, ma vmax.vx v16, v16, zero - vmax.vx v24, v24, zero - vsetvli zero, zero, e8, m4, ta, ma - vnclipu.wx v0, v16, a4 - vnclipu.wx v4, v24, a4 - vssseg2e8.v v0, (t0), a2 - addi t0, t0, 2 - addi t1, t1, 2 - bnez t5, 2b - - mul t3, a2, t2 - sub a3, a3, t2 - add a0, a0, t3 - add a1, a1, t3 + vsetvli zero, zero, e8, m2, ta, ma + vnclipu.wx v8, v16, a4 + vsetvli zero, t1, e\b, m2, ta, ma + vsse\b\().v v8, (a0), a2 + add a0, a0, t2 + .endif bnez a3, 1b ret endfunc +.endm -.irp w, 16, 8, 4, 2 -func ff_h264_weight_pixels\w\()_8_rvv, zve32x - lpad 0 - li a6, \w - .if \w == 16 - j ff_h264_weight_pixels_simple_8_rvv - .else - j ff_h264_weight_pixels_8_rvv - .endif -endfunc - -func ff_h264_biweight_pixels\w\()_8_rvv, zve32x - lpad 0 - li t6, \w - .if \w == 16 - j ff_h264_biweight_pixels_simple_8_rvv - .else - j ff_h264_biweight_pixels_8_rvv - .endif -endfunc -.endr +h264_weight 8, 2, 16 +h264_weight 8, 4, 32 +h264_weight 8, 8, 64 +h264_weight 8, 16 .global ff_h264_weight_funcs_8_rvv .hidden ff_h264_weight_funcs_8_rvv diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_init.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_init.c new file mode 100644 index 0000000000..ad407ebff6 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_init.c @@ -0,0 +1,113 @@ +/* + * RISC-V optimised DSP functions + * Copyright (c) 2024 Niklas Haas + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "config.h" +#include "libavutil/attributes.h" +#include "libavutil/riscv/cpu.h" +#include "libavcodec/h264qpel.h" + +#define DECL_QPEL_OPS(OP, SIZE, EXT) \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc00_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc10_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc20_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc30_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc01_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc11_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc21_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc31_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc02_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc12_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc22_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc32_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc03_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc13_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc23_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); \ +void ff_ ## OP ## _h264_qpel ## SIZE ## _mc33_ ## EXT(uint8_t *dst, const uint8_t *src, ptrdiff_t stride); + +DECL_QPEL_OPS(put, 16, rvv256) +DECL_QPEL_OPS(put, 8, rvv256) +// DECL_QPEL_OPS(put, 4, rvv256) + +DECL_QPEL_OPS(avg, 16, rvv256) +DECL_QPEL_OPS(avg, 8, rvv256) +// DECL_QPEL_OPS(avg, 4, rvv256) + +DECL_QPEL_OPS(put, 16, rvv) +DECL_QPEL_OPS(put, 8, rvv) +DECL_QPEL_OPS(put, 4, rvv) + +DECL_QPEL_OPS(avg, 16, rvv) +DECL_QPEL_OPS(avg, 8, rvv) +DECL_QPEL_OPS(avg, 4, rvv) + +#define SET_QPEL_FNS(OP, IDX, SIZE, EXT) \ +do { \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 0] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc00_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 1] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc10_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 2] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc20_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 3] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc30_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 4] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc01_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 5] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc11_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 6] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc21_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 7] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc31_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 8] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc02_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][ 9] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc12_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][10] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc22_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][11] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc32_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][12] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc03_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][13] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc13_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][14] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc23_ ## EXT; \ + c->OP ## _h264_qpel_pixels_tab[IDX][15] = ff_ ## OP ## _h264_qpel ## SIZE ## _mc33_ ## EXT; \ +} while (0) + +av_cold void ff_h264qpel_init_riscv(H264QpelContext *c, int bit_depth) +{ +#if HAVE_RVV + int flags = av_get_cpu_flags(); + if (flags & AV_CPU_FLAG_RVV_I32) { + const int vlen = 8 * ff_get_rv_vlenb(); + + switch (bit_depth) { + case 8: + if (vlen >= 256) { + SET_QPEL_FNS(put, 0, 16, rvv256); + SET_QPEL_FNS(put, 1, 8, rvv256); + SET_QPEL_FNS(put, 2, 4, rvv); + + SET_QPEL_FNS(avg, 0, 16, rvv256); + SET_QPEL_FNS(avg, 1, 8, rvv256); + SET_QPEL_FNS(avg, 2, 4, rvv); + } else if (vlen >= 128) { + SET_QPEL_FNS(put, 0, 16, rvv); + SET_QPEL_FNS(put, 1, 8, rvv); + SET_QPEL_FNS(put, 2, 4, rvv); + + SET_QPEL_FNS(avg, 0, 16, rvv); + SET_QPEL_FNS(avg, 1, 8, rvv); + SET_QPEL_FNS(avg, 2, 4, rvv); + } + break; + } + } +#endif +} diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_rvv.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_rvv.S new file mode 100644 index 0000000000..77a534767c --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/h264qpel_rvv.S @@ -0,0 +1,464 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 Niklas Haas + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "libavutil/riscv/asm.S" + +.macro lx rd, addr +#if (__riscv_xlen == 32) + lw \rd, \addr +#elif (__riscv_xlen == 64) + ld \rd, \addr +#else + lq \rd, \addr +#endif +.endm + +.macro sx rd, addr +#if (__riscv_xlen == 32) + sw \rd, \addr +#elif (__riscv_xlen == 64) + sd \rd, \addr +#else + sq \rd, \addr +#endif +.endm + + /* output is unclipped; clobbers v26-v31 plus t0 and t02 */ +.macro lowpass_h vdst, src + addi t4, \src, 3 + lbu t5, 2(\src) + vle8.v v31, (t4) + lbu t4, 1(\src) + vslide1up.vx v30, v31, t5 + lbu t5, 0(\src) + vslide1up.vx v29, v30, t4 + lbu t4, -1(\src) + vslide1up.vx v28, v29, t5 + lbu t5, -2(\src) + vslide1up.vx v27, v28, t4 + vslide1up.vx v26, v27, t5 + vwaddu.vv \vdst, v26, v31 + vwmaccu.vx \vdst, t6, v28 + vwmaccu.vx \vdst, t6, v29 + vwmaccsu.vx \vdst, a7, v27 + vwmaccsu.vx \vdst, a7, v30 +.endm + + /* output is unclipped */ +.macro lowpass_v vdst, vsrc0, vsrc1, vsrc2, vsrc3, vsrc4, vsrc5, signed=0 + .if \signed + vwadd.vv \vdst, \vsrc0, \vsrc5 + vwmacc.vx \vdst, t6, \vsrc2 + vwmacc.vx \vdst, t6, \vsrc3 + vwmacc.vx \vdst, a7, \vsrc1 + vwmacc.vx \vdst, a7, \vsrc4 + .else + vwaddu.vv \vdst, \vsrc0, \vsrc5 + vwmaccu.vx \vdst, t6, \vsrc2 + vwmaccu.vx \vdst, t6, \vsrc3 + vwmaccsu.vx \vdst, a7, \vsrc1 + vwmaccsu.vx \vdst, a7, \vsrc4 + .endif +.endm + +.macro qpel_mc00 op, dst, src, stride, size +func ff_\op\()_h264_qpel_pixels, zve32x +1: add t1, a2, a1 + add t2, a2, t1 + add t3, a2, t2 + vle8.v v0, (a1) + vle8.v v1, (t1) + vle8.v v2, (t2) + vle8.v v3, (t3) + addi a4, a4, -4 + add a1, a2, t3 + add t1, a2, a0 + add t2, a2, t1 + add t3, a2, t2 + .ifc \op, avg + vle8.v v4, (a0) + vle8.v v5, (t1) + vle8.v v6, (t2) + vle8.v v7, (t3) + vaaddu.vv v0, v0, v4 + vaaddu.vv v1, v1, v5 + vaaddu.vv v2, v2, v6 + vaaddu.vv v3, v3, v7 + .endif + vse8.v v0, (a0) + vse8.v v1, (t1) + vse8.v v2, (t2) + vse8.v v3, (t3) + add a0, a2, t3 + bnez a4, 1b + jr t0 +endfunc +.endm + + qpel_mc00 put, a0, a1, a2, a4 + qpel_mc00 avg, a0, a1, a2, a4 + +.macro qpel_lowpass op, ext, lmul, lmul2 +func ff_\op\()_h264_qpel_h_lowpass_\lmul\ext, zve32x +1: add t1, a3, a1 + add t2, a3, t1 + add t3, a3, t2 + lowpass_h v0, a1 + lowpass_h v2, t1 + lowpass_h v4, t2 + lowpass_h v6, t3 + add a1, a3, t3 + addi a4, a4, -4 + vsetvli zero, zero, e16, \lmul2, ta, ma + vmax.vx v0, v0, zero + vmax.vx v2, v2, zero + vmax.vx v4, v4, zero + vmax.vx v6, v6, zero + vsetvli zero, zero, e8, \lmul, ta, ma + vnclipu.wi v0, v0, 5 + vnclipu.wi v2, v2, 5 + vnclipu.wi v4, v4, 5 + vnclipu.wi v6, v6, 5 + .ifc \ext, _l2 + add t1, a6, a5 + add t2, a6, t1 + add t3, a6, t2 + vle8.v v8, (a5) + vle8.v v10, (t1) + vle8.v v12, (t2) + vle8.v v14, (t3) + add a5, a2, t3 + vaaddu.vv v0, v0, v8 + vaaddu.vv v2, v2, v10 + vaaddu.vv v4, v4, v12 + vaaddu.vv v6, v6, v14 + .endif + add t1, a2, a0 + add t2, a2, t1 + add t3, a2, t2 + .ifc \op, avg + vle8.v v1, (a0) + vle8.v v3, (t1) + vle8.v v5, (t2) + vle8.v v7, (t3) + vaaddu.vv v0, v0, v1 + vaaddu.vv v2, v2, v3 + vaaddu.vv v4, v4, v5 + vaaddu.vv v6, v6, v7 + .endif + vse8.v v0, (a0) + vse8.v v2, (t1) + vse8.v v4, (t2) + vse8.v v6, (t3) + add a0, a2, t3 + bnez a4, 1b + jr t0 +endfunc + +func ff_\op\()_h264_qpel_v_lowpass_\lmul\ext, zve32x + sub t1, a1, a3 + sub t2, t1, a3 + vle8.v v2, (a1) + vle8.v v1, (t1) + vle8.v v0, (t2) + add t1, a1, a3 + add t2, t1, a3 + add a1, t2, a3 + vle8.v v3, (t1) + vle8.v v4, (t2) +1: add t1, a3, a1 + add t2, a3, t1 + add t3, a3, t2 + vle8.v v5, (a1) + vle8.v v6, (t1) + vle8.v v7, (t2) + vle8.v v8, (t3) + add a1, a3, t3 + lowpass_v v24, v0, v1, v2, v3, v4, v5 + lowpass_v v26, v1, v2, v3, v4, v5, v6 + lowpass_v v28, v2, v3, v4, v5, v6, v7 + lowpass_v v30, v3, v4, v5, v6, v7, v8 + addi a4, a4, -4 + vsetvli zero, zero, e16, \lmul2, ta, ma + vmax.vx v24, v24, zero + vmax.vx v26, v26, zero + vmax.vx v28, v28, zero + vmax.vx v30, v30, zero + vsetvli zero, zero, e8, \lmul, ta, ma + vnclipu.wi v24, v24, 5 + vnclipu.wi v26, v26, 5 + vnclipu.wi v28, v28, 5 + vnclipu.wi v30, v30, 5 + .ifc \ext, _l2 + add t1, a6, a5 + add t2, a6, t1 + add t3, a6, t2 + vle8.v v9, (a5) + vle8.v v10, (t1) + vle8.v v11, (t2) + vle8.v v12, (t3) + add a5, a6, t3 + vaaddu.vv v24, v24, v9 + vaaddu.vv v26, v26, v10 + vaaddu.vv v28, v28, v11 + vaaddu.vv v30, v30, v12 + .endif + add t1, a2, a0 + add t2, a2, t1 + add t3, a2, t2 + .ifc \op, avg + vle8.v v9, (a0) + vle8.v v10, (t1) + vle8.v v11, (t2) + vle8.v v12, (t3) + vaaddu.vv v24, v24, v9 + vaaddu.vv v26, v26, v10 + vaaddu.vv v28, v28, v11 + vaaddu.vv v30, v30, v12 + .endif + vse8.v v24, (a0) + vse8.v v26, (t1) + vse8.v v28, (t2) + vse8.v v30, (t3) + add a0, a2, t3 + vmv.v.v v0, v4 + vmv.v.v v1, v5 + vmv.v.v v2, v6 + vmv.v.v v3, v7 + vmv.v.v v4, v8 + bnez a4, 1b + jr t0 +endfunc + +func ff_\op\()_h264_qpel_hv_lowpass_\lmul\ext, zve32x + sub t1, a1, a3 + sub t2, t1, a3 + lowpass_h v4, a1 + lowpass_h v2, t1 + lowpass_h v0, t2 + add t1, a1, a3 + add t2, t1, a3 + add a1, t2, a3 + lowpass_h v6, t1 + lowpass_h v8, t2 +1: add t1, a3, a1 + add t2, a3, t1 + add t3, a3, t2 + lowpass_h v10, a1 + lowpass_h v12, t1 + lowpass_h v14, t2 + lowpass_h v16, t3 + vsetvli zero, zero, e16, \lmul2, ta, ma + addi a4, a4, -4 + lowpass_v v20, v0, v2, v4, v6, v8, v10, signed=1 + lowpass_v v24, v2, v4, v6, v8, v10, v12, signed=1 + lowpass_v v28, v4, v6, v8, v10, v12, v14, signed=1 + vnclip.wi v0, v20, 10 + lowpass_v v20, v6, v8, v10, v12, v14, v16, signed=1 + vnclip.wi v2, v24, 10 + vnclip.wi v4, v28, 10 + vnclip.wi v6, v20, 10 + vmax.vx v18, v0, zero + vmax.vx v20, v2, zero + vmax.vx v22, v4, zero + vmax.vx v24, v6, zero + vmv.v.v v0, v8 + vmv.v.v v2, v10 + vmv.v.v v4, v12 + vmv.v.v v6, v14 + vmv.v.v v8, v16 + add a1, a3, t3 + vsetvli zero, zero, e8, \lmul, ta, ma + vnclipu.wi v18, v18, 0 + vnclipu.wi v20, v20, 0 + vnclipu.wi v22, v22, 0 + vnclipu.wi v24, v24, 0 + .ifc \ext, _l2 + add t1, a6, a5 + add t2, a6, t1 + add t3, a6, t2 + vle8.v v26, (a5) + vle8.v v27, (t1) + vle8.v v28, (t2) + vle8.v v29, (t3) + add a5, a6, t3 + vaaddu.vv v18, v18, v26 + vaaddu.vv v20, v20, v27 + vaaddu.vv v22, v22, v28 + vaaddu.vv v24, v24, v29 + .endif + add t1, a2, a0 + add t2, a2, t1 + add t3, a2, t2 + .ifc \op, avg + vle8.v v26, (a0) + vle8.v v27, (t1) + vle8.v v28, (t2) + vle8.v v29, (t3) + vaaddu.vv v18, v18, v26 + vaaddu.vv v20, v20, v27 + vaaddu.vv v22, v22, v28 + vaaddu.vv v24, v24, v29 + .endif + vse8.v v18, (a0) + vse8.v v20, (t1) + vse8.v v22, (t2) + vse8.v v24, (t3) + add a0, a2, t3 + bnez a4, 1b + jr t0 +endfunc +.endm + +/* Note: We could possibly specialize for the width 8 / width 4 cases by + loading 32 bit integers, but this makes the convolutions more complicated + to implement, so it's not necessarily any faster. */ + +.macro h264_qpel lmul, lmul2 + qpel_lowpass put, , \lmul, \lmul2 + qpel_lowpass put, _l2, \lmul, \lmul2 + qpel_lowpass avg, , \lmul, \lmul2 + qpel_lowpass avg, _l2, \lmul, \lmul2 +.endm + + h264_qpel m1, m2 + h264_qpel mf2, m1 + h264_qpel mf4, mf2 + h264_qpel mf8, mf4 + +.macro h264_qpel_1pass op, case, lmul, size, ext=rvv, dir, offset +func ff_\op\()_h264_qpel\size\()_\case\()_\ext, zve32x + lpad 0 + vsetivli zero, \size, e8, \lmul, ta, ma + csrwi vxrm, 0 + li a4, \size + li t6, 20 + li a7, -5 + mv a3, a2 + mv t0, ra +.ifnb \offset + .ifc \dir, v + add a5, a1, \offset + .else + addi a5, a1, \offset + .endif + mv a6, a3 + j ff_\op\()_h264_qpel_\dir\()_lowpass_\lmul\()_l2 +.else + j ff_\op\()_h264_qpel_\dir\()_lowpass_\lmul\() +.endif +endfunc +.endm + +.macro h264_qpel_2pass op, case, lmul, size, ext=rvv, dir1, dir2, off1=0, off2 +func ff_\op\()_h264_qpel\size\()_\case\()_\ext, zve32x + lpad 0 + vsetivli zero, \size, e8, \lmul, ta, ma + csrwi vxrm, 0 + addi sp, sp, (-(__riscv_xlen >> 2)) + li a4, \size + li t6, 20 + li a7, -5 + sx a0, 0(sp) + sx a1, (__riscv_xlen >> 3)(sp) + .ifc \off1, a2 + add a1, a1, \off1 + .elseif \off1 + addi a1, a1, \off1 + .endif + mv a3, a2 + .ifc \op, avg + // Use temporary array on stack for the first pass + addi a0, sp, -(\size * \size) + li a2, \size + .endif + jal t0, ff_put_h264_qpel_\dir1\()_lowpass_\lmul + lx a0, 0(sp) + lx a1, (__riscv_xlen >> 3)(sp) + .ifc \op, put + // Directly reuse the first pass output buffer + mv a5, a0 + mv a6, a2 + .else + addi a5, sp, -(\size * \size) + li a6, \size + mv a2, a3 + .endif + .ifnb \off2 + addi a1, a1, \off2 + .endif + li a4, \size + mv t0, ra + addi sp, sp, 16 + j ff_\op\()_h264_qpel_\dir2\()_lowpass_\lmul\()_l2 +endfunc +.endm + +.macro ff_h264_qpel_fns op, lmul, size, ext=rvv +func ff_\op\()_h264_qpel\size\()_mc00_\ext, zve32x + lpad 0 + vsetivli zero, \size, e8, \lmul, ta, ma + csrwi vxrm, 0 + li a4, \size + mv t0, ra + j ff_\op\()_h264_qpel_pixels +endfunc + + h264_qpel_1pass \op, mc20, \lmul, \size, \ext, h + h264_qpel_1pass \op, mc02, \lmul, \size, \ext, v + h264_qpel_1pass \op, mc10, \lmul, \size, \ext, h, 0 + h264_qpel_1pass \op, mc30, \lmul, \size, \ext, h, 1 + h264_qpel_1pass \op, mc01, \lmul, \size, \ext, v, zero + h264_qpel_1pass \op, mc03, \lmul, \size, \ext, v, a2 + h264_qpel_1pass \op, mc22, \lmul, \size, \ext, hv + + h264_qpel_2pass \op, mc11, \lmul, \size, \ext, h, v + h264_qpel_2pass \op, mc21, \lmul, \size, \ext, h, hv + h264_qpel_2pass \op, mc12, \lmul, \size, \ext, v, hv + h264_qpel_2pass \op, mc31, \lmul, \size, \ext, h, v, off2=1 + h264_qpel_2pass \op, mc13, \lmul, \size, \ext, h, v, a2 + h264_qpel_2pass \op, mc33, \lmul, \size, \ext, h, v, a2, 1 + h264_qpel_2pass \op, mc23, \lmul, \size, \ext, h, hv, a2 + h264_qpel_2pass \op, mc32, \lmul, \size, \ext, v, hv, 1 +.endm + + ff_h264_qpel_fns put, mf2, 16, rvv256 + ff_h264_qpel_fns put, mf4, 8, rvv256 + /* ff_h264_qpel_fns put, mf8, 4, rvv256 */ + + ff_h264_qpel_fns avg, mf2, 16, rvv256 + ff_h264_qpel_fns avg, mf4, 8, rvv256 + /* ff_h264_qpel_fns avg, mf8, 4, rvv256 */ + + ff_h264_qpel_fns put, m1, 16, rvv + ff_h264_qpel_fns put, mf2, 8, rvv + ff_h264_qpel_fns put, mf4, 4, rvv + + ff_h264_qpel_fns avg, m1, 16, rvv + ff_h264_qpel_fns avg, mf2, 8, rvv + ff_h264_qpel_fns avg, mf4, 4, rvv diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/Makefile b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/Makefile new file mode 100644 index 0000000000..582b051579 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/Makefile @@ -0,0 +1,2 @@ +OBJS-$(CONFIG_VVC_DECODER) += riscv/vvc/vvcdsp_init.o +RVV-OBJS-$(CONFIG_VVC_DECODER) += riscv/vvc/vvc_mc_rvv.o diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvc_mc_rvv.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvc_mc_rvv.S new file mode 100644 index 0000000000..45f4750f82 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvc_mc_rvv.S @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2024 Institue of Software Chinese Academy of Sciences (ISCAS). + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libavutil/riscv/asm.S" + +.macro vsetvlstatic8 w, vlen + .if \w == 2 && \vlen == 128 + vsetivli zero, \w, e8, mf8, ta, ma + .elseif \w == 4 && \vlen == 128 + vsetivli zero, \w, e8, mf4, ta, ma + .elseif \w == 8 && \vlen == 128 + vsetivli zero, \w, e8, mf2, ta, ma + .elseif \w == 16 && \vlen == 128 + vsetivli zero, \w, e8, m1, ta, ma + .elseif \w == 32 && \vlen == 128 + li t0, \w + vsetvli zero, t0, e8, m2, ta, ma + .elseif \w <= 4 && \vlen == 256 + vsetivli zero, \w, e8, mf8, ta, ma + .elseif \w == 8 && \vlen == 256 + vsetivli zero, \w, e8, mf4, ta, ma + .elseif \w == 16 && \vlen == 256 + vsetivli zero, \w, e8, mf2, ta, ma + .elseif \w == 32 && \vlen == 256 + li t0, \w + vsetvli zero, t0, e8, m1, ta, ma + .elseif \w == 64 && \vlen == 256 + li t0, \w + vsetvli zero, t0, e8, m2, ta, ma + .else + li t0, \w + vsetvli zero, t0, e8, m4, ta, ma + .endif +.endm + +.macro vsetvlstatic16 w, vlen + .if \w == 2 && \vlen == 128 + vsetivli zero, \w, e16, mf4, ta, ma + .elseif \w == 4 && \vlen == 128 + vsetivli zero, \w, e16, mf2, ta, ma + .elseif \w == 8 && \vlen == 128 + vsetivli zero, \w, e16, m1, ta, ma + .elseif \w == 16 && \vlen == 128 + vsetivli zero, \w, e16, m2, ta, ma + .elseif \w == 32 && \vlen == 128 + li t0, \w + vsetvli zero, t0, e16, m4, ta, ma + .elseif \w <= 4 && \vlen == 256 + vsetivli zero, \w, e16, mf4, ta, ma + .elseif \w == 8 && \vlen == 256 + vsetivli zero, \w, e16, mf2, ta, ma + .elseif \w == 16 && \vlen == 256 + vsetivli zero, \w, e16, m1, ta, ma + .elseif \w == 32 && \vlen == 256 + li t0, \w + vsetvli zero, t0, e16, m2, ta, ma + .elseif \w == 64 && \vlen == 256 + li t0, \w + vsetvli zero, t0, e16, m4, ta, ma + .else + li t0, \w + vsetvli zero, t0, e16, m8, ta, ma + .endif +.endm + +.macro vsetvlstatic32 w, vlen + .if \w == 2 + vsetivli zero, \w, e32, mf2, ta, ma + .elseif \w == 4 && \vlen == 128 + vsetivli zero, \w, e32, m1, ta, ma + .elseif \w == 8 && \vlen == 128 + vsetivli zero, \w, e32, m2, ta, ma + .elseif \w == 16 && \vlen == 128 + vsetivli zero, \w, e32, m4, ta, ma + .elseif \w == 4 && \vlen == 256 + vsetivli zero, \w, e32, mf2, ta, ma + .elseif \w == 8 && \vlen == 256 + vsetivli zero, \w, e32, m1, ta, ma + .elseif \w == 16 && \vlen == 256 + vsetivli zero, \w, e32, m2, ta, ma + .elseif \w == 32 && \vlen == 256 + li t0, \w + vsetvli zero, t0, e32, m4, ta, ma + .else + li t0, \w + vsetvli zero, t0, e32, m8, ta, ma + .endif +.endm + +.macro avg w, vlen, id +\id\w\vlen: +.if \w < 128 + vsetvlstatic16 \w, \vlen + addi t0, a2, 128*2 + addi t1, a3, 128*2 + add t2, a0, a1 + vle16.v v0, (a2) + vle16.v v8, (a3) + addi a5, a5, -2 + vle16.v v16, (t0) + vle16.v v24, (t1) + vadd.vv v8, v8, v0 + vadd.vv v24, v24, v16 + vmax.vx v8, v8, zero + vmax.vx v24, v24, zero + vsetvlstatic8 \w, \vlen + addi a2, a2, 128*4 + vnclipu.wi v8, v8, 7 + vnclipu.wi v24, v24, 7 + addi a3, a3, 128*4 + vse8.v v8, (a0) + vse8.v v24, (t2) + sh1add a0, a1, a0 +.else + addi a5, a5, -1 + mv t1, a0 + mv t2, a2 + mv t3, a3 + mv t4, a4 +1: + vsetvli t0, a4, e16, m8, ta, ma + sub a4, a4, t0 + vle16.v v0, (a2) + vle16.v v8, (a3) + vadd.vv v8, v8, v0 + vmax.vx v8, v8, zero + vsetvli zero, zero, e8, m4, ta, ma + vnclipu.wi v8, v8, 7 + vse8.v v8, (a0) + sh1add a2, t0, a2 + sh1add a3, t0, a3 + add a0, a0, t0 + bnez a4, 1b + add a0, t1, a1 + addi a2, t2, 128*2 + addi a3, t3, 128*2 + mv a4, t4 +.endif + bnez a5, \id\w\vlen\()b + ret +.endm + + +.macro AVG_JMP_TABLE id, vlen +const jmp_table_\id\vlen + .4byte \id\()2\vlen\()f - jmp_table_\id\vlen + .4byte \id\()4\vlen\()f - jmp_table_\id\vlen + .4byte \id\()8\vlen\()f - jmp_table_\id\vlen + .4byte \id\()16\vlen\()f - jmp_table_\id\vlen + .4byte \id\()32\vlen\()f - jmp_table_\id\vlen + .4byte \id\()64\vlen\()f - jmp_table_\id\vlen + .4byte \id\()128\vlen\()f - jmp_table_\id\vlen +endconst +.endm + +.macro AVG_J vlen, id + clz t1, a4 + neg t1, t1 + lla t5, jmp_table_\id\vlen + sh2add t1, t1, t5 + lw t1, ((__riscv_xlen-2)<<2)(t1) + add t1, t1, t5 + jr t1 +.endm + +.macro func_avg vlen +func ff_vvc_avg_8_rvv_\vlen\(), zve32x, zbb, zba + lpad 0 + AVG_JMP_TABLE 1, \vlen + csrwi vxrm, 0 + AVG_J \vlen, 1 + .irp w,2,4,8,16,32,64,128 + avg \w, \vlen, 1 + .endr +endfunc +.endm + +func_avg 128 +func_avg 256 + +#if (__riscv_xlen == 64) +.macro w_avg w, vlen, id +\id\w\vlen: +.if \w <= 32 || (\w == 64 && \vlen == 256) + vsetvlstatic16 \w, \vlen + addi t0, a2, 128*2 + addi t1, a3, 128*2 + vle16.v v0, (a2) + vle16.v v4, (a3) + addi a5, a5, -2 + vle16.v v8, (t0) + vle16.v v12, (t1) + vwmul.vx v16, v0, a7 + vwmul.vx v24, v8, a7 + vwmacc.vx v16, t3, v4 + vwmacc.vx v24, t3, v12 + vsetvlstatic32 \w, \vlen + add t2, a0, a1 + vadd.vx v16, v16, t4 + vadd.vx v24, v24, t4 + vsetvlstatic16 \w, \vlen + vnsrl.wx v16, v16, t6 + vnsrl.wx v24, v24, t6 + vmax.vx v16, v16, zero + vmax.vx v24, v24, zero + vsetvlstatic8 \w, \vlen + addi a2, a2, 128*4 + vnclipu.wi v16, v16, 0 + vnclipu.wi v24, v24, 0 + vse8.v v16, (a0) + addi a3, a3, 128*4 + vse8.v v24, (t2) + sh1add a0, a1, a0 +.else + addi a5, a5, -1 + mv t1, a0 + mv t2, a2 + mv t5, a3 + mv a6, a4 +1: + vsetvli t0, a4, e16, m4, ta, ma + sub a4, a4, t0 + vle16.v v0, (a2) + vle16.v v4, (a3) + vwmul.vx v16, v0, a7 + vwmacc.vx v16, t3, v4 + vsetvli zero, zero, e32, m8, ta, ma + vadd.vx v16, v16, t4 + vsetvli zero, zero, e16, m4, ta, ma + vnsrl.wx v16, v16, t6 + vmax.vx v16, v16, zero + vsetvli zero, zero, e8, m2, ta, ma + vnclipu.wi v16, v16, 0 + vse8.v v16, (a0) + sh1add a2, t0, a2 + sh1add a3, t0, a3 + add a0, a0, t0 + bnez a4, 1b + add a0, t1, a1 + addi a2, t2, 128*2 + addi a3, t5, 128*2 + mv a4, a6 +.endif + bnez a5, \id\w\vlen\()b + ret +.endm + +.macro func_w_avg vlen +func ff_vvc_w_avg_8_rvv_\vlen\(), zve32x, zbb, zba + lpad 0 + AVG_JMP_TABLE 2, \vlen + csrwi vxrm, 0 + addi t6, a6, 7 + ld t3, (sp) + ld t4, 8(sp) + ld t5, 16(sp) + addi t4, t4, 1 // o0 + o1 + 1 + add t4, t4, t5 + addi t5, t6, -1 // shift - 1 + sll t4, t4, t5 + AVG_J \vlen, 2 + .irp w,2,4,8,16,32,64,128 + w_avg \w, \vlen, 2 + .endr +endfunc +.endm + +func_w_avg 128 +func_w_avg 256 +#endif diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvcdsp_init.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvcdsp_init.c new file mode 100644 index 0000000000..0a9f393259 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/riscv/vvc/vvcdsp_init.c @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Institue of Software Chinese Academy of Sciences (ISCAS). + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "libavutil/attributes.h" +#include "libavutil/cpu.h" +#include "libavutil/riscv/cpu.h" +#include "libavcodec/vvc/dsp.h" + +#define bf(fn, bd, opt) fn##_##bd##_##opt + +#define AVG_PROTOTYPES(bd, opt) \ +void bf(ff_vvc_avg, bd, opt)(uint8_t *dst, ptrdiff_t dst_stride, \ + const int16_t *src0, const int16_t *src1, int width, int height); \ +void bf(ff_vvc_w_avg, bd, opt)(uint8_t *dst, ptrdiff_t dst_stride, \ + const int16_t *src0, const int16_t *src1, int width, int height, \ + int denom, int w0, int w1, int o0, int o1); + +AVG_PROTOTYPES(8, rvv_128) +AVG_PROTOTYPES(8, rvv_256) + +void ff_vvc_dsp_init_riscv(VVCDSPContext *const c, const int bd) +{ +#if HAVE_RVV + const int flags = av_get_cpu_flags(); + int vlenb = ff_get_rv_vlenb(); + + if ((flags & AV_CPU_FLAG_RVV_I32) && (flags & AV_CPU_FLAG_RVB) && + vlenb >= 32) { + switch (bd) { + case 8: + c->inter.avg = ff_vvc_avg_8_rvv_256; +# if (__riscv_xlen == 64) + c->inter.w_avg = ff_vvc_w_avg_8_rvv_256; +# endif + break; + default: + break; + } + } else if ((flags & AV_CPU_FLAG_RVV_I32) && (flags & AV_CPU_FLAG_RVB) && + vlenb >= 16) { + switch (bd) { + case 8: + c->inter.avg = ff_vvc_avg_8_rvv_128; +# if (__riscv_xlen == 64) + c->inter.w_avg = ff_vvc_w_avg_8_rvv_128; +# endif + break; + default: + break; + } + } +#endif +} diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/tests/.gitignore new file mode 100644 index 0000000000..0df4ae10a0 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/tests/.gitignore @@ -0,0 +1,21 @@ +/av1_levels +/avcodec +/avpacket +/bitstream_be +/bitstream_le +/cabac +/celp_math +/codec_desc +/dct +/golomb +/h264_levels +/h265_levels +/htmlsubtitles +/iirfilter +/jpeg2000dwt +/mathops +/mjpegenc_huffman +/motion +/mpeg12framerate +/rangecoder +/snowenc diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/version.h index 4b7ec515fe..da6f3a84ac 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/version.h @@ -29,7 +29,7 @@ #include "version_major.h" -#define LIBAVCODEC_VERSION_MINOR 19 +#define LIBAVCODEC_VERSION_MINOR 21 #define LIBAVCODEC_VERSION_MICRO 100 #define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox.c index 505483ef66..a5a76e8327 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox.c @@ -56,6 +56,10 @@ enum { kCMVideoCodecType_HEVC = 'hvc1' }; enum { kCMVideoCodecType_VP9 = 'vp09' }; #endif +#if !HAVE_KCMVIDEOCODECTYPE_AV1 +enum { kCMVideoCodecType_AV1 = 'av01' }; +#endif + #define VIDEOTOOLBOX_ESDS_EXTRADATA_PADDING 12 typedef struct VTHWFrame { @@ -92,6 +96,26 @@ int ff_videotoolbox_buffer_copy(VTContext *vtctx, return 0; } +int ff_videotoolbox_buffer_append(VTContext *vtctx, + const uint8_t *buffer, + uint32_t size) +{ + void *tmp; + + tmp = av_fast_realloc(vtctx->bitstream, + &vtctx->allocated_size, + vtctx->bitstream_size + size); + + if (!tmp) + return AVERROR(ENOMEM); + + vtctx->bitstream = tmp; + memcpy(vtctx->bitstream + vtctx->bitstream_size, buffer, size); + vtctx->bitstream_size += size; + + return 0; +} + static int videotoolbox_postproc_frame(void *avctx, AVFrame *frame) { int ret; @@ -846,6 +870,13 @@ static CFDictionaryRef videotoolbox_decoder_config_create(CMVideoCodecType codec if (data) CFDictionarySetValue(avc_info, CFSTR("vpcC"), data); break; +#endif +#if CONFIG_AV1_VIDEOTOOLBOX_HWACCEL + case kCMVideoCodecType_AV1 : + data = ff_videotoolbox_av1c_extradata_create(avctx); + if (data) + CFDictionarySetValue(avc_info, CFSTR("av1C"), data); + break; #endif default: break; @@ -912,6 +943,9 @@ static int videotoolbox_start(AVCodecContext *avctx) case AV_CODEC_ID_VP9 : videotoolbox->cm_codec_type = kCMVideoCodecType_VP9; break; + case AV_CODEC_ID_AV1 : + videotoolbox->cm_codec_type = kCMVideoCodecType_AV1; + break; default : break; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox_av1.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox_av1.c new file mode 100644 index 0000000000..b4d39194d5 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/videotoolbox_av1.c @@ -0,0 +1,105 @@ +/* + * Videotoolbox hardware acceleration for AV1 + * Copyright (c) 2023 Jan Ekström + * Copyright (c) 2024 Ruslan Chernenko + * Copyright (c) 2024 Martin Storsjö + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libavutil/mem.h" + +#include "av1dec.h" +#include "hwaccel_internal.h" +#include "internal.h" +#include "vt_internal.h" + +CFDataRef ff_videotoolbox_av1c_extradata_create(AVCodecContext *avctx) +{ + AV1DecContext *s = avctx->priv_data; + uint8_t *buf; + CFDataRef data; + if (!s->raw_seq) + return NULL; + + buf = av_malloc(s->seq_data_ref->size + 4); + if (!buf) + return NULL; + buf[0] = 0x81; // version and marker (constant) + buf[1] = s->raw_seq->seq_profile << 5 | s->raw_seq->seq_level_idx[0]; + buf[2] = s->raw_seq->seq_tier[0] << 7 | + s->raw_seq->color_config.high_bitdepth << 6 | + s->raw_seq->color_config.twelve_bit << 5 | + s->raw_seq->color_config.mono_chrome << 4 | + s->raw_seq->color_config.subsampling_x << 3 | + s->raw_seq->color_config.subsampling_y << 2 | + s->raw_seq->color_config.chroma_sample_position; + + if (s->raw_seq->initial_display_delay_present_flag) + buf[3] = 0 << 5 | + s->raw_seq->initial_display_delay_present_flag << 4 | + s->raw_seq->initial_display_delay_minus_1[0]; + else + buf[3] = 0x00; + memcpy(buf + 4, s->seq_data_ref->data, s->seq_data_ref->size); + data = CFDataCreate(kCFAllocatorDefault, buf, s->seq_data_ref->size + 4); + av_free(buf); + return data; +}; + + +static int videotoolbox_av1_start_frame(AVCodecContext *avctx, + const uint8_t *buffer, + uint32_t size) +{ + return 0; +} + +static int videotoolbox_av1_decode_slice(AVCodecContext *avctx, + const uint8_t *buffer, + uint32_t size) +{ + return 0; +} + +static int videotoolbox_av1_end_frame(AVCodecContext *avctx) +{ + const AV1DecContext *s = avctx->priv_data; + VTContext *vtctx = avctx->internal->hwaccel_priv_data; + AVFrame *frame = s->cur_frame.f; + + vtctx->bitstream_size = 0; + for (int i = s->start_unit; i < s->nb_unit; i++) + ff_videotoolbox_buffer_append(vtctx, s->current_obu.units[i].data, + s->current_obu.units[i].data_size); + return ff_videotoolbox_common_end_frame(avctx, frame); +} + +const FFHWAccel ff_av1_videotoolbox_hwaccel = { + .p.name = "av1_videotoolbox", + .p.type = AVMEDIA_TYPE_VIDEO, + .p.id = AV_CODEC_ID_AV1, + .p.pix_fmt = AV_PIX_FMT_VIDEOTOOLBOX, + .alloc_frame = ff_videotoolbox_alloc_frame, + .start_frame = videotoolbox_av1_start_frame, + .decode_slice = videotoolbox_av1_decode_slice, + .end_frame = videotoolbox_av1_end_frame, + .frame_params = ff_videotoolbox_frame_params, + .init = ff_videotoolbox_common_init, + .uninit = ff_videotoolbox_uninit, + .priv_data_size = sizeof(VTContext), +}; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vt_internal.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vt_internal.h index 9502d7c7dc..effa96fc15 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vt_internal.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vt_internal.h @@ -56,6 +56,9 @@ int ff_videotoolbox_frame_params(AVCodecContext *avctx, int ff_videotoolbox_buffer_copy(VTContext *vtctx, const uint8_t *buffer, uint32_t size); +int ff_videotoolbox_buffer_append(VTContext *vtctx, + const uint8_t *buffer, + uint32_t size); int ff_videotoolbox_uninit(AVCodecContext *avctx); int ff_videotoolbox_h264_start_frame(AVCodecContext *avctx, const uint8_t *buffer, @@ -64,6 +67,7 @@ int ff_videotoolbox_h264_decode_slice(AVCodecContext *avctx, const uint8_t *buffer, uint32_t size); int ff_videotoolbox_common_end_frame(AVCodecContext *avctx, AVFrame *frame); +CFDataRef ff_videotoolbox_av1c_extradata_create(AVCodecContext *avctx); CFDataRef ff_videotoolbox_avcc_extradata_create(AVCodecContext *avctx); CFDataRef ff_videotoolbox_hvcc_extradata_create(AVCodecContext *avctx); CFDataRef ff_videotoolbox_vpcc_extradata_create(AVCodecContext *avctx); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.c index 433353c32c..9bfa46b03d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.c @@ -102,6 +102,8 @@ void ff_vvc_dsp_init(VVCDSPContext *vvcdsp, int bit_depth) #if ARCH_AARCH64 ff_vvc_dsp_init_aarch64(vvcdsp, bit_depth); +#elif ARCH_RISCV + ff_vvc_dsp_init_riscv(vvcdsp, bit_depth); #elif ARCH_X86 ff_vvc_dsp_init_x86(vvcdsp, bit_depth); #endif diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.h index 635ebcafed..fc4c3a6799 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/dsp.h @@ -179,6 +179,7 @@ typedef struct VVCDSPContext { void ff_vvc_dsp_init(VVCDSPContext *hpc, int bit_depth); void ff_vvc_dsp_init_aarch64(VVCDSPContext *hpc, const int bit_depth); +void ff_vvc_dsp_init_riscv(VVCDSPContext *hpc, const int bit_depth); void ff_vvc_dsp_init_x86(VVCDSPContext *hpc, const int bit_depth); #endif /* AVCODEC_VVC_DSP_H */ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/inter_template.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/inter_template.c index c073a73e76..aee4994c17 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/inter_template.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavcodec/vvc/inter_template.c @@ -472,6 +472,9 @@ static void FUNC(apply_bdof)(uint8_t *_dst, const ptrdiff_t _dst_stride, const i (filter[0] * src[x] + \ filter[1] * src[x + stride]) +#define DMVR_FILTER2(filter, src0, src1) \ + (filter[0] * src0 + filter[1] * src1) + //8.5.3.2.2 Luma sample bilinear interpolation process static void FUNC(dmvr)(int16_t *dst, const uint8_t *_src, const ptrdiff_t _src_stride, const int height, const intptr_t mx, const intptr_t my, const int width) @@ -541,31 +544,31 @@ static void FUNC(dmvr_v)(int16_t *dst, const uint8_t *_src, const ptrdiff_t _src static void FUNC(dmvr_hv)(int16_t *dst, const uint8_t *_src, const ptrdiff_t _src_stride, const int height, const intptr_t mx, const intptr_t my, const int width) { - int16_t tmp_array[(MAX_PB_SIZE + BILINEAR_EXTRA) * MAX_PB_SIZE]; - int16_t *tmp = tmp_array; + int16_t tmp_array[MAX_PB_SIZE * 2]; + int16_t *tmp0 = tmp_array; + int16_t *tmp1 = tmp_array + MAX_PB_SIZE; const pixel *src = (const pixel*)_src; const ptrdiff_t src_stride = _src_stride / sizeof(pixel); - const int8_t *filter = ff_vvc_inter_luma_dmvr_filters[mx]; + const int8_t *filter_x = ff_vvc_inter_luma_dmvr_filters[mx]; + const int8_t *filter_y = ff_vvc_inter_luma_dmvr_filters[my]; const int shift1 = BIT_DEPTH - 6; const int offset1 = 1 << (shift1 - 1); const int shift2 = 4; const int offset2 = 1 << (shift2 - 1); src -= BILINEAR_EXTRA_BEFORE * src_stride; - for (int y = 0; y < height + BILINEAR_EXTRA; y++) { - for (int x = 0; x < width; x++) - tmp[x] = (DMVR_FILTER(src, 1) + offset1) >> shift1; - src += src_stride; - tmp += MAX_PB_SIZE; - } + for (int x = 0; x < width; x++) + tmp0[x] = (DMVR_FILTER2(filter_x, src[x], src[x + 1]) + offset1) >> shift1; + src += src_stride; - tmp = tmp_array + BILINEAR_EXTRA_BEFORE * MAX_PB_SIZE; - filter = ff_vvc_inter_luma_dmvr_filters[my]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) - dst[x] = (DMVR_FILTER(tmp, MAX_PB_SIZE) + offset2) >> shift2; - tmp += MAX_PB_SIZE; + for (int y = 1; y < height + BILINEAR_EXTRA; y++) { + for (int x = 0; x < width; x++) { + tmp1[x] = (DMVR_FILTER2(filter_x, src[x], src[x + 1]) + offset1) >> shift1; + dst[x] = (DMVR_FILTER2(filter_y, tmp0[x], tmp1[x]) + offset2) >> shift2; + } + src += src_stride; dst += MAX_PB_SIZE; + FFSWAP(int16_t *, tmp0, tmp1); } } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/.gitignore new file mode 100644 index 0000000000..08ac3eb86a --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/.gitignore @@ -0,0 +1,2 @@ +/indev_list.c +/outdev_list.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/avfoundation.m b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/avfoundation.m index c5a09c6563..61dac4b713 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/avfoundation.m +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/avfoundation.m @@ -632,7 +632,6 @@ static int get_video_config(AVFormatContext *s) { AVFContext *ctx = (AVFContext*)s->priv_data; CVImageBufferRef image_buffer; - CMBlockBufferRef block_buffer; CGSize image_buffer_size; AVStream* stream = avformat_new_stream(s, NULL); @@ -652,7 +651,6 @@ static int get_video_config(AVFormatContext *s) avpriv_set_pts_info(stream, 64, 1, avf_time_base); image_buffer = CMSampleBufferGetImageBuffer(ctx->current_frame); - block_buffer = CMSampleBufferGetDataBuffer(ctx->current_frame); if (image_buffer) { image_buffer_size = CVImageBufferGetEncodedSize(image_buffer); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/lavfi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/lavfi.c index ce10d61f8a..17657a0638 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/lavfi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/lavfi.c @@ -249,18 +249,24 @@ av_cold static int lavfi_read_header(AVFormatContext *avctx) AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_DBL, }; - ret = avfilter_graph_create_filter(&sink, abuffersink, - inout->name, NULL, - NULL, lavfi->graph); - if (ret >= 0) - ret = av_opt_set_bin(sink, "sample_fmts", (const uint8_t*)sample_fmts, - sizeof(sample_fmts), AV_OPT_SEARCH_CHILDREN); + sink = avfilter_graph_alloc_filter(lavfi->graph, abuffersink, inout->name); + if (!sink) { + ret = AVERROR(ENOMEM); + goto end; + } + + ret = av_opt_set_bin(sink, "sample_fmts", (const uint8_t*)sample_fmts, + sizeof(sample_fmts), AV_OPT_SEARCH_CHILDREN); if (ret < 0) goto end; ret = av_opt_set_int(sink, "all_channel_counts", 1, AV_OPT_SEARCH_CHILDREN); if (ret < 0) goto end; + + ret = avfilter_init_dict(sink, NULL); + if (ret < 0) + goto end; } else { av_log(avctx, AV_LOG_ERROR, "Output '%s' is not a video or audio output, not yet supported\n", inout->name); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/tests/.gitignore new file mode 100644 index 0000000000..b4a2281a70 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/tests/.gitignore @@ -0,0 +1 @@ +/timefilter diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/version.h index 7608a8602c..0e4ce64598 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavdevice/version.h @@ -29,7 +29,7 @@ #include "version_major.h" -#define LIBAVDEVICE_VERSION_MINOR 3 +#define LIBAVDEVICE_VERSION_MINOR 4 #define LIBAVDEVICE_VERSION_MICRO 100 #define LIBAVDEVICE_VERSION_INT AV_VERSION_INT(LIBAVDEVICE_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/.gitignore new file mode 100644 index 0000000000..26bddebc93 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/.gitignore @@ -0,0 +1 @@ +/filter_list.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afdelaysrc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afdelaysrc.c index c235f0c27a..cb6f236ba6 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afdelaysrc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afdelaysrc.c @@ -80,22 +80,24 @@ static int activate(AVFilterContext *ctx) return ff_filter_frame(outlink, frame); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - AFDelaySrcContext *s = ctx->priv; + const AFDelaySrcContext *s = ctx->priv; AVChannelLayout chlayouts[] = { s->chlayout, { 0 } }; int sample_rates[] = { s->sample_rate, -1 }; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static int config_output(AVFilterLink *outlink) @@ -146,5 +148,5 @@ const AVFilter ff_asrc_afdelaysrc = { .activate = activate, .inputs = NULL, FILTER_OUTPUTS(afdelaysrc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afirsrc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afirsrc.c index bc450ec822..0ea611d837 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afirsrc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_afirsrc.c @@ -113,24 +113,26 @@ static av_cold void uninit(AVFilterContext *ctx) av_tx_uninit(&s->itx_ctx); } -static av_cold int query_formats(AVFilterContext *ctx) +static av_cold int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - AudioFIRSourceContext *s = ctx->priv; + const AudioFIRSourceContext *s = ctx->priv; static const AVChannelLayout chlayouts[] = { AV_CHANNEL_LAYOUT_MONO, { 0 } }; int sample_rates[] = { s->sample_rate, -1 }; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static int parse_string(char *str, float **items, int *nb_items, int *items_size) @@ -304,7 +306,7 @@ const AVFilter ff_asrc_afirsrc = { .priv_size = sizeof(AudioFIRSourceContext), .inputs = NULL, FILTER_OUTPUTS(afirsrc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &afirsrc_class, }; @@ -585,6 +587,6 @@ const AVFilter ff_asrc_afireqsrc = { .priv_size = sizeof(AudioFIRSourceContext), .inputs = NULL, FILTER_OUTPUTS(afireqsrc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &afireqsrc_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anoisesrc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anoisesrc.c index a67b2abe9c..0206fd69ce 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anoisesrc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anoisesrc.c @@ -83,24 +83,26 @@ static const AVOption anoisesrc_options[] = { AVFILTER_DEFINE_CLASS(anoisesrc); -static av_cold int query_formats(AVFilterContext *ctx) +static av_cold int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - ANoiseSrcContext *s = ctx->priv; + const ANoiseSrcContext *s = ctx->priv; static const AVChannelLayout chlayouts[] = { AV_CHANNEL_LAYOUT_MONO, { 0 } }; int sample_rates[] = { s->sample_rate, -1 }; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_DBL, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static double white_filter(double white, double *buf) @@ -244,6 +246,6 @@ const AVFilter ff_asrc_anoisesrc = { .inputs = NULL, .activate = activate, FILTER_OUTPUTS(anoisesrc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &anoisesrc_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anullsrc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anullsrc.c index 3249cc33ad..d8fd5e7c06 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anullsrc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_anullsrc.c @@ -61,18 +61,20 @@ static const AVOption anullsrc_options[]= { AVFILTER_DEFINE_CLASS(anullsrc); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - ANullContext *null = ctx->priv; + const ANullContext *null = ctx->priv; const AVChannelLayout chlayouts[] = { null->ch_layout, { 0 } }; int sample_rates[] = { null->sample_rate, -1 }; int ret; - if ((ret = ff_set_common_formats (ctx, ff_all_formats (AVMEDIA_TYPE_AUDIO))) < 0 || - (ret = ff_set_common_samplerates_from_list(ctx, sample_rates)) < 0) + ret = ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); + if (ret < 0) return ret; - return ff_set_common_channel_layouts_from_list(ctx, chlayouts); + return ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); } static av_cold int config_props(AVFilterLink *outlink) @@ -124,7 +126,7 @@ const AVFilter ff_asrc_anullsrc = { .priv_size = sizeof(ANullContext), .inputs = NULL, FILTER_OUTPUTS(avfilter_asrc_anullsrc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .activate = activate, .priv_class = &anullsrc_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_flite.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_flite.c index 5962bf55bb..1bbd329177 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_flite.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_flite.c @@ -255,24 +255,35 @@ static av_cold void uninit(AVFilterContext *ctx) av_audio_fifo_free(flite->fifo); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - FliteContext *flite = ctx->priv; + const FliteContext *flite = ctx->priv; + + static const enum AVSampleFormat formats[] = { + AV_SAMPLE_FMT_S16, + AV_SAMPLE_FMT_NONE, + }; + int sample_rates[] = { flite->sample_rate, -1 }; + AVChannelLayout layouts[2] = { + { .nb_channels = 0 }, + }; + int ret; - AVFilterChannelLayouts *chlayouts = NULL; - AVFilterFormats *sample_formats = NULL; - AVFilterFormats *sample_rates = NULL; - AVChannelLayout chlayout = { 0 }; + av_channel_layout_default(&layouts[0], flite->nb_channels); - av_channel_layout_default(&chlayout, flite->nb_channels); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, layouts); + if (ret < 0) + return ret; - if ((ret = ff_add_channel_layout (&chlayouts , &chlayout )) < 0 || - (ret = ff_set_common_channel_layouts (ctx , chlayouts )) < 0 || - (ret = ff_add_format (&sample_formats, AV_SAMPLE_FMT_S16 )) < 0 || - (ret = ff_set_common_formats (ctx , sample_formats )) < 0 || - (ret = ff_add_format (&sample_rates , flite->sample_rate )) < 0 || - (ret = ff_set_common_samplerates (ctx , sample_rates )) < 0) + ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, formats); + if (ret < 0) + return ret; + + ret = ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); + if (ret < 0) return ret; return 0; @@ -348,6 +359,6 @@ const AVFilter ff_asrc_flite = { .activate = activate, .inputs = NULL, FILTER_OUTPUTS(flite_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &flite_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_hilbert.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_hilbert.c index e38af7123b..3cf5e8728e 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_hilbert.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_hilbert.c @@ -75,7 +75,9 @@ static av_cold void uninit(AVFilterContext *ctx) av_freep(&s->taps); } -static av_cold int query_formats(AVFilterContext *ctx) +static av_cold int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { HilbertContext *s = ctx->priv; static const AVChannelLayout chlayouts[] = { AV_CHANNEL_LAYOUT_MONO, { 0 } }; @@ -84,15 +86,15 @@ static av_cold int query_formats(AVFilterContext *ctx) AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static av_cold int config_props(AVFilterLink *outlink) @@ -168,6 +170,6 @@ const AVFilter ff_asrc_hilbert = { .priv_size = sizeof(HilbertContext), .inputs = NULL, FILTER_OUTPUTS(hilbert_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &hilbert_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sinc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sinc.c index 94046f76b9..22b031a60d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sinc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sinc.c @@ -74,22 +74,24 @@ static int activate(AVFilterContext *ctx) return ff_filter_frame(outlink, frame); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - SincContext *s = ctx->priv; + const SincContext *s = ctx->priv; static const AVChannelLayout chlayouts[] = { AV_CHANNEL_LAYOUT_MONO, { 0 } }; int sample_rates[] = { s->sample_rate, -1 }; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static float *make_lpf(int num_taps, float Fc, float beta, float rho, @@ -428,5 +430,5 @@ const AVFilter ff_asrc_sinc = { .activate = activate, .inputs = NULL, FILTER_OUTPUTS(sinc_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sine.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sine.c index 2e444ba196..96b603dd26 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sine.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/asrc_sine.c @@ -178,22 +178,24 @@ static av_cold void uninit(AVFilterContext *ctx) av_freep(&sine->sin); } -static av_cold int query_formats(AVFilterContext *ctx) +static av_cold int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - SineContext *sine = ctx->priv; + const SineContext *sine = ctx->priv; static const AVChannelLayout chlayouts[] = { AV_CHANNEL_LAYOUT_MONO, { 0 } }; int sample_rates[] = { sine->sample_rate, -1 }; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE }; - int ret = ff_set_common_formats_from_list(ctx, sample_fmts); + int ret = ff_set_common_formats_from_list2(ctx, cfg_in, cfg_out, sample_fmts); if (ret < 0) return ret; - ret = ff_set_common_channel_layouts_from_list(ctx, chlayouts); + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, chlayouts); if (ret < 0) return ret; - return ff_set_common_samplerates_from_list(ctx, sample_rates); + return ff_set_common_samplerates_from_list2(ctx, cfg_in, cfg_out, sample_rates); } static av_cold int config_props(AVFilterLink *outlink) @@ -271,6 +273,6 @@ const AVFilter ff_asrc_sine = { .priv_size = sizeof(SineContext), .inputs = NULL, FILTER_OUTPUTS(sine_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &sine_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_a3dscope.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_a3dscope.c index dd08990512..18f647e8a6 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_a3dscope.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_a3dscope.c @@ -73,30 +73,21 @@ static const AVOption a3dscope_options[] = { AVFILTER_DEFINE_CLASS(a3dscope); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref (formats, &inlink->outcfg.formats )) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -352,7 +343,7 @@ const AVFilter ff_avf_a3dscope = { .activate = activate, FILTER_INPUTS(audio3dscope_inputs), FILTER_OUTPUTS(audio3dscope_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &a3dscope_class, .process_command = ff_filter_process_command, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_abitscope.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_abitscope.c index 30ed7d95e3..6d3a40d8d0 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_abitscope.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_abitscope.c @@ -65,12 +65,11 @@ static const AVOption abitscope_options[] = { AVFILTER_DEFINE_CLASS(abitscope); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16P, AV_SAMPLE_FMT_S32P, AV_SAMPLE_FMT_U8P, AV_SAMPLE_FMT_S64P, AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_DBLP, @@ -79,21 +78,11 @@ static int query_formats(AVFilterContext *ctx) int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if (!layouts) - return AVERROR(ENOMEM); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -322,7 +311,7 @@ const AVFilter ff_avf_abitscope = { .priv_size = sizeof(AudioBitScopeContext), FILTER_INPUTS(inputs), FILTER_OUTPUTS(outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .uninit = uninit, .activate = activate, .priv_class = &abitscope_class, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_ahistogram.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_ahistogram.c index b77307f137..baa95c0539 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_ahistogram.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_ahistogram.c @@ -92,28 +92,21 @@ static const AVOption ahistogram_options[] = { AVFILTER_DEFINE_CLASS(ahistogram); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUVA444P, AV_PIX_FMT_NONE }; int ret = AVERROR(EINVAL); formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref (formats, &inlink->outcfg.formats )) < 0 || - (layouts = ff_all_channel_counts()) == NULL || - (ret = ff_channel_layouts_ref (layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -509,6 +502,6 @@ const AVFilter ff_avf_ahistogram = { .activate = activate, FILTER_INPUTS(ahistogram_inputs), FILTER_OUTPUTS(ahistogram_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &ahistogram_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_aphasemeter.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_aphasemeter.c index fe0968c974..adcfcb3c5a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_aphasemeter.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_aphasemeter.c @@ -90,35 +90,32 @@ static const AVOption aphasemeter_options[] = { AVFILTER_DEFINE_CLASS(aphasemeter); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - AudioPhaseMeterContext *s = ctx->priv; + const AudioPhaseMeterContext *s = ctx->priv; AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layout = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; + static const AVChannelLayout layouts[] = { + AV_CHANNEL_LAYOUT_STEREO, + { .nb_channels = 0 }, + }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref (formats, &inlink->outcfg.formats )) < 0 || - (ret = ff_formats_ref (formats, &outlink->incfg.formats )) < 0 || - (ret = ff_add_channel_layout (&layout, &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO )) < 0 || - (ret = ff_channel_layouts_ref (layout , &inlink->outcfg.channel_layouts)) < 0 || - (ret = ff_channel_layouts_ref (layout , &outlink->incfg.channel_layouts)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0 || + (ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0 || - (ret = ff_formats_ref(formats, &outlink->incfg.samplerates)) < 0) + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, layouts); + if (ret < 0) return ret; if (s->do_video) { - AVFilterLink *outlink = ctx->outputs[1]; - formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[1]->formats)) < 0) return ret; } @@ -440,7 +437,7 @@ const AVFilter ff_avf_aphasemeter = { FILTER_INPUTS(inputs), .activate = activate, .outputs = NULL, - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &aphasemeter_class, .flags = AVFILTER_FLAG_DYNAMIC_OUTPUTS, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_avectorscope.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_avectorscope.c index 96bef32e2c..cfcedfda00 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_avectorscope.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_avectorscope.c @@ -230,28 +230,29 @@ static int fade(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) return 0; } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layout = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; + static const AVChannelLayout layouts[] = { + AV_CHANNEL_LAYOUT_STEREO, + { .nb_channels = 0 }, + }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref (formats, &inlink->outcfg.formats )) < 0 || - (ret = ff_add_channel_layout (&layout, &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO)) < 0 || - (ret = ff_channel_layouts_ref (layout , &inlink->outcfg.channel_layouts)) < 0) + if ((ret = ff_formats_ref (formats, &cfg_in[0]->formats )) < 0) return ret; - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, layouts); + if (ret < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -493,7 +494,7 @@ const AVFilter ff_avf_avectorscope = { .activate = activate, FILTER_INPUTS(audiovectorscope_inputs), FILTER_OUTPUTS(audiovectorscope_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &avectorscope_class, .flags = AVFILTER_FLAG_SLICE_THREADS, .process_command = ff_filter_process_command, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_concat.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_concat.c index 2a3d4c8b52..5a4319123f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_concat.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_concat.c @@ -72,9 +72,11 @@ static const AVOption concat_options[] = { AVFILTER_DEFINE_CLASS(concat); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - ConcatContext *cat = ctx->priv; + const ConcatContext *cat = ctx->priv; unsigned type, nb_str, idx0 = 0, idx, str, seg; AVFilterFormats *formats, *rates = NULL; AVFilterChannelLayouts *layouts = NULL; @@ -87,25 +89,25 @@ static int query_formats(AVFilterContext *ctx) /* Set the output formats */ formats = ff_all_formats(type); - if ((ret = ff_formats_ref(formats, &ctx->outputs[idx]->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[idx]->formats)) < 0) return ret; if (type == AVMEDIA_TYPE_AUDIO) { rates = ff_all_samplerates(); - if ((ret = ff_formats_ref(rates, &ctx->outputs[idx]->incfg.samplerates)) < 0) + if ((ret = ff_formats_ref(rates, &cfg_out[idx]->samplerates)) < 0) return ret; layouts = ff_all_channel_layouts(); - if ((ret = ff_channel_layouts_ref(layouts, &ctx->outputs[idx]->incfg.channel_layouts)) < 0) + if ((ret = ff_channel_layouts_ref(layouts, &cfg_out[idx]->channel_layouts)) < 0) return ret; } /* Set the same formats for each corresponding input */ for (seg = 0; seg < cat->nb_segments; seg++) { - if ((ret = ff_formats_ref(formats, &ctx->inputs[idx]->outcfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[idx]->formats)) < 0) return ret; if (type == AVMEDIA_TYPE_AUDIO) { - if ((ret = ff_formats_ref(rates, &ctx->inputs[idx]->outcfg.samplerates)) < 0 || - (ret = ff_channel_layouts_ref(layouts, &ctx->inputs[idx]->outcfg.channel_layouts)) < 0) + if ((ret = ff_formats_ref(rates, &cfg_in[idx]->samplerates)) < 0 || + (ret = ff_channel_layouts_ref(layouts, &cfg_in[idx]->channel_layouts)) < 0) return ret; } idx += ctx->nb_outputs; @@ -460,6 +462,6 @@ const AVFilter ff_avf_concat = { .outputs = NULL, .priv_class = &concat_class, .flags = AVFILTER_FLAG_DYNAMIC_INPUTS | AVFILTER_FLAG_DYNAMIC_OUTPUTS, - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .process_command = process_command, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcqt.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcqt.c index 00f679bc9e..9633f36edb 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcqt.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcqt.c @@ -1314,12 +1314,11 @@ static av_cold void uninit(AVFilterContext *ctx) common_uninit(ctx->priv); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, @@ -1331,20 +1330,16 @@ static int query_formats(AVFilterContext *ctx) /* set input audio formats */ formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; - layouts = ff_make_channel_layout_list(channel_layouts); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, channel_layouts); + if (ret < 0) return ret; /* set output video format */ formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -1612,6 +1607,6 @@ const AVFilter ff_avf_showcqt = { .priv_size = sizeof(ShowCQTContext), FILTER_INPUTS(ff_audio_default_filterpad), FILTER_OUTPUTS(showcqt_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showcqt_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcwt.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcwt.c index 760a07f2ff..dbb9fc7e2f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcwt.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showcwt.c @@ -222,30 +222,21 @@ static av_cold void uninit(AVFilterContext *ctx) av_freep(&s->fdsp); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUVJ444P, AV_PIX_FMT_YUVA444P, AV_PIX_FMT_NONE }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -1334,7 +1325,7 @@ const AVFilter ff_avf_showcwt = { .priv_size = sizeof(ShowCWTContext), FILTER_INPUTS(ff_audio_default_filterpad), FILTER_OUTPUTS(showcwt_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .activate = activate, .priv_class = &showcwt_class, .flags = AVFILTER_FLAG_SLICE_THREADS, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showfreqs.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showfreqs.c index da31b3215e..91e3a339e4 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showfreqs.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showfreqs.c @@ -116,32 +116,23 @@ static const AVOption showfreqs_options[] = { AVFILTER_DEFINE_CLASS(showfreqs); -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; int ret; /* set input audio formats */ formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; /* set output video format */ formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -566,6 +557,6 @@ const AVFilter ff_avf_showfreqs = { .activate = activate, FILTER_INPUTS(ff_audio_default_filterpad), FILTER_OUTPUTS(showfreqs_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showfreqs_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspatial.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspatial.c index f7380f885a..285f2f1a81 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspatial.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspatial.c @@ -80,28 +80,26 @@ static av_cold void uninit(AVFilterContext *ctx) av_audio_fifo_free(s->fifo); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layout = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_GBRP, AV_PIX_FMT_NONE }; + static const AVChannelLayout layouts[] = { AV_CHANNEL_LAYOUT_STEREO, { .nb_channels = 0 } }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref (formats, &inlink->outcfg.formats )) < 0 || - (ret = ff_add_channel_layout (&layout, &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO)) < 0 || - (ret = ff_channel_layouts_ref (layout , &inlink->outcfg.channel_layouts)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + ret = ff_set_common_channel_layouts_from_list2(ctx, cfg_in, cfg_out, layouts); + if (ret < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -330,7 +328,7 @@ const AVFilter ff_avf_showspatial = { .priv_size = sizeof(ShowSpatialContext), FILTER_INPUTS(ff_audio_default_filterpad), FILTER_OUTPUTS(showspatial_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .activate = spatial_activate, .priv_class = &showspatial_class, .flags = AVFILTER_FLAG_SLICE_THREADS, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspectrum.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspectrum.c index 565f23c28b..c6594177eb 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspectrum.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showspectrum.c @@ -358,32 +358,23 @@ static av_cold void uninit(AVFilterContext *ctx) av_freep(&s->frames); } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUVJ444P, AV_PIX_FMT_YUVA444P, AV_PIX_FMT_NONE }; int ret; /* set input audio formats */ formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; /* set output video format */ formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -1696,7 +1687,7 @@ const AVFilter ff_avf_showspectrum = { .priv_size = sizeof(ShowSpectrumContext), FILTER_INPUTS(ff_audio_default_filterpad), FILTER_OUTPUTS(showspectrum_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .activate = activate, .priv_class = &showspectrum_class, .flags = AVFILTER_FLAG_SLICE_THREADS, @@ -1883,7 +1874,7 @@ const AVFilter ff_avf_showspectrumpic = { .priv_size = sizeof(ShowSpectrumContext), FILTER_INPUTS(showspectrumpic_inputs), FILTER_OUTPUTS(showspectrumpic_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showspectrumpic_class, .flags = AVFILTER_FLAG_SLICE_THREADS, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showvolume.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showvolume.c index d26fc8841d..8caabf5376 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showvolume.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showvolume.c @@ -110,30 +110,21 @@ static av_cold int init(AVFilterContext *ctx) return 0; } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; int ret; formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_counts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -520,6 +511,6 @@ const AVFilter ff_avf_showvolume = { .priv_size = sizeof(ShowVolumeContext), FILTER_INPUTS(showvolume_inputs), FILTER_OUTPUTS(showvolume_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showvolume_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showwaves.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showwaves.c index 868e6a22d9..de73d8050f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showwaves.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avf_showwaves.c @@ -155,32 +155,23 @@ static av_cold void uninit(AVFilterContext *ctx) } } -static int query_formats(AVFilterContext *ctx) +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { AVFilterFormats *formats = NULL; - AVFilterChannelLayouts *layouts = NULL; - AVFilterLink *inlink = ctx->inputs[0]; - AVFilterLink *outlink = ctx->outputs[0]; static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE }; static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_GRAY8, AV_PIX_FMT_NONE }; int ret; /* set input audio formats */ formats = ff_make_format_list(sample_fmts); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0) - return ret; - - layouts = ff_all_channel_layouts(); - if ((ret = ff_channel_layouts_ref(layouts, &inlink->outcfg.channel_layouts)) < 0) - return ret; - - formats = ff_all_samplerates(); - if ((ret = ff_formats_ref(formats, &inlink->outcfg.samplerates)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_in[0]->formats)) < 0) return ret; /* set output video format */ formats = ff_make_format_list(pix_fmts); - if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0) + if ((ret = ff_formats_ref(formats, &cfg_out[0]->formats)) < 0) return ret; return 0; @@ -814,7 +805,7 @@ const AVFilter ff_avf_showwaves = { FILTER_INPUTS(ff_audio_default_filterpad), .activate = activate, FILTER_OUTPUTS(showwaves_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showwaves_class, }; @@ -925,7 +916,7 @@ const AVFilter ff_avf_showwavespic = { .priv_size = sizeof(ShowWavesContext), FILTER_INPUTS(showwavespic_inputs), FILTER_OUTPUTS(showwavespic_outputs), - FILTER_QUERY_FUNC(query_formats), + FILTER_QUERY_FUNC2(query_formats), .priv_class = &showwavespic_class, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.c index 8a2a9e0593..dc06ebab4d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.c @@ -159,7 +159,8 @@ int avfilter_link(AVFilterContext *src, unsigned srcpad, src->outputs[srcpad] || dst->inputs[dstpad]) return AVERROR(EINVAL); - if (!fffilterctx(src)->initialized || !fffilterctx(dst)->initialized) { + if (!(fffilterctx(src)->state_flags & AV_CLASS_STATE_INITIALIZED) || + !(fffilterctx(dst)->state_flags & AV_CLASS_STATE_INITIALIZED)) { av_log(src, AV_LOG_ERROR, "Filters must be initialized before linking.\n"); return AVERROR(EINVAL); } @@ -676,6 +677,7 @@ static const AVClass avfilter_class = { .child_next = filter_child_next, .child_class_iterate = filter_child_class_iterate, .option = avfilter_options, + .state_flags_offset = offsetof(FFFilterContext, state_flags), }; static int default_execute(AVFilterContext *ctx, avfilter_action_func *func, void *arg, @@ -909,7 +911,7 @@ int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options) FFFilterContext *ctxi = fffilterctx(ctx); int ret = 0; - if (ctxi->initialized) { + if (ctxi->state_flags & AV_CLASS_STATE_INITIALIZED) { av_log(ctx, AV_LOG_ERROR, "Filter already initialized\n"); return AVERROR(EINVAL); } @@ -940,7 +942,7 @@ int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options) return ret; } - ctxi->initialized = 1; + ctxi->state_flags |= AV_CLASS_STATE_INITIALIZED; return 0; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.h index 1401577c50..f2c4a64004 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter.h @@ -845,9 +845,9 @@ AVFilterContext *avfilter_graph_alloc_filter(AVFilterGraph *graph, AVFilterContext *avfilter_graph_get_filter(AVFilterGraph *graph, const char *name); /** - * Create and add a filter instance into an existing graph. - * The filter instance is created from the filter filt and inited - * with the parameter args. opaque is currently ignored. + * A convenience wrapper that allocates and initializes a filter in a single + * step. The filter instance is created from the filter filt and inited with the + * parameter args. opaque is currently ignored. * * In case of success put in *filt_ctx the pointer to the created * filter instance, otherwise set *filt_ctx to NULL. @@ -856,6 +856,12 @@ AVFilterContext *avfilter_graph_get_filter(AVFilterGraph *graph, const char *nam * @param graph_ctx the filter graph * @return a negative AVERROR error code in case of failure, a non * negative value otherwise + * + * @warning Since the filter is initialized after this function successfully + * returns, you MUST NOT set any further options on it. If you need to + * do that, call ::avfilter_graph_alloc_filter(), followed by setting + * the options, followed by ::avfilter_init_dict() instead of this + * function. */ int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name, const char *args, void *opaque, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter_internal.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter_internal.h index ec3933b1d1..9ba890a70c 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter_internal.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/avfilter_internal.h @@ -100,9 +100,8 @@ typedef struct FFFilterContext { avfilter_execute_func *execute; - // 1 when avfilter_init_*() was successfully called on this filter - // 0 otherwise - int initialized; + // AV_CLASS_STATE_FLAG_* + unsigned state_flags; } FFFilterContext; static inline FFFilterContext *fffilterctx(AVFilterContext *ctx) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.c index 5811720c61..0cfac2abd5 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.c @@ -28,6 +28,7 @@ #include "libavutil/channel_layout.h" #include "libavutil/common.h" #include "libavutil/internal.h" +#include "libavutil/mem.h" #include "libavutil/opt.h" #include "audio.h" @@ -42,28 +43,49 @@ typedef struct BufferSinkContext { const AVClass *class; unsigned warning_limit; + unsigned frame_size; /* only used for video */ +#if FF_API_BUFFERSINK_OPTS enum AVPixelFormat *pixel_fmts; ///< list of accepted pixel formats int pixel_fmts_size; enum AVColorSpace *color_spaces; ///< list of accepted color spaces int color_spaces_size; enum AVColorRange *color_ranges; ///< list of accepted color ranges int color_ranges_size; +#endif + + enum AVPixelFormat *pixel_formats; + unsigned nb_pixel_formats; + + int *colorspaces; + unsigned nb_colorspaces; + + int *colorranges; + unsigned nb_colorranges; /* only used for audio */ +#if FF_API_BUFFERSINK_OPTS enum AVSampleFormat *sample_fmts; ///< list of accepted sample formats int sample_fmts_size; char *channel_layouts_str; ///< list of accepted channel layouts int all_channel_counts; int *sample_rates; ///< list of accepted sample rates int sample_rates_size; +#endif + + enum AVSampleFormat *sample_formats; + unsigned nb_sample_formats; + + int *samplerates; + unsigned nb_samplerates; + + AVChannelLayout *channel_layouts; + unsigned nb_channel_layouts; AVFrame *peeked_frame; } BufferSinkContext; -#define NB_ITEMS(list) (list ## _size / sizeof(*list)) - int attribute_align_arg av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) { return av_buffersink_get_frame_flags(ctx, frame, 0); @@ -137,6 +159,39 @@ static av_cold int common_init(AVFilterContext *ctx) return 0; } +#define TERMINATE_ARRAY(arr, val) \ + if (s->arr) { \ + void *tmp = av_realloc_array(s->arr, s->nb_ ## arr + 1, sizeof(*s->arr)); \ + if (!tmp) \ + return AVERROR(ENOMEM); \ + s->arr = tmp; \ + s->arr[s->nb_ ## arr] = val; \ + } + +static int init_video(AVFilterContext *ctx) +{ + BufferSinkContext *s = ctx->priv; + + TERMINATE_ARRAY(pixel_formats, AV_PIX_FMT_NONE); + TERMINATE_ARRAY(colorranges, -1); + TERMINATE_ARRAY(colorspaces, -1); + + return common_init(ctx); +} + +static int init_audio(AVFilterContext *ctx) +{ + BufferSinkContext *s = ctx->priv; + + TERMINATE_ARRAY(sample_formats, AV_SAMPLE_FMT_NONE); + TERMINATE_ARRAY(samplerates, -1); + TERMINATE_ARRAY(channel_layouts, (AVChannelLayout){ .nb_channels = 0 }); + + return common_init(ctx); +} + +#undef TERMINATE_ARRAY + static void uninit(AVFilterContext *ctx) { BufferSinkContext *buf = ctx->priv; @@ -162,11 +217,25 @@ static int activate(AVFilterContext *ctx) return 0; } +static int config_input_audio(AVFilterLink *inlink) +{ + BufferSinkContext *buf = inlink->dst->priv; + FilterLink *l = ff_filter_link(inlink); + + l->min_samples = l->max_samples = buf->frame_size; + + return 0; +} + void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size) { - FilterLink *inlink = ff_filter_link(ctx->inputs[0]); + BufferSinkContext *buf = ctx->priv; + buf->frame_size = frame_size; - inlink->min_samples = inlink->max_samples = frame_size; + if (ctx->inputs && ctx->inputs[0]) { + FilterLink *l = ff_filter_link(ctx->inputs[0]); + l->min_samples = l->max_samples = buf->frame_size; + } } #define MAKE_AVFILTERLINK_ACCESSOR(type, field) \ @@ -220,6 +289,9 @@ int av_buffersink_get_ch_layout(const AVFilterContext *ctx, AVChannelLayout *out return 0; } +#if FF_API_BUFFERSINK_OPTS +#define NB_ITEMS(list) (list ## _size / sizeof(*list)) + #define CHECK_LIST_SIZE(field) \ if (buf->field ## _size % sizeof(*buf->field)) { \ av_log(ctx, AV_LOG_ERROR, "Invalid size for " #field ": %d, " \ @@ -227,12 +299,40 @@ int av_buffersink_get_ch_layout(const AVFilterContext *ctx, AVChannelLayout *out buf->field ## _size, (int)sizeof(*buf->field)); \ return AVERROR(EINVAL); \ } +#endif + static int vsink_query_formats(AVFilterContext *ctx) { BufferSinkContext *buf = ctx->priv; - unsigned i; int ret; +#if FF_API_BUFFERSINK_OPTS + if ((buf->pixel_fmts_size || buf->color_spaces_size || buf->color_ranges_size) && + (buf->nb_pixel_formats || buf->nb_colorspaces || buf->nb_colorranges)) { + av_log(ctx, AV_LOG_ERROR, "Cannot combine old and new format lists\n"); + return AVERROR(EINVAL); + } + + if (buf->nb_pixel_formats || buf->nb_colorspaces || buf->nb_colorranges) { +#endif + if (buf->nb_pixel_formats) { + ret = ff_set_common_formats_from_list(ctx, buf->pixel_formats); + if (ret < 0) + return ret; + } + if (buf->nb_colorspaces) { + ret = ff_set_common_color_spaces_from_list(ctx, buf->colorspaces); + if (ret < 0) + return ret; + } + if (buf->nb_colorranges) { + ret = ff_set_common_color_ranges_from_list(ctx, buf->colorranges); + if (ret < 0) + return ret; + } +#if FF_API_BUFFERSINK_OPTS + } else { + unsigned i; CHECK_LIST_SIZE(pixel_fmts) CHECK_LIST_SIZE(color_spaces) CHECK_LIST_SIZE(color_ranges) @@ -262,6 +362,8 @@ static int vsink_query_formats(AVFilterContext *ctx) if ((ret = ff_set_common_color_ranges(ctx, formats)) < 0) return ret; } + } +#endif return 0; } @@ -269,12 +371,38 @@ static int vsink_query_formats(AVFilterContext *ctx) static int asink_query_formats(AVFilterContext *ctx) { BufferSinkContext *buf = ctx->priv; + int ret; + +#if FF_API_BUFFERSINK_OPTS + if ((buf->sample_fmts_size || buf->channel_layouts_str || buf->sample_rates_size) && + (buf->nb_sample_formats || buf->nb_samplerates || buf->nb_channel_layouts)) { + av_log(ctx, AV_LOG_ERROR, "Cannot combine old and new format lists\n"); + return AVERROR(EINVAL); + } + + if (buf->nb_sample_formats || buf->nb_samplerates || buf->nb_channel_layouts) { +#endif + if (buf->nb_sample_formats) { + ret = ff_set_common_formats_from_list(ctx, buf->sample_formats); + if (ret < 0) + return ret; + } + if (buf->nb_samplerates) { + ret = ff_set_common_samplerates_from_list(ctx, buf->samplerates); + if (ret < 0) + return ret; + } + if (buf->nb_channel_layouts) { + ret = ff_set_common_channel_layouts_from_list(ctx, buf->channel_layouts); + if (ret < 0) + return ret; + } +#if FF_API_BUFFERSINK_OPTS + } else { AVFilterFormats *formats = NULL; AVChannelLayout layout = { 0 }; AVFilterChannelLayouts *layouts = NULL; unsigned i; - int ret; - CHECK_LIST_SIZE(sample_fmts) CHECK_LIST_SIZE(sample_rates) @@ -328,6 +456,8 @@ static int asink_query_formats(AVFilterContext *ctx) if ((ret = ff_set_common_samplerates(ctx, formats)) < 0) return ret; } + } +#endif return 0; } @@ -335,19 +465,38 @@ static int asink_query_formats(AVFilterContext *ctx) #define OFFSET(x) offsetof(BufferSinkContext, x) #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM static const AVOption buffersink_options[] = { - { "pix_fmts", "set the supported pixel formats", OFFSET(pixel_fmts), AV_OPT_TYPE_BINARY, .flags = FLAGS }, - { "color_spaces", "set the supported color spaces", OFFSET(color_spaces), AV_OPT_TYPE_BINARY, .flags = FLAGS }, - { "color_ranges", "set the supported color ranges", OFFSET(color_ranges), AV_OPT_TYPE_BINARY, .flags = FLAGS }, +#if FF_API_BUFFERSINK_OPTS + { "pix_fmts", "set the supported pixel formats", OFFSET(pixel_fmts), AV_OPT_TYPE_BINARY, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, + { "color_spaces", "set the supported color spaces", OFFSET(color_spaces), AV_OPT_TYPE_BINARY, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, + { "color_ranges", "set the supported color ranges", OFFSET(color_ranges), AV_OPT_TYPE_BINARY, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, +#endif + + { "pixel_formats", "array of supported pixel formats", OFFSET(pixel_formats), + AV_OPT_TYPE_PIXEL_FMT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX, .flags = FLAGS }, + { "colorspaces", "array of supported color spaces", OFFSET(colorspaces), + AV_OPT_TYPE_INT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX, .flags = FLAGS }, + { "colorranges", "array of supported color ranges", OFFSET(colorranges), + AV_OPT_TYPE_INT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX, .flags = FLAGS }, + { NULL }, }; #undef FLAGS #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_AUDIO_PARAM static const AVOption abuffersink_options[] = { - { "sample_fmts", "set the supported sample formats", OFFSET(sample_fmts), AV_OPT_TYPE_BINARY, .flags = FLAGS }, - { "sample_rates", "set the supported sample rates", OFFSET(sample_rates), AV_OPT_TYPE_BINARY, .flags = FLAGS }, +#if FF_API_BUFFERSINK_OPTS + { "sample_fmts", "set the supported sample formats", OFFSET(sample_fmts), AV_OPT_TYPE_BINARY, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, + { "sample_rates", "set the supported sample rates", OFFSET(sample_rates), AV_OPT_TYPE_BINARY, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, { "ch_layouts", "set a '|'-separated list of supported channel layouts", - OFFSET(channel_layouts_str), AV_OPT_TYPE_STRING, .flags = FLAGS }, - { "all_channel_counts", "accept all channel counts", OFFSET(all_channel_counts), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, FLAGS }, + OFFSET(channel_layouts_str), AV_OPT_TYPE_STRING, .flags = FLAGS | AV_OPT_FLAG_DEPRECATED }, + { "all_channel_counts", "accept all channel counts", OFFSET(all_channel_counts), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, FLAGS | AV_OPT_FLAG_DEPRECATED }, +#endif + + { "sample_formats", "array of supported sample formats", OFFSET(sample_formats), + AV_OPT_TYPE_SAMPLE_FMT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX, .flags = FLAGS }, + { "samplerates", "array of supported sample formats", OFFSET(samplerates), + AV_OPT_TYPE_INT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX, .flags = FLAGS }, + { "channel_layouts", "array of supported channel layouts", OFFSET(channel_layouts), + AV_OPT_TYPE_CHLAYOUT | AV_OPT_TYPE_FLAG_ARRAY, .flags = FLAGS }, { NULL }, }; #undef FLAGS @@ -360,7 +509,7 @@ const AVFilter ff_vsink_buffer = { .description = NULL_IF_CONFIG_SMALL("Buffer video frames, and make them available to the end of the filter graph."), .priv_size = sizeof(BufferSinkContext), .priv_class = &buffersink_class, - .init = common_init, + .init = init_video, .uninit = uninit, .activate = activate, FILTER_INPUTS(ff_video_default_filterpad), @@ -368,15 +517,23 @@ const AVFilter ff_vsink_buffer = { FILTER_QUERY_FUNC(vsink_query_formats), }; +static const AVFilterPad inputs_audio[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_AUDIO, + .config_props = config_input_audio, + }, +}; + const AVFilter ff_asink_abuffer = { .name = "abuffersink", .description = NULL_IF_CONFIG_SMALL("Buffer audio frames, and make them available to the end of the filter graph."), .priv_class = &abuffersink_class, .priv_size = sizeof(BufferSinkContext), - .init = common_init, + .init = init_audio, .uninit = uninit, .activate = activate, - FILTER_INPUTS(ff_audio_default_filterpad), + FILTER_INPUTS(inputs_audio), .outputs = NULL, FILTER_QUERY_FUNC(asink_query_formats), }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.h index 361d603679..08cb6b3804 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/buffersink.h @@ -54,20 +54,13 @@ * * The format can be constrained by setting options, using av_opt_set() and * related functions with the AV_OPT_SEARCH_CHILDREN flag. - * - pix_fmts (int list), - * - color_spaces (int list), - * - color_ranges (int list), - * - sample_fmts (int list), - * - sample_rates (int list), - * - ch_layouts (string), - * - channel_counts (int list), - * - all_channel_counts (bool). - * Most of these options are of type binary, and should be set using - * av_opt_set_int_list() or av_opt_set_bin(). If they are not set, all - * corresponding formats are accepted. - * - * As a special case, if ch_layouts is not set, all valid channel layouts are - * accepted except for UNSPEC layouts, unless all_channel_counts is set. + * - pixel_formats (array of pixel formats), + * - colorspaces (array of int), + * - colorranges (array of int), + * - sample_formats (array of sample formats), + * - samplerates (array of int), + * - channel_layouts (array of channel layouts) + * If an option is not set, all corresponding formats are accepted. */ /** diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/graphparser.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/graphparser.c index 912804ecd6..a23e26d2e3 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/graphparser.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/graphparser.c @@ -627,7 +627,8 @@ int avfilter_graph_segment_init(AVFilterGraphSegment *seg, int flags) if (p->filter_name) return fail_creation_pending(seg, p->filter_name, __func__); - if (!p->filter || fffilterctx(p->filter)->initialized) + if (!p->filter || + (fffilterctx(p->filter)->state_flags & AV_CLASS_STATE_INITIALIZED)) continue; ret = avfilter_init_dict(p->filter, NULL); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/metal/utils.m b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/metal/utils.m index f365d3ceea..f6f344158a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/metal/utils.m +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/metal/utils.m @@ -28,18 +28,19 @@ void ff_metal_compute_encoder_dispatch(id device, NSUInteger w = pipeline.threadExecutionWidth; NSUInteger h = pipeline.maxTotalThreadsPerThreadgroup / w; MTLSize threadsPerThreadgroup = MTLSizeMake(w, h, 1); - BOOL fallback = YES; // MAC_OS_X_VERSION_10_15 is only defined on SDKs new enough to include its functionality (including iOS, tvOS, etc) #ifdef MAC_OS_X_VERSION_10_15 if (@available(macOS 10.15, iOS 11, tvOS 14.5, *)) { if ([device supportsFamily:MTLGPUFamilyCommon3]) { MTLSize threadsPerGrid = MTLSizeMake(width, height, 1); [encoder dispatchThreads:threadsPerGrid threadsPerThreadgroup:threadsPerThreadgroup]; - fallback = NO; + return; } } #endif - if (fallback) { + + // Fallback path, if we took the above one we already returned so none of this is reached + { MTLSize threadgroups = MTLSizeMake((width + w - 1) / w, (height + h - 1) / h, 1); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/opencl/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/opencl/.gitignore new file mode 100644 index 0000000000..064a8d8ef5 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/opencl/.gitignore @@ -0,0 +1 @@ +*.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/tests/.gitignore new file mode 100644 index 0000000000..db482cd49b --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/tests/.gitignore @@ -0,0 +1,12 @@ +/dnn-layer-conv2d +/dnn-layer-depth2space +/dnn-layer-maximum +/dnn-layer-pad +/dnn-layer-mathbinary +/dnn-layer-mathunary +/dnn-layer-avgpool +/dnn-layer-dense +/drawutils +/filtfmts +/formats +/integral diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.c index 0179977af4..9be076fadb 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.c @@ -26,22 +26,17 @@ #include "formats.h" #include "vaapi_vpp.h" -int ff_vaapi_vpp_query_formats(AVFilterContext *avctx) +int ff_vaapi_vpp_query_formats(const AVFilterContext *avctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) { - enum AVPixelFormat pix_fmts[] = { + static const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_VAAPI, AV_PIX_FMT_NONE, }; int err; - if ((err = ff_formats_ref(ff_make_format_list(pix_fmts), - &avctx->inputs[0]->outcfg.formats)) < 0) - return err; - if ((err = ff_formats_ref(ff_make_format_list(pix_fmts), - &avctx->outputs[0]->incfg.formats)) < 0) - return err; - - if ((err = ff_set_common_all_color_spaces(avctx)) < 0 || - (err = ff_set_common_all_color_ranges(avctx)) < 0) + err = ff_set_common_formats_from_list2(avctx, cfg_in, cfg_out, pix_fmts); + if (err < 0) return err; return 0; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.h index 6764ab0c39..0919fbbce3 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vaapi_vpp.h @@ -67,7 +67,9 @@ void ff_vaapi_vpp_ctx_init(AVFilterContext *avctx); void ff_vaapi_vpp_ctx_uninit(AVFilterContext *avctx); -int ff_vaapi_vpp_query_formats(AVFilterContext *avctx); +int ff_vaapi_vpp_query_formats(const AVFilterContext *avctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out); void ff_vaapi_vpp_pipeline_uninit(AVFilterContext *avctx); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version.h index 4d8f28e512..f191d98883 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version.h @@ -31,7 +31,7 @@ #include "version_major.h" -#define LIBAVFILTER_VERSION_MINOR 4 +#define LIBAVFILTER_VERSION_MINOR 6 #define LIBAVFILTER_VERSION_MICRO 100 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version_major.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version_major.h index c5e660eeda..b2c761370f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version_major.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/version_major.h @@ -36,5 +36,6 @@ */ #define FF_API_LINK_PUBLIC (LIBAVFILTER_VERSION_MAJOR < 11) +#define FF_API_BUFFERSINK_OPTS (LIBAVFILTER_VERSION_MAJOR < 11) #endif /* AVFILTER_VERSION_MAJOR_H */ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_coreimage.m b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_coreimage.m index b4a4d6c476..b660d4373a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_coreimage.m +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_coreimage.m @@ -102,19 +102,13 @@ static void list_filters(CoreImageContext *ctx) filter_categories = [NSArray arrayWithObjects:kCICategoryGenerator, nil]; } - NSArray *filter_names = [CIFilter filterNamesInCategories:filter_categories]; - NSEnumerator *filters = [filter_names objectEnumerator]; + for (NSString *filter_name in [CIFilter filterNamesInCategories:filter_categories]) { + CIFilter *filter = [CIFilter filterWithName:filter_name]; + NSDictionary *filter_attribs = [filter attributes]; - NSString *filter_name; - while (filter_name = [filters nextObject]) { av_log(ctx, AV_LOG_INFO, "Filter: %s\n", [filter_name UTF8String]); - NSString *input; - CIFilter *filter = [CIFilter filterWithName:filter_name]; - NSDictionary *filter_attribs = [filter attributes]; // - NSArray *filter_inputs = [filter inputKeys]; // - - for (input in filter_inputs) { + for (NSString *input in [filter inputKeys]) { NSDictionary *input_attribs = [filter_attribs valueForKey:input]; NSString *input_class = [input_attribs valueForKey:kCIAttributeClass]; if ([input_class isEqualToString:@"NSNumber"]) { @@ -303,14 +297,18 @@ static int request_frame(AVFilterLink *link) frame->pts = ctx->pts; frame->duration = 1; + frame->flags |= AV_FRAME_FLAG_KEY; + frame->flags &= ~AV_FRAME_FLAG_INTERLACED; + +FF_DISABLE_DEPRECATION_WARNINGS #if FF_API_FRAME_KEY frame->key_frame = 1; #endif - frame->flags |= AV_FRAME_FLAG_KEY; #if FF_API_INTERLACED_FRAME frame->interlaced_frame = 0; #endif - frame->flags &= ~AV_FRAME_FLAG_INTERLACED; +FF_ENABLE_DEPRECATION_WARNINGS + frame->pict_type = AV_PICTURE_TYPE_I; frame->sample_aspect_ratio = ctx->sar; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_deinterlace_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_deinterlace_vaapi.c index 0478d6daef..35e68d6e17 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_deinterlace_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_deinterlace_vaapi.c @@ -425,7 +425,7 @@ const AVFilter ff_vf_deinterlace_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(deint_vaapi_inputs), FILTER_OUTPUTS(deint_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &deint_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_drawbox_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_drawbox_vaapi.c index 5b4f23066f..aa18f2fd39 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_drawbox_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_drawbox_vaapi.c @@ -364,6 +364,6 @@ const AVFilter ff_vf_drawbox_vaapi = { .uninit = &drawbox_vaapi_uninit, FILTER_INPUTS(drawbox_vaapi_inputs), FILTER_OUTPUTS(drawbox_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_misc_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_misc_vaapi.c index 3717a21930..981fcc878d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_misc_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_misc_vaapi.c @@ -249,7 +249,7 @@ const AVFilter ff_vf_denoise_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(misc_vaapi_inputs), FILTER_OUTPUTS(misc_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &denoise_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; @@ -262,7 +262,7 @@ const AVFilter ff_vf_sharpness_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(misc_vaapi_inputs), FILTER_OUTPUTS(misc_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &sharpness_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_pad_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_pad_vaapi.c index f45e503141..478b89d117 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_pad_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_pad_vaapi.c @@ -278,6 +278,6 @@ const AVFilter ff_vf_pad_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(pad_vaapi_inputs), FILTER_OUTPUTS(pad_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_procamp_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_procamp_vaapi.c index be3cdae8bb..a9e589f1b6 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_procamp_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_procamp_vaapi.c @@ -247,7 +247,7 @@ const AVFilter ff_vf_procamp_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(procamp_vaapi_inputs), FILTER_OUTPUTS(procamp_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &procamp_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_scale_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_scale_vaapi.c index 41f119bb55..65db05c23d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_scale_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_scale_vaapi.c @@ -299,7 +299,7 @@ const AVFilter ff_vf_scale_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(scale_vaapi_inputs), FILTER_OUTPUTS(scale_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &scale_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_tonemap_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_tonemap_vaapi.c index b1cc03709a..62e251224c 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_tonemap_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_tonemap_vaapi.c @@ -567,7 +567,7 @@ const AVFilter ff_vf_tonemap_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(tonemap_vaapi_inputs), FILTER_OUTPUTS(tonemap_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &tonemap_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_transpose_vaapi.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_transpose_vaapi.c index 4976958be1..d49c44b9d1 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_transpose_vaapi.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_transpose_vaapi.c @@ -280,7 +280,7 @@ const AVFilter ff_vf_transpose_vaapi = { .uninit = &ff_vaapi_vpp_ctx_uninit, FILTER_INPUTS(transpose_vaapi_inputs), FILTER_OUTPUTS(transpose_vaapi_outputs), - FILTER_QUERY_FUNC(&ff_vaapi_vpp_query_formats), + FILTER_QUERY_FUNC2(&ff_vaapi_vpp_query_formats), .priv_class = &transpose_vaapi_class, .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE, }; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_yadif_videotoolbox.m b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_yadif_videotoolbox.m index 09520db35d..f9e8326565 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_yadif_videotoolbox.m +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vf_yadif_videotoolbox.m @@ -173,7 +173,6 @@ exit: static av_cold void do_uninit(AVFilterContext *ctx) API_AVAILABLE(macos(10.11), ios(8.0)) { YADIFVTContext *s = ctx->priv; - YADIFContext *y = &s->yadif; ff_yadif_uninit(ctx); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vulkan/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vulkan/.gitignore new file mode 100644 index 0000000000..064a8d8ef5 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavfilter/vulkan/.gitignore @@ -0,0 +1 @@ +*.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/.gitignore new file mode 100644 index 0000000000..fb70c122c4 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/.gitignore @@ -0,0 +1,3 @@ +/protocol_list.c +/muxer_list.c +/demuxer_list.c diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.c index 06dcde0565..89ee669bf9 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.c @@ -102,6 +102,8 @@ void ff_free_stream_group(AVStreamGroup **pstg) case AV_STREAM_GROUP_PARAMS_TILE_GRID: av_opt_free(stg->params.tile_grid); av_freep(&stg->params.tile_grid->offsets); + av_packet_side_data_free(&stg->params.tile_grid->coded_side_data, + &stg->params.tile_grid->nb_coded_side_data); av_freep(&stg->params.tile_grid); break; case AV_STREAM_GROUP_PARAMS_LCEVC: diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.h index 56c1c80289..2e5f2dc795 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/avformat.h @@ -1082,6 +1082,19 @@ typedef struct AVStreamGroupTileGrid { * final image before presentation. */ int height; + + /** + * Additional data associated with the grid. + * + * Should be allocated with av_packet_side_data_new() or + * av_packet_side_data_add(), and will be freed by avformat_free_context(). + */ + AVPacketSideData *coded_side_data; + + /** + * Amount of entries in @ref coded_side_data. + */ + int nb_coded_side_data; } AVStreamGroupTileGrid; /** diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/dump.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/dump.c index f20c2c4953..7dc7f0ad5a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/dump.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/dump.c @@ -377,7 +377,7 @@ static void dump_ambient_viewing_environment_metadata(void *ctx, const AVPacketS av_q2d(ambient->ambient_light_y)); } -static void dump_spherical(void *ctx, const AVCodecParameters *par, +static void dump_spherical(void *ctx, int w, int h, const AVPacketSideData *sd, int log_level) { const AVSphericalMapping *spherical = (const AVSphericalMapping *)sd->data; @@ -399,7 +399,7 @@ static void dump_spherical(void *ctx, const AVCodecParameters *par, if (spherical->projection == AV_SPHERICAL_EQUIRECTANGULAR_TILE) { size_t l, t, r, b; - av_spherical_tile_bounds(spherical, par->width, par->height, + av_spherical_tile_bounds(spherical, w, h, &l, &t, &r, &b); av_log(ctx, log_level, "[%"SIZE_SPECIFIER", %"SIZE_SPECIFIER", %"SIZE_SPECIFIER", %"SIZE_SPECIFIER"] ", @@ -427,7 +427,7 @@ static void dump_dovi_conf(void *ctx, const AVPacketSideData *sd, dovi->dv_md_compression); } -static void dump_s12m_timecode(void *ctx, const AVStream *st, const AVPacketSideData *sd, +static void dump_s12m_timecode(void *ctx, AVRational avg_frame_rate, const AVPacketSideData *sd, int log_level) { const uint32_t *tc = (const uint32_t *)sd->data; @@ -439,7 +439,7 @@ static void dump_s12m_timecode(void *ctx, const AVStream *st, const AVPacketSide for (int j = 1; j <= tc[0]; j++) { char tcbuf[AV_TIMECODE_STR_SIZE]; - av_timecode_make_smpte_tc_string2(tcbuf, st->avg_frame_rate, tc[j], 0, 0); + av_timecode_make_smpte_tc_string2(tcbuf, avg_frame_rate, tc[j], 0, 0); av_log(ctx, log_level, "timecode - %s%s", tcbuf, j != tc[0] ? ", " : ""); } } @@ -461,16 +461,17 @@ static void dump_cropping(void *ctx, const AVPacketSideData *sd) av_log(ctx, AV_LOG_INFO, "%d/%d/%d/%d", left, right, top, bottom); } -static void dump_sidedata(void *ctx, const AVStream *st, const char *indent, - int log_level) +static void dump_sidedata(void *ctx, const AVPacketSideData *side_data, int nb_side_data, + int w, int h, AVRational avg_frame_rate, + const char *indent, int log_level) { int i; - if (st->codecpar->nb_coded_side_data) + if (nb_side_data) av_log(ctx, log_level, "%sSide data:\n", indent); - for (i = 0; i < st->codecpar->nb_coded_side_data; i++) { - const AVPacketSideData *sd = &st->codecpar->coded_side_data[i]; + for (i = 0; i < nb_side_data; i++) { + const AVPacketSideData *sd = &side_data[i]; av_log(ctx, log_level, "%s ", indent); switch (sd->type) { @@ -516,7 +517,7 @@ static void dump_sidedata(void *ctx, const AVStream *st, const char *indent, break; case AV_PKT_DATA_SPHERICAL: av_log(ctx, log_level, "spherical: "); - dump_spherical(ctx, st->codecpar, sd, log_level); + dump_spherical(ctx, w, h, sd, log_level); break; case AV_PKT_DATA_CONTENT_LIGHT_LEVEL: dump_content_light_metadata(ctx, sd, log_level); @@ -530,7 +531,7 @@ static void dump_sidedata(void *ctx, const AVStream *st, const char *indent, break; case AV_PKT_DATA_S12M_TIMECODE: av_log(ctx, log_level, "SMPTE ST 12-1:2014: "); - dump_s12m_timecode(ctx, st, sd, log_level); + dump_s12m_timecode(ctx, avg_frame_rate, sd, log_level); break; case AV_PKT_DATA_AMBIENT_VIEWING_ENVIRONMENT: dump_ambient_viewing_environment_metadata(ctx, sd); @@ -602,8 +603,8 @@ static void dump_stream_format(const AVFormatContext *ic, int i, const FFStream *const sti = cffstream(st); const AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, 0); const char *separator = ic->dump_separator; - const char *group_indent = group_index >= 0 ? " " : ""; - const char *extra_indent = group_index >= 0 ? " " : " "; + const char *group_indent = group_index >= 0 ? " " : ""; + const char *extra_indent = group_index >= 0 ? " " : " "; AVCodecContext *avctx; int ret; @@ -678,7 +679,9 @@ static void dump_stream_format(const AVFormatContext *ic, int i, dump_metadata(NULL, st->metadata, extra_indent, log_level); - dump_sidedata(NULL, st, extra_indent, log_level); + dump_sidedata(NULL, st->codecpar->coded_side_data, st->codecpar->nb_coded_side_data, + st->codecpar->width, st->codecpar->height, st->avg_frame_rate, + extra_indent, log_level); } static void dump_stream_group(const AVFormatContext *ic, uint8_t *printed, @@ -782,11 +785,21 @@ static void dump_stream_group(const AVFormatContext *ic, uint8_t *printed, dump_disposition(stg->disposition, AV_LOG_INFO); av_log(NULL, AV_LOG_INFO, "\n"); dump_metadata(NULL, stg->metadata, " ", AV_LOG_INFO); - for (int i = 0; i < stg->nb_streams; i++) { - const AVStream *st = stg->streams[i]; + dump_sidedata(NULL, tile_grid->coded_side_data, tile_grid->nb_coded_side_data, + tile_grid->width, tile_grid->height, (AVRational) {0,1}, + " ", AV_LOG_INFO); + for (int i = 0; i < tile_grid->nb_tiles; i++) { + const AVStream *st = stg->streams[tile_grid->offsets[i].idx]; dump_stream_format(ic, st->index, i, index, is_output, AV_LOG_VERBOSE); printed[st->index] = 1; } + for (int i = 0; i < stg->nb_streams; i++) { + const AVStream *st = stg->streams[i]; + if (!printed[st->index]) { + dump_stream_format(ic, st->index, i, index, is_output, AV_LOG_INFO); + printed[st->index] = 1; + } + } break; } case AV_STREAM_GROUP_PARAMS_LCEVC: { diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/hlsenc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/hlsenc.c index 1e932b7b0e..571d6b2752 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/hlsenc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/hlsenc.c @@ -1668,7 +1668,7 @@ static int hls_window(AVFormatContext *s, int last, VariantStream *vs) ff_hls_write_playlist_header(hls->sub_m3u8_out, hls->version, hls->allowcache, target_duration, sequence, PLAYLIST_TYPE_NONE, 0); for (en = vs->segments; en; en = en->next) { - ret = ff_hls_write_file_entry(hls->sub_m3u8_out, 0, byterange_mode, + ret = ff_hls_write_file_entry(hls->sub_m3u8_out, en->discont, byterange_mode, en->duration, 0, en->size, en->pos, hls->baseurl, en->sub_filename, NULL, 0, 0, 0); if (ret < 0) { diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/img2enc.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/img2enc.c index 526a11e5ee..41638d92b8 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/img2enc.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/img2enc.c @@ -160,13 +160,13 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt) return AVERROR(EINVAL); } } else if (img->frame_pts) { - if (av_get_frame_filename2(filename, sizeof(filename), s->url, pkt->pts, AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) { + if (ff_get_frame_filename(filename, sizeof(filename), s->url, pkt->pts, AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) { av_log(s, AV_LOG_ERROR, "Cannot write filename by pts of the frames."); return AVERROR(EINVAL); } - } else if (av_get_frame_filename2(filename, sizeof(filename), s->url, - img->img_number, - AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) { + } else if (ff_get_frame_filename(filename, sizeof(filename), s->url, + img->img_number, + AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) { if (img->img_number == img->start_img_number) { av_log(s, AV_LOG_WARNING, "The specified filename '%s' does not contain an image sequence pattern or a pattern is invalid.\n", s->url); av_log(s, AV_LOG_WARNING, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/internal.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/internal.h index 8e8971bfeb..6c026f08a0 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/internal.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/internal.h @@ -745,6 +745,22 @@ void ff_format_set_url(AVFormatContext *s, char *url); */ int ff_match_url_ext(const char *url, const char *extensions); +/** + * Return in 'buf' the path with '%d' replaced by a number. + * + * Also handles the '%0nd' format where 'n' is the total number + * of digits and '%%'. + * + * @param buf destination buffer + * @param buf_size destination buffer size + * @param path path with substitution template + * @param number the number to substitute + * @param flags AV_FRAME_FILENAME_FLAGS_* + * @return 0 if OK, -1 on format error + */ +int ff_get_frame_filename(char *buf, int buf_size, const char *path, + int64_t number, int flags); + struct FFOutputFormat; struct FFInputFormat; void avpriv_register_devices(const struct FFOutputFormat * const o[], diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/isom.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/isom.h index 4723397048..204addbab2 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/isom.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/isom.h @@ -283,6 +283,7 @@ typedef struct HEIFItem { int64_t extent_offset; int width; int height; + int rotation; int type; int is_idat_relative; } HEIFItem; diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mov.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mov.c index a2333ac1fd..67e87094cf 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mov.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mov.c @@ -40,6 +40,7 @@ #include "libavutil/avassert.h" #include "libavutil/avstring.h" #include "libavutil/dict.h" +#include "libavutil/display.h" #include "libavutil/mem.h" #include "libavutil/opt.h" #include "libavutil/aes.h" @@ -188,6 +189,10 @@ static int mov_read_mac_string(MOVContext *c, AVIOContext *pb, int len, return p - dst; } +/** + * Get the current stream in the parsing process. This can either be the + * latest stream added to the context, or the stream referenced by an item. + */ static AVStream *get_curr_st(MOVContext *c) { AVStream *st = NULL; @@ -206,7 +211,7 @@ static AVStream *get_curr_st(MOVContext *c) st = item->st; break; } - if (!st) + if (!st && c->cur_item_id == -1) st = c->fc->streams[c->fc->nb_streams-1]; return st; @@ -8929,6 +8934,27 @@ static int mov_read_ispe(MOVContext *c, AVIOContext *pb, MOVAtom atom) return 0; } +static int mov_read_irot(MOVContext *c, AVIOContext *pb, MOVAtom atom) +{ + int angle; + + angle = avio_r8(pb) & 0x3; + + av_log(c->fc, AV_LOG_TRACE, "irot: item_id %d, angle %u\n", + c->cur_item_id, angle); + + for (int i = 0; i < c->nb_heif_item; i++) { + if (c->heif_item[i].item_id == c->cur_item_id) { + // angle * 90 specifies the angle (in anti-clockwise direction) + // in units of degrees. + c->heif_item[i].rotation = angle * 90; + break; + } + } + + return 0; +} + static int mov_read_iprp(MOVContext *c, AVIOContext *pb, MOVAtom atom) { typedef struct MOVAtoms { @@ -9155,6 +9181,7 @@ static const MOVParseTableEntry mov_default_parse_table[] = { { MKTAG('i','d','a','t'), mov_read_idat }, { MKTAG('i','r','e','f'), mov_read_iref }, { MKTAG('i','s','p','e'), mov_read_ispe }, +{ MKTAG('i','r','o','t'), mov_read_irot }, { MKTAG('i','p','r','p'), mov_read_iprp }, { MKTAG('i','i','n','f'), mov_read_iinf }, { MKTAG('a','m','v','e'), mov_read_amve }, /* ambient viewing environment box */ @@ -9824,6 +9851,26 @@ fail: return ret; } +static int set_display_matrix_from_item(AVPacketSideData **coded_side_data, int *nb_coded_side_data, + const HEIFItem *item) +{ + int32_t *matrix; + AVPacketSideData *sd = av_packet_side_data_new(coded_side_data, + nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX, + 9 * sizeof(*matrix), 0); + if (!sd) + return AVERROR(ENOMEM); + + matrix = (int32_t*)sd->data; + /* rotation is in the counter-clockwise direction whereas + * av_display_rotation_set() expects its argument to be + * oriented clockwise, so we need to negate it. */ + av_display_rotation_set(matrix, -item->rotation); + + return 0; +} + static int read_image_grid(AVFormatContext *s, const HEIFGrid *grid, AVStreamGroupTileGrid *tile_grid) { @@ -9857,6 +9904,14 @@ static int read_image_grid(AVFormatContext *s, const HEIFGrid *grid, tile_grid->width = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); tile_grid->height = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); + /* rotation */ + if (item->rotation) { + int ret = set_display_matrix_from_item(&tile_grid->coded_side_data, + &tile_grid->nb_coded_side_data, item); + if (ret < 0) + return ret; + } + av_log(c->fc, AV_LOG_TRACE, "grid: grid_rows %d grid_cols %d output_width %d output_height %d\n", tile_rows, tile_cols, tile_grid->width, tile_grid->height); @@ -9947,6 +10002,15 @@ static int read_image_iovl(AVFormatContext *s, const HEIFGrid *grid, tile_grid->coded_width = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); tile_grid->height = tile_grid->coded_height = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); + + /* rotation */ + if (item->rotation) { + int ret = set_display_matrix_from_item(&tile_grid->coded_side_data, + &tile_grid->nb_coded_side_data, item); + if (ret < 0) + return ret; + } + av_log(c->fc, AV_LOG_TRACE, "iovl: output_width %d, output_height %d\n", tile_grid->width, tile_grid->height); @@ -10149,6 +10213,13 @@ static int mov_read_header(AVFormatContext *s) if (item->item_id == mov->primary_item_id) st->disposition |= AV_DISPOSITION_DEFAULT; + if (item->rotation) { + int ret = set_display_matrix_from_item(&st->codecpar->coded_side_data, + &st->codecpar->nb_coded_side_data, item); + if (ret < 0) + return ret; + } + mov_build_index(mov, st); } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mxfdec.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mxfdec.c index 24f4ed1c33..1b53a9771d 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mxfdec.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/mxfdec.c @@ -262,6 +262,7 @@ typedef struct MXFIndexTableSegment { int *flag_entries; uint64_t *stream_offset_entries; int nb_index_entries; + int64_t offset; } MXFIndexTableSegment; typedef struct MXFPackage { @@ -428,7 +429,7 @@ static void mxf_free_metadataset(MXFMetadataSet **ctx, enum MXFMetadataSetType t av_freep(ctx); } -static int64_t klv_decode_ber_length(AVIOContext *pb) +static int64_t klv_decode_ber_length(AVIOContext *pb, int *llen) { uint64_t size = avio_r8(pb); if (size & 0x80) { /* long form */ @@ -436,9 +437,13 @@ static int64_t klv_decode_ber_length(AVIOContext *pb) /* SMPTE 379M 5.3.4 guarantee that bytes_num must not exceed 8 bytes */ if (bytes_num > 8) return AVERROR_INVALIDDATA; + if (llen) + *llen = bytes_num + 1; size = 0; while (bytes_num--) size = size << 8 | avio_r8(pb); + } else if (llen) { + *llen = 1; } if (size > INT64_MAX) return AVERROR_INVALIDDATA; @@ -458,10 +463,28 @@ static int mxf_read_sync(AVIOContext *pb, const uint8_t *key, unsigned size) return i == size; } +// special case of mxf_read_sync for mxf_klv_key +static int mxf_read_sync_klv(AVIOContext *pb) +{ + uint32_t key = avio_rb32(pb); + // key will never match mxf_klv_key on EOF + if (key == AV_RB32(mxf_klv_key)) + return 1; + + while (!avio_feof(pb)) { + key = (key << 8) | avio_r8(pb); + if (key == AV_RB32(mxf_klv_key)) + return 1; + } + return 0; +} + static int klv_read_packet(MXFContext *mxf, KLVPacket *klv, AVIOContext *pb) { int64_t length, pos; - if (!mxf_read_sync(pb, mxf_klv_key, 4)) + int llen; + + if (!mxf_read_sync_klv(pb)) return AVERROR_INVALIDDATA; klv->offset = avio_tell(pb) - 4; if (klv->offset < mxf->run_in) @@ -469,11 +492,11 @@ static int klv_read_packet(MXFContext *mxf, KLVPacket *klv, AVIOContext *pb) memcpy(klv->key, mxf_klv_key, 4); avio_read(pb, klv->key + 4, 12); - length = klv_decode_ber_length(pb); + length = klv_decode_ber_length(pb, &llen); if (length < 0) return length; klv->length = length; - pos = avio_tell(pb); + pos = klv->offset + 16 + llen; if (pos > INT64_MAX - length) return AVERROR_INVALIDDATA; klv->next_klv = pos + length; @@ -644,15 +667,15 @@ static int mxf_decrypt_triplet(AVFormatContext *s, AVPacket *pkt, KLVPacket *klv av_aes_init(mxf->aesc, s->key, 128, 1); } // crypto context - size = klv_decode_ber_length(pb); + size = klv_decode_ber_length(pb, NULL); if (size < 0) return size; avio_skip(pb, size); // plaintext offset - klv_decode_ber_length(pb); + klv_decode_ber_length(pb ,NULL); plaintext_size = avio_rb64(pb); // source klv key - klv_decode_ber_length(pb); + klv_decode_ber_length(pb, NULL); avio_read(pb, klv->key, 16); if (!IS_KLV_KEY(klv, mxf_essence_element_key)) return AVERROR_INVALIDDATA; @@ -662,12 +685,12 @@ static int mxf_decrypt_triplet(AVFormatContext *s, AVPacket *pkt, KLVPacket *klv if (index < 0) return AVERROR_INVALIDDATA; // source size - klv_decode_ber_length(pb); + klv_decode_ber_length(pb, NULL); orig_size = avio_rb64(pb); if (orig_size < plaintext_size) return AVERROR_INVALIDDATA; // enc. code - size = klv_decode_ber_length(pb); + size = klv_decode_ber_length(pb, NULL); if (size < 32 || size - 32 < orig_size || (int)orig_size != orig_size) return AVERROR_INVALIDDATA; avio_read(pb, ivec, 16); @@ -1883,18 +1906,44 @@ static int64_t mxf_essence_container_end(MXFContext *mxf, int body_sid) /* EditUnit -> absolute offset */ static int mxf_edit_unit_absolute_offset(MXFContext *mxf, MXFIndexTable *index_table, int64_t edit_unit, AVRational edit_rate, int64_t *edit_unit_out, int64_t *offset_out, MXFPartition **partition_out, int nag) { - int i; - int64_t offset_temp = 0; + int i = 0; + int64_t index_duration, index_end; + MXFIndexTableSegment *first_segment, *last_segment; + + if (!index_table->nb_segments) { + av_log(mxf->fc, AV_LOG_ERROR, "no index table segments\n"); + return AVERROR_INVALIDDATA; + } edit_unit = av_rescale_q(edit_unit, index_table->segments[0]->index_edit_rate, edit_rate); - for (i = 0; i < index_table->nb_segments; i++) { + first_segment = index_table->segments[0]; + last_segment = index_table->segments[index_table->nb_segments - 1]; + + // clamp to actual range of index + index_end = av_sat_add64(last_segment->index_start_position, last_segment->index_duration); + edit_unit = FFMAX(FFMIN(edit_unit, index_end), first_segment->index_start_position); + + // guess which table segment this edit unit is in + // saturation is fine since it's just a guess + // if the guess is wrong we revert to a linear search + index_duration = av_sat_sub64(index_end, first_segment->index_start_position); + + // compute the guess, taking care not to cause overflow or division by zero + if (index_duration > 0 && edit_unit <= INT64_MAX / index_table->nb_segments) { + // a simple linear guesstimate + // this is accurate to within +-1 when partitions are generated at a constant rate like mxfenc does + int64_t i64 = index_table->nb_segments * edit_unit / index_duration; + // clamp and downcast to 32-bit + i = FFMAX(0, FFMIN(index_table->nb_segments - 1, i64)); + } + + for (; i >= 0 && i < index_table->nb_segments;) { MXFIndexTableSegment *s = index_table->segments[i]; - edit_unit = FFMAX(edit_unit, s->index_start_position); /* clamp if trying to seek before start */ - - if (edit_unit < s->index_start_position + s->index_duration) { + if (s->index_start_position <= edit_unit && edit_unit < s->index_start_position + s->index_duration) { int64_t index = edit_unit - s->index_start_position; + int64_t offset_temp = s->offset; if (s->edit_unit_byte_count) { if (index > INT64_MAX / s->edit_unit_byte_count || @@ -1919,14 +1968,12 @@ static int mxf_edit_unit_absolute_offset(MXFContext *mxf, MXFIndexTable *index_t *edit_unit_out = av_rescale_q(edit_unit, edit_rate, s->index_edit_rate); return mxf_absolute_bodysid_offset(mxf, index_table->body_sid, offset_temp, offset_out, partition_out); + } else if (edit_unit < s->index_start_position) { + // the segments are sorted by IndexStartPosition, so this is guaranteed to terminate + i--; } else { - /* EditUnitByteCount == 0 for VBR indexes, which is fine since they use explicit StreamOffsets */ - if (s->edit_unit_byte_count && (s->index_duration > INT64_MAX / s->edit_unit_byte_count || - s->edit_unit_byte_count * s->index_duration > INT64_MAX - offset_temp) - ) - return AVERROR_INVALIDDATA; - - offset_temp += s->edit_unit_byte_count * s->index_duration; + // edit_unit >= s->index_start_position + s->index_duration + i++; } } @@ -2111,6 +2158,7 @@ static int mxf_compute_index_tables(MXFContext *mxf) for (int i = 0, j = 0; j < mxf->nb_index_tables; i += mxf->index_tables[j++].nb_segments) { MXFIndexTable *t = &mxf->index_tables[j]; MXFTrack *mxf_track = NULL; + int64_t offset_temp = 0; t->segments = av_calloc(t->nb_segments, sizeof(*t->segments)); if (!t->segments) { @@ -2139,8 +2187,10 @@ static int mxf_compute_index_tables(MXFContext *mxf) } } - /* fix zero IndexDurations */ + /* fix zero IndexDurations and compute segment offsets */ for (int k = 0; k < t->nb_segments; k++) { + MXFIndexTableSegment *s = t->segments[k]; + if (!t->segments[k]->index_edit_rate.num || !t->segments[k]->index_edit_rate.den) { av_log(mxf->fc, AV_LOG_WARNING, "IndexSID %i segment %i has invalid IndexEditRate\n", t->index_sid, k); @@ -2148,6 +2198,17 @@ static int mxf_compute_index_tables(MXFContext *mxf) t->segments[k]->index_edit_rate = mxf_track->edit_rate; } + s->offset = offset_temp; + + /* EditUnitByteCount == 0 for VBR indexes, which is fine since they use explicit StreamOffsets */ + if (s->edit_unit_byte_count && (s->index_duration > INT64_MAX / s->edit_unit_byte_count || + s->edit_unit_byte_count * s->index_duration > INT64_MAX - offset_temp)) { + ret = AVERROR_INVALIDDATA; + goto finish_decoding_index; + } + + offset_temp += t->segments[k]->edit_unit_byte_count * t->segments[k]->index_duration; + if (t->segments[k]->index_duration) continue; @@ -3982,6 +4043,7 @@ static int mxf_read_packet(AVFormatContext *s, AVPacket *pkt) ret = klv_read_packet(mxf, &klv, s->pb); if (ret < 0) break; + // klv.key[0..3] == mxf_klv_key from here forward max_data_size = klv.length; pos = klv.next_klv - klv.length; PRINT_KEY(s, "read packet", klv.key); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/oggdec.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/oggdec.c index deb566593c..5339fdd32c 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/oggdec.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/oggdec.c @@ -373,8 +373,7 @@ static int ogg_read_page(AVFormatContext *s, int *sid, int probing) flags = avio_r8(bc); gp = avio_rl64(bc); serial = avio_rl32(bc); - avio_rl32(bc) /* seq */; - //avio_skip(bc, 4); /* seq */ + avio_skip(bc, 4); /* seq */ crc_tmp = ffio_get_checksum(bc); crc = avio_rb32(bc); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/tests/.gitignore new file mode 100644 index 0000000000..cdd0cce061 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/tests/.gitignore @@ -0,0 +1,9 @@ +/fifo_muxer +/imf +/movenc +/noproxy +/rtmpdh +/seek +/srtp +/url +/seek_utils diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/utils.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/utils.c index e9ded627ad..e892e8bde7 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/utils.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/utils.c @@ -280,7 +280,7 @@ uint64_t ff_parse_ntp_time(uint64_t ntp_ts) return (sec * 1000000) + usec; } -int av_get_frame_filename2(char *buf, int buf_size, const char *path, int number, int flags) +int ff_get_frame_filename(char *buf, int buf_size, const char *path, int64_t number, int flags) { const char *p; char *q, buf1[20], c; @@ -313,7 +313,7 @@ int av_get_frame_filename2(char *buf, int buf_size, const char *path, int number percentd_found = 1; if (number < 0) nd += 1; - snprintf(buf1, sizeof(buf1), "%0*d", nd, number); + snprintf(buf1, sizeof(buf1), "%0*" PRId64, nd, number); len = strlen(buf1); if ((q - buf + len) > buf_size - 1) goto fail; @@ -338,9 +338,14 @@ fail: return -1; } +int av_get_frame_filename2(char *buf, int buf_size, const char *path, int number, int flags) +{ + return ff_get_frame_filename(buf, buf_size, path, number, flags); +} + int av_get_frame_filename(char *buf, int buf_size, const char *path, int number) { - return av_get_frame_filename2(buf, buf_size, path, number, 0); + return ff_get_frame_filename(buf, buf_size, path, number, 0); } void av_url_split(char *proto, int proto_size, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/version.h index 70c554c19c..1b079ebce8 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavformat/version.h @@ -31,7 +31,7 @@ #include "version_major.h" -#define LIBAVFORMAT_VERSION_MINOR 7 +#define LIBAVFORMAT_VERSION_MINOR 9 #define LIBAVFORMAT_VERSION_MICRO 100 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/.gitignore new file mode 100644 index 0000000000..4dc7466772 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/.gitignore @@ -0,0 +1,2 @@ +/avconfig.h +/ffversion.h diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/Makefile b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/Makefile index eba0151337..992e95e4df 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/Makefile +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/Makefile @@ -4,3 +4,5 @@ OBJS += aarch64/cpu.o \ NEON-OBJS += aarch64/float_dsp_neon.o \ aarch64/tx_float_neon.o \ + +SVE-OBJS += aarch64/cpu_sve.o \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/asm.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/asm.S index 1840f9fb01..50ce7d4dfd 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/asm.S +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/asm.S @@ -56,8 +56,26 @@ #define DISABLE_I8MM #endif +#if HAVE_AS_ARCHEXT_SVE_DIRECTIVE +#define ENABLE_SVE .arch_extension sve +#define DISABLE_SVE .arch_extension nosve +#else +#define ENABLE_SVE +#define DISABLE_SVE +#endif + +#if HAVE_AS_ARCHEXT_SVE2_DIRECTIVE +#define ENABLE_SVE2 .arch_extension sve2 +#define DISABLE_SVE2 .arch_extension nosve2 +#else +#define ENABLE_SVE2 +#define DISABLE_SVE2 +#endif + DISABLE_DOTPROD DISABLE_I8MM +DISABLE_SVE +DISABLE_SVE2 /* Support macros for diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.c index 7631d13de0..e82c0f19ab 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.c @@ -25,6 +25,8 @@ #include #define HWCAP_AARCH64_ASIMDDP (1 << 20) +#define HWCAP_AARCH64_SVE (1 << 22) +#define HWCAP2_AARCH64_SVE2 (1 << 1) #define HWCAP2_AARCH64_I8MM (1 << 13) static int detect_flags(void) @@ -36,6 +38,10 @@ static int detect_flags(void) if (hwcap & HWCAP_AARCH64_ASIMDDP) flags |= AV_CPU_FLAG_DOTPROD; + if (hwcap & HWCAP_AARCH64_SVE) + flags |= AV_CPU_FLAG_SVE; + if (hwcap2 & HWCAP2_AARCH64_SVE2) + flags |= AV_CPU_FLAG_SVE2; if (hwcap2 & HWCAP2_AARCH64_I8MM) flags |= AV_CPU_FLAG_I8MM; @@ -112,6 +118,21 @@ static int detect_flags(void) #ifdef PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE if (IsProcessorFeaturePresent(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE)) flags |= AV_CPU_FLAG_DOTPROD; +#endif +#ifdef PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE + /* There's no PF_* flag that indicates whether plain I8MM is available + * or not. But if SVE_I8MM is available, that also implies that + * regular I8MM is available. */ + if (IsProcessorFeaturePresent(PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE)) + flags |= AV_CPU_FLAG_I8MM; +#endif +#ifdef PF_ARM_SVE_INSTRUCTIONS_AVAILABLE + if (IsProcessorFeaturePresent(PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)) + flags |= AV_CPU_FLAG_SVE; +#endif +#ifdef PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE + if (IsProcessorFeaturePresent(PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)) + flags |= AV_CPU_FLAG_SVE2; #endif return flags; } @@ -135,6 +156,12 @@ int ff_get_cpu_flags_aarch64(void) #ifdef __ARM_FEATURE_MATMUL_INT8 flags |= AV_CPU_FLAG_I8MM; #endif +#ifdef __ARM_FEATURE_SVE + flags |= AV_CPU_FLAG_SVE; +#endif +#ifdef __ARM_FEATURE_SVE2 + flags |= AV_CPU_FLAG_SVE2; +#endif flags |= detect_flags(); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.h index 64d703be37..a41b729659 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu.h @@ -27,5 +27,11 @@ #define have_vfp(flags) CPUEXT(flags, VFP) #define have_dotprod(flags) CPUEXT(flags, DOTPROD) #define have_i8mm(flags) CPUEXT(flags, I8MM) +#define have_sve(flags) CPUEXT(flags, SVE) +#define have_sve2(flags) CPUEXT(flags, SVE2) + +#if HAVE_SVE +int ff_aarch64_sve_length(void); +#endif #endif /* AVUTIL_AARCH64_CPU_H */ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu_sve.S b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu_sve.S new file mode 100644 index 0000000000..d216ed2c49 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/aarch64/cpu_sve.S @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Martin Storsjo + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" +#include "asm.S" + +ENABLE_SVE + +function ff_aarch64_sve_length, export=1 + cntb x0 + ret +endfunc diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/avutil.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/avutil.h index d2900dcb48..ee709fbb2a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/avutil.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/avutil.h @@ -314,6 +314,7 @@ static inline void *av_x_if_null(const void *p, const void *x) return (void *)(intptr_t)(p ? p : x); } +#if FF_API_OPT_INT_LIST /** * Compute the length of an integer list. * @@ -322,6 +323,7 @@ static inline void *av_x_if_null(const void *p, const void *x) * @param list pointer to the list * @return length of the list, in elements, not counting the terminator */ +attribute_deprecated unsigned av_int_list_length_for_size(unsigned elsize, const void *list, uint64_t term) av_pure; @@ -334,6 +336,7 @@ unsigned av_int_list_length_for_size(unsigned elsize, */ #define av_int_list_length(list, term) \ av_int_list_length_for_size(sizeof(*(list)), list, term) +#endif /** * Return the fractional representation of the internal time base. diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.c index 44cbb9e9ff..f1184192be 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.c @@ -182,6 +182,8 @@ int av_parse_cpu_caps(unsigned *flags, const char *s) { "vfp", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_VFP }, .unit = "flags" }, { "dotprod", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_DOTPROD }, .unit = "flags" }, { "i8mm", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_I8MM }, .unit = "flags" }, + { "sve", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_SVE }, .unit = "flags" }, + { "sve2", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_SVE2 }, .unit = "flags" }, #elif ARCH_MIPS { "mmi", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_MMI }, .unit = "flags" }, { "msa", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_CPU_FLAG_MSA }, .unit = "flags" }, diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.h index ba6c234e04..6b6e50f07a 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/cpu.h @@ -72,6 +72,8 @@ #define AV_CPU_FLAG_VFP_VM (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in various CPUs implementations #define AV_CPU_FLAG_DOTPROD (1 << 8) #define AV_CPU_FLAG_I8MM (1 << 9) +#define AV_CPU_FLAG_SVE (1 <<10) +#define AV_CPU_FLAG_SVE2 (1 <<11) #define AV_CPU_FLAG_SETEND (1 <<16) #define AV_CPU_FLAG_MMI (1 << 0) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/log.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/log.h index ab7ceabe22..4c8c92266f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/log.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/log.h @@ -46,6 +46,15 @@ typedef enum { AV_CLASS_CATEGORY_NB ///< not part of ABI/API }AVClassCategory; +enum AVClassStateFlags { + /** + * Object initialization has finished and it is now in the 'runtime' stage. + * This affects e.g. what options can be set on the object (only + * AV_OPT_FLAG_RUNTIME_PARAM options can be set on initialized objects). + */ + AV_CLASS_STATE_INITIALIZED = (1 << 0), +}; + #define AV_IS_INPUT_DEVICE(category) \ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \ @@ -77,7 +86,9 @@ typedef struct AVClass { const char* (*item_name)(void* ctx); /** - * a pointer to the first option specified in the class if any or NULL + * An array of options for the structure or NULL. + * When non-NULL, the array must be terminated by an option with a NULL + * name. * * @see av_set_default_options() */ @@ -85,43 +96,50 @@ typedef struct AVClass { /** * LIBAVUTIL_VERSION with which this structure was created. - * This is used to allow fields to be added without requiring major - * version bumps everywhere. + * This is used to allow fields to be added to AVClass without requiring + * major version bumps everywhere. */ int version; /** - * Offset in the structure where log_level_offset is stored. - * 0 means there is no such variable + * Offset in the structure where the log level offset is stored. The log + * level offset is an int added to the log level for logging with this + * object as the context. + * + * 0 means there is no such variable. */ int log_level_offset_offset; /** * Offset in the structure where a pointer to the parent context for * logging is stored. For example a decoder could pass its AVCodecContext - * to eval as such a parent context, which an av_log() implementation + * to eval as such a parent context, which an ::av_log() implementation * could then leverage to display the parent context. - * The offset can be NULL. + * + * When the pointer is NULL, or this offset is zero, the object is assumed + * to have no parent. */ int parent_log_context_offset; /** - * Category used for visualization (like color) - * This is only set if the category is equal for all objects using this class. - * available since version (51 << 16 | 56 << 8 | 100) + * Category used for visualization (like color). + * + * Only used when ::get_category() is NULL. Use this field when all + * instances of this class have the same category, use ::get_category() + * otherwise. */ AVClassCategory category; /** - * Callback to return the category. - * available since version (51 << 16 | 59 << 8 | 100) + * Callback to return the instance category. Use this callback when + * different instances of this class may have different categories, + * ::category otherwise. */ AVClassCategory (*get_category)(void* ctx); /** * Callback to return the supported/allowed ranges. - * available since version (52.12) */ int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags); @@ -139,11 +157,22 @@ typedef struct AVClass { * @return AVClass for the next AVOptions-enabled child or NULL if there are * no more such children. * - * @note The difference between child_next and this is that child_next - * iterates over _already existing_ objects, while child_class_iterate - * iterates over _all possible_ children. + * @note The difference between ::child_next() and ::child_class_iterate() + * is that ::child_next() iterates over _actual_ children of an + * _existing_ object instance, while ::child_class_iterate() iterates + * over the classes of all _potential_ children of any possible + * instance of this class. */ const struct AVClass* (*child_class_iterate)(void **iter); + + /** + * When non-zero, offset in the object to an unsigned int holding object + * state flags, a combination of AVClassStateFlags values. The flags are + * updated by the object to signal its state to the generic code. + * + * Added in version 59.41.100. + */ + int state_flags_offset; } AVClass; /** diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.c index 93f2bb1320..7dc4fdb062 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.c @@ -58,26 +58,29 @@ const AVOption *av_opt_next(const void *obj, const AVOption *last) return NULL; } -static const size_t opt_elem_size[] = { - [AV_OPT_TYPE_FLAGS] = sizeof(unsigned), - [AV_OPT_TYPE_INT] = sizeof(int), - [AV_OPT_TYPE_INT64] = sizeof(int64_t), - [AV_OPT_TYPE_UINT] = sizeof(unsigned), - [AV_OPT_TYPE_UINT64] = sizeof(uint64_t), - [AV_OPT_TYPE_DOUBLE] = sizeof(double), - [AV_OPT_TYPE_FLOAT] = sizeof(float), - [AV_OPT_TYPE_STRING] = sizeof(char *), - [AV_OPT_TYPE_RATIONAL] = sizeof(AVRational), - [AV_OPT_TYPE_BINARY] = sizeof(uint8_t *), - [AV_OPT_TYPE_DICT] = sizeof(AVDictionary *), - [AV_OPT_TYPE_IMAGE_SIZE] = sizeof(int[2]), - [AV_OPT_TYPE_VIDEO_RATE] = sizeof(AVRational), - [AV_OPT_TYPE_PIXEL_FMT] = sizeof(int), - [AV_OPT_TYPE_SAMPLE_FMT] = sizeof(int), - [AV_OPT_TYPE_DURATION] = sizeof(int64_t), - [AV_OPT_TYPE_COLOR] = sizeof(uint8_t[4]), - [AV_OPT_TYPE_CHLAYOUT] = sizeof(AVChannelLayout), - [AV_OPT_TYPE_BOOL] = sizeof(int), +static const struct { + size_t size; + const char *name; +} opt_type_desc[] = { + [AV_OPT_TYPE_FLAGS] = { sizeof(unsigned), "" }, + [AV_OPT_TYPE_INT] = { sizeof(int), "" }, + [AV_OPT_TYPE_INT64] = { sizeof(int64_t), "" }, + [AV_OPT_TYPE_UINT] = { sizeof(unsigned), "" }, + [AV_OPT_TYPE_UINT64] = { sizeof(uint64_t), "" }, + [AV_OPT_TYPE_DOUBLE] = { sizeof(double), "" }, + [AV_OPT_TYPE_FLOAT] = { sizeof(float), "" }, + [AV_OPT_TYPE_STRING] = { sizeof(char *), "" }, + [AV_OPT_TYPE_RATIONAL] = { sizeof(AVRational), "" }, + [AV_OPT_TYPE_BINARY] = { sizeof(uint8_t *), "" }, + [AV_OPT_TYPE_DICT] = { sizeof(AVDictionary *), "" }, + [AV_OPT_TYPE_IMAGE_SIZE] = { sizeof(int[2]), "" }, + [AV_OPT_TYPE_VIDEO_RATE] = { sizeof(AVRational), "" }, + [AV_OPT_TYPE_PIXEL_FMT] = { sizeof(int), "" }, + [AV_OPT_TYPE_SAMPLE_FMT] = { sizeof(int), "" }, + [AV_OPT_TYPE_DURATION] = { sizeof(int64_t), "" }, + [AV_OPT_TYPE_COLOR] = { sizeof(uint8_t[4]), "" }, + [AV_OPT_TYPE_CHLAYOUT] = { sizeof(AVChannelLayout),"" }, + [AV_OPT_TYPE_BOOL] = { sizeof(int), "" }, }; // option is plain old data @@ -114,7 +117,7 @@ static uint8_t opt_array_sep(const AVOption *o) static void *opt_array_pelem(const AVOption *o, void *array, unsigned idx) { av_assert1(o->type & AV_OPT_TYPE_FLAG_ARRAY); - return (uint8_t *)array + idx * opt_elem_size[TYPE_BASE(o->type)]; + return (uint8_t *)array + idx * opt_type_desc[TYPE_BASE(o->type)].size; } static unsigned *opt_array_pcount(const void *parray) @@ -152,6 +155,82 @@ static void opt_free_array(const AVOption *o, void *parray, unsigned *count) *count = 0; } +/** + * Perform common setup for option-setting functions. + * + * @param require_type when non-0, require the option to be of this type + * @param ptgt target object is written here + * @param po the option is written here + * @param pdst pointer to option value is written here + */ +static int opt_set_init(void *obj, const char *name, int search_flags, + int require_type, + void **ptgt, const AVOption **po, void **pdst) +{ + const AVOption *o; + void *tgt; + + o = av_opt_find2(obj, name, NULL, 0, search_flags, &tgt); + if (!o || !tgt) + return AVERROR_OPTION_NOT_FOUND; + + if (o->flags & AV_OPT_FLAG_READONLY) + return AVERROR(EINVAL); + + if (require_type && (o->type != require_type)) { + av_log(obj, AV_LOG_ERROR, + "Tried to set option '%s' of type %s from value of type %s, " + "this is not supported\n", o->name, opt_type_desc[o->type].name, + opt_type_desc[require_type].name); + return AVERROR(EINVAL); + } + + if (!(o->flags & AV_OPT_FLAG_RUNTIME_PARAM)) { + unsigned *state_flags = NULL; + const AVClass *class; + + // try state flags first from the target (child), then from its parent + class = *(const AVClass**)tgt; + if ( +#if LIBAVUTIL_VERSION_MAJOR < 60 + class->version >= AV_VERSION_INT(59, 41, 100) && +#endif + class->state_flags_offset) + state_flags = (unsigned*)((uint8_t*)tgt + class->state_flags_offset); + + if (!state_flags && obj != tgt) { + class = *(const AVClass**)obj; + if ( +#if LIBAVUTIL_VERSION_MAJOR < 60 + class->version >= AV_VERSION_INT(59, 41, 100) && +#endif + class->state_flags_offset) + state_flags = (unsigned*)((uint8_t*)obj + class->state_flags_offset); + } + + if (state_flags && (*state_flags & AV_CLASS_STATE_INITIALIZED)) { + av_log(obj, AV_LOG_ERROR, "Option '%s' is not a runtime option and " + "so cannot be set after the object has been initialized\n", + o->name); +#if LIBAVUTIL_VERSION_MAJOR >= 60 + return AVERROR(EINVAL); +#endif + } + } + + if (o->flags & AV_OPT_FLAG_DEPRECATED) + av_log(obj, AV_LOG_WARNING, "The \"%s\" option is deprecated: %s\n", name, o->help); + + if (po) + *po = o; + if (ptgt) + *ptgt = tgt; + if (pdst) + *pdst = ((uint8_t *)tgt) + o->offset; + + return 0; +} + static int read_number(const AVOption *o, const void *dst, double *num, int *den, int64_t *intnum) { switch (TYPE_BASE(o->type)) { @@ -670,7 +749,7 @@ static int opt_set_array(void *obj, void *target_obj, const AVOption *o, const char *val, void *dst) { const AVOptionArrayDef *arr = o->default_val.arr; - const size_t elem_size = opt_elem_size[TYPE_BASE(o->type)]; + const size_t elem_size = opt_type_desc[TYPE_BASE(o->type)].size; const uint8_t sep = opt_array_sep(o); uint8_t *str = NULL; @@ -748,17 +827,12 @@ fail: int av_opt_set(void *obj, const char *name, const char *val, int search_flags) { void *dst, *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; + const AVOption *o; + int ret; - if (o->flags & AV_OPT_FLAG_READONLY) - return AVERROR(EINVAL); - - if (o->flags & AV_OPT_FLAG_DEPRECATED) - av_log(obj, AV_LOG_WARNING, "The \"%s\" option is deprecated: %s\n", name, o->help); - - dst = ((uint8_t *)target_obj) + o->offset; + ret = opt_set_init(obj, name, search_flags, 0, &target_obj, &o, &dst); + if (ret < 0) + return ret; return ((o->type & AV_OPT_TYPE_FLAG_ARRAY) ? opt_set_array : opt_set_elem)(obj, target_obj, o, val, dst); @@ -782,55 +856,50 @@ OPT_EVAL_NUMBER(double, AV_OPT_TYPE_DOUBLE, double) OPT_EVAL_NUMBER(q, AV_OPT_TYPE_RATIONAL, AVRational) static int set_number(void *obj, const char *name, double num, int den, int64_t intnum, - int search_flags) + int search_flags, int require_type) { - void *dst, *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); + void *dst; + const AVOption *o; + int ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; + ret = opt_set_init(obj, name, search_flags, require_type, NULL, &o, &dst); + if (ret < 0) + return ret; - if ((o->flags & AV_OPT_FLAG_READONLY) || (o->type & AV_OPT_TYPE_FLAG_ARRAY)) - return AVERROR(EINVAL); - - dst = ((uint8_t *)target_obj) + o->offset; return write_number(obj, o, dst, num, den, intnum); } int av_opt_set_int(void *obj, const char *name, int64_t val, int search_flags) { - return set_number(obj, name, 1, 1, val, search_flags); + return set_number(obj, name, 1, 1, val, search_flags, 0); } int av_opt_set_double(void *obj, const char *name, double val, int search_flags) { - return set_number(obj, name, val, 1, 1, search_flags); + return set_number(obj, name, val, 1, 1, search_flags, 0); } int av_opt_set_q(void *obj, const char *name, AVRational val, int search_flags) { - return set_number(obj, name, val.num, val.den, 1, search_flags); + return set_number(obj, name, val.num, val.den, 1, search_flags, 0); } int av_opt_set_bin(void *obj, const char *name, const uint8_t *val, int len, int search_flags) { - void *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); uint8_t *ptr; uint8_t **dst; int *lendst; + int ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - - if (o->type != AV_OPT_TYPE_BINARY || o->flags & AV_OPT_FLAG_READONLY) - return AVERROR(EINVAL); + ret = opt_set_init(obj, name, search_flags, AV_OPT_TYPE_BINARY, + NULL, NULL, (void**)&dst); + if (ret < 0) + return ret; ptr = len ? av_malloc(len) : NULL; if (len && !ptr) return AVERROR(ENOMEM); - dst = (uint8_t **)(((uint8_t *)target_obj) + o->offset); lendst = (int *)(dst + 1); av_free(*dst); @@ -844,59 +913,42 @@ int av_opt_set_bin(void *obj, const char *name, const uint8_t *val, int len, int int av_opt_set_image_size(void *obj, const char *name, int w, int h, int search_flags) { - void *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); + const AVOption *o; + int *dst; + int ret; + + ret = opt_set_init(obj, name, search_flags, AV_OPT_TYPE_IMAGE_SIZE, + NULL, &o, (void**)&dst); + if (ret < 0) + return ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - if (o->type != AV_OPT_TYPE_IMAGE_SIZE) { - av_log(obj, AV_LOG_ERROR, - "The value set by option '%s' is not an image size.\n", o->name); - return AVERROR(EINVAL); - } if (w<0 || h<0) { av_log(obj, AV_LOG_ERROR, "Invalid negative size value %dx%d for size '%s'\n", w, h, o->name); return AVERROR(EINVAL); } - *(int *)(((uint8_t *)target_obj) + o->offset) = w; - *(int *)(((uint8_t *)target_obj+sizeof(int)) + o->offset) = h; + + dst[0] = w; + dst[1] = h; + return 0; } int av_opt_set_video_rate(void *obj, const char *name, AVRational val, int search_flags) { - void *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); - - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - if (o->type != AV_OPT_TYPE_VIDEO_RATE) { - av_log(obj, AV_LOG_ERROR, - "The value set by option '%s' is not a video rate.\n", - o->name); - return AVERROR(EINVAL); - } - if (val.num <= 0 || val.den <= 0) - return AVERROR(EINVAL); - return set_number(obj, name, val.num, val.den, 1, search_flags); + return set_number(obj, name, val.num, val.den, 1, search_flags, AV_OPT_TYPE_VIDEO_RATE); } static int set_format(void *obj, const char *name, int fmt, int search_flags, enum AVOptionType type, const char *desc, int nb_fmts) { - void *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, - search_flags, &target_obj); - int min, max; + const AVOption *o; + int *dst; + int min, max, ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - if (o->type != type) { - av_log(obj, AV_LOG_ERROR, - "The value set by option '%s' is not a %s format", name, desc); - return AVERROR(EINVAL); - } + ret = opt_set_init(obj, name, search_flags, type, NULL, &o, (void**)&dst); + if (ret < 0) + return ret; min = FFMAX(o->min, -1); max = FFMIN(o->max, nb_fmts-1); @@ -907,7 +959,7 @@ static int set_format(void *obj, const char *name, int fmt, int search_flags, fmt, name, desc, min, max); return AVERROR(ERANGE); } - *(int *)(((uint8_t *)target_obj) + o->offset) = fmt; + *dst = fmt; return 0; } @@ -924,16 +976,14 @@ int av_opt_set_sample_fmt(void *obj, const char *name, enum AVSampleFormat fmt, int av_opt_set_dict_val(void *obj, const char *name, const AVDictionary *val, int search_flags) { - void *target_obj; AVDictionary **dst; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); + int ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - if (o->flags & AV_OPT_FLAG_READONLY) - return AVERROR(EINVAL); + ret = opt_set_init(obj, name, search_flags, AV_OPT_TYPE_DICT, NULL, NULL, + (void**)&dst); + if (ret < 0) + return ret; - dst = (AVDictionary **)(((uint8_t *)target_obj) + o->offset); av_dict_free(dst); return av_dict_copy(dst, val, 0); @@ -943,16 +993,13 @@ int av_opt_set_chlayout(void *obj, const char *name, const AVChannelLayout *channel_layout, int search_flags) { - void *target_obj; - const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); AVChannelLayout *dst; + int ret; - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; - if (o->flags & AV_OPT_FLAG_READONLY) - return AVERROR(EINVAL); - - dst = (AVChannelLayout*)((uint8_t*)target_obj + o->offset); + ret = opt_set_init(obj, name, search_flags, AV_OPT_TYPE_CHLAYOUT, NULL, NULL, + (void**)&dst); + if (ret < 0) + return ret; return av_channel_layout_copy(dst, channel_layout); } @@ -1441,36 +1488,15 @@ static char *get_opt_flags_string(void *obj, const char *unit, int64_t value) static void log_type(void *av_log_obj, const AVOption *o, enum AVOptionType parent_type) { - const char *desc[] = { - [AV_OPT_TYPE_FLAGS] = "", - [AV_OPT_TYPE_INT] = "", - [AV_OPT_TYPE_INT64] = "", - [AV_OPT_TYPE_UINT] = "", - [AV_OPT_TYPE_UINT64] = "", - [AV_OPT_TYPE_DOUBLE] = "", - [AV_OPT_TYPE_FLOAT] = "", - [AV_OPT_TYPE_STRING] = "", - [AV_OPT_TYPE_RATIONAL] = "", - [AV_OPT_TYPE_BINARY] = "", - [AV_OPT_TYPE_DICT] = "", - [AV_OPT_TYPE_IMAGE_SIZE] = "", - [AV_OPT_TYPE_VIDEO_RATE] = "", - [AV_OPT_TYPE_PIXEL_FMT] = "", - [AV_OPT_TYPE_SAMPLE_FMT] = "", - [AV_OPT_TYPE_DURATION] = "", - [AV_OPT_TYPE_COLOR] = "", - [AV_OPT_TYPE_CHLAYOUT] = "", - [AV_OPT_TYPE_BOOL] = "", - }; const enum AVOptionType type = TYPE_BASE(o->type); if (o->type == AV_OPT_TYPE_CONST && TYPE_BASE(parent_type) == AV_OPT_TYPE_INT) av_log(av_log_obj, AV_LOG_INFO, "%-12"PRId64" ", o->default_val.i64); - else if (type < FF_ARRAY_ELEMS(desc) && desc[type]) { + else if (type < FF_ARRAY_ELEMS(opt_type_desc) && opt_type_desc[type].name) { if (o->type & AV_OPT_TYPE_FLAG_ARRAY) - av_log(av_log_obj, AV_LOG_INFO, "[%-10s]", desc[type]); + av_log(av_log_obj, AV_LOG_INFO, "[%-10s]", opt_type_desc[type].name); else - av_log(av_log_obj, AV_LOG_INFO, "%-12s ", desc[type]); + av_log(av_log_obj, AV_LOG_INFO, "%-12s ", opt_type_desc[type].name); } else av_log(av_log_obj, AV_LOG_INFO, "%-12s ", ""); @@ -2068,7 +2094,7 @@ static int opt_copy_elem(void *logctx, enum AVOptionType type, if (dst != src) return av_channel_layout_copy(dst, src); } else if (opt_is_pod(type)) { - size_t size = opt_elem_size[type]; + size_t size = opt_type_desc[type].size; memcpy(dst, src, size); } else { av_log(logctx, AV_LOG_ERROR, "Unhandled option type: %d\n", type); @@ -2092,7 +2118,7 @@ static int opt_copy_array(void *logctx, const AVOption *o, opt_free_array(o, pdst, opt_array_pcount(pdst)); - dst = av_calloc(nb_elems, opt_elem_size[TYPE_BASE(o->type)]); + dst = av_calloc(nb_elems, opt_type_desc[TYPE_BASE(o->type)].size); if (!dst) return AVERROR(ENOMEM); @@ -2160,7 +2186,7 @@ int av_opt_get_array(void *obj, const char *name, int search_flags, unsigned int start_elem, unsigned int nb_elems, enum AVOptionType out_type, void *out_val) { - const size_t elem_size_out = opt_elem_size[TYPE_BASE(out_type)]; + const size_t elem_size_out = opt_type_desc[TYPE_BASE(out_type)].size; const AVOption *o; void *target_obj; @@ -2248,7 +2274,7 @@ int av_opt_set_array(void *obj, const char *name, int search_flags, unsigned int start_elem, unsigned int nb_elems, enum AVOptionType val_type, const void *val) { - const size_t elem_size_val = opt_elem_size[TYPE_BASE(val_type)]; + const size_t elem_size_val = opt_type_desc[TYPE_BASE(val_type)].size; const AVOption *o; const AVOptionArrayDef *arr; @@ -2261,17 +2287,17 @@ int av_opt_set_array(void *obj, const char *name, int search_flags, int ret = 0; - o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj); - if (!o || !target_obj) - return AVERROR_OPTION_NOT_FOUND; + ret = opt_set_init(obj, name, search_flags, 0, &target_obj, &o, &parray); + if (ret < 0) + return ret; + if (!(o->type & AV_OPT_TYPE_FLAG_ARRAY) || (val_type & AV_OPT_TYPE_FLAG_ARRAY)) return AVERROR(EINVAL); arr = o->default_val.arr; - parray = (uint8_t *)target_obj + o->offset; array_size = opt_array_pcount(parray); - elem_size = opt_elem_size[TYPE_BASE(o->type)]; + elem_size = opt_type_desc[TYPE_BASE(o->type)].size; if (start_elem > *array_size) return AVERROR(EINVAL); diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.h index be189f7653..17374211a4 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/opt.h @@ -886,6 +886,7 @@ int av_opt_set_chlayout(void *obj, const char *name, const AVChannelLayout *layo */ int av_opt_set_dict_val(void *obj, const char *name, const AVDictionary *val, int search_flags); +#if FF_API_OPT_INT_LIST /** * Set a binary option to an integer list. * @@ -901,6 +902,7 @@ int av_opt_set_dict_val(void *obj, const char *name, const AVDictionary *val, in AVERROR(EINVAL) : \ av_opt_set_bin(obj, name, (const uint8_t *)(val), \ av_int_list_length(val, term) * sizeof(*(val)), flags)) +#endif /** * Add, replace, or remove elements for an array option. Which of these diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/.gitignore new file mode 100644 index 0000000000..951cafb2d7 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/.gitignore @@ -0,0 +1,54 @@ +/adler32 +/aes +/aes_ctr +/atomic +/audio_fifo +/avstring +/base64 +/blowfish +/bprint +/camellia +/cast5 +/channel_layout +/color_utils +/cpu +/cpu_init +/crc +/des +/dict +/display +/error +/encryption_info +/eval +/fifo +/file +/hash +/hmac +/hwdevice +/imgutils +/integer +/lfg +/lls +/log +/lzo +/md5 +/murmur3 +/opt +/parseutils +/pca +/pixdesc +/pixelutils +/pixfmt_best +/random_seed +/rational +/ripemd +/sha +/sha512 +/side_data_array +/softfloat +/tea +/tree +/twofish +/utf8 +/uuid +/xtea diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/cpu.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/cpu.c index 53e9f99950..9e45527408 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/cpu.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/tests/cpu.c @@ -23,6 +23,10 @@ #include "libavutil/cpu.h" #include "libavutil/avstring.h" +#if ARCH_AARCH64 +#include "libavutil/aarch64/cpu.h" +#endif + #if HAVE_UNISTD_H #include #endif @@ -40,6 +44,8 @@ static const struct { { AV_CPU_FLAG_VFP, "vfp" }, { AV_CPU_FLAG_DOTPROD, "dotprod" }, { AV_CPU_FLAG_I8MM, "i8mm" }, + { AV_CPU_FLAG_SVE, "sve" }, + { AV_CPU_FLAG_SVE2, "sve2" }, #elif ARCH_ARM { AV_CPU_FLAG_ARMV5TE, "armv5te" }, { AV_CPU_FLAG_ARMV6, "armv6" }, @@ -161,6 +167,10 @@ int main(int argc, char **argv) print_cpu_flags(cpu_flags_raw, "raw"); print_cpu_flags(cpu_flags_eff, "effective"); printf("threads = %s (cpu_count = %d)\n", threads, cpu_count); +#if ARCH_AARCH64 && HAVE_SVE + if (cpu_flags_raw & AV_CPU_FLAG_SVE) + printf("sve_vector_length = %d\n", 8 * ff_aarch64_sve_length()); +#endif return 0; } diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/utils.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/utils.c index 94d247bbee..162a4dee26 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/utils.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/utils.c @@ -51,6 +51,7 @@ char av_get_picture_type_char(enum AVPictureType pict_type) } } +#if FF_API_OPT_INT_LIST unsigned av_int_list_length_for_size(unsigned elsize, const void *list, uint64_t term) { @@ -69,6 +70,7 @@ unsigned av_int_list_length_for_size(unsigned elsize, } return i; } +#endif char *av_fourcc_make_string(char *buf, uint32_t fourcc) { diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/version.h index 753f85b91c..18f1d00145 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libavutil/version.h @@ -79,7 +79,7 @@ */ #define LIBAVUTIL_VERSION_MAJOR 59 -#define LIBAVUTIL_VERSION_MINOR 39 +#define LIBAVUTIL_VERSION_MINOR 41 #define LIBAVUTIL_VERSION_MICRO 100 #define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ @@ -115,6 +115,7 @@ #define FF_API_MOD_UINTP2 (LIBAVUTIL_VERSION_MAJOR < 60) #define FF_API_RISCV_FD_ZBA (LIBAVUTIL_VERSION_MAJOR < 60) #define FF_API_VULKAN_FIXED_QUEUES (LIBAVUTIL_VERSION_MAJOR < 60) +#define FF_API_OPT_INT_LIST (LIBAVUTIL_VERSION_MAJOR < 60) /** * @} diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libpostproc/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libpostproc/version.h index e77dff98c5..4459d251d4 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libpostproc/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libpostproc/version.h @@ -30,7 +30,7 @@ #include "version_major.h" -#define LIBPOSTPROC_VERSION_MINOR 3 +#define LIBPOSTPROC_VERSION_MINOR 4 #define LIBPOSTPROC_VERSION_MICRO 100 #define LIBPOSTPROC_VERSION_INT AV_VERSION_INT(LIBPOSTPROC_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/tests/.gitignore new file mode 100644 index 0000000000..2dc986bd0e --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/tests/.gitignore @@ -0,0 +1 @@ +/swresample diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/version.h index d1795b5545..9a514e6d6f 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswresample/version.h @@ -30,7 +30,7 @@ #include "version_major.h" -#define LIBSWRESAMPLE_VERSION_MINOR 3 +#define LIBSWRESAMPLE_VERSION_MINOR 4 #define LIBSWRESAMPLE_VERSION_MICRO 100 #define LIBSWRESAMPLE_VERSION_INT AV_VERSION_INT(LIBSWRESAMPLE_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/tests/.gitignore new file mode 100644 index 0000000000..c56abf0ee7 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/tests/.gitignore @@ -0,0 +1,4 @@ +/colorspace +/floatimg_cmp +/pixdesc_query +/swscale diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/version.h b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/version.h index 51eb013a29..12412bd538 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/version.h +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/libswscale/version.h @@ -28,7 +28,7 @@ #include "version_major.h" -#define LIBSWSCALE_VERSION_MINOR 3 +#define LIBSWSCALE_VERSION_MINOR 4 #define LIBSWSCALE_VERSION_MICRO 100 #define LIBSWSCALE_VERSION_INT AV_VERSION_INT(LIBSWSCALE_VERSION_MAJOR, \ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/.gitignore new file mode 100644 index 0000000000..4766e56945 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/.gitignore @@ -0,0 +1,11 @@ +/audiogen +/audiomatch +/base64 +/data/ +/pixfmts.mak +/rotozoom +/test_copy.ffmeta +/tiny_psnr +/tiny_ssim +/videogen +/vsynth1/ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/api/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/api/.gitignore new file mode 100644 index 0000000000..bbab80ca73 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/api/.gitignore @@ -0,0 +1 @@ +/*-test diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/.gitignore new file mode 100644 index 0000000000..9ee0e5cfc1 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/.gitignore @@ -0,0 +1 @@ +/checkasm diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/checkasm.c b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/checkasm.c index 73a998ae3a..c9d2b5faf1 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/checkasm.c +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/checkasm/checkasm.c @@ -94,6 +94,10 @@ #define isatty(fd) 1 #endif +#if ARCH_AARCH64 +#include "libavutil/aarch64/cpu.h" +#endif + #if ARCH_ARM && HAVE_ARMV5TE_EXTERNAL #include "libavutil/arm/cpu.h" @@ -305,6 +309,8 @@ static const struct { { "NEON", "neon", AV_CPU_FLAG_NEON }, { "DOTPROD", "dotprod", AV_CPU_FLAG_DOTPROD }, { "I8MM", "i8mm", AV_CPU_FLAG_I8MM }, + { "SVE", "sve", AV_CPU_FLAG_SVE }, + { "SVE2", "sve2", AV_CPU_FLAG_SVE2 }, #elif ARCH_ARM { "ARMV5TE", "armv5te", AV_CPU_FLAG_ARMV5TE }, { "ARMV6", "armv6", AV_CPU_FLAG_ARMV6 }, @@ -915,6 +921,7 @@ int main(int argc, char *argv[]) { unsigned int seed = av_get_random_seed(); int i, ret = 0; + char arch_info_buf[50] = ""; #ifdef _WIN32 #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) @@ -979,7 +986,12 @@ int main(int argc, char *argv[]) } } - fprintf(stderr, "checkasm: using random seed %u\n", seed); +#if ARCH_AARCH64 && HAVE_SVE + if (have_sve(av_get_cpu_flags())) + snprintf(arch_info_buf, sizeof(arch_info_buf), + "SVE %d bits, ", 8 * ff_aarch64_sve_length()); +#endif + fprintf(stderr, "checkasm: %susing random seed %u\n", arch_info_buf, seed); av_lfg_init(&checkasm_lfg, seed); if (state.bench_pattern) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/filter-audio.mak b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/filter-audio.mak index eb8582ab31..84478740c7 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/filter-audio.mak +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/filter-audio.mak @@ -420,7 +420,7 @@ fate-filter-atempo: REF = $(SAMPLES)/filter-reference/atempo.pcm fate-filter-crazychannels: tests/data/filtergraphs/crazychannels fate-filter-crazychannels: CMD = framecrc -auto_conversion_filters -/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/crazychannels -FATE_AFILTER-$(call FILTERFRAMECRC, SINE JOIN ATRIM CHANNELMAP CHANNELSPLIT) += fate-filter-crazychannels +FATE_AFILTER-$(call FILTERFRAMECRC, ARESAMPLE SINE JOIN ATRIM CHANNELMAP CHANNELSPLIT) += fate-filter-crazychannels FATE_AFILTER-yes += fate-filter-formats fate-filter-formats: libavfilter/tests/formats$(EXESUF) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/hevc.mak b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/hevc.mak index 7f7ec43902..9e6fd72618 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/hevc.mak +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/hevc.mak @@ -284,7 +284,7 @@ $(TARGET_SAMPLES)/hevc/mv_nuh_layer_id.bit|$\ $(TARGET_SAMPLES)/hevc-conformance/NoOutPrior_B_Qualcomm_1.bit|$\ $(TARGET_SAMPLES)/hevc-conformance/MVHEVCS_A.bit fate-hevc-mv-switch: CMD = framecrc -i "concat:$(INPUT)" -fps_mode passthrough -map 0:vidx:0 -map 0:vidx:1 -sws_flags +accurate_rnd+bitexact -FATE_HEVC-$(call FRAMECRC, HEVC, HEVC, CONCAT_PROTOCOL) += fate-hevc-mv-switch +FATE_HEVC-$(call FRAMECRC, HEVC, HEVC, SCALE_FILTER CONCAT_PROTOCOL) += fate-hevc-mv-switch # multiview stream, select view by position # (depends on Three Dimensional Reference Displays Information SEI) diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/vcodec.mak b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/vcodec.mak index 463e9e34e7..17648aef59 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/vcodec.mak +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/fate/vcodec.mak @@ -157,8 +157,9 @@ $(FATE_VCODEC_DV:%=fate-vsynth\%-%): DECOPTS += $(DEFAULT_SIZE) FATE_VCODEC-$(call ENCDEC, FFV1, AVI) += ffv1 ffv1-v0 ffv1-v2 \ ffv1-v3-yuv420p \ ffv1-2pass + FATE_VCODEC_SCALE-$(call ENCDEC, FFV1, AVI) += ffv1-v3-yuv422p10 ffv1-v3-yuv444p16 \ - ffv1-v3-bgr0 ffv1-v3-rgb48 + ffv1-v3-bgr0 ffv1-v3-rgb48 ffv1-2pass10 fate-vsynth%-ffv1: ENCOPTS = -slices 4 fate-vsynth%-ffv1-v0: CODEC = ffv1 fate-vsynth%-ffv1-v2: ENCOPTS = -level 2 -strict experimental @@ -177,6 +178,9 @@ fate-vsynth%-ffv1-v3-rgb48: ENCOPTS = -level 3 -pix_fmt rgb48 -strict -2 \ fate-vsynth%-ffv1-v3-rgb48: DECOPTS = -sws_flags neighbor+bitexact fate-vsynth%-ffv1-2pass: TWOPASS = 1 fate-vsynth%-ffv1-2pass: ENCOPTS = -coder range_tab -context 1 +fate-vsynth%-ffv1-2pass10: TWOPASS = 1 +fate-vsynth%-ffv1-2pass10: ENCOPTS = -coder range_tab -context 1 -pix_fmt yuv422p10 \ + -sws_flags neighbor+bitexact FATE_VCODEC-$(call ENCDEC, FFVHUFF, AVI) += ffvhuff FATE_VCODEC_SCALE-$(call ENCDEC, FFVHUFF, AVI) += ffvhuff444 ffvhuff420p12 ffvhuff422p10left ffvhuff444p16 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_compact b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_compact index 25632fd179..89069792b8 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_compact +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_compact @@ -26,7 +26,7 @@ packet|codec_type=video|stream_index=1|pts=6144|pts_time=0.120000|dts=6144|dts_t frame|media_type=video|stream_index=1|key_frame=1|pts=6144|pts_time=0.120000|pkt_dts=6144|pkt_dts_time=0.120000|best_effort_timestamp=6144|best_effort_timestamp_time=0.120000|duration=2048|duration_time=0.040000|pkt_pos=793142|pkt_size=230400|width=320|height=240|crop_top=0|crop_bottom=0|crop_left=0|crop_right=0|pix_fmt=rgb24|sample_aspect_ratio=1:1|pict_type=I|interlaced_frame=0|top_field_first=0|repeat_pict=0|color_range=unknown|color_space=unknown|color_primaries=unknown|color_transfer=unknown|chroma_location=unspecified packet|codec_type=video|stream_index=2|pts=6144|pts_time=0.120000|dts=6144|dts_time=0.120000|duration=2048|duration_time=0.040000|size=30000|pos=1023566|flags=K__ frame|media_type=video|stream_index=2|key_frame=1|pts=6144|pts_time=0.120000|pkt_dts=6144|pkt_dts_time=0.120000|best_effort_timestamp=6144|best_effort_timestamp_time=0.120000|duration=2048|duration_time=0.040000|pkt_pos=1023566|pkt_size=30000|width=100|height=100|crop_top=0|crop_bottom=0|crop_left=0|crop_right=0|pix_fmt=rgb24|sample_aspect_ratio=1:1|pict_type=I|interlaced_frame=0|top_field_first=0|repeat_pict=0|color_range=unknown|color_space=unknown|color_primaries=unknown|color_transfer=unknown|chroma_location=unspecified -stream|index=0|codec_name=pcm_s16le|profile=unknown|codec_type=audio|codec_tag_string=PSD[16]|codec_tag=0x10445350|sample_fmt=s16|sample_rate=44100|channels=1|channel_layout=unknown|bits_per_sample=16|initial_padding=0|id=N/A|r_frame_rate=0/0|avg_frame_rate=0/0|time_base=1/44100|start_pts=0|start_time=0.000000|duration_ts=N/A|duration=N/A|bit_rate=705600|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=6|nb_read_packets=6|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:E=mc²|tag:encoder=Lavc pcm_s16le -stream|index=1|codec_name=rawvideo|profile=unknown|codec_type=video|codec_tag_string=RGB[24]|codec_tag=0x18424752|width=320|height=240|coded_width=320|coded_height=240|closed_captions=0|film_grain=0|has_b_frames=0|sample_aspect_ratio=1:1|display_aspect_ratio=4:3|pix_fmt=rgb24|level=-99|color_range=unknown|color_space=unknown|color_transfer=unknown|color_primaries=unknown|chroma_location=unspecified|field_order=unknown|refs=1|id=N/A|r_frame_rate=25/1|avg_frame_rate=25/1|time_base=1/51200|start_pts=0|start_time=0.000000|duration_ts=N/A|duration=N/A|bit_rate=N/A|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=4|nb_read_packets=4|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:title=foobar|tag:duration_ts=field-and-tags-conflict-attempt|tag:encoder=Lavc rawvideo +stream|index=0|codec_name=pcm_s16le|profile=unknown|codec_type=audio|codec_tag_string=PSD[16]|codec_tag=0x10445350|sample_fmt=s16|sample_rate=44100|channels=1|channel_layout=unknown|bits_per_sample=16|initial_padding=0|id=N/A|r_frame_rate=0/0|avg_frame_rate=0/0|time_base=1/44100|start_pts=0|start_time=0.000000|duration_ts=N/A|duration=N/A|bit_rate=705600|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=6|nb_read_packets=6|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:encoder=Lavc pcm_s16le|tag:E=mc² +stream|index=1|codec_name=rawvideo|profile=unknown|codec_type=video|codec_tag_string=RGB[24]|codec_tag=0x18424752|width=320|height=240|coded_width=320|coded_height=240|closed_captions=0|film_grain=0|has_b_frames=0|sample_aspect_ratio=1:1|display_aspect_ratio=4:3|pix_fmt=rgb24|level=-99|color_range=unknown|color_space=unknown|color_transfer=unknown|color_primaries=unknown|chroma_location=unspecified|field_order=unknown|refs=1|id=N/A|r_frame_rate=25/1|avg_frame_rate=25/1|time_base=1/51200|start_pts=0|start_time=0.000000|duration_ts=N/A|duration=N/A|bit_rate=N/A|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=4|nb_read_packets=4|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:encoder=Lavc rawvideo|tag:title=foobar|tag:duration_ts=field-and-tags-conflict-attempt stream|index=2|codec_name=rawvideo|profile=unknown|codec_type=video|codec_tag_string=RGB[24]|codec_tag=0x18424752|width=100|height=100|coded_width=100|coded_height=100|closed_captions=0|film_grain=0|has_b_frames=0|sample_aspect_ratio=1:1|display_aspect_ratio=1:1|pix_fmt=rgb24|level=-99|color_range=unknown|color_space=unknown|color_transfer=unknown|color_primaries=unknown|chroma_location=unspecified|field_order=unknown|refs=1|id=N/A|r_frame_rate=25/1|avg_frame_rate=25/1|time_base=1/51200|start_pts=0|start_time=0.000000|duration_ts=N/A|duration=N/A|bit_rate=N/A|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=4|nb_read_packets=4|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:encoder=Lavc rawvideo format|filename=tests/data/ffprobe-test.nut|nb_streams=3|nb_programs=0|nb_stream_groups=0|format_name=nut|start_time=0.000000|duration=0.120000|size=1053646|bit_rate=70243066|probe_score=100|tag:title=ffprobe test file|tag:comment='A comment with CSV, XML & JSON special chars': |tag:comment2=I ♥ Üñîçød€ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_csv b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_csv index defe72d4c4..8eb267fe22 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_csv +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_csv @@ -26,7 +26,7 @@ packet,video,1,6144,0.120000,6144,0.120000,2048,0.040000,230400,793142,K__ frame,video,1,1,6144,0.120000,6144,0.120000,6144,0.120000,2048,0.040000,793142,230400,320,240,0,0,0,0,rgb24,1:1,I,0,0,0,unknown,unknown,unknown,unknown,unspecified packet,video,2,6144,0.120000,6144,0.120000,2048,0.040000,30000,1023566,K__ frame,video,2,1,6144,0.120000,6144,0.120000,6144,0.120000,2048,0.040000,1023566,30000,100,100,0,0,0,0,rgb24,1:1,I,0,0,0,unknown,unknown,unknown,unknown,unspecified -stream,0,pcm_s16le,unknown,audio,PSD[16],0x10445350,s16,44100,1,unknown,16,0,N/A,0/0,0/0,1/44100,0,0.000000,N/A,N/A,705600,N/A,N/A,N/A,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,mc²,Lavc pcm_s16le -stream,1,rawvideo,unknown,video,RGB[24],0x18424752,320,240,320,240,0,0,0,1:1,4:3,rgb24,-99,unknown,unknown,unknown,unknown,unspecified,unknown,1,N/A,25/1,25/1,1/51200,0,0.000000,N/A,N/A,N/A,N/A,N/A,N/A,4,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,foobar,field-and-tags-conflict-attempt,Lavc rawvideo +stream,0,pcm_s16le,unknown,audio,PSD[16],0x10445350,s16,44100,1,unknown,16,0,N/A,0/0,0/0,1/44100,0,0.000000,N/A,N/A,705600,N/A,N/A,N/A,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,Lavc pcm_s16le,mc² +stream,1,rawvideo,unknown,video,RGB[24],0x18424752,320,240,320,240,0,0,0,1:1,4:3,rgb24,-99,unknown,unknown,unknown,unknown,unspecified,unknown,1,N/A,25/1,25/1,1/51200,0,0.000000,N/A,N/A,N/A,N/A,N/A,N/A,4,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,Lavc rawvideo,foobar,field-and-tags-conflict-attempt stream,2,rawvideo,unknown,video,RGB[24],0x18424752,100,100,100,100,0,0,0,1:1,1:1,rgb24,-99,unknown,unknown,unknown,unknown,unspecified,unknown,1,N/A,25/1,25/1,1/51200,0,0.000000,N/A,N/A,N/A,N/A,N/A,N/A,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,Lavc rawvideo format,tests/data/ffprobe-test.nut,3,0,0,nut,0.000000,0.120000,1053646,70243066,100,ffprobe test file,"'A comment with CSV, XML & JSON special chars': ",I ♥ Üñîçød€ diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_default b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_default index ac55749295..6596a06b0c 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_default +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_default @@ -596,8 +596,8 @@ DISPOSITION:metadata=0 DISPOSITION:dependent=0 DISPOSITION:still_image=0 DISPOSITION:multilayer=0 -TAG:E=mc² TAG:encoder=Lavc pcm_s16le +TAG:E=mc² [/STREAM] [STREAM] index=1 @@ -657,9 +657,9 @@ DISPOSITION:metadata=0 DISPOSITION:dependent=0 DISPOSITION:still_image=0 DISPOSITION:multilayer=0 +TAG:encoder=Lavc rawvideo TAG:title=foobar TAG:duration_ts=field-and-tags-conflict-attempt -TAG:encoder=Lavc rawvideo [/STREAM] [STREAM] index=2 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_flat b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_flat index e1aefe6afb..110b06e0ea 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_flat +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_flat @@ -539,8 +539,8 @@ streams.stream.0.disposition.metadata=0 streams.stream.0.disposition.dependent=0 streams.stream.0.disposition.still_image=0 streams.stream.0.disposition.multilayer=0 -streams.stream.0.tags.E="mc²" streams.stream.0.tags.encoder="Lavc pcm_s16le" +streams.stream.0.tags.E="mc²" streams.stream.1.index=1 streams.stream.1.codec_name="rawvideo" streams.stream.1.profile="unknown" @@ -598,9 +598,9 @@ streams.stream.1.disposition.metadata=0 streams.stream.1.disposition.dependent=0 streams.stream.1.disposition.still_image=0 streams.stream.1.disposition.multilayer=0 +streams.stream.1.tags.encoder="Lavc rawvideo" streams.stream.1.tags.title="foobar" streams.stream.1.tags.duration_ts="field-and-tags-conflict-attempt" -streams.stream.1.tags.encoder="Lavc rawvideo" streams.stream.2.index=2 streams.stream.2.codec_name="rawvideo" streams.stream.2.profile="unknown" diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_ini b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_ini index 4d046539d6..9ab4df9ddb 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_ini +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_ini @@ -602,8 +602,8 @@ still_image=0 multilayer=0 [streams.stream.0.tags] -E=mc² encoder=Lavc pcm_s16le +E=mc² [streams.stream.1] index=1 @@ -667,9 +667,9 @@ still_image=0 multilayer=0 [streams.stream.1.tags] +encoder=Lavc rawvideo title=foobar duration_ts=field-and-tags-conflict-attempt -encoder=Lavc rawvideo [streams.stream.2] index=2 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_json b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_json index 5583063eb4..9d497517fe 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_json +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_json @@ -577,8 +577,8 @@ "multilayer": 0 }, "tags": { - "E": "mc²", - "encoder": "Lavc pcm_s16le" + "encoder": "Lavc pcm_s16le", + "E": "mc²" } }, { @@ -628,9 +628,9 @@ "multilayer": 0 }, "tags": { + "encoder": "Lavc rawvideo", "title": "foobar", - "duration_ts": "field-and-tags-conflict-attempt", - "encoder": "Lavc rawvideo" + "duration_ts": "field-and-tags-conflict-attempt" } }, { diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xml b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xml index 3b41121902..c0d41e6f20 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xml +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xml @@ -35,16 +35,16 @@ - + + - diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xsd b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xsd index d5eb0a3a46..887f3ea1c6 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xsd +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/ffprobe_xsd @@ -35,16 +35,16 @@ - + + - diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-encoding-delay b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-encoding-delay index b828dec2e6..cfa1675b94 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-encoding-delay +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-encoding-delay @@ -1,4 +1,4 @@ -604531e6d08cb35b2f3eec838ac45b80 *tests/data/fate/matroska-encoding-delay.matroska +ece637ed28526724c88f5c69b8bf7d6f *tests/data/fate/matroska-encoding-delay.matroska 961252 tests/data/fate/matroska-encoding-delay.matroska #extradata 0: 22, 0x32ea0490 #tb 0: 1/1000 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-mastering-display-metadata b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-mastering-display-metadata index 53f84c1793..91ce6a0558 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-mastering-display-metadata +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/matroska-mastering-display-metadata @@ -1,4 +1,4 @@ -9d0fb8123a2e90e85153428a91d1ee9d *tests/data/fate/matroska-mastering-display-metadata.matroska +ad5e3c4e338599c81ef7d0f9ae25f871 *tests/data/fate/matroska-mastering-display-metadata.matroska 1669589 tests/data/fate/matroska-mastering-display-metadata.matroska #extradata 0: 4, 0x040901a3 #extradata 3: 200, 0x506463a8 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/rgb24-mkv b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/rgb24-mkv index 1cbed136dd..5f4cb78318 100644 --- a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/rgb24-mkv +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/fate/rgb24-mkv @@ -1,4 +1,4 @@ -7d767e8238c674ecfa80458cb281c09e *tests/data/fate/rgb24-mkv.matroska +0013b260aa50b70d1a00d9770581a177 *tests/data/fate/rgb24-mkv.matroska 58236 tests/data/fate/rgb24-mkv.matroska #tb 0: 1/10 #media_type 0: video diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth1-ffv1-2pass10 b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth1-ffv1-2pass10 new file mode 100644 index 0000000000..1436cde80d --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth1-ffv1-2pass10 @@ -0,0 +1,4 @@ +1093853767c7eff69cb235895de52c1e *tests/data/fate/vsynth1-ffv1-2pass10.avi +2563110 tests/data/fate/vsynth1-ffv1-2pass10.avi +4c9d3ec84a39f77860f92538c8e542ad *tests/data/fate/vsynth1-ffv1-2pass10.out.rawvideo +stddev: 1.29 PSNR: 45.90 MAXDIFF: 21 bytes: 7603200/ 7603200 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth2-ffv1-2pass10 b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth2-ffv1-2pass10 new file mode 100644 index 0000000000..9b47dc361d --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth2-ffv1-2pass10 @@ -0,0 +1,4 @@ +a0604ec563bcbf4c1e27fc2e779e4d80 *tests/data/fate/vsynth2-ffv1-2pass10.avi +3858802 tests/data/fate/vsynth2-ffv1-2pass10.avi +3d4c55f943e6cd8eeac5954d71f43a5d *tests/data/fate/vsynth2-ffv1-2pass10.out.rawvideo +stddev: 0.31 PSNR: 58.28 MAXDIFF: 6 bytes: 7603200/ 7603200 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth3-ffv1-2pass10 b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth3-ffv1-2pass10 new file mode 100644 index 0000000000..5cc7fb8673 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth3-ffv1-2pass10 @@ -0,0 +1,4 @@ +f3e4a564f649e6494d1d75663be3234b *tests/data/fate/vsynth3-ffv1-2pass10.avi +57880 tests/data/fate/vsynth3-ffv1-2pass10.avi +2a99d28da942139ac8d83e44b9847347 *tests/data/fate/vsynth3-ffv1-2pass10.out.rawvideo +stddev: 1.47 PSNR: 44.75 MAXDIFF: 18 bytes: 86700/ 86700 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth_lena-ffv1-2pass10 b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth_lena-ffv1-2pass10 new file mode 100644 index 0000000000..9a5b952336 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tests/ref/vsynth/vsynth_lena-ffv1-2pass10 @@ -0,0 +1,4 @@ +602ab4d53e2c56b77062b3720d483f3b *tests/data/fate/vsynth_lena-ffv1-2pass10.avi +3775066 tests/data/fate/vsynth_lena-ffv1-2pass10.avi +e692506c522fa6af70da9eab65cbfe49 *tests/data/fate/vsynth_lena-ffv1-2pass10.out.rawvideo +stddev: 0.26 PSNR: 59.53 MAXDIFF: 4 bytes: 7603200/ 7603200 diff --git a/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tools/.gitignore b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tools/.gitignore new file mode 100644 index 0000000000..7c45896923 --- /dev/null +++ b/submodules/ffmpeg/Sources/FFMpeg/ffmpeg-7.1/tools/.gitignore @@ -0,0 +1,22 @@ +/aviocat +/ffbisect +/bisect.need +/crypto_bench +/cws2fws +/enum_options +/fourcc2pixfmt +/ffescape +/ffeval +/ffhash +/graph2dot +/ismindex +/pktdumper +/probetest +/qt-faststart +/scale_slice_test +/sidxindex +/trasher +/seek_print +/uncoded_frame +/venc_data_dump +/zmqsend diff --git a/versions.json b/versions.json index 1d9db2c8d4..77784a5a49 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.5.3", + "app": "11.6", "xcode": "16.0", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15.0"