diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 70daf23f13..33a38f754a 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -157,7 +157,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { content = NativeVideoContent(id: .message(message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath) } else { - content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 36a6a8e203..75bda68d39 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -18,7 +18,7 @@ import AppBundle public enum UniversalVideoGalleryItemContentInfo { case message(Message) - case webPage(TelegramMediaWebpage, Media) + case webPage(TelegramMediaWebpage, Media, ((@escaping () -> GalleryTransitionArguments?, NavigationController?, (ViewController, Any?) -> Void) -> Void)?) } public class UniversalVideoGalleryItem: GalleryItem { @@ -108,7 +108,7 @@ public class UniversalVideoGalleryItem: GalleryItem { } } } - } else if case let .webPage(webPage, media) = contentInfo, let file = media as? TelegramMediaFile { + } else if case let .webPage(webPage, media, _) = contentInfo, let file = media as? TelegramMediaFile { if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: .webPage(webPage: WebpageReference(webPage), media: file)) { return (0, item) } @@ -470,6 +470,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var disablePictureInPicture = false var disablePlayerControls = false + var forceEnablePiP = false var isAnimated = false if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated @@ -487,6 +488,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { default: break } + } else if let _ = item.content as? PlatformVideoContent { + disablePlayerControls = true + forceEnablePiP = true } if let videoNode = self.videoNode { @@ -511,7 +515,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.playOnContentOwnership = false strongSelf.initiallyActivated = true strongSelf.skipInitialPause = true - strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : .stop) + if let item = strongSelf.item, let _ = item.content as? PlatformVideoContent { + strongSelf.videoNode?.play() + } else { + strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : .stop) + } } } } @@ -698,7 +706,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) barButtonItems.append(rightBarButtonItem) } - if !isAnimated && !disablePlayerControls && !disablePictureInPicture { + if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) barButtonItems.append(rightBarButtonItem) self.hasPictureInPicture = true @@ -725,9 +733,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message): self.footerContentNode.setMessage(message) - case let .webPage(webPage, media): + case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) - break } } self.footerContentNode.setup(origin: item.originData, caption: item.caption) @@ -757,7 +764,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } private func shouldAutoplayOnCentrality() -> Bool { -// !self.initiallyActivated if let item = self.item, let content = item.content as? NativeVideoContent { var isLocal = false if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { @@ -772,6 +778,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if isLocal || isStreamable { return true } + } else if let item = self.item, let _ = item.content as? PlatformVideoContent { + return true } return false } @@ -1335,8 +1343,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return nil })) - case .webPage: - break + case let .webPage(_, _, expandFromPip): + if let expandFromPip = expandFromPip, let baseNavigationController = baseNavigationController { + expandFromPip({ [weak overlayNode] in + if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { + return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in + return (overlayNode?.view.snapshotContentTree(), nil) + }), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in + guard let context = context, let overlayNode = overlayNode else { + return + } + if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) { + overlaySupernode?.view.addSubview(view) + } + overlayNode.canAttachContent = false + }) + } + return nil + }, baseNavigationController, { [weak baseNavigationController] c, a in + (baseNavigationController?.topViewController as? ViewController)?.present(c, in: .window(.root), with: a) + }) + } } } if customUnembedWhenPortrait(overlayNode) { @@ -1398,8 +1425,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return nil })) - case .webPage: - break + case let .webPage(_, _, expandFromPip): + if let expandFromPip = expandFromPip, let baseNavigationController = baseNavigationController { + expandFromPip({ [weak overlayNode] in + if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { + return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in + return (overlayNode?.view.snapshotContentTree(), nil) + }), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in + guard let context = context, let overlayNode = overlayNode else { + return + } + if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) { + overlaySupernode?.view.addSubview(view) + } + overlayNode.canAttachContent = false + }) + } + return nil + }, baseNavigationController, { [weak baseNavigationController] c, a in + (baseNavigationController?.topViewController as? ViewController)?.present(c, in: .window(.root), with: a) + }) + } } } context.sharedContext.mediaManager.setOverlayVideoNode(overlayNode) diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 7a7cdc3a09..b135b65242 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -113,7 +113,7 @@ public struct InstantPageGalleryEntry: Equatable { nativeId = .instantPage(self.pageId, file.fileId) } - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) } else { var representations: [TelegramMediaImageRepresentation] = [] representations.append(contentsOf: file.previewRepresentations) @@ -124,10 +124,24 @@ public struct InstantPageGalleryEntry: Equatable { return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } } else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content { - if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) { - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + if webpageContent.url.hasSuffix(".m3u8") { + let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), content: .url(webpageContent.url), streamVideo: true, loopVideo: false) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in + let gallery = InstantPageGalleryController(context: context, webPage: webPage, entries: [self], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in + if let navigationController = navigationController { + navigationController.replaceTopController(controller, animated: false, ready: ready) + } + }, baseNavigationController: navigationController) + present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in + return makeArguments() + })) + }), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) } else { - preconditionFailure() + if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) { + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + } else { + preconditionFailure() + } } } else { preconditionFailure() @@ -169,6 +183,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemRightBarButtonItem = Promise() + private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>(nil) private let centralItemNavigationStyle = Promise() private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); @@ -239,8 +254,15 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable self?.navigationItem.titleView = titleView })) - self.centralItemAttributesDisposable.add(self.centralItemRightBarButtonItem.get().start(next: { [weak self] rightBarButtonItem in - self?.navigationItem.rightBarButtonItem = rightBarButtonItem + self.centralItemAttributesDisposable.add(combineLatest(self.centralItemRightBarButtonItem.get(), self.centralItemRightBarButtonItems.get()).start(next: { [weak self] rightBarButtonItem, rightBarButtonItems in + if let rightBarButtonItem = rightBarButtonItem { + self?.navigationItem.rightBarButtonItem = rightBarButtonItem + } else if let rightBarButtonItems = rightBarButtonItems { + self?.navigationItem.rightBarButtonItems = rightBarButtonItems + } else { + self?.navigationItem.rightBarButtonItem = nil + self?.navigationItem.rightBarButtonItems = nil + } })) self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in @@ -362,6 +384,11 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable self?.presentingViewController?.dismiss(animated: false, completion: nil) } + self.galleryNode.completeCustomDismiss = { [weak self] in + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.galleryNode.pager.replaceItems(self.entries.map({ $0.item(context: self.context, webPage: self.webPage, message: self.message, presentationData: self.presentationData, fromPlayingVideo: self.fromPlayingVideo, landscape: self.landscape, openUrl: self.innerOpenUrl, openUrlOptions: self.openUrlOptions) }), centralItemIndex: self.centralEntryIndex) @@ -376,6 +403,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable strongSelf.centralItemTitle.set(node.title()) strongSelf.centralItemTitleView.set(node.titleView()) strongSelf.centralItemRightBarButtonItem.set(node.rightBarButtonItem()) + strongSelf.centralItemRightBarButtonItems.set(node.rightBarButtonItems()) strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } @@ -386,6 +414,11 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable } } + let baseNavigationController = self.baseNavigationController + self.galleryNode.baseNavigationController = { [weak baseNavigationController] in + return baseNavigationController + } + let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in self?.didSetReady = true } @@ -401,6 +434,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem()) + self.centralItemRightBarButtonItems.set(centralItemNode.rightBarButtonItems()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 92e090e841..d06d8a8810 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -70,6 +70,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { case photoPreview(PresentationTheme, Bool) case knockoutWallpaper(PresentationTheme, Bool) case alternativeFolderTabs(Bool) + case playerEmbedding(Bool) + case playlistPlayback(Bool) case videoCalls(Bool) case videoCallsInfo(PresentationTheme, String) case hostInfo(PresentationTheme, String) @@ -85,7 +87,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs: + case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback: return DebugControllerSection.experiments.rawValue case .videoCalls, .videoCallsInfo: return DebugControllerSection.videoExperiments.rawValue @@ -142,14 +144,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 22 case .alternativeFolderTabs: return 23 - case .videoCalls: + case .playerEmbedding: return 24 - case .videoCallsInfo: + case .playlistPlayback: return 25 - case .hostInfo: + case .videoCalls: return 26 - case .versionInfo: + case .videoCallsInfo: return 27 + case .hostInfo: + return 28 + case .versionInfo: + return 29 } } @@ -547,6 +553,26 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .playerEmbedding(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings + settings.playerEmbedding = value + return settings + }) + }).start() + }) + case let .playlistPlayback(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Playlist Playback", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings + settings.playlistPlayback = value + return settings + }) + }).start() + }) case let .videoCalls(value): return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Feature", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -602,6 +628,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS //entries.append(.photoPreview(presentationData.theme, experimentalSettings.chatListPhotos)) entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) entries.append(.alternativeFolderTabs(experimentalSettings.foldersTabAtBottom)) + entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) + entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.videoCalls(experimentalSettings.videoCalls)) entries.append(.videoCallsInfo(presentationData.theme, "Enables experimental transmission of electromagnetic radiation synchronized with pressure waves. Needs to be enabled on both sides.")) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 36927aa312..fcec260cc9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -376,7 +376,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .inline: navigationBarPresentationData = nil default: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: false, hideBadge: false) + navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) } super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) @@ -2806,9 +2806,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let navigationBarTheme: NavigationBarTheme if self.hasEmbeddedTitleContent { - navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true, hideBadge: true) + navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: true) } else { - navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: false, hideBadge: false) + navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) } self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) @@ -8452,6 +8452,58 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) { self.commitPurposefulAction() + if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback { + if url.hasSuffix(".m3u8") { + let navigationController = self.navigationController as? NavigationController + + let webPage = TelegramMediaWebpage( + webpageId: MediaId(namespace: 0, id: 0), + content: .Loaded(TelegramMediaWebpageLoadedContent( + url: url, + displayUrl: url, + hash: 0, + type: "video", + websiteName: nil, + title: nil, + text: nil, + embedUrl: url, + embedType: "video", + embedSize: nil, + duration: nil, + author: nil, + image: nil, + file: nil, + attributes: [], + instantPage: nil + )) + ) + let entry = InstantPageGalleryEntry( + index: 0, + pageId: webPage.webpageId, + media: InstantPageMedia( + index: 0, + media: webPage, + url: nil, + caption: nil, + credit: nil + ), + caption: nil, + credit: nil, + location: nil + ) + + let gallery = InstantPageGalleryController(context: context, webPage: webPage, entries: [entry], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in + if let navigationController = navigationController { + navigationController.replaceTopController(controller, animated: false, ready: ready) + } + }, baseNavigationController: navigationController) + self.present(gallery, in: .window(.root), with: InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in + return nil + })) + return; + } + } + openUserGeneratedUrl(context: self.context, url: url, concealed: concealed, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 7f26f41e83..5697d193b8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -563,9 +563,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.navigateButtons) self.addSubnode(self.navigationBarBackroundNode) - self.navigationBarBackroundNode.isHidden = true self.addSubnode(self.navigationBarSeparatorNode) - self.navigationBarSeparatorNode.isHidden = true + if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { + self.navigationBarBackroundNode.isHidden = true + self.navigationBarSeparatorNode.isHidden = true + } self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) @@ -2691,7 +2693,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func updateEmbeddedTitlePeekContent(content: NavigationControllerDropContent?) { - return; + if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { + return + } guard let (_, navigationHeight) = self.validLayout else { return @@ -2719,7 +2723,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var updateHasEmbeddedTitleContent: (() -> Void)? func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool { - return false; + if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { + return false + } guard let (_, navigationHeight) = self.validLayout else { return false diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 668607bcdc..53d7c35a41 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -10,12 +10,34 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { public var knockoutWallpaper: Bool public var foldersTabAtBottom: Bool public var videoCalls: Bool + public var playerEmbedding: Bool + public var playlistPlayback: Bool public static var defaultSettings: ExperimentalUISettings { - return ExperimentalUISettings(keepChatNavigationStack: false, skipReadHistory: false, crashOnLongQueries: false, chatListPhotos: false, knockoutWallpaper: false, foldersTabAtBottom: false, videoCalls: false) + return ExperimentalUISettings( + keepChatNavigationStack: false, + skipReadHistory: false, + crashOnLongQueries: false, + chatListPhotos: false, + knockoutWallpaper: false, + foldersTabAtBottom: false, + videoCalls: false, + playerEmbedding: false, + playlistPlayback: false + ) } - public init(keepChatNavigationStack: Bool, skipReadHistory: Bool, crashOnLongQueries: Bool, chatListPhotos: Bool, knockoutWallpaper: Bool, foldersTabAtBottom: Bool, videoCalls: Bool) { + public init( + keepChatNavigationStack: Bool, + skipReadHistory: Bool, + crashOnLongQueries: Bool, + chatListPhotos: Bool, + knockoutWallpaper: Bool, + foldersTabAtBottom: Bool, + videoCalls: Bool, + playerEmbedding: Bool, + playlistPlayback: Bool + ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory self.crashOnLongQueries = crashOnLongQueries @@ -23,6 +45,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { self.knockoutWallpaper = knockoutWallpaper self.foldersTabAtBottom = foldersTabAtBottom self.videoCalls = videoCalls + self.playerEmbedding = playerEmbedding + self.playlistPlayback = playlistPlayback } public init(decoder: PostboxDecoder) { @@ -33,6 +57,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { self.knockoutWallpaper = decoder.decodeInt32ForKey("knockoutWallpaper", orElse: 0) != 0 self.foldersTabAtBottom = decoder.decodeInt32ForKey("foldersTabAtBottom", orElse: 0) != 0 self.videoCalls = decoder.decodeInt32ForKey("videoCalls", orElse: 0) != 0 + self.playerEmbedding = decoder.decodeInt32ForKey("playerEmbedding", orElse: 0) != 0 + self.playlistPlayback = decoder.decodeInt32ForKey("playlistPlayback", orElse: 0) != 0 } public func encode(_ encoder: PostboxEncoder) { @@ -43,6 +69,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { encoder.encodeInt32(self.knockoutWallpaper ? 1 : 0, forKey: "knockoutWallpaper") encoder.encodeInt32(self.foldersTabAtBottom ? 1 : 0, forKey: "foldersTabAtBottom") encoder.encodeInt32(self.videoCalls ? 1 : 0, forKey: "videoCalls") + encoder.encodeInt32(self.playerEmbedding ? 1 : 0, forKey: "playerEmbedding") + encoder.encodeInt32(self.playlistPlayback ? 1 : 0, forKey: "playlistPlayback") } public func isEqual(to: PreferencesEntry) -> Bool { diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 0688178c82..4f078b082c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -46,9 +46,32 @@ public enum PlatformVideoContentId: Hashable { } public final class PlatformVideoContent: UniversalVideoContent { + public enum Content { + case file(FileMediaReference) + case url(String) + + var duration: Int32? { + switch self { + case let .file(file): + return file.media.duration + case .url: + return nil + } + } + + var dimensions: PixelDimensions? { + switch self { + case let .file(file): + return file.media.dimensions + case .url: + return PixelDimensions(width: 480, height: 300) + } + } + } + public let id: AnyHashable let nativeId: PlatformVideoContentId - let fileReference: FileMediaReference + let content: Content public let dimensions: CGSize public let duration: Int32 let streamVideo: Bool @@ -57,12 +80,12 @@ public final class PlatformVideoContent: UniversalVideoContent { let baseRate: Double let fetchAutomatically: Bool - public init(id: PlatformVideoContentId, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { + public init(id: PlatformVideoContentId, content: Content, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { self.id = id self.nativeId = id - self.fileReference = fileReference - self.dimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 128.0, height: 128.0) - self.duration = fileReference.media.duration ?? 0 + self.content = content + self.dimensions = self.content.dimensions?.cgSize ?? CGSize(width: 480, height: 320) + self.duration = self.content.duration ?? 0 self.streamVideo = streamVideo self.loopVideo = loopVideo self.enableSound = enableSound @@ -71,15 +94,17 @@ public final class PlatformVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } public func isEqual(to other: UniversalVideoContent) -> Bool { if let other = other as? PlatformVideoContent { if case let .message(_, stableId, _) = self.nativeId { if case .message(_, stableId, _) = other.nativeId { - if self.fileReference.media.isInstantVideo { - return true + if case let .file(file) = self.content { + if file.media.isInstantVideo { + return true + } } } } @@ -90,7 +115,7 @@ public final class PlatformVideoContent: UniversalVideoContent { private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let postbox: Postbox - private let fileReference: FileMediaReference + private let content: PlatformVideoContent.Content private let approximateDuration: Double private let intrinsicDimensions: CGSize @@ -125,7 +150,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private let imageNode: TransformImageNode - private let playerItem: AVPlayerItem + private var playerItem: AVPlayerItem? private let player: AVPlayer private let playerNode: ASDisplayNode @@ -133,6 +158,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private var statusDisposable: Disposable? private var didPlayToEndTimeObserver: NSObjectProtocol? + private var didBecomeActiveObserver: NSObjectProtocol? + private var willResignActiveObserver: NSObjectProtocol? + private var playerItemFailedToPlayToEndTimeObserver: NSObjectProtocol? private let fetchDisposable = MetaDisposable() @@ -141,16 +169,15 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private var validLayout: CGSize? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { self.postbox = postbox - self.fileReference = fileReference - self.approximateDuration = Double(fileReference.media.duration ?? 1) + self.content = content + self.approximateDuration = Double(content.duration ?? 1) self.audioSessionManager = audioSessionManager self.imageNode = TransformImageNode() - self.playerItem = AVPlayerItem(url: URL(string: postbox.mediaBox.completedResourcePath(fileReference.media.resource, pathExtension: "mov") ?? "")!) - let player = AVPlayer(playerItem: self.playerItem) + let player = AVPlayer(playerItem: nil) self.player = player self.playerNode = ASDisplayNode() @@ -158,26 +185,31 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte return AVPlayerLayer(player: player) }) - self.intrinsicDimensions = fileReference.media.dimensions?.cgSize ?? CGSize() + self.intrinsicDimensions = content.dimensions?.cgSize ?? CGSize() self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) super.init() - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: fileReference) |> map { [weak self] getSize, getData in - Queue.mainQueue().async { - if let strongSelf = self, strongSelf.dimensions == nil { - if let dimensions = getSize() { - strongSelf.dimensions = dimensions - strongSelf.dimensionsPromise.set(dimensions) - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + switch content { + case let .file(file): + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: file) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let size = strongSelf.validLayout { + strongSelf.updateLayout(size: size, transition: .immediate) + } } } } - } - return getData - }) + return getData + }) + case .url: + break + } self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) @@ -192,18 +224,36 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte } self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil) - playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) - playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) - playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) self._bufferingStatus.set(.single(nil)) + + let playerItem: AVPlayerItem + switch content { + case let .file(file): + playerItem = AVPlayerItem(url: URL(string: postbox.mediaBox.completedResourcePath(file.media.resource, pathExtension: "mov") ?? "")!) + case let .url(url): + playerItem = AVPlayerItem(url: URL(string: url)!) + } + self.setPlayerItem(playerItem) + + 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 + } + layer.player = strongSelf.player + }) + self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in + guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { + return + } + layer.player = nil + }) } deinit { self.player.removeObserver(self, forKeyPath: "rate") - self.playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") - self.playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") - self.playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") + + self.setPlayerItem(nil) self.audioSessionDisposable.dispose() @@ -213,12 +263,57 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte 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) + } + } + + private func setPlayerItem(_ item: AVPlayerItem?) { + if let playerItem = self.playerItem { + playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") + 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 + } + } + + self.playerItem = item + + if let playerItem = self.playerItem { + playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) + self.playerItemFailedToPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { [weak self] _ in + guard let strongSelf = self else { + return + } + switch strongSelf.content { + case .file: + break + case let .url(url): + let updatedPlayerItem = AVPlayerItem(url: URL(string: url)!) + strongSelf.setPlayerItem(updatedPlayerItem) + } + }) + } + + self.player.replaceCurrentItem(with: self.playerItem) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "rate" { let isPlaying = !self.player.rate.isZero let status: MediaPlayerPlaybackStatus + if isPlaying { + self.isBuffering = false + } if self.isBuffering { status = .buffering(initial: false, whilePlaying: isPlaying) } else { @@ -248,6 +343,21 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte } self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: status, soundEnabled: true) self._status.set(self.statusValue) + } else if keyPath == "status" { + if let playerItem = self.playerItem, false { + switch playerItem.status { + case .failed: + switch self.content { + case .file: + break + case let .url(url): + let updatedPlayerItem = AVPlayerItem(url: URL(string: url)!) + self.setPlayerItem(updatedPlayerItem) + } + default: + break + } + } } }