mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Video player and calls
This commit is contained in:
parent
9cdad135c0
commit
af267cae64
58 changed files with 1643 additions and 538 deletions
|
|
@ -1135,7 +1135,7 @@ public protocol AccountContext: AnyObject {
|
|||
func chatLocationUnreadCount(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<Int, NoError>
|
||||
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
||||
|
||||
func scheduleGroupCall(peerId: PeerId)
|
||||
func scheduleGroupCall(peerId: PeerId, parentController: ViewController)
|
||||
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription)
|
||||
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import AsyncDisplayKit
|
|||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramAudio
|
||||
import Display
|
||||
|
||||
public enum RequestCallResult {
|
||||
case requested
|
||||
|
|
@ -472,5 +473,5 @@ public protocol PresentationCallManager: AnyObject {
|
|||
|
||||
func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||
func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
|
||||
func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public protocol UniversalVideoContent {
|
|||
var dimensions: CGSize { get }
|
||||
var duration: Double { get }
|
||||
|
||||
func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
|
||||
func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
|
||||
|
||||
func isEqual(to other: UniversalVideoContent) -> Bool
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ public enum UniversalVideoNodeFetchControl {
|
|||
}
|
||||
|
||||
public final class UniversalVideoNode: ASDisplayNode {
|
||||
private let accountId: AccountRecordId
|
||||
private let postbox: Postbox
|
||||
private let audioSession: ManagedAudioSession
|
||||
private let manager: UniversalVideoManager
|
||||
|
|
@ -135,11 +136,12 @@ public final class UniversalVideoNode: ASDisplayNode {
|
|||
if self.canAttachContent {
|
||||
assert(self.contentRequestIndex == nil)
|
||||
|
||||
let accountId = self.accountId
|
||||
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(postbox: postbox, audioSession: audioSession)
|
||||
return content.makeContentNode(accountId: accountId, postbox: postbox, audioSession: audioSession)
|
||||
}, update: { [weak self] contentNodeAndFlags in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateContentNode(contentNodeAndFlags)
|
||||
|
|
@ -160,7 +162,8 @@ public final class UniversalVideoNode: ASDisplayNode {
|
|||
return self.contentNode != nil
|
||||
}
|
||||
|
||||
public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) {
|
||||
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
|
||||
self.postbox = postbox
|
||||
self.audioSession = audioSession
|
||||
self.manager = manager
|
||||
|
|
|
|||
|
|
@ -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(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
|
||||
let videoNode = UniversalVideoNode(accountId: context.account.id, 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
|
||||
|
|
|
|||
|
|
@ -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(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||
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)
|
||||
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
||||
videoNode.alpha = 0.01
|
||||
self.videoNode = videoNode
|
||||
|
|
|
|||
|
|
@ -330,6 +330,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||
|
||||
private func setupWithVideo(_ file: TelegramMediaFile) {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import UniversalMediaPlayer
|
|||
import TelegramPresentationData
|
||||
import RangeSet
|
||||
import ShimmerEffect
|
||||
import TelegramUniversalVideoContent
|
||||
|
||||
private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
|
||||
|
||||
|
|
|
|||
|
|
@ -578,6 +578,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
private let landscape: Bool
|
||||
private let timecode: Double?
|
||||
private var playbackRate: Double?
|
||||
private var videoQuality: UniversalVideoContentVideoQuality = .auto
|
||||
|
||||
private let accountInUseDisposable = MetaDisposable()
|
||||
private let disposable = MetaDisposable()
|
||||
|
|
@ -1757,6 +1758,16 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
}
|
||||
}
|
||||
|
||||
func updateSharedVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||
self.videoQuality = videoQuality
|
||||
|
||||
self.galleryNode.pager.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? UniversalVideoGalleryItemNode {
|
||||
itemNode.updateVideoQuality(videoQuality)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var keyShortcuts: [KeyShortcut] {
|
||||
var keyShortcuts: [KeyShortcut] = []
|
||||
keyShortcuts.append(
|
||||
|
|
|
|||
|
|
@ -769,6 +769,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
private var moreBarButtonRate: Double = 1.0
|
||||
private var moreBarButtonRateTimestamp: Double?
|
||||
|
||||
private let settingsBarButton: MoreHeaderButton
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var videoNodeUserInteractionEnabled: Bool = false
|
||||
private var videoFramePreview: FramePreview?
|
||||
|
|
@ -798,6 +800,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
private var item: UniversalVideoGalleryItem?
|
||||
private var playbackRate: Double?
|
||||
private var videoQuality: UniversalVideoContentVideoQuality = .auto
|
||||
private let playbackRatePromise = ValuePromise<Double>()
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
|
|
@ -849,11 +852,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.moreBarButton.isUserInteractionEnabled = true
|
||||
self.moreBarButton.setContent(.more(optionsCircleImage(dark: false)))
|
||||
|
||||
self.settingsBarButton = MoreHeaderButton()
|
||||
self.settingsBarButton.isUserInteractionEnabled = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.footerContentNode.interacting = { [weak self] value in
|
||||
self?.isInteractingPromise.set(value)
|
||||
|
|
@ -966,7 +973,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in
|
||||
self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture)
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false)
|
||||
}
|
||||
|
||||
self.titleContentView = GalleryTitleView(frame: CGRect())
|
||||
|
|
@ -1106,6 +1116,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
var forceEnableUserInteraction = false
|
||||
var isAnimated = false
|
||||
var isEnhancedWebPlayer = false
|
||||
var isAdaptive = false
|
||||
if let content = item.content as? NativeVideoContent {
|
||||
isAnimated = content.fileReference.media.isAnimated
|
||||
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference)
|
||||
|
|
@ -1129,6 +1140,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
} else if let _ = item.content as? PlatformVideoContent {
|
||||
disablePlayerControls = true
|
||||
forceEnablePiP = true
|
||||
} else if let _ = item.content as? HLSVideoContent {
|
||||
isAdaptive = true
|
||||
}
|
||||
|
||||
if isAdaptive {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
|
||||
} else {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white)))
|
||||
}
|
||||
|
||||
let dimensions = item.content.dimensions
|
||||
|
|
@ -1149,7 +1168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
let mediaManager = item.context.sharedContext.mediaManager
|
||||
|
||||
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
|
||||
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 videoScale: CGFloat
|
||||
if item.content is WebEmbedVideoContent {
|
||||
videoScale = 1.0
|
||||
|
|
@ -1250,7 +1269,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file)
|
||||
if !isWebpage {
|
||||
scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size)
|
||||
if !NativeVideoContent.isHLSVideo(file: file) {
|
||||
scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size)
|
||||
}
|
||||
}
|
||||
|
||||
self.requiresDownload = !isMediaStreamable(message: message, media: file)
|
||||
|
|
@ -1443,6 +1464,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers
|
||||
barButtonItems.append(rightBarButtonItem)
|
||||
}
|
||||
|
||||
if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) {
|
||||
let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed))
|
||||
rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_PictureInPicture
|
||||
|
|
@ -1487,6 +1509,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
hasMoreButton = true
|
||||
}
|
||||
|
||||
if !isAnimated && !disablePlayerControls {
|
||||
let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)!
|
||||
settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title
|
||||
barButtonItems.append(settingsMenuItem)
|
||||
}
|
||||
|
||||
if hasMoreButton {
|
||||
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
|
||||
moreMenuItem.accessibilityLabel = self.presentationData.strings.Common_More
|
||||
|
|
@ -2169,7 +2197,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
let baseNavigationController = self.baseNavigationController()
|
||||
let mediaManager = self.context.sharedContext.mediaManager
|
||||
var expandImpl: (() -> Void)?
|
||||
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
|
||||
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: {
|
||||
expandImpl?()
|
||||
}, close: { [weak mediaManager] in
|
||||
mediaManager?.setOverlayVideoNode(nil)
|
||||
|
|
@ -2271,7 +2299,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported {
|
||||
self.disablePictureInPicturePlaceholder = true
|
||||
|
||||
let overlayVideoNode = UniversalVideoNode(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(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 absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil)
|
||||
overlayVideoNode.frame = absoluteRect
|
||||
overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate)
|
||||
|
|
@ -2354,7 +2382,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
shouldBeDismissed = .single(false)
|
||||
}
|
||||
|
||||
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
|
||||
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: {
|
||||
expandImpl?()
|
||||
}, close: { [weak mediaManager] in
|
||||
mediaManager?.setOverlayVideoNode(nil)
|
||||
|
|
@ -2501,7 +2529,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
|
||||
}
|
||||
|
||||
private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
|
||||
private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) {
|
||||
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
||||
return
|
||||
}
|
||||
|
|
@ -2510,12 +2538,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
|
||||
items = self.adMenuMainItems()
|
||||
} else {
|
||||
items = self.contextMenuMainItems(dismiss: {
|
||||
items = self.contextMenuMainItems(isSettings: isSettings, dismiss: {
|
||||
dismissImpl?()
|
||||
})
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
self.isShowingContextMenuPromise.set(true)
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
dismissImpl = { [weak contextController] in
|
||||
|
|
@ -2666,7 +2694,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
|
||||
private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
|
||||
private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
|
||||
guard let videoNode = self.videoNode, let item = self.item else {
|
||||
return .single([])
|
||||
}
|
||||
|
|
@ -2687,172 +2715,172 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
var didSetSpeedValue = false
|
||||
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
|
||||
if abs(speed - status.baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
didSetSpeedValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !didSetSpeedValue && status.baseRate != 1.0 {
|
||||
speedValue = String(format: "%.1fx", status.baseRate)
|
||||
speedIconText = speedValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c?.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c?.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty {
|
||||
//TODO:localize
|
||||
|
||||
let qualityText: String
|
||||
switch videoQualityState.preferred {
|
||||
case .auto:
|
||||
if videoQualityState.current != 0 {
|
||||
qualityText = "Auto (\(videoQualityState.current)p)"
|
||||
} else {
|
||||
qualityText = "Auto"
|
||||
if isSettings {
|
||||
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
var didSetSpeedValue = false
|
||||
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
|
||||
if abs(speed - status.baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
didSetSpeedValue = true
|
||||
break
|
||||
}
|
||||
case let .quality(value):
|
||||
qualityText = "\(value)p"
|
||||
}
|
||||
if !didSetSpeedValue && status.baseRate != 1.0 {
|
||||
speedValue = String(format: "%.1fx", status.baseRate)
|
||||
speedIconText = speedValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in
|
||||
return nil
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c?.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c?.setItems(.single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss)))), minHeight: nil, animated: true)
|
||||
|
||||
c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) })
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if let (message, _, _) = strongSelf.contentInfo() {
|
||||
let context = strongSelf.context
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty {
|
||||
items.append(.separator)
|
||||
|
||||
//TODO:localize
|
||||
|
||||
let qualityText: String
|
||||
switch videoQualityState.preferred {
|
||||
case .auto:
|
||||
if videoQualityState.current != 0 {
|
||||
qualityText = "Auto (\(videoQualityState.current)p)"
|
||||
} else {
|
||||
qualityText = "Auto"
|
||||
}
|
||||
case let .quality(value):
|
||||
qualityText = "\(value)p"
|
||||
}
|
||||
if let navigationController = strongSelf.baseNavigationController() {
|
||||
strongSelf.beginCustomDismiss(true)
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in
|
||||
return nil
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c?.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss)))))
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
if let (message, _, _) = strongSelf.contentInfo() {
|
||||
let context = strongSelf.context
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
if let navigationController = strongSelf.baseNavigationController() {
|
||||
strongSelf.beginCustomDismiss(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: false)))
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
strongSelf.completeCustomDismiss()
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
// if #available(iOS 11.0, *) {
|
||||
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// strongSelf.beginAirPlaySetup()
|
||||
// })))
|
||||
// }
|
||||
|
||||
if let (message, _, _) = strongSelf.contentInfo() {
|
||||
for media in message.media {
|
||||
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
let url = content.url
|
||||
|
||||
let item = OpenInItem.url(url: url)
|
||||
let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self, let controller = strongSelf.galleryController() {
|
||||
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if !presentationData.theme.overallDarkAppearance {
|
||||
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
}
|
||||
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
|
||||
}
|
||||
})
|
||||
controller.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
})))
|
||||
break
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
// if #available(iOS 11.0, *) {
|
||||
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// strongSelf.beginAirPlaySetup()
|
||||
// })))
|
||||
// }
|
||||
|
||||
if let (message, _, _) = strongSelf.contentInfo() {
|
||||
for media in message.media {
|
||||
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
let url = content.url
|
||||
|
||||
let item = OpenInItem.url(url: url)
|
||||
let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self, let controller = strongSelf.galleryController() {
|
||||
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if !presentationData.theme.overallDarkAppearance {
|
||||
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
}
|
||||
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
|
||||
}
|
||||
})
|
||||
controller.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
})))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self {
|
||||
switch strongSelf.fetchStatus {
|
||||
case .Local:
|
||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file))
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self {
|
||||
switch strongSelf.fetchStatus {
|
||||
case .Local:
|
||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file))
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let controller = strongSelf.galleryController() else {
|
||||
return
|
||||
}
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
})
|
||||
default:
|
||||
guard let controller = strongSelf.galleryController() else {
|
||||
return
|
||||
}
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
})
|
||||
default:
|
||||
guard let controller = strongSelf.galleryController() else {
|
||||
return
|
||||
controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
|
||||
if let self, let navigationController = self.baseNavigationController() {
|
||||
self.beginCustomDismiss(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) {
|
||||
self.completeCustomDismiss()
|
||||
})))
|
||||
}
|
||||
|
||||
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
|
||||
if let self, let navigationController = self.baseNavigationController() {
|
||||
self.beginCustomDismiss(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) {
|
||||
self.completeCustomDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if strongSelf.canDelete() {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self {
|
||||
strongSelf.footerContentNode.deleteButtonPressed()
|
||||
}
|
||||
})))
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if strongSelf.canDelete() {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self {
|
||||
strongSelf.footerContentNode.deleteButtonPressed()
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
|
|
@ -2877,11 +2905,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c?.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
c?.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
|
||||
c?.popItems()
|
||||
})))
|
||||
|
||||
let sliderValuePromise = ValuePromise<Double?>(nil)
|
||||
|
|
@ -2938,12 +2962,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { [weak self] c, _ in
|
||||
guard let self else {
|
||||
c?.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
c?.setItems(self.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c?.popItems()
|
||||
})))
|
||||
|
||||
do {
|
||||
|
|
@ -2967,6 +2987,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return
|
||||
}
|
||||
videoNode.setVideoQuality(.auto)
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
|
||||
|
||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||
controller.updateSharedPlaybackRate(rate)
|
||||
|
|
@ -2990,6 +3011,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return
|
||||
}
|
||||
videoNode.setVideoQuality(.quality(quality))
|
||||
if quality >= 700 {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white)))
|
||||
} else {
|
||||
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white)))
|
||||
}
|
||||
|
||||
/*if let controller = strongSelf.galleryController() as? GalleryController {
|
||||
controller.updateSharedPlaybackRate(rate)
|
||||
|
|
@ -3082,6 +3108,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
})
|
||||
}
|
||||
|
||||
@objc private func settingsButtonPressed() {
|
||||
self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true)
|
||||
}
|
||||
|
||||
override func adjustForPreviewing() {
|
||||
super.adjustForPreviewing()
|
||||
|
||||
|
|
@ -3102,6 +3132,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
||||
}
|
||||
|
||||
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||
self.videoQuality = videoQuality
|
||||
|
||||
self.videoNode?.setVideoQuality(videoQuality)
|
||||
}
|
||||
|
||||
public func seekToStart() {
|
||||
self.videoNode?.seek(0.0)
|
||||
self.videoNode?.play()
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
|||
fileValue = file
|
||||
}
|
||||
|
||||
self.videoNode = UniversalVideoNode(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(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.isUserInteractionEnabled = false
|
||||
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
||||
|
|
|
|||
|
|
@ -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(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.isHidden = true
|
||||
self.videoStartTimestamp = video.representation.startTimestamp
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
let mediaManager = self.context.sharedContext.mediaManager
|
||||
let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.canAttachContent = true
|
||||
videoNode.isHidden = true
|
||||
|
|
|
|||
|
|
@ -140,8 +140,8 @@ public final class MediaBox {
|
|||
|
||||
private let statusQueue = Queue()
|
||||
private let concurrentQueue = Queue.concurrentDefaultQueue()
|
||||
private let dataQueue = Queue(name: "MediaBox-Data")
|
||||
private let dataFileManager: MediaBoxFileManager
|
||||
public let dataQueue = Queue(name: "MediaBox-Data")
|
||||
public let dataFileManager: MediaBoxFileManager
|
||||
private let cacheQueue = Queue()
|
||||
private let timeBasedCleanup: TimeBasedCleanup
|
||||
|
||||
|
|
@ -209,60 +209,6 @@ public final class MediaBox {
|
|||
self.dataFileManager = MediaBoxFileManager(queue: self.dataQueue)
|
||||
|
||||
let _ = self.ensureDirectoryCreated
|
||||
|
||||
//self.updateResourceIndex()
|
||||
|
||||
/*#if DEBUG
|
||||
self.dataQueue.async {
|
||||
for _ in 0 ..< 5 {
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||
print("MediaBox test: file \(tempFile.path)")
|
||||
let queue2 = Queue.concurrentDefaultQueue()
|
||||
if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: tempFile.path.data(using: .utf8)!, path: tempFile.path + "_complete", partialPath: tempFile.path + "_partial", metaPath: tempFile.path + "_partial" + ".meta") {
|
||||
let _ = fileContext.fetched(
|
||||
range: 0 ..< Int64.max,
|
||||
priority: .default,
|
||||
fetch: { ranges in
|
||||
return ranges
|
||||
|> filter { !$0.isEmpty }
|
||||
|> take(1)
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { _ in
|
||||
return Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(524288))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(393216))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(655360))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(169608))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 131072, data: Data(repeating: 0xbb, count: 38536), range: 0 ..< 38536, complete: true))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(repeating: 0xaa, count: 131072), range: 0 ..< 131072, complete: false))
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
},
|
||||
error: { _ in
|
||||
},
|
||||
completed: {
|
||||
assert(try! Data(contentsOf: URL(fileURLWithPath: tempFile.path + "_complete")) == Data(repeating: 0xaa, count: 131072) + Data(repeating: 0xbb, count: 38536))
|
||||
let _ = fileContext.addReference()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif*/
|
||||
}
|
||||
|
||||
public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) {
|
||||
|
|
@ -641,21 +587,12 @@ public final class MediaBox {
|
|||
paths.partial + ".meta"
|
||||
])
|
||||
|
||||
#if true
|
||||
if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") {
|
||||
context = fileContext
|
||||
self.fileContexts[resourceId] = fileContext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
if let fileContext = MediaBoxFileContextImpl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") {
|
||||
context = fileContext
|
||||
self.fileContexts[resourceId] = fileContext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if let context = context {
|
||||
let index = context.addReference()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import RangeSet
|
||||
import SwiftSignalKit
|
||||
|
||||
final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
public final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
private final class RangeRequest {
|
||||
let value: Range<Int64>
|
||||
let priority: MediaBoxFetchPriority
|
||||
|
|
@ -99,7 +99,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
private final class PartialState {
|
||||
private let queue: Queue
|
||||
private let manager: MediaBoxFileManager
|
||||
private let storageBox: StorageBox
|
||||
private let storageBox: StorageBox?
|
||||
private let resourceId: Data
|
||||
private let partialPath: String
|
||||
private let fullPath: String
|
||||
|
|
@ -124,7 +124,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
init(
|
||||
queue: Queue,
|
||||
manager: MediaBoxFileManager,
|
||||
storageBox: StorageBox,
|
||||
storageBox: StorageBox?,
|
||||
resourceId: Data,
|
||||
partialPath: String,
|
||||
fullPath: String,
|
||||
|
|
@ -461,7 +461,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
self.fileMap.fill(range)
|
||||
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
||||
|
||||
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||
self.storageBox?.update(id: self.resourceId, size: self.fileMap.sum)
|
||||
} else {
|
||||
postboxLog("MediaBoxFileContextV2Impl: error seeking file to \(resourceOffset) at \(self.partialPath)")
|
||||
}
|
||||
|
|
@ -474,7 +474,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
private func processMovedFile() {
|
||||
if let size = fileSize(self.fullPath) {
|
||||
self.isComplete = true
|
||||
self.storageBox.update(id: self.resourceId, size: size)
|
||||
self.storageBox?.update(id: self.resourceId, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +623,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
|
||||
private let queue: Queue
|
||||
private let manager: MediaBoxFileManager
|
||||
private let storageBox: StorageBox
|
||||
private let storageBox: StorageBox?
|
||||
private let resourceId: Data
|
||||
private let path: String
|
||||
private let partialPath: String
|
||||
|
|
@ -637,10 +637,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
return self.references.isEmpty
|
||||
}
|
||||
|
||||
init?(
|
||||
public init?(
|
||||
queue: Queue,
|
||||
manager: MediaBoxFileManager,
|
||||
storageBox: StorageBox,
|
||||
storageBox: StorageBox?,
|
||||
resourceId: Data,
|
||||
path: String,
|
||||
partialPath: String,
|
||||
|
|
@ -683,7 +683,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func data(range: Range<Int64>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
|
||||
public func data(range: Range<Int64>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if let size = fileSize(self.path) {
|
||||
|
|
@ -708,7 +708,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func fetched(
|
||||
public func fetched(
|
||||
range: Range<Int64>,
|
||||
priority: MediaBoxFetchPriority,
|
||||
fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>,
|
||||
|
|
@ -734,7 +734,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func fetchedFullRange(
|
||||
public func fetchedFullRange(
|
||||
fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>,
|
||||
error: @escaping (MediaResourceDataFetchError) -> Void,
|
||||
completed: @escaping () -> Void
|
||||
|
|
@ -758,7 +758,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func cancelFullRangeFetches() {
|
||||
public func cancelFullRangeFetches() {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if let partialState = self.partialState {
|
||||
|
|
@ -766,7 +766,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func rangeStatus(next: @escaping (RangeSet<Int64>) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
public func rangeStatus(next: @escaping (RangeSet<Int64>) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if let size = fileSize(self.path) {
|
||||
|
|
@ -781,7 +781,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
|||
}
|
||||
}
|
||||
|
||||
func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable {
|
||||
public func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if let _ = fileSize(self.path) {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import Foundation
|
|||
import SwiftSignalKit
|
||||
import ManagedFile
|
||||
|
||||
final class MediaBoxFileManager {
|
||||
enum Mode {
|
||||
public final class MediaBoxFileManager {
|
||||
public enum Mode {
|
||||
case read
|
||||
case readwrite
|
||||
}
|
||||
|
||||
enum AccessError: Error {
|
||||
public enum AccessError: Error {
|
||||
case generic
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ final class MediaBoxFileManager {
|
|||
private var nextItemId: Int = 0
|
||||
private let maxOpenFiles: Int
|
||||
|
||||
init(queue: Queue?) {
|
||||
public init(queue: Queue?) {
|
||||
self.queue = queue
|
||||
self.maxOpenFiles = 16
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ private final class PhoneView: UIView {
|
|||
hintDimensions: CGSize(width: 1170, height: 1754),
|
||||
storeAfterDownload: nil
|
||||
)
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
|
||||
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)
|
||||
videoNode.canAttachContent = true
|
||||
self.videoNode = videoNode
|
||||
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte
|
|||
|
||||
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(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
|
||||
|
|
|
|||
|
|
@ -634,7 +634,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
}
|
||||
}
|
||||
|
||||
private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
|
||||
private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId(), parentController: ViewController) -> Signal<Bool, NoError> {
|
||||
let (presentationData, present, openSettings) = self.getDeviceAccessData()
|
||||
|
||||
let isVideo = false
|
||||
|
|
@ -668,7 +668,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { [weak self] accessEnabled, peer -> Signal<Bool, NoError> in
|
||||
|> mapToSignal { [weak self, weak parentController] accessEnabled, peer -> Signal<Bool, NoError> in
|
||||
guard let strongSelf = self else {
|
||||
return .single(false)
|
||||
}
|
||||
|
|
@ -681,46 +681,98 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
audioSession: strongSelf.audioSession,
|
||||
callKitIntegration: nil,
|
||||
getDeviceAccessData: strongSelf.getDeviceAccessData,
|
||||
initialCall: nil,
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
isChannel: isChannel,
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(true)
|
||||
strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||
guard let strongSelf = self, let call = call else {
|
||||
return
|
||||
|
||||
if shouldUseV2VideoChatImpl(context: accountContext) {
|
||||
if let parentController {
|
||||
parentController.push(ScheduleVideoChatSheetScreen(
|
||||
context: accountContext,
|
||||
scheduleAction: { timestamp in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
audioSession: self.audioSession,
|
||||
callKitIntegration: nil,
|
||||
getDeviceAccessData: self.getDeviceAccessData,
|
||||
initialCall: nil,
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
isChannel: isChannel,
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false
|
||||
)
|
||||
call.schedule(timestamp: timestamp)
|
||||
|
||||
self.updateCurrentGroupCall(call)
|
||||
self.currentGroupCallPromise.set(.single(call))
|
||||
self.hasActiveGroupCallsPromise.set(true)
|
||||
self.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak call] value in
|
||||
guard let self, let call else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if self.currentGroupCall === call {
|
||||
self.updateCurrentGroupCall(nil)
|
||||
self.currentGroupCallPromise.set(.single(nil))
|
||||
self.hasActiveGroupCallsPromise.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
))
|
||||
}
|
||||
if value {
|
||||
if strongSelf.currentGroupCall === call {
|
||||
strongSelf.updateCurrentGroupCall(nil)
|
||||
strongSelf.currentGroupCallPromise.set(.single(nil))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(false)
|
||||
|
||||
return .single(true)
|
||||
} else {
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
audioSession: strongSelf.audioSession,
|
||||
callKitIntegration: nil,
|
||||
getDeviceAccessData: strongSelf.getDeviceAccessData,
|
||||
initialCall: nil,
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
isChannel: isChannel,
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(true)
|
||||
strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||
guard let strongSelf = self, let call = call else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
if value {
|
||||
if strongSelf.currentGroupCall === call {
|
||||
strongSelf.updateCurrentGroupCall(nil)
|
||||
strongSelf.currentGroupCallPromise.set(.single(nil))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return .single(true)
|
||||
}
|
||||
}
|
||||
|
||||
public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult {
|
||||
let begin: () -> Void = { [weak self] in
|
||||
let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start()
|
||||
public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult {
|
||||
let begin: () -> Void = { [weak self, weak parentController] in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId, parentController: parentController).start()
|
||||
}
|
||||
|
||||
if let currentGroupCall = self.currentGroupCallValue {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,466 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import AccountContext
|
||||
import SheetComponent
|
||||
import ButtonComponent
|
||||
import TelegramCore
|
||||
import AnimatedTextComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
import Markdown
|
||||
|
||||
private final class ScheduleVideoChatSheetContentComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let scheduleAction: (Int32) -> Void
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
scheduleAction: @escaping (Int32) -> Void,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.scheduleAction = scheduleAction
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: ScheduleVideoChatSheetContentComponent, rhs: ScheduleVideoChatSheetContentComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let button = ComponentView<Empty>()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let mainText = ComponentView<Empty>()
|
||||
private var pickerView: UIDatePicker?
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
private let dateFormatter: DateFormatter
|
||||
|
||||
private var component: ScheduleVideoChatSheetContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.dateFormatter = DateFormatter()
|
||||
self.dateFormatter.timeStyle = .none
|
||||
self.dateFormatter.dateStyle = .short
|
||||
self.dateFormatter.timeZone = TimeZone.current
|
||||
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func scheduleDatePickerUpdated() {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateSchedulePickerLimits() {
|
||||
let timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = timeZone
|
||||
let currentDate = Date()
|
||||
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
|
||||
components.second = 0
|
||||
|
||||
let roundedDate = calendar.date(from: components)!
|
||||
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate)
|
||||
|
||||
let minute = components.minute ?? 0
|
||||
components.minute = 0
|
||||
let roundedToHourDate = calendar.date(from: components)!
|
||||
components.hour = 0
|
||||
|
||||
let roundedToMidnightDate = calendar.date(from: components)!
|
||||
let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate)
|
||||
let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate)
|
||||
|
||||
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
|
||||
self.pickerView?.maximumDate = date
|
||||
}
|
||||
if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate {
|
||||
self.pickerView?.minimumDate = next1MinDate
|
||||
self.pickerView?.maximumDate = maxDate
|
||||
self.pickerView?.date = nextTwoHourDate
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
let _ = previousComponent
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += 16.0
|
||||
|
||||
//TODO:localize
|
||||
let titleString = NSMutableAttributedString()
|
||||
titleString.append(NSAttributedString(string: "Schedule Video Chat", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(titleString),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize))
|
||||
}
|
||||
contentHeight += titleSize.height
|
||||
contentHeight += 16.0
|
||||
|
||||
let pickerView: UIDatePicker
|
||||
if let current = self.pickerView {
|
||||
pickerView = current
|
||||
} else {
|
||||
let textColor = UIColor.white
|
||||
UILabel.setDateLabel(textColor)
|
||||
|
||||
pickerView = UIDatePicker()
|
||||
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
pickerView.datePickerMode = .countDownTimer
|
||||
pickerView.datePickerMode = .dateAndTime
|
||||
pickerView.locale = Locale.current
|
||||
pickerView.timeZone = TimeZone.current
|
||||
pickerView.minuteInterval = 1
|
||||
self.addSubview(pickerView)
|
||||
pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged)
|
||||
if #available(iOS 13.4, *) {
|
||||
pickerView.preferredDatePickerStyle = .wheels
|
||||
}
|
||||
pickerView.setValue(textColor, forKey: "textColor")
|
||||
self.pickerView = pickerView
|
||||
self.addSubview(pickerView)
|
||||
|
||||
self.updateSchedulePickerLimits()
|
||||
}
|
||||
|
||||
let pickerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 216.0))
|
||||
transition.setFrame(view: pickerView, frame: pickerFrame)
|
||||
contentHeight += pickerFrame.height
|
||||
contentHeight += 26.0
|
||||
|
||||
let date = pickerView.date
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let timestamp = Int32(date.timeIntervalSince1970)
|
||||
let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: PresentationDateTimeFormat())
|
||||
let buttonTitle: String
|
||||
if calendar.isDateInToday(date) {
|
||||
buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleToday(time).string
|
||||
} else if calendar.isDateInTomorrow(date) {
|
||||
buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleTomorrow(time).string
|
||||
} else {
|
||||
buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).string
|
||||
}
|
||||
|
||||
let delta = timestamp - currentTimestamp
|
||||
|
||||
let isGroup = "".isEmpty
|
||||
let intervalString = scheduledTimeIntervalString(strings: environment.strings, value: max(60, delta))
|
||||
|
||||
let text: String = isGroup ? environment.strings.ScheduleVoiceChat_GroupText(intervalString).string : environment.strings.ScheduleLiveStream_ChannelText(intervalString).string
|
||||
|
||||
let mainText = NSMutableAttributedString()
|
||||
mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(
|
||||
font: Font.regular(14.0),
|
||||
textColor: UIColor(rgb: 0x8e8e93)
|
||||
),
|
||||
bold: MarkdownAttributeSet(
|
||||
font: Font.semibold(14.0),
|
||||
textColor: UIColor(rgb: 0x8e8e93)
|
||||
),
|
||||
link: MarkdownAttributeSet(
|
||||
font: Font.regular(14.0),
|
||||
textColor: environment.theme.list.itemAccentColor,
|
||||
additionalAttributes: [:]
|
||||
),
|
||||
linkAttribute: { attributes in
|
||||
return ("URL", "")
|
||||
}
|
||||
)))
|
||||
|
||||
let mainTextSize = self.mainText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(mainText),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
if let mainTextView = self.mainText.view {
|
||||
if mainTextView.superview == nil {
|
||||
self.addSubview(mainTextView)
|
||||
}
|
||||
transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize))
|
||||
}
|
||||
contentHeight += mainTextSize.height
|
||||
contentHeight += 10.0
|
||||
|
||||
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
|
||||
)))
|
||||
let buttonTransition = transition
|
||||
let buttonSize = self.button.update(
|
||||
transition: buttonTransition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: UIColor(rgb: 0x3252EF),
|
||||
foreground: .white,
|
||||
pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
HStack(buttonContents, spacing: 5.0)
|
||||
)),
|
||||
isEnabled: true,
|
||||
tintWhenDisabled: false,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let pickerView = self.pickerView else {
|
||||
return
|
||||
}
|
||||
component.scheduleAction(Int32(pickerView.date.timeIntervalSince1970))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
contentHeight += buttonSize.height
|
||||
contentHeight += 10.0
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: buttonTransition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: UIColor(rgb: 0x2B2B2F),
|
||||
foreground: .white,
|
||||
pressedColor: UIColor(rgb: 0x2B2B2F).withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
Text(text: "Cancel", font: Font.semibold(17.0), color: environment.theme.list.itemPrimaryTextColor)
|
||||
)),
|
||||
isEnabled: true,
|
||||
tintWhenDisabled: false,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.dismiss()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: cancelButtonSize)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
||||
}
|
||||
contentHeight += cancelButtonSize.height
|
||||
|
||||
if environment.safeInsets.bottom.isZero {
|
||||
contentHeight += 16.0
|
||||
} else {
|
||||
contentHeight += environment.safeInsets.bottom + 14.0
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScheduleVideoChatSheetScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let scheduleAction: (Int32) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
scheduleAction: @escaping (Int32) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.scheduleAction = scheduleAction
|
||||
}
|
||||
|
||||
static func ==(lhs: ScheduleVideoChatSheetScreenComponent, rhs: ScheduleVideoChatSheetScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
|
||||
private let sheetAnimateOut = ActionSlot<Action<Void>>()
|
||||
|
||||
private var component: ScheduleVideoChatSheetScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ScheduleVideoChatSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let sheetEnvironment = SheetComponentEnvironment(
|
||||
isDisplaying: environment.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { [weak self] _ in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { _ in
|
||||
if let controller = environment.controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
let _ = self.sheet.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(SheetComponent(
|
||||
content: AnyComponent(ScheduleVideoChatSheetContentComponent(
|
||||
scheduleAction: { [weak self] timestamp in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
|
||||
component.scheduleAction(timestamp)
|
||||
})
|
||||
},
|
||||
dismiss: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(UIColor(rgb: 0x1C1C1E)),
|
||||
animateOut: self.sheetAnimateOut
|
||||
)),
|
||||
environment: {
|
||||
environment
|
||||
sheetEnvironment
|
||||
},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let sheetView = self.sheet.view {
|
||||
if sheetView.superview == nil {
|
||||
self.addSubview(sheetView)
|
||||
}
|
||||
transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleVideoChatSheetScreen: ViewControllerComponentContainer {
|
||||
public init(context: AccountContext, scheduleAction: @escaping (Int32) -> Void) {
|
||||
super.init(context: context, component: ScheduleVideoChatSheetScreenComponent(
|
||||
context: context,
|
||||
scheduleAction: scheduleAction
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||
case muted
|
||||
case unmuted
|
||||
case raiseHand
|
||||
case scheduled
|
||||
}
|
||||
|
||||
let strings: PresentationStrings
|
||||
|
|
@ -156,7 +157,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||
backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF)
|
||||
case .unmuted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||
case .raiseHand:
|
||||
case .raiseHand, .scheduled:
|
||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
|
|
@ -169,7 +170,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||
backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF)
|
||||
case .unmuted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||
case .raiseHand:
|
||||
case .raiseHand, .scheduled:
|
||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
|
|
|
|||
|
|
@ -175,11 +175,17 @@ private final class GlowView: UIView {
|
|||
}
|
||||
|
||||
final class VideoChatMicButtonComponent: Component {
|
||||
enum ScheduledState: Equatable {
|
||||
case start
|
||||
case toggleSubscription(isSubscribed: Bool)
|
||||
}
|
||||
|
||||
enum Content: Equatable {
|
||||
case connecting
|
||||
case muted
|
||||
case unmuted(pushToTalk: Bool)
|
||||
case raiseHand
|
||||
case scheduled(state: ScheduledState)
|
||||
}
|
||||
|
||||
let call: PresentationGroupCall
|
||||
|
|
@ -187,19 +193,22 @@ final class VideoChatMicButtonComponent: Component {
|
|||
let isCollapsed: Bool
|
||||
let updateUnmutedStateIsPushToTalk: (Bool?) -> Void
|
||||
let raiseHand: () -> Void
|
||||
let scheduleAction: () -> Void
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
content: Content,
|
||||
isCollapsed: Bool,
|
||||
updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void,
|
||||
raiseHand: @escaping () -> Void
|
||||
raiseHand: @escaping () -> Void,
|
||||
scheduleAction: @escaping () -> Void
|
||||
) {
|
||||
self.call = call
|
||||
self.content = content
|
||||
self.isCollapsed = isCollapsed
|
||||
self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk
|
||||
self.raiseHand = raiseHand
|
||||
self.scheduleAction = scheduleAction
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
|
||||
|
|
@ -245,7 +254,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||
self.beginTrackingTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
if let component = self.component {
|
||||
switch component.content {
|
||||
case .connecting, .unmuted, .raiseHand:
|
||||
case .connecting, .unmuted, .raiseHand, .scheduled:
|
||||
self.beginTrackingWasPushToTalk = false
|
||||
case .muted:
|
||||
self.beginTrackingWasPushToTalk = true
|
||||
|
|
@ -291,6 +300,8 @@ final class VideoChatMicButtonComponent: Component {
|
|||
self.icon.playRandomAnimation()
|
||||
|
||||
component.raiseHand()
|
||||
case .scheduled:
|
||||
component.scheduleAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -322,6 +333,17 @@ final class VideoChatMicButtonComponent: Component {
|
|||
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
||||
case .raiseHand:
|
||||
titleText = "Raise Hand"
|
||||
case let .scheduled(state):
|
||||
switch state {
|
||||
case .start:
|
||||
titleText = "Start Now"
|
||||
case let .toggleSubscription(isSubscribed):
|
||||
if isSubscribed {
|
||||
titleText = "Clear Reminder"
|
||||
} else {
|
||||
titleText = "Set Reminder"
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isEnabled = isEnabled
|
||||
|
||||
|
|
@ -390,12 +412,14 @@ final class VideoChatMicButtonComponent: Component {
|
|||
case .connecting:
|
||||
context.setFillColor(UIColor(white: 0.1, alpha: 1.0).cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
case .muted, .unmuted, .raiseHand:
|
||||
case .muted, .unmuted, .raiseHand, .scheduled:
|
||||
let colors: [UIColor]
|
||||
if case .muted = component.content {
|
||||
colors = [UIColor(rgb: 0x0080FF), UIColor(rgb: 0x00A1FE)]
|
||||
} else if case .raiseHand = component.content {
|
||||
colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)]
|
||||
} else if case .scheduled = component.content {
|
||||
colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0x33C659), UIColor(rgb: 0x0BA8A5)]
|
||||
}
|
||||
|
|
@ -477,10 +501,21 @@ final class VideoChatMicButtonComponent: Component {
|
|||
self.icon.enqueueState(.unmute)
|
||||
case .raiseHand:
|
||||
self.icon.enqueueState(.hand)
|
||||
case let .scheduled(state):
|
||||
switch state {
|
||||
case .start:
|
||||
self.icon.enqueueState(.start)
|
||||
case let .toggleSubscription(isSubscribed):
|
||||
if isSubscribed {
|
||||
self.icon.enqueueState(.unsubscribe)
|
||||
} else {
|
||||
self.icon.enqueueState(.subscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch component.content {
|
||||
case .muted, .unmuted, .raiseHand:
|
||||
case .muted, .unmuted, .raiseHand, .scheduled:
|
||||
let blobSize = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0)).insetBy(dx: -40.0, dy: -40.0).size
|
||||
|
||||
let blobTintTransition: ComponentTransition
|
||||
|
|
@ -512,6 +547,8 @@ final class VideoChatMicButtonComponent: Component {
|
|||
blobsColor = UIColor(rgb: 0x0086FF)
|
||||
} else if case .raiseHand = component.content {
|
||||
blobsColor = UIColor(rgb: 0x914BAD)
|
||||
} else if case .scheduled = component.content {
|
||||
blobsColor = UIColor(rgb: 0x914BAD)
|
||||
} else {
|
||||
blobsColor = UIColor(rgb: 0x33C758)
|
||||
}
|
||||
|
|
@ -528,7 +565,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||
blobView.updateLevel(CGFloat(value), immediately: false)
|
||||
})
|
||||
}
|
||||
case .connecting, .muted, .raiseHand:
|
||||
case .connecting, .muted, .raiseHand, .scheduled:
|
||||
if let audioLevelDisposable = self.audioLevelDisposable {
|
||||
self.audioLevelDisposable = nil
|
||||
audioLevelDisposable.dispose()
|
||||
|
|
@ -561,6 +598,8 @@ final class VideoChatMicButtonComponent: Component {
|
|||
glowColor = UIColor(rgb: 0x0086FF)
|
||||
} else if case .raiseHand = component.content {
|
||||
glowColor = UIColor(rgb: 0x3252EF)
|
||||
} else if case .scheduled = component.content {
|
||||
glowColor = UIColor(rgb: 0x3252EF)
|
||||
} else {
|
||||
glowColor = UIColor(rgb: 0x33C758)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
private let pink = UIColor(rgb: 0xef436c)
|
||||
|
||||
private let latePurple = UIColor(rgb: 0x974aa9)
|
||||
private let latePink = UIColor(rgb: 0xf0436c)
|
||||
|
||||
final class VideoChatScheduledInfoComponent: Component {
|
||||
let timestamp: Int32
|
||||
let strings: PresentationStrings
|
||||
|
||||
init(
|
||||
timestamp: Int32,
|
||||
strings: PresentationStrings
|
||||
) {
|
||||
self.timestamp = timestamp
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatScheduledInfoComponent, rhs: VideoChatScheduledInfoComponent) -> Bool {
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let countdownText = ComponentView<Empty>()
|
||||
private let dateText = ComponentView<Empty>()
|
||||
|
||||
private let countdownContainerView: UIView
|
||||
private let countdownMaskView: UIView
|
||||
private let countdownGradientLayer: SimpleGradientLayer
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private var component: VideoChatScheduledInfoComponent?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.countdownContainerView = UIView()
|
||||
self.countdownMaskView = UIView()
|
||||
|
||||
self.countdownGradientLayer = SimpleGradientLayer()
|
||||
self.countdownGradientLayer.type = .radial
|
||||
self.countdownGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.countdownGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.countdownGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.countdownGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.countdownContainerView.layer.addSublayer(self.countdownGradientLayer)
|
||||
self.addSubview(self.countdownContainerView)
|
||||
|
||||
self.countdownContainerView.mask = self.countdownMaskView
|
||||
|
||||
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
self.updateAnimations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateAnimations() {
|
||||
if let _ = self.countdownGradientLayer.animation(forKey: "movement") {
|
||||
} else {
|
||||
let previousValue = self.countdownGradientLayer.startPoint
|
||||
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45))
|
||||
self.countdownGradientLayer.startPoint = newValue
|
||||
|
||||
CATransaction.begin()
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "startPoint")
|
||||
animation.duration = Double.random(in: 0.8 ..< 1.4)
|
||||
animation.fromValue = previousValue
|
||||
animation.toValue = newValue
|
||||
|
||||
CATransaction.setCompletionBlock { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.hierarchyTrackingLayer.isInHierarchy {
|
||||
self.updateAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
self.countdownGradientLayer.add(animation, forKey: "movement")
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Starts in", font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0)
|
||||
)
|
||||
|
||||
let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970))
|
||||
let countdownText: String
|
||||
if remainingSeconds >= 86400 {
|
||||
countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
|
||||
} else {
|
||||
countdownText = textForTimeout(value: abs(remainingSeconds))
|
||||
/*if remainingSeconds < 0 && !self.isLate {
|
||||
self.isLate = true
|
||||
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
|
||||
}*/
|
||||
}
|
||||
|
||||
let countdownTextSize = self.countdownText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0)
|
||||
)
|
||||
|
||||
let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: component.timestamp, alwaysShowTime: true).string
|
||||
|
||||
let dateTextSize = self.dateText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: dateText, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 5.0
|
||||
let dateSpacing: CGFloat = 5.0
|
||||
|
||||
let contentHeight: CGFloat = titleSize.height + titleSpacing + countdownTextSize.height + dateSpacing + dateTextSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize)
|
||||
let countdownTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - countdownTextSize.width) * 0.5), y: titleFrame.maxY + titleSpacing), size: countdownTextSize)
|
||||
let dateTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dateTextSize.width) * 0.5), y: countdownTextFrame.maxY + dateSpacing), size: dateTextSize)
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
}
|
||||
|
||||
if let countdownTextView = self.countdownText.view {
|
||||
if countdownTextView.superview == nil {
|
||||
self.countdownMaskView.addSubview(countdownTextView)
|
||||
}
|
||||
transition.setFrame(view: countdownTextView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size))
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.countdownContainerView, frame: countdownTextFrame)
|
||||
transition.setFrame(view: self.countdownMaskView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size))
|
||||
transition.setFrame(layer: self.countdownGradientLayer, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size))
|
||||
|
||||
if let dateTextView = self.dateText.view {
|
||||
if dateTextView.superview == nil {
|
||||
self.addSubview(dateTextView)
|
||||
}
|
||||
transition.setPosition(view: dateTextView, position: dateTextFrame.center)
|
||||
dateTextView.bounds = CGRect(origin: CGPoint(), size: dateTextFrame.size)
|
||||
}
|
||||
|
||||
self.updateAnimations()
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ final class VideoChatScreenComponent: Component {
|
|||
let microphoneButton = ComponentView<Empty>()
|
||||
|
||||
let participants = ComponentView<Empty>()
|
||||
var scheduleInfo: ComponentView<Empty>?
|
||||
|
||||
var reconnectedAsEventsDisposable: Disposable?
|
||||
|
||||
|
|
@ -561,6 +562,13 @@ final class VideoChatScreenComponent: Component {
|
|||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let alphaTransition: ComponentTransition
|
||||
if transition.animation.isImmediate {
|
||||
alphaTransition = .immediate
|
||||
} else {
|
||||
alphaTransition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
|
||||
|
|
@ -1058,10 +1066,16 @@ final class VideoChatScreenComponent: Component {
|
|||
}
|
||||
|
||||
let idleTitleStatusText: String
|
||||
if let callState = self.callState, callState.networkState == .connected, let members = self.members {
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount)))
|
||||
if let callState = self.callState {
|
||||
if callState.networkState == .connected, let members = self.members {
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount)))
|
||||
} else if callState.scheduleTimestamp != nil {
|
||||
idleTitleStatusText = "scheduled"
|
||||
} else {
|
||||
idleTitleStatusText = "connecting..."
|
||||
}
|
||||
} else {
|
||||
idleTitleStatusText = "connecting..."
|
||||
idleTitleStatusText = " "
|
||||
}
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
|
|
@ -1324,35 +1338,88 @@ final class VideoChatScreenComponent: Component {
|
|||
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: participantsSize)
|
||||
if let participantsView = self.participants.view {
|
||||
if participantsView.superview == nil {
|
||||
participantsView.layer.allowsGroupOpacity = true
|
||||
self.containerView.addSubview(participantsView)
|
||||
}
|
||||
transition.setFrame(view: participantsView, frame: participantsFrame)
|
||||
var participantsAlpha: CGFloat = 1.0
|
||||
if let callState = self.callState, callState.scheduleTimestamp != nil {
|
||||
participantsAlpha = 0.0
|
||||
}
|
||||
alphaTransition.setAlpha(view: participantsView, alpha: participantsAlpha)
|
||||
}
|
||||
|
||||
if let callState = self.callState, let scheduleTimestamp = callState.scheduleTimestamp {
|
||||
let scheduleInfo: ComponentView<Empty>
|
||||
var scheduleInfoTransition = transition
|
||||
if let current = self.scheduleInfo {
|
||||
scheduleInfo = current
|
||||
} else {
|
||||
scheduleInfoTransition = scheduleInfoTransition.withAnimation(.none)
|
||||
scheduleInfo = ComponentView()
|
||||
self.scheduleInfo = scheduleInfo
|
||||
}
|
||||
let scheduleInfoSize = scheduleInfo.update(
|
||||
transition: scheduleInfoTransition,
|
||||
component: AnyComponent(VideoChatScheduledInfoComponent(
|
||||
timestamp: scheduleTimestamp,
|
||||
strings: environment.strings
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: participantsSize
|
||||
)
|
||||
let scheduleInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scheduleInfoSize)
|
||||
if let scheduleInfoView = scheduleInfo.view {
|
||||
if scheduleInfoView.superview == nil {
|
||||
scheduleInfoView.isUserInteractionEnabled = false
|
||||
self.containerView.addSubview(scheduleInfoView)
|
||||
}
|
||||
scheduleInfoTransition.setFrame(view: scheduleInfoView, frame: scheduleInfoFrame)
|
||||
}
|
||||
} else if let scheduleInfo = self.scheduleInfo {
|
||||
self.scheduleInfo = nil
|
||||
if let scheduleInfoView = scheduleInfo.view {
|
||||
alphaTransition.setAlpha(view: scheduleInfoView, alpha: 0.0, completion: { [weak scheduleInfoView] _ in
|
||||
scheduleInfoView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let micButtonContent: VideoChatMicButtonComponent.Content
|
||||
let actionButtonMicrophoneState: VideoChatActionButtonComponent.MicrophoneState
|
||||
if let callState = self.callState {
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
micButtonContent = .connecting
|
||||
actionButtonMicrophoneState = .connecting
|
||||
case .connected:
|
||||
if let callState = callState.muteState {
|
||||
if callState.canUnmute {
|
||||
if self.isPushToTalkActive {
|
||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
if callState.scheduleTimestamp != nil {
|
||||
let scheduledState: VideoChatMicButtonComponent.ScheduledState
|
||||
if callState.canManageCall {
|
||||
scheduledState = .start
|
||||
} else {
|
||||
scheduledState = .toggleSubscription(isSubscribed: callState.subscribedToScheduled)
|
||||
}
|
||||
micButtonContent = .scheduled(state: scheduledState)
|
||||
actionButtonMicrophoneState = .scheduled
|
||||
} else {
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
micButtonContent = .connecting
|
||||
actionButtonMicrophoneState = .connecting
|
||||
case .connected:
|
||||
if let callState = callState.muteState {
|
||||
if callState.canUnmute {
|
||||
if self.isPushToTalkActive {
|
||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
} else {
|
||||
micButtonContent = .muted
|
||||
actionButtonMicrophoneState = .muted
|
||||
}
|
||||
} else {
|
||||
micButtonContent = .muted
|
||||
actionButtonMicrophoneState = .muted
|
||||
micButtonContent = .raiseHand
|
||||
actionButtonMicrophoneState = .raiseHand
|
||||
}
|
||||
} else {
|
||||
micButtonContent = .raiseHand
|
||||
actionButtonMicrophoneState = .raiseHand
|
||||
micButtonContent = .unmuted(pushToTalk: false)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
}
|
||||
} else {
|
||||
micButtonContent = .unmuted(pushToTalk: false)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1412,6 +1479,23 @@ final class VideoChatScreenComponent: Component {
|
|||
if !callState.raisedHand {
|
||||
component.call.raiseHand()
|
||||
}
|
||||
},
|
||||
scheduleAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
guard callState.scheduleTimestamp != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if callState.canManageCall {
|
||||
component.call.startScheduled()
|
||||
} else {
|
||||
component.call.toggleScheduledSubscription(!callState.subscribedToScheduled)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
|
|
|||
|
|
@ -7097,7 +7097,7 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
|
|||
}
|
||||
}
|
||||
|
||||
private func calculateUseV2(context: AccountContext) -> Bool {
|
||||
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
|
||||
var useV2 = true
|
||||
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
|
||||
useV2 = false
|
||||
|
|
@ -7109,7 +7109,7 @@ private func calculateUseV2(context: AccountContext) -> Bool {
|
|||
}
|
||||
|
||||
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
|
||||
let useV2 = calculateUseV2(context: accountContext)
|
||||
let useV2 = shouldUseV2VideoChatImpl(context: accountContext)
|
||||
|
||||
if useV2 {
|
||||
return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any }
|
||||
|
|
@ -7119,7 +7119,7 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte
|
|||
}
|
||||
|
||||
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController {
|
||||
let useV2 = calculateUseV2(context: accountContext)
|
||||
let useV2 = shouldUseV2VideoChatImpl(context: accountContext)
|
||||
|
||||
if useV2 {
|
||||
return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call)
|
||||
|
|
|
|||
|
|
@ -184,6 +184,12 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me
|
|||
return representation.resource
|
||||
}
|
||||
}
|
||||
|
||||
for alternativeRepresentation in file.alternativeRepresentations {
|
||||
if let result = findMediaResource(media: alternativeRepresentation, previousMedia: previousMedia, resource: resource) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content {
|
||||
if let image = content.image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) {
|
||||
|
|
@ -254,6 +260,12 @@ func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> Telegra
|
|||
return representation.resource
|
||||
}
|
||||
}
|
||||
|
||||
for alternativeRepresentation in file.alternativeRepresentations {
|
||||
if let result = findMediaResourceById(media: alternativeRepresentation, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content {
|
||||
if let image = content.image, let result = findMediaResourceById(media: image, resourceId: resourceId) {
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
|
|||
continuePlayingWithoutSoundOnLostAudioSession: false,
|
||||
storeAfterDownload: nil
|
||||
)
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
|
||||
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)
|
||||
videoNode.canAttachContent = true
|
||||
self.videoNode = videoNode
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
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(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -763,7 +763,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
})
|
||||
}
|
||||
let mediaManager = item.context.sharedContext.mediaManager
|
||||
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: {
|
||||
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: {
|
||||
if let strongSelf = self {
|
||||
if let item = strongSelf.item {
|
||||
if strongSelf.infoBackgroundNode.alpha.isZero {
|
||||
|
|
|
|||
|
|
@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
let loopVideo = updatedVideoFile.isAnimated
|
||||
|
||||
let videoContent: UniversalVideoContent
|
||||
if NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
||||
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
||||
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
|
||||
} else {
|
||||
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
|
||||
|
|
@ -1669,7 +1669,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
|
||||
})
|
||||
}
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||
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
|
||||
if let strongSelf = self {
|
||||
|
|
@ -2162,10 +2162,15 @@ 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) {
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
|
||||
} else {
|
||||
|
|
@ -2264,7 +2269,11 @@ 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 isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max {
|
||||
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 {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -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(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(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)
|
||||
|
||||
self.videoStatusDisposable.set((videoNode.status
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
|||
self.videoNode?.removeFromSupernode()
|
||||
|
||||
let mediaManager = self.context.sharedContext.mediaManager
|
||||
let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.isHidden = true
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
|
|||
self.videoNode?.removeFromSupernode()
|
||||
|
||||
let mediaManager = self.context.sharedContext.mediaManager
|
||||
let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery)
|
||||
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)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
self.videoStartTimestamp = video.representation.startTimestamp
|
||||
self.videoContent = videoContent
|
||||
|
|
|
|||
|
|
@ -7067,7 +7067,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
}
|
||||
|
||||
private func scheduleGroupCall() {
|
||||
self.context.scheduleGroupCall(peerId: self.peerId)
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
self.context.scheduleGroupCall(peerId: self.peerId, parentController: controller)
|
||||
}
|
||||
|
||||
private func createExternalStream(credentialsPromise: Promise<GroupCallStreamCredentials>?) {
|
||||
|
|
|
|||
|
|
@ -200,6 +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,
|
||||
postbox: component.context.account.postbox,
|
||||
audioSession: component.context.sharedContext.mediaManager.audioSession,
|
||||
manager: component.context.sharedContext.mediaManager.universalVideoManager,
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "videosettings_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "videosettingsauto_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "videosettingshd_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "videosettingssd_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -578,8 +578,8 @@ public final class AccountContextImpl: AccountContext {
|
|||
}
|
||||
}
|
||||
|
||||
public func scheduleGroupCall(peerId: PeerId) {
|
||||
let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true)
|
||||
public func scheduleGroupCall(peerId: PeerId, parentController: ViewController) {
|
||||
let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true, parentController: parentController)
|
||||
}
|
||||
|
||||
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) {
|
||||
|
|
|
|||
|
|
@ -1753,7 +1753,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||
if resetOnce {
|
||||
resetOnce = false
|
||||
if count == 0 {
|
||||
UIApplication.shared.applicationIconBadgeNumber = 1
|
||||
//UIApplication.shared.applicationIconBadgeNumber = 1
|
||||
}
|
||||
}
|
||||
UIApplication.shared.applicationIconBadgeNumber = Int(count)
|
||||
|
|
|
|||
|
|
@ -1073,7 +1073,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}
|
||||
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatSchedule, action: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.scheduleGroupCall(peerId: message.id.peerId)
|
||||
strongSelf.context.scheduleGroupCall(peerId: message.id.peerId, parentController: strongSelf)
|
||||
}
|
||||
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
|
|||
|
||||
var playbackEnded: (() -> Void)?
|
||||
|
||||
init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) {
|
||||
init(accountId: AccountRecordId, 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(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true)
|
||||
self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true)
|
||||
self.decoration = decoration
|
||||
|
||||
super.init()
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ final class SharedMediaPlayer {
|
|||
if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem {
|
||||
switch playbackData.source {
|
||||
case let .telegramFile(fileReference, _, _):
|
||||
let videoNode = OverlayInstantVideoNode(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(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
|
||||
mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause))
|
||||
})
|
||||
strongSelf.playbackItem = .instantVideo(videoNode)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ swift_library(
|
|||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/TelegramVoip",
|
||||
"//submodules/ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import AccountContext
|
|||
import PhotoResources
|
||||
import RangeSet
|
||||
import TelegramVoip
|
||||
import ManagedFile
|
||||
|
||||
public final class HLSVideoContent: UniversalVideoContent {
|
||||
public let id: AnyHashable
|
||||
|
|
@ -40,8 +41,8 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||
self.fetchAutomatically = fetchAutomatically
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return HLSVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically)
|
||||
public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return HLSVideoContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically)
|
||||
}
|
||||
|
||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||
|
|
@ -60,7 +61,7 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||
|
||||
private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||
private final class HLSServerSource: SharedHLSServer.Source {
|
||||
let id: UUID
|
||||
let id: String
|
||||
let postbox: Postbox
|
||||
let userLocation: MediaResourceUserLocation
|
||||
let playlistFiles: [Int: FileMediaReference]
|
||||
|
|
@ -68,8 +69,8 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
private var playlistFetchDisposables: [Int: Disposable] = [:]
|
||||
|
||||
init(id: UUID, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) {
|
||||
self.id = id
|
||||
init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) {
|
||||
self.id = "\(UInt64(bitPattern: accountId))_\(fileId)"
|
||||
self.postbox = postbox
|
||||
self.userLocation = userLocation
|
||||
self.playlistFiles = playlistFiles
|
||||
|
|
@ -143,10 +144,11 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
return .never()
|
||||
}
|
||||
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(Data, Int)?, NoError> {
|
||||
guard let file = self.qualityFiles.values.first(where: { $0.media.fileId.id == id }) else {
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
|
||||
guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else {
|
||||
return .single(nil)
|
||||
}
|
||||
let _ = quality
|
||||
guard let size = file.media.size else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
|
@ -156,77 +158,80 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
let mappedRange: Range<Int64> = Int64(range.lowerBound) ..< Int64(range.upperBound)
|
||||
|
||||
return Signal { subscriber in
|
||||
if let fetchResource = postbox.mediaBox.fetchResource {
|
||||
let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource))
|
||||
let params = MediaResourceFetchParameters(
|
||||
tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video),
|
||||
info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true),
|
||||
location: location,
|
||||
contentType: .video,
|
||||
isRandomAccessAllowed: true
|
||||
)
|
||||
|
||||
final class StoredState {
|
||||
let range: Range<Int64>
|
||||
var data: Data
|
||||
var ranges: RangeSet<Int64>
|
||||
|
||||
init(range: Range<Int64>) {
|
||||
self.range = range
|
||||
self.data = Data(count: Int(range.upperBound - range.lowerBound))
|
||||
self.ranges = RangeSet(range)
|
||||
}
|
||||
}
|
||||
let storedState = Atomic<StoredState>(value: StoredState(range: mappedRange))
|
||||
|
||||
return fetchResource(file.media.resource, .single([(mappedRange, .elevated)]), params).start(next: { result in
|
||||
switch result {
|
||||
case let .dataPart(resourceOffset, data, _, _):
|
||||
if !data.isEmpty {
|
||||
let partRange = resourceOffset ..< (resourceOffset + Int64(data.count))
|
||||
var isReady = false
|
||||
storedState.with { storedState in
|
||||
let overlapRange = partRange.clamped(to: storedState.range)
|
||||
guard !overlapRange.isEmpty else {
|
||||
return
|
||||
}
|
||||
let innerRange = (overlapRange.lowerBound - storedState.range.lowerBound) ..< (overlapRange.upperBound - storedState.range.lowerBound)
|
||||
let dataStart = overlapRange.lowerBound - partRange.lowerBound
|
||||
let dataEnd = overlapRange.upperBound - partRange.lowerBound
|
||||
let innerData = data.subdata(in: Int(dataStart) ..< Int(dataEnd))
|
||||
storedState.data.replaceSubrange(Int(innerRange.lowerBound) ..< Int(innerRange.upperBound), with: innerData)
|
||||
storedState.ranges.subtract(RangeSet(overlapRange))
|
||||
if storedState.ranges.isEmpty {
|
||||
isReady = true
|
||||
}
|
||||
}
|
||||
if isReady {
|
||||
subscriber.putNext((storedState.with({ $0.data }), Int(size)))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let queue = postbox.mediaBox.dataQueue
|
||||
return Signal<(TempBoxFile, Range<Int>, Int)?, NoError> { subscriber in
|
||||
guard let fetchResource = postbox.mediaBox.fetchResource else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
/*let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: file, resource: file.media.resource, range: (mappedRange, .elevated)).startStandalone()
|
||||
let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource))
|
||||
let params = MediaResourceFetchParameters(
|
||||
tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video),
|
||||
info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true),
|
||||
location: location,
|
||||
contentType: .video,
|
||||
isRandomAccessAllowed: true
|
||||
)
|
||||
|
||||
let dataDisposable = postbox.mediaBox.resourceData(file.media.resource, size: size, in: mappedRange).startStandalone(next: { value, isComplete in
|
||||
if isComplete {
|
||||
subscriber.putNext((value, Int(size)))
|
||||
subscriber.putCompletion()
|
||||
let completeFile = TempBox.shared.tempFile(fileName: "data")
|
||||
let partialFile = TempBox.shared.tempFile(fileName: "data")
|
||||
let metaFile = TempBox.shared.tempFile(fileName: "data")
|
||||
|
||||
guard let fileContext = MediaBoxFileContextV2Impl(
|
||||
queue: queue,
|
||||
manager: postbox.mediaBox.dataFileManager,
|
||||
storageBox: nil,
|
||||
resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!,
|
||||
path: completeFile.path,
|
||||
partialPath: partialFile.path,
|
||||
metaPath: metaFile.path
|
||||
) else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let fetchDisposable = fileContext.fetched(
|
||||
range: mappedRange,
|
||||
priority: .default,
|
||||
fetch: { intervals in
|
||||
return fetchResource(file.media.resource, intervals, params)
|
||||
},
|
||||
error: { _ in
|
||||
},
|
||||
completed: {
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
#endif
|
||||
|
||||
let dataDisposable = fileContext.data(
|
||||
range: mappedRange,
|
||||
waitUntilAfterInitialFetch: true,
|
||||
next: { result in
|
||||
if result.complete {
|
||||
#if DEBUG
|
||||
let fetchTime = CFAbsoluteTimeGetCurrent() - startTime
|
||||
print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms")
|
||||
#endif
|
||||
subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size)))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return ActionDisposable {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposable.dispose()
|
||||
}*/
|
||||
queue.async {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposable.dispose()
|
||||
fileContext.cancelFullRangeFetches()
|
||||
|
||||
TempBox.shared.dispose(completeFile)
|
||||
TempBox.shared.dispose(metaFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(queue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +249,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
private var initializedStatus = false
|
||||
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
|
||||
private var baseRate: Double = 1.0
|
||||
private var isBuffering = false
|
||||
private var seekId: Int = 0
|
||||
private let _status = ValuePromise<MediaPlayerStatus>()
|
||||
|
|
@ -272,7 +278,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
private let imageNode: TransformImageNode
|
||||
|
||||
private var playerItem: AVPlayerItem?
|
||||
private let player: AVPlayer
|
||||
private var player: AVPlayer?
|
||||
private let playerNode: ASDisplayNode
|
||||
|
||||
private var loadProgressDisposable: Disposable?
|
||||
|
|
@ -296,24 +302,38 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto
|
||||
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
|
||||
init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
|
||||
self.postbox = postbox
|
||||
self.fileReference = fileReference
|
||||
self.approximateDuration = fileReference.media.duration ?? 0.0
|
||||
self.audioSessionManager = audioSessionManager
|
||||
self.userLocation = userLocation
|
||||
self.baseRate = baseRate
|
||||
|
||||
if var dimensions = fileReference.media.dimensions {
|
||||
if let thumbnail = fileReference.media.previewRepresentations.first {
|
||||
let dimensionsVertical = dimensions.width < dimensions.height
|
||||
let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height
|
||||
if dimensionsVertical != thumbnailVertical {
|
||||
dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width)
|
||||
}
|
||||
}
|
||||
self.dimensions = dimensions.cgSize
|
||||
} else {
|
||||
self.dimensions = CGSize(width: 128.0, height: 128.0)
|
||||
}
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
var startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
let player = AVPlayer(playerItem: nil)
|
||||
var player: AVPlayer?
|
||||
player = AVPlayer(playerItem: nil)
|
||||
self.player = player
|
||||
if !enableSound {
|
||||
player.volume = 0.0
|
||||
if #available(iOS 16.0, *) {
|
||||
player?.defaultRate = Float(baseRate)
|
||||
}
|
||||
if !enableSound {
|
||||
player?.volume = 0.0
|
||||
}
|
||||
|
||||
print("Player created in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
|
||||
self.playerNode = ASDisplayNode()
|
||||
self.playerNode.setLayerBlock({
|
||||
|
|
@ -363,10 +383,9 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
}
|
||||
if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys {
|
||||
self.playerSource = HLSServerSource(id: UUID(), postbox: postbox, userLocation: userLocation, playlistFiles: playlistFiles, qualityFiles: qualityFiles)
|
||||
self.playerSource = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: playlistFiles, qualityFiles: qualityFiles)
|
||||
}
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in
|
||||
|
|
@ -386,49 +405,39 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.playerNode)
|
||||
self.player.actionAtItemEnd = .pause
|
||||
self.player?.actionAtItemEnd = .pause
|
||||
|
||||
self.imageNode.imageUpdated = { [weak self] _ in
|
||||
self?._ready.set(.single(Void()))
|
||||
}
|
||||
|
||||
self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil)
|
||||
self.player?.addObserver(self, forKeyPath: "rate", options: [], context: nil)
|
||||
|
||||
self._bufferingStatus.set(.single(nil))
|
||||
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
if let playerSource = self.playerSource {
|
||||
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource)
|
||||
|
||||
let playerItem: AVPlayerItem
|
||||
let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8"
|
||||
#if DEBUG
|
||||
print("HLSVideoContentNode: playing \(assetUrl)")
|
||||
#endif
|
||||
playerItem = AVPlayerItem(url: URL(string: assetUrl)!)
|
||||
print("Player item created in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
playerItem.startsOnFirstEligibleVariant = true
|
||||
}
|
||||
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
self.setPlayerItem(playerItem)
|
||||
print("Set player item in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let playerItem: AVPlayerItem
|
||||
let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8"
|
||||
#if DEBUG
|
||||
print("HLSVideoContentNode: playing \(assetUrl)")
|
||||
#endif
|
||||
playerItem = AVPlayerItem(url: URL(string: assetUrl)!)
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
playerItem.startsOnFirstEligibleVariant = true
|
||||
}
|
||||
|
||||
self.setPlayerItem(playerItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in
|
||||
self?.performActionAtEnd()
|
||||
})
|
||||
|
||||
self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: self.player.currentItem, queue: .main, using: { notification in
|
||||
print("Player Error: \(notification.description)")
|
||||
})
|
||||
self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: self.player.currentItem, queue: .main, using: { notification in
|
||||
print("Player Error: \(notification.description)")
|
||||
})
|
||||
|
||||
self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else {
|
||||
return
|
||||
|
|
@ -441,16 +450,10 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
layer.player = nil
|
||||
})
|
||||
if let currentItem = self.player.currentItem {
|
||||
currentItem.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.player.removeObserver(self, forKeyPath: "rate")
|
||||
if let currentItem = self.player.currentItem {
|
||||
currentItem.removeObserver(self, forKeyPath: "presentationSize")
|
||||
}
|
||||
self.player?.removeObserver(self, forKeyPath: "rate")
|
||||
|
||||
self.setPlayerItem(nil)
|
||||
|
||||
|
|
@ -459,15 +462,16 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
self.loadProgressDisposable?.dispose()
|
||||
self.statusDisposable?.dispose()
|
||||
|
||||
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
|
||||
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
|
||||
}
|
||||
if let didBecomeActiveObserver = self.didBecomeActiveObserver {
|
||||
NotificationCenter.default.removeObserver(didBecomeActiveObserver)
|
||||
}
|
||||
if let willResignActiveObserver = self.willResignActiveObserver {
|
||||
NotificationCenter.default.removeObserver(willResignActiveObserver)
|
||||
}
|
||||
|
||||
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
|
||||
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
|
||||
}
|
||||
if let failureObserverId = self.failureObserverId {
|
||||
NotificationCenter.default.removeObserver(failureObserverId)
|
||||
}
|
||||
|
|
@ -486,14 +490,53 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||
playerItem.removeObserver(self, forKeyPath: "playbackBufferFull")
|
||||
playerItem.removeObserver(self, forKeyPath: "status")
|
||||
if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver {
|
||||
NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver)
|
||||
self.playerItemFailedToPlayToEndTimeObserver = nil
|
||||
}
|
||||
playerItem.removeObserver(self, forKeyPath: "presentationSize")
|
||||
}
|
||||
|
||||
if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver {
|
||||
self.playerItemFailedToPlayToEndTimeObserver = nil
|
||||
NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver)
|
||||
}
|
||||
|
||||
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
|
||||
self.didPlayToEndTimeObserver = nil
|
||||
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
|
||||
}
|
||||
if let failureObserverId = self.failureObserverId {
|
||||
self.failureObserverId = nil
|
||||
NotificationCenter.default.removeObserver(failureObserverId)
|
||||
}
|
||||
if let errorObserverId = self.errorObserverId {
|
||||
self.errorObserverId = nil
|
||||
NotificationCenter.default.removeObserver(errorObserverId)
|
||||
}
|
||||
|
||||
self.playerItem = item
|
||||
|
||||
if let item {
|
||||
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil, using: { [weak self] notification in
|
||||
self?.performActionAtEnd()
|
||||
})
|
||||
|
||||
self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: item, queue: .main, using: { notification in
|
||||
#if DEBUG
|
||||
print("Player Error: \(notification.description)")
|
||||
#endif
|
||||
})
|
||||
self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: item, queue: .main, using: { [weak item] notification in
|
||||
if let item {
|
||||
let event = item.errorLog()?.events.last
|
||||
if let event {
|
||||
let _ = event
|
||||
#if DEBUG
|
||||
print("Player Error: \(event.errorComment ?? "<no comment>")")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
})
|
||||
item.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil)
|
||||
}
|
||||
|
||||
if let playerItem = self.playerItem {
|
||||
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
|
||||
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
|
||||
|
|
@ -507,23 +550,26 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
})
|
||||
}
|
||||
|
||||
self.player.replaceCurrentItem(with: self.playerItem)
|
||||
self.player?.replaceCurrentItem(with: self.playerItem)
|
||||
}
|
||||
|
||||
private func updateStatus() {
|
||||
let isPlaying = !self.player.rate.isZero
|
||||
guard let player = self.player else {
|
||||
return
|
||||
}
|
||||
let isPlaying = !player.rate.isZero
|
||||
let status: MediaPlayerPlaybackStatus
|
||||
if self.isBuffering {
|
||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||
} else {
|
||||
status = isPlaying ? .playing : .paused
|
||||
}
|
||||
var timestamp = self.player.currentTime().seconds
|
||||
var timestamp = player.currentTime().seconds
|
||||
if timestamp.isFinite && !timestamp.isNaN {
|
||||
} else {
|
||||
timestamp = 0.0
|
||||
}
|
||||
self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: Double(self.player.rate), seekId: self.seekId, status: status, soundEnabled: true)
|
||||
self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, seekId: self.seekId, status: status, soundEnabled: true)
|
||||
self._status.set(self.statusValue)
|
||||
|
||||
if case .playing = status {
|
||||
|
|
@ -543,9 +589,11 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "rate" {
|
||||
let isPlaying = !self.player.rate.isZero
|
||||
if isPlaying {
|
||||
self.isBuffering = false
|
||||
if let player = self.player {
|
||||
let isPlaying = !player.rate.isZero
|
||||
if isPlaying {
|
||||
self.isBuffering = false
|
||||
}
|
||||
}
|
||||
self.updateStatus()
|
||||
} else if keyPath == "playbackBufferEmpty" {
|
||||
|
|
@ -555,7 +603,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
self.isBuffering = false
|
||||
self.updateStatus()
|
||||
} else if keyPath == "presentationSize" {
|
||||
if let currentItem = self.player.currentItem {
|
||||
if let currentItem = self.player?.currentItem {
|
||||
print("Presentation size: \(Int(currentItem.presentationSize.height))")
|
||||
}
|
||||
}
|
||||
|
|
@ -573,42 +621,57 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let makeImageLayout = self.imageNode.asyncLayout()
|
||||
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
applyImageLayout()
|
||||
if let dimensions = self.dimensions {
|
||||
let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0))
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear))
|
||||
applyLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func play() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if !self.initializedStatus {
|
||||
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
|
||||
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
|
||||
}
|
||||
if !self.hasAudioSession {
|
||||
if self.player.volume != 0.0 {
|
||||
if self.player?.volume != 0.0 {
|
||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
|
||||
self?.hasAudioSession = true
|
||||
self?.player.play()
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.hasAudioSession = true
|
||||
self.player?.play()
|
||||
}, deactivate: { [weak self] _ in
|
||||
self?.hasAudioSession = false
|
||||
self?.player.pause()
|
||||
guard let self else {
|
||||
return .complete()
|
||||
}
|
||||
self.hasAudioSession = false
|
||||
self.player?.pause()
|
||||
|
||||
return .complete()
|
||||
}))
|
||||
} else {
|
||||
self.player.play()
|
||||
self.player?.play()
|
||||
}
|
||||
} else {
|
||||
self.player.play()
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
self.player.pause()
|
||||
self.player?.pause()
|
||||
}
|
||||
|
||||
func togglePlayPause() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if self.player.rate.isZero {
|
||||
|
||||
guard let player = self.player else {
|
||||
return
|
||||
}
|
||||
|
||||
if player.rate.isZero {
|
||||
self.play()
|
||||
} else {
|
||||
self.pause()
|
||||
|
|
@ -621,15 +684,15 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
if !self.hasAudioSession {
|
||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
|
||||
self?.hasAudioSession = true
|
||||
self?.player.volume = 1.0
|
||||
self?.player?.volume = 1.0
|
||||
}, deactivate: { [weak self] _ in
|
||||
self?.hasAudioSession = false
|
||||
self?.player.pause()
|
||||
self?.player?.pause()
|
||||
return .complete()
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
self.player.volume = 0.0
|
||||
self.player?.volume = 0.0
|
||||
self.hasAudioSession = false
|
||||
self.audioSessionDisposable.set(nil)
|
||||
}
|
||||
|
|
@ -638,16 +701,16 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
func seek(_ timestamp: Double) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
self.seekId += 1
|
||||
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
|
||||
self.player?.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
self.player.volume = 1.0
|
||||
self.player?.volume = 1.0
|
||||
self.play()
|
||||
}
|
||||
|
||||
func setSoundMuted(soundMuted: Bool) {
|
||||
self.player.volume = soundMuted ? 0.0 : 1.0
|
||||
self.player?.volume = soundMuted ? 0.0 : 1.0
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||
|
|
@ -657,7 +720,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
|
||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
self.player.volume = 0.0
|
||||
self.player?.volume = 0.0
|
||||
self.hasAudioSession = false
|
||||
self.audioSessionDisposable.set(nil)
|
||||
}
|
||||
|
|
@ -666,13 +729,23 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
|
||||
func setBaseRate(_ baseRate: Double) {
|
||||
self.player.rate = Float(baseRate)
|
||||
guard let player = self.player else {
|
||||
return
|
||||
}
|
||||
self.baseRate = baseRate
|
||||
if #available(iOS 16.0, *) {
|
||||
player.defaultRate = Float(baseRate)
|
||||
}
|
||||
if player.rate != 0.0 {
|
||||
player.rate = Float(baseRate)
|
||||
}
|
||||
self.updateStatus()
|
||||
}
|
||||
|
||||
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||
self.preferredVideoQuality = videoQuality
|
||||
|
||||
guard let currentItem = self.player.currentItem else {
|
||||
guard let currentItem = self.player?.currentItem else {
|
||||
return
|
||||
}
|
||||
guard let playerSource = self.playerSource else {
|
||||
|
|
@ -694,7 +767,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||
}
|
||||
|
||||
func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? {
|
||||
guard let currentItem = self.player.currentItem else {
|
||||
guard let currentItem = self.player?.currentItem else {
|
||||
return nil
|
||||
}
|
||||
guard let playerSource = self.playerSource else {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||
self.hasSentFramesToDisplay = hasSentFramesToDisplay
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP
|
|||
private var statusDisposable: Disposable?
|
||||
private var status: MediaPlayerStatus?
|
||||
|
||||
public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal<Bool, NoError> = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) {
|
||||
public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal<Bool, NoError> = .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(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay)
|
||||
self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay)
|
||||
self.decoration = decoration
|
||||
|
||||
super.init()
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ public final class PlatformVideoContent: UniversalVideoContent {
|
|||
self.fetchAutomatically = fetchAutomatically
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
public func makeContentNode(accountId: AccountRecordId, 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public final class SystemVideoContent: UniversalVideoContent {
|
|||
self.duration = duration
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
public func makeContentNode(accountId: AccountRecordId, 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public final class WebEmbedVideoContent: UniversalVideoContent {
|
|||
self.openUrl = openUrl
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
public func makeContentNode(accountId: AccountRecordId, 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ swift_library(
|
|||
"//submodules/TgVoip:TgVoip",
|
||||
"//submodules/TgVoipWebrtc:TgVoipWebrtc",
|
||||
"//submodules/FFMpegBinding",
|
||||
"//submodules/ManagedFile",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import TelegramCore
|
|||
import Network
|
||||
import Postbox
|
||||
import FFMpegBinding
|
||||
|
||||
import ManagedFile
|
||||
|
||||
@available(iOS 12.0, macOS 14.0, *)
|
||||
public final class WrappedMediaStreamingContext {
|
||||
|
|
@ -275,7 +275,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
|
|||
}
|
||||
}
|
||||
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(Data, Int)?, NoError> {
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
|
@ -285,8 +285,8 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
|
|||
private let impl: QueueLocalObject<Impl>
|
||||
private var hlsServerDisposable: Disposable?
|
||||
|
||||
public var id: UUID {
|
||||
return self.internalId
|
||||
public var id: String {
|
||||
return self.internalId.uuidString
|
||||
}
|
||||
|
||||
public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) {
|
||||
|
|
@ -296,7 +296,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
|
|||
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
|
||||
})
|
||||
|
||||
self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self)
|
||||
self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self, completion: {})
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -331,7 +331,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
|
|||
}
|
||||
}
|
||||
|
||||
public func fileData(id: Int64, range: Range<Int>) -> Signal<(Data, Int)?, NoError> {
|
||||
public func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
impl.fileData(id: id, range: range).start(next: subscriber.putNext)
|
||||
}
|
||||
|
|
@ -339,12 +339,12 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
|
|||
}
|
||||
|
||||
public protocol SharedHLSServerSource: AnyObject {
|
||||
var id: UUID { get }
|
||||
var id: String { get }
|
||||
|
||||
func masterPlaylistData() -> Signal<String, NoError>
|
||||
func playlistData(quality: Int) -> Signal<String, NoError>
|
||||
func partData(index: Int, quality: Int) -> Signal<Data?, NoError>
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(Data, Int)?, NoError>
|
||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError>
|
||||
}
|
||||
|
||||
@available(iOS 12.0, macOS 14.0, *)
|
||||
|
|
@ -387,14 +387,66 @@ public final class SharedHLSServer {
|
|||
private var listener: NWListener?
|
||||
|
||||
private var sourceReferences = Bag<SourceReference>()
|
||||
private var referenceCheckTimer: SwiftSignalKit.Timer?
|
||||
private var shutdownTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(queue: Queue, port: UInt16) {
|
||||
self.queue = queue
|
||||
self.port = NWEndpoint.Port(rawValue: port)!
|
||||
self.start()
|
||||
}
|
||||
|
||||
func start() {
|
||||
deinit {
|
||||
self.referenceCheckTimer?.invalidate()
|
||||
self.shutdownTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func updateNeedsListener() {
|
||||
var isEmpty = true
|
||||
for item in self.sourceReferences.copyItems() {
|
||||
if let _ = item.source {
|
||||
isEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isEmpty {
|
||||
if self.listener != nil {
|
||||
if self.shutdownTimer == nil {
|
||||
self.shutdownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.shutdownTimer = nil
|
||||
self.stopListener()
|
||||
}, queue: self.queue)
|
||||
self.shutdownTimer?.start()
|
||||
}
|
||||
}
|
||||
if let referenceCheckTimer = self.referenceCheckTimer {
|
||||
self.referenceCheckTimer = nil
|
||||
referenceCheckTimer.invalidate()
|
||||
}
|
||||
} else {
|
||||
if let shutdownTimer = self.shutdownTimer {
|
||||
self.shutdownTimer = nil
|
||||
shutdownTimer.invalidate()
|
||||
}
|
||||
if self.listener == nil {
|
||||
self.startListener()
|
||||
}
|
||||
if self.referenceCheckTimer == nil {
|
||||
self.referenceCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateNeedsListener()
|
||||
}, queue: self.queue)
|
||||
self.referenceCheckTimer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startListener() {
|
||||
let listener: NWListener
|
||||
do {
|
||||
listener = try NWListener(using: .tcp, on: self.port)
|
||||
|
|
@ -411,8 +463,8 @@ public final class SharedHLSServer {
|
|||
self.handleConnection(connection: connection)
|
||||
}
|
||||
|
||||
listener.stateUpdateHandler = { [weak self] state in
|
||||
guard let self else {
|
||||
listener.stateUpdateHandler = { [weak self, weak listener] state in
|
||||
guard let self, let listener else {
|
||||
return
|
||||
}
|
||||
switch state {
|
||||
|
|
@ -420,9 +472,9 @@ public final class SharedHLSServer {
|
|||
Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)")
|
||||
case let .failed(error):
|
||||
Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)")
|
||||
self.listener?.cancel()
|
||||
listener.cancel()
|
||||
|
||||
self.listener?.start(queue: self.queue.queue)
|
||||
listener.start(queue: self.queue.queue)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -431,9 +483,17 @@ public final class SharedHLSServer {
|
|||
listener.start(queue: self.queue.queue)
|
||||
}
|
||||
|
||||
private func stopListener() {
|
||||
guard let listener = self.listener else {
|
||||
return
|
||||
}
|
||||
self.listener = nil
|
||||
listener.cancel()
|
||||
}
|
||||
|
||||
private func handleConnection(connection: NWConnection) {
|
||||
connection.start(queue: self.queue.queue)
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] data, _, isComplete, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -488,10 +548,7 @@ public final class SharedHLSServer {
|
|||
self.sendErrorAndClose(connection: connection, error: .notFound)
|
||||
return
|
||||
}
|
||||
guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])
|
||||
guard let source = self.sourceReferences.copyItems().first(where: { $0.source?.id == streamId })?.source else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
|
|
@ -581,13 +638,14 @@ public final class SharedHLSServer {
|
|||
}
|
||||
let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|
||||
|> deliverOn(self.queue)
|
||||
//|> timeout(5.0, queue: self.queue, alternate: .single(nil))
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let (data, totalSize) = result {
|
||||
self.sendResponseAndClose(connection: connection, data: data, range: requestRange, totalSize: totalSize)
|
||||
if let (tempFile, tempFileRange, totalSize) = result {
|
||||
self.sendResponseFileAndClose(connection: connection, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize)
|
||||
} else {
|
||||
self.sendErrorAndClose(connection: connection, error: .internalServerError)
|
||||
}
|
||||
|
|
@ -628,9 +686,62 @@ public final class SharedHLSServer {
|
|||
})
|
||||
}
|
||||
|
||||
func registerPlayer(source: SharedHLSServerSource) -> Disposable {
|
||||
private static func sendRemainingFileRange(queue: Queue, connection: NWConnection, tempFile: TempBoxFile, managedFile: ManagedFile, remainingRange: Range<Int>, fileSize: Int) -> Void {
|
||||
let blockSize = 256 * 1024
|
||||
|
||||
let clippedLowerBound = min(remainingRange.lowerBound, fileSize)
|
||||
var clippedUpperBound = min(remainingRange.upperBound, fileSize)
|
||||
clippedUpperBound = min(clippedUpperBound, clippedLowerBound + blockSize)
|
||||
|
||||
if clippedUpperBound == clippedLowerBound {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
connection.cancel()
|
||||
} else {
|
||||
let _ = managedFile.seek(position: Int64(clippedLowerBound))
|
||||
let data = managedFile.readData(count: Int(clippedUpperBound - clippedLowerBound))
|
||||
let nextRange = clippedUpperBound ..< remainingRange.upperBound
|
||||
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
queue.async {
|
||||
if let error {
|
||||
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
|
||||
connection.cancel()
|
||||
TempBox.shared.dispose(tempFile)
|
||||
} else {
|
||||
sendRemainingFileRange(queue: queue, connection: connection, tempFile: tempFile, managedFile: managedFile, remainingRange: nextRange, fileSize: fileSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func sendResponseFileAndClose(connection: NWConnection, file: TempBoxFile, fileRange: Range<Int>, range: Range<Int>, totalSize: Int) {
|
||||
let queue = self.queue
|
||||
|
||||
guard let managedFile = ManagedFile(queue: nil, path: file.path, mode: .read), let fileSize = managedFile.getSize() else {
|
||||
self.sendErrorAndClose(connection: connection, error: .internalServerError)
|
||||
TempBox.shared.dispose(file)
|
||||
return
|
||||
}
|
||||
|
||||
var responseHeaders = "HTTP/1.1 200 OK\r\n"
|
||||
responseHeaders.append("Content-Length: \(fileRange.upperBound - fileRange.lowerBound)\r\n")
|
||||
responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n")
|
||||
responseHeaders.append("Content-Type: application/octet-stream\r\n")
|
||||
responseHeaders.append("Connection: close\r\n")
|
||||
responseHeaders.append("Access-Control-Allow-Origin: *\r\n")
|
||||
responseHeaders.append("\r\n")
|
||||
|
||||
connection.send(content: responseHeaders.data(using: .utf8)!, completion: .contentProcessed({ _ in }))
|
||||
|
||||
Impl.sendRemainingFileRange(queue: queue, connection: connection, tempFile: file, managedFile: managedFile, remainingRange: fileRange, fileSize: Int(fileSize))
|
||||
}
|
||||
|
||||
func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable {
|
||||
let queue = self.queue
|
||||
let index = self.sourceReferences.add(SourceReference(source: source))
|
||||
self.updateNeedsListener()
|
||||
completion()
|
||||
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
|
|
@ -638,6 +749,7 @@ public final class SharedHLSServer {
|
|||
return
|
||||
}
|
||||
self.sourceReferences.remove(index)
|
||||
self.updateNeedsListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -655,11 +767,11 @@ public final class SharedHLSServer {
|
|||
})
|
||||
}
|
||||
|
||||
public func registerPlayer(source: SharedHLSServerSource) -> Disposable {
|
||||
public func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.registerPlayer(source: source))
|
||||
disposable.set(impl.registerPlayer(source: source, completion: completion))
|
||||
}
|
||||
|
||||
return disposable
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
let mediaManager = item.context.sharedContext.mediaManager
|
||||
|
||||
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
|
||||
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 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue