HLS video improvements

This commit is contained in:
Isaac 2024-10-05 20:14:59 +04:00
parent 9f11ee1c5a
commit 9e1dc11997
13 changed files with 395 additions and 29 deletions

View File

@ -1405,8 +1405,15 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
}
self.galleryNode.completeCustomDismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in
if isPictureInPicture {
if let chatController = self?.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
}
} else {
self?._hiddenMedia.set(.single(nil))
}
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}

View File

@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
public var pager: GalleryPagerNode
public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { }
public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
@ -123,9 +123,9 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
}
}
self.pager.completeCustomDismiss = { [weak self] in
self.pager.completeCustomDismiss = { [weak self] isPictureInPicture in
if let strongSelf = self {
strongSelf.completeCustomDismiss()
strongSelf.completeCustomDismiss(isPictureInPicture)
}
}

View File

@ -25,7 +25,7 @@ open class GalleryItemNode: ASDisplayNode {
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { }
public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
public var alternativeDismiss: () -> Bool = { return false }

View File

@ -110,7 +110,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { }
public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }

View File

@ -687,7 +687,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)
@ -719,7 +719,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)

View File

@ -804,6 +804,245 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
}
}
@available(iOS 15.0, *)
private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate {
private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
private let node: UniversalVideoNode
private var statusDisposable: Disposable?
private var status: MediaPlayerStatus?
weak var pictureInPictureController: AVPictureInPictureController?
private var previousIsPlaying = false
init(node: UniversalVideoNode) {
self.node = node
super.init()
var invalidatedStateOnce = false
self.statusDisposable = (self.node.status
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let strongSelf = self else {
return
}
strongSelf.status = status
if let status {
let isPlaying = status.status == .playing
if !invalidatedStateOnce {
invalidatedStateOnce = true
strongSelf.pictureInPictureController?.invalidatePlaybackState()
} else if strongSelf.previousIsPlaying != isPlaying {
strongSelf.previousIsPlaying = isPlaying
strongSelf.pictureInPictureController?.invalidatePlaybackState()
}
}
}).strict()
}
deinit {
self.statusDisposable?.dispose()
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
self.node.togglePlayPause()
}
public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
guard let status = self.status else {
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)))
}
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration - status.timestamp, preferredTimescale: CMTimeScale(30.0)))
}
public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
guard let status = self.status else {
return false
}
switch status.status {
case .playing:
return false
case .buffering, .paused:
return true
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
let node = self.node
let _ = (self.node.status
|> take(1)
|> deliverOnMainQueue).start(next: { [weak node] status in
if let node = node, let timestamp = status?.timestamp, let duration = status?.duration {
let nextTimestamp = timestamp + skipInterval.seconds
if nextTimestamp > duration {
node.seek(0.0)
node.pause()
} else {
node.seek(min(duration, nextTimestamp))
}
}
completionHandler()
})
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
}
private let context: AccountContext
private let accountId: AccountRecordId
private let hiddenMedia: (MessageId, Media)?
private weak var mediaManager: MediaManager?
private var pictureInPictureController: AVPictureInPictureController?
private var contentDelegate: PlaybackDelegate?
private let node: UniversalVideoNode
private let willBegin: (NativePictureInPictureContentImpl) -> Void
private let didBegin: (NativePictureInPictureContentImpl) -> Void
private let expand: (@escaping () -> Void) -> Void
private var pictureInPictureTimer: SwiftSignalKit.Timer?
private var didExpand: Bool = false
private var hiddenMediaManagerIndex: Int?
private var messageRemovedDisposable: Disposable?
private var isNativePictureInPictureActiveDisposable: Disposable?
init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
self.context = context
self.mediaManager = mediaManager
self.accountId = accountId
self.hiddenMedia = hiddenMedia
self.node = videoNode
self.willBegin = willBegin
self.didBegin = didBegin
self.expand = expand
super.init()
if let videoLayer = videoNode.getVideoLayer() {
let contentDelegate = PlaybackDelegate(node: self.node)
self.contentDelegate = contentDelegate
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate))
self.pictureInPictureController = pictureInPictureController
contentDelegate.pictureInPictureController = pictureInPictureController
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
pictureInPictureController.requiresLinearPlayback = !canSkip
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
}
if let (messageId, _) = hiddenMedia {
self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> map { message -> Bool in
if let _ = message {
return false
} else {
return true
}
}
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.node.canAttachContent = false
})
}
}
deinit {
self.messageRemovedDisposable?.dispose()
self.isNativePictureInPictureActiveDisposable?.dispose()
self.pictureInPictureTimer?.invalidate()
self.node.setCanPlaybackWithoutHierarchy(false)
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager {
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
}
func updateIsCentral(isCentral: Bool) {
guard let pictureInPictureController = self.pictureInPictureController else {
return
}
if isCentral {
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
} else {
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
}
}
func beginPictureInPicture() {
guard let pictureInPictureController = self.pictureInPictureController else {
return
}
if pictureInPictureController.isPictureInPicturePossible {
pictureInPictureController.startPictureInPicture()
}
}
func invalidatePlaybackState() {
self.pictureInPictureController?.invalidatePlaybackState()
}
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.node.setCanPlaybackWithoutHierarchy(true)
if let hiddenMedia = self.hiddenMedia, let mediaManager = self.mediaManager {
let accountId = self.accountId
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia)
|> map { messageIdAndMedia in
if let (messageId, media) = messageIdAndMedia {
return .chat(accountId, messageId, media)
} else {
return nil
}
})
}
self.willBegin(self)
}
public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.didBegin(self)
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
print(error)
}
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.node.setCanPlaybackWithoutHierarchy(false)
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager {
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
self.hiddenMediaManagerIndex = nil
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
self.expand { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.didExpand = true
completionHandler(true)
}
}
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable?
private var automaticPictureInPictureDisposable: Disposable?
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
private var pictureInPictureContent: AnyObject?
private var nativePictureInPictureContent: AnyObject?
private var activePictureInPictureNavigationController: NavigationController?
private var activePictureInPictureController: ViewController?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
self.automaticPictureInPictureDisposable?.dispose()
}
override func ready() -> Signal<Void, NoError> {
@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.videoNode?.setBaseRate(playbackRate)
}
}
if strongSelf.nativePictureInPictureContent == nil {
strongSelf.setupNativePictureInPicture()
}
}
}
self.videoNode = videoNode
@ -1749,6 +1998,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
}
if #available(iOS 15.0, *) {
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
nativePictureInPictureContent.updateIsCentral(isCentral: isCentral)
}
}
}
}
@ -2330,13 +2585,79 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.beginCustomDismiss(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
self?.completeCustomDismiss(false)
})
}
}
}
private func setupNativePictureInPicture() {
guard let item = self.item, let videoNode = self.videoNode else {
return
}
var hiddenMedia: (MessageId, Media)? = nil
switch item.contentInfo {
case let .message(message, _):
for media in message.media {
if let media = media as? TelegramMediaImage {
hiddenMedia = (message.id, media)
} else if let media = media as? TelegramMediaFile, media.isVideo {
hiddenMedia = (message.id, media)
}
}
default:
break
}
if #available(iOS 15.0, *) {
let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in
guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else {
return
}
self.activePictureInPictureNavigationController = navigationController
self.activePictureInPictureController = controller
controller.view.alpha = 0.0
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.completeCustomDismiss(true)
})
}, didBegin: { [weak self] _ in
guard let self else {
return
}
let _ = self
}, expand: { [weak self] completion in
guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else {
completion()
return
}
self.activePictureInPictureController = nil
self.activePictureInPictureNavigationController = nil
activePictureInPictureController.presentationArguments = nil
activePictureInPictureNavigationController.currentWindow?.present(activePictureInPictureController, on: .root, blockInteraction: false, completion: {
})
activePictureInPictureController.view.alpha = 1.0
activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
completion()
})
})
self.nativePictureInPictureContent = content
}
}
@objc func pictureInPictureButtonPressed() {
if #available(iOS 15.0, *) {
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
nativePictureInPictureContent.beginPictureInPicture()
return
}
}
var isNativePictureInPictureSupported = false
switch self.item?.contentInfo {
case let .message(message, _):
@ -2391,7 +2712,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
guard let strongSelf = self else {
return
}
strongSelf.completeCustomDismiss()
strongSelf.completeCustomDismiss(false)
}, expand: { [weak baseNavigationController] completion in
guard let contentInfo = item.contentInfo else {
return
@ -2524,7 +2845,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.beginCustomDismiss(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
self?.completeCustomDismiss(false)
})
}
}
@ -2847,7 +3168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss()
strongSelf.completeCustomDismiss(false)
}
}
f(.default)
@ -2928,7 +3249,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)

View File

@ -300,7 +300,7 @@ public final class SecretMediaPreviewController: ViewController {
}
}
self.controllerNode.completeCustomDismiss = { [weak self] in
self.controllerNode.completeCustomDismiss = { [weak self] _ in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}

View File

@ -398,7 +398,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.galleryNode.completeCustomDismiss = { [weak self] in
self.galleryNode.completeCustomDismiss = { [weak self] _ in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}

View File

@ -160,7 +160,7 @@ final class VideoChatActionButtonComponent: Component {
case .unmuted:
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
case .raiseHand, .scheduled:
backgroundColor = UIColor(rgb: 0x3252EF)
backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF)
}
iconDiameter = 60.0
case let .video(isActive):
@ -282,6 +282,7 @@ final class VideoChatActionButtonComponent: Component {
}
self.isEnabled = isEnabled
self.isUserInteractionEnabled = isEnabled
return size
}

View File

@ -1022,7 +1022,7 @@ final class VideoChatScreenComponent: Component {
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
displayEvent = false
}
if members.totalCount < 250 {
if members.totalCount < 40 {
displayEvent = true
} else if event.peer.isVerified {
displayEvent = true

View File

@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
let effectiveMediaVisibility = self.visibility
var isPlaying = true
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if !item.controllerInteraction.canReadHistory {
isPlaying = false
}
if self.forceStopAnimations {
isPlaying = false
}
@ -6228,7 +6224,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
for contentNode in self.contentNodes {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
if contentNode is ChatMessageMediaBubbleContentNode {
contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
} else {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
}
}
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if let threadInfoNode = self.threadInfoNode {

View File

@ -25,9 +25,22 @@ public final class HLSQualitySet {
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
for attribute in alternativeFile.attributes {
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
let _ = size
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) {
qualityFiles[Int(size.height)] = baseFile.withMedia(alternativeFile)
let key = Int(size.height)
if let currentFile = qualityFiles[key] {
var currentCodec: String?
for attribute in currentFile.media.attributes {
if case let .Video(_, _, _, _, _, videoCodec) = attribute {
currentCodec = videoCodec
}
}
if let currentCodec, currentCodec == "av1" {
} else {
qualityFiles[key] = baseFile.withMedia(alternativeFile)
}
} else {
qualityFiles[key] = baseFile.withMedia(alternativeFile)
}
}
}
}

View File

@ -11,6 +11,7 @@ import AccountContext
import PhotoResources
import UIKitRuntimeUtils
import RangeSet
import VideoToolbox
private extension CGRect {
var center: CGPoint {
@ -25,6 +26,11 @@ public enum NativeVideoContentId: Hashable {
case profileVideo(Int64, String?)
}
private let isAv1Supported: Bool = {
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
return value
}()
public final class NativeVideoContent: UniversalVideoContent {
public let id: AnyHashable
public let nativeId: NativeVideoContentId
@ -58,7 +64,17 @@ public final class NativeVideoContent: UniversalVideoContent {
let hasSentFramesToDisplay: (() -> Void)?
public static func isVideoCodecSupported(videoCodec: String) -> Bool {
return videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc"
if videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" {
return true
}
if videoCodec == "av1" {
if isAv1Supported {
return true
}
}
return false
}
public static func isHLSVideo(file: TelegramMediaFile) -> Bool {