Video player improvements

This commit is contained in:
Isaac
2024-10-21 20:21:15 +04:00
parent a38e1cf06e
commit fa2da2832e
24 changed files with 294 additions and 888 deletions

View File

@@ -513,6 +513,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
public var updateMessageReaction: ((Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void)?
public var playMessageEffect: ((Message) -> Void)?
public var activateAgeRestrictedMedia: (() -> Void)?
public var requestInlineUpdate: (() -> Void)?
private var hlsInlinePlaybackRange: Range<Int64>?
private var appliedHlsInlinePlaybackRange: Range<Int64>?
private var hlsInlinePlaybackRangeDisposable: Disposable?
override public init() {
self.pinchContainerNode = PinchSourceContainerNode()
@@ -618,6 +623,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
self.playerStatusDisposable.dispose()
self.fetchDisposable.dispose()
self.secretTimer?.invalidate()
self.hlsInlinePlaybackRangeDisposable?.dispose()
}
public func isAvailableForGalleryTransition() -> Bool {
@@ -779,6 +785,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
let currentAutomaticDownload = self.automaticDownload
let currentAutomaticPlayback = self.automaticPlayback
let hlsInlinePlaybackRange = self.hlsInlinePlaybackRange
let appliedHlsInlinePlaybackRange = self.appliedHlsInlinePlaybackRange
return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
let _ = peerType
@@ -1085,6 +1094,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
} else {
mediaUpdated = true
}
if hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange {
mediaUpdated = true
}
var isSendingUpdated = false
if let currentMessage = currentMessage {
@@ -1107,6 +1119,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
var updateAnimatedStickerFile: TelegramMediaFile?
var onlyFullSizeVideoThumbnail: Bool?
var loadHLSRangeVideoFile: TelegramMediaFile?
var emptyColor: UIColor
var patternArguments: PatternWallpaperArguments?
if isSticker {
@@ -1216,7 +1230,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: resource)
}
})
} else if let file = media as? TelegramMediaFile {
} else if var file = media as? TelegramMediaFile {
if isSecretMedia {
updateImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file))
@@ -1248,20 +1262,34 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading {
updateVideoFile = file
if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile {
if currentFile.resource is EmptyMediaResource {
replaceVideoNode = true
} else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId {
replaceVideoNode = true
} else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile {
replaceVideoNode = true
} else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile {
replaceVideoNode = true
}
loadHLSRangeVideoFile = file
var passFile = true
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
file = minimizedQualityFile.file.media
if hlsInlinePlaybackRange == nil {
passFile = false
}
} else if !(file.resource is LocalFileVideoMediaResource) {
}
if passFile {
updateVideoFile = file
if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile {
if currentFile.resource is EmptyMediaResource {
replaceVideoNode = true
} else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId {
replaceVideoNode = true
} else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile {
replaceVideoNode = true
} else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile {
replaceVideoNode = true
}
}
} else if !(file.resource is LocalFileVideoMediaResource) {
replaceVideoNode = true
}
} else if hasCurrentVideoNode {
replaceVideoNode = true
}
} else {
@@ -1352,7 +1380,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}, cancel: {
chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image)
})
} else if let file = media as? TelegramMediaFile {
} else if var file = media as? TelegramMediaFile {
if isSecretMedia {
updateImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file))
@@ -1384,20 +1412,34 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading {
updateVideoFile = file
if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile {
if currentFile.resource is EmptyMediaResource {
replaceVideoNode = true
} else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId {
replaceVideoNode = true
} else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile {
replaceVideoNode = true
} else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile {
replaceVideoNode = true
}
loadHLSRangeVideoFile = file
var passFile = true
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
file = minimizedQualityFile.file.media
if hlsInlinePlaybackRange == nil {
passFile = false
}
} else if !(file.resource is LocalFileVideoMediaResource) {
}
if passFile {
updateVideoFile = file
if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile {
if currentFile.resource is EmptyMediaResource {
replaceVideoNode = true
} else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId {
replaceVideoNode = true
} else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile {
replaceVideoNode = true
} else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile {
replaceVideoNode = true
}
}
} else if !(file.resource is LocalFileVideoMediaResource) {
replaceVideoNode = true
}
} else if hasCurrentVideoNode {
replaceVideoNode = true
}
} else {
@@ -1661,12 +1703,26 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
let loopVideo = updatedVideoFile.isAnimated
let videoContent: UniversalVideoContent
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
guard let context, let peerId else {
return
videoContent = NativeVideoContent(
id: .message(message.stableId, updatedVideoFile.fileId),
userLocation: .peer(message.id.peerId),
fileReference: .message(message: MessageReference(message), media: updatedVideoFile),
limitedFileRange: hlsInlinePlaybackRange,
streamVideo: streamVideo ? .conservative : .none,
loopVideo: loopVideo,
enableSound: false,
fetchAutomatically: false,
onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false),
continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo,
placeholderColor: emptyColor,
captureProtected: message.isCopyProtected() || isExtendedMedia,
storeAfterDownload: { [weak context] in
guard let context, let peerId else {
return
}
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
}
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
})
)
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
@@ -1916,6 +1972,36 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.updateStatus(animated: synchronousLoads)
strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia && !isExtendedMediaPreview && !hasSpoiler
strongSelf.appliedHlsInlinePlaybackRange = hlsInlinePlaybackRange
if let loadHLSRangeVideoFile, NativeVideoContent.isHLSVideo(file: loadHLSRangeVideoFile) {
if strongSelf.hlsInlinePlaybackRangeDisposable == nil {
strongSelf.hlsInlinePlaybackRangeDisposable = (HLSVideoContent.minimizedHLSQualityPreloadData(
postbox: context.account.postbox,
file: .message(message: MessageReference(message), media: loadHLSRangeVideoFile),
userLocation: .peer(message.id.peerId),
prefixSeconds: 10,
autofetchPlaylist: false
)
|> deliverOnMainQueue).startStrict(next: { [weak strongSelf] preloadData in
guard let strongSelf else {
return
}
if let preloadData {
strongSelf.hlsInlinePlaybackRange = preloadData.1
} else {
strongSelf.hlsInlinePlaybackRange = nil
}
strongSelf.requestInlineUpdate?()
})
}
} else {
if let hlsInlinePlaybackRangeDisposable = strongSelf.hlsInlinePlaybackRangeDisposable {
strongSelf.hlsInlinePlaybackRangeDisposable = nil
hlsInlinePlaybackRangeDisposable.dispose()
}
}
}
})
})
@@ -2140,6 +2226,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
}
if let file = self.media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) {
fetchStatus = .Local
}
let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator)
@@ -2185,15 +2275,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if let duration = file.duration, !message.flags.contains(.Unsent) {
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : Int32(duration), position: playerPosition)
if isMediaStreamable(message: message, media: file) {
if NativeVideoContent.isHLSVideo(file: file) {
mediaDownloadState = .fetching(progress: nil)
badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil)
} else {
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active)
mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress)
if self.playerStatus?.status == .playing {
mediaDownloadState = nil
}
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active)
mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress)
if self.playerStatus?.status == .playing {
mediaDownloadState = nil
}
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
} else {
@@ -2292,11 +2377,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
do {
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration.flatMap { Int32(floor($0)) } ?? 0), position: playerPosition)
if wideLayout {
if NativeVideoContent.isHLSVideo(file: file) {
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
mediaDownloadState = nil
badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil)
} else if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max {
if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max {
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(fileSize, formatting: formatting), muted: muted, active: true)
mediaDownloadState = .remote
@@ -2680,8 +2761,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
var isAnimated = false
if let file = self.media as? TelegramMediaFile, file.isAnimated {
isAnimated = true
if let file = self.media as? TelegramMediaFile {
if NativeVideoContent.isHLSVideo(file: file) {
return nil
}
if file.isAnimated {
isAnimated = true
}
}
var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound