mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
HLS video improvements
This commit is contained in:
parent
9f11ee1c5a
commit
9e1dc11997
@ -1405,8 +1405,15 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.galleryNode.completeCustomDismiss = { [weak self] in
|
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?._hiddenMedia.set(.single(nil))
|
||||||
|
}
|
||||||
|
|
||||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
|||||||
public var pager: GalleryPagerNode
|
public var pager: GalleryPagerNode
|
||||||
|
|
||||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var completeCustomDismiss: () -> Void = { }
|
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||||
public var galleryController: () -> ViewController? = { 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 {
|
if let strongSelf = self {
|
||||||
strongSelf.completeCustomDismiss()
|
strongSelf.completeCustomDismiss(isPictureInPicture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ open class GalleryItemNode: ASDisplayNode {
|
|||||||
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
||||||
public var dismiss: () -> Void = { }
|
public var dismiss: () -> Void = { }
|
||||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var completeCustomDismiss: () -> Void = { }
|
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||||
public var galleryController: () -> ViewController? = { return nil }
|
public var galleryController: () -> ViewController? = { return nil }
|
||||||
public var alternativeDismiss: () -> Bool = { return false }
|
public var alternativeDismiss: () -> Bool = { return false }
|
||||||
|
@ -110,7 +110,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
|
|||||||
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
||||||
public var dismiss: () -> Void = { }
|
public var dismiss: () -> Void = { }
|
||||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var completeCustomDismiss: () -> Void = { }
|
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||||
public var galleryController: () -> ViewController? = { return nil }
|
public var galleryController: () -> ViewController? = { return nil }
|
||||||
|
|
||||||
|
@ -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)))
|
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) {
|
Queue.mainQueue().after(0.3) {
|
||||||
self.completeCustomDismiss()
|
self.completeCustomDismiss(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f(.default)
|
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)))
|
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) {
|
Queue.mainQueue().after(0.3) {
|
||||||
self.completeCustomDismiss()
|
self.completeCustomDismiss(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f(.default)
|
f(.default)
|
||||||
|
@ -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 {
|
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let presentationData: PresentationData
|
private let presentationData: PresentationData
|
||||||
@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
private let hasExpandedCaptionPromise = Promise<Bool>()
|
private let hasExpandedCaptionPromise = Promise<Bool>()
|
||||||
private var hideControlsDisposable: Disposable?
|
private var hideControlsDisposable: Disposable?
|
||||||
|
private var automaticPictureInPictureDisposable: Disposable?
|
||||||
|
|
||||||
var playbackCompleted: (() -> Void)?
|
var playbackCompleted: (() -> Void)?
|
||||||
|
|
||||||
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
|
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
|
||||||
|
|
||||||
private var pictureInPictureContent: AnyObject?
|
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) {
|
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
self.mediaPlaybackStateDisposable.dispose()
|
self.mediaPlaybackStateDisposable.dispose()
|
||||||
self.scrubbingFrameDisposable?.dispose()
|
self.scrubbingFrameDisposable?.dispose()
|
||||||
self.hideControlsDisposable?.dispose()
|
self.hideControlsDisposable?.dispose()
|
||||||
|
self.automaticPictureInPictureDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func ready() -> Signal<Void, NoError> {
|
override func ready() -> Signal<Void, NoError> {
|
||||||
@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
strongSelf.videoNode?.setBaseRate(playbackRate)
|
strongSelf.videoNode?.setBaseRate(playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strongSelf.nativePictureInPictureContent == nil {
|
||||||
|
strongSelf.setupNativePictureInPicture()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.videoNode = videoNode
|
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.beginCustomDismiss(false)
|
||||||
self.statusNode.isHidden = true
|
self.statusNode.isHidden = true
|
||||||
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
|
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() {
|
@objc func pictureInPictureButtonPressed() {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
|
||||||
|
nativePictureInPictureContent.beginPictureInPicture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isNativePictureInPictureSupported = false
|
var isNativePictureInPictureSupported = false
|
||||||
switch self.item?.contentInfo {
|
switch self.item?.contentInfo {
|
||||||
case let .message(message, _):
|
case let .message(message, _):
|
||||||
@ -2391,7 +2712,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.completeCustomDismiss()
|
strongSelf.completeCustomDismiss(false)
|
||||||
}, expand: { [weak baseNavigationController] completion in
|
}, expand: { [weak baseNavigationController] completion in
|
||||||
guard let contentInfo = item.contentInfo else {
|
guard let contentInfo = item.contentInfo else {
|
||||||
return
|
return
|
||||||
@ -2524,7 +2845,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
self.beginCustomDismiss(false)
|
self.beginCustomDismiss(false)
|
||||||
self.statusNode.isHidden = true
|
self.statusNode.isHidden = true
|
||||||
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
|
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)))
|
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) {
|
Queue.mainQueue().after(0.3) {
|
||||||
strongSelf.completeCustomDismiss()
|
strongSelf.completeCustomDismiss(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f(.default)
|
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)))
|
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) {
|
Queue.mainQueue().after(0.3) {
|
||||||
self.completeCustomDismiss()
|
self.completeCustomDismiss(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f(.default)
|
f(.default)
|
||||||
|
@ -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?._hiddenMedia.set(.single(nil))
|
||||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
@ -398,7 +398,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
|
|||||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
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?._hiddenMedia.set(.single(nil))
|
||||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||||||
case .unmuted:
|
case .unmuted:
|
||||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||||
case .raiseHand, .scheduled:
|
case .raiseHand, .scheduled:
|
||||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF)
|
||||||
}
|
}
|
||||||
iconDiameter = 60.0
|
iconDiameter = 60.0
|
||||||
case let .video(isActive):
|
case let .video(isActive):
|
||||||
@ -282,6 +282,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.isEnabled = isEnabled
|
self.isEnabled = isEnabled
|
||||||
|
self.isUserInteractionEnabled = isEnabled
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
@ -1022,7 +1022,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||||
displayEvent = false
|
displayEvent = false
|
||||||
}
|
}
|
||||||
if members.totalCount < 250 {
|
if members.totalCount < 40 {
|
||||||
displayEvent = true
|
displayEvent = true
|
||||||
} else if event.peer.isVerified {
|
} else if event.peer.isVerified {
|
||||||
displayEvent = true
|
displayEvent = true
|
||||||
|
@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let effectiveMediaVisibility = self.visibility
|
||||||
|
|
||||||
var isPlaying = true
|
var isPlaying = true
|
||||||
if case let .visible(_, subRect) = self.visibility {
|
|
||||||
if subRect.minY > 32.0 {
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
if !item.controllerInteraction.canReadHistory {
|
if !item.controllerInteraction.canReadHistory {
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.forceStopAnimations {
|
if self.forceStopAnimations {
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
}
|
}
|
||||||
@ -6228,8 +6224,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
|
|
||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
|
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)
|
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 {
|
if let threadInfoNode = self.threadInfoNode {
|
||||||
threadInfoNode.visibility = effectiveVisibility != .none
|
threadInfoNode.visibility = effectiveVisibility != .none
|
||||||
|
@ -25,9 +25,22 @@ public final class HLSQualitySet {
|
|||||||
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
|
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
|
||||||
for attribute in alternativeFile.attributes {
|
for attribute in alternativeFile.attributes {
|
||||||
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
|
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
|
||||||
let _ = size
|
|
||||||
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import AccountContext
|
|||||||
import PhotoResources
|
import PhotoResources
|
||||||
import UIKitRuntimeUtils
|
import UIKitRuntimeUtils
|
||||||
import RangeSet
|
import RangeSet
|
||||||
|
import VideoToolbox
|
||||||
|
|
||||||
private extension CGRect {
|
private extension CGRect {
|
||||||
var center: CGPoint {
|
var center: CGPoint {
|
||||||
@ -25,6 +26,11 @@ public enum NativeVideoContentId: Hashable {
|
|||||||
case profileVideo(Int64, String?)
|
case profileVideo(Int64, String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let isAv1Supported: Bool = {
|
||||||
|
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
|
||||||
|
return value
|
||||||
|
}()
|
||||||
|
|
||||||
public final class NativeVideoContent: UniversalVideoContent {
|
public final class NativeVideoContent: UniversalVideoContent {
|
||||||
public let id: AnyHashable
|
public let id: AnyHashable
|
||||||
public let nativeId: NativeVideoContentId
|
public let nativeId: NativeVideoContentId
|
||||||
@ -58,7 +64,17 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
let hasSentFramesToDisplay: (() -> Void)?
|
let hasSentFramesToDisplay: (() -> Void)?
|
||||||
|
|
||||||
public static func isVideoCodecSupported(videoCodec: String) -> Bool {
|
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 {
|
public static func isHLSVideo(file: TelegramMediaFile) -> Bool {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user