mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Video player improvements
This commit is contained in:
parent
9a55df8fc9
commit
1e75c0fa02
@ -53,6 +53,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
|
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
|
||||||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||||
"//submodules/UrlHandling",
|
"//submodules/UrlHandling",
|
||||||
|
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -27,6 +27,7 @@ import Pasteboard
|
|||||||
import AdUI
|
import AdUI
|
||||||
import AdsInfoScreen
|
import AdsInfoScreen
|
||||||
import AdsReportScreen
|
import AdsReportScreen
|
||||||
|
import SaveProgressScreen
|
||||||
|
|
||||||
public enum UniversalVideoGalleryItemContentInfo {
|
public enum UniversalVideoGalleryItemContentInfo {
|
||||||
case message(Message, Int?)
|
case message(Message, Int?)
|
||||||
@ -3238,28 +3239,104 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil && !(item.content is HLSVideoContent) {
|
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in
|
||||||
f(.default)
|
guard let self else {
|
||||||
|
c?.dismiss(result: .default, completion: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if let strongSelf = self {
|
if let content = item.content as? HLSVideoContent {
|
||||||
switch strongSelf.fetchStatus {
|
guard let videoNode = self.videoNode, let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if qualityState.available.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let qualitySet = HLSQualitySet(baseFile: content.fileReference) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, iconPosition: .left, action: { c, _ in
|
||||||
|
c?.popItems()
|
||||||
|
})))
|
||||||
|
|
||||||
|
for quality in qualityState.available {
|
||||||
|
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
guard let qualityFileSize = qualityFile.media.size else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "\(quality)p (\(fileSizeString))", icon: { _ in
|
||||||
|
return nil
|
||||||
|
}, action: { [weak self] c, _ in
|
||||||
|
c?.dismiss(result: .default, completion: nil)
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let controller = self.galleryController() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let saveScreen = SaveProgressScreen(context: self.context, content: .progress(self.presentationData.strings.Story_TooltipSaving, 0.0))
|
||||||
|
controller.present(saveScreen, in: .current)
|
||||||
|
|
||||||
|
let stringSaving = self.presentationData.strings.Story_TooltipSaving
|
||||||
|
let stringSaved = self.presentationData.strings.Story_TooltipSaved
|
||||||
|
|
||||||
|
let saveFileReference: AnyMediaReference = qualityFile.abstract
|
||||||
|
let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference)
|
||||||
|
|
||||||
|
let disposable = (saveSignal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak saveScreen] progress in
|
||||||
|
guard let saveScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveScreen.content = .progress(stringSaving, progress)
|
||||||
|
}, completed: { [weak saveScreen] in
|
||||||
|
guard let saveScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveScreen.content = .completion(stringSaved)
|
||||||
|
Queue.mainQueue().after(3.0, { [weak saveScreen] in
|
||||||
|
saveScreen?.dismiss()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
saveScreen.cancelled = {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
|
||||||
|
} else {
|
||||||
|
c?.dismiss(result: .default, completion: nil)
|
||||||
|
|
||||||
|
switch self.fetchStatus {
|
||||||
case .Local:
|
case .Local:
|
||||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file))
|
let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file))
|
||||||
|> deliverOnMainQueue).start(completed: {
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let controller = strongSelf.galleryController() else {
|
guard let controller = self.galleryController() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
controller.present(UndoOverlayController(presentationData: self.presentationData, content: .mediaSaved(text: self.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
guard let controller = strongSelf.galleryController() else {
|
guard let controller = self.galleryController() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
controller.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
|
||||||
})]), in: .window(.root))
|
})]), in: .window(.root))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,13 +63,6 @@ public enum ChunkMediaPlayerPlayOnceWithSoundActionAtEnd {
|
|||||||
case repeatIfNeeded
|
case repeatIfNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChunkMediaPlayerSeek {
|
|
||||||
case none
|
|
||||||
case start
|
|
||||||
case automatic
|
|
||||||
case timecode(Double)
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ChunkMediaPlayerStreaming {
|
public enum ChunkMediaPlayerStreaming {
|
||||||
case none
|
case none
|
||||||
case conservative
|
case conservative
|
||||||
@ -165,9 +158,11 @@ private final class ChunkMediaPlayerContext {
|
|||||||
private var keepAudioSessionWhilePaused: Bool
|
private var keepAudioSessionWhilePaused: Bool
|
||||||
private var continuePlayingWithoutSoundOnLostAudioSession: Bool
|
private var continuePlayingWithoutSoundOnLostAudioSession: Bool
|
||||||
private let isAudioVideoMessage: Bool
|
private let isAudioVideoMessage: Bool
|
||||||
|
private let onSeeked: () -> Void
|
||||||
|
|
||||||
private var seekId: Int = 0
|
private var seekId: Int = 0
|
||||||
private var initialSeekTimestamp: Double?
|
private var initialSeekTimestamp: Double?
|
||||||
|
private var notifySeeked: Bool = false
|
||||||
|
|
||||||
private let loadedState: ChunkMediaPlayerLoadedState
|
private let loadedState: ChunkMediaPlayerLoadedState
|
||||||
private var isSeeking: Bool = false
|
private var isSeeking: Bool = false
|
||||||
@ -206,7 +201,8 @@ private final class ChunkMediaPlayerContext {
|
|||||||
mixWithOthers: Bool,
|
mixWithOthers: Bool,
|
||||||
keepAudioSessionWhilePaused: Bool,
|
keepAudioSessionWhilePaused: Bool,
|
||||||
continuePlayingWithoutSoundOnLostAudioSession: Bool,
|
continuePlayingWithoutSoundOnLostAudioSession: Bool,
|
||||||
isAudioVideoMessage: Bool
|
isAudioVideoMessage: Bool,
|
||||||
|
onSeeked: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
|
|
||||||
@ -225,6 +221,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
||||||
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
|
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
|
||||||
self.isAudioVideoMessage = isAudioVideoMessage
|
self.isAudioVideoMessage = isAudioVideoMessage
|
||||||
|
self.onSeeked = onSeeked
|
||||||
|
|
||||||
self.videoRenderer = VideoPlayerProxy(queue: queue)
|
self.videoRenderer = VideoPlayerProxy(queue: queue)
|
||||||
|
|
||||||
@ -261,7 +258,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
self.pause(lostAudioSession: true, faded: false)
|
self.pause(lostAudioSession: true, faded: false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.seek(timestamp: 0.0, action: .play)
|
self.seek(timestamp: 0.0, action: .play, notify: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,7 +351,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
self.partsDisposable?.dispose()
|
self.partsDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func seek(timestamp: Double) {
|
fileprivate func seek(timestamp: Double, notify: Bool) {
|
||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
let action: ChunkMediaPlayerPlaybackAction
|
let action: ChunkMediaPlayerPlaybackAction
|
||||||
@ -364,10 +361,10 @@ private final class ChunkMediaPlayerContext {
|
|||||||
case .playing:
|
case .playing:
|
||||||
action = .play
|
action = .play
|
||||||
}
|
}
|
||||||
self.seek(timestamp: timestamp, action: action)
|
self.seek(timestamp: timestamp, action: action, notify: notify)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func seek(timestamp: Double, action: ChunkMediaPlayerPlaybackAction) {
|
fileprivate func seek(timestamp: Double, action: ChunkMediaPlayerPlaybackAction, notify: Bool) {
|
||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
self.isSeeking = true
|
self.isSeeking = true
|
||||||
@ -375,6 +372,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
|
|
||||||
self.seekId += 1
|
self.seekId += 1
|
||||||
self.initialSeekTimestamp = timestamp
|
self.initialSeekTimestamp = timestamp
|
||||||
|
self.notifySeeked = true
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .play:
|
case .play:
|
||||||
@ -421,11 +419,11 @@ private final class ChunkMediaPlayerContext {
|
|||||||
timestamp = self.initialSeekTimestamp ?? 0.0
|
timestamp = self.initialSeekTimestamp ?? 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
self.seek(timestamp: timestamp, action: .play)
|
self.seek(timestamp: timestamp, action: .play, notify: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func playOnceWithSound(playAndRecord: Bool, seek: ChunkMediaPlayerSeek = .start) {
|
fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
if !self.enableSound {
|
if !self.enableSound {
|
||||||
@ -446,10 +444,10 @@ private final class ChunkMediaPlayerContext {
|
|||||||
} else {
|
} else {
|
||||||
timestamp = 0.0
|
timestamp = 0.0
|
||||||
}
|
}
|
||||||
self.seek(timestamp: timestamp, action: .play)
|
self.seek(timestamp: timestamp, action: .play, notify: true)
|
||||||
} else {
|
} else {
|
||||||
if case let .timecode(time) = seek {
|
if case let .timecode(time) = seek {
|
||||||
self.seek(timestamp: Double(time), action: .play)
|
self.seek(timestamp: Double(time), action: .play, notify: true)
|
||||||
} else if case .playing = self.state {
|
} else if case .playing = self.state {
|
||||||
} else {
|
} else {
|
||||||
self.play()
|
self.play()
|
||||||
@ -471,7 +469,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func continuePlayingWithoutSound(seek: ChunkMediaPlayerSeek) {
|
fileprivate func continuePlayingWithoutSound(seek: MediaPlayerSeek) {
|
||||||
if self.enableSound {
|
if self.enableSound {
|
||||||
self.lastStatusUpdateTimestamp = nil
|
self.lastStatusUpdateTimestamp = nil
|
||||||
|
|
||||||
@ -493,7 +491,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
timestamp = 0.0
|
timestamp = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
self.seek(timestamp: timestamp, action: .play)
|
self.seek(timestamp: timestamp, action: .play, notify: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -632,6 +630,8 @@ private final class ChunkMediaPlayerContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.initialSeekTimestamp = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loadedState.partStates.removeAll(where: { partState in
|
self.loadedState.partStates.removeAll(where: { partState in
|
||||||
@ -733,11 +733,7 @@ private final class ChunkMediaPlayerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
|
||||||
var performActionAtEndNow = false
|
var performActionAtEndNow = false
|
||||||
if !"".isEmpty {
|
|
||||||
performActionAtEndNow = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var worstStatus: MediaTrackFrameBufferStatus?
|
var worstStatus: MediaTrackFrameBufferStatus?
|
||||||
for status in [videoStatus, audioStatus] {
|
for status in [videoStatus, audioStatus] {
|
||||||
@ -800,6 +796,11 @@ private final class ChunkMediaPlayerContext {
|
|||||||
} else {
|
} else {
|
||||||
rate = 0.0
|
rate = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//print("finished timestamp: \(timestamp), finishedAt: \(finishedAt), duration: \(duration)")
|
||||||
|
if duration > 0.0 && timestamp >= finishedAt && finishedAt >= duration - 0.2 {
|
||||||
|
performActionAtEndNow = true
|
||||||
|
}
|
||||||
} else if case .buffering = worstStatus {
|
} else if case .buffering = worstStatus {
|
||||||
bufferingProgress = 0.0
|
bufferingProgress = 0.0
|
||||||
rate = 0.0
|
rate = 0.0
|
||||||
@ -808,6 +809,10 @@ private final class ChunkMediaPlayerContext {
|
|||||||
bufferingProgress = 0.0
|
bufferingProgress = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if duration > 0.0 && timestamp >= duration {
|
||||||
|
performActionAtEndNow = true
|
||||||
|
}
|
||||||
|
|
||||||
var reportRate = rate
|
var reportRate = rate
|
||||||
|
|
||||||
if let controlTimebase = self.loadedState.controlTimebase {
|
if let controlTimebase = self.loadedState.controlTimebase {
|
||||||
@ -829,12 +834,10 @@ private final class ChunkMediaPlayerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
|
||||||
if let controlTimebase = self.loadedState.controlTimebase, let videoTrackFrameBuffer = self.loadedState.partStates.first?.mediaBuffers?.videoBuffer, videoTrackFrameBuffer.hasFrames {
|
if let controlTimebase = self.loadedState.controlTimebase, let videoTrackFrameBuffer = self.loadedState.partStates.first?.mediaBuffers?.videoBuffer, videoTrackFrameBuffer.hasFrames {
|
||||||
self.videoRenderer.state = (controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect)
|
self.videoRenderer.state = (controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
|
||||||
if let audioRenderer = self.audioRenderer {
|
if let audioRenderer = self.audioRenderer {
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
audioRenderer.requestedFrames = true
|
audioRenderer.requestedFrames = true
|
||||||
@ -904,12 +907,17 @@ private final class ChunkMediaPlayerContext {
|
|||||||
let _ = self.playerStatusValue.swap(status)
|
let _ = self.playerStatusValue.swap(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.notifySeeked {
|
||||||
|
self.notifySeeked = false
|
||||||
|
self.onSeeked()
|
||||||
|
}
|
||||||
|
|
||||||
if performActionAtEndNow {
|
if performActionAtEndNow {
|
||||||
/*if !self.stoppedAtEnd {
|
if !self.stoppedAtEnd {
|
||||||
switch self.actionAtEnd {
|
switch self.actionAtEnd {
|
||||||
case let .loop(f):
|
case let .loop(f):
|
||||||
self.stoppedAtEnd = false
|
self.stoppedAtEnd = false
|
||||||
self.seek(timestamp: 0.0, action: .play)
|
self.seek(timestamp: 0.0, action: .play, notify: true)
|
||||||
f?()
|
f?()
|
||||||
case .stop:
|
case .stop:
|
||||||
self.stoppedAtEnd = true
|
self.stoppedAtEnd = true
|
||||||
@ -921,10 +929,10 @@ private final class ChunkMediaPlayerContext {
|
|||||||
case let .loopDisablingSound(f):
|
case let .loopDisablingSound(f):
|
||||||
self.stoppedAtEnd = false
|
self.stoppedAtEnd = false
|
||||||
self.enableSound = false
|
self.enableSound = false
|
||||||
self.seek(timestamp: 0.0, action: .play)
|
self.seek(timestamp: 0.0, action: .play, notify: true)
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -969,7 +977,8 @@ public final class ChunkMediaPlayer {
|
|||||||
mixWithOthers: Bool = false,
|
mixWithOthers: Bool = false,
|
||||||
keepAudioSessionWhilePaused: Bool = false,
|
keepAudioSessionWhilePaused: Bool = false,
|
||||||
continuePlayingWithoutSoundOnLostAudioSession: Bool = false,
|
continuePlayingWithoutSoundOnLostAudioSession: Bool = false,
|
||||||
isAudioVideoMessage: Bool = false
|
isAudioVideoMessage: Bool = false,
|
||||||
|
onSeeked: (() -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
let audioLevelPipe = self.audioLevelPipe
|
let audioLevelPipe = self.audioLevelPipe
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
@ -990,7 +999,10 @@ public final class ChunkMediaPlayer {
|
|||||||
mixWithOthers: mixWithOthers,
|
mixWithOthers: mixWithOthers,
|
||||||
keepAudioSessionWhilePaused: keepAudioSessionWhilePaused,
|
keepAudioSessionWhilePaused: keepAudioSessionWhilePaused,
|
||||||
continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession,
|
continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession,
|
||||||
isAudioVideoMessage: isAudioVideoMessage
|
isAudioVideoMessage: isAudioVideoMessage,
|
||||||
|
onSeeked: {
|
||||||
|
onSeeked?()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
self.contextRef = Unmanaged.passRetained(context)
|
self.contextRef = Unmanaged.passRetained(context)
|
||||||
}
|
}
|
||||||
@ -1011,7 +1023,7 @@ public final class ChunkMediaPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func playOnceWithSound(playAndRecord: Bool, seek: ChunkMediaPlayerSeek = .start) {
|
public func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
||||||
@ -1035,7 +1047,7 @@ public final class ChunkMediaPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func continuePlayingWithoutSound(seek: ChunkMediaPlayerSeek = .start) {
|
public func continuePlayingWithoutSound(seek: MediaPlayerSeek = .start) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.continuePlayingWithoutSound(seek: seek)
|
context.continuePlayingWithoutSound(seek: seek)
|
||||||
@ -1086,10 +1098,10 @@ public final class ChunkMediaPlayer {
|
|||||||
public func seek(timestamp: Double, play: Bool? = nil) {
|
public func seek(timestamp: Double, play: Bool? = nil) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
if let play = play {
|
if let play {
|
||||||
context.seek(timestamp: timestamp, action: play ? .play : .pause)
|
context.seek(timestamp: timestamp, action: play ? .play : .pause, notify: false)
|
||||||
} else {
|
} else {
|
||||||
context.seek(timestamp: timestamp)
|
context.seek(timestamp: timestamp, notify: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
|||||||
chatPeerId: nil,
|
chatPeerId: nil,
|
||||||
selectedItems: Set(),
|
selectedItems: Set(),
|
||||||
backgroundIconColor: nil,
|
backgroundIconColor: nil,
|
||||||
hasSearch: false,
|
hasSearch: true,
|
||||||
forceHasPremium: true
|
forceHasPremium: true
|
||||||
)
|
)
|
||||||
self.emojiContentDisposable = (emojiContent
|
self.emojiContentDisposable = (emojiContent
|
||||||
|
File diff suppressed because one or more lines are too long
@ -83,6 +83,7 @@ export class SourceBufferStub extends EventTarget {
|
|||||||
this.buffered._ranges = ranges;
|
this.buffered._ranges = ranges;
|
||||||
|
|
||||||
this.mediaSource._reopen();
|
this.mediaSource._reopen();
|
||||||
|
this.mediaSource.emitUpdatedBuffer();
|
||||||
|
|
||||||
this.updating = false;
|
this.updating = false;
|
||||||
this.dispatchEvent(new Event('update'));
|
this.dispatchEvent(new Event('update'));
|
||||||
@ -122,6 +123,7 @@ export class SourceBufferStub extends EventTarget {
|
|||||||
this.buffered._ranges = ranges;
|
this.buffered._ranges = ranges;
|
||||||
|
|
||||||
this.mediaSource._reopen();
|
this.mediaSource._reopen();
|
||||||
|
this.mediaSource.emitUpdatedBuffer();
|
||||||
|
|
||||||
this.updating = false;
|
this.updating = false;
|
||||||
this.dispatchEvent(new Event('update'));
|
this.dispatchEvent(new Event('update'));
|
||||||
@ -162,6 +164,17 @@ export class MediaSourceStub extends EventTarget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitUpdatedBuffer() {
|
||||||
|
this.dispatchEvent(new Event("bufferChanged"));
|
||||||
|
}
|
||||||
|
|
||||||
|
getBufferedRanges() {
|
||||||
|
if (this.sourceBuffers._buffers.length != 0) {
|
||||||
|
return this.sourceBuffers._buffers[0].buffered._ranges;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
addSourceBuffer(mimeType) {
|
addSourceBuffer(mimeType) {
|
||||||
if (this.readyState !== 'open') {
|
if (this.readyState !== 'open') {
|
||||||
throw new DOMException('MediaSource is not open', 'InvalidStateError');
|
throw new DOMException('MediaSource is not open', 'InvalidStateError');
|
||||||
@ -170,6 +183,8 @@ export class MediaSourceStub extends EventTarget {
|
|||||||
this.sourceBuffers._add(sourceBuffer);
|
this.sourceBuffers._add(sourceBuffer);
|
||||||
this.activeSourceBuffers._add(sourceBuffer);
|
this.activeSourceBuffers._add(sourceBuffer);
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("bufferChanged"));
|
||||||
|
|
||||||
window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", {
|
window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", {
|
||||||
"ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId)
|
"ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId)
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
@ -184,6 +199,8 @@ export class MediaSourceStub extends EventTarget {
|
|||||||
}
|
}
|
||||||
this.activeSourceBuffers._remove(sourceBuffer);
|
this.activeSourceBuffers._remove(sourceBuffer);
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("bufferChanged"));
|
||||||
|
|
||||||
window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", {
|
window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", {
|
||||||
"ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId)
|
"ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId)
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
|
@ -14,7 +14,7 @@ export class VideoElementStub extends EventTarget {
|
|||||||
this._currentTime = 0.0;
|
this._currentTime = 0.0;
|
||||||
this.duration = NaN;
|
this.duration = NaN;
|
||||||
this.paused = true;
|
this.paused = true;
|
||||||
this.playbackRate = 1.0;
|
this._playbackRate = 1.0;
|
||||||
this.volume = 1.0;
|
this.volume = 1.0;
|
||||||
this.muted = false;
|
this.muted = false;
|
||||||
this.readyState = 0;
|
this.readyState = 0;
|
||||||
@ -30,6 +30,7 @@ export class VideoElementStub extends EventTarget {
|
|||||||
this.videoHeight = 0;
|
this.videoHeight = 0;
|
||||||
this.textTracks = new TextTrackListStub();
|
this.textTracks = new TextTrackListStub();
|
||||||
this.isWaiting = false;
|
this.isWaiting = false;
|
||||||
|
this.currentMedia = null;
|
||||||
|
|
||||||
window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "constructor", {
|
window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "constructor", {
|
||||||
"instanceId": this.instanceId
|
"instanceId": this.instanceId
|
||||||
@ -63,14 +64,36 @@ export class VideoElementStub extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get playbackRate() {
|
||||||
|
return this._playbackRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
set playbackRate(value) {
|
||||||
|
this._playbackRate = value;
|
||||||
|
|
||||||
|
window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setPlaybackRate", {
|
||||||
|
"instanceId": this.instanceId,
|
||||||
|
"playbackRate": value
|
||||||
|
}).then((result) => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
get src() {
|
get src() {
|
||||||
return this._src;
|
return this._src;
|
||||||
}
|
}
|
||||||
|
|
||||||
set src(value) {
|
set src(value) {
|
||||||
|
if (this.currentMedia) {
|
||||||
|
this.currentMedia.removeEventListener("bufferChanged", false);
|
||||||
|
}
|
||||||
|
|
||||||
this._src = value;
|
this._src = value;
|
||||||
var media = window.mediaSourceMap[this._src];
|
var media = window.mediaSourceMap[this._src];
|
||||||
|
this.currentMedia = media;
|
||||||
if (media) {
|
if (media) {
|
||||||
|
media.addEventListener("bufferChanged", () => {
|
||||||
|
this.updateBufferedFromMediaSource();
|
||||||
|
}, false);
|
||||||
window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setMediaSource", {
|
window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setMediaSource", {
|
||||||
"instanceId": this.instanceId,
|
"instanceId": this.instanceId,
|
||||||
"mediaSourceId": media.bridgeId
|
"mediaSourceId": media.bridgeId
|
||||||
@ -90,16 +113,13 @@ export class VideoElementStub extends EventTarget {
|
|||||||
return fragment.querySelectorAll('*');
|
return fragment.querySelectorAll('*');
|
||||||
}
|
}
|
||||||
|
|
||||||
bridgeUpdateBuffered(value) {
|
updateBufferedFromMediaSource() {
|
||||||
const updatedRanges = value;
|
var currentMedia = this.currentMedia;
|
||||||
var ranges = [];
|
if (currentMedia) {
|
||||||
for (var i = 0; i < updatedRanges.length; i += 2) {
|
this.buffered._ranges = currentMedia.getBufferedRanges();
|
||||||
ranges.push({
|
} else {
|
||||||
start: updatedRanges[i],
|
this.buffered._ranges = [];
|
||||||
end: updatedRanges[i + 1]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.buffered._ranges = ranges;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bridgeUpdateStatus(dict) {
|
bridgeUpdateStatus(dict) {
|
||||||
@ -160,10 +180,6 @@ export class VideoElementStub extends EventTarget {
|
|||||||
return 'probably';
|
return 'probably';
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMedia() {
|
|
||||||
return window.mediaSourceMap[this.src];
|
|
||||||
}
|
|
||||||
|
|
||||||
addTextTrack(kind, label, language) {
|
addTextTrack(kind, label, language) {
|
||||||
const textTrack = new TextTrackStub(kind, label, language);
|
const textTrack = new TextTrackStub(kind, label, language);
|
||||||
this.textTracks._add(textTrack);
|
this.textTracks._add(textTrack);
|
||||||
@ -172,4 +188,8 @@ export class VideoElementStub extends EventTarget {
|
|||||||
|
|
||||||
load() {
|
load() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifySeeked() {
|
||||||
|
this.dispatchEvent(new Event('seeked'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,150 @@
|
|||||||
|
|
||||||
|
function base64ToArrayBuffer(base64) {
|
||||||
|
var binaryString = atob(base64);
|
||||||
|
var bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class XMLHttpRequestStub extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.bridgeId = window.nextInternalId;
|
||||||
|
window.nextInternalId += 1;
|
||||||
|
|
||||||
|
this.readyState = 0;
|
||||||
|
this.status = 0;
|
||||||
|
this.statusText = "";
|
||||||
|
this.responseText = "";
|
||||||
|
this.responseXML = null;
|
||||||
|
this._responseData = null;
|
||||||
|
this.onreadystatechange = null;
|
||||||
|
this._requestHeaders = {};
|
||||||
|
this._responseHeaders = {};
|
||||||
|
this._method = "";
|
||||||
|
this._url = "";
|
||||||
|
this._async = true;
|
||||||
|
this._user = null;
|
||||||
|
this._password = null;
|
||||||
|
this._responseType = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
open(method, url, async = true, user = null, password = null) {
|
||||||
|
this._method = method;
|
||||||
|
this._url = url;
|
||||||
|
this._async = async;
|
||||||
|
this._user = user;
|
||||||
|
this._password = password;
|
||||||
|
this.readyState = 1; // Opened
|
||||||
|
this._triggerReadyStateChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestHeader(header, value) {
|
||||||
|
this._requestHeaders[header] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseHeader(header) {
|
||||||
|
return this._responseHeaders[header.toLowerCase()] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllResponseHeaders() {
|
||||||
|
return Object.entries(this._responseHeaders)
|
||||||
|
.map(([header, value]) => `${header}: ${value}`)
|
||||||
|
.join('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
send(body = null) {
|
||||||
|
this.readyState = 2;
|
||||||
|
this._triggerReadyStateChange();
|
||||||
|
|
||||||
|
this.readyState = 3; // Loading
|
||||||
|
this._triggerReadyStateChange();
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("loadstart"));
|
||||||
|
|
||||||
|
window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "load", {
|
||||||
|
"id": this.bridgeId,
|
||||||
|
"url": this._url,
|
||||||
|
"requestHeaders": this._requestHeaders
|
||||||
|
}).then((result) => {
|
||||||
|
if (result["error"]) {
|
||||||
|
this.dispatchEvent(new Event("error"));
|
||||||
|
} else {
|
||||||
|
this.status = result["status"];
|
||||||
|
this.statusText = result["statusText"];
|
||||||
|
|
||||||
|
if (result["responseData"]) {
|
||||||
|
if (this._responseType === "arraybuffer") {
|
||||||
|
this._responseData = base64ToArrayBuffer(result["responseData"]);
|
||||||
|
} else {
|
||||||
|
this.responseText = atob(result["responseData"]);
|
||||||
|
}
|
||||||
|
this.responseXML = null;
|
||||||
|
} else {
|
||||||
|
this.response = null;
|
||||||
|
this.responseText = result["responseText"] || null;
|
||||||
|
this.responseXML = result["responseXML"] || null;
|
||||||
|
}
|
||||||
|
this._responseHeaders = result["responseHeaders"];
|
||||||
|
|
||||||
|
this.readyState = 4; // Done
|
||||||
|
this._triggerReadyStateChange();
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("load"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("loadend"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this.dispatchEvent(new Event("abort"));
|
||||||
|
|
||||||
|
window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "abort", {
|
||||||
|
"id": this.bridgeId
|
||||||
|
});
|
||||||
|
this.readyState = 0;
|
||||||
|
this.status = 0;
|
||||||
|
this.statusText = '';
|
||||||
|
this.responseText = '';
|
||||||
|
this.responseXML = null;
|
||||||
|
this._responseHeaders = {};
|
||||||
|
this._triggerReadyStateChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideMimeType(mime) {
|
||||||
|
}
|
||||||
|
|
||||||
|
set responseType(type) {
|
||||||
|
this._responseType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get responseType() {
|
||||||
|
return this._responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
get response() {
|
||||||
|
if (this._responseType === '' || this._responseType === 'text') {
|
||||||
|
return this.responseText;
|
||||||
|
}
|
||||||
|
return this._responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerReadyStateChange() {
|
||||||
|
this.dispatchEvent(new Event('readystatechange'));
|
||||||
|
if (typeof this.onreadystatechange === 'function') {
|
||||||
|
this.onreadystatechange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional methods to simulate responses
|
||||||
|
_setResponse(status, statusText, responseText, responseHeaders = {}) {
|
||||||
|
this.status = status;
|
||||||
|
this.statusText = statusText;
|
||||||
|
this.responseText = responseText;
|
||||||
|
this._responseHeaders = responseHeaders;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
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"
|
||||||
|
|
||||||
window.bridgeObjectMap = {};
|
window.bridgeObjectMap = {};
|
||||||
window.bridgeCallbackMap = {};
|
window.bridgeCallbackMap = {};
|
||||||
@ -48,6 +49,8 @@ if (typeof window !== 'undefined') {
|
|||||||
window.MediaSource = MediaSourceStub;
|
window.MediaSource = MediaSourceStub;
|
||||||
window.ManagedMediaSource = MediaSourceStub;
|
window.ManagedMediaSource = MediaSourceStub;
|
||||||
window.SourceBuffer = SourceBufferStub;
|
window.SourceBuffer = SourceBufferStub;
|
||||||
|
window.XMLHttpRequest = XMLHttpRequestStub;
|
||||||
|
|
||||||
URL.createObjectURL = function(ms) {
|
URL.createObjectURL = function(ms) {
|
||||||
const url = "blob:mock-media-source:" + ms.internalId;
|
const url = "blob:mock-media-source:" + ms.internalId;
|
||||||
window.mediaSourceMap[url] = ms;
|
window.mediaSourceMap[url] = ms;
|
||||||
@ -66,6 +69,7 @@ export class HlsPlayerInstance {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
this.isManifestParsed = false;
|
this.isManifestParsed = false;
|
||||||
this.currentTimeUpdateTimeout = null;
|
this.currentTimeUpdateTimeout = null;
|
||||||
|
this.notifySeekedOnNextStatusUpdate = false;
|
||||||
this.video = new VideoElementStub(this.id);
|
this.video = new VideoElementStub(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +190,15 @@ export class HlsPlayerInstance {
|
|||||||
this.currentTimeUpdateTimeout = null;
|
this.currentTimeUpdateTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.notifySeekedOnNextStatusUpdate) {
|
||||||
|
this.notifySeekedOnNextStatusUpdate = false;
|
||||||
|
this.video.notifySeeked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerNotifySeekedOnNextStatusUpdate() {
|
||||||
|
this.notifySeekedOnNextStatusUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPlayerCurrentTime() {
|
refreshPlayerCurrentTime() {
|
||||||
|
@ -17,6 +17,21 @@ import ManagedFile
|
|||||||
import FFMpegBinding
|
import FFMpegBinding
|
||||||
import RangeSet
|
import RangeSet
|
||||||
|
|
||||||
|
private func parseRange(from rangeString: String) -> Range<Int>? {
|
||||||
|
guard rangeString.hasPrefix("bytes=") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let rangeValues = rangeString.dropFirst("bytes=".count).split(separator: "-")
|
||||||
|
|
||||||
|
guard rangeValues.count == 2,
|
||||||
|
let start = Int(rangeValues[0]),
|
||||||
|
let end = Int(rangeValues[1]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return start ..< end
|
||||||
|
}
|
||||||
|
|
||||||
final class HLSJSServerSource: SharedHLSServer.Source {
|
final class HLSJSServerSource: SharedHLSServer.Source {
|
||||||
let id: String
|
let id: String
|
||||||
let postbox: Postbox
|
let postbox: Postbox
|
||||||
@ -253,59 +268,6 @@ final class HLSJSServerSource: SharedHLSServer.Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class HLSJSHTMLServerSource: SharedHLSServer.Source {
|
|
||||||
let id: String
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.id = UUID().uuidString
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
}
|
|
||||||
|
|
||||||
func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> {
|
|
||||||
return Signal { subscriber in
|
|
||||||
let bundle = Bundle(for: HLSJSServerSource.self)
|
|
||||||
|
|
||||||
let bundlePath = bundle.bundlePath + "/HlsBundle.bundle"
|
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: bundlePath + "/" + path)) {
|
|
||||||
let mimeType: String
|
|
||||||
let pathExtension = (path as NSString).pathExtension
|
|
||||||
if pathExtension == "html" {
|
|
||||||
mimeType = "text/html"
|
|
||||||
} else if pathExtension == "html" {
|
|
||||||
mimeType = "application/javascript"
|
|
||||||
} else {
|
|
||||||
mimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
subscriber.putNext((data, mimeType))
|
|
||||||
} else {
|
|
||||||
subscriber.putNext(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriber.putCompletion()
|
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func masterPlaylistData() -> Signal<String, NoError> {
|
|
||||||
return .never()
|
|
||||||
}
|
|
||||||
|
|
||||||
func playlistData(quality: Int) -> Signal<String, NoError> {
|
|
||||||
return .never()
|
|
||||||
}
|
|
||||||
|
|
||||||
func partData(index: Int, quality: Int) -> Signal<Data?, NoError> {
|
|
||||||
return .never()
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
|
|
||||||
return .never()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
private let f: (WKScriptMessage) -> ()
|
private let f: (WKScriptMessage) -> ()
|
||||||
|
|
||||||
@ -320,7 +282,7 @@ private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SharedHLSVideoWebView {
|
private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate {
|
||||||
private final class ContextReference {
|
private final class ContextReference {
|
||||||
weak var contentNode: HLSVideoJSNativeContentNode?
|
weak var contentNode: HLSVideoJSNativeContentNode?
|
||||||
|
|
||||||
@ -329,11 +291,27 @@ private final class SharedHLSVideoWebView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum ResponseError {
|
||||||
|
case badRequest
|
||||||
|
case notFound
|
||||||
|
case internalServerError
|
||||||
|
|
||||||
|
var httpStatus: (Int, String) {
|
||||||
|
switch self {
|
||||||
|
case .badRequest:
|
||||||
|
return (400, "Bad Request")
|
||||||
|
case .notFound:
|
||||||
|
return (404, "Not Found")
|
||||||
|
case .internalServerError:
|
||||||
|
return (500, "Internal Server Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static let shared: SharedHLSVideoWebView = SharedHLSVideoWebView()
|
static let shared: SharedHLSVideoWebView = SharedHLSVideoWebView()
|
||||||
|
|
||||||
private var contextReferences: [Int: ContextReference] = [:]
|
private var contextReferences: [Int: ContextReference] = [:]
|
||||||
|
|
||||||
let htmlSource: HLSJSHTMLServerSource
|
|
||||||
let webView: WKWebView
|
let webView: WKWebView
|
||||||
|
|
||||||
var videoElements: [Int: VideoElement] = [:]
|
var videoElements: [Int: VideoElement] = [:]
|
||||||
@ -343,11 +321,9 @@ private final class SharedHLSVideoWebView {
|
|||||||
private var isWebViewReady: Bool = false
|
private var isWebViewReady: Bool = false
|
||||||
private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = []
|
private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = []
|
||||||
|
|
||||||
private var serverDisposable: Disposable?
|
private var tempTasks: [Int: URLSessionTask] = [:]
|
||||||
|
|
||||||
init() {
|
|
||||||
self.htmlSource = HLSJSHTMLServerSource()
|
|
||||||
|
|
||||||
|
override init() {
|
||||||
let config = WKWebViewConfiguration()
|
let config = WKWebViewConfiguration()
|
||||||
config.allowsInlineMediaPlayback = true
|
config.allowsInlineMediaPlayback = true
|
||||||
config.mediaTypesRequiringUserActionForPlayback = []
|
config.mediaTypesRequiringUserActionForPlayback = []
|
||||||
@ -381,6 +357,10 @@ private final class SharedHLSVideoWebView {
|
|||||||
self.webView.isInspectable = isDebug
|
self.webView.isInspectable = isDebug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.webView.navigationDelegate = self
|
||||||
|
|
||||||
handleScriptMessage = { [weak self] message in
|
handleScriptMessage = { [weak self] message in
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -474,21 +454,12 @@ private final class SharedHLSVideoWebView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let htmlSourceId = self.htmlSource.id
|
let bundle = Bundle(for: SharedHLSVideoWebView.self)
|
||||||
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: self.htmlSource, completion: { [weak self] in
|
let bundlePath = bundle.bundlePath + "/HlsBundle.bundle"
|
||||||
Queue.mainQueue().async {
|
self.webView.loadFileURL(URL(fileURLWithPath: bundlePath + "/index.html"), allowingReadAccessTo: URL(fileURLWithPath: bundlePath))
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let htmlUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(htmlSourceId)/index.html"
|
|
||||||
self.webView.load(URLRequest(url: URL(string: htmlUrl)!))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.serverDisposable?.dispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bridgeInvoke(
|
private func bridgeInvoke(
|
||||||
@ -534,6 +505,21 @@ private final class SharedHLSVideoWebView {
|
|||||||
instance.onSetCurrentTime(timestamp: currentTime)
|
instance.onSetCurrentTime(timestamp: currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completion([:])
|
||||||
|
} else if (methodName == "setPlaybackRate") {
|
||||||
|
guard let instanceId = params["instanceId"] as? Int else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let playbackRate = params["playbackRate"] as? Double else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let instance = self.contextReferences[instanceId]?.contentNode {
|
||||||
|
instance.onSetPlaybackRate(playbackRate: playbackRate)
|
||||||
|
}
|
||||||
|
|
||||||
completion([:])
|
completion([:])
|
||||||
} else if (methodName == "play") {
|
} else if (methodName == "play") {
|
||||||
guard let instanceId = params["instanceId"] as? Int else {
|
guard let instanceId = params["instanceId"] as? Int else {
|
||||||
@ -657,6 +643,215 @@ private final class SharedHLSVideoWebView {
|
|||||||
sourceBuffer.abortOperation()
|
sourceBuffer.abortOperation()
|
||||||
completion([:])
|
completion([:])
|
||||||
}
|
}
|
||||||
|
} else if className == "XMLHttpRequest" {
|
||||||
|
if methodName == "load" {
|
||||||
|
guard let id = params["id"] as? Int else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let url = params["url"] as? String else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let requestHeaders = params["requestHeaders"] as? [String: String] else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let parsedUrl = URL(string: url) else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let host = parsedUrl.host, host == "server" else {
|
||||||
|
completion(["error": 1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestPath = parsedUrl.path
|
||||||
|
if requestPath.hasPrefix("/") {
|
||||||
|
requestPath = String(requestPath[requestPath.index(after: requestPath.startIndex) ..< requestPath.endIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let firstSlash = requestPath.range(of: "/") else {
|
||||||
|
completion(["error": 1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestRange: Range<Int>?
|
||||||
|
if let rangeString = requestHeaders["Range"] {
|
||||||
|
requestRange = parseRange(from: rangeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])
|
||||||
|
|
||||||
|
var handlerFound = false
|
||||||
|
for (_, contextReference) in self.contextReferences {
|
||||||
|
if let context = contextReference.contentNode, let source = context.playerSource, source.id == streamId {
|
||||||
|
handlerFound = true
|
||||||
|
|
||||||
|
let filePath = String(requestPath[firstSlash.upperBound...])
|
||||||
|
if filePath == "master.m3u8" {
|
||||||
|
let _ = (source.masterPlaylistData()
|
||||||
|
|> deliverOn(.mainQueue())
|
||||||
|
|> take(1)).start(next: { [weak self] result in
|
||||||
|
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") {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (source.playlistData(quality: levelIndex)
|
||||||
|
|> deliverOn(.mainQueue())
|
||||||
|
|> take(1)).start(next: { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion)
|
||||||
|
})
|
||||||
|
} 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)])
|
||||||
|
guard let fileIdValue = Int64(fileId) else {
|
||||||
|
self.sendErrorAndClose(id: id, error: .notFound, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let requestRange else {
|
||||||
|
self.sendErrorAndClose(id: id, error: .badRequest, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|
||||||
|
|> deliverOn(.mainQueue())
|
||||||
|
//|> timeout(5.0, queue: self.queue, alternate: .single(nil))
|
||||||
|
|> take(1)).start(next: { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (tempFile, tempFileRange, totalSize) = result {
|
||||||
|
self.sendResponseFileAndClose(id: id, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize, completion: completion)
|
||||||
|
} else {
|
||||||
|
self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handlerFound) {
|
||||||
|
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" {
|
||||||
|
guard let id = params["id"] as? Int else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let task = self.tempTasks.removeValue(forKey: id) {
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
completion([:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendErrorAndClose(id: Int, error: ResponseError, completion: @escaping ([String: Any]) -> Void) {
|
||||||
|
let (code, status) = error.httpStatus
|
||||||
|
completion([
|
||||||
|
"status": code,
|
||||||
|
"statusText": status,
|
||||||
|
"responseData": "",
|
||||||
|
"responseHeaders": [
|
||||||
|
"Content-Type": "text/html"
|
||||||
|
] as [String: String]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendResponseAndClose(id: Int, data: Data, contentType: String = "application/octet-stream", completion: @escaping ([String: Any]) -> Void) {
|
||||||
|
completion([
|
||||||
|
"status": 200,
|
||||||
|
"statusText": "OK",
|
||||||
|
"responseData": data.base64EncodedString(),
|
||||||
|
"responseHeaders": [
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Content-Length": "\(data.count)"
|
||||||
|
] as [String: String]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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) {
|
||||||
|
completion([
|
||||||
|
"status": 200,
|
||||||
|
"statusText": "OK",
|
||||||
|
"responseData": data.base64EncodedString(),
|
||||||
|
"responseHeaders": [
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Content-Range": "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)",
|
||||||
|
"Content-Length": "\(fileRange.upperBound - fileRange.lowerBound)"
|
||||||
|
] as [String: String]
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -754,7 +949,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
private let audioSessionDisposable = MetaDisposable()
|
private let audioSessionDisposable = MetaDisposable()
|
||||||
private var hasAudioSession = false
|
private var hasAudioSession = false
|
||||||
|
|
||||||
private let playerSource: HLSJSServerSource?
|
fileprivate let playerSource: HLSJSServerSource?
|
||||||
private var serverDisposable: Disposable?
|
private var serverDisposable: Disposable?
|
||||||
|
|
||||||
private let playbackCompletedListeners = Bag<() -> Void>()
|
private let playbackCompletedListeners = Bag<() -> Void>()
|
||||||
@ -816,7 +1011,6 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
private var hasRequestedPlayerLoad: Bool = false
|
private var hasRequestedPlayerLoad: Bool = false
|
||||||
|
|
||||||
private var requestedPlaying: Bool = false
|
|
||||||
private var requestedBaseRate: Double = 1.0
|
private var requestedBaseRate: Double = 1.0
|
||||||
private var requestedLevelIndex: Int?
|
private var requestedLevelIndex: Int?
|
||||||
|
|
||||||
@ -870,13 +1064,17 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale)
|
intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale)
|
||||||
self.intrinsicDimensions = intrinsicDimensions
|
self.intrinsicDimensions = intrinsicDimensions
|
||||||
|
|
||||||
|
var onSeeked: (() -> Void)?
|
||||||
self.player = ChunkMediaPlayer(
|
self.player = ChunkMediaPlayer(
|
||||||
postbox: postbox,
|
postbox: postbox,
|
||||||
audioSessionManager: audioSessionManager,
|
audioSessionManager: audioSessionManager,
|
||||||
partsState: self.chunkPlayerPartsState.get(),
|
partsState: self.chunkPlayerPartsState.get(),
|
||||||
video: true,
|
video: true,
|
||||||
enableSound: true,
|
enableSound: true,
|
||||||
baseRate: baseRate
|
baseRate: baseRate,
|
||||||
|
onSeeked: {
|
||||||
|
onSeeked?()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.playerNode = MediaPlayerNode()
|
self.playerNode = MediaPlayerNode()
|
||||||
@ -912,19 +1110,6 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
self._bufferingStatus.set(.single(nil))
|
self._bufferingStatus.set(.single(nil))
|
||||||
|
|
||||||
if let playerSource = self.playerSource {
|
|
||||||
let playerSourceId = playerSource.id
|
|
||||||
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedHLSVideoWebView.shared.initializeWhenReady(context: self, urlPrefix: "/\(playerSourceId)/")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||||
let _ = self
|
let _ = self
|
||||||
})
|
})
|
||||||
@ -946,6 +1131,19 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
self.updateStatus()
|
self.updateStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onSeeked = { [weak self] in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerNotifySeekedOnNextStatusUpdate();", completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let playerSource {
|
||||||
|
SharedHLSVideoWebView.shared.initializeWhenReady(context: self, urlPrefix: "http://server/\(playerSource.id)/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -1049,12 +1247,6 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
|
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
|
||||||
|
|
||||||
if self.requestedPlaying {
|
|
||||||
self.requestPlay()
|
|
||||||
} else {
|
|
||||||
self.requestPause()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateStatus()
|
self.updateStatus()
|
||||||
@ -1070,6 +1262,10 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
self.player.seek(timestamp: timestamp)
|
self.player.seek(timestamp: timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func onSetPlaybackRate(playbackRate: Double) {
|
||||||
|
self.player.setBaseRate(playbackRate)
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func onPlay() {
|
fileprivate func onPlay() {
|
||||||
self.player.play()
|
self.player.play()
|
||||||
}
|
}
|
||||||
@ -1161,13 +1357,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
|
|
||||||
let bufferedRanges = sourceBuffer.ranges
|
let bufferedRanges = sourceBuffer.ranges
|
||||||
|
|
||||||
if let (bridgeId, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
if let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) {
|
||||||
let result = serializeRanges(bufferedRanges)
|
|
||||||
|
|
||||||
let jsonResult = try! JSONSerialization.data(withJSONObject: result)
|
|
||||||
let jsonResultString = String(data: jsonResult, encoding: .utf8)!
|
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.bridgeObjectMap[\(bridgeId)].bridgeUpdateBuffered(\(jsonResultString));", completionHandler: nil)
|
|
||||||
|
|
||||||
if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] {
|
if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] {
|
||||||
if let duration = mediaSource.duration {
|
if let duration = mediaSource.duration {
|
||||||
var mappedRanges = RangeSet<Int64>()
|
var mappedRanges = RangeSet<Int64>()
|
||||||
@ -1208,82 +1398,26 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
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: true))
|
||||||
}
|
}
|
||||||
/*if !self.hasAudioSession {
|
self.player.play()
|
||||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.hasAudioSession = true
|
|
||||||
self.requestPlay()
|
|
||||||
}
|
|
||||||
}, deactivate: { [weak self] _ in
|
|
||||||
return Signal { subscriber in
|
|
||||||
if let self {
|
|
||||||
self.hasAudioSession = false
|
|
||||||
self.requestPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriber.putCompletion()
|
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
}
|
|
||||||
|> runOn(.mainQueue())
|
|
||||||
}))
|
|
||||||
} else*/ do {
|
|
||||||
self.requestPlay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func requestPlay() {
|
|
||||||
self.requestedPlaying = true
|
|
||||||
if self.playerIsReady {
|
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerPlay();", completionHandler: nil)
|
|
||||||
}
|
|
||||||
self.updateStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func requestPause() {
|
|
||||||
self.requestedPlaying = false
|
|
||||||
if self.playerIsReady {
|
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerPause();", completionHandler: nil)
|
|
||||||
}
|
|
||||||
self.updateStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
self.requestPause()
|
self.player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePlayPause() {
|
func togglePlayPause() {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
self.player.togglePlayPause()
|
||||||
if self.requestedPlaying {
|
|
||||||
self.pause()
|
|
||||||
} else {
|
|
||||||
self.play()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSoundEnabled(_ value: Bool) {
|
func setSoundEnabled(_ value: Bool) {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
/*if value {
|
if value {
|
||||||
if !self.hasAudioSession {
|
self.player.playOnceWithSound(playAndRecord: false, seek: .none)
|
||||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
|
|
||||||
self?.hasAudioSession = true
|
|
||||||
self?.player?.volume = 1.0
|
|
||||||
}, deactivate: { [weak self] _ in
|
|
||||||
self?.hasAudioSession = false
|
|
||||||
self?.player?.pause()
|
|
||||||
return .complete()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.player?.volume = 0.0
|
self.player.continuePlayingWithoutSound(seek: .none)
|
||||||
self.hasAudioSession = false
|
}
|
||||||
self.audioSessionDisposable.set(nil)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(_ timestamp: Double) {
|
func seek(_ timestamp: Double) {
|
||||||
@ -1294,28 +1428,75 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetIsMuted(false);", completionHandler: nil)
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
let action = { [weak self] in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
self?.performActionAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch actionAtEnd {
|
||||||
|
case .loop:
|
||||||
|
self.player.actionAtEnd = .loop({})
|
||||||
|
case .loopDisablingSound:
|
||||||
|
self.player.actionAtEnd = .loopDisablingSound(action)
|
||||||
|
case .stop:
|
||||||
|
self.player.actionAtEnd = .action(action)
|
||||||
|
case .repeatIfNeeded:
|
||||||
|
let _ = (self.player.status
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> take(1)).start(next: { [weak self] status in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status.timestamp > status.duration * 0.1 {
|
||||||
|
strongSelf.player.actionAtEnd = .loop({ [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.player.actionAtEnd = .loopDisablingSound(action)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
strongSelf.player.actionAtEnd = .loopDisablingSound(action)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
self.play()
|
self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSoundMuted(soundMuted: Bool) {
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetIsMuted(\(soundMuted));", completionHandler: nil)
|
self.player.setSoundMuted(soundMuted: soundMuted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
|
self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
self.player.setForceAudioToSpeaker(forceAudioToSpeaker)
|
||||||
}
|
}
|
||||||
|
|
||||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||||
SharedHLSVideoWebView.shared.webView.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetIsMuted(true);", completionHandler: nil)
|
assert(Queue.mainQueue().isCurrent())
|
||||||
self.hasAudioSession = false
|
let action = { [weak self] in
|
||||||
self.audioSessionDisposable.set(nil)
|
Queue.mainQueue().async {
|
||||||
|
self?.performActionAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch actionAtEnd {
|
||||||
|
case .loop:
|
||||||
|
self.player.actionAtEnd = .loop({})
|
||||||
|
case .loopDisablingSound, .repeatIfNeeded:
|
||||||
|
self.player.actionAtEnd = .loopDisablingSound(action)
|
||||||
|
case .stop:
|
||||||
|
self.player.actionAtEnd = .action(action)
|
||||||
|
}
|
||||||
|
self.player.continuePlayingWithoutSound()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
||||||
|
self.player.setContinuePlayingWithoutSoundOnLostAudioSession(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setBaseRate(_ baseRate: Double) {
|
func setBaseRate(_ baseRate: Double) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user