mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Video improvements
This commit is contained in:
parent
bae29f301e
commit
3797f3af4f
@ -259,7 +259,7 @@ public func galleryItemForEntry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isHLS {
|
if isHLS {
|
||||||
content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||||
} else {
|
} else {
|
||||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||||
}
|
}
|
||||||
@ -1364,7 +1364,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||||||
})
|
})
|
||||||
|
|
||||||
let disableTapNavigation = !(self.context.sharedContext.currentMediaDisplaySettings.with { $0 }.showNextMediaOnTap)
|
let disableTapNavigation = !(self.context.sharedContext.currentMediaDisplaySettings.with { $0 }.showNextMediaOnTap)
|
||||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction, disableTapNavigation: disableTapNavigation)
|
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, disableTapNavigation: disableTapNavigation)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.galleryNode.statusBar = self.statusBar
|
self.galleryNode.statusBar = self.statusBar
|
||||||
|
@ -5,8 +5,11 @@ import Display
|
|||||||
import Postbox
|
import Postbox
|
||||||
import SwipeToDismissGesture
|
import SwipeToDismissGesture
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
public var statusBar: StatusBar?
|
public var statusBar: StatusBar?
|
||||||
public var navigationBar: NavigationBar? {
|
public var navigationBar: NavigationBar? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -48,7 +51,8 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(controllerInteraction: GalleryControllerInteraction, pageGap: CGFloat = 20.0, disableTapNavigation: Bool = false) {
|
public init(context: AccountContext, controllerInteraction: GalleryControllerInteraction, pageGap: CGFloat = 20.0, disableTapNavigation: Bool = false) {
|
||||||
|
self.context = context
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.backgroundColor = UIColor.black
|
self.backgroundNode.backgroundColor = UIColor.black
|
||||||
self.scrollView = UIScrollView()
|
self.scrollView = UIScrollView()
|
||||||
@ -471,6 +475,16 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
|||||||
let minimalDismissDistance = scrollView.contentSize.height / 12.0
|
let minimalDismissDistance = scrollView.contentSize.height / 12.0
|
||||||
if abs(velocity.y) > 1.0 || abs(distanceFromEquilibrium) > minimalDismissDistance {
|
if abs(velocity.y) > 1.0 || abs(distanceFromEquilibrium) > minimalDismissDistance {
|
||||||
if distanceFromEquilibrium > 1.0, let centralItemNode = self.pager.centralItemNode(), centralItemNode.maybePerformActionForSwipeDismiss() {
|
if distanceFromEquilibrium > 1.0, let centralItemNode = self.pager.centralItemNode(), centralItemNode.maybePerformActionForSwipeDismiss() {
|
||||||
|
if let chatController = self.baseNavigationController()?.topViewController as? ChatController {
|
||||||
|
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
|
//TODO:localize
|
||||||
|
chatController.present(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .hidArchive(title: "Video Minimized", text: "Swipe down on a video to close it.", undo: false),
|
||||||
|
elevatedLayout: false, action: { _ in true }
|
||||||
|
), in: .current)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@ public final class SecretMediaPreviewController: ViewController {
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction)
|
self.displayNode = SecretMediaPreviewControllerNode(context: self.context, controllerInteraction: controllerInteraction)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.controllerNode.statusPressed = { [weak self] _ in
|
self.controllerNode.statusPressed = { [weak self] _ in
|
||||||
|
@ -377,7 +377,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
self.displayNode = GalleryControllerNode(context: self.context,controllerInteraction: controllerInteraction)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.galleryNode.statusBar = self.statusBar
|
self.galleryNode.statusBar = self.statusBar
|
||||||
|
@ -230,45 +230,6 @@ private final class ChunkMediaPlayerContext {
|
|||||||
self.loadedState = ChunkMediaPlayerLoadedState()
|
self.loadedState = ChunkMediaPlayerLoadedState()
|
||||||
|
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
let audioRendererContext = MediaPlayerAudioRenderer(
|
|
||||||
audioSession: .manager(self.audioSessionManager),
|
|
||||||
forAudioVideoMessage: self.isAudioVideoMessage,
|
|
||||||
playAndRecord: self.playAndRecord,
|
|
||||||
soundMuted: self.soundMuted,
|
|
||||||
ambient: self.ambient,
|
|
||||||
mixWithOthers: self.mixWithOthers,
|
|
||||||
forceAudioToSpeaker: self.forceAudioToSpeaker,
|
|
||||||
baseRate: self.baseRate,
|
|
||||||
audioLevelPipe: self.audioLevelPipe,
|
|
||||||
updatedRate: { [weak self] in
|
|
||||||
queue.async {
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.tick()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
audioPaused: { [weak self] in
|
|
||||||
queue.async {
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if self.enableSound {
|
|
||||||
if self.continuePlayingWithoutSoundOnLostAudioSession {
|
|
||||||
self.continuePlayingWithoutSound(seek: .start)
|
|
||||||
} else {
|
|
||||||
self.pause(lostAudioSession: true, faded: false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.seek(timestamp: 0.0, action: .play, notify: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.audioRenderer = MediaPlayerAudioRendererContext(renderer: audioRendererContext)
|
|
||||||
|
|
||||||
self.loadedState.controlTimebase = ChunkMediaPlayerControlTimebase(timebase: audioRendererContext.audioTimebase, isAudio: true)
|
|
||||||
|
|
||||||
self.videoRenderer.visibilityUpdated = { [weak self] value in
|
self.videoRenderer.visibilityUpdated = { [weak self] value in
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
|
|
||||||
@ -328,9 +289,6 @@ private final class ChunkMediaPlayerContext {
|
|||||||
return .noFrames
|
return .noFrames
|
||||||
})
|
})
|
||||||
|
|
||||||
audioRendererContext.start()
|
|
||||||
self.tick()
|
|
||||||
|
|
||||||
let tickTimer = SwiftSignalKit.Timer(timeout: 1.0 / 25.0, repeat: true, completion: { [weak self] in
|
let tickTimer = SwiftSignalKit.Timer(timeout: 1.0 / 25.0, repeat: true, completion: { [weak self] in
|
||||||
self?.tick()
|
self?.tick()
|
||||||
}, queue: self.queue)
|
}, queue: self.queue)
|
||||||
@ -344,6 +302,8 @@ private final class ChunkMediaPlayerContext {
|
|||||||
self.partsState = partsState
|
self.partsState = partsState
|
||||||
self.tick()
|
self.tick()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -457,6 +417,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
} else {
|
} else {
|
||||||
timestamp = 0.0
|
timestamp = 0.0
|
||||||
}
|
}
|
||||||
|
let _ = timestamp
|
||||||
self.seek(timestamp: timestamp, action: .play, notify: true)
|
self.seek(timestamp: timestamp, action: .play, notify: true)
|
||||||
} else {
|
} else {
|
||||||
if case let .timecode(time) = seek {
|
if case let .timecode(time) = seek {
|
||||||
@ -598,12 +559,31 @@ private final class ChunkMediaPlayerContext {
|
|||||||
}
|
}
|
||||||
timestamp = max(0.0, timestamp)
|
timestamp = max(0.0, timestamp)
|
||||||
|
|
||||||
if let firstPart = self.loadedState.partStates.first, let mediaBuffers = firstPart.mediaBuffers, mediaBuffers.videoBuffer != nil, mediaBuffers.audioBuffer == nil {
|
var disableAudio = false
|
||||||
// No audio
|
if !self.enableSound {
|
||||||
|
disableAudio = true
|
||||||
|
}
|
||||||
|
var hasAudio = false
|
||||||
|
if let firstPart = self.loadedState.partStates.first, let mediaBuffers = firstPart.mediaBuffers, mediaBuffers.videoBuffer != nil {
|
||||||
|
if mediaBuffers.audioBuffer != nil {
|
||||||
|
hasAudio = true
|
||||||
|
} else {
|
||||||
|
disableAudio = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if disableAudio {
|
||||||
|
var resetTimebase = false
|
||||||
if self.audioRenderer != nil {
|
if self.audioRenderer != nil {
|
||||||
self.audioRenderer?.renderer.stop()
|
self.audioRenderer?.renderer.stop()
|
||||||
self.audioRenderer = nil
|
self.audioRenderer = nil
|
||||||
|
resetTimebase = true
|
||||||
|
}
|
||||||
|
if self.loadedState.controlTimebase == nil {
|
||||||
|
resetTimebase = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetTimebase {
|
||||||
var timebase: CMTimebase?
|
var timebase: CMTimebase?
|
||||||
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
|
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
|
||||||
let controlTimebase = ChunkMediaPlayerControlTimebase(timebase: timebase!, isAudio: false)
|
let controlTimebase = ChunkMediaPlayerControlTimebase(timebase: timebase!, isAudio: false)
|
||||||
@ -611,6 +591,50 @@ private final class ChunkMediaPlayerContext {
|
|||||||
|
|
||||||
self.loadedState.controlTimebase = controlTimebase
|
self.loadedState.controlTimebase = controlTimebase
|
||||||
}
|
}
|
||||||
|
} else if hasAudio {
|
||||||
|
if self.audioRenderer == nil {
|
||||||
|
let queue = self.queue
|
||||||
|
let audioRendererContext = MediaPlayerAudioRenderer(
|
||||||
|
audioSession: .manager(self.audioSessionManager),
|
||||||
|
forAudioVideoMessage: self.isAudioVideoMessage,
|
||||||
|
playAndRecord: self.playAndRecord,
|
||||||
|
soundMuted: self.soundMuted,
|
||||||
|
ambient: self.ambient,
|
||||||
|
mixWithOthers: self.mixWithOthers,
|
||||||
|
forceAudioToSpeaker: self.forceAudioToSpeaker,
|
||||||
|
baseRate: self.baseRate,
|
||||||
|
audioLevelPipe: self.audioLevelPipe,
|
||||||
|
updatedRate: { [weak self] in
|
||||||
|
queue.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.tick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
audioPaused: { [weak self] in
|
||||||
|
queue.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.enableSound {
|
||||||
|
if self.continuePlayingWithoutSoundOnLostAudioSession {
|
||||||
|
self.continuePlayingWithoutSound(seek: .start)
|
||||||
|
} else {
|
||||||
|
self.pause(lostAudioSession: true, faded: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.seek(timestamp: 0.0, action: .play, notify: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.audioRenderer = MediaPlayerAudioRendererContext(renderer: audioRendererContext)
|
||||||
|
|
||||||
|
self.loadedState.controlTimebase = ChunkMediaPlayerControlTimebase(timebase: audioRendererContext.audioTimebase, isAudio: true)
|
||||||
|
audioRendererContext.flushBuffers(at: CMTimeMakeWithSeconds(timestamp, preferredTimescale: 44000), completion: {})
|
||||||
|
audioRendererContext.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//print("Timestamp: \(timestamp)")
|
//print("Timestamp: \(timestamp)")
|
||||||
|
@ -187,7 +187,7 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.galleryNode.statusBar = self.statusBar
|
self.galleryNode.statusBar = self.statusBar
|
||||||
|
@ -633,7 +633,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.galleryNode.pager.updateOnReplacement = true
|
self.galleryNode.pager.updateOnReplacement = true
|
||||||
|
@ -472,8 +472,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
let controller = UndoOverlayController(
|
let controller = UndoOverlayController(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
content: .universal(
|
content: .universal(
|
||||||
animation: "anim_profileunmute",
|
animation: "anim_set_notification",
|
||||||
scale: 0.075,
|
scale: 0.06,
|
||||||
colors: [
|
colors: [
|
||||||
"Middle.Group 1.Fill 1": UIColor.white,
|
"Middle.Group 1.Fill 1": UIColor.white,
|
||||||
"Top.Group 1.Fill 1": UIColor.white,
|
"Top.Group 1.Fill 1": UIColor.white,
|
||||||
|
@ -796,6 +796,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
||||||
let _ = peerType
|
let _ = peerType
|
||||||
|
|
||||||
|
let useInlineHLS = "".isEmpty
|
||||||
|
|
||||||
var nativeSize: CGSize
|
var nativeSize: CGSize
|
||||||
|
|
||||||
let isSecretMedia = message.containsSecretMedia
|
let isSecretMedia = message.containsSecretMedia
|
||||||
@ -1270,7 +1272,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
|
|
||||||
var passFile = true
|
var passFile = true
|
||||||
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
|
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
|
||||||
file = minimizedQualityFile.file.media
|
if !useInlineHLS {
|
||||||
|
file = minimizedQualityFile.file.media
|
||||||
|
}
|
||||||
if hlsInlinePlaybackRange == nil {
|
if hlsInlinePlaybackRange == nil {
|
||||||
passFile = false
|
passFile = false
|
||||||
}
|
}
|
||||||
@ -1395,7 +1399,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
|
|
||||||
var passFile = true
|
var passFile = true
|
||||||
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
|
if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) {
|
||||||
file = minimizedQualityFile.file.media
|
if !useInlineHLS {
|
||||||
|
file = minimizedQualityFile.file.media
|
||||||
|
}
|
||||||
if hlsInlinePlaybackRange == nil {
|
if hlsInlinePlaybackRange == nil {
|
||||||
passFile = false
|
passFile = false
|
||||||
}
|
}
|
||||||
@ -1777,27 +1783,38 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
let loopVideo = updatedVideoFile.isAnimated
|
let loopVideo = updatedVideoFile.isAnimated
|
||||||
|
|
||||||
let videoContent: UniversalVideoContent
|
let videoContent: UniversalVideoContent
|
||||||
videoContent = NativeVideoContent(
|
if useInlineHLS && NativeVideoContent.isHLSVideo(file: updatedVideoFile) {
|
||||||
id: .message(message.stableId, updatedVideoFile.fileId),
|
videoContent = HLSVideoContent(
|
||||||
userLocation: .peer(message.id.peerId),
|
id: .message(message.stableId, updatedVideoFile.fileId),
|
||||||
fileReference: .message(message: MessageReference(message), media: updatedVideoFile),
|
userLocation: .peer(message.id.peerId),
|
||||||
limitedFileRange: hlsInlinePlaybackRange,
|
fileReference: .message(message: MessageReference(message), media: updatedVideoFile),
|
||||||
streamVideo: streamVideo ? .conservative : .none,
|
loopVideo: loopVideo,
|
||||||
loopVideo: loopVideo,
|
enableSound: false,
|
||||||
enableSound: false,
|
fetchAutomatically: false
|
||||||
fetchAutomatically: false,
|
)
|
||||||
onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false),
|
} else {
|
||||||
autoFetchFullSizeThumbnail: true,
|
videoContent = NativeVideoContent(
|
||||||
continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo,
|
id: .message(message.stableId, updatedVideoFile.fileId),
|
||||||
placeholderColor: emptyColor,
|
userLocation: .peer(message.id.peerId),
|
||||||
captureProtected: message.isCopyProtected() || isExtendedMedia,
|
fileReference: .message(message: MessageReference(message), media: updatedVideoFile),
|
||||||
storeAfterDownload: { [weak context] in
|
limitedFileRange: hlsInlinePlaybackRange,
|
||||||
guard let context, let peerId else {
|
streamVideo: streamVideo ? .conservative : .none,
|
||||||
return
|
loopVideo: loopVideo,
|
||||||
|
enableSound: false,
|
||||||
|
fetchAutomatically: false,
|
||||||
|
onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false),
|
||||||
|
autoFetchFullSizeThumbnail: true,
|
||||||
|
continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo,
|
||||||
|
placeholderColor: emptyColor,
|
||||||
|
captureProtected: message.isCopyProtected() || isExtendedMedia,
|
||||||
|
storeAfterDownload: { [weak context] in
|
||||||
|
guard let context, let peerId else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
|
||||||
}
|
}
|
||||||
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||||
videoNode.isUserInteractionEnabled = false
|
videoNode.isUserInteractionEnabled = false
|
||||||
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
||||||
|
@ -8159,7 +8159,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
canCreateStream = true
|
canCreateStream = true
|
||||||
}
|
}
|
||||||
case let channel as TelegramChannel:
|
case let channel as TelegramChannel:
|
||||||
if channel.flags.contains(.isCreator) {
|
if channel.hasPermission(.manageCalls) {
|
||||||
canCreateStream = true
|
canCreateStream = true
|
||||||
credentialsPromise = Promise()
|
credentialsPromise = Promise()
|
||||||
credentialsPromise?.set(context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
credentialsPromise?.set(context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
||||||
|
@ -446,7 +446,7 @@ public class WallpaperGalleryController: ViewController {
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = WallpaperGalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0, disableTapNavigation: true)
|
self.displayNode = WallpaperGalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, pageGap: 0.0, disableTapNavigation: true)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
(self.displayNode as? WallpaperGalleryControllerNode)?.nativeStatusBar = self.statusBar
|
(self.displayNode as? WallpaperGalleryControllerNode)?.nativeStatusBar = self.statusBar
|
||||||
|
Binary file not shown.
@ -42,7 +42,6 @@ import MediaEditor
|
|||||||
import TelegramUIDeclareEncodables
|
import TelegramUIDeclareEncodables
|
||||||
import ContextMenuScreen
|
import ContextMenuScreen
|
||||||
import MetalEngine
|
import MetalEngine
|
||||||
import TranslateUI
|
|
||||||
|
|
||||||
#if canImport(AppCenter)
|
#if canImport(AppCenter)
|
||||||
import AppCenter
|
import AppCenter
|
||||||
@ -363,13 +362,6 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
if #available(iOS 18.0, *) {
|
|
||||||
let translationService = ExperimentalInternalTranslationServiceImpl(view: hostView.containerView)
|
|
||||||
engineExperimentalInternalTranslationService = translationService
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
|
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
|
||||||
if #available(iOS 10.0, *) {
|
if #available(iOS 10.0, *) {
|
||||||
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
||||||
|
@ -126,6 +126,19 @@ import AdsInfoScreen
|
|||||||
|
|
||||||
extension ChatControllerImpl {
|
extension ChatControllerImpl {
|
||||||
func loadDisplayNodeImpl() {
|
func loadDisplayNodeImpl() {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
if self.context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation {
|
||||||
|
if engineExperimentalInternalTranslationService == nil, let hostView = self.context.sharedContext.mainWindow?.hostView {
|
||||||
|
let translationService = ExperimentalInternalTranslationServiceImpl(view: hostView.containerView)
|
||||||
|
engineExperimentalInternalTranslationService = translationService
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if engineExperimentalInternalTranslationService != nil {
|
||||||
|
engineExperimentalInternalTranslationService = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self)
|
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self)
|
||||||
|
|
||||||
if let currentItem = self.tempVoicePlaylistCurrentItem {
|
if let currentItem = self.tempVoicePlaylistCurrentItem {
|
||||||
|
@ -216,7 +216,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false
|
self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false
|
||||||
self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false
|
self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false
|
||||||
self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false
|
self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false
|
||||||
self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming") ?? false
|
self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming_v2") ?? false
|
||||||
self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? false
|
self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
class ConsolePolyfill {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
log(...messageArgs) {
|
||||||
|
var string = "";
|
||||||
|
for (const arg of messageArgs) {
|
||||||
|
string += arg;
|
||||||
|
}
|
||||||
|
_JsCorePolyfills.consoleLog(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...messageArgs) {
|
||||||
|
var string = "";
|
||||||
|
for (const arg of messageArgs) {
|
||||||
|
string += arg;
|
||||||
|
}
|
||||||
|
_JsCorePolyfills.consoleLog(string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PerformancePolyfill {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
return _JsCorePolyfills.performanceNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console = new ConsolePolyfill();
|
||||||
|
performance = new PerformancePolyfill();
|
||||||
|
|
||||||
|
self = {
|
||||||
|
console: console,
|
||||||
|
performance: performance
|
||||||
|
};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
/*! https://mths.be/base64 v1.0.0 by @mathias | MIT license */
|
@ -1,6 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
rm -rf ../HlsBundle
|
mkdir -p ../HlsBundle
|
||||||
mkdir ../HlsBundle
|
rm -rf ../HlsBundle/index
|
||||||
|
mkdir ../HlsBundle/index
|
||||||
npm run build-$1
|
npm run build-$1
|
||||||
cp ./dist/* ../HlsBundle/
|
cp ./dist/* ../HlsBundle/index/
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"base-64": "^1.0.0",
|
||||||
|
"event-target-polyfill": "^0.0.4",
|
||||||
"hls.js": "^1.5.15"
|
"hls.js": "^1.5.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -657,6 +659,11 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base-64": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
|
||||||
|
},
|
||||||
"node_modules/batch": {
|
"node_modules/batch": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||||
@ -1453,6 +1460,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-polyfill": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
"webpack-merge": "^6.0.1"
|
"webpack-merge": "^6.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"base-64": "^1.0.0",
|
||||||
|
"event-target-polyfill": "^0.0.4",
|
||||||
"hls.js": "^1.5.15"
|
"hls.js": "^1.5.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,8 +109,15 @@ export class VideoElementStub extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
querySelectorAll(name) {
|
querySelectorAll(name) {
|
||||||
const fragment = document.createDocumentFragment();
|
if (global.isJsCore) {
|
||||||
return fragment.querySelectorAll('*');
|
return [];
|
||||||
|
} else {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
return fragment.querySelectorAll('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChild(child) {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBufferedFromMediaSource() {
|
updateBufferedFromMediaSource() {
|
||||||
|
@ -1,8 +1,60 @@
|
|||||||
import Hls from "hls.js";
|
import "event-target-polyfill";
|
||||||
|
import {decode, encode} from "base-64";
|
||||||
|
|
||||||
import { VideoElementStub } from "./VideoElementStub.js"
|
import { VideoElementStub } from "./VideoElementStub.js"
|
||||||
import { MediaSourceStub, SourceBufferStub } from "./MediaSourceStub.js"
|
import { MediaSourceStub, SourceBufferStub } from "./MediaSourceStub.js"
|
||||||
import { XMLHttpRequestStub } from "./XMLHttpRequestStub.js"
|
import { XMLHttpRequestStub } from "./XMLHttpRequestStub.js"
|
||||||
|
|
||||||
|
global.isJsCore = false;
|
||||||
|
|
||||||
|
if (!global.btoa) {
|
||||||
|
global.btoa = encode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.atob) {
|
||||||
|
global.atob = decode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
global.isJsCore = true;
|
||||||
|
|
||||||
|
global.navigator = {
|
||||||
|
userAgent: "Telegram"
|
||||||
|
};
|
||||||
|
|
||||||
|
global.now = function() {
|
||||||
|
return _JsCorePolyfills.performanceNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
global.window = {
|
||||||
|
};
|
||||||
|
|
||||||
|
global.URL = {
|
||||||
|
};
|
||||||
|
|
||||||
|
window.webkit = {
|
||||||
|
};
|
||||||
|
window.webkit.messageHandlers = {
|
||||||
|
};
|
||||||
|
window.webkit.messageHandlers.performAction = {
|
||||||
|
};
|
||||||
|
window.webkit.messageHandlers.performAction.postMessage = function(dict) {
|
||||||
|
_JsCorePolyfills.postMessage(dict);
|
||||||
|
};
|
||||||
|
|
||||||
|
global.self.location = {
|
||||||
|
href: "http://127.0.0.1"
|
||||||
|
};
|
||||||
|
global.self.setTimeout = global.setTimeout;
|
||||||
|
global.self.setInterval = global.setInterval;
|
||||||
|
global.self.clearTimeout = global.clearTimeout;
|
||||||
|
global.self.clearInterval = global.clearTimeout;
|
||||||
|
global.self.URL = global.URL;
|
||||||
|
global.self.Date = global.Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
import Hls from "hls.js";
|
||||||
|
|
||||||
window.bridgeObjectMap = {};
|
window.bridgeObjectMap = {};
|
||||||
window.bridgeCallbackMap = {};
|
window.bridgeCallbackMap = {};
|
||||||
|
|
||||||
@ -56,6 +108,19 @@ if (typeof window !== 'undefined') {
|
|||||||
window.mediaSourceMap[url] = ms;
|
window.mediaSourceMap[url] = ms;
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
URL.revokeObjectURL = function(url) {
|
||||||
|
};
|
||||||
|
|
||||||
|
if (global.isJsCore) {
|
||||||
|
global.HTMLVideoElement = VideoElementStub;
|
||||||
|
|
||||||
|
global.self.MediaSource = window.MediaSource;
|
||||||
|
global.self.ManagedMediaSource = window.ManagedMediaSource;
|
||||||
|
global.self.SourceBuffer = window.SourceBuffer;
|
||||||
|
global.self.XMLHttpRequest = window.XMLHttpRequest;
|
||||||
|
global.self.HTMLVideoElement = VideoElementStub;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function postPlayerEvent(id, eventName, eventData) {
|
function postPlayerEvent(id, eventName, eventData) {
|
||||||
@ -139,6 +204,15 @@ export class HlsPlayerInstance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerSetCapAutoLevel(level) {
|
||||||
|
if (level >= 0) {
|
||||||
|
this.hls.autoLevelCapping = level;
|
||||||
|
} else {
|
||||||
|
this.hls.autoLevelCapping = -1;
|
||||||
|
this.hls.currentLevel = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playerSeek(value) {
|
playerSeek(value) {
|
||||||
this.video.currentTime = value;
|
this.video.currentTime = value;
|
||||||
}
|
}
|
||||||
@ -236,3 +310,7 @@ window.hlsPlayer_destroyInstance = function(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.bridgeInvokeCallback = bridgeInvokeCallback;
|
window.bridgeInvokeCallback = bridgeInvokeCallback;
|
||||||
|
|
||||||
|
if (global.isJsCore) {
|
||||||
|
window.onload();
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ const common = require('./webpack.common.js');
|
|||||||
|
|
||||||
module.exports = merge(common, {
|
module.exports = merge(common, {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: 'source-map',
|
devtool: 'inline-source-map',
|
||||||
devServer: {
|
devServer: {
|
||||||
static: './dist',
|
static: './dist',
|
||||||
},
|
},
|
||||||
|
@ -188,7 +188,7 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public let id: AnyHashable
|
public let id: AnyHashable
|
||||||
public let nativeId: PlatformVideoContentId
|
public let nativeId: NativeVideoContentId
|
||||||
let userLocation: MediaResourceUserLocation
|
let userLocation: MediaResourceUserLocation
|
||||||
public let fileReference: FileMediaReference
|
public let fileReference: FileMediaReference
|
||||||
public let dimensions: CGSize
|
public let dimensions: CGSize
|
||||||
@ -199,7 +199,7 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||||||
let baseRate: Double
|
let baseRate: Double
|
||||||
let fetchAutomatically: Bool
|
let fetchAutomatically: Bool
|
||||||
|
|
||||||
public init(id: PlatformVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) {
|
public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
self.nativeId = id
|
self.nativeId = id
|
||||||
@ -218,9 +218,9 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||||
if let other = other as? HLSVideoContent {
|
if let other = other as? NativeVideoContent {
|
||||||
if case let .message(_, stableId, _) = self.nativeId {
|
if case let .message(stableId, _) = self.nativeId {
|
||||||
if case .message(_, stableId, _) = other.nativeId {
|
if case .message(stableId, _) = other.nativeId {
|
||||||
if self.fileReference.media.isInstantVideo {
|
if self.fileReference.media.isInstantVideo {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import SwiftSignalKit
|
|||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import WebKit
|
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TelegramAudio
|
import TelegramAudio
|
||||||
@ -327,21 +326,11 @@ final class HLSJSServerSource: SharedHLSServer.Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
protocol HLSJSContext: AnyObject {
|
||||||
private let f: (WKScriptMessage) -> ()
|
func evaluateJavaScript(_ string: String)
|
||||||
|
|
||||||
init(_ f: @escaping (WKScriptMessage) -> ()) {
|
|
||||||
self.f = f
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
|
|
||||||
self.f(scriptMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
private final class SharedHLSVideoJSContext: NSObject {
|
||||||
private final class ContextReference {
|
private final class ContextReference {
|
||||||
weak var contentNode: HLSVideoJSNativeContentNode?
|
weak var contentNode: HLSVideoJSNativeContentNode?
|
||||||
|
|
||||||
@ -367,17 +356,17 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let shared: SharedHLSVideoWebView = SharedHLSVideoWebView()
|
static let shared: SharedHLSVideoJSContext = SharedHLSVideoJSContext()
|
||||||
|
|
||||||
private var contextReferences: [Int: ContextReference] = [:]
|
private var contextReferences: [Int: ContextReference] = [:]
|
||||||
|
|
||||||
var webView: WKWebView?
|
var jsContext: HLSJSContext?
|
||||||
|
|
||||||
var videoElements: [Int: VideoElement] = [:]
|
var videoElements: [Int: VideoElement] = [:]
|
||||||
var mediaSources: [Int: MediaSource] = [:]
|
var mediaSources: [Int: MediaSource] = [:]
|
||||||
var sourceBuffers: [Int: SourceBuffer] = [:]
|
var sourceBuffers: [Int: SourceBuffer] = [:]
|
||||||
|
|
||||||
private var isWebViewReady: Bool = false
|
private var isJsContextReady: Bool = false
|
||||||
private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = []
|
private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = []
|
||||||
|
|
||||||
private var tempTasks: [Int: URLSessionTask] = [:]
|
private var tempTasks: [Int: URLSessionTask] = [:]
|
||||||
@ -392,64 +381,24 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
self.emptyTimer?.invalidate()
|
self.emptyTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createWebView() {
|
private func createJsContext() {
|
||||||
let config = WKWebViewConfiguration()
|
let handleScriptMessage: ([String: Any]) -> Void = { [weak self] message in
|
||||||
config.allowsInlineMediaPlayback = true
|
|
||||||
config.mediaTypesRequiringUserActionForPlayback = []
|
|
||||||
config.allowsPictureInPictureMediaPlayback = true
|
|
||||||
|
|
||||||
let userController = WKUserContentController()
|
|
||||||
|
|
||||||
var handleScriptMessage: ((WKScriptMessage) -> Void)?
|
|
||||||
userController.add(WeakScriptMessageHandler { message in
|
|
||||||
handleScriptMessage?(message)
|
|
||||||
}, name: "performAction")
|
|
||||||
|
|
||||||
let isDebug: Bool
|
|
||||||
#if DEBUG
|
|
||||||
isDebug = true
|
|
||||||
#else
|
|
||||||
isDebug = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
config.userContentController = userController
|
|
||||||
|
|
||||||
let webView = WKWebView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)), configuration: config)
|
|
||||||
self.webView = webView
|
|
||||||
|
|
||||||
webView.scrollView.isScrollEnabled = false
|
|
||||||
webView.allowsLinkPreview = false
|
|
||||||
webView.allowsBackForwardNavigationGestures = false
|
|
||||||
webView.accessibilityIgnoresInvertColors = true
|
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
|
||||||
webView.alpha = 0.0
|
|
||||||
|
|
||||||
if #available(iOS 16.4, *) {
|
|
||||||
webView.isInspectable = isDebug
|
|
||||||
}
|
|
||||||
|
|
||||||
webView.navigationDelegate = self
|
|
||||||
|
|
||||||
handleScriptMessage = { [weak self] message in
|
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let body = message.body as? [String: Any] else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let eventName = body["event"] as? String else {
|
guard let eventName = message["event"] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch eventName {
|
switch eventName {
|
||||||
case "windowOnLoad":
|
case "windowOnLoad":
|
||||||
self.isWebViewReady = true
|
self.isJsContextReady = true
|
||||||
|
|
||||||
self.initializePendingInstances()
|
self.initializePendingInstances()
|
||||||
case "bridgeInvoke":
|
case "bridgeInvoke":
|
||||||
guard let eventData = body["data"] as? [String: Any] else {
|
guard let eventData = message["data"] as? [String: Any] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let bridgeId = eventData["bridgeId"] as? Int else {
|
guard let bridgeId = eventData["bridgeId"] as? Int else {
|
||||||
@ -478,31 +427,31 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
let jsonResult = try! JSONSerialization.data(withJSONObject: result)
|
let jsonResult = try! JSONSerialization.data(withJSONObject: result)
|
||||||
let jsonResultString = String(data: jsonResult, encoding: .utf8)!
|
let jsonResultString = String(data: jsonResult, encoding: .utf8)!
|
||||||
self.webView?.evaluateJavaScript("bridgeInvokeCallback(\(callbackId), \(jsonResultString));", completionHandler: nil)
|
self.jsContext?.evaluateJavaScript("window.bridgeInvokeCallback(\(callbackId), \(jsonResultString));")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
case "playerStatus":
|
case "playerStatus":
|
||||||
guard let instanceId = body["instanceId"] as? Int else {
|
guard let instanceId = message["instanceId"] as? Int else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let instance = self.contextReferences[instanceId]?.contentNode else {
|
guard let instance = self.contextReferences[instanceId]?.contentNode else {
|
||||||
self.contextReferences.removeValue(forKey: instanceId)
|
self.contextReferences.removeValue(forKey: instanceId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let eventData = body["data"] as? [String: Any] else {
|
guard let eventData = message["data"] as? [String: Any] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.onPlayerStatusUpdated(eventData: eventData)
|
instance.onPlayerStatusUpdated(eventData: eventData)
|
||||||
case "playerCurrentTime":
|
case "playerCurrentTime":
|
||||||
guard let instanceId = body["instanceId"] as? Int else {
|
guard let instanceId = message["instanceId"] as? Int else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let instance = self.contextReferences[instanceId]?.contentNode else {
|
guard let instance = self.contextReferences[instanceId]?.contentNode else {
|
||||||
self.contextReferences.removeValue(forKey: instanceId)
|
self.contextReferences.removeValue(forKey: instanceId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let eventData = body["data"] as? [String: Any] else {
|
guard let eventData = message["data"] as? [String: Any] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let value = eventData["value"] as? Double else {
|
guard let value = eventData["value"] as? Double else {
|
||||||
@ -523,18 +472,20 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isWebViewReady = false
|
self.isJsContextReady = false
|
||||||
|
|
||||||
let bundle = Bundle(for: SharedHLSVideoWebView.self)
|
/*#if DEBUG
|
||||||
let bundlePath = bundle.bundlePath + "/HlsBundle.bundle"
|
self.jsContext = WebViewNativeJSContextImpl(handleScriptMessage: handleScriptMessage)
|
||||||
webView.loadFileURL(URL(fileURLWithPath: bundlePath + "/index.html"), allowingReadAccessTo: URL(fileURLWithPath: bundlePath))
|
#else*/
|
||||||
|
self.jsContext = WebViewHLSJSContextImpl(handleScriptMessage: handleScriptMessage)
|
||||||
|
//#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func disposeWebView() {
|
private func disposeJsContext() {
|
||||||
if let _ = self.webView {
|
if let _ = self.jsContext {
|
||||||
self.webView = nil
|
self.jsContext = nil
|
||||||
}
|
}
|
||||||
self.isWebViewReady = false
|
self.isJsContextReady = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bridgeInvoke(
|
private func bridgeInvoke(
|
||||||
@ -551,7 +502,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let videoElement = VideoElement(instanceId: instanceId)
|
let videoElement = VideoElement(instanceId: instanceId)
|
||||||
SharedHLSVideoWebView.shared.videoElements[bridgeId] = videoElement
|
SharedHLSVideoJSContext.shared.videoElements[bridgeId] = videoElement
|
||||||
completion([:])
|
completion([:])
|
||||||
} else if (methodName == "setMediaSource") {
|
} else if (methodName == "setMediaSource") {
|
||||||
guard let instanceId = params["instanceId"] as? Int else {
|
guard let instanceId = params["instanceId"] as? Int else {
|
||||||
@ -562,7 +513,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == instanceId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == instanceId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
videoElement.mediaSourceId = mediaSourceId
|
videoElement.mediaSourceId = mediaSourceId
|
||||||
@ -622,14 +573,14 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
} else if (className == "MediaSource") {
|
} else if (className == "MediaSource") {
|
||||||
if (methodName == "constructor") {
|
if (methodName == "constructor") {
|
||||||
let mediaSource = MediaSource()
|
let mediaSource = MediaSource()
|
||||||
SharedHLSVideoWebView.shared.mediaSources[bridgeId] = mediaSource
|
SharedHLSVideoJSContext.shared.mediaSources[bridgeId] = mediaSource
|
||||||
completion([:])
|
completion([:])
|
||||||
} else if (methodName == "setDuration") {
|
} else if (methodName == "setDuration") {
|
||||||
guard let duration = params["duration"] as? Double else {
|
guard let duration = params["duration"] as? Double else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else {
|
guard let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[bridgeId] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -639,7 +590,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
durationUpdated = true
|
durationUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,13 +605,13 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else {
|
guard let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[bridgeId] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mediaSource.sourceBufferIds = ids
|
mediaSource.sourceBufferIds = ids
|
||||||
|
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,7 +630,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let sourceBuffer = SourceBuffer(mediaSourceId: mediaSourceId, mimeType: mimeType)
|
let sourceBuffer = SourceBuffer(mediaSourceId: mediaSourceId, mimeType: mimeType)
|
||||||
SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] = sourceBuffer
|
SharedHLSVideoJSContext.shared.sourceBuffers[bridgeId] = sourceBuffer
|
||||||
|
|
||||||
completion([:])
|
completion([:])
|
||||||
} else if (methodName == "appendBuffer") {
|
} else if (methodName == "appendBuffer") {
|
||||||
@ -691,7 +642,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else {
|
guard let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[bridgeId] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -703,7 +654,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else {
|
guard let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[bridgeId] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -711,7 +662,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
completion(["ranges": serializeRanges(bufferedRanges)])
|
completion(["ranges": serializeRanges(bufferedRanges)])
|
||||||
})
|
})
|
||||||
} else if methodName == "abort" {
|
} else if methodName == "abort" {
|
||||||
guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else {
|
guard let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[bridgeId] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -766,51 +717,38 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
let filePath = String(requestPath[firstSlash.upperBound...])
|
let filePath = String(requestPath[firstSlash.upperBound...])
|
||||||
if filePath == "master.m3u8" {
|
if filePath == "master.m3u8" {
|
||||||
let _ = (source.masterPlaylistData()
|
let _ = (source.masterPlaylistData()
|
||||||
|> deliverOn(.mainQueue())
|
|> take(1)).start(next: { result in
|
||||||
|> take(1)).start(next: { [weak self] result in
|
SharedHLSVideoJSContext.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion)
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion)
|
|
||||||
})
|
})
|
||||||
} else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") {
|
} else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") {
|
||||||
guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else {
|
guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else {
|
||||||
self.sendErrorAndClose(id: id, error: .notFound, completion: completion)
|
SharedHLSVideoJSContext.sendErrorAndClose(id: id, error: .notFound, completion: completion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = (source.playlistData(quality: levelIndex)
|
let _ = (source.playlistData(quality: levelIndex)
|
||||||
|> deliverOn(.mainQueue())
|
|> deliverOn(.mainQueue())
|
||||||
|> take(1)).start(next: { [weak self] result in
|
|> take(1)).start(next: { result in
|
||||||
guard let self else {
|
SharedHLSVideoJSContext.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion)
|
|
||||||
})
|
})
|
||||||
} else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") {
|
} else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") {
|
||||||
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)])
|
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)])
|
||||||
guard let fileIdValue = Int64(fileId) else {
|
guard let fileIdValue = Int64(fileId) else {
|
||||||
self.sendErrorAndClose(id: id, error: .notFound, completion: completion)
|
SharedHLSVideoJSContext.sendErrorAndClose(id: id, error: .notFound, completion: completion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let requestRange else {
|
guard let requestRange else {
|
||||||
self.sendErrorAndClose(id: id, error: .badRequest, completion: completion)
|
SharedHLSVideoJSContext.sendErrorAndClose(id: id, error: .badRequest, completion: completion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|
let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|
||||||
|> deliverOn(.mainQueue())
|
|> deliverOn(.mainQueue())
|
||||||
//|> timeout(5.0, queue: self.queue, alternate: .single(nil))
|
//|> timeout(5.0, queue: self.queue, alternate: .single(nil))
|
||||||
|> take(1)).start(next: { [weak self] result in
|
|> take(1)).start(next: { result in
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (tempFile, tempFileRange, totalSize) = result {
|
if let (tempFile, tempFileRange, totalSize) = result {
|
||||||
self.sendResponseFileAndClose(id: id, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize, completion: completion)
|
SharedHLSVideoJSContext.sendResponseFileAndClose(id: id, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
SharedHLSVideoJSContext.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -822,58 +760,6 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
if (!handlerFound) {
|
if (!handlerFound) {
|
||||||
completion(["error": 1])
|
completion(["error": 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
/*var request = URLRequest(url: URL(string: url)!)
|
|
||||||
for (key, value) in requestHeaders {
|
|
||||||
request.setValue(value, forHTTPHeaderField: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
let isCompleted = Atomic<Bool>(value: false)
|
|
||||||
let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] data, response, error in
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isCompleted.swap(true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tempTasks.removeValue(forKey: id)
|
|
||||||
|
|
||||||
if let _ = error {
|
|
||||||
completion([
|
|
||||||
"error": 1
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
if let response = response as? HTTPURLResponse {
|
|
||||||
completion([
|
|
||||||
"status": response.statusCode,
|
|
||||||
"statusText": "OK",
|
|
||||||
"responseData": data?.base64EncodedString() ?? "",
|
|
||||||
"responseHeaders": response.allHeaderFields as? [String: String] ?? [:]
|
|
||||||
])
|
|
||||||
|
|
||||||
let _ = response
|
|
||||||
/*if let response = response as? HTTPURLResponse, let requestUrl {
|
|
||||||
if let updatedResponse = HTTPURLResponse(
|
|
||||||
url: requestUrl,
|
|
||||||
statusCode: response.statusCode,
|
|
||||||
httpVersion: "HTTP/1.1",
|
|
||||||
headerFields: response.allHeaderFields as? [String: String] ?? [:]
|
|
||||||
) {
|
|
||||||
sourceTask.didReceive(updatedResponse)
|
|
||||||
} else {
|
|
||||||
sourceTask.didReceive(response)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sourceTask.didReceive(response)
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self.tempTasks[id] = task
|
|
||||||
task.resume()*/
|
|
||||||
} else if methodName == "abort" {
|
} else if methodName == "abort" {
|
||||||
guard let id = params["id"] as? Int else {
|
guard let id = params["id"] as? Int else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
@ -889,7 +775,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendErrorAndClose(id: Int, error: ResponseError, completion: @escaping ([String: Any]) -> Void) {
|
private static func sendErrorAndClose(id: Int, error: ResponseError, completion: @escaping ([String: Any]) -> Void) {
|
||||||
let (code, status) = error.httpStatus
|
let (code, status) = error.httpStatus
|
||||||
completion([
|
completion([
|
||||||
"status": code,
|
"status": code,
|
||||||
@ -901,7 +787,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendResponseAndClose(id: Int, data: Data, contentType: String = "application/octet-stream", completion: @escaping ([String: Any]) -> Void) {
|
private static func sendResponseAndClose(id: Int, data: Data, contentType: String = "application/octet-stream", completion: @escaping ([String: Any]) -> Void) {
|
||||||
completion([
|
completion([
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"statusText": "OK",
|
"statusText": "OK",
|
||||||
@ -913,20 +799,22 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendResponseFileAndClose(id: Int, file: TempBoxFile, fileRange: Range<Int>, range: Range<Int>, totalSize: Int, completion: @escaping ([String: Any]) -> Void) {
|
private static func sendResponseFileAndClose(id: Int, file: TempBoxFile, fileRange: Range<Int>, range: Range<Int>, totalSize: Int, completion: @escaping ([String: Any]) -> Void) {
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe).subdata(in: fileRange) {
|
Queue.concurrentDefaultQueue().async {
|
||||||
completion([
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe).subdata(in: fileRange) {
|
||||||
"status": 200,
|
completion([
|
||||||
"statusText": "OK",
|
"status": 200,
|
||||||
"responseData": data.base64EncodedString(),
|
"statusText": "OK",
|
||||||
"responseHeaders": [
|
"responseData": data.base64EncodedString(),
|
||||||
"Content-Type": "application/octet-stream",
|
"responseHeaders": [
|
||||||
"Content-Range": "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)",
|
"Content-Type": "application/octet-stream",
|
||||||
"Content-Length": "\(fileRange.upperBound - fileRange.lowerBound)"
|
"Content-Range": "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)",
|
||||||
] as [String: String]
|
"Content-Length": "\(fileRange.upperBound - fileRange.lowerBound)"
|
||||||
])
|
] as [String: String]
|
||||||
} else {
|
])
|
||||||
self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
} else {
|
||||||
|
SharedHLSVideoJSContext.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -934,8 +822,8 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
let contextInstanceId = context.instanceId
|
let contextInstanceId = context.instanceId
|
||||||
self.contextReferences[contextInstanceId] = ContextReference(contentNode: context)
|
self.contextReferences[contextInstanceId] = ContextReference(contentNode: context)
|
||||||
|
|
||||||
if self.webView == nil {
|
if self.jsContext == nil {
|
||||||
self.createWebView()
|
self.createJsContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let emptyTimer = self.emptyTimer {
|
if let emptyTimer = self.emptyTimer {
|
||||||
@ -960,7 +848,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.webView?.evaluateJavaScript("window.hlsPlayer_destroyInstance(\(contextInstanceId));")
|
self.jsContext?.evaluateJavaScript("window.hlsPlayer_destroyInstance(\(contextInstanceId));")
|
||||||
|
|
||||||
if self.contextReferences.isEmpty {
|
if self.contextReferences.isEmpty {
|
||||||
if self.emptyTimer == nil {
|
if self.emptyTimer == nil {
|
||||||
@ -972,7 +860,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
self.emptyTimer = nil
|
self.emptyTimer = nil
|
||||||
}
|
}
|
||||||
if self.contextReferences.isEmpty {
|
if self.contextReferences.isEmpty {
|
||||||
self.disposeWebView()
|
self.disposeJsContext()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -984,7 +872,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
func initializeWhenReady(context: HLSVideoJSNativeContentNode, urlPrefix: String) {
|
func initializeWhenReady(context: HLSVideoJSNativeContentNode, urlPrefix: String) {
|
||||||
self.pendingInitializeInstanceIds.append((context.instanceId, urlPrefix))
|
self.pendingInitializeInstanceIds.append((context.instanceId, urlPrefix))
|
||||||
|
|
||||||
if self.isWebViewReady {
|
if self.isJsContextReady {
|
||||||
self.initializePendingInstances()
|
self.initializePendingInstances()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1020,7 +908,7 @@ private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.webView?.evaluateJavaScript(userScriptJs)
|
self.jsContext?.evaluateJavaScript(userScriptJs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,6 +945,8 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
private let approximateDuration: Double
|
private let approximateDuration: Double
|
||||||
private let intrinsicDimensions: CGSize
|
private let intrinsicDimensions: CGSize
|
||||||
|
|
||||||
|
private var enableSound: Bool
|
||||||
|
|
||||||
private let audioSessionManager: ManagedAudioSession
|
private let audioSessionManager: ManagedAudioSession
|
||||||
private let audioSessionDisposable = MetaDisposable()
|
private let audioSessionDisposable = MetaDisposable()
|
||||||
private var hasAudioSession = false
|
private var hasAudioSession = false
|
||||||
@ -1150,6 +1040,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self.audioSessionManager = audioSessionManager
|
self.audioSessionManager = audioSessionManager
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
self.requestedBaseRate = baseRate
|
self.requestedBaseRate = baseRate
|
||||||
|
self.enableSound = enableSound
|
||||||
|
|
||||||
if var dimensions = fileReference.media.dimensions {
|
if var dimensions = fileReference.media.dimensions {
|
||||||
if let thumbnail = fileReference.media.previewRepresentations.first {
|
if let thumbnail = fileReference.media.previewRepresentations.first {
|
||||||
@ -1186,7 +1077,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
audioSessionManager: audioSessionManager,
|
audioSessionManager: audioSessionManager,
|
||||||
partsState: self.chunkPlayerPartsState.get(),
|
partsState: self.chunkPlayerPartsState.get(),
|
||||||
video: true,
|
video: true,
|
||||||
enableSound: true,
|
enableSound: self.enableSound,
|
||||||
baseRate: baseRate,
|
baseRate: baseRate,
|
||||||
onSeeked: {
|
onSeeked: {
|
||||||
onSeeked?()
|
onSeeked?()
|
||||||
@ -1198,7 +1089,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.contextDisposable = SharedHLSVideoWebView.shared.register(context: self)
|
self.contextDisposable = SharedHLSVideoJSContext.shared.register(context: self)
|
||||||
|
|
||||||
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
|
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
|
||||||
|
|
||||||
@ -1255,12 +1146,12 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerNotifySeekedOnNextStatusUpdate();", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerNotifySeekedOnNextStatusUpdate();")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let playerSource {
|
if let playerSource {
|
||||||
SharedHLSVideoWebView.shared.initializeWhenReady(context: self, urlPrefix: "http://server/\(playerSource.id)/")
|
SharedHLSVideoJSContext.shared.initializeWhenReady(context: self, urlPrefix: "http://server/\(playerSource.id)/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1380,13 +1271,21 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key
|
selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key
|
||||||
}
|
}
|
||||||
if let selectedLevelIndex {
|
if let selectedLevelIndex {
|
||||||
|
var effectiveSelectedLevelIndex = selectedLevelIndex
|
||||||
|
if !self.enableSound {
|
||||||
|
effectiveSelectedLevelIndex = self.resolveCurrentLevelIndex() ?? -1
|
||||||
|
}
|
||||||
|
|
||||||
self.hasRequestedPlayerLoad = true
|
self.hasRequestedPlayerLoad = true
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerLoad(\(selectedLevelIndex));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("""
|
||||||
|
window.hlsPlayer_instances[\(self.instanceId)].playerSetCapAutoLevel(\(self.resolveCurrentLevelIndex() ?? -1));
|
||||||
|
window.hlsPlayer_instances[\(self.instanceId)].playerLoad(\(effectiveSelectedLevelIndex));
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateStatus()
|
self.updateStatus()
|
||||||
@ -1415,13 +1314,13 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func onMediaSourceDurationUpdated() {
|
fileprivate func onMediaSourceDurationUpdated() {
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else {
|
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[mediaSourceId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else {
|
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[sourceBufferId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1429,13 +1328,13 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func onMediaSourceBuffersUpdated() {
|
fileprivate func onMediaSourceBuffersUpdated() {
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else {
|
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[mediaSourceId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else {
|
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[sourceBufferId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1446,7 +1345,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
guard let self, let sourceBuffer else {
|
guard let self, let sourceBuffer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[sourceBuffer.mediaSourceId] else {
|
guard let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[sourceBuffer.mediaSourceId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items)))
|
self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items)))
|
||||||
@ -1459,7 +1358,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
private func updatePlayerStatus(status: MediaPlayerStatus) {
|
private func updatePlayerStatus(status: MediaPlayerStatus) {
|
||||||
self._status.set(status)
|
self._status.set(status)
|
||||||
|
|
||||||
if let (bridgeId, _) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
if let (bridgeId, _) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
||||||
var isPlaying: Bool = false
|
var isPlaying: Bool = false
|
||||||
var isBuffering = false
|
var isBuffering = false
|
||||||
switch status.status {
|
switch status.status {
|
||||||
@ -1480,25 +1379,25 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
let jsonResult = try! JSONSerialization.data(withJSONObject: result)
|
let jsonResult = try! JSONSerialization.data(withJSONObject: result)
|
||||||
let jsonResultString = String(data: jsonResult, encoding: .utf8)!
|
let jsonResultString = String(data: jsonResult, encoding: .utf8)!
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.bridgeObjectMap[\(bridgeId)].bridgeUpdateStatus(\(jsonResultString));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("window.bridgeObjectMap[\(bridgeId)].bridgeUpdateStatus(\(jsonResultString));")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateBuffered() {
|
private func updateBuffered() {
|
||||||
guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
guard let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else {
|
guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[mediaSourceId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else {
|
guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoJSContext.shared.sourceBuffers[sourceBufferId] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let bufferedRanges = sourceBuffer.ranges
|
let bufferedRanges = sourceBuffer.ranges
|
||||||
|
|
||||||
if let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
if let (_, videoElement) = SharedHLSVideoJSContext.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
||||||
if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] {
|
if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoJSContext.shared.mediaSources[mediaSourceId] {
|
||||||
if let duration = mediaSource.duration {
|
if let duration = mediaSource.duration {
|
||||||
var mappedRanges = RangeSet<Int64>()
|
var mappedRanges = RangeSet<Int64>()
|
||||||
for range in bufferedRanges.ranges {
|
for range in bufferedRanges.ranges {
|
||||||
@ -1538,7 +1437,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
func play() {
|
func play() {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
if !self.initializedStatus {
|
if !self.initializedStatus {
|
||||||
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
|
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: self.enableSound))
|
||||||
}
|
}
|
||||||
self.player.play()
|
self.player.play()
|
||||||
}
|
}
|
||||||
@ -1555,10 +1454,14 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
func setSoundEnabled(_ value: Bool) {
|
func setSoundEnabled(_ value: Bool) {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
if value {
|
if self.enableSound != value {
|
||||||
self.player.playOnceWithSound(playAndRecord: false, seek: .none)
|
self.enableSound = value
|
||||||
} else {
|
if value {
|
||||||
self.player.continuePlayingWithoutSound(seek: .none)
|
self.player.playOnceWithSound(playAndRecord: false, seek: .none)
|
||||||
|
} else {
|
||||||
|
self.player.continuePlayingWithoutSound(seek: .none)
|
||||||
|
}
|
||||||
|
self.updateInternalQualityLevel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1566,7 +1469,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
self.seekId += 1
|
self.seekId += 1
|
||||||
|
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSeek(\(timestamp));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSeek(\(timestamp));")
|
||||||
}
|
}
|
||||||
|
|
||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||||
@ -1576,6 +1479,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self?.performActionAtEnd()
|
self?.performActionAtEnd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.enableSound = true
|
||||||
switch actionAtEnd {
|
switch actionAtEnd {
|
||||||
case .loop:
|
case .loop:
|
||||||
self.player.actionAtEnd = .loop({})
|
self.player.actionAtEnd = .loop({})
|
||||||
@ -1604,6 +1508,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
||||||
|
self.updateInternalQualityLevel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSoundMuted(soundMuted: Bool) {
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
@ -1626,6 +1531,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self?.performActionAtEnd()
|
self?.performActionAtEnd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.enableSound = false
|
||||||
switch actionAtEnd {
|
switch actionAtEnd {
|
||||||
case .loop:
|
case .loop:
|
||||||
self.player.actionAtEnd = .loop({})
|
self.player.actionAtEnd = .loop({})
|
||||||
@ -1635,6 +1541,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self.player.actionAtEnd = .action(action)
|
self.player.actionAtEnd = .action(action)
|
||||||
}
|
}
|
||||||
self.player.continuePlayingWithoutSound(seek: .none)
|
self.player.continuePlayingWithoutSound(seek: .none)
|
||||||
|
self.updateInternalQualityLevel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
||||||
@ -1644,15 +1551,41 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
func setBaseRate(_ baseRate: Double) {
|
func setBaseRate(_ baseRate: Double) {
|
||||||
self.requestedBaseRate = baseRate
|
self.requestedBaseRate = baseRate
|
||||||
if self.playerIsReady {
|
if self.playerIsReady {
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));")
|
||||||
}
|
}
|
||||||
self.updateStatus()
|
self.updateStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolveCurrentLevelIndex() -> Int? {
|
||||||
|
if self.enableSound {
|
||||||
|
return self.requestedLevelIndex
|
||||||
|
} else {
|
||||||
|
var foundIndex: Int?
|
||||||
|
if let minQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file, let dimensions = minQualityFile.media.dimensions {
|
||||||
|
for (index, level) in self.playerAvailableLevels {
|
||||||
|
if level.width == Int(dimensions.width) && level.height == Int(dimensions.height) {
|
||||||
|
foundIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateInternalQualityLevel() {
|
||||||
|
if self.playerIsReady {
|
||||||
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("""
|
||||||
|
window.hlsPlayer_instances[\(self.instanceId)].playerSetCapAutoLevel(\(self.resolveCurrentLevelIndex() ?? -1));
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
|
||||||
self.preferredVideoQuality = videoQuality
|
self.preferredVideoQuality = videoQuality
|
||||||
|
|
||||||
switch videoQuality {
|
let resolvedVideoQuality = self.preferredVideoQuality
|
||||||
|
switch resolvedVideoQuality {
|
||||||
case .auto:
|
case .auto:
|
||||||
self.requestedLevelIndex = nil
|
self.requestedLevelIndex = nil
|
||||||
case let .quality(quality):
|
case let .quality(quality):
|
||||||
@ -1666,7 +1599,9 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self.updateVideoQualityState()
|
self.updateVideoQualityState()
|
||||||
|
|
||||||
if self.playerIsReady {
|
if self.playerIsReady {
|
||||||
SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetLevel(\(self.requestedLevelIndex ?? -1));", completionHandler: nil)
|
SharedHLSVideoJSContext.shared.jsContext?.evaluateJavaScript("""
|
||||||
|
window.hlsPlayer_instances[\(self.instanceId)].playerSetLevel(\(self.requestedLevelIndex ?? -1));
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
@preconcurrency import WebKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
|
private let f: (WKScriptMessage) -> ()
|
||||||
|
|
||||||
|
init(_ f: @escaping (WKScriptMessage) -> ()) {
|
||||||
|
self.f = f
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
|
||||||
|
self.f(scriptMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WebViewHLSJSContextImpl: HLSJSContext {
|
||||||
|
let webView: WKWebView
|
||||||
|
|
||||||
|
init(handleScriptMessage: @escaping ([String: Any]) -> Void) {
|
||||||
|
let config = WKWebViewConfiguration()
|
||||||
|
config.allowsInlineMediaPlayback = true
|
||||||
|
config.mediaTypesRequiringUserActionForPlayback = []
|
||||||
|
config.allowsPictureInPictureMediaPlayback = true
|
||||||
|
|
||||||
|
let userController = WKUserContentController()
|
||||||
|
|
||||||
|
var handleScriptMessageImpl: (([String: Any]) -> Void)?
|
||||||
|
userController.add(WeakScriptMessageHandler { message in
|
||||||
|
guard let body = message.body as? [String: Any] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleScriptMessageImpl?(body)
|
||||||
|
}, name: "performAction")
|
||||||
|
|
||||||
|
let isDebug: Bool
|
||||||
|
#if DEBUG
|
||||||
|
isDebug = true
|
||||||
|
#else
|
||||||
|
isDebug = false
|
||||||
|
#endif
|
||||||
|
|
||||||
|
config.userContentController = userController
|
||||||
|
|
||||||
|
let webView = WKWebView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)), configuration: config)
|
||||||
|
self.webView = webView
|
||||||
|
|
||||||
|
webView.scrollView.isScrollEnabled = false
|
||||||
|
webView.allowsLinkPreview = false
|
||||||
|
webView.allowsBackForwardNavigationGestures = false
|
||||||
|
webView.accessibilityIgnoresInvertColors = true
|
||||||
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
webView.alpha = 0.0
|
||||||
|
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
webView.isInspectable = isDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScriptMessageImpl = { message in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
handleScriptMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundle = Bundle(for: WebViewHLSJSContextImpl.self)
|
||||||
|
let bundlePath = bundle.bundlePath + "/HlsBundle.bundle"
|
||||||
|
webView.loadFileURL(URL(fileURLWithPath: bundlePath + "/index.html"), allowingReadAccessTo: URL(fileURLWithPath: bundlePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateJavaScript(_ string: String) {
|
||||||
|
self.webView.evaluateJavaScript(string, completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import JavaScriptCore
|
||||||
|
import TelegramCore
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
private var ObjCKey_ContextReference: Int?
|
||||||
|
|
||||||
|
@objc private protocol JsCorePolyfillsExport: JSExport {
|
||||||
|
func postMessage(_ object: JSValue)
|
||||||
|
func consoleLog(_ object: JSValue)
|
||||||
|
func consoleLog(_ object: JSValue, _ arg1: JSValue)
|
||||||
|
func consoleLog(_ object: JSValue, _ arg1: JSValue, _ arg2: JSValue)
|
||||||
|
func performanceNow() -> Double
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private final class JsCorePolyfills: NSObject, JsCorePolyfillsExport {
|
||||||
|
private let queue: Queue
|
||||||
|
private let context: WebViewNativeJSContextImpl.Reference
|
||||||
|
|
||||||
|
init(queue: Queue, context: WebViewNativeJSContextImpl.Reference) {
|
||||||
|
self.queue = queue
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func postMessage(_ object: JSValue) {
|
||||||
|
guard object.isObject else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let message = object.toDictionary() as? [String: Any] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let context = self.context
|
||||||
|
self.queue.async {
|
||||||
|
guard let context = context.context else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let handleScriptMessage = context.handleScriptMessage
|
||||||
|
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
handleScriptMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func consoleLog(_ object: JSValue) {
|
||||||
|
#if DEBUG
|
||||||
|
print("\(object)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func consoleLog(_ object: JSValue, _ arg1: JSValue) {
|
||||||
|
#if DEBUG
|
||||||
|
print("\(object) \(arg1)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func consoleLog(_ object: JSValue, _ arg1: JSValue, _ arg2: JSValue) {
|
||||||
|
#if DEBUG
|
||||||
|
print("\(object) \(arg1) \(arg2)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func performanceNow() -> Double {
|
||||||
|
return CFAbsoluteTimeGetCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private protocol TimerJSExport: JSExport {
|
||||||
|
func setTimeout(_ callback: JSValue, _ ms: Double) -> Int32
|
||||||
|
func setInterval(_ callback: JSValue, _ ms: Double) -> Int32
|
||||||
|
|
||||||
|
func clearTimeout(_ id: Int32)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private class TimeoutPolyfill: NSObject, TimerJSExport {
|
||||||
|
private let queue: Queue
|
||||||
|
|
||||||
|
private var timers: [Int32: SwiftSignalKit.Timer] = [:]
|
||||||
|
private var nextId: Int32 = 0
|
||||||
|
|
||||||
|
init(queue: Queue) {
|
||||||
|
self.queue = queue
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
for (_, timer) in self.timers {
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func register(jsContext: JSContext) {
|
||||||
|
jsContext.evaluateScript("""
|
||||||
|
function setTimeout(...args) {
|
||||||
|
if (args.length === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [callback, delay = 0, ...callbackArgs] = args;
|
||||||
|
|
||||||
|
return _timeoutPolyfill.setTimeout(() => {
|
||||||
|
callback(...callbackArgs);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInterval(...args) {
|
||||||
|
if (args.length === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [callback, delay = 0, ...callbackArgs] = args;
|
||||||
|
|
||||||
|
return _timeoutPolyfill.setInterval(() => {
|
||||||
|
callback(...callbackArgs);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTimeout(indentifier) {
|
||||||
|
_timeoutPolyfill.clearTimeout(indentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInterval(indentifier) {
|
||||||
|
_timeoutPolyfill.clearTimeout(indentifier)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearTimeout(_ id: Int32) {
|
||||||
|
let timer = self.timers.removeValue(forKey: id)
|
||||||
|
timer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTimeout(_ callback: JSValue, _ ms: Double) -> Int32 {
|
||||||
|
return self.createTimer(callback: callback, ms: ms, repeats: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInterval(_ callback: JSValue, _ ms: Double) -> Int32 {
|
||||||
|
return self.createTimer(callback: callback, ms: ms, repeats: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTimer(callback: JSValue, ms: Double, repeats: Bool) -> Int32 {
|
||||||
|
let timeInterval = ms / 1000.0
|
||||||
|
|
||||||
|
let id = self.nextId
|
||||||
|
self.nextId += 1
|
||||||
|
let timer = SwiftSignalKit.Timer(timeout: timeInterval, repeat: repeats, completion: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback.call(withArguments: nil)
|
||||||
|
|
||||||
|
if !repeats {
|
||||||
|
self.timers.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
}, queue: self.queue)
|
||||||
|
self.timers[id] = timer
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WebViewNativeJSContextImpl: HLSJSContext {
|
||||||
|
fileprivate final class Reference {
|
||||||
|
weak var context: WebViewNativeJSContextImpl.Impl?
|
||||||
|
|
||||||
|
init(context: WebViewNativeJSContextImpl.Impl) {
|
||||||
|
self.context = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate final class Impl {
|
||||||
|
let queue: Queue
|
||||||
|
let context: JSContext
|
||||||
|
let handleScriptMessage: ([String: Any]) -> Void
|
||||||
|
|
||||||
|
init(queue: Queue, handleScriptMessage: @escaping ([String: Any]) -> Void) {
|
||||||
|
self.queue = queue
|
||||||
|
self.context = JSContext()
|
||||||
|
self.handleScriptMessage = handleScriptMessage
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
self.context.isInspectable = true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
self.context.exceptionHandler = { context, exception in
|
||||||
|
if let exception {
|
||||||
|
Logger.shared.log("WebViewNativeJSContextImpl", "JS exception: \(exception)")
|
||||||
|
#if DEBUG
|
||||||
|
print("JS exception: \(exception)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutPolyfill = TimeoutPolyfill(queue: self.queue)
|
||||||
|
self.context.setObject(timeoutPolyfill, forKeyedSubscript: "_timeoutPolyfill" as (NSCopying & NSObjectProtocol))
|
||||||
|
timeoutPolyfill.register(jsContext: self.context)
|
||||||
|
|
||||||
|
self.context.setObject(JsCorePolyfills(queue: self.queue, context: Reference(context: self)), forKeyedSubscript: "_JsCorePolyfills" as (NSCopying & NSObjectProtocol))
|
||||||
|
|
||||||
|
let bundle = Bundle(for: WebViewHLSJSContextImpl.self)
|
||||||
|
let bundlePath = bundle.bundlePath + "/HlsBundle.bundle"
|
||||||
|
if let indexJsString = try? String(contentsOf: URL(fileURLWithPath: bundlePath + "/headless_prologue.js"), encoding: .utf8) {
|
||||||
|
self.context.evaluateScript(indexJsString, withSourceURL: URL(fileURLWithPath: "index/index.bundle.js"))
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let indexJsString = try? String(contentsOf: URL(fileURLWithPath: bundlePath + "/index.bundle.js"), encoding: .utf8) {
|
||||||
|
self.context.evaluateScript(indexJsString, withSourceURL: URL(fileURLWithPath: "index.bundle.js"))
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("WebViewNativeJSContextImpl.deinit")
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateJavaScript(_ string: String) {
|
||||||
|
self.context.evaluateScript(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let sharedQueue = Queue(name: "WebViewNativeJSContextImpl", qos: .default)
|
||||||
|
|
||||||
|
private let queue: Queue
|
||||||
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
|
||||||
|
init(handleScriptMessage: @escaping ([String: Any]) -> Void) {
|
||||||
|
let queue = WebViewNativeJSContextImpl.sharedQueue
|
||||||
|
self.queue = queue
|
||||||
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||||
|
return Impl(queue: queue, handleScriptMessage: handleScriptMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateJavaScript(_ string: String) {
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.evaluateJavaScript(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -230,7 +230,7 @@ class WebSearchGalleryController: ViewController {
|
|||||||
}, controller: { [weak self] in
|
}, controller: { [weak self] in
|
||||||
return self
|
return self
|
||||||
})
|
})
|
||||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.galleryNode.statusBar = self.statusBar
|
self.galleryNode.statusBar = self.statusBar
|
||||||
|
Loading…
x
Reference in New Issue
Block a user