Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2020-07-01 04:43:46 +03:00
commit a4ea5e5642
10 changed files with 394 additions and 71 deletions

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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<String>()
private let centralItemTitleView = Promise<UIView?>()
private let centralItemRightBarButtonItem = Promise<UIBarButtonItem?>()
private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>(nil)
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
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())

View File

@ -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."))

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,8 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
maxSpeed: 0.6,
minScale: 0.45,
maxScale: 0.55,
scaleSpeed: 0.2
scaleSpeed: 0.2,
isCircle: true
)
private let mediumBlob = BlobView(
pointsCount: 8,
@ -28,7 +29,8 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
maxSpeed: 7,
minScale: 0.55,
maxScale: 0.9,
scaleSpeed: 0.2
scaleSpeed: 0.2,
isCircle: false
)
private let bigBlob = BlobView(
pointsCount: 8,
@ -38,7 +40,8 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
maxSpeed: 7,
minScale: 0.55,
maxScale: 1,
scaleSpeed: 0.2
scaleSpeed: 0.2,
isCircle: false
)
override init(frame: CGRect) {
@ -101,6 +104,9 @@ final class BlobView: UIView {
let maxScale: CGFloat
let scaleSpeed: CGFloat
// If true ignores randomness and pointsCount
let isCircle: Bool
var level: CGFloat = 0 {
didSet {
speedLevel = max(level, speedLevel)
@ -153,7 +159,8 @@ final class BlobView: UIView {
maxSpeed: CGFloat,
minScale: CGFloat,
maxScale: CGFloat,
scaleSpeed: CGFloat
scaleSpeed: CGFloat,
isCircle: Bool
) {
self.pointsCount = pointsCount
self.minRandomness = minRandomness
@ -163,6 +170,7 @@ final class BlobView: UIView {
self.minScale = minScale
self.maxScale = maxScale
self.scaleSpeed = scaleSpeed
self.isCircle = isCircle
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
@ -208,6 +216,8 @@ final class BlobView: UIView {
}
func animateToNewShape() {
guard !isCircle else { return }
if pop_animation(forKey: "blob") != nil {
fromPoints = currentPoints
toPoints = nil
@ -296,6 +306,13 @@ final class BlobView: UIView {
CATransaction.begin()
CATransaction.setDisableActions(true)
shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
if isCircle {
let halfWidth = bounds.width * 0.5
shapeLayer.path = UIBezierPath(
roundedRect: bounds.offsetBy(dx: -halfWidth, dy: -halfWidth),
cornerRadius: halfWidth
).cgPath
}
CATransaction.commit()
}
}

View File

@ -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
@ -9387,7 +9439,9 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
func parseUrl(url: String, wasConcealed: Bool) -> (string: String, concealed: Bool) {
var parsedUrlValue: URL?
if let parsed = URL(string: url) {
if url.hasPrefix("tel:") {
return (url, false)
} else if let parsed = URL(string: url) {
parsedUrlValue = parsed
} else if let parsed = URL(string: "https://" + url) {
parsedUrlValue = parsed

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}
}
}