Swiftgram/TelegramUI/YoutubeEmbedImplementation.swift
Ilya Laktyushin 4ec048feac no message
2018-09-02 11:55:04 +03:00

296 lines
12 KiB
Swift

import Foundation
import WebKit
import SwiftSignalKit
func extractYoutubeVideoIdAndTimestamp(url: String) -> (String, Int)? {
guard let url = URL(string: url), let host = url.host?.lowercased() else {
return nil
}
let match = ["youtube.com", "youtu.be"].contains(where: { (domain) -> Bool in
return host == domain || host.contains(".\(domain)")
})
guard match else {
return nil
}
var videoId: String?
var timestamp = 0
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "v" {
videoId = value
} else if queryItem.name == "t" || queryItem.name == "time_continue" {
if value.contains("s") {
var range = value.startIndex..<value.endIndex
if let hoursRange = value.range(of: "h", options: .caseInsensitive, range: range, locale: nil) {
let subvalue = String(value[range.lowerBound..<hoursRange.lowerBound])
if let hours = Int(subvalue) {
timestamp = timestamp + hours * 3600
}
range = hoursRange.upperBound..<value.endIndex
}
if let minutesRange = value.range(of: "m", options: .caseInsensitive, range: range, locale: nil) {
let subvalue = String(value[range.lowerBound..<minutesRange.lowerBound])
if let minutes = Int(subvalue) {
timestamp = timestamp + minutes * 60
}
range = minutesRange.upperBound..<value.endIndex
}
if let secondsRange = value.range(of: "s", options: .caseInsensitive, range: range, locale: nil) {
let subvalue = String(value[range.lowerBound..<secondsRange.lowerBound])
if let seconds = Int(subvalue) {
timestamp = timestamp + seconds
}
}
} else {
if let seconds = Int(value) {
timestamp = seconds
}
}
}
}
}
}
if videoId == nil {
let pathComponents = components.path.components(separatedBy: "/")
var nextComponentIsVideoId = host.contains("youtu.be")
for component in pathComponents {
if component.count > 0 && nextComponentIsVideoId {
videoId = component
break
} else if component == "embed" {
nextComponentIsVideoId = true
}
}
}
}
if let videoId = videoId {
return (videoId, timestamp)
}
return nil
}
final class YoutubeEmbedImplementation: WebEmbedImplementation {
private var evalImpl: ((String) -> Void)?
private var updateStatus: ((MediaPlayerStatus) -> Void)?
private var onPlaybackStarted: (() -> Void)?
private let videoId: String
private let timestamp: Int
private var status : MediaPlayerStatus
private var ready: Bool = false
private var started: Bool = false
private var ignorePosition: Int?
enum PlaybackDelay {
case None
case AfterPositionUpdates(count: Int)
}
private var playbackDelay = PlaybackDelay.None
init(videoId: String, timestamp: Int = 0) {
self.videoId = videoId
self.timestamp = timestamp
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))
}
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
let bundle = Bundle(for: type(of: self))
guard let userScriptPath = bundle.path(forResource: "YoutubeUserScript", ofType: "js") else {
return
}
guard let userScriptData = try? Data(contentsOf: URL(fileURLWithPath: userScriptPath)) else {
return
}
guard let userScript = String(data: userScriptData, encoding: .utf8) else {
return
}
guard let htmlTemplatePath = bundle.path(forResource: "Youtube", ofType: "html") else {
return
}
guard let htmlTemplateData = try? Data(contentsOf: URL(fileURLWithPath: htmlTemplatePath)) else {
return
}
guard let htmlTemplate = String(data: htmlTemplateData, encoding: .utf8) else {
return
}
let params: [String : Any] = [ "videoId": self.videoId,
"width": "100%",
"height": "100%",
"events": [ "onReady": "onReady",
"onStateChange": "onStateChange",
"onPlaybackQualityChange": "onPlaybackQualityChange",
"onError": "onPlayerError" ],
"playerVars": [ "cc_load_policy": 1,
"iv_load_policy": 3,
"controls": 0,
"playsinline": 1,
"autohide": 1,
"showinfo": 0,
"rel": 0,
"modestbranding": 1,
"start": timestamp ] ]
guard let paramsJsonData = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted), let paramsJson = String(data: paramsJsonData, encoding: .utf8) else {
return
}
self.evalImpl = evaluateJavaScript
self.updateStatus = updateStatus
self.onPlaybackStarted = onPlaybackStarted
updateStatus(self.status)
let html = String(format: htmlTemplate, paramsJson)
webView.loadHTMLString(html, baseURL: URL(string: "https://youtube.com/"))
webView.isUserInteractionEnabled = false
userContentController.addUserScript(WKUserScript(source: userScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
}
func play() {
guard ready else {
self.playbackDelay = .AfterPositionUpdates(count: 2)
return
}
if let eval = evalImpl {
eval("play();")
}
}
func pause() {
if let eval = evalImpl {
eval("pause();")
}
}
func togglePlayPause() {
if case .playing = self.status.status {
pause()
} else {
play()
}
}
func seek(timestamp: Double) {
if let eval = evalImpl {
eval("seek(\(timestamp));")
}
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)
if let updateStatus = self.updateStatus {
updateStatus(self.status)
}
}
func pageReady() {
}
func callback(url: URL) {
switch url.host {
case "onState":
var newTimestamp = self.status.timestamp
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
var playback: Int?
var position: Double?
var duration: Int?
var download: Float?
var failed: Bool?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "playback" {
playback = Int(value)
} else if queryItem.name == "position" {
position = Double(value)
} else if queryItem.name == "duration" {
duration = Int(value)
} else if queryItem.name == "download" {
download = Float(value)
} else if queryItem.name == "failed" {
failed = Bool(value)
}
}
}
}
if let position = position {
if let ticksToIgnore = self.ignorePosition {
if ticksToIgnore > 1 {
self.ignorePosition = ticksToIgnore - 1
} else {
self.ignorePosition = nil
}
} else {
newTimestamp = Double(position)
}
}
if let updateStatus = self.updateStatus, let playback = playback, let duration = duration {
let playbackStatus: MediaPlayerPlaybackStatus
switch playback {
case 0:
playbackStatus = .paused
newTimestamp = 0.0
case 1:
playbackStatus = .playing
case 2:
playbackStatus = .paused
case 3:
playbackStatus = .buffering(initial: false, whilePlaying: false)
default:
playbackStatus = .buffering(initial: true, whilePlaying: false)
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: 1.0, seekId: 0, status: playbackStatus)
updateStatus(self.status)
}
}
if case let .AfterPositionUpdates(count) = self.playbackDelay {
if count == 1 {
self.ready = true
self.playbackDelay = .None
self.play()
} else {
self.playbackDelay = .AfterPositionUpdates(count: count - 1)
}
}
case "onReady":
self.ready = true
if case .AfterPositionUpdates(_) = self.playbackDelay {
self.playbackDelay = .None
self.play()
}
Queue.mainQueue().async {
self.play()
Queue.mainQueue().after(2.0, {
if !self.started {
self.play()
}
})
}
default:
break
}
}
}