mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Add YouTube scrubbing thumbnails
This commit is contained in:
parent
890db9606c
commit
9ae27ffde6
@ -1106,7 +1106,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
}
|
||||
let textSize = videoFrameTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
videoFrameTextNode.frame = CGRect(origin: CGPoint(), size: textSize)
|
||||
videoFramePreviewNode.addSubnode(videoFrameTextNode)
|
||||
// videoFramePreviewNode.addSubnode(videoFrameTextNode)
|
||||
|
||||
self.videoFramePreviewNode = (videoFramePreviewNode, videoFrameTextNode)
|
||||
self.addSubnode(videoFramePreviewNode)
|
||||
|
@ -136,7 +136,7 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
if let mappedStatus = mappedStatus {
|
||||
self.chapterDisposable.set((mappedStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self, status.duration > 0.0 {
|
||||
if let strongSelf = self, status.duration > 1.0 {
|
||||
var text: String = ""
|
||||
|
||||
for chapter in strongSelf.chapters {
|
||||
|
@ -251,7 +251,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var videoFramePreview: MediaPlayerFramePreview?
|
||||
private var videoFramePreview: FramePreview?
|
||||
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
|
||||
private let statusButtonNode: HighlightableButtonNode
|
||||
private let statusNode: RadialStatusNode
|
||||
@ -280,7 +280,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private var fetchStatus: MediaResourceStatus?
|
||||
private var fetchControls: FetchControls?
|
||||
|
||||
private var scrubbingFrame = Promise<MediaPlayerFramePreviewResult?>(nil)
|
||||
private var scrubbingFrame = Promise<FramePreviewResult?>(nil)
|
||||
private var scrubbingFrames = false
|
||||
private var scrubbingFrameDisposable: Disposable?
|
||||
|
||||
@ -459,6 +459,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
switch type {
|
||||
case .youtube:
|
||||
disablePictureInPicture = !(item.configuration?.youtubePictureInPictureEnabled ?? false)
|
||||
self.videoFramePreview = YoutubeEmbedFramePreview(context: item.context, content: content)
|
||||
case .iframe:
|
||||
disablePlayerControls = true
|
||||
default:
|
||||
@ -841,7 +842,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
} else if let _ = item.content as? WebEmbedVideoContent {
|
||||
if let time = item.timecode {
|
||||
seek = .timecode(time)
|
||||
// seek = .timecode(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ public final class MapSnapshotMediaResourceRepresentation: CachedMediaResourceRe
|
||||
}
|
||||
|
||||
public func isEqual(to: CachedMediaResourceRepresentation) -> Bool {
|
||||
if let to = to as? MapSnapshotMediaResourceRepresentation {
|
||||
if to is MapSnapshotMediaResourceRepresentation {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
@ -5,6 +5,18 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import FFMpegBinding
|
||||
|
||||
public enum FramePreviewResult {
|
||||
case image(UIImage)
|
||||
case waitingForData
|
||||
}
|
||||
|
||||
public protocol FramePreview {
|
||||
var generatedFrames: Signal<FramePreviewResult, NoError> { get }
|
||||
|
||||
func generateFrame(at timestamp: Double)
|
||||
func cancelPendingFrames()
|
||||
}
|
||||
|
||||
private final class FramePreviewContext {
|
||||
let source: UniversalSoftwareVideoSource
|
||||
|
||||
@ -29,18 +41,13 @@ private func initializedPreviewContext(queue: Queue, postbox: Postbox, fileRefer
|
||||
}
|
||||
}
|
||||
|
||||
public enum MediaPlayerFramePreviewResult {
|
||||
case image(UIImage)
|
||||
case waitingForData
|
||||
}
|
||||
|
||||
private final class MediaPlayerFramePreviewImpl {
|
||||
private let queue: Queue
|
||||
private let context: Promise<QueueLocalObject<FramePreviewContext>>
|
||||
private let currentFrameDisposable = MetaDisposable()
|
||||
private var currentFrameTimestamp: Double?
|
||||
private var nextFrameTimestamp: Double?
|
||||
fileprivate let framePipe = ValuePipe<MediaPlayerFramePreviewResult>()
|
||||
fileprivate let framePipe = ValuePipe<FramePreviewResult>()
|
||||
|
||||
init(queue: Queue, postbox: Postbox, fileReference: FileMediaReference) {
|
||||
self.queue = queue
|
||||
@ -108,11 +115,11 @@ private final class MediaPlayerFramePreviewImpl {
|
||||
}
|
||||
}
|
||||
|
||||
public final class MediaPlayerFramePreview {
|
||||
public final class MediaPlayerFramePreview: FramePreview {
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<MediaPlayerFramePreviewImpl>
|
||||
|
||||
public var generatedFrames: Signal<MediaPlayerFramePreviewResult, NoError> {
|
||||
public var generatedFrames: Signal<FramePreviewResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
|
@ -29,6 +29,7 @@
|
||||
var availableQualities = "";
|
||||
var failed = false;
|
||||
var autostarted = false;
|
||||
var storyboardSpec = ""
|
||||
|
||||
YT.ready(function() {
|
||||
player = new YT.Player("player", %@);
|
||||
@ -36,7 +37,8 @@
|
||||
|
||||
function getCurrentTime() {
|
||||
downloadProgress = player.getVideoLoadedFraction();
|
||||
position = player.getCurrentTime()
|
||||
position = player.getCurrentTime();
|
||||
storyboardSpec = player.getStoryboardFormat();
|
||||
|
||||
updateState();
|
||||
invoke("tick");
|
||||
@ -59,7 +61,7 @@
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
window.location.href = "embed://onState?failed=" + failed + "&playback=" + playbackState + "&position=" + position + "&duration=" + duration + "&download=" + downloadProgress + '&quality=' + quality + '&availableQualities=' + availableQualities;
|
||||
window.location.href = "embed://onState?failed=" + failed + "&playback=" + playbackState + "&position=" + position + "&duration=" + duration + "&download=" + downloadProgress + '&quality=' + quality + '&availableQualities=' + availableQualities + '&storyboard=' + storyboardSpec;
|
||||
}
|
||||
|
||||
function onReady(event) {
|
||||
|
@ -49,6 +49,12 @@ function tick() {
|
||||
paid.style.opacity = "0";
|
||||
}
|
||||
|
||||
var gradient = document.getElementsByClassName("ytp-gradient-top")[0];
|
||||
if (gradient != null) {
|
||||
gradient.style.display = "none";
|
||||
gradient.style.opacity = "0";
|
||||
}
|
||||
|
||||
var end = document.getElementsByClassName("html5-endscreen")[0];
|
||||
if (end != null) {
|
||||
end.style.display = "none";
|
||||
|
@ -18,6 +18,7 @@ import TelegramAnimatedStickerNode
|
||||
import WallpaperResources
|
||||
import Svg
|
||||
import GZip
|
||||
import TelegramUniversalVideoContent
|
||||
|
||||
public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
if let representation = representation as? CachedStickerAJpegRepresentation {
|
||||
@ -133,6 +134,8 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR
|
||||
}
|
||||
} else if let resource = resource as? MapSnapshotMediaResource, let _ = representation as? MapSnapshotMediaResourceRepresentation {
|
||||
return fetchMapSnapshotResource(resource: resource)
|
||||
} else if let resource = resource as? YoutubeEmbedStoryboardMediaResource, let _ = representation as? YoutubeEmbedStoryboardMediaResourceRepresentation {
|
||||
return fetchYoutubeEmbedStoryboardResource(resource: resource)
|
||||
}
|
||||
return .never()
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import UniversalMediaPlayer
|
||||
import AppBundle
|
||||
|
||||
final class GenericEmbedImplementation: WebEmbedImplementation {
|
||||
private var evalImpl: ((String) -> Void)?
|
||||
private var evalImpl: ((String, ((Any?) -> Void)?) -> Void)?
|
||||
private var updateStatus: ((MediaPlayerStatus) -> Void)?
|
||||
private var onPlaybackStarted: (() -> Void)?
|
||||
private var status : MediaPlayerStatus
|
||||
@ -17,7 +17,7 @@ final class GenericEmbedImplementation: WebEmbedImplementation {
|
||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true), soundEnabled: true)
|
||||
}
|
||||
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
let bundle = getAppBundle()
|
||||
guard let userScriptPath = bundle.path(forResource: "GenericUserScript", ofType: "js") else {
|
||||
return
|
||||
|
@ -9,7 +9,7 @@ func isTwitchVideoUrl(_ url: String) -> Bool {
|
||||
}
|
||||
|
||||
final class TwitchEmbedImplementation: WebEmbedImplementation {
|
||||
private var evalImpl: ((String) -> Void)?
|
||||
private var evalImpl: ((String, ((Any?) -> Void)?) -> Void)?
|
||||
private var updateStatus: ((MediaPlayerStatus) -> Void)?
|
||||
private var onPlaybackStarted: (() -> Void)?
|
||||
|
||||
@ -23,7 +23,7 @@ final class TwitchEmbedImplementation: WebEmbedImplementation {
|
||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true), soundEnabled: true)
|
||||
}
|
||||
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
let bundle = getAppBundle()
|
||||
guard let userScriptPath = bundle.path(forResource: "TwitchUserScript", ofType: "js") else {
|
||||
return
|
||||
@ -57,7 +57,7 @@ final class TwitchEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func play() {
|
||||
if let eval = self.evalImpl {
|
||||
eval("playPause()")
|
||||
eval("playPause()", nil)
|
||||
}
|
||||
|
||||
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: 1.0, seekId: self.status.seekId, status: .playing, soundEnabled: self.status.soundEnabled)
|
||||
@ -68,7 +68,7 @@ final class TwitchEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func pause() {
|
||||
if let eval = self.evalImpl {
|
||||
eval("playPause()")
|
||||
eval("playPause()", nil)
|
||||
}
|
||||
|
||||
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: 1.0, seekId: self.status.seekId, status: .paused, soundEnabled: self.status.soundEnabled)
|
||||
|
@ -82,7 +82,7 @@ func extractVimeoVideoIdAndTimestamp(url: String) -> (String, Int)? {
|
||||
}
|
||||
|
||||
final class VimeoEmbedImplementation: WebEmbedImplementation {
|
||||
private var evalImpl: ((String) -> Void)?
|
||||
private var evalImpl: ((String, ((Any?) -> Void)?) -> Void)?
|
||||
private var updateStatus: ((MediaPlayerStatus) -> Void)?
|
||||
private var onPlaybackStarted: (() -> Void)?
|
||||
|
||||
@ -100,7 +100,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true), soundEnabled: true)
|
||||
}
|
||||
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
let bundle = getAppBundle()
|
||||
guard let userScriptPath = bundle.path(forResource: "VimeoUserScript", ofType: "js") else {
|
||||
return
|
||||
@ -135,7 +135,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func play() {
|
||||
if let eval = self.evalImpl {
|
||||
eval("play();")
|
||||
eval("play();", nil)
|
||||
}
|
||||
|
||||
ignorePosition = 2
|
||||
@ -143,7 +143,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func pause() {
|
||||
if let eval = self.evalImpl {
|
||||
eval("pause();")
|
||||
eval("pause();", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func seek(timestamp: Double) {
|
||||
if let eval = self.evalImpl {
|
||||
eval("seek(\(timestamp));")
|
||||
eval("seek(\(timestamp));", nil)
|
||||
}
|
||||
|
||||
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: 1.0, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: self.status.soundEnabled)
|
||||
|
@ -8,7 +8,7 @@ import SyncCore
|
||||
import UniversalMediaPlayer
|
||||
|
||||
protocol WebEmbedImplementation {
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void)
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void)
|
||||
|
||||
func play()
|
||||
func pause()
|
||||
@ -73,7 +73,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
||||
return self.readyValue.get()
|
||||
}
|
||||
|
||||
private let impl: WebEmbedImplementation
|
||||
let impl: WebEmbedImplementation
|
||||
|
||||
private let intrinsicDimensions: CGSize
|
||||
private let webView: WKWebView
|
||||
@ -118,8 +118,8 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
||||
}
|
||||
self.view.addSubview(self.webView)
|
||||
|
||||
self.impl.setup(self.webView, userContentController: userContentController, evaluateJavaScript: { [weak self] js in
|
||||
self?.evaluateJavaScript(js: js)
|
||||
self.impl.setup(self.webView, userContentController: userContentController, evaluateJavaScript: { [weak self] js, completion in
|
||||
self?.evaluateJavaScript(js: js, completion: completion)
|
||||
}, updateStatus: { [weak self] status in
|
||||
self?.statusValue.set(status)
|
||||
}, onPlaybackStarted: { [weak self] in
|
||||
@ -168,12 +168,15 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func evaluateJavaScript(js: String) {
|
||||
private func evaluateJavaScript(js: String, completion: ((Any?) -> Void)?) {
|
||||
self.queue.async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let impl = {
|
||||
strongSelf.webView.evaluateJavaScript(js, completionHandler: { (_, _) in
|
||||
strongSelf.webView.evaluateJavaScript(js, completionHandler: { (result, _) in
|
||||
strongSelf.semaphore.signal()
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,8 @@ public final class WebEmbedVideoContent: UniversalVideoContent {
|
||||
return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||
|
||||
final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||
private let webpageContent: TelegramMediaWebpageLoadedContent
|
||||
private let intrinsicDimensions: CGSize
|
||||
|
||||
@ -64,6 +64,10 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte
|
||||
private let imageNode: TransformImageNode
|
||||
private let playerNode: WebEmbedPlayerNode
|
||||
|
||||
var impl: WebEmbedImplementation {
|
||||
return playerNode.impl
|
||||
}
|
||||
|
||||
private var readyDisposable = MetaDisposable()
|
||||
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil) {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import Postbox
|
||||
import SyncCore
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import WebKit
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
@ -84,11 +89,16 @@ func extractYoutubeVideoIdAndTimestamp(url: String) -> (String, Int)? {
|
||||
}
|
||||
|
||||
final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
private var evalImpl: ((String) -> Void)?
|
||||
private var evalImpl: ((String, ((Any?) -> Void)?) -> Void)?
|
||||
private var updateStatus: ((MediaPlayerStatus) -> Void)?
|
||||
private var onPlaybackStarted: (() -> Void)?
|
||||
|
||||
private let videoId: String
|
||||
fileprivate let videoId: String
|
||||
fileprivate var storyboardSpec: String?
|
||||
fileprivate var duration: Double {
|
||||
return self.status.duration
|
||||
}
|
||||
|
||||
private var timestamp: Int
|
||||
private var ignoreEarlierTimestamps = false
|
||||
private var status : MediaPlayerStatus
|
||||
@ -107,8 +117,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
init(videoId: String, timestamp: Int = 0) {
|
||||
self.videoId = videoId
|
||||
self.timestamp = timestamp
|
||||
if timestamp > 0 {
|
||||
self.timestamp = 0
|
||||
if self.timestamp > 0 {
|
||||
self.ignoreEarlierTimestamps = true
|
||||
}
|
||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true), soundEnabled: true)
|
||||
@ -116,7 +126,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
self.benchmarkStartTime = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||
let bundle = getAppBundle()
|
||||
guard let userScriptPath = bundle.path(forResource: "YoutubeUserScript", ofType: "js") else {
|
||||
return
|
||||
@ -177,7 +187,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
}
|
||||
|
||||
if let eval = self.evalImpl {
|
||||
eval("play();")
|
||||
eval("play();", nil)
|
||||
}
|
||||
|
||||
self.ignorePosition = 2
|
||||
@ -185,7 +195,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
|
||||
func pause() {
|
||||
if let eval = self.evalImpl {
|
||||
eval("pause();")
|
||||
eval("pause();", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,8 +213,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
self.ignoreEarlierTimestamps = true
|
||||
}
|
||||
|
||||
if let eval = evalImpl {
|
||||
eval("seek(\(timestamp));")
|
||||
if let eval = self.evalImpl {
|
||||
eval("seek(\(timestamp));", nil)
|
||||
}
|
||||
|
||||
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: 1.0, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: true)
|
||||
@ -241,6 +251,11 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
download = Float(value)
|
||||
} else if queryItem.name == "failed" {
|
||||
failed = Bool(value)
|
||||
} else if queryItem.name == "storyboard" {
|
||||
let urlString = url.absoluteString
|
||||
if value.count > 10, let range = urlString.range(of: "storyboard=") {
|
||||
self.storyboardSpec = String(urlString[range.upperBound..<urlString.endIndex]).removingPercentEncoding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -330,3 +345,296 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct YoutubeEmbedStoryboardMediaResourceId: MediaResourceId {
|
||||
public let videoId: String
|
||||
public let storyboardId: Int32
|
||||
|
||||
public var uniqueId: String {
|
||||
return "youtube-storyboard-\(self.videoId)-\(self.storyboardId)"
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.uniqueId.hashValue
|
||||
}
|
||||
|
||||
public func isEqual(to: MediaResourceId) -> Bool {
|
||||
if let to = to as? YoutubeEmbedStoryboardMediaResourceId {
|
||||
return self.videoId == to.videoId && self.storyboardId == to.storyboardId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class YoutubeEmbedStoryboardMediaResource: TelegramMediaResource {
|
||||
public let videoId: String
|
||||
public let storyboardId: Int32
|
||||
public let url: String
|
||||
|
||||
public init(videoId: String, storyboardId: Int32, url: String) {
|
||||
self.videoId = videoId
|
||||
self.storyboardId = storyboardId
|
||||
self.url = url
|
||||
}
|
||||
|
||||
public required init(decoder: PostboxDecoder) {
|
||||
self.videoId = decoder.decodeStringForKey("v", orElse: "")
|
||||
self.storyboardId = decoder.decodeInt32ForKey("i", orElse: 0)
|
||||
self.url = decoder.decodeStringForKey("u", orElse: "")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.videoId, forKey: "v")
|
||||
encoder.encodeInt32(self.storyboardId, forKey: "i")
|
||||
encoder.encodeString(self.url, forKey: "u")
|
||||
}
|
||||
|
||||
public var id: MediaResourceId {
|
||||
return YoutubeEmbedStoryboardMediaResourceId(videoId: self.videoId, storyboardId: self.storyboardId)
|
||||
}
|
||||
|
||||
public func isEqual(to: MediaResource) -> Bool {
|
||||
if let to = to as? YoutubeEmbedStoryboardMediaResource {
|
||||
return self.videoId == to.videoId && self.storyboardId == to.storyboardId && self.url == to.url
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class YoutubeEmbedStoryboardMediaResourceRepresentation: CachedMediaResourceRepresentation {
|
||||
public let keepDuration: CachedMediaRepresentationKeepDuration = .shortLived
|
||||
|
||||
public var uniqueId: String {
|
||||
return "cached"
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func isEqual(to: CachedMediaResourceRepresentation) -> Bool {
|
||||
if to is YoutubeEmbedStoryboardMediaResourceRepresentation {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchYoutubeEmbedStoryboardResource(resource: YoutubeEmbedStoryboardMediaResource) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
subscriber.putNext(.reset)
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
disposable.set(fetchHttpResource(url: resource.url).start(next: { next in
|
||||
if case let .dataPart(_, data, _, complete) = next, complete {
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "image.jpg")
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) {
|
||||
subscriber.putNext(.tempFile(tempFile))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func youtubeEmbedStoryboardData(account: Account, resource: YoutubeEmbedStoryboardMediaResource) -> Signal<Data?, NoError> {
|
||||
return Signal<Data?, NoError> { subscriber in
|
||||
let dataDisposable = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: YoutubeEmbedStoryboardMediaResourceRepresentation(), complete: true).start(next: { next in
|
||||
if next.size != 0 {
|
||||
subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))
|
||||
}
|
||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func youtubeEmbedStoryboardImage(account: Account, resource: YoutubeEmbedStoryboardMediaResource, frame: Int32, size: YoutubeEmbedFramePreview.StoryboardSpec.StoryboardSize) -> Signal<UIImage?, NoError> {
|
||||
let signal = youtubeEmbedStoryboardData(account: account, resource: resource)
|
||||
|
||||
return signal |> map { fullSizeData in
|
||||
let drawingSize = CGSize(width: CGFloat(size.width), height: CGFloat(size.height))
|
||||
let context = DrawingContext(size: drawingSize, clear: true)
|
||||
|
||||
var fullSizeImage: CGImage?
|
||||
if let fullSizeData = fullSizeData {
|
||||
let options = NSMutableDictionary()
|
||||
options[kCGImageSourceShouldCache as NSString] = false as NSNumber
|
||||
if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) {
|
||||
fullSizeImage = image
|
||||
}
|
||||
|
||||
if let fullSizeImage = fullSizeImage {
|
||||
let rect: CGRect
|
||||
let imageSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height))
|
||||
|
||||
let row = floor(CGFloat(frame) / CGFloat(size.cols))
|
||||
let col = CGFloat(frame % size.cols)
|
||||
|
||||
rect = CGRect(origin: CGPoint(x: -drawingSize.width * col, y: -drawingSize.height * row), size: imageSize)
|
||||
|
||||
context.withFlippedContext { c in
|
||||
c.setBlendMode(.copy)
|
||||
c.interpolationQuality = .medium
|
||||
c.draw(fullSizeImage, in: rect)
|
||||
}
|
||||
return context.generateImage()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public final class YoutubeEmbedFramePreview: FramePreview {
|
||||
fileprivate struct StoryboardSpec {
|
||||
struct StoryboardSize {
|
||||
let width: Int32
|
||||
let height: Int32
|
||||
let quality: Int32
|
||||
let cols: Int32
|
||||
let rows: Int32
|
||||
let duration: Int32
|
||||
let imageName: String
|
||||
let sigh: String
|
||||
}
|
||||
|
||||
let baseUrl: String
|
||||
let sizes: [StoryboardSize]
|
||||
|
||||
init?(specString: String) {
|
||||
let sections = specString.components(separatedBy: "|")
|
||||
if sections.count < 2 {
|
||||
return nil
|
||||
}
|
||||
guard let baseUrl = sections.first else {
|
||||
return nil
|
||||
}
|
||||
self.baseUrl = baseUrl
|
||||
|
||||
var sizes: [StoryboardSize] = []
|
||||
for i in 1 ..< sections.count - 1 {
|
||||
let section = sections[i]
|
||||
let data = section.components(separatedBy: "#")
|
||||
|
||||
if data.count >= 8, let width = Int32(data[0]), let height = Int32(data[1]), let quality = Int32(data[2]), let cols = Int32(data[3]), let rows = Int32(data[4]), let duration = Int32(data[5]) {
|
||||
let size = StoryboardSize(width: width, height: height, quality: quality, cols: cols, rows: rows, duration: duration, imageName: data[6], sigh: data[7])
|
||||
sizes.append(size)
|
||||
}
|
||||
}
|
||||
|
||||
self.sizes = sizes
|
||||
}
|
||||
|
||||
var bestSize: (Int, StoryboardSize)? {
|
||||
var best: (Int, StoryboardSize)?
|
||||
for i in 0 ..< self.sizes.count {
|
||||
let size = self.sizes[i]
|
||||
if let (_, currentBest) = best {
|
||||
if currentBest.width < size.width || (currentBest.width == size.width && currentBest.cols < size.cols) {
|
||||
best = (i, size)
|
||||
}
|
||||
} else {
|
||||
best = (i, size)
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
}
|
||||
|
||||
private func urlForStoryboard(spec: StoryboardSpec, sizeIndex: Int, num: Int32) -> String {
|
||||
let size = spec.sizes[sizeIndex]
|
||||
|
||||
var url = spec.baseUrl
|
||||
url = url.replacingOccurrences(of: "$L", with: "\(sizeIndex)")
|
||||
url = url.replacingOccurrences(of: "$N", with: size.imageName)
|
||||
url = url.replacingOccurrences(of: "$M", with: "\(num)")
|
||||
url += "&sigh=\(size.sigh)"
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private weak var content: WebEmbedVideoContent?
|
||||
|
||||
private let currentFrameDisposable = MetaDisposable()
|
||||
private var currentFrameTimestamp: Double?
|
||||
private var nextFrameTimestamp: Double?
|
||||
fileprivate let framePipe = ValuePipe<FramePreviewResult>()
|
||||
|
||||
public init(context: AccountContext, content: WebEmbedVideoContent) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.currentFrameDisposable.dispose()
|
||||
}
|
||||
|
||||
public var generatedFrames: Signal<FramePreviewResult, NoError> {
|
||||
return self.framePipe.signal()
|
||||
}
|
||||
|
||||
public func generateFrame(at timestamp: Double) {
|
||||
guard let content = self.content else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.currentFrameTimestamp != nil {
|
||||
self.nextFrameTimestamp = timestamp
|
||||
return
|
||||
}
|
||||
self.currentFrameTimestamp = timestamp
|
||||
|
||||
self.context.sharedContext.mediaManager.universalVideoManager.withUniversalVideoContent(id: content.id) { [weak self] node in
|
||||
guard let strongSelf = self, let node = node as? WebEmbedVideoContentNode, let youtubeImpl = node.impl as? YoutubeEmbedImplementation, youtubeImpl.duration > 0.0, let specString = youtubeImpl.storyboardSpec, let storyboardSpec = StoryboardSpec(specString: specString), let bestSize = storyboardSpec.bestSize else {
|
||||
return
|
||||
}
|
||||
|
||||
var duration: Double = Double(bestSize.1.duration) / 1000.0
|
||||
var totalStoryboards: Int32 = 1
|
||||
var totalFrames: Int32 = 1
|
||||
let framesOnStoryboard: Int32 = bestSize.1.cols * bestSize.1.rows
|
||||
|
||||
if duration > 0.0 {
|
||||
totalFrames = Int32(ceil(youtubeImpl.duration / duration))
|
||||
totalStoryboards = Int32(ceil(Double(totalFrames) / Double(framesOnStoryboard)))
|
||||
} else {
|
||||
duration = youtubeImpl.duration / Double(framesOnStoryboard)
|
||||
}
|
||||
|
||||
let globalFrame = Int32(floor(timestamp / youtubeImpl.duration * Double(totalFrames)))
|
||||
let frame: Int32 = globalFrame % framesOnStoryboard
|
||||
|
||||
let num: Int32 = Int32(floor(Double(globalFrame) / Double(framesOnStoryboard)))
|
||||
let url = urlForStoryboard(spec: storyboardSpec, sizeIndex: bestSize.0, num: num)
|
||||
|
||||
strongSelf.framePipe.putNext(.waitingForData)
|
||||
strongSelf.currentFrameDisposable.set(youtubeEmbedStoryboardImage(account: strongSelf.context.account, resource: YoutubeEmbedStoryboardMediaResource(videoId: youtubeImpl.videoId, storyboardId: num, url: url), frame: frame, size: bestSize.1).start(next: { [weak self] image in
|
||||
if let strongSelf = self {
|
||||
if let image = image {
|
||||
strongSelf.framePipe.putNext(.image(image))
|
||||
}
|
||||
strongSelf.currentFrameTimestamp = nil
|
||||
if let nextFrameTimestamp = strongSelf.nextFrameTimestamp {
|
||||
strongSelf.nextFrameTimestamp = nil
|
||||
strongSelf.generateFrame(at: nextFrameTimestamp)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
public func cancelPendingFrames() {
|
||||
self.nextFrameTimestamp = nil
|
||||
self.currentFrameTimestamp = nil
|
||||
self.currentFrameDisposable.set(nil)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user