mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
358 lines
13 KiB
Swift
358 lines
13 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private final class SharedInstantVideoContext: SharedVideoContext {
|
|
let player: MediaPlayer
|
|
let playerNode: MediaPlayerNode
|
|
|
|
private let playbackCompletedListeners = Bag<() -> Void>()
|
|
|
|
init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource) {
|
|
self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: false)
|
|
var actionAtEndImpl: (() -> Void)?
|
|
self.player.actionAtEnd = .loopDisablingSound({
|
|
actionAtEndImpl?()
|
|
})
|
|
self.playerNode = MediaPlayerNode(backgroundThread: false)
|
|
self.player.attachPlayerNode(self.playerNode)
|
|
|
|
super.init()
|
|
|
|
actionAtEndImpl = { [weak self] in
|
|
if let strongSelf = self {
|
|
for listener in strongSelf.playbackCompletedListeners.copyItems() {
|
|
listener()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func play() {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
self.player.play()
|
|
}
|
|
|
|
func pause() {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
self.player.pause()
|
|
}
|
|
|
|
func togglePlayPause() {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
self.player.togglePlayPause()
|
|
}
|
|
|
|
func setSoundEnabled(_ value: Bool) {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
if value {
|
|
self.player.playOnceWithSound()
|
|
} else {
|
|
self.player.continuePlayingWithoutSound()
|
|
}
|
|
}
|
|
|
|
func seek(_ timestamp: Double) {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
self.player.seek(timestamp: timestamp)
|
|
}
|
|
|
|
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {
|
|
return self.playbackCompletedListeners.add(f)
|
|
}
|
|
|
|
func removePlaybackCompleted(_ index: Int) {
|
|
self.playbackCompletedListeners.remove(index)
|
|
}
|
|
}
|
|
|
|
enum InstantVideoNodeSource {
|
|
case messageMedia(stableId: AnyHashable, file: TelegramMediaFile)
|
|
|
|
fileprivate var id: AnyHashable {
|
|
switch self {
|
|
case let .messageMedia(stableId, _):
|
|
return stableId
|
|
}
|
|
}
|
|
|
|
fileprivate var resource: MediaResource {
|
|
switch self {
|
|
case let .messageMedia(_, file):
|
|
return file.resource
|
|
}
|
|
}
|
|
|
|
fileprivate var file: TelegramMediaFile {
|
|
switch self {
|
|
case let .messageMedia(_, file):
|
|
return file
|
|
}
|
|
}
|
|
}
|
|
|
|
private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayInstantVideoShadow")?.precomposed()
|
|
|
|
final class InstantVideoNode: OverlayMediaItemNode {
|
|
private let manager: MediaManager
|
|
private let source: InstantVideoNodeSource
|
|
private let priority: Int32
|
|
private let withSound: Bool
|
|
private let postbox: Postbox
|
|
|
|
private var soundEnabled: Bool
|
|
|
|
private var contextId: Int32?
|
|
|
|
private var context: SharedInstantVideoContext?
|
|
private var contextPlaybackEndedIndex: Int?
|
|
private var validLayout: CGSize?
|
|
|
|
private var theme: PresentationTheme
|
|
|
|
private let backgroundNode: ASImageNode
|
|
private let imageNode: TransformImageNode
|
|
private var snapshotView: UIView?
|
|
private let progressNode: RadialProgressNode
|
|
|
|
private var statusDisposable: Disposable?
|
|
|
|
var playbackEnded: (() -> Void)?
|
|
var tapped: (() -> Void)?
|
|
var dismissed: (() -> Void)?
|
|
|
|
private var initializedStatus = false
|
|
private let _status = Promise<MediaPlayerStatus>()
|
|
var status: Signal<MediaPlayerStatus, NoError> {
|
|
return self._status.get()
|
|
}
|
|
|
|
override var group: OverlayMediaItemNodeGroup? {
|
|
return OverlayMediaItemNodeGroup(rawValue: 0)
|
|
}
|
|
|
|
override var tempExtendedTopInset: Bool {
|
|
return true
|
|
}
|
|
|
|
init(theme: PresentationTheme, manager: MediaManager, account: Account, source: InstantVideoNodeSource, priority: Int32, withSound: Bool) {
|
|
self.theme = theme
|
|
self.manager = manager
|
|
self.source = source
|
|
self.priority = priority
|
|
self.withSound = withSound
|
|
self.soundEnabled = withSound
|
|
self.postbox = account.postbox
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.displayWithoutProcessing = true
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
|
|
self.imageNode = TransformImageNode()
|
|
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil))
|
|
|
|
super.init()
|
|
|
|
self.backgroundNode.image = backgroundImage
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.imageNode)
|
|
|
|
self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: source.file))
|
|
|
|
self.statusDisposable = (chatMessageFileStatus(account: account, file: source.file) |> deliverOnMainQueue).start(next: { [weak self] status in
|
|
if let strongSelf = self {
|
|
|
|
}
|
|
})
|
|
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { [weak self] context in
|
|
if let strongSelf = self, let context = context as? SharedInstantVideoContext {
|
|
context.addPlaybackCompleted {
|
|
if let strongSelf = self {
|
|
strongSelf.playbackEnded?()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
if let context = self.context {
|
|
if context.playerNode.supernode === self {
|
|
context.playerNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
let manager = self.manager
|
|
let source = self.source
|
|
let contextId = self.contextId
|
|
|
|
Queue.mainQueue().async {
|
|
if let contextId = contextId {
|
|
manager.sharedVideoContextManager.detachSharedVideoContext(id: source.id, index: contextId)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
private func updateContext(_ context: SharedInstantVideoContext?) {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
|
|
let previous = self.context
|
|
self.context = context
|
|
if previous !== context {
|
|
if let snapshotView = self.snapshotView {
|
|
snapshotView.removeFromSuperview()
|
|
self.snapshotView = nil
|
|
}
|
|
if let previous = previous {
|
|
if let contextPlaybackEndedIndex = self.contextPlaybackEndedIndex {
|
|
previous.removePlaybackCompleted(contextPlaybackEndedIndex)
|
|
}
|
|
self.contextPlaybackEndedIndex = nil
|
|
if let snapshotView = previous.playerNode.view.snapshotView(afterScreenUpdates: false) {
|
|
self.snapshotView = snapshotView
|
|
snapshotView.frame = self.imageNode.frame
|
|
self.view.addSubview(snapshotView)
|
|
}
|
|
if previous.playerNode.supernode === self {
|
|
previous.playerNode.removeFromSupernode()
|
|
}
|
|
}
|
|
if let context = context {
|
|
self.contextPlaybackEndedIndex = context.addPlaybackCompleted { [weak self] in
|
|
self?.playbackEnded?()
|
|
}
|
|
if context.playerNode.supernode !== self {
|
|
self.addSubnode(context.playerNode)
|
|
if let validLayout = self.validLayout {
|
|
self.updateLayoutImpl(validLayout)
|
|
}
|
|
}
|
|
}
|
|
if self.hasAttachedContext != (context !== nil) {
|
|
self.hasAttachedContext = (context !== nil)
|
|
self.hasAttachedContextUpdated?(self.hasAttachedContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func updateLayout(_ size: CGSize) {
|
|
if size != self.validLayout {
|
|
self.updateLayoutImpl(size)
|
|
}
|
|
}
|
|
|
|
private func updateLayoutImpl(_ size: CGSize) {
|
|
self.validLayout = size
|
|
|
|
let arguments = TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: CGSize(width: size.width + 2.0, height: size.height + 2.0), boundingSize: size, intrinsicInsets: UIEdgeInsets())
|
|
let videoFrame = CGRect(origin: CGPoint(), size: arguments.boundingSize)
|
|
|
|
if let context = self.context {
|
|
context.playerNode.transformArguments = arguments
|
|
context.playerNode.frame = videoFrame
|
|
}
|
|
|
|
let backgroundInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
|
|
self.backgroundNode.frame = CGRect(origin: CGPoint(x: -backgroundInsets.left, y: -backgroundInsets.top), size: CGSize(width: videoFrame.size.width + backgroundInsets.left + backgroundInsets.right, height: videoFrame.size.height + backgroundInsets.top + backgroundInsets.bottom))
|
|
|
|
self.imageNode.asyncLayout()(arguments)()
|
|
self.imageNode.frame = videoFrame
|
|
self.snapshotView?.frame = self.imageNode.frame
|
|
}
|
|
|
|
func play() {
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
context.play()
|
|
}
|
|
})
|
|
}
|
|
|
|
func pause() {
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
context.pause()
|
|
}
|
|
})
|
|
}
|
|
|
|
func togglePlayPause() {
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
context.togglePlayPause()
|
|
}
|
|
})
|
|
}
|
|
|
|
func setSoundEnabled(_ value: Bool) {
|
|
self.soundEnabled = value
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
context.setSoundEnabled(value)
|
|
}
|
|
})
|
|
}
|
|
|
|
func seek(_ timestamp: Double) {
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
context.seek(timestamp)
|
|
}
|
|
})
|
|
}
|
|
|
|
override func setShouldAcquireContext(_ value: Bool) {
|
|
if value {
|
|
if self.contextId == nil {
|
|
self.contextId = self.manager.sharedVideoContextManager.attachSharedVideoContext(id: source.id, priority: self.priority, create: {
|
|
let context = SharedInstantVideoContext(audioSessionManager: manager.audioSession, postbox: self.postbox, resource: self.source.resource)
|
|
context.setSoundEnabled(self.soundEnabled)
|
|
context.play()
|
|
return context
|
|
}, update: { [weak self] context in
|
|
if let strongSelf = self {
|
|
strongSelf.updateContext(context as? SharedInstantVideoContext)
|
|
}
|
|
})
|
|
}
|
|
} else if let contextId = self.contextId {
|
|
self.manager.sharedVideoContextManager.detachSharedVideoContext(id: self.source.id, index: contextId)
|
|
self.contextId = nil
|
|
}
|
|
|
|
if !self.initializedStatus {
|
|
self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in
|
|
if let context = context as? SharedInstantVideoContext {
|
|
self.initializedStatus = true
|
|
self._status.set(context.player.status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
override func preferredSizeForOverlayDisplay() -> CGSize {
|
|
return CGSize(width: 124.0, height: 124.0)
|
|
}
|
|
|
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.tapped?()
|
|
}
|
|
}
|
|
|
|
override func dismiss() {
|
|
self.dismissed?()
|
|
}
|
|
}
|